From e56378f1f09e08b1601846a2f57b0bcad956fdd7 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Tue, 5 Nov 2024 21:56:08 +0100 Subject: [PATCH] Imlement user update on frontend --- frontend/src/components/AccountSettings.jsx | 237 +++++++++++++++----- frontend/src/components/UserMenu.jsx | 4 +- frontend/src/contexts/AuthContext.jsx | 10 + frontend/src/hooks/useProfileSettings.js | 71 ++++++ frontend/src/services/api.js | 16 ++ 5 files changed, 275 insertions(+), 63 deletions(-) create mode 100644 frontend/src/hooks/useProfileSettings.js diff --git a/frontend/src/components/AccountSettings.jsx b/frontend/src/components/AccountSettings.jsx index 191564b..8571866 100644 --- a/frontend/src/components/AccountSettings.jsx +++ b/frontend/src/components/AccountSettings.jsx @@ -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 }) => ( @@ -20,56 +22,172 @@ const AccordionControl = ({ children }) => ( ); -const ProfileSettings = ({ displayName, email }) => ( - - - - -); +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 = () => ( - - - - - - Password must be at least 8 characters long and contain at least one - uppercase letter, one lowercase letter, one number, and one special - character. - - -); + const handleSave = () => { + const updates = {}; + if (hasDisplayNameChanges) updates.displayName = newDisplayName; + if (hasEmailChanges) { + updates.email = newEmail; + updates.currentPassword = currentPassword; + } + onUpdate(updates); + }; -const DangerZone = () => ( - - - - - -); + return ( + + setNewDisplayName(e.currentTarget.value)} + placeholder="Enter display name" + /> + setNewEmail(e.currentTarget.value)} + placeholder="Enter email" + /> + {hasEmailChanges && ( + setCurrentPassword(e.currentTarget.value)} + placeholder="Required to change email" + required + /> + )} + {hasChanges && ( + + )} + + ); +}; + +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 ( + + setCurrentPassword(e.currentTarget.value)} + placeholder="Enter current password" + /> + setNewPassword(e.currentTarget.value)} + placeholder="Enter new password" + /> + setConfirmPassword(e.currentTarget.value)} + placeholder="Confirm new password" + /> + {error && ( + + {error} + + )} + + Password must be at least 8 characters long + + {hasChanges && ( + + )} + + ); +}; + +const DangerZone = ({ onDelete, loading }) => { + const [password, setPassword] = useState(''); + const [confirmDelete, setConfirmDelete] = useState(false); + + const handleDelete = () => { + if (confirmDelete && password) { + onDelete(password); + } else { + setConfirmDelete(true); + } + }; + + return ( + + {confirmDelete && ( + setPassword(e.currentTarget.value)} + placeholder="Enter password to confirm" + required + /> + )} + + + + + ); +}; 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 ( { centered size="lg" > + - - Changes are currently disabled - - ({ control: { @@ -104,11 +220,6 @@ const AccountSettings = ({ opened, onClose }) => { : theme.colors.gray[0], }, }, - chevron: { - '&[data-rotate]': { - transform: 'rotate(180deg)', - }, - }, })} > @@ -117,6 +228,8 @@ const AccountSettings = ({ opened, onClose }) => { @@ -124,23 +237,25 @@ const AccountSettings = ({ opened, onClose }) => { Security - + Danger Zone - + - diff --git a/frontend/src/components/UserMenu.jsx b/frontend/src/components/UserMenu.jsx index bca0295..6d50b44 100644 --- a/frontend/src/components/UserMenu.jsx +++ b/frontend/src/components/UserMenu.jsx @@ -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 = () => { })} > - + Account Settings diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx index 2a716d8..a93d0c5 100644 --- a/frontend/src/contexts/AuthContext.jsx +++ b/frontend/src/contexts/AuthContext.jsx @@ -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 {children}; diff --git a/frontend/src/hooks/useProfileSettings.js b/frontend/src/hooks/useProfileSettings.js new file mode 100644 index 0000000..381e784 --- /dev/null +++ b/frontend/src/hooks/useProfileSettings.js @@ -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, + }; +} diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index ad7e84f..dc8c1df 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -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();