Spreadsheet-UX op een echte relationele engine

main

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

4.7 kB Ruw bestand
import { useState, type ReactNode } from 'react';
import { useQuery } from '@tanstack/react-query';
import { api } from '../api/client';
import { useI18n } from '../i18n';
import { useSession } from '../state/session';
import { goTo, workspacePath } from '../state/route';

/**
 * The other end of an invite link.
 *
 * Three cases, and the screen says which one it is rather than making the
 * person guess: they have no account yet and choose a password here; they have
 * one and are signed in, so it is a single confirmation; or they have one and
 * are not signed in, so they sign in first and come back.
 */
export function InviteScreen({ token }: { token: string }): ReactNode {
  const session = useSession();
  const { t } = useI18n();
  const [name, setName] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);

  const invite = useQuery({
    queryKey: ['invite', token],
    queryFn: () => api.readInvite(token),
    retry: false,
  });

  if (invite.isLoading) return <Card tagline={t('auth.checkingInvite')} />;

  if (invite.error) {
    return (
      <Card tagline={t('auth.inviteInvalidTagline')}>
        <p className="hint">
          {invite.error instanceof Error ? invite.error.message : t('auth.inviteLinkExpired')}
        </p>
        <a className="btn" href="/">
          {t('auth.goToWellformed')}
        </a>
      </Card>
    );
  }

  const info = invite.data!.invite;
  const signedInAsSomeoneElse = session.account && session.account.email !== info.email;

  async function accept() {
    setBusy(true);
    setError(null);
    try {
      const result = await api.acceptInvite(
        token,
        info.needsAccount ? { name: name.trim(), password } : {},
      );
      goTo(workspacePath(result.workspace.slug));
    } catch (err) {
      setError(err instanceof Error ? err.message : t('auth.acceptFailed'));
      setBusy(false);
    }
  }

  return (
    <Card tagline={t('auth.joinTagline', info.workspaceName)}>
      <p className="hint">
        {t('auth.invitedAsBefore')}
        <b>{info.email}</b>
        {t('auth.invitedAsMiddle')}
        <b>{t(`role.${info.role}`)}</b>
        {t('auth.invitedAsAfter')}
      </p>

      {error && (
        <div className="formerror" role="alert">
          {error}
        </div>
      )}

      {signedInAsSomeoneElse && (
        <>
          <p className="hint">
            {t('auth.wrongAccountBefore')}
            <b>{session.account!.email}</b>
            {t('auth.wrongAccountAfter')}
          </p>
          <button className="btn" onClick={() => void session.signOut().then(() => goTo(location.pathname))}>
            {t('auth.signOut')}
          </button>
        </>
      )}

      {!signedInAsSomeoneElse && info.needsAccount && (
        <form
          onSubmit={(e) => {
            e.preventDefault();
            void accept();
          }}
        >
          <label>
            {t('auth.yourName')}
            <input value={name} onChange={(e) => setName(e.target.value)} autoComplete="name" required autoFocus />
          </label>
          <label>
            {t('auth.choosePassword')}
            <input
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              autoComplete="new-password"
              required
            />
            {password.length > 0 && password.length < 10 && (
              <span className="hint bad">{t('account.tooShort')}</span>
            )}
          </label>
          <button className="btn primary" type="submit" disabled={busy || !name.trim() || password.length < 10}>
            {busy ? t('auth.joining') : t('auth.joinWorkspace', info.workspaceName)}
          </button>
        </form>
      )}

      {!signedInAsSomeoneElse && !info.needsAccount && session.account && (
        <button className="btn primary" onClick={() => void accept()} disabled={busy}>
          {busy ? t('auth.joining') : t('auth.joinWorkspace', info.workspaceName)}
        </button>
      )}

      {!signedInAsSomeoneElse && !info.needsAccount && !session.account && (
        <p className="hint">
          {info.email}
          {t('auth.alreadyAccountAfter')}
          <a href="/">{t('auth.signInLink')}</a>
          {t('auth.thenOpenAgain')}
        </p>
      )}
    </Card>
  );
}

function Card({ tagline, children }: { tagline: string; children?: ReactNode }): ReactNode {
  return (
    <div className="login">
      <div className="login-card">
        <div className="wordmark">
          Well<span>formed</span>
        </div>
        <div className="tagline">{tagline}</div>
        {children}
      </div>
    </div>
  );
}