mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-06 16:04:23 +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 {
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
|
||||
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 { 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();
|
||||
|
||||
Reference in New Issue
Block a user