Lee Morgan пре 3 недеља
родитељ
комит
1f038d104b
2 измењених фајлова са 326 додато и 117 уклоњено
  1. 77 117
      src/routes/(app)/game/[game_id]/+page.svelte
  2. 249 0
      src/routes/(app)/game/[game_id]/Sidebar.svelte

+ 77 - 117
src/routes/(app)/game/[game_id]/+page.svelte

@@ -3,7 +3,7 @@
     import {PUBLIC_API_URL} from "$env/static/public";
     import {onMount} from "svelte";
 
-    import DashboardNav from "../../dashboard/DashboardNav.svelte";
+    import Sidebar from "./Sidebar.svelte";
     import Message from "./Message.svelte";
 
     let game = $state(page.data.data.game);
@@ -14,6 +14,7 @@
     let streaming = $state(false);
 
     let inputText = $state("");
+    let sidebarOpen = $state(false);
 
     const autogrow = (node) => {
         const update = () => {
@@ -81,30 +82,32 @@
 </script>
 
 <div class="page">
-    <DashboardNav />
-
-    <div class="game-bar">
-        <div class="game-bar-left">
-            <div class="game-title-block">
-                <span class="game-label">Adventure</span>
-                <h1>{game.title}</h1>
-            </div>
-            <div class="divider"></div>
-            <div class="character-block">
-                <span class="game-label">Character</span>
-                <span class="character-name">{character.name}</span>
-            </div>
-        </div>
-        <div class="token-badge" class:token-low={tokensRemaining < 500}>
-            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-                <circle cx="12" cy="12" r="10"/>
-                <path d="M12 6v12M9 9h4.5a2.5 2.5 0 0 1 0 5H9"/>
+
+{#if sidebarOpen}
+    <div class="sidebar-overlay" onclick={() => sidebarOpen = false}></div>
+{/if}
+
+<Sidebar
+    {game}
+    {character}
+    {tokensRemaining}
+    open={sidebarOpen}
+    onClose={() => sidebarOpen = false}
+/>
+
+<div class="main">
+    <div class="mobile-bar">
+        <button class="menu-btn" onclick={() => sidebarOpen = true} aria-label="Open menu">
+            <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
+                <line x1="3" y1="6" x2="21" y2="6"/>
+                <line x1="3" y1="12" x2="21" y2="12"/>
+                <line x1="3" y1="18" x2="21" y2="18"/>
             </svg>
-            {tokensRemaining.toLocaleString()} tokens
-        </div>
+        </button>
+        <span class="mobile-title">{game.title}</span>
     </div>
 
-    <main class="messages" id="message-list">
+    <main class="messages">
         <div class="messages-inner">
             {#each turns as turn, i}
                 {#if i !== 0}
@@ -145,88 +148,64 @@
     </div>
 </div>
 
+</div>
+
 <style>
     .page {
+        display: flex;
         height: 100vh;
+        overflow: hidden;
+        background: #0b0b10;
+    }
+
+    .sidebar-overlay {
+        display: none;
+    }
+
+    .main {
+        flex: 1;
         display: flex;
         flex-direction: column;
+        height: 100vh;
         background: #0b0b10;
         overflow: hidden;
+        min-width: 0;
     }
 
-    /* ── Game bar ── */
-    .game-bar {
-        display: flex;
+    /* ── Mobile bar ── */
+    .mobile-bar {
+        display: none;
         align-items: center;
-        justify-content: space-between;
-        padding: 0.75rem 2rem;
+        gap: 0.75rem;
+        padding: 0.75rem 1rem;
         border-bottom: 1px solid #1a1a26;
         background: #0d0d16;
         flex-shrink: 0;
-        gap: 1rem;
     }
 
-    .game-bar-left {
+    .menu-btn {
+        background: none;
+        border: none;
+        color: #6b6b7a;
+        cursor: pointer;
+        padding: 4px;
+        border-radius: 6px;
         display: flex;
         align-items: center;
-        gap: 1.25rem;
-        min-width: 0;
+        transition: color 0.15s;
     }
 
-    .game-label {
-        display: block;
-        font-size: 0.65rem;
-        font-weight: 600;
-        letter-spacing: 0.08em;
-        text-transform: uppercase;
-        color: #44445a;
-        margin-bottom: 0.15rem;
-    }
+    .menu-btn:hover { color: #e5e5ea; }
 
-    h1 {
+    .mobile-title {
         font-size: 0.95rem;
         font-weight: 700;
         color: #e5e5ea;
-        letter-spacing: -0.2px;
         white-space: nowrap;
         overflow: hidden;
         text-overflow: ellipsis;
     }
 
-    .divider {
-        width: 1px;
-        height: 2rem;
-        background: #1e1e2e;
-        flex-shrink: 0;
-    }
-
-    .character-name {
-        font-size: 0.95rem;
-        font-weight: 600;
-        color: #c084fc;
-    }
-
-    .token-badge {
-        display: inline-flex;
-        align-items: center;
-        gap: 5px;
-        padding: 0.35rem 0.75rem;
-        border-radius: 20px;
-        background: #13131e;
-        border: 1px solid #1e1e2e;
-        font-size: 0.78rem;
-        font-weight: 600;
-        color: #6b6b7a;
-        white-space: nowrap;
-        flex-shrink: 0;
-        transition: color 0.2s, border-color 0.2s;
-    }
-
-    .token-badge.token-low {
-        color: #e05555;
-        border-color: rgba(224, 85, 85, 0.3);
-    }
-
     /* ── Messages ── */
     .messages {
         flex: 1;
@@ -234,21 +213,12 @@
         scroll-behavior: smooth;
     }
 
-    .messages::-webkit-scrollbar {
-        width: 6px;
-    }
-
-    .messages::-webkit-scrollbar-track {
-        background: transparent;
-    }
-
-    .messages::-webkit-scrollbar-thumb {
-        background: #1e1e2e;
-        border-radius: 3px;
-    }
+    .messages::-webkit-scrollbar { width: 5px; }
+    .messages::-webkit-scrollbar-track { background: transparent; }
+    .messages::-webkit-scrollbar-thumb { background: #1e1e2e; border-radius: 3px; }
 
     .messages-inner {
-        max-width: 820px;
+        max-width: 800px;
         margin: 0 auto;
         padding: 2rem 2rem 1rem;
         display: flex;
@@ -294,7 +264,7 @@
     }
 
     .composer {
-        max-width: 820px;
+        max-width: 800px;
         margin: 0 auto;
         display: flex;
         align-items: flex-end;
@@ -327,15 +297,8 @@
         padding: 0;
     }
 
-    .composer textarea::placeholder {
-        color: #44445a;
-        font-style: italic;
-    }
-
-    .composer textarea:disabled {
-        opacity: 0.5;
-        cursor: not-allowed;
-    }
+    .composer textarea::placeholder { color: #44445a; font-style: italic; }
+    .composer textarea:disabled { opacity: 0.5; cursor: not-allowed; }
 
     .composer button {
         width: 36px;
@@ -352,21 +315,12 @@
         transition: background 0.15s, transform 0.1s, opacity 0.15s;
     }
 
-    .composer button:hover:not(:disabled) {
-        background: #4646e0;
-    }
-
-    .composer button:active:not(:disabled) {
-        transform: scale(0.93);
-    }
-
-    .composer button:disabled {
-        opacity: 0.35;
-        cursor: not-allowed;
-    }
+    .composer button:hover:not(:disabled) { background: #4646e0; }
+    .composer button:active:not(:disabled) { transform: scale(0.93); }
+    .composer button:disabled { opacity: 0.35; cursor: not-allowed; }
 
     .composer-hint {
-        max-width: 820px;
+        max-width: 800px;
         margin: 0.4rem auto 0;
         font-size: 0.7rem;
         color: #2e2e44;
@@ -374,13 +328,19 @@
         padding: 0 0.25rem;
     }
 
-    @media (max-width: 700px) {
-        .game-bar { padding: 0.6rem 1rem; }
+    /* ── Mobile ── */
+    @media (max-width: 768px) {
+        .sidebar-overlay {
+            display: block;
+            position: fixed;
+            inset: 0;
+            background: rgba(0, 0, 0, 0.6);
+            z-index: 199;
+        }
+
+        .mobile-bar { display: flex; }
         .messages-inner { padding: 1.25rem 1rem 0.75rem; }
         .composer-wrap { padding: 0.75rem 1rem 0.5rem; }
-        .game-bar-left { gap: 0.75rem; }
-        .divider { display: none; }
-        .character-block { display: none; }
         .composer-hint { display: none; }
     }
 </style>

+ 249 - 0
src/routes/(app)/game/[game_id]/Sidebar.svelte

@@ -0,0 +1,249 @@
+<script>
+    import favicon from "$lib/assets/favicon.png";
+    import { PUBLIC_API_URL } from "$env/static/public";
+    import { goto } from "$app/navigation";
+
+    let { game, character, tokensRemaining, open = false, onClose } = $props();
+
+    const logout = async () => {
+        await fetch(`${PUBLIC_API_URL}/user/logout`, { credentials: "include" });
+        goto("/");
+    };
+</script>
+
+<aside class="sidebar" class:open>
+    <div class="sidebar-header">
+        <a href="/dashboard" class="brand">
+            <img src={favicon} alt="ChatRPG" width="26" height="26" />
+            <span>ChatRPG</span>
+        </a>
+        <button class="close-btn" onclick={onClose} aria-label="Close menu">
+            <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
+                <path d="M18 6L6 18M6 6l12 12"/>
+            </svg>
+        </button>
+    </div>
+
+    <div class="info-group">
+        <span class="info-label">Adventure</span>
+        <p class="info-value">{game.title}</p>
+    </div>
+
+    <div class="info-group">
+        <span class="info-label">Character</span>
+        <p class="info-value character">{character.name}</p>
+    </div>
+
+    <div class="info-group">
+        <span class="info-label">Tokens remaining</span>
+        <div class="token-display" class:low={tokensRemaining < 500}>
+            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                <circle cx="12" cy="12" r="10"/>
+                <path d="M12 6v12M9 9h4.5a2.5 2.5 0 0 1 0 5H9"/>
+            </svg>
+            {tokensRemaining.toLocaleString()}
+            {#if tokensRemaining < 500}
+                <span class="low-warning">— running low</span>
+            {/if}
+        </div>
+    </div>
+
+    <div class="sidebar-sep"></div>
+
+    <nav class="sidebar-nav">
+        <a href="/game" class="nav-item">
+            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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>
+            My Games
+        </a>
+        <a href="/tokens" class="nav-item">
+            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                <circle cx="12" cy="12" r="10"/>
+                <path d="M12 6v12M9 9h4.5a2.5 2.5 0 0 1 0 5H9"/>
+            </svg>
+            Add Tokens
+        </a>
+    </nav>
+
+    <button class="logout-btn" onclick={logout}>
+        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+            <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
+            <polyline points="16 17 21 12 16 7"/>
+            <line x1="21" y1="12" x2="9" y2="12"/>
+        </svg>
+        Log Out
+    </button>
+</aside>
+
+<style>
+    .sidebar {
+        width: 220px;
+        flex-shrink: 0;
+        height: 100vh;
+        display: flex;
+        flex-direction: column;
+        background: #0d0d16;
+        border-right: 1px solid #1a1a26;
+        padding: 1.25rem 1rem;
+        gap: 0;
+        overflow-y: auto;
+    }
+
+    /* ── Header ── */
+    .sidebar-header {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin-bottom: 2rem;
+    }
+
+    .brand {
+        display: flex;
+        align-items: center;
+        gap: 9px;
+        text-decoration: none;
+        color: #e5e5ea;
+        font-weight: 700;
+        font-size: 1rem;
+        letter-spacing: -0.3px;
+    }
+
+    .close-btn {
+        display: none;
+        background: none;
+        border: none;
+        color: #6b6b7a;
+        cursor: pointer;
+        padding: 4px;
+        border-radius: 6px;
+        transition: color 0.15s;
+    }
+
+    .close-btn:hover { color: #e5e5ea; }
+
+    /* ── Info groups ── */
+    .info-group {
+        padding: 0.75rem 0.5rem;
+        border-bottom: 1px solid #14141f;
+    }
+
+    .info-label {
+        display: block;
+        font-size: 0.63rem;
+        font-weight: 700;
+        letter-spacing: 0.1em;
+        text-transform: uppercase;
+        color: #44445a;
+        margin-bottom: 0.35rem;
+    }
+
+    .info-value {
+        font-size: 0.9rem;
+        font-weight: 600;
+        color: #e5e5ea;
+        line-height: 1.4;
+        word-break: break-word;
+    }
+
+    .info-value.character {
+        color: #c084fc;
+    }
+
+    .token-display {
+        display: flex;
+        align-items: center;
+        gap: 5px;
+        font-size: 0.9rem;
+        font-weight: 600;
+        color: #6b6b7a;
+        flex-wrap: wrap;
+    }
+
+    .token-display.low {
+        color: #e05555;
+    }
+
+    .low-warning {
+        font-size: 0.75rem;
+        font-weight: 500;
+        color: #e05555;
+        opacity: 0.8;
+    }
+
+    /* ── Separator ── */
+    .sidebar-sep {
+        flex: 1;
+    }
+
+    /* ── Nav ── */
+    .sidebar-nav {
+        display: flex;
+        flex-direction: column;
+        gap: 2px;
+        margin-bottom: 0.5rem;
+    }
+
+    .nav-item {
+        display: flex;
+        align-items: center;
+        gap: 9px;
+        padding: 0.55rem 0.6rem;
+        border-radius: 8px;
+        font-size: 0.875rem;
+        font-weight: 500;
+        color: #6b6b7a;
+        text-decoration: none;
+        transition: background 0.15s, color 0.15s;
+    }
+
+    .nav-item:hover {
+        background: #14141f;
+        color: #e5e5ea;
+    }
+
+    /* ── Logout ── */
+    .logout-btn {
+        display: flex;
+        align-items: center;
+        gap: 9px;
+        padding: 0.55rem 0.6rem;
+        border-radius: 8px;
+        font-size: 0.875rem;
+        font-weight: 500;
+        color: #6b6b7a;
+        background: none;
+        border: none;
+        cursor: pointer;
+        font-family: inherit;
+        width: 100%;
+        transition: background 0.15s, color 0.15s;
+    }
+
+    .logout-btn:hover {
+        background: #14141f;
+        color: #e05555;
+    }
+
+    /* ── Mobile ── */
+    @media (max-width: 768px) {
+        .sidebar {
+            position: fixed;
+            top: 0;
+            left: 0;
+            z-index: 200;
+            width: 260px;
+            transform: translateX(-100%);
+            transition: transform 0.25s ease;
+            box-shadow: none;
+        }
+
+        .sidebar.open {
+            transform: translateX(0);
+            box-shadow: 4px 0 40px rgba(0, 0, 0, 0.5);
+        }
+
+        .close-btn { display: flex; }
+    }
+</style>