Files
shadow-gunbuilder-ai-proto/app/(account)/account/page.tsx
T

440 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// app/(account)/account/page.tsx
"use client";
import { useMemo, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { useAuth } from "@/context/AuthContext";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
export default function AccountPage() {
const { user, loading, token, setSession, logout } = useAuth();
const searchParams = useSearchParams();
const router = useRouter();
const welcome = searchParams.get("welcome") === "1";
const needsDisplayName = useMemo(() => {
return !user?.displayName || user.displayName.trim().length === 0;
}, [user?.displayName]);
// Only show welcome panel if they actually need setup
const showWelcomeCard = welcome && needsDisplayName;
// ----------------------------
// Display Name state (shared)
// ----------------------------
const [displayName, setDisplayName] = useState(user?.displayName ?? "");
const [saving, setSaving] = useState(false);
const [msg, setMsg] = useState<string | null>(null);
// Inline profile editor
const [editingName, setEditingName] = useState(false);
// ----------------------------
// Password state
// ----------------------------
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 canSaveName = useMemo(() => {
return displayName.trim().length > 0 && !saving;
}, [displayName, saving]);
const pwMismatch = useMemo(
() => pw1.length > 0 && pw2.length > 0 && pw1 !== pw2,
[pw1, pw2]
);
const pwTooShort = useMemo(() => pw1.length > 0 && pw1.length < 8, [pw1]);
const canSavePassword = useMemo(() => {
return !pwSaving && pw1.length >= 8 && pw1 === pw2;
}, [pwSaving, pw1, pw2]);
if (loading) return <div className="text-sm opacity-70">Loading</div>;
if (!user) {
return (
<div className="text-sm opacity-70">
Youre not logged in. Please log in to view your account.
</div>
);
}
const authedUser = user; // user is non-null from here down
async function saveDisplayName() {
setSaving(true);
setMsg(null);
try {
const nextName = displayName.trim();
const res = await fetch(`${API_BASE_URL}/api/users/me`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ displayName: nextName }),
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(text || "Failed to update profile");
}
const updated = await res.json().catch(() => null);
const nextUser = {
email: authedUser.email,
displayName: updated?.displayName ?? nextName,
role: authedUser.role,
};
if (token) setSession(token, nextUser);
setMsg("Saved ✅");
setEditingName(false);
// If we came from the welcome flow, clear the param
if (welcome) router.replace("/account");
} catch (e: any) {
setMsg(e?.message || "Couldnt save. Try again.");
} finally {
setSaving(false);
}
}
async function savePassword() {
setPwMsg(null);
if (pw1.length < 8) {
setPwMsg("Password must be at least 8 characters.");
return;
}
if (pw1 !== pw2) {
setPwMsg("Passwords dont match.");
return;
}
setPwSaving(true);
try {
const res = await fetch(`${API_BASE_URL}/api/users/me/password`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ password: pw1 }),
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(text || "Failed to set password");
}
setPwMsg("Password set ✅ You can now use email + password login.");
setPw1("");
setPw2("");
setShowPassword(false);
if (welcome) router.replace("/account");
} catch (e: any) {
setPwMsg(e?.message || "Couldnt set password. Try again.");
} finally {
setPwSaving(false);
}
}
return (
<div className="space-y-6">
{/* Welcome / Setup card (only if needed) */}
{showWelcomeCard && (
<div className="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 space-y-4">
<div>
<h2 className="text-base font-semibold">Welcome to the beta 👋</h2>
<p className="text-sm text-zinc-300/80">
Quick setup and youre ready to cook.
</p>
</div>
{/* Display name */}
<div className="space-y-2">
<label className="text-xs font-medium text-zinc-200">
Display name
</label>
<input
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="e.g. Sean / S2Tactical / DirtNinja69"
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
onClick={saveDisplayName}
disabled={!canSaveName}
className="rounded-md bg-amber-400 px-3 py-2 text-sm font-medium text-black disabled:opacity-50"
>
{saving ? "Saving…" : "Save"}
</button>
{msg && (
<span
className={`text-xs ${
msg.toLowerCase().includes("couldn")
? "text-red-300"
: "text-zinc-200/80"
}`}
>
{msg}
</span>
)}
</div>
</div>
{/* Password */}
<div className="pt-2 border-t border-white/10">
<p className="text-xs text-zinc-300/80">
Want password login later? Optional (but recommended).
</p>
{!showPassword ? (
<button
onClick={() => setShowPassword(true)}
className="mt-2 text-xs underline opacity-80 hover:opacity-100"
>
Set a password
</button>
) : (
<div className="mt-3 space-y-2">
<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")
? "text-red-300"
: "text-zinc-200/80"
}`}
>
{pwMsg}
</div>
)}
</div>
)}
</div>
</div>
)}
{/* Profile header */}
<div>
<h2 className="text-lg font-semibold">Profile</h2>
<p className="text-sm opacity-70">Basic account info.</p>
</div>
{/* Profile card */}
<div className="rounded-xl border border-white/10 bg-white/5 p-4 space-y-3">
<div className="text-sm">
<span className="opacity-70">Email:</span>{" "}
<span className="font-medium">{user.email}</span>
</div>
{/* Display name (inline editable) */}
<div className="text-sm">
<span className="opacity-70">Display name:</span>{" "}
{!editingName ? (
<>
<span className="font-medium">{user.displayName || "—"}</span>
<button
onClick={() => {
setEditingName(true);
setMsg(null);
setDisplayName(user.displayName ?? "");
}}
className="ml-3 text-xs underline opacity-70 hover:opacity-100"
>
Edit
</button>
</>
) : (
<div className="mt-2 space-y-2">
<input
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="Your display name"
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
onClick={saveDisplayName}
disabled={!canSaveName}
className="rounded-md bg-amber-400 px-3 py-2 text-xs font-medium text-black disabled:opacity-50"
>
{saving ? "Saving…" : "Save"}
</button>
<button
onClick={() => {
setEditingName(false);
setDisplayName(user.displayName ?? "");
setMsg(null);
}}
className="text-xs opacity-80 hover:opacity-100"
>
Cancel
</button>
{msg && (
<span
className={`text-xs ${
msg.toLowerCase().includes("couldn")
? "text-red-300"
: "text-zinc-200/80"
}`}
>
{msg}
</span>
)}
</div>
</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>
{/* Optional: quick link to set password later */}
<button
onClick={() => {
setShowPassword(true);
// Scroll users attention to welcome panel area if it exists,
// otherwise just open the password UI in their mind.
// (Kept simple: you can add a dedicated card later if you want)
setPwMsg(null);
}}
className="text-xs underline opacity-70 hover:opacity-100"
>
Set / change password
</button>
</div>
{/* If user clicks "Set/change password" outside welcome flow, show it here */}
{!showWelcomeCard && 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")
? "text-red-300"
: "text-zinc-200/80"
}`}
>
{pwMsg}
</div>
)}
</div>
)}
</div>
</div>
);
}