"use client"; import React, { useEffect, useState } from "react"; import { useAuth } from "@/hooks/useAuth"; import { useRouter } from "next/navigation"; import { apiFetch } from "@/lib/api"; import { isValidZAPhone } from "@/lib/phone"; import { useDismissingState } from "@/hooks/useDismissingState"; import { User, Lock, ShieldAlert, Trash2, Clock, LogIn, KeyRound, RotateCcw } from "lucide-react"; type ActivityEvent = { id: string; type: "login" | "password_changed" | "password_reset"; device: string | null; createdAt: string }; const ACTIVITY_META: Record string; icon: typeof LogIn }> = { login: { label: d => `Logged in${d ? ` from ${d}` : ""}`, icon: LogIn }, password_changed: { label: () => "Password changed", icon: KeyRound }, password_reset: { label: () => "Password reset via email link", icon: RotateCcw }, }; export default function UserProfilePage() { const { user, token, logout, updateToken } = useAuth(); const router = useRouter(); useEffect(() => { if (!user && !token) router.replace("/login"); }, [user, token, router]); // ── Profile form ───────────────────────────────────────────────────────── const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [phone, setPhone] = useState(""); const [notifPref, setNotifPref] = useState<"email" | "whatsapp" | "both">("email"); const [profileMsg, setProfileMsg] = useDismissingState<{ type: "ok" | "err"; text: string } | null>(null); const [savingProfile, setSavingProfile] = useState(false); const hasValidPhone = isValidZAPhone(phone); useEffect(() => { if (user) { setName(user.name || ""); setEmail(user.email || ""); setPhone(user.phoneNumber || ""); setNotifPref(user.notificationPreference || "email"); } }, [user]); const saveProfile = async (e: React.FormEvent) => { e.preventDefault(); if (!token) return; setSavingProfile(true); setProfileMsg(null); try { const res = await apiFetch("/api/users/profile", { method: "PUT", authToken: token, body: { name, email, phoneNumber: phone || null, notificationPreference: hasValidPhone ? notifPref : "email", }, }); // Backend returns a refreshed token — save it so the session stays valid if (res?.token) updateToken(res.token); setProfileMsg({ type: "ok", text: "Profile updated." }); } catch (e: any) { setProfileMsg({ type: "err", text: e?.message || "Failed to update profile." }); } finally { setSavingProfile(false); } }; // ── Password change ─────────────────────────────────────────────────────── const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [pwMsg, setPwMsg] = useDismissingState<{ type: "ok" | "err"; text: string } | null>(null); const [savingPw, setSavingPw] = useState(false); const changePassword = async (e: React.FormEvent) => { e.preventDefault(); if (newPassword !== confirmPassword) { setPwMsg({ type: "err", text: "Passwords do not match." }); return; } if (newPassword.length < 8) { setPwMsg({ type: "err", text: "Password must be at least 8 characters." }); return; } if (!token) return; setSavingPw(true); setPwMsg(null); try { const res = await apiFetch("/api/users/profile", { method: "PUT", authToken: token, body: { currentPassword, password: newPassword }, }); if (res?.token) updateToken(res.token); setCurrentPassword(""); setNewPassword(""); setConfirmPassword(""); setPwMsg({ type: "ok", text: "Password changed." }); loadActivity(); } catch (e: any) { setPwMsg({ type: "err", text: e?.message || "Failed to change password." }); } finally { setSavingPw(false); } }; // ── Revoke sessions ─────────────────────────────────────────────────────── const [revokeMsg, setRevokeMsg] = useDismissingState<{ type: "ok" | "err"; text: string } | null>(null); const [revoking, setRevoking] = useState(false); const revokeSessions = async () => { if (!confirm("This will sign you out of all other devices. You will need to log in again everywhere except here. Continue?")) return; if (!token) return; setRevoking(true); setRevokeMsg(null); try { const res = await apiFetch("/api/users/revoke-sessions", { method: "POST", authToken: token, }); // Revoking sessions invalidates the token this device was using too — // save the freshly issued one so this device stays signed in. if (res?.token) updateToken(res.token); setRevokeMsg({ type: "ok", text: res?.message || "All other sessions signed out." }); } catch (e: any) { setRevokeMsg({ type: "err", text: e?.message || "Failed to revoke sessions." }); } finally { setRevoking(false); } }; // ── Account activity ────────────────────────────────────────────────────── const [activity, setActivity] = useState([]); const [loadingActivity, setLoadingActivity] = useState(false); const loadActivity = async () => { if (!token) return; setLoadingActivity(true); try { const res = await apiFetch("/api/users/activity", { authToken: token }); setActivity(Array.isArray(res) ? res : []); } catch (e) { // Non-critical — just leave the list empty rather than showing an error banner. } finally { setLoadingActivity(false); } }; useEffect(() => { loadActivity(); }, [token]); // ── Account closure ─────────────────────────────────────────────────────── const [closeStep, setCloseStep] = useState<"idle" | "confirm">("idle"); const [deleteData, setDeleteData] = useState(false); const [closePassword, setClosePassword] = useState(""); const [closeMsg, setCloseMsg] = useDismissingState<{ type: "ok" | "err"; text: string } | null>(null); const [closing, setClosing] = useState(false); const submitAccountClosure = async (e: React.FormEvent) => { e.preventDefault(); if (!token) return; setClosing(true); setCloseMsg(null); try { const res = await apiFetch("/api/users/close-account", { method: "POST", authToken: token, body: { password: closePassword, deleteData }, }); setCloseMsg({ type: "ok", text: res?.message || "Account closed." }); // Slight delay so the user can read the message, then log out setTimeout(() => logout(), 2500); } catch (e: any) { setCloseMsg({ type: "err", text: e?.message || "Failed to close account." }); } finally { setClosing(false); } }; // ── Shared helpers ──────────────────────────────────────────────────────── const Alert = ({ msg }: { msg: { type: "ok" | "err"; text: string } }) => (
{msg.text}
); const inputCls = "w-full border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"; const SectionHeader = ({ icon: Icon, title, tone = "brand" }: { icon: typeof User; title: string; tone?: "brand" | "amber" | "red" }) => { const toneCls = tone === "amber" ? "bg-amber-50 text-amber-600" : tone === "red" ? "bg-red-50 text-red-600" : "bg-brand-50 text-brand-600"; return (

{title}

); }; return (

Profile & Security

Manage your personal information and keep your account secure.

{/* ── Profile info ─────────────────────────────────────────────── */}
setName(e.target.value)} required />
setEmail(e.target.value)} required />
setPhone(e.target.value)} placeholder="e.g. 082 123 4567" />
{hasValidPhone && (

Security alerts (password reset, login notifications) are always sent via email regardless of this setting.

)} {profileMsg && }
{/* ── Account activity ─────────────────────────────────────────── */}
{loadingActivity && activity.length === 0 ? (

Loading…

) : activity.length === 0 ? (

No recent activity recorded yet.

) : (
    {activity.map(ev => { const meta = ACTIVITY_META[ev.type] || ACTIVITY_META.login; const Icon = meta.icon; return (
  • {meta.label(ev.device)}
    {new Date(ev.createdAt).toLocaleString()}
  • ); })}
)}
{/* ── Password ─────────────────────────────────────────────────── */}
setCurrentPassword(e.target.value)} required autoComplete="current-password" />
setNewPassword(e.target.value)} required minLength={8} autoComplete="new-password" />
setConfirmPassword(e.target.value)} required autoComplete="new-password" />
{pwMsg && }
{/* ── Security ─────────────────────────────────────────────────── */}

If you suspect someone else has access to your account, you can sign out of all other devices immediately. You will remain logged in on this device.

{revokeMsg && }
{/* ── Danger zone ──────────────────────────────────────────────── */}

Closing your account will deactivate it immediately. You can also request that your personal information (name, email, phone number) be permanently deleted. Tickets and payment records will remain for accounting purposes but will show as "Deleted User".

{closeStep === "idle" && ( )} {closeStep === "confirm" && (
This action cannot be undone. Please read carefully before continuing.
setClosePassword(e.target.value)} required placeholder="Enter your password to confirm" autoComplete="current-password" />
{closeMsg && } )}
); }