feat: Sprint 1 slice 1 — API signup + web signup/chat shell
Mirror to GitHub / mirror (push) Canceled after 0s

- api: Fastify POST /auth/signup (validation, scrypt hash, in-memory store)
- web: React+Vite SignUp form and chat shell with dev proxy
- 35 tests green (api 5, web 2, e2e 28)
This commit is contained in:
SinachPat
2026-08-15 19:53:13 +01:00
parent 2f53b1901b
commit 9733ba6e3c
21 changed files with 1706 additions and 14 deletions
View File
+36
View File
@@ -0,0 +1,36 @@
import { useState } from 'react';
import { SignUp } from './components/SignUp.tsx';
async function signUp(email: string, password: string): Promise<void> {
const res = await fetch('/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
const body = (await res.json().catch(() => null)) as { error?: string } | null;
throw new Error(body?.error ?? 'Sign up failed');
}
}
export function App() {
const [signedUp, setSignedUp] = useState(false);
if (signedUp) {
return (
<div className="wursor-welcome">
<p>Describe what you want.</p>
<input className="wursor-chat-input" placeholder="Describe what you want…" />
</div>
);
}
return (
<SignUp
onSignUp={async (email, password) => {
await signUp(email, password);
setSignedUp(true);
}}
/>
);
}
+42
View File
@@ -0,0 +1,42 @@
import { useState, type FormEvent } from 'react';
type SignUpProps = {
onSignUp: (email: string, password: string) => Promise<void>;
};
export function SignUp({ onSignUp }: SignUpProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
async function submit(event: FormEvent) {
event.preventDefault();
setError(null);
setSubmitting(true);
try {
await onSignUp(email, password);
} catch (err) {
setError(err instanceof Error ? err.message : 'Something went wrong');
} finally {
setSubmitting(false);
}
}
return (
<form className="wursor-signup" onSubmit={submit}>
<input name="email" type="email" aria-label="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
<input
name="password"
type="password"
aria-label="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{error ? <p role="alert">{error}</p> : null}
<button type="submit" disabled={submitting}>
Sign up
</button>
</form>
);
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App.tsx';
import './styles/global.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
+8
View File
@@ -0,0 +1,8 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, -apple-system, sans-serif;
}
+7
View File
@@ -0,0 +1,7 @@
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
afterEach(() => {
cleanup();
});