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;