Spreadsheet-UX op een echte relationele engine

main

Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.

3.6 kB Ruw bestand
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>
  );
}