- Replaced HABIT_PALETTES with HABIT_COLOR_CHOICES in HabitColorPicker component and related tests. - Adjusted the layout and styles of the color picker for improved usability. - Updated tests to reflect changes in color selection logic. - Modified WorkspacePanel and SettingRow components to allow optional eyebrow and description props. - Enhanced accessibility tests for calendar navigation. - Updated calendar model to include a 1-month view option. - Refined styles across various components for consistency and improved responsiveness.
54 lines
4.4 KiB
TypeScript
54 lines
4.4 KiB
TypeScript
import { ThemeControl } from "./ThemeProvider";
|
||
import { ReminderSettings } from "./ReminderSettings";
|
||
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 { Download, Trash2 } from "lucide-react";
|
||
import { FormMessage, SettingRow, WorkspacePanel } from "./design-system/WorkspacePanel";
|
||
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 [reminderBusy, setReminderBusy] = useState(false);
|
||
const saving = useRef(false);
|
||
const [error, setError] = useState("");
|
||
const listId = useId();
|
||
async function run(action: () => Promise<unknown>) {
|
||
if (saving.current || reminderBusy) 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 <WorkspacePanel id="account-settings" title="Account settings" onClose={onClose} busy={busy || reminderBusy} closeLabel="Close settings">
|
||
<SettingRow title="Appearance"><ThemeControl /></SettingRow>
|
||
<SettingRow title="Daily timezone" description={<p>Sets day boundaries. Earlier deadlines stay unchanged.</p>}>
|
||
<form className="ds-form-stack" onSubmit={event => { event.preventDefault(); void run(() => habitRequest("/me", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ timezone }) })); }}>
|
||
<Field label="Timezone">
|
||
{id => <input id={id} list={listId} value={timezone} required maxLength={100} disabled={busy || reminderBusy} onChange={event => setTimezone(event.target.value)} />}
|
||
</Field>
|
||
<datalist id={listId}>{["UTC", ...Intl.supportedValuesOf("timeZone")].map(zone => <option key={zone} value={zone} />)}</datalist>
|
||
<div className="ds-action-row"><Button type="submit" variant="secondary" disabled={busy || reminderBusy}>{busy && !deleting ? "Saving…" : "Save timezone"}</Button></div>
|
||
</form>
|
||
</SettingRow>
|
||
<ReminderSettings timezone={user.timezone} onBusyChange={setReminderBusy} disabled={busy} />
|
||
<SettingRow title="Export data" description={<p>Download your profile, habits, and history.</p>}>
|
||
<div className="ds-action-row"><ButtonLink variant="secondary" href="/api/account/export" download="mina-habits-export.json"><Download size={16} aria-hidden="true" />Export my data</ButtonLink><span className="ds-meta-label">JSON file</span></div>
|
||
</SettingRow>
|
||
<SettingRow title="Delete account" description={<p>Permanently remove your account and habit history, and sign out every device.</p>}>
|
||
{!deleting ? <div className="ds-action-row"><Button variant="text" disabled={busy || reminderBusy} onClick={() => setDeleting(true)} aria-expanded={false}><Trash2 size={16} aria-hidden="true" />Delete account</Button></div> :
|
||
<form className="ds-form-stack ds-confirmation" onSubmit={event => { event.preventDefault(); void run(() => habitRequest("/account", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ confirmation }) })); }}>
|
||
<p className="type-ui-heading">This cannot be undone.</p>
|
||
<p className="ds-supporting-copy">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 || reminderBusy} autoComplete="off" onChange={event => setConfirmation(event.target.value)} />}</Field>
|
||
<div className="ds-action-row"><Button type="submit" disabled={busy || reminderBusy || confirmation !== "DELETE"}>Permanently delete my account</Button><Button variant="text" disabled={busy || reminderBusy} onClick={() => { setDeleting(false); setConfirmation(""); }}>Cancel deletion</Button></div>
|
||
</form>}
|
||
</SettingRow>
|
||
<FormMessage error>{error}</FormMessage>
|
||
</WorkspacePanel>;
|
||
}
|