mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 15:44:21 +00:00
Migrate account settings
This commit is contained in:
@@ -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 || ''}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
@@ -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
23
app/src/types/settings.ts
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user