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
+8
View File
@@ -0,0 +1,8 @@
export default function FavoritesPage() {
return (
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<h2 className="text-lg font-semibold">Favorite Parts</h2>
<p className="mt-2 text-sm text-zinc-400">Coming soon.</p>
</div>
);
}
+350
View File
@@ -0,0 +1,350 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/context/AuthContext";
export default function OnboardingPage() {
const router = useRouter();
const { user, loading, token, logout, refreshMeAndSession } = useAuth();
// Step: 1=password, 2=username, 3=optional display name
const [step, setStep] = useState<1 | 2 | 3>(1);
// Password
const [pw1, setPw1] = useState("");
const [pw2, setPw2] = useState("");
const [pwSaving, setPwSaving] = useState(false);
const [pwMsg, setPwMsg] = useState<string | null>(null);
// Username
const [username, setUsername] = useState("");
const [checking, setChecking] = useState(false);
const [available, setAvailable] = useState<boolean | null>(null);
const [userMsg, setUserMsg] = useState<string | null>(null);
const [savingUsername, setSavingUsername] = useState(false);
// Display name (optional)
const [displayName, setDisplayName] = useState("");
const [savingName, setSavingName] = useState(false);
const [nameMsg, setNameMsg] = 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 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;
// Hydrate local inputs when user becomes available
useEffect(() => {
if (!user) return;
setUsername(String((user as any)?.username ?? ""));
setDisplayName(String(user.displayName ?? ""));
}, [user]);
// Route guards + step sync (NO redirects during render)
useEffect(() => {
if (loading) return;
if (!user) return;
// If complete, bounce them out
if (hasUsername && hasPassword) {
router.replace("/builder");
return;
}
// Choose the right step based on what's missing
if (!hasPassword) setStep(1);
else if (!hasUsername) setStep(2);
else setStep(3);
}, [loading, user, hasUsername, hasPassword, 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>;
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 saved ✅");
setPw1("");
setPw2("");
// step will auto-sync via useEffect once user updates, but this makes UI feel instant:
setStep(2);
} catch (e: any) {
setPwMsg(e?.message || "Couldnt set password.");
} finally {
setPwSaving(false);
}
}
async function checkUsername() {
setChecking(true);
setUserMsg(null);
setAvailable(null);
try {
const u = username.trim().toLowerCase();
const res = await fetch(
`/api/users/me/username-available?username=${encodeURIComponent(u)}`,
{
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) },
cache: "no-store",
}
);
if (!res.ok) {
throw new Error((await res.text().catch(() => "")) || "Failed to check username");
}
const data = await res.json();
setAvailable(!!data?.available);
} catch (e: any) {
setUserMsg(e?.message || "Couldnt check username.");
} finally {
setChecking(false);
}
}
async function saveUsername() {
setSavingUsername(true);
setUserMsg(null);
try {
const u = username.trim().toLowerCase();
const res = await fetch(`/api/users/me`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ username: u }),
});
if (!res.ok) {
throw new Error((await res.text().catch(() => "")) || "Failed to save username");
}
await refreshMeAndSession();
setAvailable(true);
setUserMsg("Username saved ✅");
setStep(3);
} catch (e: any) {
setUserMsg(e?.message || "Couldnt save username.");
} finally {
setSavingUsername(false);
}
}
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);
}
}
return (
<div className="rounded-2xl border border-amber-500/20 bg-amber-500/5 p-6">
<h2 className="text-lg font-semibold">Welcome to the beta 👋</h2>
<p className="mt-1 text-sm text-zinc-300/80">
Step {step} of 3. Password + username required.
</p>
{step === 1 && (
<div className="mt-6 space-y-3">
<div className="text-sm font-medium">Set password *</div>
<input
type="password"
value={pw1}
onChange={(e) => setPw1(e.target.value)}
placeholder="Password (min 8 chars)"
className="w-full rounded-md border border-white/10 bg-black/40 px-3 py-2"
/>
<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"
/>
{(pwTooShort || pwMismatch) && (
<div className="text-xs text-red-300">
{pwTooShort ? "Password must be at least 8 characters." : "Passwords dont match."}
</div>
)}
<button
type="button"
onClick={savePassword}
disabled={!canSavePassword}
className="rounded-md bg-amber-400 px-4 py-2 text-sm font-medium text-black disabled:opacity-50"
>
{pwSaving ? "Saving…" : "Save password"}
</button>
{pwMsg && <div className="text-xs text-zinc-200/80">{pwMsg}</div>}
<div className="pt-4 border-t border-white/10 flex items-center justify-between">
<button
type="button"
onClick={logout}
className="text-xs underline opacity-70 hover:opacity-100"
>
Log out
</button>
</div>
</div>
)}
{step === 2 && (
<div className="mt-6 space-y-3">
<div className="text-sm font-medium">Choose username *</div>
<input
value={username}
onChange={(e) => {
setUsername(e.target.value);
setAvailable(null);
setUserMsg(null);
}}
placeholder="e.g. s2tactical"
className="w-full rounded-md border border-white/10 bg-black/40 px-3 py-2"
/>
<div className="flex items-center gap-2">
<button
type="button"
onClick={checkUsername}
disabled={checking || savingUsername || username.trim().length < 3}
className="rounded-md border border-white/10 px-3 py-2 text-xs hover:bg-white/5 disabled:opacity-50"
>
{checking ? "Checking…" : "Check availability"}
</button>
<button
type="button"
onClick={saveUsername}
disabled={savingUsername || available !== true}
className="rounded-md bg-amber-400 px-3 py-2 text-xs font-medium text-black disabled:opacity-50"
>
{savingUsername ? "Saving…" : "Save username"}
</button>
{available !== null && (
<span className={`text-xs ${available ? "text-emerald-300" : "text-red-300"}`}>
{available ? "Available" : "Taken / invalid"}
</span>
)}
</div>
{userMsg && <div className="text-xs text-zinc-200/80">{userMsg}</div>}
<div className="pt-4 border-t border-white/10 flex items-center justify-between">
<button
type="button"
onClick={logout}
className="text-xs underline opacity-70 hover:opacity-100"
>
Log out
</button>
</div>
</div>
)}
{step === 3 && (
<div className="mt-6 space-y-3">
<div className="text-sm font-medium">
Display name <span className="text-zinc-400">(optional)</span>
</div>
<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"
/>
<button
type="button"
onClick={saveDisplayName}
disabled={savingName || displayName.trim().length === 0}
className="rounded-md border border-white/10 px-3 py-2 text-xs hover:bg-white/5 disabled:opacity-50"
>
{savingName ? "Saving…" : "Save display name"}
</button>
{nameMsg && <div className="text-xs text-zinc-200/80">{nameMsg}</div>}
<div className="mt-6 flex items-center justify-between border-t border-white/10 pt-4">
<button
type="button"
onClick={() => router.replace("/builder")}
disabled={!(hasUsername && hasPassword)}
className="rounded-md bg-amber-400 px-4 py-2 text-sm font-medium text-black disabled:opacity-50"
>
Continue to Builder
</button>
<button
type="button"
onClick={() => router.replace("/account/profile")}
className="text-xs underline opacity-70 hover:opacity-100"
>
Ill finish later
</button>
</div>
</div>
)}
</div>
);
}
+4 -438
View File
@@ -1,439 +1,5 @@
// app/(account)/account/page.tsx
"use client";
import { redirect } from "next/navigation";
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>
);
}
export default function AccountIndex() {
redirect("/account/profile");
}
+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>
);
}
+8
View File
@@ -0,0 +1,8 @@
export default function SecurityPage() {
return (
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<h2 className="text-lg font-semibold">Security</h2>
<p className="mt-2 text-sm text-zinc-400">Coming soon.</p>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
export default function VaultPage() {
return (
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<h2 className="text-lg font-semibold">My Vault</h2>
<p className="mt-2 text-sm text-zinc-400">Coming soon.</p>
</div>
);
}