Migrate account settings

This commit is contained in:
2025-05-16 22:57:47 +02:00
parent 924d710b2f
commit c478e8e8a1
5 changed files with 112 additions and 40 deletions

View File

@@ -16,24 +16,38 @@ import SecuritySettings from './SecuritySettings';
import ProfileSettings from './ProfileSettings'; import ProfileSettings from './ProfileSettings';
import DangerZoneSettings from './DangerZoneSettings'; import DangerZoneSettings from './DangerZoneSettings';
import AccordionControl from '../AccordionControl'; import AccordionControl from '../AccordionControl';
import {
SettingsActionType,
UserProfileSettings,
ProfileSettingsState,
SettingsAction,
} from '../../../types/settings';
interface AccountSettingsProps {
opened: boolean;
onClose: () => void;
}
// Reducer for managing settings state // Reducer for managing settings state
const initialState = { const initialState: ProfileSettingsState = {
localSettings: {}, localSettings: {},
initialSettings: {}, initialSettings: {},
hasUnsavedChanges: false, hasUnsavedChanges: false,
}; };
function settingsReducer(state, action) { function settingsReducer(
state: ProfileSettingsState,
action: SettingsAction<UserProfileSettings>
): ProfileSettingsState {
switch (action.type) { switch (action.type) {
case 'INIT_SETTINGS': case SettingsActionType.INIT_SETTINGS:
return { return {
...state, ...state,
localSettings: action.payload, localSettings: action.payload || {},
initialSettings: action.payload, initialSettings: action.payload || {},
hasUnsavedChanges: false, hasUnsavedChanges: false,
}; };
case 'UPDATE_LOCAL_SETTINGS': case SettingsActionType.UPDATE_LOCAL_SETTINGS:
const newLocalSettings = { ...state.localSettings, ...action.payload }; const newLocalSettings = { ...state.localSettings, ...action.payload };
const hasChanges = const hasChanges =
JSON.stringify(newLocalSettings) !== JSON.stringify(newLocalSettings) !==
@@ -43,7 +57,7 @@ function settingsReducer(state, action) {
localSettings: newLocalSettings, localSettings: newLocalSettings,
hasUnsavedChanges: hasChanges, hasUnsavedChanges: hasChanges,
}; };
case 'MARK_SAVED': case SettingsActionType.MARK_SAVED:
return { return {
...state, ...state,
initialSettings: state.localSettings, initialSettings: state.localSettings,
@@ -54,33 +68,45 @@ function settingsReducer(state, action) {
} }
} }
const AccountSettings = ({ opened, onClose }) => { const AccountSettings: React.FC<AccountSettingsProps> = ({
opened,
onClose,
}) => {
const { user, refreshUser } = useAuth(); const { user, refreshUser } = useAuth();
const { loading, updateProfile } = useProfileSettings(); const { loading, updateProfile } = useProfileSettings();
const [state, dispatch] = useReducer(settingsReducer, initialState); const [state, dispatch] = useReducer(settingsReducer, initialState);
const isInitialMount = useRef(true); const isInitialMount = useRef<boolean>(true);
const [emailModalOpened, setEmailModalOpened] = useState(false); const [emailModalOpened, setEmailModalOpened] = useState<boolean>(false);
// Initialize settings on mount // Initialize settings on mount
useEffect(() => { useEffect(() => {
if (isInitialMount.current && user) { if (isInitialMount.current && user) {
isInitialMount.current = false; isInitialMount.current = false;
const settings = { const settings: UserProfileSettings = {
displayName: user.displayName, displayName: user.displayName,
email: user.email, email: user.email,
currentPassword: '', currentPassword: '',
newPassword: '', newPassword: '',
}; };
dispatch({ type: 'INIT_SETTINGS', payload: settings }); dispatch({
type: SettingsActionType.INIT_SETTINGS,
payload: settings,
});
} }
}, [user]); }, [user]);
const handleInputChange = (key, value) => { const handleInputChange = (
dispatch({ type: 'UPDATE_LOCAL_SETTINGS', payload: { [key]: value } }); key: keyof UserProfileSettings,
value: string
): void => {
dispatch({
type: SettingsActionType.UPDATE_LOCAL_SETTINGS,
payload: { [key]: value } as UserProfileSettings,
});
}; };
const handleSubmit = async () => { const handleSubmit = async (): Promise<void> => {
const updates = {}; const updates: UserProfileSettings = {};
const needsPasswordConfirmation = const needsPasswordConfirmation =
state.localSettings.email !== state.initialSettings.email; state.localSettings.email !== state.initialSettings.email;
@@ -113,10 +139,10 @@ const AccountSettings = ({ opened, onClose }) => {
} }
} }
const result = await updateProfile(updates); const updatedUser = await updateProfile(updates);
if (result.success) { if (updatedUser) {
await refreshUser(); await refreshUser();
dispatch({ type: 'MARK_SAVED' }); dispatch({ type: SettingsActionType.MARK_SAVED });
onClose(); onClose();
} }
} else { } else {
@@ -125,17 +151,20 @@ const AccountSettings = ({ opened, onClose }) => {
} }
}; };
const handleEmailConfirm = async (password) => { const handleEmailConfirm = async (password: string): Promise<void> => {
const updates = { const updates: UserProfileSettings = {
...state.localSettings, ...state.localSettings,
currentPassword: password, currentPassword: password,
}; };
// Remove any undefined/empty values // Remove any undefined/empty values
Object.keys(updates).forEach((key) => { Object.keys(updates).forEach((key) => {
if (updates[key] === undefined || updates[key] === '') { const typedKey = key as keyof UserProfileSettings;
delete updates[key]; if (updates[typedKey] === undefined || updates[typedKey] === '') {
delete updates[typedKey];
} }
}); });
// Remove keys that haven't changed // Remove keys that haven't changed
if (updates.displayName === state.initialSettings.displayName) { if (updates.displayName === state.initialSettings.displayName) {
delete updates.displayName; delete updates.displayName;
@@ -144,10 +173,10 @@ const AccountSettings = ({ opened, onClose }) => {
delete updates.email; delete updates.email;
} }
const result = await updateProfile(updates); const updatedUser = await updateProfile(updates);
if (result.success) { if (updatedUser) {
await refreshUser(); await refreshUser();
dispatch({ type: 'MARK_SAVED' }); dispatch({ type: SettingsActionType.MARK_SAVED });
setEmailModalOpened(false); setEmailModalOpened(false);
onClose(); onClose();
} }
@@ -162,7 +191,7 @@ const AccountSettings = ({ opened, onClose }) => {
centered centered
size="lg" size="lg"
> >
<Stack spacing="xl"> <Stack gap="xl">
{state.hasUnsavedChanges && ( {state.hasUnsavedChanges && (
<Badge color="yellow" variant="light"> <Badge color="yellow" variant="light">
Unsaved Changes Unsaved Changes
@@ -172,7 +201,7 @@ const AccountSettings = ({ opened, onClose }) => {
<Accordion <Accordion
defaultValue={['profile', 'security', 'danger']} defaultValue={['profile', 'security', 'danger']}
multiple multiple
styles={(theme) => ({ styles={(theme: any) => ({
control: { control: {
paddingTop: theme.spacing.md, paddingTop: theme.spacing.md,
paddingBottom: theme.spacing.md, paddingBottom: theme.spacing.md,
@@ -239,7 +268,7 @@ const AccountSettings = ({ opened, onClose }) => {
opened={emailModalOpened} opened={emailModalOpened}
onClose={() => setEmailModalOpened(false)} onClose={() => setEmailModalOpened(false)}
onConfirm={handleEmailConfirm} onConfirm={handleEmailConfirm}
email={state.localSettings.email} email={state.localSettings.email || ''}
/> />
</> </>
); );

View File

@@ -4,14 +4,14 @@ import DeleteAccountModal from '../../modals/account/DeleteAccountModal';
import { useAuth } from '../../../contexts/AuthContext'; import { useAuth } from '../../../contexts/AuthContext';
import { useProfileSettings } from '../../../hooks/useProfileSettings'; import { useProfileSettings } from '../../../hooks/useProfileSettings';
const DangerZoneSettings = () => { const DangerZoneSettings: React.FC = () => {
const { logout } = useAuth(); const { logout } = useAuth();
const { deleteAccount } = useProfileSettings(); const { deleteAccount } = useProfileSettings();
const [deleteModalOpened, setDeleteModalOpened] = useState(false); const [deleteModalOpened, setDeleteModalOpened] = useState<boolean>(false);
const handleDelete = async (password) => { const handleDelete = async (password: string): Promise<void> => {
const result = await deleteAccount(password); const success = await deleteAccount(password);
if (result.success) { if (success) {
setDeleteModalOpened(false); setDeleteModalOpened(false);
logout(); logout();
} }

View File

@@ -1,9 +1,18 @@
import React from 'react'; import React from 'react';
import { Box, Stack, TextInput } from '@mantine/core'; 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<ProfileSettingsProps> = ({
settings,
onInputChange,
}) => (
<Box> <Box>
<Stack spacing="md"> <Stack gap="md">
<TextInput <TextInput
label="Display Name" label="Display Name"
value={settings.displayName || ''} value={settings.displayName || ''}
@@ -20,4 +29,4 @@ const ProfileSettings = ({ settings, onInputChange }) => (
</Box> </Box>
); );
export default ProfileSettings; export default ProfileSettingsComponent;

View File

@@ -1,11 +1,22 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Box, PasswordInput, Stack, Text } from '@mantine/core'; 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<SecuritySettingsProps> = ({
settings,
onInputChange,
}) => {
const [confirmPassword, setConfirmPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
const handlePasswordChange = (field, value) => { const handlePasswordChange = (field: PasswordField, value: string) => {
if (field === 'confirmNewPassword') { if (field === 'confirmNewPassword') {
setConfirmPassword(value); setConfirmPassword(value);
// Check if passwords match when either password field changes // Check if passwords match when either password field changes
@@ -27,7 +38,7 @@ const SecuritySettings = ({ settings, onInputChange }) => {
return ( return (
<Box> <Box>
<Stack spacing="md"> <Stack gap="md">
<PasswordInput <PasswordInput
label="Current Password" label="Current Password"
value={settings.currentPassword || ''} value={settings.currentPassword || ''}

23
app/src/types/settings.ts Normal file
View File

@@ -0,0 +1,23 @@
export enum SettingsActionType {
INIT_SETTINGS = 'INIT_SETTINGS',
UPDATE_LOCAL_SETTINGS = 'UPDATE_LOCAL_SETTINGS',
MARK_SAVED = 'MARK_SAVED',
}
export interface UserProfileSettings {
displayName?: string;
email?: string;
currentPassword?: string;
newPassword?: string;
}
export interface ProfileSettingsState {
localSettings: UserProfileSettings;
initialSettings: UserProfileSettings;
hasUnsavedChanges: boolean;
}
export interface SettingsAction<T> {
type: SettingsActionType;
payload?: T;
}