diff --git a/frontend/src/components/AccountSettings.jsx b/frontend/src/components/AccountSettings.jsx index 8571866..c4ad495 100644 --- a/frontend/src/components/AccountSettings.jsx +++ b/frontend/src/components/AccountSettings.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useReducer, useRef } from 'react'; import { Modal, Badge, @@ -8,253 +8,85 @@ import { Stack, Accordion, TextInput, - Text, PasswordInput, Box, - LoadingOverlay, + Text, } from '@mantine/core'; +import { notifications } from '@mantine/notifications'; import { useAuth } from '../contexts/AuthContext'; import { useProfileSettings } from '../hooks/useProfileSettings'; -const AccordionControl = ({ children }) => ( - - {children} - -); - -const ProfileSettings = ({ displayName, email, onUpdate, loading }) => { - const [newDisplayName, setNewDisplayName] = useState(displayName || ''); - const [newEmail, setNewEmail] = useState(email); - const [currentPassword, setCurrentPassword] = useState(''); - const hasEmailChanges = newEmail !== email; - const hasDisplayNameChanges = newDisplayName !== displayName; - const hasChanges = hasEmailChanges || hasDisplayNameChanges; - - const handleSave = () => { - const updates = {}; - if (hasDisplayNameChanges) updates.displayName = newDisplayName; - if (hasEmailChanges) { - updates.email = newEmail; - updates.currentPassword = currentPassword; - } - onUpdate(updates); - }; - - return ( - - setNewDisplayName(e.currentTarget.value)} - placeholder="Enter display name" - /> - setNewEmail(e.currentTarget.value)} - placeholder="Enter email" - /> - {hasEmailChanges && ( - setCurrentPassword(e.currentTarget.value)} - placeholder="Required to change email" - required - /> - )} - {hasChanges && ( - - )} - - ); +// Reducer for managing settings state +const initialState = { + localSettings: {}, + initialSettings: {}, + hasUnsavedChanges: false, }; -const SecuritySettings = ({ onUpdate, loading }) => { - const [currentPassword, setCurrentPassword] = useState(''); - const [newPassword, setNewPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [error, setError] = useState(''); +function settingsReducer(state, action) { + switch (action.type) { + case 'INIT_SETTINGS': + return { + ...state, + localSettings: action.payload, + initialSettings: action.payload, + hasUnsavedChanges: false, + }; + case 'UPDATE_LOCAL_SETTINGS': + const newLocalSettings = { ...state.localSettings, ...action.payload }; + const hasChanges = + JSON.stringify(newLocalSettings) !== + JSON.stringify(state.initialSettings); + return { + ...state, + localSettings: newLocalSettings, + hasUnsavedChanges: hasChanges, + }; + case 'MARK_SAVED': + return { + ...state, + initialSettings: state.localSettings, + hasUnsavedChanges: false, + }; + default: + return state; + } +} - const handlePasswordChange = () => { - if (newPassword !== confirmPassword) { - setError('Passwords do not match'); - return; - } - if (newPassword.length < 8) { - setError('Password must be at least 8 characters long'); - return; - } - setError(''); - onUpdate({ currentPassword, newPassword }); - }; - - const hasChanges = currentPassword && newPassword && confirmPassword; - - return ( - - setCurrentPassword(e.currentTarget.value)} - placeholder="Enter current password" - /> - setNewPassword(e.currentTarget.value)} - placeholder="Enter new password" - /> - setConfirmPassword(e.currentTarget.value)} - placeholder="Confirm new password" - /> - {error && ( - - {error} - - )} - - Password must be at least 8 characters long - - {hasChanges && ( - - )} - - ); -}; - -const DangerZone = ({ onDelete, loading }) => { +// Password confirmation modal for email changes +const EmailPasswordModal = ({ opened, onClose, onConfirm, email }) => { const [password, setPassword] = useState(''); - const [confirmDelete, setConfirmDelete] = useState(false); - - const handleDelete = () => { - if (confirmDelete && password) { - onDelete(password); - } else { - setConfirmDelete(true); - } - }; - - return ( - - {confirmDelete && ( - setPassword(e.currentTarget.value)} - placeholder="Enter password to confirm" - required - /> - )} - - - - - ); -}; - -const AccountSettings = ({ opened, onClose }) => { - const { user, logout, refreshUser } = useAuth(); - const { loading, updateProfile, deleteAccount } = useProfileSettings(); - const [activeSection, setActiveSection] = useState(['profile']); - - const handleProfileUpdate = async (updates) => { - const result = await updateProfile(updates); - if (result.success) { - await refreshUser(); - } - }; - - const handleDelete = async (password) => { - const result = await deleteAccount(password); - if (result.success) { - onClose(); - logout(); - } - }; return ( Account Settings} + title="Confirm Password" centered - size="lg" + size="sm" > - - - ({ - control: { - paddingTop: theme.spacing.md, - paddingBottom: theme.spacing.md, - }, - item: { - borderBottom: `1px solid ${ - theme.colorScheme === 'dark' - ? theme.colors.dark[4] - : theme.colors.gray[3] - }`, - '&[data-active]': { - backgroundColor: - theme.colorScheme === 'dark' - ? theme.colors.dark[7] - : theme.colors.gray[0], - }, - }, - })} - > - - Profile - - - - - - - Security - - - - - - - Danger Zone - - - - - - - + + + Please enter your password to confirm changing your email to: {email} + + setPassword(e.currentTarget.value)} + required + /> + + @@ -262,4 +94,350 @@ const AccountSettings = ({ opened, onClose }) => { ); }; +// Delete account confirmation modal +const DeleteAccountModal = ({ opened, onClose, onConfirm }) => { + const [password, setPassword] = useState(''); + + return ( + + + + Warning: This action cannot be undone + + + Please enter your password to confirm account deletion. + + setPassword(e.currentTarget.value)} + required + /> + + + + + + + ); +}; + +const AccordionControl = ({ children }) => ( + + {children} + +); + +const ProfileSettings = ({ settings, onInputChange }) => ( + + + onInputChange('displayName', e.currentTarget.value)} + placeholder="Enter display name" + /> + onInputChange('email', e.currentTarget.value)} + placeholder="Enter email" + /> + + +); + +const SecuritySettings = ({ settings, onInputChange }) => { + const [confirmPassword, setConfirmPassword] = useState(''); + const [error, setError] = useState(''); + + const handlePasswordChange = (field, value) => { + if (field === 'confirmNewPassword') { + setConfirmPassword(value); + // Check if passwords match when either password field changes + if (value !== settings.newPassword) { + setError('Passwords do not match'); + } else { + setError(''); + } + } else { + onInputChange(field, value); + // Check if passwords match when either password field changes + if (field === 'newPassword' && value !== confirmPassword) { + setError('Passwords do not match'); + } else if (value === confirmPassword) { + setError(''); + } + } + }; + + return ( + + + + handlePasswordChange('currentPassword', e.currentTarget.value) + } + placeholder="Enter current password" + /> + + handlePasswordChange('newPassword', e.currentTarget.value) + } + placeholder="Enter new password" + /> + + handlePasswordChange('confirmNewPassword', e.currentTarget.value) + } + placeholder="Confirm new password" + error={error} + /> + + Password must be at least 8 characters long. Leave password fields + empty if you don't want to change it. + + + + ); +}; + +const DangerZone = ({ onDeleteClick }) => ( + + + +); + +const AccountSettings = ({ opened, onClose }) => { + const { user, logout, refreshUser } = useAuth(); + const { loading, updateProfile, deleteAccount } = useProfileSettings(); + const [state, dispatch] = useReducer(settingsReducer, initialState); + const isInitialMount = useRef(true); + const [deleteModalOpened, setDeleteModalOpened] = useState(false); + const [emailModalOpened, setEmailModalOpened] = useState(false); + + // Initialize settings on mount + React.useEffect(() => { + if (isInitialMount.current && user) { + isInitialMount.current = false; + const settings = { + displayName: user.displayName, + email: user.email, + currentPassword: '', + newPassword: '', + }; + dispatch({ type: 'INIT_SETTINGS', payload: settings }); + } + }, [user]); + + const handleInputChange = (key, value) => { + dispatch({ type: 'UPDATE_LOCAL_SETTINGS', payload: { [key]: value } }); + }; + + const handleSubmit = async () => { + const updates = {}; + const needsPasswordConfirmation = + state.localSettings.email !== state.initialSettings.email; + + // Add display name if changed + if (state.localSettings.displayName !== state.initialSettings.displayName) { + updates.displayName = state.localSettings.displayName; + } + + // Handle password change + if (state.localSettings.newPassword) { + if (!state.localSettings.currentPassword) { + notifications.show({ + title: 'Error', + message: 'Current password is required to change password', + color: 'red', + }); + return; + } + updates.newPassword = state.localSettings.newPassword; + updates.currentPassword = state.localSettings.currentPassword; + } + + // If we're only changing display name or have password already provided, proceed directly + if (!needsPasswordConfirmation || state.localSettings.currentPassword) { + if (needsPasswordConfirmation) { + updates.email = state.localSettings.email; + // If we don't have a password change, we still need to include the current password for email change + if (!updates.currentPassword) { + updates.currentPassword = state.localSettings.currentPassword; + } + } + + const result = await updateProfile(updates); + if (result.success) { + await refreshUser(); + dispatch({ type: 'MARK_SAVED' }); + onClose(); + } + } else { + // Only show the email confirmation modal if we don't already have the password + setEmailModalOpened(true); + } + }; + + const handleEmailConfirm = async (password) => { + const updates = { + ...state.localSettings, + currentPassword: password, + }; + // Remove any undefined/empty values + Object.keys(updates).forEach((key) => { + if (updates[key] === undefined || updates[key] === '') { + delete updates[key]; + } + }); + // Remove keys that haven't changed + if (updates.displayName === state.initialSettings.displayName) { + delete updates.displayName; + } + if (updates.email === state.initialSettings.email) { + delete updates.email; + } + + const result = await updateProfile(updates); + if (result.success) { + await refreshUser(); + dispatch({ type: 'MARK_SAVED' }); + setEmailModalOpened(false); + onClose(); + } + }; + + const handleDelete = async (password) => { + const result = await deleteAccount(password); + if (result.success) { + setDeleteModalOpened(false); + onClose(); + logout(); + } + }; + + return ( + <> + Account Settings} + centered + size="lg" + > + + {state.hasUnsavedChanges && ( + + Unsaved Changes + + )} + + ({ + control: { + paddingTop: theme.spacing.md, + paddingBottom: theme.spacing.md, + }, + item: { + borderBottom: `1px solid ${ + theme.colorScheme === 'dark' + ? theme.colors.dark[4] + : theme.colors.gray[3] + }`, + '&[data-active]': { + backgroundColor: + theme.colorScheme === 'dark' + ? theme.colors.dark[7] + : theme.colors.gray[0], + }, + }, + })} + > + + Profile + + + + + + + Security + + + + + + + Danger Zone + + setDeleteModalOpened(true)} /> + + + + + + + + + + + + setEmailModalOpened(false)} + onConfirm={handleEmailConfirm} + email={state.localSettings.email} + /> + + setDeleteModalOpened(false)} + onConfirm={handleDelete} + /> + + ); +}; + export default AccountSettings;