Spreadsheet-UX op een echte relationele engine

main

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

3.7 kB Ruw bestand
import { useState, type ReactNode } from 'react';
import { ApiError } from '../api/client';
import { usePublicConfig } from '../api/hooks';
import { useI18n } from '../i18n';
import { useSession } from '../state/session';
import { useDismissSsr } from '../state/ssr';
import { goTo, workspacePath } from '../state/route';

/**
 * The sign-in card, and on an install that is licensed rather than sold, the
 * page you land on at the root as well — see `FrontDoor`. So it has to answer
 * the question a stranger arrives with on its own, without a page in front of
 * it: how do I get in, and what do I do if I cannot.
 */
export function LoginScreen(): ReactNode {
  const session = useSession();
  const install = usePublicConfig();
  const { t } = useI18n();
  useDismissSsr();
  const signupsOpen = install.data?.signupsOpen ?? true;
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    setBusy(true);
    setError(null);
    try {
      const workspaces = await session.signIn(email.trim(), password);
      // Signing in at `/w/acme` re-renders straight into it. Signing in at `/`
      // does not, and showing someone with exactly one workspace a list of one
      // is a step with nothing in it.
      const only = workspaces.length === 1 ? workspaces[0] : undefined;
      if (only && !window.location.pathname.startsWith('/w/')) {
        goTo(workspacePath(only.slug));
        return;
      }
    } catch (err) {
      setError(
        err instanceof ApiError && err.status === 401
          ? t('auth.badCredentials')
          : err instanceof Error
            ? err.message
            : t('auth.signInFailed'),
      );
      setPassword('');
    } finally {
      setBusy(false);
    }
  }

  return (
    <div className="login">
      <div className="login-card">
        <div className="wordmark">
          Well<span>formed</span>
        </div>
        <div className="tagline">{t('auth.loginTagline')}</div>

        <form onSubmit={(e) => void submit(e)}>
          {error && (
            <div className="formerror" role="alert">
              {error}
            </div>
          )}

          <label>
            {t('common.email')}
            <input
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              autoComplete="username"
              required
              autoFocus
            />
          </label>

          <label>
            {t('common.password')}
            <input
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              autoComplete="current-password"
              required
            />
          </label>

          <button className="btn primary" type="submit" disabled={busy}>
            {busy ? t('auth.signingIn') : t('auth.signIn')}
          </button>
        </form>

        <p className="hint">
          <a href="/forgot">{t('auth.forgotPassword')}</a>
        </p>

        {/*
          An invitation-only install has nowhere to send someone who has no
          account, so it says that plainly rather than offering a link that
          answers 403. The person who runs it is the way in.
        */}
        <p className="hint">
          {signupsOpen ? (
            <>
              {t('auth.noAccountBefore')}
              <a href="/signup">{t('auth.noAccountLink')}</a>.
            </>
          ) : (
            t('auth.inviteOnly')
          )}
        </p>
      </div>
    </div>
  );
}