소스 검색

Refactor game/character creation to one route

Lee Morgan 3 주 전
부모
커밋
b18f574da6

+ 55 - 0
src/lib/global.css

@@ -135,3 +135,58 @@
 .standardForm a:hover {
     text-decoration: underline;
 }
+
+.form-glow {
+    position: relative;
+    border-radius: 14px;
+    width: min(780px, 100%);
+    box-shadow: 0 0 0 1px rgba(192, 132, 252, 0.2),
+                0 0 40px rgba(192, 132, 252, 0.08),
+                0 0 80px rgba(90, 90, 255, 0.05);
+    animation: glow-pulse 4s ease-in-out infinite alternate;
+}
+
+.form-glow .standardForm {
+    max-width: 100%;
+    border-color: transparent;
+    overflow: hidden;
+    position: relative;
+}
+
+.form-glow .standardForm::before {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: -75%;
+    width: 50%;
+    height: 100%;
+    background: linear-gradient(
+        105deg,
+        transparent 20%,
+        rgba(255, 255, 255, 0.045) 50%,
+        transparent 80%
+    );
+    transform: skewX(-10deg);
+    animation: shimmer 6s ease-in-out infinite;
+    pointer-events: none;
+    z-index: 10;
+}
+
+@keyframes shimmer {
+    0%   { left: -75%; }
+    25%  { left: 125%; }
+    100% { left: 125%; }
+}
+
+@keyframes glow-pulse {
+    from {
+        box-shadow: 0 0 0 1px rgba(192, 132, 252, 0.15),
+                    0 0 30px rgba(192, 132, 252, 0.06),
+                    0 0 60px rgba(90, 90, 255, 0.04);
+    }
+    to {
+        box-shadow: 0 0 0 1px rgba(192, 132, 252, 0.35),
+                    0 0 50px rgba(192, 132, 252, 0.14),
+                    0 0 100px rgba(90, 90, 255, 0.08);
+    }
+}

+ 0 - 6
src/routes/(app)/game/[game_id]/character/new/+page.server.js

@@ -1,6 +0,0 @@
-import {PUBLIC_API_URL} from "$env/static/public";
-import {error} from "@sveltejs/kit";
-
-export async function load({params}){
-    return {params};
-}

+ 0 - 104
src/routes/(app)/game/[game_id]/character/new/+page.svelte

