122 lines
4.1 KiB
TypeScript
122 lines
4.1 KiB
TypeScript
// app/portal/account/AccountClient.tsx
|
||
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import Link from "next/link";
|
||
|
||
type Me = {
|
||
id: string;
|
||
username: string;
|
||
first_name?: string | null;
|
||
last_name?: string | null;
|
||
email?: string | null;
|
||
location?: string | null;
|
||
avatar?: { id: string; filename_download?: string } | null;
|
||
};
|
||
|
||
export default function AccountClient() {
|
||
const [me, setMe] = useState<Me | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [needReauth, setNeedReauth] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
async function load() {
|
||
setLoading(true);
|
||
setError(null);
|
||
setNeedReauth(false);
|
||
try {
|
||
const res = await fetch("/api/account", {
|
||
credentials: "include",
|
||
cache: "no-store",
|
||
});
|
||
if (res.status === 401 || res.status === 403) {
|
||
setNeedReauth(true);
|
||
setMe(null);
|
||
return;
|
||
}
|
||
if (!res.ok) {
|
||
const j = await res.json().catch(() => ({}));
|
||
throw new Error(j?.error || `Failed: ${res.status}`);
|
||
}
|
||
const j = await res.json();
|
||
setMe(j as Me);
|
||
} catch (e: any) {
|
||
setError(e?.message || "Failed to load account");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
useEffect(() => { load(); }, []);
|
||
|
||
if (loading) {
|
||
return <div className="p-6 text-sm opacity-70">Loading account…</div>;
|
||
}
|
||
|
||
if (needReauth) {
|
||
return (
|
||
<div className="p-6 max-w-xl">
|
||
<div className="rounded-md border p-4">
|
||
<h2 className="text-lg font-semibold mb-2">Confirm it’s you</h2>
|
||
<p className="text-sm text-muted-foreground mb-3">
|
||
For security, please sign in again before changing account details.
|
||
</p>
|
||
<Link
|
||
href={`/auth/sign-in?reauth=1&force=1&next=${encodeURIComponent("/portal/account")}`}
|
||
className="inline-block px-3 py-2 rounded-md border bg-accent text-background"
|
||
>
|
||
Re-authenticate
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="p-6 max-w-xl">
|
||
<div className="rounded-md border p-4">
|
||
<div className="text-red-600 mb-2">Error: {error}</div>
|
||
<button
|
||
onClick={load}
|
||
className="px-3 py-2 rounded-md border hover:bg-muted text-sm"
|
||
>
|
||
Retry
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!me) return null;
|
||
|
||
return (
|
||
<div className="p-6 max-w-2xl space-y-6">
|
||
{/* View-only header */}
|
||
<section className="rounded-md border p-4">
|
||
<h1 className="text-xl font-semibold mb-2">Account</h1>
|
||
<p className="text-sm">
|
||
<span className="font-medium">Username:</span> {me.username}{" "}
|
||
<span className="text-muted-foreground">(usernames can’t be changed)</span>
|
||
</p>
|
||
</section>
|
||
|
||
{/* Your existing edit forms can sit here, wired to:
|
||
- PATCH /api/account for first_name, last_name, email (optional), location
|
||
- POST /api/account/password for password change (asks current password in the form)
|
||
- POST /api/account/avatar for avatar upload (to your folder)
|
||
None of these fire automatically; only on submit. */}
|
||
|
||
{/* Example placeholder showing current profile fields */}
|
||
<section className="rounded-md border p-4">
|
||
<h2 className="text-lg font-semibold mb-3">Profile</h2>
|
||
<div className="grid gap-2 text-sm">
|
||
<div><span className="text-muted-foreground">First name:</span> {me.first_name || "—"}</div>
|
||
<div><span className="text-muted-foreground">Last name:</span> {me.last_name || "—"}</div>
|
||
<div><span className="text-muted-foreground">Email:</span> {me.email || "—"}</div>
|
||
<div><span className="text-muted-foreground">Location:</span> {me.location || "—"}</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|