import React, { useState, useReducer, useRef, useEffect } from 'react'; import { Modal, Badge, Button, Group, Title, Stack, Accordion, } from '@mantine/core'; import { notifications } from '@mantine/notifications'; import { useAuth } from '../../../contexts/AuthContext'; import { useProfileSettings } from '../../../hooks/useProfileSettings'; import EmailPasswordModal from '../../modals/account/EmailPasswordModal'; import SecuritySettings from './SecuritySettings'; import ProfileSettings from './ProfileSettings'; import DangerZoneSettings from './DangerZoneSettings'; import AccordionControl from '../AccordionControl'; import { SettingsActionType, UserProfileSettings, ProfileSettingsState, SettingsAction, } from '../../../types/settings'; interface AccountSettingsProps { opened: boolean; onClose: () => void; } // Reducer for managing settings state const initialState: ProfileSettingsState = { localSettings: {}, initialSettings: {}, hasUnsavedChanges: false, }; function settingsReducer( state: ProfileSettingsState, action: SettingsAction ): ProfileSettingsState { switch (action.type) { case SettingsActionType.INIT_SETTINGS: return { ...state, localSettings: action.payload || {}, initialSettings: action.payload || {}, hasUnsavedChanges: false, }; case SettingsActionType.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 SettingsActionType.MARK_SAVED: return { ...state, initialSettings: state.localSettings, hasUnsavedChanges: false, }; default: return state; } } const AccountSettings: React.FC = ({ opened, onClose, }) => { const { user, refreshUser } = useAuth(); const { loading, updateProfile } = useProfileSettings(); const [state, dispatch] = useReducer(settingsReducer, initialState); const isInitialMount = useRef(true); const [emailModalOpened, setEmailModalOpened] = useState(false); // Initialize settings on mount useEffect(() => { if (isInitialMount.current && user) { isInitialMount.current = false; const settings: UserProfileSettings = { displayName: user.displayName || '', email: user.email, currentPassword: '', newPassword: '', }; dispatch({ type: SettingsActionType.INIT_SETTINGS, payload: settings, }); } }, [user]); const handleInputChange = ( key: keyof UserProfileSettings, value: string ): void => { dispatch({ type: SettingsActionType.UPDATE_LOCAL_SETTINGS, payload: { [key]: value } as UserProfileSettings, }); }; const handleSubmit = async (): Promise => { const updates: UserProfileSettings = {}; 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 updatedUser = await updateProfile(updates); if (updatedUser) { await refreshUser(); dispatch({ type: SettingsActionType.MARK_SAVED }); onClose(); } } else { // Only show the email confirmation modal if we don't already have the password setEmailModalOpened(true); } }; const handleEmailConfirm = async (password: string): Promise => { const updates: UserProfileSettings = { ...state.localSettings, currentPassword: password, }; // Remove any undefined/empty values Object.keys(updates).forEach((key) => { const typedKey = key as keyof UserProfileSettings; if (updates[typedKey] === undefined || updates[typedKey] === '') { delete updates[typedKey]; } }); // 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 updatedUser = await updateProfile(updates); if (updatedUser) { await refreshUser(); dispatch({ type: SettingsActionType.MARK_SAVED }); setEmailModalOpened(false); onClose(); } }; 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 setEmailModalOpened(false)} onConfirm={handleEmailConfirm} email={state.localSettings.email || ''} /> ); }; export default AccountSettings;