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>
This commit is contained in:
74
web/src/pages/SetupPage.tsx
Normal file
74
web/src/pages/SetupPage.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user