feat: initial commit, draft frontend

This commit is contained in:
2026-01-13 22:07:44 -08:00
commit 5a2bec37b9
41 changed files with 3260 additions and 0 deletions

42
src/lib/Header.svelte Normal file
View File

@@ -0,0 +1,42 @@
<script lang="ts">
import { Avatar, Button, DropdownMenu } from 'bits-ui';
import { authClient } from './auth-client';
import Image from './Image.svelte';
import { User } from '@lucide/svelte';
const session = authClient.useSession();
const user = $derived($session.data?.user);
const signIn = async () => {
await authClient.signIn.social({
provider: 'github'
});
};
</script>
<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>
</div>
</div>

21
src/lib/Image.svelte Normal file
View File

@@ -0,0 +1,21 @@
<script lang="ts">
type ImageStatus = 'loading' | 'loaded' | 'error';
import type { HTMLImgAttributes } from 'svelte/elements';
let { src, ...props }: HTMLImgAttributes = $props();
let status: ImageStatus = $state('loading');
</script>
{#if status === 'loading'}
<div class="h-full w-full animate-pulse bg-gray-800"></div>
{/if}
<img
{src}
hidden={status === 'loading'}
onload={() => {
status = 'loaded';
}}
onerror={() => (status = 'error')}
{...props}
/>

83
src/lib/Projects.svelte Normal file
View File

@@ -0,0 +1,83 @@
<script lang="ts">
import { Play } from '@lucide/svelte';
import { Button } from 'bits-ui';
import type { Project } from './types/project';
import { goto } from '$app/navigation';
import Image from './Image.svelte';
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'
},
{
id: 2,
name: 'Space Invaders',
description: 'Retro arcade shooter',
thumbnail: 'https://picsum.photos/seed/space/400/225'
},
{
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'
}
];
</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"
/>
<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);
});
}}
>
<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>
{/each}
</div>

5
src/lib/auth-client.ts Normal file
View File

@@ -0,0 +1,5 @@
import { createAuthClient } from 'better-auth/svelte';
export const authClient = createAuthClient({
baseURL: 'http://localhost:5173'
});

17
src/lib/auth.ts Normal file
View File

@@ -0,0 +1,17 @@
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { db } from './db/drizzle';
import * as authSchema from './db/auth-schema';
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'pg',
schema: { ...authSchema }
}),
socialProviders: {
github: {
clientId: process.env.GH_CLIENT_ID!,
clientSecret: process.env.GH_CLIENT_SECRET!
}
}
});

93
src/lib/db/auth-schema.ts Normal file
View File

@@ -0,0 +1,93 @@
import { relations } from "drizzle-orm";
import { pgTable, text, timestamp, boolean, index } from "drizzle-orm/pg-core";
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
});
export const session = pgTable(
"session",
{
id: text("id").primaryKey(),
expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
},
(table) => [index("session_userId_idx").on(table.userId)],
);
export const account = pgTable(
"account",
{
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at"),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("account_userId_idx").on(table.userId)],
);
export const verification = pgTable(
"verification",
{
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("verification_identifier_idx").on(table.identifier)],
);
export const userRelations = relations(user, ({ many }) => ({
sessions: many(session),
accounts: many(account),
}));
export const sessionRelations = relations(session, ({ one }) => ({
user: one(user, {
fields: [session.userId],
references: [user.id],
}),
}));
export const accountRelations = relations(account, ({ one }) => ({
user: one(user, {
fields: [account.userId],
references: [user.id],
}),
}));

3
src/lib/db/drizzle.ts Normal file
View File

@@ -0,0 +1,3 @@
import { drizzle } from 'drizzle-orm/neon-http';
export const db = drizzle(process.env.DATABASE_URL!);

7
src/lib/types/project.ts Normal file
View File

@@ -0,0 +1,7 @@
export type Project = {
id: number;
name: string;
thumbnail?: string;
description: string;
url?: string;
};