From c478e8e8a1cfc2283a10542d1f5f79e162741b54 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Fri, 16 May 2025 22:57:47 +0200 Subject: [PATCH] Migrate account settings --- ...ccountSettings.jsx => AccountSettings.tsx} | 87 ++++++++++++------- ...oneSettings.jsx => DangerZoneSettings.tsx} | 10 +-- ...rofileSettings.jsx => ProfileSettings.tsx} | 15 +++- ...uritySettings.jsx => SecuritySettings.tsx} | 17 +++- app/src/types/settings.ts | 23 +++++ 5 files changed, 112 insertions(+), 40 deletions(-) rename app/src/components/settings/account/{AccountSettings.jsx => AccountSettings.tsx} (76%) rename app/src/components/settings/account/{DangerZoneSettings.jsx => DangerZoneSettings.tsx} (79%) rename app/src/components/settings/account/{ProfileSettings.jsx => ProfileSettings.tsx} (58%) rename app/src/components/settings/account/{SecuritySettings.jsx => SecuritySettings.tsx} (80%) create mode 100644 app/src/types/settings.ts diff --git a/app/src/components/settings/account/AccountSettings.jsx b/app/src/components/settings/account/AccountSettings.tsx similarity index 76% rename from app/src/components/settings/account/AccountSettings.jsx rename to app/src/components/settings/account/AccountSettings.tsx index 098240b..71b2731 100644 --- a/app/src/components/settings/account/AccountSettings.jsx +++ b/app/src/components/settings/account/AccountSettings.tsx @@ -16,24 +16,38 @@ 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 = { +const initialState: ProfileSettingsState = { localSettings: {}, initialSettings: {}, hasUnsavedChanges: false, }; -function settingsReducer(state, action) { +function settingsReducer( + state: ProfileSettingsState, + action: SettingsAction +): ProfileSettingsState { switch (action.type) { - case 'INIT_SETTINGS': + case SettingsActionType.INIT_SETTINGS: return { ...state, - localSettings: action.payload, - initialSettings: action.payload, + localSettings: action.payload || {}, + initialSettings: action.payload || {}, hasUnsavedChanges: false, }; - case 'UPDATE_LOCAL_SETTINGS': + case SettingsActionType.UPDATE_LOCAL_SETTINGS: const newLocalSettings = { ...state.localSettings, ...action.payload }; const hasChanges = JSON.stringify(newLocalSettings) !== @@ -43,7 +57,7 @@ function settingsReducer(state, action) { localSettings: newLocalSettings, hasUnsavedChanges: hasChanges, }; - case 'MARK_SAVED': + case SettingsActionType.MARK_SAVED: return { ...state, initialSettings: state.localSettings, @@ -54,33 +68,45 @@ function settingsReducer(state, action) { } } -const AccountSettings = ({ opened, onClose }) => { +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); + const isInitialMount = useRef(true); + const [emailModalOpened, setEmailModalOpened] = useState(false); // Initialize settings on mount useEffect(() => { if (isInitialMount.current && user) { isInitialMount.current = false; - const settings = { + const settings: UserProfileSettings = { displayName: user.displayName, email: user.email, currentPassword: '', newPassword: '', }; - dispatch({ type: 'INIT_SETTINGS', payload: settings }); + dispatch({ + type: SettingsActionType.INIT_SETTINGS, + payload: settings, + }); } }, [user]); - const handleInputChange = (key, value) => { - dispatch({ type: 'UPDATE_LOCAL_SETTINGS', payload: { [key]: value } }); + const handleInputChange = ( + key: keyof UserProfileSettings, + value: string + ): void => { + dispatch({ + type: SettingsActionType.UPDATE_LOCAL_SETTINGS, + payload: { [key]: value } as UserProfileSettings, + }); }; - const handleSubmit = async () => { - const updates = {}; + const handleSubmit = async (): Promise => { + const updates: UserProfileSettings = {}; const needsPasswordConfirmation = state.localSettings.email !== state.initialSettings.email; @@ -113,10 +139,10 @@ const AccountSettings = ({ opened, onClose }) => { } } - const result = await updateProfile(updates); - if (result.success) { + const updatedUser = await updateProfile(updates); + if (updatedUser) { await refreshUser(); - dispatch({ type: 'MARK_SAVED' }); + dispatch({ type: SettingsActionType.MARK_SAVED }); onClose(); } } else { @@ -125,17 +151,20 @@ const AccountSettings = ({ opened, onClose }) => { } }; - const handleEmailConfirm = async (password) => { - const updates = { + const handleEmailConfirm = async (password: string): Promise => { + const updates: UserProfileSettings = { ...state.localSettings, currentPassword: password, }; + // Remove any undefined/empty values Object.keys(updates).forEach((key) => { - if (updates[key] === undefined || updates[key] === '') { - delete updates[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; @@ -144,10 +173,10 @@ const AccountSettings = ({ opened, onClose }) => { delete updates.email; } - const result = await updateProfile(updates); - if (result.success) { + const updatedUser = await updateProfile(updates); + if (updatedUser) { await refreshUser(); - dispatch({ type: 'MARK_SAVED' }); + dispatch({ type: SettingsActionType.MARK_SAVED }); setEmailModalOpened(false); onClose(); } @@ -162,7 +191,7 @@ const AccountSettings = ({ opened, onClose }) => { centered size="lg" > - + {state.hasUnsavedChanges && ( Unsaved Changes @@ -172,7 +201,7 @@ const AccountSettings = ({ opened, onClose }) => { ({ + styles={(theme: any) => ({ control: { paddingTop: theme.spacing.md, paddingBottom: theme.spacing.md, @@ -239,7 +268,7 @@ const AccountSettings = ({ opened, onClose }) => { opened={emailModalOpened} onClose={() => setEmailModalOpened(false)} onConfirm={handleEmailConfirm} - email={state.localSettings.email} + email={state.localSettings.email || ''} /> ); diff --git a/app/src/components/settings/account/DangerZoneSettings.jsx b/app/src/components/settings/account/DangerZoneSettings.tsx similarity index 79% rename from app/src/components/settings/account/DangerZoneSettings.jsx rename to app/src/components/settings/account/DangerZoneSettings.tsx index 9fed1bc..c324c01 100644 --- a/app/src/components/settings/account/DangerZoneSettings.jsx +++ b/app/src/components/settings/account/DangerZoneSettings.tsx @@ -4,14 +4,14 @@ import DeleteAccountModal from '../../modals/account/DeleteAccountModal'; import { useAuth } from '../../../contexts/AuthContext'; import { useProfileSettings } from '../../../hooks/useProfileSettings'; -const DangerZoneSettings = () => { +const DangerZoneSettings: React.FC = () => { const { logout } = useAuth(); const { deleteAccount } = useProfileSettings(); - const [deleteModalOpened, setDeleteModalOpened] = useState(false); + const [deleteModalOpened, setDeleteModalOpened] = useState(false); - const handleDelete = async (password) => { - const result = await deleteAccount(password); - if (result.success) { + const handleDelete = async (password: string): Promise => { + const success = await deleteAccount(password); + if (success) { setDeleteModalOpened(false); logout(); } diff --git a/app/src/components/settings/account/ProfileSettings.jsx b/app/src/components/settings/account/ProfileSettings.tsx similarity index 58% rename from app/src/components/settings/account/ProfileSettings.jsx rename to app/src/components/settings/account/ProfileSettings.tsx index 687df65..c8434b6 100644 --- a/app/src/components/settings/account/ProfileSettings.jsx +++ b/app/src/components/settings/account/ProfileSettings.tsx @@ -1,9 +1,18 @@ import React from 'react'; import { Box, Stack, TextInput } from '@mantine/core'; +import { UserProfileSettings } from '../../../types/settings'; -const ProfileSettings = ({ settings, onInputChange }) => ( +interface ProfileSettingsProps { + settings: UserProfileSettings; + onInputChange: (key: keyof UserProfileSettings, value: string) => void; +} + +const ProfileSettingsComponent: React.FC = ({ + settings, + onInputChange, +}) => ( - + ( ); -export default ProfileSettings; +export default ProfileSettingsComponent; diff --git a/app/src/components/settings/account/SecuritySettings.jsx b/app/src/components/settings/account/SecuritySettings.tsx similarity index 80% rename from app/src/components/settings/account/SecuritySettings.jsx rename to app/src/components/settings/account/SecuritySettings.tsx index 3568ebc..c11b1e1 100644 --- a/app/src/components/settings/account/SecuritySettings.jsx +++ b/app/src/components/settings/account/SecuritySettings.tsx @@ -1,11 +1,22 @@ import React, { useState } from 'react'; import { Box, PasswordInput, Stack, Text } from '@mantine/core'; +import { UserProfileSettings } from '@/types/settings'; -const SecuritySettings = ({ settings, onInputChange }) => { +interface SecuritySettingsProps { + settings: UserProfileSettings; + onInputChange: (key: keyof UserProfileSettings, value: string) => void; +} + +type PasswordField = 'currentPassword' | 'newPassword' | 'confirmNewPassword'; + +const SecuritySettings: React.FC = ({ + settings, + onInputChange, +}) => { const [confirmPassword, setConfirmPassword] = useState(''); const [error, setError] = useState(''); - const handlePasswordChange = (field, value) => { + const handlePasswordChange = (field: PasswordField, value: string) => { if (field === 'confirmNewPassword') { setConfirmPassword(value); // Check if passwords match when either password field changes @@ -27,7 +38,7 @@ const SecuritySettings = ({ settings, onInputChange }) => { return ( - + { + type: SettingsActionType; + payload?: T; +}