account settings and account creation

This commit is contained in:
2025-12-27 20:01:17 -05:00
parent aa2dfb0407
commit 2202abe89f
21 changed files with 1848 additions and 718 deletions
+270
View File
@@ -0,0 +1,270 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/context/AuthContext";
export default function ProfilePage() {
const router = useRouter();
const { user, loading, token, logout, refreshMeAndSession } = useAuth();
const [displayName, setDisplayName] = useState("");
const [savingName, setSavingName] = useState(false);
const [nameMsg, setNameMsg] = useState<string | null>(null);
const [showPassword, setShowPassword] = useState(false);
const [pw1, setPw1] = useState("");
const [pw2, setPw2] = useState("");
const [pwSaving, setPwSaving] = useState(false);
const [pwMsg, setPwMsg] = useState<string | null>(null);
const hasUsername = useMemo(() => {
const current = String((user as any)?.username ?? "").trim();
return current.length > 0;
}, [user]);
const hasPassword = useMemo(() => {
return !!(user as any)?.passwordSetAt;
}, [user]);
const needsOnboarding = !!user && (!hasUsername || !hasPassword);
const pwMismatch = pw1.length > 0 && pw2.length > 0 && pw1 !== pw2;
const pwTooShort = pw1.length > 0 && pw1.length < 8;
const canSavePassword = !pwSaving && pw1.length >= 8 && pw1 === pw2;
useEffect(() => {
if (!user) return;
setDisplayName(String(user.displayName ?? ""));
}, [user]);
// If missing required setup, bounce to onboarding
useEffect(() => {
if (loading) return;
if (!user) return;
if (needsOnboarding) {
router.replace("/account/onboarding");
}
}, [loading, user, needsOnboarding, router]);
if (loading) return <div className="text-sm opacity-70">Loading</div>;
if (!user) return <div className="text-sm opacity-70">Not logged in.</div>;
// While redirecting, don't flash profile UI
if (needsOnboarding) return null;
async function saveDisplayName() {
setSavingName(true);
setNameMsg(null);
try {
const next = displayName.trim();
const res = await fetch(`/api/users/me`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ displayName: next }),
});
if (!res.ok) {
throw new Error((await res.text().catch(() => "")) || "Failed to save display name");
}
await refreshMeAndSession();
setNameMsg("Saved ✅");
} catch (e: any) {
setNameMsg(e?.message || "Couldnt save display name.");
} finally {
setSavingName(false);
}
}
async function savePassword() {
setPwMsg(null);
if (pw1.length < 8) return setPwMsg("Password must be at least 8 characters.");
if (pw1 !== pw2) return setPwMsg("Passwords dont match.");
setPwSaving(true);
try {
const res = await fetch(`/api/users/me/password`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ password: pw1 }),
});
if (!res.ok) {
throw new Error((await res.text().catch(() => "")) || "Failed to set password");
}
await refreshMeAndSession();
setPwMsg("Password updated ✅");
setPw1("");
setPw2("");
setShowPassword(false);
} catch (e: any) {
setPwMsg(e?.message || "Couldnt set password. Try again.");
} finally {
setPwSaving(false);
}
}
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold">Profile</h2>
<p className="text-sm opacity-70">Basic account info.</p>
</div>
<div className="rounded-xl border border-white/10 bg-white/5 p-4 space-y-4">
<div className="text-sm">
<span className="opacity-70">Email:</span>{" "}
<span className="font-medium">{user.email}</span>
</div>
<div className="space-y-2">
<div className="text-sm">
<span className="opacity-70">Username:</span>{" "}
<span className="font-medium">{String((user as any)?.username ?? "—")}</span>
</div>
<div className="text-sm font-medium">Display name</div>
<input
value={displayName}
onChange={(e) => {
setDisplayName(e.target.value);
setNameMsg(null);
}}
placeholder="e.g. Sean / S2Tactical"
className="w-full rounded-md border border-white/10 bg-black/40 px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-amber-500/40"
/>
<div className="flex items-center gap-3">
<button
type="button"
onClick={saveDisplayName}
disabled={savingName || displayName.trim().length === 0}
className="rounded-md border border-white/10 px-3 py-2 text-xs opacity-80 hover:opacity-100 disabled:opacity-50"
>
{savingName ? "Saving…" : "Save display name"}
</button>
{nameMsg && (
<span
className={`text-xs ${
nameMsg.toLowerCase().includes("couldn") || nameMsg.toLowerCase().includes("failed")
? "text-red-300"
: "text-zinc-200/80"
}`}
>
{nameMsg}
</span>
)}
</div>
</div>
<div className="text-sm">
<span className="opacity-70">Role:</span>{" "}
<span className="font-medium">{user.role}</span>
</div>
<div className="pt-3 border-t border-white/10 flex items-center justify-between">
<button
onClick={logout}
className="rounded-md border border-white/10 px-3 py-2 text-xs opacity-80 hover:opacity-100"
>
Log out
</button>
<button
onClick={() => {
setShowPassword((v) => !v);
setPwMsg(null);
}}
className="text-xs underline opacity-70 hover:opacity-100"
>
{showPassword ? "Hide password" : "Set / change password"}
</button>
</div>
{showPassword && (
<div className="pt-4 border-t border-white/10 space-y-2">
<div className="text-sm font-medium">Set password</div>
<input
type="password"
value={pw1}
onChange={(e) => setPw1(e.target.value)}
placeholder="New password (min 8 chars)"
className="w-full rounded-md border border-white/10 bg-black/40 px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-amber-500/40"
/>
<input
type="password"
value={pw2}
onChange={(e) => setPw2(e.target.value)}
placeholder="Confirm password"
className="w-full rounded-md border border-white/10 bg-black/40 px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-amber-500/40"
/>
{(pwTooShort || pwMismatch) && (
<div className="text-xs text-red-300">
{pwTooShort ? "Password must be at least 8 characters." : "Passwords dont match."}
</div>
)}
<div className="flex items-center gap-3">
<button
onClick={savePassword}
disabled={!canSavePassword}
className="rounded-md bg-amber-400 px-3 py-2 text-sm font-medium text-black disabled:opacity-50"
>
{pwSaving ? "Saving…" : "Save password"}
</button>
<button
onClick={() => {
setShowPassword(false);
setPw1("");
setPw2("");
setPwMsg(null);
}}
className="text-xs opacity-80 hover:opacity-100"
>
Cancel
</button>
</div>
{pwMsg && (
<div
className={`text-xs ${
pwMsg.toLowerCase().includes("couldn") || pwMsg.toLowerCase().includes("failed")
? "text-red-300"
: "text-zinc-200/80"
}`}
>
{pwMsg}
</div>
)}
</div>
)}
<div className="pt-3 border-t border-white/10 flex items-center justify-end">
<button
type="button"
onClick={() => router.replace("/builder")}
className="rounded-md bg-amber-400 px-3 py-2 text-sm font-medium text-black"
>
Back to Builder
</button>
</div>
</div>
</div>
);
}