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