Spreadsheet-UX op een echte relationele engine
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
import { useState, type ReactNode } from 'react';
import { useI18n } from '../i18n';
import { useSession } from '../state/session';
import { currentVoiceId } from '../state/voice';
import { goTo } from '../state/route';
/**
* Signup asks for three things because that is what an account is: a person,
* an address, a password. The workspace is deliberately not one of them —
* naming a workspace before ever having seen one produced names like "test" —
* so a fresh account lands in the picker, and the picker creates the first
* workspace with the choices that belong to it: the name, and whether to
* start from the demo.
*/
export function SignupScreen(): ReactNode {
const session = useSession();
const { t } = useI18n();
const [email, setEmail] = useState('');
const [name, setName] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const ready = email.trim() && name.trim() && password.length >= 10;
async function submit(event: React.FormEvent) {
event.preventDefault();
setBusy(true);
setError(null);
try {
await session.signUp({
email: email.trim(),
name: name.trim(),
password,
// Which landing page produced this, if any. Read at submit rather than
// held in state so arriving here straight from a bookmark — with no
// impression in the tab — simply omits it.
...(currentVoiceId() ? { voice: currentVoiceId() } : {}),
});
// No workspaces yet, so the router lands on the picker, mid-creation.
goTo('/');
} catch (err) {
setError(err instanceof Error ? err.message : t('auth.signupFailed'));
setBusy(false);
}
}
return (
<div className="login">
<div className="login-card wide">
<div className="wordmark">
Well<span>formed</span>
</div>
<div className="tagline">{t('auth.signupTagline')}</div>
<form onSubmit={(e) => void submit(e)}>
{error && (
<div className="formerror" role="alert">
{error}
</div>
)}
<label>
{t('auth.yourName')}
<input
value={name}
onChange={(e) => setName(e.target.value)}
autoComplete="name"
required
autoFocus
/>
</label>
<label>
{t('common.email')}
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
autoComplete="username"
required
/>
</label>
<label>
{t('common.password')}
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="new-password"
required
/>
{/* The rule stands before anyone types: a requirement that only
shows up as a rejection reads as one. */}
<span className={password.length > 0 && password.length < 10 ? 'hint bad' : 'hint'}>
{t('auth.passwordRule')}
</span>
</label>
<button className="btn primary" type="submit" disabled={busy || !ready}>
{busy ? t('auth.creating') : t('auth.createAccount')}
</button>
</form>
<p className="hint">
{t('auth.haveAccountBefore')}
<a href="/">{t('auth.signInLink')}</a>.
</p>
</div>
</div>
);
}