diff --git a/app/portal/account/AccountClient.tsx b/app/portal/account/AccountClient.tsx index b731d6a2..3b08dfc5 100644 --- a/app/portal/account/AccountClient.tsx +++ b/app/portal/account/AccountClient.tsx @@ -1,7 +1,8 @@ // app/portal/account/AccountClient.tsx "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import Link from "next/link"; type Me = { id: string; @@ -10,144 +11,112 @@ type Me = { last_name?: string | null; email?: string | null; location?: string | null; - avatar?: { id: string; filename_download?: string } | string | null; + avatar?: { id: string; filename_download?: string } | null; }; -export default function AccountClient({ me }: { me: Me }) { - // local edit state; no network until you press Save - const [first, setFirst] = useState(me.first_name ?? ""); - const [last, setLast] = useState(me.last_name ?? ""); - const [email, setEmail] = useState(me.email ?? ""); - const [location, setLocation] = useState(me.location ?? ""); - const [busy, setBusy] = useState(false); - const [msg, setMsg] = useState(null); +export default function AccountClient() { + const [me, setMe] = useState(null); + const [loading, setLoading] = useState(true); + const [needReauth, setNeedReauth] = useState(false); + const [error, setError] = useState(null); - const avatarId = - typeof me.avatar === "string" ? me.avatar : (me.avatar as any)?.id ?? null; - - async function saveProfile(e: React.FormEvent) { - e.preventDefault(); - setBusy(true); - setMsg(null); + async function load() { + setLoading(true); + setError(null); + setNeedReauth(false); try { const res = await fetch("/api/account", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, credentials: "include", - body: JSON.stringify({ - first_name: first || null, - last_name: last || null, - email: email || null, // email optional per your model - location: location || null, - }), + cache: "no-store", }); - const j = await res.json().catch(() => ({})); - if (!res.ok) throw new Error(j?.error || "Update failed"); - setMsg("Saved!"); + 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) { - const m = String(e?.message || "Update failed"); - setMsg(m.includes("sign in") ? "Session expired — please sign in again." : m); + setError(e?.message || "Failed to load account"); } finally { - setBusy(false); + setLoading(false); } } + useEffect(() => { load(); }, []); + + if (loading) { + return
Loading account…
; + } + + if (needReauth) { + return ( +
+
+

Confirm it’s you

+

+ For security, please sign in again before changing account details. +

+ + Re-authenticate + +
+
+ ); + } + + if (error) { + return ( +
+
+
Error: {error}
+ +
+
+ ); + } + + if (!me) return null; + return ( -
-
-

Profile

- -
-
- - -

- Usernames can’t be changed. +

+ {/* View-only header */} +
+

Account

+

+ Username: {me.username}{" "} + (usernames can’t be changed)

-
- -
-
- {avatarId ? ( - // You already serve files via Directus; swap URL builder if needed - Avatar - ) : ( - No avatar - )} -
-
{ - // no-op placeholder so the input is controlled by browser until submit - }} - onSubmit={async (e) => { - e.preventDefault(); - const input = (e.currentTarget.elements.namedItem("avatar") as HTMLInputElement) ?? null; - const file = input?.files?.[0]; - if (!file) return; - setBusy(true); - setMsg(null); - try { - const fd = new FormData(); - fd.append("file", file); - const res = await fetch("/api/account/avatar", { - method: "POST", - credentials: "include", - body: fd, - }); - const j = await res.json().catch(() => ({})); - if (!res.ok) throw new Error(j?.error || "Avatar upload failed"); - setMsg("Avatar updated!"); - } catch (e: any) { - setMsg(String(e?.message || "Avatar upload failed")); - } finally { - setBusy(false); - (e.currentTarget as HTMLFormElement).reset(); - } - }} - > - - -
-
-
- -
-
- - setFirst(e.target.value)} /> -
-
- - setLast(e.target.value)} /> -
-
- - setEmail(e.target.value)} /> -
-
- - setLocation(e.target.value)} /> -
- -
- - {msg && {msg}} -
-
-
-

Change Password

- {/* render your ConfirmIdentity + password form here; nothing fires until submit */} -
-
+ {/* 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 */} +
+

Profile

+
+
First name: {me.first_name || "—"}
+
Last name: {me.last_name || "—"}
+
Email: {me.email || "—"}
+
Location: {me.location || "—"}
+
+
+ ); } diff --git a/app/portal/account/page.tsx b/app/portal/account/page.tsx index 57de52d2..030347d8 100644 --- a/app/portal/account/page.tsx +++ b/app/portal/account/page.tsx @@ -1,54 +1,8 @@ // app/portal/account/page.tsx -import { cookies } from "next/headers"; -import { redirect } from "next/navigation"; -import { dxGET } from "@/lib/directus"; +export const dynamic = "force-dynamic"; // don't cache this page + import AccountClient from "./AccountClient"; -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 } | string | null; -}; - -export default async function Page() { - const jar = await cookies(); - const token = jar.get("ma_at")?.value; - if (!token) { - redirect(`/auth/sign-in?next=${encodeURIComponent("/portal/account")}`); - } - const bearer = `Bearer ${token}`; - - const fields = - "id,username,first_name,last_name,email,location,avatar.id,avatar.filename_download"; - - let me: Me | null = null; - let loadError: string | null = null; - - try { - const res = await dxGET(`/users/me?fields=${encodeURIComponent(fields)}`, bearer); - me = (res?.data ?? res) as Me; - } catch (e: any) { - const status = e?.status ?? 0; - const msg = String(e?.message || ""); - // Only force reauth on auth errors - if (status === 401 || status === 403 || /unauth|expired|credential/i.test(msg)) { - redirect(`/auth/sign-in?reauth=1&next=${encodeURIComponent("/portal/account")}`); - } - loadError = "Couldn’t load your profile. Please try again."; - } - - return ( -
-

Account

- {loadError ? ( -
{loadError}
- ) : ( - - )} -
- ); +export default function Page() { + return ; }