Imlement user update on frontend

This commit is contained in:
2024-11-05 21:56:08 +01:00
parent 505b93ff09
commit e56378f1f0
5 changed files with 275 additions and 63 deletions

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import { import {
Modal, Modal,
Badge, Badge,
@@ -11,8 +11,10 @@ import {
Text, Text,
PasswordInput, PasswordInput,
Box, Box,
LoadingOverlay,
} from '@mantine/core'; } from '@mantine/core';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { useProfileSettings } from '../hooks/useProfileSettings';
const AccordionControl = ({ children }) => ( const AccordionControl = ({ children }) => (
<Accordion.Control> <Accordion.Control>
@@ -20,56 +22,172 @@ const AccordionControl = ({ children }) => (
</Accordion.Control> </Accordion.Control>
); );
const ProfileSettings = ({ displayName, email }) => ( const ProfileSettings = ({ displayName, email, onUpdate, loading }) => {
<Stack spacing="md"> const [newDisplayName, setNewDisplayName] = useState(displayName || '');
<TextInput label="Display Name" defaultValue={displayName || ''} disabled /> const [newEmail, setNewEmail] = useState(email);
<TextInput label="Email" defaultValue={email} disabled /> const [currentPassword, setCurrentPassword] = useState('');
</Stack> const hasEmailChanges = newEmail !== email;
); const hasDisplayNameChanges = newDisplayName !== displayName;
const hasChanges = hasEmailChanges || hasDisplayNameChanges;
const SecuritySettings = () => ( const handleSave = () => {
const updates = {};
if (hasDisplayNameChanges) updates.displayName = newDisplayName;
if (hasEmailChanges) {
updates.email = newEmail;
updates.currentPassword = currentPassword;
}
onUpdate(updates);
};
return (
<Stack spacing="md">
<TextInput
label="Display Name"
value={newDisplayName}
onChange={(e) => setNewDisplayName(e.currentTarget.value)}
placeholder="Enter display name"
/>
<TextInput
label="Email"
value={newEmail}
onChange={(e) => setNewEmail(e.currentTarget.value)}
placeholder="Enter email"
/>
{hasEmailChanges && (
<PasswordInput
label="Current Password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.currentTarget.value)}
placeholder="Required to change email"
required
/>
)}
{hasChanges && (
<Button onClick={handleSave} loading={loading}>
Save Changes
</Button>
)}
</Stack>
);
};
const SecuritySettings = ({ onUpdate, loading }) => {
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
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 (
<Stack spacing="md"> <Stack spacing="md">
<PasswordInput <PasswordInput
label="Current Password" label="Current Password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.currentTarget.value)}
placeholder="Enter current password" placeholder="Enter current password"
disabled
/> />
<PasswordInput <PasswordInput
label="New Password" label="New Password"
value={newPassword}
onChange={(e) => setNewPassword(e.currentTarget.value)}
placeholder="Enter new password" placeholder="Enter new password"
disabled
/> />
<PasswordInput <PasswordInput
label="Confirm New Password" label="Confirm New Password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.currentTarget.value)}
placeholder="Confirm new password" placeholder="Confirm new password"
disabled
/> />
<Text size="xs" c="dimmed"> {error && (
Password must be at least 8 characters long and contain at least one <Text color="red" size="sm">
uppercase letter, one lowercase letter, one number, and one special {error}
character.
</Text> </Text>
)}
<Text size="xs" c="dimmed">
Password must be at least 8 characters long
</Text>
{hasChanges && (
<Button onClick={handlePasswordChange} loading={loading}>
Change Password
</Button>
)}
</Stack> </Stack>
); );
};
const DangerZone = () => ( const DangerZone = ({ onDelete, loading }) => {
const [password, setPassword] = useState('');
const [confirmDelete, setConfirmDelete] = useState(false);
const handleDelete = () => {
if (confirmDelete && password) {
onDelete(password);
} else {
setConfirmDelete(true);
}
};
return (
<Stack spacing="md"> <Stack spacing="md">
{confirmDelete && (
<PasswordInput
label="Current Password"
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
placeholder="Enter password to confirm"
required
/>
)}
<Box mb="md"> <Box mb="md">
<Button <Button
color="red" color="red"
variant="light" variant="light"
onClick={() => console.log('Delete Account')} onClick={handleDelete}
fullWidth fullWidth
disabled loading={loading}
> >
Delete Account {confirmDelete ? 'Confirm Delete Account' : 'Delete Account'}
</Button> </Button>
</Box> </Box>
</Stack> </Stack>
); );
};
const AccountSettings = ({ opened, onClose }) => { const AccountSettings = ({ opened, onClose }) => {
const { user } = useAuth(); 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 ( return (
<Modal <Modal
opened={opened} opened={opened}
@@ -78,13 +196,11 @@ const AccountSettings = ({ opened, onClose }) => {
centered centered
size="lg" size="lg"
> >
<LoadingOverlay visible={loading} />
<Stack spacing="xl"> <Stack spacing="xl">
<Badge color="yellow" variant="light">
Changes are currently disabled
</Badge>
<Accordion <Accordion
defaultValue={['profile', 'security', 'danger']} value={activeSection}
onChange={setActiveSection}
multiple multiple
styles={(theme) => ({ styles={(theme) => ({
control: { control: {
@@ -104,11 +220,6 @@ const AccountSettings = ({ opened, onClose }) => {
: theme.colors.gray[0], : theme.colors.gray[0],
}, },
}, },
chevron: {
'&[data-rotate]': {
transform: 'rotate(180deg)',
},
},
})} })}
> >
<Accordion.Item value="profile"> <Accordion.Item value="profile">
@@ -117,6 +228,8 @@ const AccountSettings = ({ opened, onClose }) => {
<ProfileSettings <ProfileSettings
displayName={user.displayName} displayName={user.displayName}
email={user.email} email={user.email}
onUpdate={handleProfileUpdate}
loading={loading}
/> />
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
@@ -124,23 +237,25 @@ const AccountSettings = ({ opened, onClose }) => {
<Accordion.Item value="security"> <Accordion.Item value="security">
<AccordionControl>Security</AccordionControl> <AccordionControl>Security</AccordionControl>
<Accordion.Panel> <Accordion.Panel>
<SecuritySettings /> <SecuritySettings
onUpdate={handleProfileUpdate}
loading={loading}
/>
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
<Accordion.Item value="danger"> <Accordion.Item value="danger">
<AccordionControl>Danger Zone</AccordionControl> <AccordionControl>Danger Zone</AccordionControl>
<Accordion.Panel> <Accordion.Panel>
<DangerZone /> <DangerZone onDelete={handleDelete} loading={loading} />
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
</Accordion> </Accordion>
<Group justify="flex-end"> <Group justify="flex-end">
<Button variant="default" onClick={onClose}> <Button variant="default" onClick={onClose}>
Cancel Close
</Button> </Button>
<Button disabled>Save Changes</Button>
</Group> </Group>
</Stack> </Stack>
</Modal> </Modal>

View File

@@ -8,7 +8,7 @@ import {
Text, Text,
Divider, Divider,
} from '@mantine/core'; } from '@mantine/core';
import { IconUser, IconLogout, IconUserCircle } from '@tabler/icons-react'; import { IconUser, IconLogout, IconSettings } from '@tabler/icons-react';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import AccountSettings from './AccountSettings'; import AccountSettings from './AccountSettings';
@@ -76,7 +76,7 @@ const UserMenu = () => {
})} })}
> >
<Group> <Group>
<IconUserCircle size={16} /> <IconSettings size={16} />
<Text size="sm">Account Settings</Text> <Text size="sm">Account Settings</Text>
</Group> </Group>
</UnstyledButton> </UnstyledButton>

View File

@@ -89,6 +89,15 @@ export const AuthProvider = ({ children }) => {
} }
}, [logout]); }, [logout]);
const refreshUser = useCallback(async () => {
try {
const userData = await authApi.getCurrentUser();
setUser(userData);
} catch (error) {
console.error('Failed to refresh user data:', error);
}
}, []);
const value = { const value = {
user, user,
loading, loading,
@@ -96,6 +105,7 @@ export const AuthProvider = ({ children }) => {
login, login,
logout, logout,
refreshToken, refreshToken,
refreshUser,
}; };
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;

View File

@@ -0,0 +1,71 @@
import { useState, useCallback } from 'react';
import { notifications } from '@mantine/notifications';
import { updateProfile, deleteProfile } from '../services/api';
export function useProfileSettings() {
const [loading, setLoading] = useState(false);
const handleProfileUpdate = useCallback(async (updates) => {
setLoading(true);
try {
const updatedUser = await updateProfile(updates);
notifications.show({
title: 'Success',
message: 'Profile updated successfully',
color: 'green',
});
return { success: true, user: updatedUser };
} catch (error) {
let errorMessage = 'Failed to update profile';
if (error.message.includes('password')) {
errorMessage = 'Current password is incorrect';
} else if (error.message.includes('email')) {
errorMessage = 'Email is already in use';
}
notifications.show({
title: 'Error',
message: errorMessage,
color: 'red',
});
return { success: false, error: error.message };
} finally {
setLoading(false);
}
}, []);
const handleAccountDeletion = useCallback(async (password) => {
setLoading(true);
try {
await deleteProfile(password);
notifications.show({
title: 'Success',
message: 'Account deleted successfully',
color: 'green',
});
return { success: true };
} catch (error) {
notifications.show({
title: 'Error',
message: error.message || 'Failed to delete account',
color: 'red',
});
return { success: false, error: error.message };
} finally {
setLoading(false);
}
}, []);
return {
loading,
updateProfile: handleProfileUpdate,
deleteAccount: handleAccountDeletion,
};
}

View File

@@ -1,6 +1,22 @@
import { API_BASE_URL } from '../utils/constants'; import { API_BASE_URL } from '../utils/constants';
import { apiCall } from './authApi'; import { apiCall } from './authApi';
export const updateProfile = async (updates) => {
const response = await apiCall(`${API_BASE_URL}/profile`, {
method: 'PUT',
body: JSON.stringify(updates),
});
return response.json();
};
export const deleteProfile = async (password) => {
const response = await apiCall(`${API_BASE_URL}/profile`, {
method: 'DELETE',
body: JSON.stringify({ password }),
});
return response.json();
};
export const fetchLastWorkspaceName = async () => { export const fetchLastWorkspaceName = async () => {
const response = await apiCall(`${API_BASE_URL}/workspaces/last`); const response = await apiCall(`${API_BASE_URL}/workspaces/last`);
return response.json(); return response.json();