refactor!: setup file proxy for projects

This commit is contained in:
2026-01-20 19:10:01 -08:00
parent 5eccfe32da
commit a2afc3fa05
17 changed files with 279 additions and 103 deletions

View File

@@ -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}>

View File

@@ -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}

View 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
}
}

View File

@@ -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;
};

View File

@@ -5,7 +5,7 @@
import { Maximize, Minimize, ArrowLeft } from '@lucide/svelte';
const id = $derived(page.params.id);
const src = $derived(`/projects/${id}/index.html`);
const src = $derived(`/api/v0/repos/${id}/files/index.html`);
let isFullscreen = $state(false);
let containerRef = $state<HTMLDivElement | null>(null);