makearmy-app/components/account/ProfileEditor.tsx

161 lines
5.1 KiB
TypeScript

// components/account/ProfileEditor.tsx
"use client";
import { useEffect, useMemo, useState } from "react";
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 ProfileEditor({
me: meProp,
onUpdated,
}: {
me?: Me | null;
onUpdated?: () => void;
}) {
const [me, setMe] = useState<Me | null>(meProp ?? null);
const [first_name, setFirst] = useState("");
const [last_name, setLast] = useState("");
const [email, setEmail] = useState("");
const [location, setLocation] = useState("");
const [msg, setMsg] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [loading, setLoading] = useState(!meProp);
const nextAccount = "/portal/account";
useEffect(() => {
if (meProp) {
setMe(meProp);
setLoading(false);
} else {
(async () => {
try {
const r = await fetch("/api/account", { credentials: "include", cache: "no-store" });
if (!r.ok) throw new Error(String(r.status));
const j = await r.json();
const user: Me | undefined = j?.user ?? j?.data ?? undefined;
if (!user) throw new Error("Bad response");
setMe(user);
} catch (e: any) {
setMsg(`Failed to load: ${e?.message || e}`);
} finally {
setLoading(false);
}
})();
}
}, [meProp]);
useEffect(() => {
if (!me) return;
setFirst(me.first_name || "");
setLast(me.last_name || "");
setEmail(me.email || "");
setLocation(me.location || "");
}, [me]);
const onSave = async () => {
setMsg(null);
setBusy(true);
try {
const payload: Record<string, any> = {
first_name: first_name.trim(),
last_name: last_name.trim(),
email: email.trim() || null, // allow clearing email
location: location.trim(),
};
const r = await fetch("/api/account/profile", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (r.status === 428) {
// Need reauth for sensitive change (email)
location.assign(`/auth/sign-in?reauth=1&next=${encodeURIComponent(nextAccount + "#security")}`);
return;
}
if (r.status === 401) {
location.assign(`/auth/sign-in?reauth=1&next=${encodeURIComponent(nextAccount)}`);
return;
}
const j = await r.json().catch(() => ({}));
if (!r.ok) {
setMsg(j?.error || "Update failed");
return;
}
setMsg("Saved.");
onUpdated?.();
} catch (e: any) {
setMsg(e?.message || "Update failed");
} finally {
setBusy(false);
}
};
if (loading) return <div className="rounded-md border p-4 text-sm opacity-70">Loading editor</div>;
return (
<div className="rounded-md border p-4 space-y-3">
<h3 className="font-semibold">Edit Profile</h3>
<div className="grid sm:grid-cols-2 gap-3 text-sm">
<label className="grid gap-1">
<span className="opacity-60">First Name</span>
<input
className="rounded-md border px-2 py-1"
value={first_name}
onChange={(e) => setFirst(e.target.value)}
/>
</label>
<label className="grid gap-1">
<span className="opacity-60">Last Name</span>
<input
className="rounded-md border px-2 py-1"
value={last_name}
onChange={(e) => setLast(e.target.value)}
/>
</label>
<label className="grid gap-1 sm:col-span-2">
<span className="opacity-60">Email (reauth required)</span>
<input
className="rounded-md border px-2 py-1"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
/>
</label>
<label className="grid gap-1 sm:col-span-2">
<span className="opacity-60">Location</span>
<input
className="rounded-md border px-2 py-1"
value={location}
onChange={(e) => setLocation(e.target.value)}
placeholder="City, Country"
/>
</label>
</div>
<div className="flex items-center gap-3">
<button
onClick={onSave}
disabled={busy}
className="rounded-md bg-black text-white px-3 py-2 text-sm disabled:opacity-60"
>
{busy ? "Saving…" : "Save Changes"}
</button>
{msg && <div className="text-sm opacity-80">{msg}</div>}
</div>
</div>
);
}