diff --git a/app/(account)/account/page.tsx b/app/(account)/account/page.tsx index 3492f35..c298fa1 100644 --- a/app/(account)/account/page.tsx +++ b/app/(account)/account/page.tsx @@ -1,210 +1,439 @@ // app/(account)/account/page.tsx "use client"; +import { useMemo, useState } from "react"; +import { useSearchParams, useRouter } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; -function StatChip({ - label, - value, -}: { - label: string; - value: React.ReactNode; -}) { - return ( -
-
- {label} -
-
{value}
-
- ); -} - -function Card({ - title, - description, - children, - badge, -}: { - title: string; - description?: string; - children: React.ReactNode; - badge?: React.ReactNode; -}) { - return ( -
-
-
-

{title}

- {description ? ( -

{description}

- ) : null} -
- {badge ?
{badge}
: null} -
- -
{children}
-
- ); -} - -function Row({ label, value }: { label: string; value: React.ReactNode }) { - return ( -
-
{label}
-
{value}
-
- ); -} +const API_BASE_URL = + process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"; export default function AccountPage() { - const { user, loading } = useAuth(); + const { user, loading, token, setSession, logout } = useAuth(); + const searchParams = useSearchParams(); + const router = useRouter(); - if (loading) return
Loading…
; + 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(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(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
Loading…
; if (!user) { return ( -
-

- You’re not logged in -

-

- Please log in to view your account. -

+
+ You’re not logged in. Please log in to view your account.
); } + const authedUser = user; // user is non-null from here down - const planLabel = user.role === "ADMIN" ? "Admin" : "Beta Access"; + 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 || "Couldn’t 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 don’t 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 || "Couldn’t set password. Try again."); + } finally { + setPwSaving(false); + } + } return (
- {/* Quick stats (lightweight “premium” feel) */} - {/*
- - - - -
*/} - -
- - Read-only (for now) - - } - > -
- - - + {/* Welcome / Setup card (only if needed) */} + {showWelcomeCard && ( +
+
+

Welcome to the beta 👋

+

+ Quick setup and you’re ready to cook. +

- -
-
- - Coming soon - - } - > -
-