瀏覽代碼

Create the page to display all games

Lee Morgan 3 周之前
父節點
當前提交
24b2832b2c

+ 1 - 1
src/routes/(app)/dashboard/+page.svelte

@@ -34,7 +34,7 @@
                 </div>
             </a>
 
-            <a href="/dashboard/games" class="card card-games">
+            <a href="/game" class="card card-games">
                 <div class="card-icon">
                     <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
                         <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>

+ 1 - 1
src/routes/(app)/dashboard/DashboardNav.svelte

@@ -18,7 +18,7 @@
     </a>
     <nav>
         <a href="/tokens" class="nav-link">Tokens</a>
-        <a href="/dashboard/games" class="nav-link">Games</a>
+        <a href="/game" class="nav-link">Games</a>
         <button class="nav-link logout" onclick={logout}>Log Out</button>
     </nav>
 </header>

+ 20 - 0
src/routes/(app)/game/+page.server.js

@@ -0,0 +1,20 @@
+import {PUBLIC_API_URL} from "$env/static/public";
+import {error} from "@sveltejs/kit";
+
+export async function load({fetch}){
+    const res = await fetch(`${PUBLIC_API_URL}/game`, {
+        method: "GET",
+        headers: {"Content-Type": "application/json"},
+        credentials: "include"
+    });
+
+    const data = await res.json();
+    if(!res.ok){
+        error(res.status, {
+            message: data.error.message,
+            back: "/"
+        });
+    }
+
+    return {data};
+}

+ 160 - 0
src/routes/(app)/game/+page.svelte

