feat: add repos page

This commit is contained in:
2026-01-14 19:08:06 -08:00
parent 5a2bec37b9
commit 92d409f812
14 changed files with 2833 additions and 26 deletions

View File

@@ -1,8 +1,12 @@
<script lang="ts">
import { Avatar, Button, DropdownMenu } from 'bits-ui';
import { Avatar, Button, DropdownMenu, Separator } from 'bits-ui';
import { authClient } from './auth-client';
import Image from './Image.svelte';
import { User } from '@lucide/svelte';
import { CircleUser, FolderGit2, User } from '@lucide/svelte';
import { goto } from '$app/navigation';
type MockUser = {
image: string;
};
const session = authClient.useSession();
const user = $derived($session.data?.user);
@@ -16,27 +20,38 @@
<div class="h-14 w-full bg-[#292e42]">
<div class="mx-auto flex h-full max-w-[78rem] items-center justify-between">
<h1 class="header-title font-light">Godot Host</h1>
<div>
{#if !user}
<Button.Root class="rounded-md p-2 transition-colors hover:bg-gray-800" onclick={signIn}>
Sign In
</Button.Root>
{:else}
<DropdownMenu.Root open={true}>
<DropdownMenu.Trigger>
<Image class="shadow-4xl h-10 w-10 rounded-full" src={user.image} />
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="w-42 rounded-md bg-black shadow-xl shadow-gray-800/5">
<DropdownMenu.Item class="mt-1 flex h-10 items-center rounded-md px-2 py-1.5">
<User />
<span>Sign Out</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
{/if}
</div>
<Button.Root onclick={() => goto('/')}>
<h1 class="header-title font-light">Godot Host</h1>
</Button.Root>
{#if !user && !$session.isPending}
<Button.Root class="rounded-md p-2 transition-colors hover:bg-gray-800" onclick={signIn}>
Sign In
</Button.Root>
{:else if user}
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Image class="shadow-4xl h-10 w-10 rounded-full" src={user.image} />
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
class="w-42 rounded-xl bg-black px-1 py-1.5 shadow-xl shadow-gray-800/5"
>
<DropdownMenu.Item class="dropdown-item cursor-pointer" onclick={() => goto('/add')}>
<FolderGit2 class="mr-2" />
<span>Add Project</span>
</DropdownMenu.Item>
<DropdownMenu.Item
class="dropdown-item cursor-pointer"
onclick={() => authClient.signOut()}
>
<User class="mr-2" />
<span>Sign Out</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
{:else}
<div class="h-10 w-10 animate-pulse rounded-full bg-gray-700"></div>
{/if}
</div>
</div>

147
src/lib/Repos.svelte Normal file
View File

@@ -0,0 +1,147 @@
<script lang="ts">
import { Button } from 'bits-ui';
import { GitBranch, Star, Clock, Import, Search, RefreshCw } from '@lucide/svelte';
import type { Repository } from './types/repo';
import { createQuery } from '@tanstack/svelte-query';
import { authClient } from './auth-client';
const session = authClient.useSession();
const query = createQuery(() => ({
queryKey: ['github-repositories'],
queryFn: async () => {
const response = await fetch('https://api.github.com/user/repos?affiliation=owner', {
headers: {}
});
return await response.json();
}
}));
$inspect(query.data);
const mockRepositories: Repository[] = [
{
id: 1,
name: 'Mock',
fullName: 'Mock',
description: 'Mock Data',
language: 'GDScript',
stars: 42,
updatedAt: new Date().toISOString(),
isPrivate: false
}
];
let searchQuery = $state('');
let adding = $state<number | null>(null);
const filteredRepositories = $derived(
mockRepositories.filter(
(repo) =>
repo.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
repo.description?.toLowerCase().includes(searchQuery.toLowerCase())
)
);
const handleImport = async (repoId: number) => {
adding = repoId;
await new Promise((resolve) => setTimeout(resolve, 2000));
adding = null;
};
const languageColors: Record<string, string> = {
GDScript: 'bg-blue-500',
'C#': 'bg-green-500',
Rust: 'bg-orange-500',
C: 'bg-gray-500',
'C++': 'bg-pink-500'
};
</script>
<div class="rounded-xl bg-gray-900 p-6 shadow-xl ring-1 ring-gray-800">
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-3">
<GitBranch class="h-6 w-6 text-gray-400" />
<h2 class="text-xl font-semibold text-white">Import Git Repository</h2>
</div>
<Button.Root
class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-400 transition-colors hover:bg-gray-800 hover:text-white"
>
<RefreshCw class="h-4 w-4" />
<span>Refresh</span>
</Button.Root>
</div>
<div class="relative mb-6">
<Search class="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-500" />
<input
type="text"
bind:value={searchQuery}
placeholder="Search repositories..."
class="w-full rounded-lg border border-gray-700 bg-gray-800 py-2.5 pr-4 pl-10 text-white placeholder-gray-500 transition-colors outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
/>
</div>
<div class="space-y-3">
{#each filteredRepositories as repo (repo.id)}
<div
class="group flex items-center justify-between rounded-lg border border-gray-800 bg-gray-800/50 p-4 transition-colors hover:border-gray-700 hover:bg-gray-800"
>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<h3 class="truncate font-medium text-white">{repo.name}</h3>
{#if repo.isPrivate}
<span class="rounded-full border border-gray-600 px-2 py-0.5 text-xs text-gray-400">
Private
</span>
{/if}
</div>
{#if repo.description}
<p class="mt-1 truncate text-sm text-gray-400">{repo.description}</p>
{:else}
<p class="mt-1 text-sm text-gray-500 italic">No description</p>
{/if}
<div class="mt-2 flex items-center gap-4 text-xs text-gray-500">
{#if repo.language}
<span class="flex items-center gap-1.5">
<span
class="h-2.5 w-2.5 rounded-full {languageColors[repo.language] ?? 'bg-gray-500'}"
></span>
{repo.language}
</span>
{/if}
<span class="flex items-center gap-1">
<Star class="h-3.5 w-3.5" />
{repo.stars}
</span>
<span class="flex items-center gap-1">
<Clock class="h-3.5 w-3.5" />
{repo.updatedAt}
</span>
</div>
</div>
<Button.Root
class="ml-4 flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-500 disabled:cursor-not-allowed disabled:opacity-50"
disabled={adding !== null}
onclick={() => handleImport(repo.id)}
>
{#if adding === repo.id}
<RefreshCw class="h-4 w-4 animate-spin" />
<span>Adding...</span>
{:else}
<Import class="h-4 w-4" />
<span>Add</span>
{/if}
</Button.Root>
</div>
{:else}
<div class="py-8 text-center text-gray-500">
<p>No repositories found matching "{searchQuery}"</p>
</div>
{/each}
</div>
<p class="mt-6 text-center text-sm text-gray-500">
You must have a dist/ folder with index.html + index.wasm
</p>
</div>

10
src/lib/types/repo.ts Normal file
View File

@@ -0,0 +1,10 @@
export type Repository = {
id: number;
name: string;
fullName: string;
description: string | null;
language: string | null;
stars: number;
updatedAt: string;
isPrivate: boolean;
};

View File

@@ -1,9 +1,16 @@
<script lang="ts">
import Header from '$lib/Header.svelte';
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
import './layout.css';
import { SvelteQueryDevtools } from '@tanstack/svelte-query-devtools';
let { children } = $props();
const queryClient = new QueryClient();
</script>
<Header />
{@render children()}
<QueryClientProvider client={queryClient}>
{@render children()}
<SvelteQueryDevtools />
</QueryClientProvider>

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import Repos from '$lib/Repos.svelte';
</script>
<main class="mx-auto mt-8 max-w-3xl px-4">
<Repos />
</main>

View File

@@ -52,6 +52,15 @@ h1 {
font-family: 'Times New Roman';
}
.dropdown-item {
display: flex;
align-items: center;
@apply h-10;
@apply rounded-lg;
@apply px-2 py-1;
@apply hover:bg-gray-800;
}
button:default {
border-radius: 8px;
border: 1px solid transparent;