refactor!: setup file proxy for projects
This commit is contained in:
@@ -18,10 +18,10 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="h-14 w-full bg-[#292e42]">
|
||||
<div class="h-14 w-full bg-[#292e42] sm:p-2">
|
||||
<div class="mx-auto flex h-full max-w-[78rem] items-center justify-between">
|
||||
<Button.Root onclick={() => goto('/')}>
|
||||
<h1 class="header-title font-light">Godot Host</h1>
|
||||
<h1 class="header-title font-light">Project Host</h1>
|
||||
</Button.Root>
|
||||
{#if !user && !$session.isPending}
|
||||
<Button.Root class="rounded-md p-2 transition-colors hover:bg-gray-800" onclick={signIn}>
|
||||
|
||||
@@ -1,83 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { Play } from '@lucide/svelte';
|
||||
import { Play, LoaderCircle } from '@lucide/svelte';
|
||||
import { Button } from 'bits-ui';
|
||||
import type { Project } from './types/project';
|
||||
import type { Project, RepoDefinition } from './types/project';
|
||||
import { goto } from '$app/navigation';
|
||||
import Image from './Image.svelte';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
|
||||
const mockProjects: Project[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Uno',
|
||||
description: 'Recreation of the classic card game',
|
||||
thumbnail: 'https://picsum.photos/seed/uno/400/225',
|
||||
url: '/game/index.html'
|
||||
const reposQuery = createQuery<RepoDefinition[]>(() => ({
|
||||
queryKey: ['global-repos'],
|
||||
queryFn: async () => {
|
||||
const response = await fetch('/api/v0/repos');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch repositories');
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Space Invaders',
|
||||
description: 'Retro arcade shooter',
|
||||
thumbnail: 'https://picsum.photos/seed/space/400/225'
|
||||
staleTime: 60000
|
||||
}));
|
||||
|
||||
const projectsQuery = createQuery<Project[]>(() => ({
|
||||
queryKey: ['projects', reposQuery.data],
|
||||
queryFn: async () => {
|
||||
const repos = reposQuery.data ?? [];
|
||||
const projects = await Promise.all(
|
||||
repos.map(async (repo: RepoDefinition) => {
|
||||
const project = await resolveProjectData(repo);
|
||||
return project;
|
||||
})
|
||||
);
|
||||
return projects;
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Tetris',
|
||||
description: 'Block stacking puzzle game',
|
||||
thumbnail: 'https://picsum.photos/seed/tetris/400/225'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Snake',
|
||||
description: 'Classic snake game with power-ups',
|
||||
thumbnail: 'https://picsum.photos/seed/snake/400/225'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Pong',
|
||||
description: 'Two-player paddle game',
|
||||
thumbnail: 'https://picsum.photos/seed/pong/400/225'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Breakout',
|
||||
description: 'Brick-breaking arcade game',
|
||||
thumbnail: 'https://picsum.photos/seed/breakout/400/225'
|
||||
}
|
||||
];
|
||||
enabled: !!reposQuery.data && reposQuery.data.length > 0,
|
||||
staleTime: 60000
|
||||
}));
|
||||
|
||||
const projects = $derived(projectsQuery.data ?? []);
|
||||
const isLoading = $derived(
|
||||
reposQuery.isPending || (projectsQuery.isPending && projectsQuery.isEnabled)
|
||||
);
|
||||
const hasError = $derived(reposQuery.isError || projectsQuery.isError);
|
||||
const isEmpty = $derived(!isLoading && !hasError && projects.length === 0);
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each mockProjects as project (project.id)}
|
||||
<div
|
||||
class="group relative overflow-hidden rounded-lg bg-gray-900 shadow-xl ring-1 ring-gray-800 transition-transform hover:scale-[1.02]"
|
||||
>
|
||||
<div class="relative aspect-video overflow-hidden">
|
||||
<Image
|
||||
src={project.thumbnail}
|
||||
alt={project.name}
|
||||
class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
{#if isLoading}
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<LoaderCircle class="h-8 w-8 animate-spin text-text-muted" />
|
||||
</div>
|
||||
{:else if hasError}
|
||||
<div class="py-12 text-center text-text-muted">
|
||||
<p>Failed to load projects. Please try again later.</p>
|
||||
</div>
|
||||
{:else if isEmpty}
|
||||
<div class="py-12 text-center text-text-muted">
|
||||
<p>No projects available yet.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each projects as project (project.id)}
|
||||
<div
|
||||
class="group relative overflow-hidden rounded-lg bg-gray-900 shadow-xl ring-1 ring-gray-800 transition-transform hover:scale-[1.02]"
|
||||
>
|
||||
<div class="relative aspect-video overflow-hidden">
|
||||
<Image
|
||||
src={'https://picsum.photos/seed/yama/400/225'}
|
||||
alt={project.name}
|
||||
class="min-h-56.5 w-full min-w-100 object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
|
||||
>
|
||||
<Button.Root
|
||||
class="flex h-10 w-14 items-center justify-center rounded-md bg-white/90 text-gray-900 shadow-lg transition-transform hover:scale-110 hover:bg-white"
|
||||
onclick={() => {
|
||||
goto(`/p/${project.id}`).catch((e) => {
|
||||
console.warn(e);
|
||||
});
|
||||
}}
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
|
||||
>
|
||||
<Play />
|
||||
</Button.Root>
|
||||
<Button.Root
|
||||
class="flex h-10 w-14 items-center justify-center rounded-md bg-white/90 text-gray-900 shadow-lg transition-transform hover:scale-110 hover:bg-white"
|
||||
onclick={() => {
|
||||
goto(`/p/${project.id}`).catch((e) => {
|
||||
console.warn(e);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Play />
|
||||
</Button.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-semibold text-white">{project.name}</h3>
|
||||
<p class="mt-1 text-sm text-gray-400">{project.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-semibold text-white">{project.name}</h3>
|
||||
<p class="mt-1 text-sm text-gray-400">{project.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
28
src/lib/project/resolve.ts
Normal file
28
src/lib/project/resolve.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { Project, RepoDefinition } from "$lib/types/project";
|
||||
|
||||
export async function resolveProjectData(repo: RepoDefinition): Promise<Project> {
|
||||
const { id, full_name: fullName } = repo;
|
||||
let full_name_p = fullName.split("/")
|
||||
const name = full_name_p[1] ?? fullName;
|
||||
|
||||
let description;
|
||||
const repoResponse = await fetch(`https://api.github.com/repos/${fullName}`);
|
||||
if (repoResponse.ok) {
|
||||
const repoData = await repoResponse.json();
|
||||
description = repoData.description ?? `by ${full_name_p[0]}`;
|
||||
}
|
||||
|
||||
let thumbnail = `https://raw.githubusercontent.com/${fullName}/HEAD/thumbnail.webp`
|
||||
const thumbResponse = await fetch(thumbnail, { method: 'HEAD' });
|
||||
if (!thumbResponse.ok) {
|
||||
thumbnail = `https://picsum.photos/seed/${encodeURIComponent(fullName.replaceAll("/", "-"))}/400/225`;
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
full_name: fullName,
|
||||
name,
|
||||
thumbnail,
|
||||
description
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
export type Project = {
|
||||
id: number;
|
||||
name: string;
|
||||
thumbnail?: string;
|
||||
description: string;
|
||||
url?: string;
|
||||
id: string;
|
||||
full_name: string;
|
||||
name: string;
|
||||
thumbnail?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type RepoDefinition = {
|
||||
id: string;
|
||||
full_name: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user