Files
provenance/frontend/app/settings/page.tsx
T
justin 0262ed3d97 Account menu + Settings (change password); per-tree home person; full-width tree
- Sidebar bottom-left now shows the signed-in user; clicking opens a menu with
  Settings and Sign out. New /settings page: account info + change password
  (POST /auth/change-password, re-verifies current password). Export/restore/
  delete are stubbed there for the next pass.
- Per-tree default/home person: tree.home_person_id (migration) + TreeUpdate/
  Read; the tree and family views open focused on it; the person page gets a
  "Set as default" control and "Default person" badge. Cleared if that person
  is deleted. Complements the account-level "this is me" link.
- Tree visualization now fills the content area (AppShell drops the max-width
  column on the /tree route); other pages stay centered.
- Audit records are coerced JSON-safe (UUIDs/enums), so PATCHing UUID fields
  like home_person_id audits cleanly.

50 backend tests pass; migration up/down verified; frontend builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 11:05:04 -04:00

121 lines
3.7 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { api } from "@/lib/api/client";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
export default function SettingsPage() {
const [me, setMe] = useState<{ display_name: string | null; email: string } | null>(null);
const [current, setCurrent] = useState("");
const [next, setNext] = useState("");
const [confirm, setConfirm] = useState("");
const [msg, setMsg] = useState<{ kind: "ok" | "err"; text: string } | null>(null);
const [busy, setBusy] = useState(false);
useEffect(() => {
api.GET("/api/v1/users/me").then((r) => setMe(r.data ?? null));
}, []);
async function changePassword(e: React.FormEvent) {
e.preventDefault();
setMsg(null);
if (next.length < 8) {
setMsg({ kind: "err", text: "New password must be at least 8 characters." });
return;
}
if (next !== confirm) {
setMsg({ kind: "err", text: "New passwords don't match." });
return;
}
setBusy(true);
const { error } = await api.POST("/api/v1/auth/change-password", {
body: { current_password: current, new_password: next },
});
setBusy(false);
if (error) {
setMsg({ kind: "err", text: "Current password is incorrect." });
return;
}
setCurrent("");
setNext("");
setConfirm("");
setMsg({ kind: "ok", text: "Password changed." });
}
return (
<div className="space-y-6">
<h1 className="text-2xl font-semibold">Settings</h1>
<Card>
<CardHeader>
<CardTitle className="text-base">Account</CardTitle>
</CardHeader>
<CardContent className="space-y-1 text-sm">
<div>
<span className="text-[var(--muted)]">Name: </span>
{me?.display_name ?? "—"}
</div>
<div>
<span className="text-[var(--muted)]">Email: </span>
{me?.email ?? "—"}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Change password</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={changePassword} className="flex max-w-sm flex-col gap-3">
<Input
type="password"
placeholder="Current password"
autoComplete="current-password"
value={current}
onChange={(e) => setCurrent(e.target.value)}
/>
<Input
type="password"
placeholder="New password (min 8 chars)"
autoComplete="new-password"
value={next}
onChange={(e) => setNext(e.target.value)}
/>
<Input
type="password"
placeholder="Confirm new password"
autoComplete="new-password"
value={confirm}
onChange={(e) => setConfirm(e.target.value)}
/>
{msg && (
<p className={msg.kind === "ok" ? "text-sm text-bronze" : "text-sm text-red-600"}>
{msg.text}
</p>
)}
<Button type="submit" disabled={busy || !current || !next}>
{busy ? "Saving…" : "Change password"}
</Button>
</form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Your data</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-[var(--muted)]">
Full-account export, restore, and account deletion are coming next.
</p>
</CardContent>
</Card>
</div>
);
}