mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-07 08:24:27 +00:00
Imlement user update on frontend
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
71
frontend/src/hooks/useProfileSettings.js
Normal file
71
frontend/src/hooks/useProfileSettings.js
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user