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 { api } from '../api/client';
import { usePublicConfig } from '../api/hooks';
import { useI18n } from '../i18n';
import { useSession } from '../state/session';
import { useToasts } from '../state/toasts';
import { Modal } from './Modal';
export function AccountDialog({ onClose }: { onClose(): void }): ReactNode {
const session = useSession();
const toasts = useToasts();
const install = usePublicConfig();
const { t } = useI18n();
const [current, setCurrent] = useState('');
const [next, setNext] = useState('');
const [confirm, setConfirm] = useState('');
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [sending, setSending] = useState(false);
// Only worth asking about where a confirmation could actually be delivered.
const askToConfirm =
install.data?.emailEnabled === true && session.account?.verified === false;
async function sendConfirmation() {
setSending(true);
try {
await api.sendVerification();
toasts.push(t('account.confirmationSent', session.account?.email ?? ''), 'success');
} catch (err) {
toasts.fail(err);
} finally {
setSending(false);
}
}
const mismatch = confirm.length > 0 && next !== confirm;
const canSave = !busy && current.length > 0 && next.length >= 10 && next === confirm;
async function save() {
setBusy(true);
setError(null);
try {
await api.changePassword(current, next);
toasts.push(t('account.passwordChanged'), 'success');
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : t('account.passwordChangeFailed'));
} finally {
setBusy(false);
}
}
return (
<Modal
title={t('account.title')}
onClose={onClose}
footer={
<>
<button className="btn" onClick={onClose} disabled={busy}>
{t('common.close')}
</button>
<button className="btn primary" onClick={() => void save()} disabled={!canSave}>
{busy ? t('common.saving') : t('account.changePassword')}
</button>
</>
}
>
<div className="mb">
<div className="hint">
{t('account.signedInAs')} <b>{session.account?.email}</b>
{session.workspace &&
t('account.roleIn', t(`role.${session.workspace.role}`), session.workspace.name)}
.
</div>
{session.workspaces.length > 1 && (
<div className="hint">{t('account.otherWorkspaces', session.workspaces.length - 1)}</div>
)}
{askToConfirm && (
<div className="notice">
<div className="hint">{t('account.unconfirmed')}</div>
<div className="rowend">
<button className="btn" onClick={() => void sendConfirmation()} disabled={sending}>
{sending ? t('common.sending') : t('account.sendConfirmation')}
</button>
</div>
</div>
)}
{error && <div className="formerror">{error}</div>}
<label>
{t('account.currentPassword')}
<input
type="password"
value={current}
onChange={(e) => setCurrent(e.target.value)}
autoComplete="current-password"
/>
</label>
<label>
{t('account.newPassword')}
<input
type="password"
value={next}
onChange={(e) => setNext(e.target.value)}
autoComplete="new-password"
/>
{next.length > 0 && next.length < 10 && (
<span className="hint bad">{t('account.tooShort')}</span>
)}
</label>
<label>
{t('account.confirmNewPassword')}
<input
type="password"
value={confirm}
onChange={(e) => setConfirm(e.target.value)}
autoComplete="new-password"
/>
{mismatch && <span className="hint bad">{t('account.mismatch')}</span>}
</label>
<div className="hint">{t('account.signsOutOthers')}</div>
</div>
</Modal>
);
}