@@ -0,0 +1,160 @@
+<script>
+    import DashboardNav from "../dashboard/DashboardNav.svelte";
+    import Game from "./Game.svelte";
+
+    let { data } = $props();
+    let games = $derived(data?.data ?? data ?? []);
+</script>
+
+<div class="page">
+    <DashboardNav />
+
+    <main>
+        <div class="heading">
+            <a href="/dashboard" class="back">
+                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
+                    <path d="M19 12H5M12 5l-7 7 7 7"/>
+                </svg>
+                Dashboard
+            </a>
+            <div class="heading-row">
+                <h1>My Games</h1>
+                <a href="/game/new" class="new-btn">
+                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
+                        <path d="M12 5v14M5 12h14"/>
+                    </svg>
+                    New Game
+                </a>
+            </div>
+            <p>{games.length} {games.length === 1 ? "adventure" : "adventures"}</p>
+        </div>
+
+        {#if games.length > 0}
+            <div class="game-list">
+                {#each games as game (game.id)}
+                    <Game {game} />
+                {/each}
+            </div>
+        {:else}
+            <div class="empty">
+                <div class="empty-icon">
+                    <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
+                        <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
+                        <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
+                    </svg>
+                </div>
+                <p>No adventures yet. Start your first one.</p>
+            </div>
+        {/if}
+    </main>
+</div>
+
+<style>
+    .page {
+        min-height: 100vh;
+        display: flex;
+        flex-direction: column;
+        background: #0b0b10;
+    }
+
+    main {
+        flex: 1;
+        max-width: 720px;
+        width: 100%;
+        margin: 0 auto;
+        padding: 3rem 2rem 6rem;
+    }
+
+    .heading {
+        margin-bottom: 2rem;
+    }
+
+    .back {
+        display: inline-flex;
+        align-items: center;
+        gap: 6px;
+        font-size: 0.85rem;
+        font-weight: 500;
+        color: #6b6b7a;
+        text-decoration: none;
+        margin-bottom: 1.25rem;
+        transition: color 0.15s;
+    }
+
+    .back:hover { color: #a1a1aa; }
+
+    h1 {
+        font-size: clamp(1.8rem, 4vw, 2.4rem);
+        font-weight: 800;
+        letter-spacing: -1.5px;
+        color: #e5e5ea;
+        margin-bottom: 0.25rem;
+    }
+
+    .heading-row {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        gap: 1rem;
+        margin-bottom: 0.25rem;
+    }
+
+    .heading p {
+        font-size: 0.9rem;
+        color: #6b6b7a;
+    }
+
+    .new-btn {
+        display: inline-flex;
+        align-items: center;
+        gap: 6px;
+        padding: 0.5rem 1rem;
+        background: #c084fc;
+        color: #0b0b10;
+        font-size: 0.875rem;
+        font-weight: 600;
+        border-radius: 8px;
+        text-decoration: none;
+        white-space: nowrap;
+        transition: background 0.15s;
+        flex-shrink: 0;
+    }
+
+    .new-btn:hover { background: #d8b4fe; }
+
+    .game-list {
+        display: flex;
+        flex-direction: column;
+        gap: 0.75rem;
+    }
+
+    .empty {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        gap: 1rem;
+        padding: 5rem 2rem;
+        color: #6b6b7a;
+        text-align: center;
+    }
+
+    .empty-icon {
+        width: 64px;
+        height: 64px;
+        border-radius: 16px;
+        background: #0f0f18;
+        border: 1px solid #1e1e28;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: #3a3a50;
+    }
+
+    .empty p {
+        font-size: 0.95rem;
+    }
+
+    @media (max-width: 600px) {
+        main { padding: 2rem 1.25rem 4rem; }
+    }
+</style>

+ 120 - 0
src/routes/(app)/game/Game.svelte

@@ -0,0 +1,120 @@
+<script>
+    let { game } = $props();
+
+    function formatDate(dateStr) {
+        return new Date(dateStr).toLocaleDateString("en-US", {
+            month: "short", day: "numeric", year: "numeric"
+        });
+    }
+</script>
+
+<a href={`/game/${game.id}`} class="game-card">
+    <div class="game-icon">
+        <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
+            <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
+            <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
+            <path d="M8 7h8M8 11h5"/>
+        </svg>
+    </div>
+
+    <div class="game-info">
+        <h2>{game.title}</h2>
+        <p class="context">{game.world_context}</p>
+        <p class="date">Started {formatDate(game.created_at)}</p>
+    </div>
+
+    <div class="game-arrow">
+        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+            <path d="M5 12h14M12 5l7 7-7 7"/>
+        </svg>
+    </div>
+</a>
+
+<style>
+    .game-card {
+        display: flex;
+        align-items: center;
+        gap: 1.25rem;
+        padding: 1.25rem 1.5rem;
+        background: #0f0f18;
+        border: 1px solid #1e1e28;
+        border-radius: 12px;
+        text-decoration: none;
+        color: inherit;
+        transition: border-color 0.2s, transform 0.15s, box-shadow 0.2s;
+        position: relative;
+        overflow: hidden;
+    }
+
+    .game-card::before {
+        content: "";
+        position: absolute;
+        inset: 0;
+        background: radial-gradient(ellipse at left, rgba(192, 132, 252, 0.06) 0%, transparent 70%);
+        opacity: 0;
+        transition: opacity 0.2s;
+    }
+
+    .game-card:hover {
+        border-color: #c084fc;
+        transform: translateX(4px);
+        box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
+    }
+
+    .game-card:hover::before {
+        opacity: 1;
+    }
+
+    .game-icon {
+        width: 44px;
+        height: 44px;
+        border-radius: 10px;
+        background: rgba(192, 132, 252, 0.1);
+        color: #c084fc;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        flex-shrink: 0;
+    }
+
+    .game-info {
+        flex: 1;
+        min-width: 0;
+    }
+
+    .game-info h2 {
+        font-size: 1rem;
+        font-weight: 600;
+        color: #e5e5ea;
+        letter-spacing: -0.2px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+    }
+
+    .context {
+        font-size: 0.85rem;
+        color: #6b6b7a;
+        margin-top: 0.25rem;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+    }
+
+    .date {
+        font-size: 0.75rem;
+        color: #44445a;
+        margin-top: 0.2rem;
+    }
+
+    .game-arrow {
+        color: #3a3a50;
+        flex-shrink: 0;
+        transition: color 0.2s, transform 0.2s;
+    }
+
+    .game-card:hover .game-arrow {
+        color: #c084fc;
+        transform: translateX(3px);
+    }
+</style>