Files
Spendlight/web/src/pages/SetupPage.tsx
Chris Olson c3cf0f3eb8 Milestones 0 and 1: foundation and money in (20261007.10)
Foundation: single arm64 container (Hono + SQLite via Drizzle, React + Vite),
local sign-in with long-lived sessions and passkeys, roles (read-only, full
access, admin), nightly and pre-upgrade backups, light and dark themes,
date-based version numbers, and the ideas lightbulb behind IDEAS_ENABLED.

Money in: accounts with balances, CSV import with bank detection, a column
mapping pop-up for unknown layouts, duplicate checks, transfer pairing and
undo; one-tap review with learned rules; activity with search, splits, tags,
notes and cash entries. Statement files are blocked from commits by a
pre-commit hook; tests use synthetic fixtures only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:52:07 -04:00

75 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Me } from '@spendlight/shared';
import { useQueryClient } from '@tanstack/react-query';
import { useState, type FormEvent } from 'react';
import { Navigate, useNavigate } from 'react-router';
import { toast } from 'sonner';
import { AuthLayout, FormError } from '@/components/AuthLayout';
import { Splash } from '@/components/RequireAuth';
import { Button } from '@/components/ui/button';
import { Field, Input, PasswordInput } from '@/components/ui/form';
import { api, errorMessage } from '@/lib/api';
import { useConfig } from '@/lib/session';
export default function SetupPage() {
const config = useConfig();
const client = useQueryClient();
const navigate = useNavigate();
const [displayName, setDisplayName] = useState('');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
if (config.isPending) return <Splash />;
if (config.data && !config.data.needsSetup) return <Navigate to="/" replace />;
const submit = async (e: FormEvent) => {
e.preventDefault();
setBusy(true);
setError(null);
try {
const me = await api<Me>('/setup', { method: 'POST', body: { displayName, username, password } });
client.setQueryData(['me'], me);
await client.invalidateQueries({ queryKey: ['config'] });
toast.success(`Welcome to Spendlight, ${me.displayName}!`);
navigate('/', { replace: true });
} catch (err) {
setError(errorMessage(err));
} finally {
setBusy(false);
}
};
return (
<AuthLayout title="Welcome to Spendlight" subtitle="Let’s create your sign-in. You’ll be the admin, so you can invite the rest of the household later.">
<form onSubmit={submit} className="grid gap-4">
<FormError message={error} />
<Field label="Your name">
{(id) => <Input id={id} value={displayName} onChange={(e) => setDisplayName(e.target.value)} autoComplete="name" placeholder="Chris" required />}
</Field>
<Field label="Username" hint="What you’ll type to sign in. Letters and numbers work best.">
{(id) => (
<Input
id={id}
value={username}
onChange={(e) => setUsername(e.target.value)}
autoComplete="username"
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
placeholder="chris"
required
/>
)}
</Field>
<Field label="Password" hint="At least 8 characters. A short phrase is easy to remember.">
{(id) => <PasswordInput id={id} value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="new-password" minLength={8} required />}
</Field>
<Button type="submit" size="lg" loading={busy} className="mt-1 w-full">
Create and continue
</Button>
</form>
</AuthLayout>
);
}