diff --git a/src/lib/api.ts b/src/lib/api.ts index d15b82a..fd4ca1e 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -95,6 +95,33 @@ class ApiClient { } } + // Account (email & password changes via Better Auth) + async changePassword(currentPassword: string, newPassword: string): Promise { + const response = await fetch(`${AUTH_BASE}/api/auth/change-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.authHeaders() }, + credentials: 'include', + body: JSON.stringify({ currentPassword, newPassword }), + }); + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Failed to change password' })); + throw new Error(error.message || 'Failed to change password'); + } + } + + async changeEmail(newEmail: string): Promise { + const response = await fetch(`${AUTH_BASE}/api/auth/change-email`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...this.authHeaders() }, + credentials: 'include', + body: JSON.stringify({ newEmail }), + }); + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Failed to change email' })); + throw new Error(error.message || 'Failed to change email'); + } + } + // Profile async getProfile(): Promise { return this.fetch('/profile'); diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 27c176f..b431cf4 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -1,39 +1,107 @@ import { useEffect, useState } from 'react'; import { api } from '@/lib/api'; +import { useAuthStore } from '@/stores/auth'; import type { Profile } from '@/types'; -import { Save, User } from 'lucide-react'; +import { Save, User, Lock, Mail, CheckCircle2, AlertCircle } from 'lucide-react'; import LoadingSpinner, { PageLoader } from '@/components/LoadingSpinner'; +function StatusMessage({ type, message }: { type: 'success' | 'error'; message: string }) { + return ( +
+ {type === 'success' ? : } + {message} +
+ ); +} + export default function SettingsPage() { + const { checkSession } = useAuthStore(); const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); - const [success, setSuccess] = useState(false); + const [profileStatus, setProfileStatus] = useState<{ type: 'success' | 'error'; message: string } | null>(null); + + // Email change + const [newEmail, setNewEmail] = useState(''); + const [emailSaving, setEmailSaving] = useState(false); + const [emailStatus, setEmailStatus] = useState<{ type: 'success' | 'error'; message: string } | null>(null); + + // Password change + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [passwordSaving, setPasswordSaving] = useState(false); + const [passwordStatus, setPasswordStatus] = useState<{ type: 'success' | 'error'; message: string } | null>(null); useEffect(() => { api.getProfile().then((p) => { setProfile(p); + setNewEmail(p.email || ''); setLoading(false); }).catch(() => setLoading(false)); }, []); - const handleSave = async (e: React.FormEvent) => { + const handleSaveProfile = async (e: React.FormEvent) => { e.preventDefault(); if (!profile) return; setSaving(true); - setSuccess(false); + setProfileStatus(null); try { const updated = await api.updateProfile(profile); setProfile(updated); - setSuccess(true); - setTimeout(() => setSuccess(false), 3000); - } catch (err) { - console.error(err); + setProfileStatus({ type: 'success', message: 'Profile saved' }); + setTimeout(() => setProfileStatus(null), 3000); + } catch (err: any) { + setProfileStatus({ type: 'error', message: err.message || 'Failed to save' }); } finally { setSaving(false); } }; + const handleChangeEmail = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newEmail || newEmail === profile?.email) return; + setEmailSaving(true); + setEmailStatus(null); + try { + await api.changeEmail(newEmail); + await checkSession(); + setProfile(prev => prev ? { ...prev, email: newEmail } : prev); + setEmailStatus({ type: 'success', message: 'Email updated' }); + setTimeout(() => setEmailStatus(null), 3000); + } catch (err: any) { + setEmailStatus({ type: 'error', message: err.message || 'Failed to update email' }); + } finally { + setEmailSaving(false); + } + }; + + const handleChangePassword = async (e: React.FormEvent) => { + e.preventDefault(); + setPasswordStatus(null); + if (newPassword !== confirmPassword) { + setPasswordStatus({ type: 'error', message: 'Passwords do not match' }); + return; + } + if (newPassword.length < 8) { + setPasswordStatus({ type: 'error', message: 'Password must be at least 8 characters' }); + return; + } + setPasswordSaving(true); + try { + await api.changePassword(currentPassword, newPassword); + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + setPasswordStatus({ type: 'success', message: 'Password changed' }); + setTimeout(() => setPasswordStatus(null), 3000); + } catch (err: any) { + setPasswordStatus({ type: 'error', message: err.message || 'Failed to change password' }); + } finally { + setPasswordSaving(false); + } + }; + if (loading) return ; const inputClass = 'w-full px-3.5 py-2.5 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent'; @@ -43,11 +111,11 @@ export default function SettingsPage() {

Settings

-

Manage your profile and preferences

+

Manage your profile, email, and password

-
- {/* Profile section */} + {/* Profile Information */} +
@@ -68,17 +136,6 @@ export default function SettingsPage() { className={inputClass} />
-
- - -
-
- -
+
+ +
+
+ + setProfile({ ...profile!, phone: e.target.value })} + className={inputClass} + /> +
+
+ + {/* Signature */} +
+ +