0262ed3d97
- 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>
121 lines
3.7 KiB
TypeScript
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>
|
|
);
|
|
}
|