@@ -1,104 +0,0 @@
-<script>
-    import {page} from "$app/state";
-
-    let name = $state("");
-    let description = $state("");
-
-    const submit = async ()=>{
-        console.log("submit");
-    }
-</script>
-
-<div class="page">
-    <main>
-        <div class="heading">
-            <h1>Create Your Character</h1>
-            <p>Who will you be in this world?</p>
-        </div>
-
-        <form class="standardForm" onsubmit={submit}>
-            <label>Name
-                <input bind:value={name} type="text" placeholder="e.g. Aldric the Grey" required>
-            </label>
-
-            <label>Description
-                <textarea bind:value={description} placeholder="Describe your character's appearance, background, personality, or anything else that defines them…" rows="6" required></textarea>
-            </label>
-
-            <button type="submit">Create Character</button>
-        </form>
-    </main>
-</div>
-
-<style>
-    .page {
-        min-height: 100vh;
-        display: flex;
-        flex-direction: column;
-        background: #0b0b10;
-    }
-
-    header {
-        padding: 1.25rem 2rem;
-        border-bottom: 1px solid #1e1e28;
-    }
-
-    .back {
-        display: inline-flex;
-        align-items: center;
-        gap: 6px;
-        font-size: 0.85rem;
-        font-weight: 500;
-        color: #6b6b7a;
-        text-decoration: none;
-        transition: color 0.15s;
-    }
-
-    .back:hover { color: #a1a1aa; }
-
-    main {
-        flex: 1;
-        display: flex;
-        flex-direction: column;
-        align-items: center;
-        justify-content: center;
-        padding: 3rem 1.5rem 6rem;
-        gap: 2rem;
-    }
-
-    .heading {
-        text-align: center;
-        max-width: 480px;
-    }
-
-    h1 {
-        font-size: clamp(1.8rem, 4vw, 2.4rem);
-        font-weight: 800;
-        letter-spacing: -1.5px;
-        color: #e5e5ea;
-        margin-bottom: 0.5rem;
-    }
-
-    .heading p {
-        font-size: 0.95rem;
-        color: #6b6b7a;
-    }
-
-    .standardForm {
-        width: 100%;
-        max-width: 780px;
-        padding: 3rem;
-    }
-
-    textarea {
-        resize: vertical;
-        min-height: 220px;
-        line-height: 1.6;
-    }
-
-    @media (max-width: 700px) {
-        .standardForm { padding: 1.75rem; }
-        textarea { min-height: 140px; }
-        main { padding: 2rem 1.25rem 4rem; }
-    }
-</style>

+ 108 - 68
src/routes/(app)/game/new/+page.svelte

@@ -2,72 +2,96 @@
     import {PUBLIC_API_URL} from "$env/static/public";
     import {toast} from "$lib/toast.svelte.js";
     import {goto} from "$app/navigation";
+    import Game from "./Game.svelte";
+    import Character from "./Character.svelte";
+
+    let step = $state(1);
 
     let title = $state("");
     let gameContext = $state("");
 
-    const submit = async ()=>{
-        const body = {
-            title: title,
-            game_context: gameContext
-        };
+    let characterName = $state("");
+    let characterDescription = $state("");
+
+    const nextStep = (e) => {
+        e.preventDefault();
+        step = 2;
+    };
+
+    const submit = async (e) => {
+        e.preventDefault();
 
-        const response = await fetch(`${PUBLIC_API_URL}/game`, {
+        const gameRes = await fetch(`${PUBLIC_API_URL}/game`, {
             method: "POST",
             headers: {"Content-Type": "application/json"},
             credentials: "include",
-            body: JSON.stringify(body)
+            body: JSON.stringify({ title, game_context: gameContext })
         });
 
-        const data = await response.json();
-        if(!response.ok){
-            toast(data.error.message, "error");
-            if(response.status === 401){
-                goto("/");
-            }
-        }else{
-            toast("Game created");
-            goto(`/game/${data.id}/character/new`);
+        const gameData = await gameRes.json();
+        if (!gameRes.ok) {
+            toast(gameData.error.message, "error");
+            if (gameRes.status === 401) goto("/");
+            return;
         }
-    }
 
-    const autofocus = (node)=>{
-        node.focus();
-    }
+        const charRes = await fetch(`${PUBLIC_API_URL}/game/${gameData.id}/character`, {
+            method: "POST",
+            headers: {"Content-Type": "application/json"},
+            credentials: "include",
+            body: JSON.stringify({ name: characterName, description: characterDescription })
+        });
+
+        const charData = await charRes.json();
+        if (!charRes.ok) {
+            toast(charData.error.message, "error");
+            return;
+        }
+
+        toast("Adventure created!");
+        goto(`/game/${gameData.id}`);
+    };
 </script>
 
 <div class="page">
     <header>
-        <a href="/game" 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>
-            My Games
-        </a>
+        {#if step === 1}
+            <a href="/game" 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>
+                My Games
+            </a>
+        {:else}
+            <button class="back" onclick={() => step = 1}>
+                <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>
+                Back
+            </button>
+        {/if}
     </header>
 
     <main>
-        <div class="heading">
-            <h1>New Adventure</h1>
-            <p>Set the stage for your story</p>
+        <div class="step-indicator">
+            <div class="step-node" class:active={step >= 1} class:done={step > 1}>
+                {#if step > 1}
+                    <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
+                        <path d="M20 6L9 17l-5-5"/>
+                    </svg>
+                {:else}
+                    1
+                {/if}
+            </div>
+            <div class="step-line" class:active={step > 1}></div>
+            <div class="step-node" class:active={step >= 2}>2</div>
         </div>
 
-        <form class="standardForm" onsubmit={submit}>
-            <label>Title
-                <input bind:value={title} type="text" placeholder="e.g. The Lost Kingdom of Arath" required use:autofocus>
-            </label>
-
-            <label>Game Information
-                <textarea
-                    bind:value={gameContext}
-                    placeholder="Describe the world, setting, tone, or any context you want the AI to know about your adventure…"
-                    rows="6"
-                    required
-                ></textarea>
-            </label>
-
-            <button type="submit">Begin Adventure</button>
-        </form>
+        {#if step === 1}
+            <Game bind:title bind:gameContext onNext={nextStep} />
+        {:else}
+            <Character bind:name={characterName} bind:description={characterDescription} onSubmit={submit} />
+        {/if}
     </main>
 </div>
 
@@ -92,6 +116,11 @@
         font-weight: 500;
         color: #6b6b7a;
         text-decoration: none;
+        background: none;
+        border: none;
+        padding: 0;
+        cursor: pointer;
+        font-family: inherit;
         transition: color 0.15s;
     }
 
@@ -107,40 +136,51 @@
         gap: 2rem;
     }
 
-    .heading {
-        text-align: center;
-        max-width: 480px;
+    .step-indicator {
+        display: flex;
+        align-items: center;
+    }
+
+    .step-node {
+        width: 32px;
+        height: 32px;
+        border-radius: 50%;
+        border: 2px solid #2a2a38;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-size: 0.8rem;
+        font-weight: 600;
+        color: #44445a;
+        background: #0f0f18;
+        transition: border-color 0.3s, color 0.3s, background 0.3s;
+        flex-shrink: 0;
     }
 
-    h1 {
-        font-size: clamp(1.8rem, 4vw, 2.4rem);
-        font-weight: 800;
-        letter-spacing: -1.5px;
-        color: #e5e5ea;
-        margin-bottom: 0.5rem;
+    .step-node.active {
+        border-color: #c084fc;
+        color: #c084fc;
     }
 
-    .heading p {
-        font-size: 0.95rem;
-        color: #6b6b7a;
+    .step-node.done {
+        background: #c084fc;
+        border-color: #c084fc;
+        color: #0b0b10;
     }
 
-    .standardForm {
-        width: 100%;
-        max-width: 780px;
-        padding: 3rem;
+    .step-line {
+        width: 80px;
+        height: 2px;
+        background: #2a2a38;
+        transition: background 0.3s;
     }
 
-    textarea {
-        resize: vertical;
-        min-height: 280px;
-        line-height: 1.6;
-        font-size: 1rem;
+    .step-line.active {
+        background: #c084fc;
     }
 
     @media (max-width: 700px) {
-        .standardForm { padding: 1.75rem; }
-        textarea { min-height: 160px; }
         main { padding: 2rem 1.25rem 4rem; }
+        .step-line { width: 48px; }
     }
 </style>

+ 65 - 0
src/routes/(app)/game/new/Character.svelte

@@ -0,0 +1,65 @@
+<script>
+    let { name = $bindable(""), description = $bindable(""), onSubmit } = $props();
+
+    const autofocus = (node) => { node.focus(); };
+</script>
+
+<div class="heading">
+    <h1>Your Character</h1>
+    <p>Who will you be in this world?</p>
+</div>
+
+<div class="form-glow">
+<form class="standardForm" onsubmit={onSubmit}>
+    <label>Name
+        <input bind:value={name} type="text" placeholder="e.g. Aldric the Grey" required use:autofocus>
+    </label>
+
+    <label>Description
+        <textarea
+            bind:value={description}
+            placeholder="Describe your character's appearance, background, personality, or anything else that defines them…"
+            required
+        ></textarea>
+    </label>
+
+    <button type="submit">Begin Adventure</button>
+</form>
+</div>
+
+<style>
+    .heading {
+        text-align: center;
+        max-width: 480px;
+    }
+
+    h1 {
+        font-size: clamp(1.8rem, 4vw, 2.4rem);
+        font-weight: 800;
+        letter-spacing: -1.5px;
+        color: #e5e5ea;
+        margin-bottom: 0.5rem;
+    }
+
+    .heading p {
+        font-size: 0.95rem;
+        color: #6b6b7a;
+    }
+
+    .standardForm {
+        width: 100%;
+        padding: 3rem;
+    }
+
+    textarea {
+        resize: vertical;
+        min-height: 240px;
+        line-height: 1.6;
+        font-size: 1rem;
+    }
+
+    @media (max-width: 700px) {
+        .standardForm { padding: 1.75rem; }
+        textarea { min-height: 160px; }
+    }
+</style>

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

@@ -0,0 +1,65 @@
+<script>
+    let { title = $bindable(""), gameContext = $bindable(""), onNext } = $props();
+
+    const autofocus = (node) => { node.focus(); };
+</script>
+
+<div class="heading">
+    <h1>New Adventure</h1>
+    <p>Set the stage for your story.</p>
+</div>
+
+<div class="form-glow">
+<form class="standardForm" onsubmit={onNext}>
+    <label>Title
+        <input bind:value={title} type="text" placeholder="e.g. The Lost Kingdom of Arath" required use:autofocus>
+    </label>
+
+    <label>Game Information
+        <textarea
+            bind:value={gameContext}
+            placeholder="Describe the world, setting, tone, or any context you want the AI to know about your adventure…"
+            required
+        ></textarea>
+    </label>
+
+    <button type="submit">Next &rarr;</button>
+</form>
+</div>
+
+<style>
+    .heading {
+        text-align: center;
+        max-width: 480px;
+    }
+
+    h1 {
+        font-size: clamp(1.8rem, 4vw, 2.4rem);
+        font-weight: 800;
+        letter-spacing: -1.5px;
+        color: #e5e5ea;
+        margin-bottom: 0.5rem;
+    }
+
+    .heading p {
+        font-size: 0.95rem;
+        color: #6b6b7a;
+    }
+
+    .standardForm {
+        width: 100%;
+        padding: 3rem;
+    }
+
+    textarea {
+        resize: vertical;
+        min-height: 240px;
+        line-height: 1.6;
+        font-size: 1rem;
+    }
+
+    @media (max-width: 700px) {
+        .standardForm { padding: 1.75rem; }
+        textarea { min-height: 160px; }
+    }
+</style>

+ 0 - 54
src/routes/login/+page.svelte

@@ -94,58 +94,4 @@
         text-align: center;
     }
 
-    .form-glow {
-        position: relative;
-        border-radius: 14px;
-        width: min(660px, 100%);
-        box-shadow: 0 0 0 1px rgba(192, 132, 252, 0.2),
-                    0 0 40px rgba(192, 132, 252, 0.08),
-                    0 0 80px rgba(90, 90, 255, 0.05);
-        animation: glow-pulse 4s ease-in-out infinite alternate;
-    }
-
-    .form-glow .standardForm {
-        max-width: 660px;
-        border-color: transparent;
-        overflow: hidden;
-        position: relative;
-    }
-
-    .form-glow .standardForm::before {
-        content: "";
-        position: absolute;
-        top: 0;
-        left: -75%;
-        width: 50%;
-        height: 100%;
-        background: linear-gradient(
-            105deg,
-            transparent 20%,
-            rgba(255, 255, 255, 0.045) 50%,
-            transparent 80%
-        );
-        transform: skewX(-10deg);
-        animation: shimmer 6s ease-in-out infinite;
-        pointer-events: none;
-        z-index: 10;
-    }
-
-    @keyframes shimmer {
-        0%   { left: -75%; }
-        25%  { left: 125%; }
-        100% { left: 125%; }
-    }
-
-    @keyframes glow-pulse {
-        from {
-            box-shadow: 0 0 0 1px rgba(192, 132, 252, 0.15),
-                        0 0 30px rgba(192, 132, 252, 0.06),
-                        0 0 60px rgba(90, 90, 255, 0.04);
-        }
-        to {
-            box-shadow: 0 0 0 1px rgba(192, 132, 252, 0.35),
-                        0 0 50px rgba(192, 132, 252, 0.14),
-                        0 0 100px rgba(90, 90, 255, 0.08);
-        }
-    }
 </style>

+ 0 - 54
src/routes/register/+page.svelte

@@ -110,58 +110,4 @@
         text-align: center;
     }
 
-    .form-glow {
-        position: relative;
-        border-radius: 14px;
-        width: min(660px, 100%);
-        box-shadow: 0 0 0 1px rgba(192, 132, 252, 0.2),
-                    0 0 40px rgba(192, 132, 252, 0.08),
-                    0 0 80px rgba(90, 90, 255, 0.05);
-        animation: glow-pulse 4s ease-in-out infinite alternate;
-    }
-
-    .form-glow .standardForm {
-        max-width: 660px;
-        border-color: transparent;
-        overflow: hidden;
-        position: relative;
-    }
-
-    .form-glow .standardForm::before {
-        content: "";
-        position: absolute;
-        top: 0;
-        left: -75%;
-        width: 50%;
-        height: 100%;
-        background: linear-gradient(
-            105deg,
-            transparent 20%,
-            rgba(255, 255, 255, 0.045) 50%,
-            transparent 80%
-        );
-        transform: skewX(-10deg);
-        animation: shimmer 6s ease-in-out infinite;
-        pointer-events: none;
-        z-index: 10;
-    }
-
-    @keyframes shimmer {
-        0%   { left: -75%; }
-        25%  { left: 125%; }
-        100% { left: 125%; }
-    }
-
-    @keyframes glow-pulse {
-        from {
-            box-shadow: 0 0 0 1px rgba(192, 132, 252, 0.15),
-                        0 0 30px rgba(192, 132, 252, 0.06),
-                        0 0 60px rgba(90, 90, 255, 0.04);
-        }
-        to {
-            box-shadow: 0 0 0 1px rgba(192, 132, 252, 0.35),
-                        0 0 50px rgba(192, 132, 252, 0.14),
-                        0 0 100px rgba(90, 90, 255, 0.08);
-        }
-    }
 </style>