From 2dd6402c86d9e4185a2d22fcc7b9f576e2d49bdd Mon Sep 17 00:00:00 2001 From: kitadmin Date: Thu, 30 Apr 2026 02:07:19 +0000 Subject: [PATCH] feat: React app shell with global styles and state-based routing (Task 8) - index.css: brand CSS custom properties, .btn, input styles, .app-shell layout - App.tsx: session token extraction from URL, /api/auth/me validation, 4-route shell - main.tsx: renders App with StrictMode Co-Authored-By: Claude Sonnet 4.6 --- client/src/App.tsx | 85 ++++++++++++++++++++++++++++++++++++++++++++ client/src/index.css | 63 ++++++++++++++++++++++++++++++++ client/src/main.tsx | 4 ++- 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 client/src/App.tsx create mode 100644 client/src/index.css diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..35dae06 --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,85 @@ +import { useState, useEffect } from 'react' +import './index.css' + +type Route = 'login' | 'onboarding' | 'game' | 'admin' + +export default function App() { + const [route, setRoute] = useState('login') + const [sessionToken, setSessionToken] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + // 1. Check for ?session= in URL (from magic link redirect) + const params = new URLSearchParams(window.location.search) + const urlToken = params.get('session') + if (urlToken) { + localStorage.setItem('session', urlToken) + window.history.replaceState({}, '', window.location.pathname) + } + + // 2. Try stored token + const stored = urlToken || localStorage.getItem('session') + if (!stored) { setLoading(false); return } + + // 3. Validate token with /api/auth/me + fetch('/api/auth/me', { + headers: { Authorization: `Bearer ${stored}` }, + }) + .then(r => { + if (r.ok) { + setSessionToken(stored) + // TODO in Task 9: check if onboarding complete, route accordingly + setRoute('game') + } else { + localStorage.removeItem('session') + } + }) + .catch(() => localStorage.removeItem('session')) + .finally(() => setLoading(false)) + }, []) + + if (loading) { + return ( +
+

+ Loading quest... +

+
+ ) + } + + return ( +
+ {route === 'login' && ( +
+

MorseQuest

+

Login page coming in Task 9.

+ +
+ )} + {route === 'onboarding' && ( +
+

Onboarding

+

Mnemonic builder coming in Task 10.

+
+ )} + {route === 'game' && ( +
+

Practice

+

Game loop coming in Task 11.

+ +
+ )} + {route === 'admin' && ( +
+

Admin

+

Admin panel coming in Task 12.

+
+ )} +
+ ) +} diff --git a/client/src/index.css b/client/src/index.css new file mode 100644 index 0000000..cb12d20 --- /dev/null +++ b/client/src/index.css @@ -0,0 +1,63 @@ +/* CSS custom properties */ +:root { + --color-gold: #D4AF37; + --color-blue: #4169E1; + --color-parchment: #F5E6D3; + --color-forest: #3C2415; + --color-brown: #654321; + --font-header: 'Cinzel', serif; + --font-body: 'Open Sans', sans-serif; + --font-mono: 'Source Code Pro', monospace; +} + +/* Reset + base */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +body { + font-family: var(--font-body); + background: var(--color-parchment); + color: var(--color-forest); + min-height: 100vh; +} + +/* Typography */ +h1, h2, h3 { font-family: var(--font-header); color: var(--color-forest); } +code, .mono { font-family: var(--font-mono); } + +/* Shared button style */ +.btn { + display: inline-block; + padding: 0.7rem 1.6rem; + background: var(--color-gold); + color: var(--color-forest); + border: 2px solid var(--color-forest); + font-family: var(--font-header); + font-size: 1rem; + font-weight: 600; + cursor: pointer; + text-decoration: none; +} +.btn:hover { background: var(--color-forest); color: var(--color-parchment); } +.btn-ghost { + background: transparent; + border-color: var(--color-gold); + color: var(--color-forest); +} + +/* Input */ +input[type="text"], input[type="email"] { + width: 100%; + padding: 0.6rem 0.8rem; + border: 2px solid var(--color-brown); + background: white; + font-family: var(--font-body); + font-size: 1rem; + color: var(--color-forest); +} +input:focus { outline: 2px solid var(--color-gold); outline-offset: 1px; } + +/* Layout shell */ +.app-shell { + max-width: 680px; + margin: 0 auto; + padding: 1rem; +} diff --git a/client/src/main.tsx b/client/src/main.tsx index a94e4fe..be4ff18 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,8 +1,10 @@ import React from 'react' import ReactDOM from 'react-dom/client' +import './index.css' +import App from './App' ReactDOM.createRoot(document.getElementById('root')!).render( -
MorseQuest loading...
+
)