diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx index 0552fb6..8750383 100644 --- a/src/app/dashboard/settings/page.tsx +++ b/src/app/dashboard/settings/page.tsx @@ -1,6 +1,9 @@ 'use client'; import { useEffect, useState } from 'react'; +import { Bell, Shield, Save, Loader2, Trash2 } from 'lucide-react'; +import { Breadcrumb } from '@/components/ui'; +import DeleteAccountModal from '@/components/auth/DeleteAccountModal'; import { Bell, Shield, Save, Loader2, Zap } from 'lucide-react'; import { Breadcrumb } from '@/components/ui'; import { useReducedMotion } from '@/lib/hooks/use-reduced-motion'; @@ -13,6 +16,7 @@ export default function SettingsPage() { const [emailUpdates, setEmailUpdates] = useState(true); const [courseUpdates, setCourseUpdates] = useState(true); const [securityAlerts, setSecurityAlerts] = useState(true); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); const { prefersReducedMotion, setManualOverride, clearManualOverride, hasManualOverride, mounted } = useReducedMotion(); const [localReducedMotion, setLocalReducedMotion] = useState(false); @@ -199,6 +203,26 @@ export default function SettingsPage() { Preferences will be updated once backend endpoints are available. + + + + + + + Danger zone + + + Permanently delete your account, progress, certificates, and course access. + + + setDeleteModalOpen(true)} className="btn-danger shrink-0"> + + Delete account + + + + + setDeleteModalOpen(false)} /> ); } diff --git a/src/components/auth/DeleteAccountModal.tsx b/src/components/auth/DeleteAccountModal.tsx new file mode 100644 index 0000000..324b765 --- /dev/null +++ b/src/components/auth/DeleteAccountModal.tsx @@ -0,0 +1,206 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { AlertTriangle, Check, Loader2, X } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { useForm } from 'react-hook-form'; +import { useAuthStore } from '@/lib/hooks/use-auth-store'; + +type DeleteAccountFormValues = { + understood: boolean; + password: string; +}; + +type DeleteAccountModalProps = { + open: boolean; + onClose: () => void; +}; + +const consequences = [ + 'All course progress and enrolled course access will be permanently removed.', + 'Your certificates and learning history will be deleted.', + 'Any pending refunds or account-related claims may no longer be available.', +]; + +export default function DeleteAccountModal({ open, onClose }: DeleteAccountModalProps) { + const router = useRouter(); + const deleteAccount = useAuthStore((state) => state.deleteAccount); + const [step, setStep] = useState<1 | 2 | 3>(1); + const [formError, setFormError] = useState(null); + const [secondsRemaining, setSecondsRemaining] = useState(5); + + const { + register, + handleSubmit, + reset, + trigger, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { understood: false, password: '' }, + mode: 'onSubmit', + }); + + useEffect(() => { + if (open) { + setStep(1); + setFormError(null); + setSecondsRemaining(5); + reset({ understood: false, password: '' }); + } + }, [open, reset]); + + useEffect(() => { + if (!open || step !== 3) return; + + const countdown = window.setInterval(() => { + setSecondsRemaining((value) => Math.max(value - 1, 0)); + }, 1000); + const redirect = window.setTimeout(() => router.replace('/'), 5000); + + return () => { + window.clearInterval(countdown); + window.clearTimeout(redirect); + }; + }, [open, router, step]); + + useEffect(() => { + if (!open) return; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape' && step !== 3) onClose(); + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [onClose, open, step]); + + if (!open) return null; + + const goToPasswordStep = async () => { + setFormError(null); + if (await trigger('understood')) setStep(2); + }; + + const submitPassword = async ({ password }: DeleteAccountFormValues) => { + setFormError(null); + + try { + await deleteAccount(password); + setStep(3); + } catch (error: any) { + const message = error?.response?.data?.message ?? error?.response?.data?.error; + setFormError(typeof message === 'string' ? message : 'We could not delete your account. Check your password and try again.'); + } + }; + + return ( + + + + event.stopPropagation()} + > + {step !== 3 && ( + + + + )} + + {step === 3 ? ( + + + + + + Your account has been deleted + + + Your enrolled course access has been revoked. You will be redirected to the homepage in {secondsRemaining} seconds. + + + ) : ( + + + + + + + Step {step} of 2 + + {step === 1 ? 'Delete your account?' : 'Confirm your identity'} + + + + + {step === 1 ? ( + <> + + This action is permanent. Once your account is deleted, it cannot be restored. + + + {consequences.map((consequence) => ( + + • + {consequence} + + ))} + + + + I understand that deleting my account is permanent. + + {errors.understood && {errors.understood.message}} + + Cancel + Continue + + > + ) : ( + <> + + Enter your current password to verify your identity before we permanently delete your account. + + Current password + + {errors.password && {errors.password.message}} + {formError && {formError}} + + { setFormError(null); setStep(1); }} className="btn-secondary">Back + + {isSubmitting ? <> Deleting...> : 'Delete account'} + + + > + )} + + )} + + + ); +} diff --git a/src/lib/api/services.ts b/src/lib/api/services.ts index 6e1fc63..3277882 100644 --- a/src/lib/api/services.ts +++ b/src/lib/api/services.ts @@ -376,6 +376,9 @@ export const usersApi = { const { data } = await apiClient.patch>('/users/me', payload); return data.data; }, + deleteAccount: async (password: string): Promise => { + await apiClient.delete('/users/me', { data: { password } }); + }, getInstructorStats: async () => { const { data } = await apiClient.get('/users/me/instructor-stats'); return data.data; diff --git a/src/lib/hooks/use-auth-store.ts b/src/lib/hooks/use-auth-store.ts index 6a4885d..e5a89bb 100644 --- a/src/lib/hooks/use-auth-store.ts +++ b/src/lib/hooks/use-auth-store.ts @@ -1,5 +1,6 @@ import { create } from 'zustand'; import type { User } from '@/types'; +import { usersApi } from '@/lib/api/services'; interface AuthState { address: string | null; @@ -9,6 +10,7 @@ interface AuthState { setAuth: (address: string, token: string, user: User, rememberMe?: boolean) => void; setAddress: (address: string) => void; updateUser: (patch: Partial) => void; + deleteAccount: (password: string) => Promise; logout: () => void; rehydrate: () => void; } @@ -31,6 +33,16 @@ export const useAuthStore = create((set) => ({ updateUser: (patch) => set((state) => ({ user: state.user ? { ...state.user, ...patch } : state.user })), + deleteAccount: async (password) => { + await usersApi.deleteAccount(password); + if (typeof window !== 'undefined') { + localStorage.removeItem('hamplard_token'); + localStorage.removeItem('hamplard_address'); + document.cookie = 'hamplard_token=; path=/; max-age=0'; + } + set({ address: null, token: null, user: null, isConnected: false }); + }, + logout: () => { if (typeof window !== 'undefined') { localStorage.removeItem('hamplard_token');
+ Permanently delete your account, progress, certificates, and course access. +
+ Your enrolled course access has been revoked. You will be redirected to the homepage in {secondsRemaining} seconds. +
Step {step} of 2
+ This action is permanent. Once your account is deleted, it cannot be restored. +
{errors.understood.message}
+ Enter your current password to verify your identity before we permanently delete your account. +
{errors.password.message}
{formError}