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

View File

@@ -8,7 +8,7 @@ import {
Text,
Divider,
} 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 AccountSettings from './AccountSettings';
@@ -76,7 +76,7 @@ const UserMenu = () => {
})}
>
<Group>
<IconUserCircle size={16} />
<IconSettings size={16} />
<Text size="sm">Account Settings</Text>
</Group>
</UnstyledButton>

View File

@@ -89,6 +89,15 @@ export const AuthProvider = ({ children }) => {
}
}, [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 = {
user,
loading,
@@ -96,6 +105,7 @@ export const AuthProvider = ({ children }) => {
login,
logout,
refreshToken,
refreshUser,
};
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 { 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 () => {
const response = await apiCall(`${API_BASE_URL}/workspaces/last`);
return response.json();