scoreboardtools/apps/web/src/pages/Login.tsx
Ashraf Shafiq 9169bea79f Scaffold real-time SSBU scoreboard PWA
pnpm monorepo with three workspaces:
- @sbt/shared: zod ScoreboardState + WebSocket protocol (single source of truth)
- @sbt/server: Fastify REST + raw ws WebSocket + Drizzle/Postgres, run via tsx
- @sbt/web: React + Vite + Tailwind installable PWA

Real-time core: the WebSocket server holds authoritative per-board state in
memory, broadcasts to all clients (editors + OBS overlays) instantly, and
debounces Postgres saves (~750ms). One useScoreboardSync hook powers the editor,
the no-login co-edit control page, and the read-only OBS overlay.

Includes email+password auth (JWT cookie), scoreboard CRUD, logo upload,
customizable scorebug (characters/stocks/score/subtitles/callout/side-swap/theme),
Docker Compose + Caddy/nginx deploy configs, and docs/PLAN.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 00:09:27 -04:00

65 lines
2.2 KiB
TypeScript

import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../auth/AuthContext';
import { Button, Card, Field, Input } from '../components/ui';
export function Login() {
return <AuthForm mode="login" />;
}
export function Signup() {
return <AuthForm mode="signup" />;
}
function AuthForm({ mode }: { mode: 'login' | 'signup' }) {
const { login, signup } = useAuth();
const nav = useNavigate();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [busy, setBusy] = useState(false);
const submit = async (e: React.FormEvent) => {
e.preventDefault();
setBusy(true);
setError('');
try {
await (mode === 'login' ? login(email, password) : signup(email, password));
nav('/');
} catch (err) {
setError((err as Error).message);
} finally {
setBusy(false);
}
};
return (
<div className="grid min-h-screen place-items-center bg-ink p-4 text-white">
<Card className="w-full max-w-sm">
<h1 className="mb-1 text-xl font-bold">Scoreboard Tools</h1>
<p className="mb-4 text-sm text-slate-400">
{mode === 'login' ? 'Sign in to your dashboard' : 'Create an account'}
</p>
<form onSubmit={submit} className="flex flex-col gap-3">
<Field label="Email">
<Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
</Field>
<Field label="Password">
<Input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required minLength={6} />
</Field>
{error && <p className="text-sm text-red-400">{error}</p>}
<Button type="submit" variant="primary" disabled={busy}>
{busy ? '…' : mode === 'login' ? 'Sign in' : 'Sign up'}
</Button>
</form>
<p className="mt-4 text-center text-sm text-slate-400">
{mode === 'login' ? (
<>No account? <Link className="text-red-400" to="/signup">Sign up</Link></>
) : (
<>Have an account? <Link className="text-red-400" to="/login">Sign in</Link></>
)}
</p>
</Card>
</div>
);
}