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 { 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>
);
}