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 {
LOCALE_LABELS,
SUPPORTED_LOCALES,
isLocale,
resolveLocale,
slugifyWorkspaceName,
} from '@wellformed/shared';
import { api } from '../api/client';
import { useI18n } from '../i18n';
import { useSession } from '../state/session';
import { useDismissSsr } from '../state/ssr';
import { useToasts } from '../state/toasts';
import { goTo, workspacePath } from '../state/route';
/**
* Where a signed-in account lands when the URL names no workspace, or names one
* it cannot open. Both cases are the same question — which of these do you
* want? — so they get the same screen, with a line of explanation when the URL
* was the reason.
*/
export function WorkspacePicker({
unknownSlug,
startCreating = false,
}: {
unknownSlug?: string;
startCreating?: boolean;
}): ReactNode {
const session = useSession();
const toasts = useToasts();
const { t } = useI18n();
useDismissSsr();
const [creating, setCreating] = useState(startCreating || session.workspaces.length === 0);
const [name, setName] = useState('');
// Off by default: most people arrive with the spreadsheet they want to
// replace, and a demo on top of real intent is furniture in the way.
const [demoStructure, setDemoStructure] = useState(false);
const [demoData, setDemoData] = useState(false);
// Asked here, not guessed from a header: the language decides how every
// number and date in the workspace is written, and someone who imports
// "12-3-2021" should never first learn the answer from "3/12/2021".
const [locale, setLocale] = useState(() =>
resolveLocale(navigator.languages ?? [navigator.language]),
);
const [busy, setBusy] = useState(false);
async function create() {
setBusy(true);
try {
const { workspace } = await api.createWorkspace({
name: name.trim(),
demo: demoStructure ? (demoData ? 'full' : 'structure') : 'none',
locale,
});
goTo(workspacePath(workspace.slug));
} catch (error) {
toasts.fail(error);
setBusy(false);
}
}
return (
<div className="login">
<div className="login-card wide">
<div className="wordmark">
Well<span>formed</span>
</div>
<div className="tagline">
{session.workspaces.length ? t('picker.choose') : t('picker.first')}
</div>
{unknownSlug && (
<div className="formerror" role="alert">
<span className="mono">/w/{unknownSlug}</span>
{t('picker.unknownAfter', session.account?.email ?? '')}
</div>
)}
{session.workspaces.length > 0 && (
<ul className="wslist">
{session.workspaces.map((workspace) => (
<li key={workspace.id}>
<a href={workspacePath(workspace.slug)}>
<span className="wsname">{workspace.name}</span>
<span className="hint mono">/w/{workspace.slug}</span>
<span className="rolechip">{t(`role.${workspace.role}`)}</span>
</a>
</li>
))}
</ul>
)}
{creating ? (
<form
onSubmit={(e) => {
e.preventDefault();
void create();
}}
>
<label>
{t('picker.workspaceName')}
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Acme Marine"
required
autoFocus
/>
{name.trim() && (
<span className="hint">
{t('picker.address')} <span className="mono">/w/{slugifyWorkspaceName(name)}</span>
</span>
)}
</label>
<label>
{t('workspace.language')}
<select
value={locale}
onChange={(e) => {
if (isLocale(e.target.value)) setLocale(e.target.value);
}}
>
{SUPPORTED_LOCALES.map((loc) => (
<option key={loc} value={loc}>
{LOCALE_LABELS[loc]}
</option>
))}
</select>
<span className="hint">{t('workspace.languageHint')}</span>
</label>
{/* The demo is a property of creation, not of signup: it can only
ever be the first thing in a workspace. */}
<label className="checkrow">
<input
type="checkbox"
checked={demoStructure}
onChange={(e) => {
setDemoStructure(e.target.checked);
if (!e.target.checked) setDemoData(false);
}}
/>
<span>
{t('picker.demoStructure')}
<span className="hint">{t('picker.demoStructureHint')}</span>
</span>
</label>
{demoStructure && (
<label className="checkrow nested">
<input
type="checkbox"
checked={demoData}
onChange={(e) => setDemoData(e.target.checked)}
/>
<span>
{t('picker.demoData')}
<span className="hint">{t('picker.demoDataHint')}</span>
</span>
</label>
)}
<div className="rowend">
{session.workspaces.length > 0 && (
<button className="btn" type="button" onClick={() => setCreating(false)}>
{t('common.cancel')}
</button>
)}
<button className="btn primary" type="submit" disabled={busy || !name.trim()}>
{busy ? t('picker.creating') : t('picker.createIt')}
</button>
</div>
</form>
) : (
<button className="btn" onClick={() => setCreating(true)}>
{t('picker.new')}
</button>
)}
<p className="hint">
{t('account.signedInAs')} {session.account?.email}.{' '}
<button className="linkbtn" onClick={() => void session.signOut().then(() => goTo('/'))}>
{t('chrome.signOut')}
</button>
</p>
</div>
</div>
);
}