"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"; 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." }); } 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 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}
); return (

Profile & Security

{/* ── Profile info ─────────────────────────────────────────────────── */}

Personal information

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 && }
{/* ── Password ─────────────────────────────────────────────────────── */}

Change 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 ─────────────────────────────────────────────────────── */}

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 ──────────────────────────────────────────────────── */}

Close account

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 && } )}
); }