feat: add timezone settings data export and account deletion
This commit is contained in:
46
src/components/AccountSettings.tsx
Normal file
46
src/components/AccountSettings.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useId, useRef, useState } from "react";
|
||||
import type { PublicUser } from "../shared/user";
|
||||
import { habitRequest } from "../lib/dashboard";
|
||||
import { Button, ButtonLink } from "./design-system/primitives";
|
||||
import { Card } from "./design-system/Card";
|
||||
import { Field } from "./design-system/Field";
|
||||
|
||||
export function AccountSettings({ user, onChanged, onClose }: { user: PublicUser; onChanged: () => void; onClose: () => void }) {
|
||||
const [timezone, setTimezone] = useState(user.timezone);
|
||||
const [confirmation, setConfirmation] = useState("");
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const saving = useRef(false);
|
||||
const [error, setError] = useState("");
|
||||
const listId = useId();
|
||||
async function run(action: () => Promise<unknown>) {
|
||||
if (saving.current) return;
|
||||
saving.current = true; setBusy(true); setError("");
|
||||
try { await action(); onChanged(); }
|
||||
catch (error) { setError(error instanceof Error ? error.message : "Could not save. Try again."); }
|
||||
finally { saving.current = false; setBusy(false); }
|
||||
}
|
||||
return <section className="ds-section" id="account-settings" aria-label="Account settings">
|
||||
<Card heading="Account settings" headingLevel={2}>
|
||||
<form onSubmit={event => { event.preventDefault(); void run(() => habitRequest("/me", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ timezone }) })); }}>
|
||||
<Field label="Timezone" hint="Daily check-ins follow this timezone. Earlier deadlines stay as recorded.">
|
||||
{(id, hintId) => <input id={id} aria-describedby={hintId} list={listId} value={timezone} required maxLength={100} disabled={busy} onChange={event => setTimezone(event.target.value)} />}
|
||||
</Field>
|
||||
<datalist id={listId}>{["UTC", ...Intl.supportedValuesOf("timeZone")].map(zone => <option key={zone} value={zone} />)}</datalist>
|
||||
<Button type="submit" disabled={busy}>Save timezone</Button>
|
||||
</form>
|
||||
<div className="ds-actions">
|
||||
<ButtonLink href="/api/account/export" download="minabot-export.json">Export my data</ButtonLink>
|
||||
<Button variant="text" disabled={busy} onClick={() => setDeleting(!deleting)} aria-expanded={deleting}>Delete account</Button>
|
||||
<Button variant="text" disabled={busy} onClick={onClose}>Close settings</Button>
|
||||
</div>
|
||||
<p className="type-small">Export includes your profile, habits, history, and sharing records. Sessions and credentials are excluded.</p>
|
||||
{deleting && <form onSubmit={event => { event.preventDefault(); void run(() => habitRequest("/account", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ confirmation }) })); }}>
|
||||
<p>This permanently deletes your account and habit history and signs out every device. Download an export first if you want a copy. Images already posted to Discord remain there; server backups expire under the operator’s retention policy.</p>
|
||||
<Field label="Type DELETE to confirm">{id => <input id={id} value={confirmation} disabled={busy} autoComplete="off" onChange={event => setConfirmation(event.target.value)} />}</Field>
|
||||
<div className="ds-actions"><Button type="submit" disabled={busy || confirmation !== "DELETE"}>Permanently delete my account</Button><Button variant="text" disabled={busy} onClick={() => { setDeleting(false); setConfirmation(""); }}>Cancel deletion</Button></div>
|
||||
</form>}
|
||||
{error && <p role="alert">{error}</p>}
|
||||
</Card>
|
||||
</section>;
|
||||
}
|
||||
Reference in New Issue
Block a user