From a5b0f43c85bd13ce59c79ba6008309668c55aad2 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 29 Jul 2025 14:50:47 +0800 Subject: [PATCH 01/20] working on new Profile page --- src/components/Navbar.jsx | 2 +- src/main.jsx | 3 +- src/pages/Login.jsx | 2 +- src/pages/Profile.jsx | 72 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 src/pages/Profile.jsx diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index b5a8314..c4c031a 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -16,7 +16,7 @@ function Navbar() { } const handleProfileClick = () => { - navigate('/sampleProtected'); + navigate('/profile'); } const toggleSidebar = () => { diff --git a/src/main.jsx b/src/main.jsx index 96f4afa..ee62501 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -21,6 +21,7 @@ import ProtectedLayout from './ProtectedLayout.jsx'; import AnimateIn from './AnimateIn.jsx'; import PublicGallery from './pages/PublicGallery.jsx'; import PublicProfile from './pages/PublicProfile.jsx'; +import Profile from './pages/Profile.jsx'; const store = configureStore({ reducer: { @@ -49,7 +50,7 @@ createRoot(document.getElementById('root')).render( {/* Protected Pages */} }> - } /> + } /> diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index edbd086..6308a50 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -51,7 +51,7 @@ function Login() { // Success case dispatch(fetchSession()); - navigate("/sampleProtected"); + navigate("/profile"); ToastWizard.standard("success", "Login successful.", "Welcome back to ArchAIve!") } else { console.log("Unexpected response in login request:", res.data); diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx new file mode 100644 index 0000000..a30abd3 --- /dev/null +++ b/src/pages/Profile.jsx @@ -0,0 +1,72 @@ +import { Avatar, Box, Field, Flex, HStack, Input, Spacer, Stack, Text, useMediaQuery } from '@chakra-ui/react' +import React from 'react' + +function Profile() { + const [isSmallerThan800] = useMediaQuery("(max-width: 800px)"); + + return ( + + + + + + + John Appleseed + + {/* + Name & Account Details + + + + First Name + + + + Last Name + + + + Username + + + + */} + + {/* + + First Name + + + + First Name + + + + First Name + + + */} + + + + First Name + + + + + Last Name + + + + + User Name + + + + + + + ) +} + +export default Profile \ No newline at end of file From 29c207b2b40936e3c602608b7330a4c09d63b873 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 29 Jul 2025 15:29:05 +0800 Subject: [PATCH 02/20] changed to grid-based layout instead --- package.json | 3 +- src/pages/Profile.jsx | 114 +++++++++++++++++++++++++----------------- 2 files changed, 69 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index 07ac251..41fd265 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "react-icons": "^5.5.0", "react-redux": "^9.2.0", "react-router-dom": "^7.6.2", - "redux": "^5.0.1" + "redux": "^5.0.1", + "use-mask-input": "^3.5.0" }, "devDependencies": { "@chakra-ui/cli": "^3.21.1", diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index a30abd3..e071216 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -1,71 +1,91 @@ -import { Avatar, Box, Field, Flex, HStack, Input, Spacer, Stack, Text, useMediaQuery } from '@chakra-ui/react' +import { Avatar, Box, Button, Field, Flex, Grid, GridItem, HStack, Input, Spacer, Stack, Text, useMediaQuery, VStack } from '@chakra-ui/react' import React from 'react' +import { withMask } from 'use-mask-input'; function Profile() { const [isSmallerThan800] = useMediaQuery("(max-width: 800px)"); return ( - - - - - + + + + + + - John Appleseed + John Appleseed + Research Consultant, National Museum - {/* - Name & Account Details + Name & Account Details - - + + First Name - + - + + Last Name - + - + + Username - + - - */} + + + - {/* - - First Name - - - - First Name - + + + Contact Details + + Email + - - First Name - + + Contact + - */} + + - - - First Name - - - - - Last Name - + + + Security & Authentication + + Change Password + + {/* + Confirm Password + + */} + - - User Name - - - - - - + + + + + + + ) } From 7e755d8ba902b78305f566cfd58bc6939ec9231d Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 29 Jul 2025 15:38:05 +0800 Subject: [PATCH 03/20] slight UI refinements --- src/pages/Profile.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index e071216..55a3c6d 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -8,8 +8,8 @@ function Profile() { return ( - + @@ -53,7 +53,7 @@ function Profile() { - + Contact Details Email @@ -67,7 +67,7 @@ function Profile() { - + Security & Authentication Change Password @@ -79,7 +79,7 @@ function Profile() { */} - + From 0c2fd0bd9e6f0e5a2c9fcbc676ecffc98e797090 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 29 Jul 2025 15:46:05 +0800 Subject: [PATCH 04/20] added logout functionality to profile --- src/pages/Profile.jsx | 67 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 55a3c6d..3be1500 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -1,26 +1,63 @@ import { Avatar, Box, Button, Field, Flex, Grid, GridItem, HStack, Input, Spacer, Stack, Text, useMediaQuery, VStack } from '@chakra-ui/react' -import React from 'react' +import { useState } from 'react' +import { useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; import { withMask } from 'use-mask-input'; +import { logout } from '../slices/AuthState'; +import ToastWizard from '../components/toastWizard'; function Profile() { + const navigate = useNavigate(); + const dispatch = useDispatch(); + const [loggingOut, setLoggingOut] = useState(false); + const [isSmallerThan800] = useMediaQuery("(max-width: 800px)"); + const handleLogout = () => { + // ToastWizard.standard("success", "Logout successful.", "See you soon!"); + setLoggingOut(true); + const logoutPromise = new Promise((resolve, reject) => { + dispatch(logout(true, (msg) => { + setLoggingOut(false); + if (typeof msg === 'string') { + reject(msg); + } else { + navigate('/auth/login'); + resolve(msg); + } + })); + }) + + ToastWizard.promiseBased( + logoutPromise, + "Logged out successfully.", + "You have been logged out.", + "Logout failed.", + "Something went wrong while logging out. Please try again.", + "Logging out...", + "Connecting to server...", + handleLogout, + "Retry", + ['error'] + ) + } + return ( @@ -80,8 +117,10 @@ function Profile() { - - + + + + From e4fdf4be3c2d148c871461c67dabafbe822edecb Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 29 Jul 2025 16:45:18 +0800 Subject: [PATCH 05/20] added getAccountInfo request to Profile --- src/pages/Profile.jsx | 59 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 3be1500..d762b24 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -1,15 +1,25 @@ import { Avatar, Box, Button, Field, Flex, Grid, GridItem, HStack, Input, Spacer, Stack, Text, useMediaQuery, VStack } from '@chakra-ui/react' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import { withMask } from 'use-mask-input'; import { logout } from '../slices/AuthState'; import ToastWizard from '../components/toastWizard'; +import server, { JSONResponse } from '../networking'; +import { GiExitDoor } from 'react-icons/gi'; +import { IoLogOutOutline } from 'react-icons/io5'; function Profile() { const navigate = useNavigate(); const dispatch = useDispatch(); + + // Loading states const [loggingOut, setLoggingOut] = useState(false); + const [infoLoading, setInfoLoading] = useState(true); + + // Profile data + const [originalAccountInfo, setOriginalAccountInfo] = useState({}); + const [accountInfo, setAccountInfo] = useState({}); const [isSmallerThan800] = useMediaQuery("(max-width: 800px)"); @@ -42,6 +52,51 @@ function Profile() { ) } + const getAccountInfo = async () => { + setInfoLoading(true); + try { + const response = await server.get('/profile/info') + + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw new Error(errObject); + } + + // Success case + setOriginalAccountInfo(response.data.raw.info); + setAccountInfo(response.data.raw.info); + setInfoLoading(false); + } else { + throw new Error("Unexpected response format"); + } + } catch (err) { + if (err.response && err.response.data instanceof JSONResponse) { + console.log("Error response in profile info request:", err.response.data.fullMessage()); + if (err.response.data.userErrorType()) { + ToastWizard.standard("error", "Profile info retrieval failed.", err.response.data.message, 3000, true, getAccountInfo, 'Retry'); + } else { + ToastWizard.standard("error", "Couldn't retrieve profile.", "Failed to retrieve your information. Please try again.", 3000, true, getAccountInfo, 'Retry'); + } + } else { + console.log("Unexpected error in profile info request:", err); + ToastWizard.standard("error", "Couldn't retrieve profile.", "Failed to retrieve your information. Please try again.", 3000, true, getAccountInfo, 'Retry'); + } + } + } + + useEffect(() => { + getAccountInfo(); + }, []) + + useEffect(() => { + console.log(accountInfo) + }, [accountInfo]) + return ( - + From 3b0f283b38c9be14d705104cc3c2c89be86cebc8 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 29 Jul 2025 17:33:03 +0800 Subject: [PATCH 06/20] added detection of changes and trigger of new ActionBar component --- package.json | 1 + src/components/Identity/ProfileActionBar.jsx | 57 ++++++++++++++++++++ src/pages/Profile.jsx | 31 +++++++---- 3 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 src/components/Identity/ProfileActionBar.jsx diff --git a/package.json b/package.json index 41fd265..affd4a6 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@reduxjs/toolkit": "^2.8.2", "axios": "^1.9.0", "framer-motion": "^12.23.0", + "lodash": "^4.17.21", "next-themes": "^0.4.6", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/src/components/Identity/ProfileActionBar.jsx b/src/components/Identity/ProfileActionBar.jsx new file mode 100644 index 0000000..5018be3 --- /dev/null +++ b/src/components/Identity/ProfileActionBar.jsx @@ -0,0 +1,57 @@ +import { ActionBar, Button, Dialog, Portal } from '@chakra-ui/react' +import { isEqual } from 'lodash'; +import { useEffect, useState } from 'react' +import { LuSquarePlus, LuTrash2 } from 'react-icons/lu'; + +function ProfileActionBar({ accountInfo, originalAccountInfo }) { + const [changesMade, setChangesMade] = useState(false); + + useEffect(() => { + setChangesMade(!isEqual(accountInfo, originalAccountInfo)); + }, [accountInfo, originalAccountInfo]) + + return ( + + + + + + + + + + + + + + + + Delete projects + + + + Are you sure you want to delete 4 projects? + + + + + + + + + + + + + + + ) +} + +export default ProfileActionBar \ No newline at end of file diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index d762b24..813bbac 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -8,6 +8,7 @@ import ToastWizard from '../components/toastWizard'; import server, { JSONResponse } from '../networking'; import { GiExitDoor } from 'react-icons/gi'; import { IoLogOutOutline } from 'react-icons/io5'; +import ProfileActionBar from '../components/Identity/ProfileActionBar'; function Profile() { const navigate = useNavigate(); @@ -21,6 +22,8 @@ function Profile() { const [originalAccountInfo, setOriginalAccountInfo] = useState({}); const [accountInfo, setAccountInfo] = useState({}); + let fullName = (originalAccountInfo.fname || 'Name') + ' ' + (originalAccountInfo.lname || 'Unavailable'); + const [isSmallerThan800] = useMediaQuery("(max-width: 800px)"); const handleLogout = () => { @@ -97,14 +100,14 @@ function Profile() { console.log(accountInfo) }, [accountInfo]) - return ( + return <> - John Appleseed - Research Consultant, National Museum + {fullName} + {originalAccountInfo.role} Name & Account Details First Name - + setAccountInfo({ ...accountInfo, fname: e.target.value })} /> Last Name - + setAccountInfo({ ...accountInfo, lname: e.target.value })} /> Username - + setAccountInfo({ ...accountInfo, username: e.target.value })} /> @@ -146,21 +149,26 @@ function Profile() { + Contact Details + Email - + setAccountInfo({ ...accountInfo, email: e.target.value })} /> + Contact - + setAccountInfo({ ...accountInfo, contact: e.target.value })} /> + Security & Authentication + Change Password @@ -180,7 +188,8 @@ function Profile() { - ) + + } export default Profile \ No newline at end of file From 4b0067278b98aaa6067c876bfc31870e4afc82b2 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 29 Jul 2025 17:42:33 +0800 Subject: [PATCH 07/20] refinements to ProfileActionBar component --- src/components/Identity/ProfileActionBar.jsx | 27 ++++++++++---------- src/pages/Profile.jsx | 2 +- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/components/Identity/ProfileActionBar.jsx b/src/components/Identity/ProfileActionBar.jsx index 5018be3..8e542a2 100644 --- a/src/components/Identity/ProfileActionBar.jsx +++ b/src/components/Identity/ProfileActionBar.jsx @@ -3,45 +3,44 @@ import { isEqual } from 'lodash'; import { useEffect, useState } from 'react' import { LuSquarePlus, LuTrash2 } from 'react-icons/lu'; -function ProfileActionBar({ accountInfo, originalAccountInfo }) { +function ProfileActionBar({ accountInfo, originalAccountInfo, setAccountInfo }) { const [changesMade, setChangesMade] = useState(false); + const [isConfirmSaveOpen, setIsConfirmSaveOpen] = useState(false); useEffect(() => { setChangesMade(!isEqual(accountInfo, originalAccountInfo)); }, [accountInfo, originalAccountInfo]) + const handleCancelChanges = () => { + setAccountInfo(originalAccountInfo); + } + return ( - + - + setIsConfirmSaveOpen(e.open)}> - + - Delete projects + Update Profile - Are you sure you want to delete 4 projects? + Are you sure you'd like to save these changes? This action cannot be undone. - - + + diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 813bbac..07a4676 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -188,7 +188,7 @@ function Profile() { - + } From f2f4e42898d9c4fc4c52ff184599e1d3d64c3a89 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 29 Jul 2025 19:00:05 +0800 Subject: [PATCH 08/20] added profile updating functionality --- src/components/Identity/ProfileActionBar.jsx | 55 ++++++++++++++++++-- src/pages/Profile.jsx | 5 +- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/components/Identity/ProfileActionBar.jsx b/src/components/Identity/ProfileActionBar.jsx index 8e542a2..c7ad855 100644 --- a/src/components/Identity/ProfileActionBar.jsx +++ b/src/components/Identity/ProfileActionBar.jsx @@ -2,10 +2,13 @@ import { ActionBar, Button, Dialog, Portal } from '@chakra-ui/react' import { isEqual } from 'lodash'; import { useEffect, useState } from 'react' import { LuSquarePlus, LuTrash2 } from 'react-icons/lu'; +import ToastWizard from '../toastWizard'; +import server, { JSONResponse } from '../../networking'; -function ProfileActionBar({ accountInfo, originalAccountInfo, setAccountInfo }) { +function ProfileActionBar({ accountInfo, originalAccountInfo, setAccountInfo, getAccountInfo }) { const [changesMade, setChangesMade] = useState(false); const [isConfirmSaveOpen, setIsConfirmSaveOpen] = useState(false); + const [saveLoading, setSaveLoading] = useState(false); useEffect(() => { setChangesMade(!isEqual(accountInfo, originalAccountInfo)); @@ -15,12 +18,58 @@ function ProfileActionBar({ accountInfo, originalAccountInfo, setAccountInfo }) setAccountInfo(originalAccountInfo); } + const handleSaveChanges = async () => { + setSaveLoading(true); + try { + const response = await server.post('/profile/update', { + username: accountInfo.username, + fname: accountInfo.fname, + lname: accountInfo.lname, + contact: accountInfo.contact, + email: accountInfo.email, + }); + + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw new Error(errObject); + } + + // Success case + getAccountInfo(); + setIsConfirmSaveOpen(false); + + ToastWizard.standard("success", "Profile updated.", "Your changes have been saved."); + } else { + throw new Error("Unexpected response format"); + } + } catch (err) { + if (err.response && err.response.data instanceof JSONResponse) { + console.log("Error response in update profile data request:", err.response.data.fullMessage()); + if (err.response.data.userErrorType()) { + ToastWizard.standard("error", "Profile update failed.", err.response.data.message, 3000); + } else { + ToastWizard.standard("error", "Profile update failed.", "Failed to update your profile information. Please try again.", 3000); + } + } else { + console.log("Unexpected error in profile info request:", err); + ToastWizard.standard("error", "Profile update failed.", "Failed to update your profile information. Please try again.", 3000); + } + } + + setSaveLoading(false); + } + return ( - + setIsConfirmSaveOpen(e.open)}> @@ -40,7 +89,7 @@ function ProfileActionBar({ accountInfo, originalAccountInfo, setAccountInfo }) - + diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 07a4676..3091ddb 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -73,7 +73,6 @@ function Profile() { // Success case setOriginalAccountInfo(response.data.raw.info); setAccountInfo(response.data.raw.info); - setInfoLoading(false); } else { throw new Error("Unexpected response format"); } @@ -90,6 +89,8 @@ function Profile() { ToastWizard.standard("error", "Couldn't retrieve profile.", "Failed to retrieve your information. Please try again.", 3000, true, getAccountInfo, 'Retry'); } } + + setInfoLoading(false); } useEffect(() => { @@ -188,7 +189,7 @@ function Profile() { - + } From 7fae0a1f5aa66394746cc46e3251e697afd2c116 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 29 Jul 2025 19:57:04 +0800 Subject: [PATCH 09/20] working on change password fields --- src/pages/Profile.jsx | 46 +++++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 3091ddb..7f30e97 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -21,6 +21,9 @@ function Profile() { // Profile data const [originalAccountInfo, setOriginalAccountInfo] = useState({}); const [accountInfo, setAccountInfo] = useState({}); + const [currentPass, setCurrentPass] = useState(''); + const [newPass, setNewPass] = useState(''); + const [confirmNewPass, setConfirmNewPass] = useState(''); let fullName = (originalAccountInfo.fname || 'Name') + ' ' + (originalAccountInfo.lname || 'Unavailable'); @@ -55,6 +58,12 @@ function Profile() { ) } + useEffect(() => { + if (newPass.length == 0) { + setConfirmNewPass(''); + } + }, [newPass]) + const getAccountInfo = async () => { setInfoLoading(true); try { @@ -104,10 +113,10 @@ function Profile() { return <> First Name - setAccountInfo({ ...accountInfo, fname: e.target.value })} /> + setAccountInfo({ ...accountInfo, fname: e.target.value })} /> Last Name - setAccountInfo({ ...accountInfo, lname: e.target.value })} /> + setAccountInfo({ ...accountInfo, lname: e.target.value })} /> Username - setAccountInfo({ ...accountInfo, username: e.target.value })} /> + setAccountInfo({ ...accountInfo, username: e.target.value })} /> @@ -155,12 +164,12 @@ function Profile() { Email - setAccountInfo({ ...accountInfo, email: e.target.value })} /> + setAccountInfo({ ...accountInfo, email: e.target.value })} /> Contact - setAccountInfo({ ...accountInfo, contact: e.target.value })} /> + setAccountInfo({ ...accountInfo, contact: e.target.value })} /> @@ -171,13 +180,24 @@ function Profile() { Security & Authentication - Change Password - + Current Password + setCurrentPass(e.target.value)} /> + + + + New Password + setNewPass(e.target.value)} /> + + + + Confirm New Password + setConfirmNewPass(e.target.value)} /> + {newPass !== confirmNewPass && ( + + Passwords do not match. + + )} - {/* - Confirm Password - - */} From 9b3ed02d286d475b355e63cdba67435ae7b29060 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Wed, 30 Jul 2025 14:41:08 +0800 Subject: [PATCH 10/20] moved contact details into main card on the left top right card will be account activity, aka audit logs --- src/pages/Profile.jsx | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 7f30e97..0d72950 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -67,7 +67,7 @@ function Profile() { const getAccountInfo = async () => { setInfoLoading(true); try { - const response = await server.get('/profile/info') + const response = await server.get('/profile/info?includeLogs=true') if (response.data instanceof JSONResponse) { if (response.data.isErrorStatus()) { @@ -143,34 +143,41 @@ function Profile() { First Name setAccountInfo({ ...accountInfo, fname: e.target.value })} /> + + Last Name setAccountInfo({ ...accountInfo, lname: e.target.value })} /> + + Username setAccountInfo({ ...accountInfo, username: e.target.value })} /> + + + + + Email + setAccountInfo({ ...accountInfo, email: e.target.value })} /> + + + + + + Contact + setAccountInfo({ ...accountInfo, contact: e.target.value })} /> + - - Contact Details - - - Email - setAccountInfo({ ...accountInfo, email: e.target.value })} /> - - - - Contact - setAccountInfo({ ...accountInfo, contact: e.target.value })} /> - + Account Activity @@ -179,7 +186,7 @@ function Profile() { Security & Authentication - + {/* Current Password setCurrentPass(e.target.value)} /> @@ -197,7 +204,7 @@ function Profile() { Passwords do not match. )} - + */} From d27026bb96c9e048238f5a4ae8f9f66f3d06e59d Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Wed, 30 Jul 2025 15:17:07 +0800 Subject: [PATCH 11/20] added AccountActivity card with rendering of audit logs --- .../Identity/AccountActivityCard.jsx | 40 ++++++++++++ src/pages/Profile.jsx | 65 +++++++++++++++---- 2 files changed, 91 insertions(+), 14 deletions(-) create mode 100644 src/components/Identity/AccountActivityCard.jsx diff --git a/src/components/Identity/AccountActivityCard.jsx b/src/components/Identity/AccountActivityCard.jsx new file mode 100644 index 0000000..9093dbc --- /dev/null +++ b/src/components/Identity/AccountActivityCard.jsx @@ -0,0 +1,40 @@ +import { Box, Card, CardBody, CardHeader, Heading, Spinner, Stack, StackSeparator, Text } from '@chakra-ui/react' +import React from 'react' + +function AccountActivityCard({ auditLogsData }) { + console.log('rendered!') + + return ( + + + Account Activity + + + + {auditLogsData.length == 0 ? ( + <> + {/* + Retrieving... */} + 💤 Oops! No account activity yet. + + ) : ( + } gap={4}> + {auditLogsData.map((log, index) => ( + + + {log.title} + + {log.created} + + {log.description} + + + ))} + + )} + + + ) +} + +export default AccountActivityCard \ No newline at end of file diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 0d72950..80aa818 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -9,6 +9,7 @@ import server, { JSONResponse } from '../networking'; import { GiExitDoor } from 'react-icons/gi'; import { IoLogOutOutline } from 'react-icons/io5'; import ProfileActionBar from '../components/Identity/ProfileActionBar'; +import AccountActivityCard from '../components/Identity/AccountActivityCard'; function Profile() { const navigate = useNavigate(); @@ -21,9 +22,10 @@ function Profile() { // Profile data const [originalAccountInfo, setOriginalAccountInfo] = useState({}); const [accountInfo, setAccountInfo] = useState({}); - const [currentPass, setCurrentPass] = useState(''); - const [newPass, setNewPass] = useState(''); - const [confirmNewPass, setConfirmNewPass] = useState(''); + const [auditLogsData, setAuditLogsData] = useState([]); + // const [currentPass, setCurrentPass] = useState(''); + // const [newPass, setNewPass] = useState(''); + // const [confirmNewPass, setConfirmNewPass] = useState(''); let fullName = (originalAccountInfo.fname || 'Name') + ' ' + (originalAccountInfo.lname || 'Unavailable'); @@ -58,11 +60,36 @@ function Profile() { ) } - useEffect(() => { - if (newPass.length == 0) { - setConfirmNewPass(''); + // useEffect(() => { + // if (newPass.length == 0) { + // setConfirmNewPass(''); + // } + // }, [newPass]) + + const processAuditLogs = (data) => { + var logs = []; + + for (var log of data) { + log["originalCreated"] = structuredClone(log["created"]); + log["created"] = new Date(log["created"]).toLocaleString('en-GB', { dateStyle: "long", timeStyle: "medium", hour12: true }); + logs.push(log); } - }, [newPass]) + + logs.sort((a, b) => { + // Sort by created in descending order + return new Date(b["originalCreated"]) - new Date(a["originalCreated"]); + }) + + return logs; + } + + const processAccountInfo = (data) => { + if (data.lastLogin) { + data.lastLogin = new Date(data.lastLogin).toLocaleString('en-GB', { dateStyle: "long", timeStyle: "medium", hour12: true }); + } + + return data; + } const getAccountInfo = async () => { setInfoLoading(true); @@ -79,9 +106,13 @@ function Profile() { throw new Error(errObject); } + const preppedAccInfo = processAccountInfo(structuredClone(response.data.raw.info)); + delete preppedAccInfo.logs; + // Success case - setOriginalAccountInfo(response.data.raw.info); - setAccountInfo(response.data.raw.info); + setOriginalAccountInfo(preppedAccInfo); + setAccountInfo(preppedAccInfo); + setAuditLogsData(processAuditLogs(response.data.raw.info.logs || [])); } else { throw new Error("Unexpected response format"); } @@ -108,7 +139,8 @@ function Profile() { useEffect(() => { console.log(accountInfo) - }, [accountInfo]) + console.log(auditLogsData) + }, [accountInfo, auditLogsData]) return <> - + + {/* Account Activity - + + + */} - + Security & Authentication @@ -206,6 +241,8 @@ function Profile() { )} */} + Last Login: {originalAccountInfo.lastLogin} + From 474496a34adb12c34d6477161c88efe68182f837 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Wed, 30 Jul 2025 15:24:04 +0800 Subject: [PATCH 12/20] changed grid layout such that account activity has more space --- src/components/Identity/AccountActivityCard.jsx | 6 +++--- src/pages/Profile.jsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Identity/AccountActivityCard.jsx b/src/components/Identity/AccountActivityCard.jsx index 9093dbc..4ea36a4 100644 --- a/src/components/Identity/AccountActivityCard.jsx +++ b/src/components/Identity/AccountActivityCard.jsx @@ -1,8 +1,8 @@ -import { Box, Card, CardBody, CardHeader, Heading, Spinner, Stack, StackSeparator, Text } from '@chakra-ui/react' +import { Box, Card, CardBody, CardHeader, Heading, Spinner, Stack, StackSeparator, Text, useMediaQuery } from '@chakra-ui/react' import React from 'react' function AccountActivityCard({ auditLogsData }) { - console.log('rendered!') + const [isSmallerThan800] = useMediaQuery("(max-width: 800px)"); return ( @@ -10,7 +10,7 @@ function AccountActivityCard({ auditLogsData }) { Account Activity - + {auditLogsData.length == 0 ? ( <> {/* diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 80aa818..913d235 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -145,7 +145,7 @@ function Profile() { return <> - + @@ -207,7 +207,7 @@ function Profile() { - + {/* Account Activity From 34b89cf6153e3fcdc47b85cd3899fc390eb7c319 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Wed, 30 Jul 2025 15:32:20 +0800 Subject: [PATCH 13/20] enter key now opens save dialog if changes are made --- src/components/Identity/ProfileActionBar.jsx | 8 +++++- src/pages/Profile.jsx | 28 +++++++++----------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/components/Identity/ProfileActionBar.jsx b/src/components/Identity/ProfileActionBar.jsx index c7ad855..d03e22a 100644 --- a/src/components/Identity/ProfileActionBar.jsx +++ b/src/components/Identity/ProfileActionBar.jsx @@ -5,7 +5,7 @@ import { LuSquarePlus, LuTrash2 } from 'react-icons/lu'; import ToastWizard from '../toastWizard'; import server, { JSONResponse } from '../../networking'; -function ProfileActionBar({ accountInfo, originalAccountInfo, setAccountInfo, getAccountInfo }) { +function ProfileActionBar({ accountInfo, originalAccountInfo, setAccountInfo, getAccountInfo, enterKeyHit }) { const [changesMade, setChangesMade] = useState(false); const [isConfirmSaveOpen, setIsConfirmSaveOpen] = useState(false); const [saveLoading, setSaveLoading] = useState(false); @@ -14,6 +14,12 @@ function ProfileActionBar({ accountInfo, originalAccountInfo, setAccountInfo, ge setChangesMade(!isEqual(accountInfo, originalAccountInfo)); }, [accountInfo, originalAccountInfo]) + useEffect(() => { + if (changesMade) { + setIsConfirmSaveOpen(true); + } + }, [enterKeyHit]); + const handleCancelChanges = () => { setAccountInfo(originalAccountInfo); } diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 913d235..b32b789 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -23,9 +23,7 @@ function Profile() { const [originalAccountInfo, setOriginalAccountInfo] = useState({}); const [accountInfo, setAccountInfo] = useState({}); const [auditLogsData, setAuditLogsData] = useState([]); - // const [currentPass, setCurrentPass] = useState(''); - // const [newPass, setNewPass] = useState(''); - // const [confirmNewPass, setConfirmNewPass] = useState(''); + const [enterKeyHit, setEnterKeyHit] = useState(false); let fullName = (originalAccountInfo.fname || 'Name') + ' ' + (originalAccountInfo.lname || 'Unavailable'); @@ -60,12 +58,6 @@ function Profile() { ) } - // useEffect(() => { - // if (newPass.length == 0) { - // setConfirmNewPass(''); - // } - // }, [newPass]) - const processAuditLogs = (data) => { var logs = []; @@ -142,6 +134,12 @@ function Profile() { console.log(auditLogsData) }, [accountInfo, auditLogsData]) + const handleEnterKey = (e) => { + if (e.key === 'Enter') { + setEnterKeyHit(prev => !prev); + } + } + return <> First Name - setAccountInfo({ ...accountInfo, fname: e.target.value })} /> + setAccountInfo({ ...accountInfo, fname: e.target.value })} /> Last Name - setAccountInfo({ ...accountInfo, lname: e.target.value })} /> + setAccountInfo({ ...accountInfo, lname: e.target.value })} /> Username - setAccountInfo({ ...accountInfo, username: e.target.value })} /> + setAccountInfo({ ...accountInfo, username: e.target.value })} /> Email - setAccountInfo({ ...accountInfo, email: e.target.value })} /> + setAccountInfo({ ...accountInfo, email: e.target.value })} /> Contact - setAccountInfo({ ...accountInfo, contact: e.target.value })} /> + setAccountInfo({ ...accountInfo, contact: e.target.value })} /> @@ -253,7 +251,7 @@ function Profile() { - + } From 570b11ffb750f5dfd3a96d1e7c896aa011bba845 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Wed, 30 Jul 2025 15:35:14 +0800 Subject: [PATCH 14/20] fixed bug where details not updated overwrote database updates due to all information being included in profile update payload --- src/components/Identity/ProfileActionBar.jsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/Identity/ProfileActionBar.jsx b/src/components/Identity/ProfileActionBar.jsx index d03e22a..2a87cc8 100644 --- a/src/components/Identity/ProfileActionBar.jsx +++ b/src/components/Identity/ProfileActionBar.jsx @@ -26,14 +26,16 @@ function ProfileActionBar({ accountInfo, originalAccountInfo, setAccountInfo, ge const handleSaveChanges = async () => { setSaveLoading(true); + + var updateDict = {}; + if (accountInfo.username !== originalAccountInfo.username) updateDict.username = accountInfo.username; + if (accountInfo.fname !== originalAccountInfo.fname) updateDict.fname = accountInfo.fname; + if (accountInfo.lname !== originalAccountInfo.lname) updateDict.lname = accountInfo.lname; + if (accountInfo.contact !== originalAccountInfo.contact) updateDict.contact = accountInfo.contact; + if (accountInfo.email !== originalAccountInfo.email) updateDict.email = accountInfo.email; + try { - const response = await server.post('/profile/update', { - username: accountInfo.username, - fname: accountInfo.fname, - lname: accountInfo.lname, - contact: accountInfo.contact, - email: accountInfo.email, - }); + const response = await server.post('/profile/update', updateDict); if (response.data instanceof JSONResponse) { if (response.data.isErrorStatus()) { From 2bd078bf78d9aefe52734e70e41e05851622b514 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Thu, 31 Jul 2025 14:47:51 +0800 Subject: [PATCH 15/20] added ChangePasswordDialogButton --- .../Identity/ChangePasswordDialogButton.jsx | 73 +++++++++++++++++++ src/pages/Profile.jsx | 37 +++------- src/themes/MainTheme.js | 6 ++ 3 files changed, 88 insertions(+), 28 deletions(-) create mode 100644 src/components/Identity/ChangePasswordDialogButton.jsx diff --git a/src/components/Identity/ChangePasswordDialogButton.jsx b/src/components/Identity/ChangePasswordDialogButton.jsx new file mode 100644 index 0000000..9cdd00f --- /dev/null +++ b/src/components/Identity/ChangePasswordDialogButton.jsx @@ -0,0 +1,73 @@ +import { Button, CloseButton, Dialog, Field, Input, Portal, Text } from '@chakra-ui/react' +import React, { useEffect, useState } from 'react' + +function ChangePasswordDialogButton() { + const [currentPass, setCurrentPass] = useState(''); + const [newPass, setNewPass] = useState(''); + const [confirmNewPass, setConfirmNewPass] = useState(''); + const [startedTyping, setStartedTyping] = useState(false); + + useEffect(() => { + if (newPass.length === 0) { + setStartedTyping(false); + setConfirmNewPass(''); + } + }, [newPass]); + + useEffect(() => { + if (confirmNewPass.length > 0 && !startedTyping) { + setStartedTyping(true); + } + }, [confirmNewPass]) + + return ( + + + + + + + + + + Change Password + + + Verify your identity before changing your password. + + Current Password + setCurrentPass(e.target.value)} /> + + + + New Password + setNewPass(e.target.value)} /> + + + + Confirm New Password + setConfirmNewPass(e.target.value)} /> + {startedTyping && newPass !== confirmNewPass && ( + + Passwords do not match. + + )} + + + + {/* + + */} + + + + + + + + + + ) +} + +export default ChangePasswordDialogButton \ No newline at end of file diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index b32b789..c06363d 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -1,4 +1,4 @@ -import { Avatar, Box, Button, Field, Flex, Grid, GridItem, HStack, Input, Spacer, Stack, Text, useMediaQuery, VStack } from '@chakra-ui/react' +import { Avatar, Box, Button, CloseButton, Dialog, Field, Flex, Grid, GridItem, HStack, Input, Portal, Spacer, Stack, Text, useMediaQuery, VStack } from '@chakra-ui/react' import { useEffect, useState } from 'react' import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router-dom'; @@ -10,6 +10,7 @@ import { GiExitDoor } from 'react-icons/gi'; import { IoLogOutOutline } from 'react-icons/io5'; import ProfileActionBar from '../components/Identity/ProfileActionBar'; import AccountActivityCard from '../components/Identity/AccountActivityCard'; +import ChangePasswordDialogButton from '../components/Identity/ChangePasswordDialogButton'; function Profile() { const navigate = useNavigate(); @@ -129,10 +130,10 @@ function Profile() { getAccountInfo(); }, []) - useEffect(() => { - console.log(accountInfo) - console.log(auditLogsData) - }, [accountInfo, auditLogsData]) + // useEffect(() => { + // console.log(accountInfo) + // console.log(auditLogsData) + // }, [accountInfo, auditLogsData]) const handleEnterKey = (e) => { if (e.key === 'Enter') { @@ -219,38 +220,18 @@ function Profile() { Security & Authentication - {/* - Current Password - setCurrentPass(e.target.value)} /> - - - - New Password - setNewPass(e.target.value)} /> - - - - Confirm New Password - setConfirmNewPass(e.target.value)} /> - {newPass !== confirmNewPass && ( - - Passwords do not match. - - )} - */} - Last Login: {originalAccountInfo.lastLogin} - - + + - + } diff --git a/src/themes/MainTheme.js b/src/themes/MainTheme.js index a912f61..3011fec 100644 --- a/src/themes/MainTheme.js +++ b/src/themes/MainTheme.js @@ -13,6 +13,12 @@ const buttonRecipe = defineRecipe({ bg: "rgb(190, 0, 25)", color: "white", _hover: { bg: "primaryColour" }, + }, + ArchPrimaryAlt: { + border: '1px solid #12144C', + bg: 'white', + color: 'primaryColour', + _hover: { bg: "primaryColour", color: "white" }, } } } From 90acc2601dcb4f8d4fca40e85f8b1b08a6e88f28 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Thu, 31 Jul 2025 16:02:30 +0800 Subject: [PATCH 16/20] change password dialog is now functioning --- .../Identity/ChangePasswordDialogButton.jsx | 82 +++++++++++++++++-- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/src/components/Identity/ChangePasswordDialogButton.jsx b/src/components/Identity/ChangePasswordDialogButton.jsx index 9cdd00f..94a8379 100644 --- a/src/components/Identity/ChangePasswordDialogButton.jsx +++ b/src/components/Identity/ChangePasswordDialogButton.jsx @@ -1,11 +1,20 @@ import { Button, CloseButton, Dialog, Field, Input, Portal, Text } from '@chakra-ui/react' -import React, { useEffect, useState } from 'react' +import { useEffect, useState } from 'react' +import server, { JSONResponse } from '../../networking' +import ToastWizard from '../toastWizard'; +import { useDispatch } from 'react-redux'; +import { logout } from '../../slices/AuthState'; +import { useNavigate } from 'react-router-dom'; function ChangePasswordDialogButton() { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [currentPass, setCurrentPass] = useState(''); const [newPass, setNewPass] = useState(''); const [confirmNewPass, setConfirmNewPass] = useState(''); const [startedTyping, setStartedTyping] = useState(false); + const [changing, setChanging] = useState(false); useEffect(() => { if (newPass.length === 0) { @@ -20,6 +29,69 @@ function ChangePasswordDialogButton() { } }, [confirmNewPass]) + const handleKeyDown = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleChangePassword(); + } + } + + const handleChangePassword = async () => { + if (!(currentPass && newPass && confirmNewPass && newPass === confirmNewPass)) { + ToastWizard.standard("error", "Invalid input", "Please fill in all fields and ensure passwords match."); + return; + } else if (currentPass === confirmNewPass) { + ToastWizard.standard("error", "Invalid input", "New password cannot be the same as the current password."); + return; + } + + setChanging(true); + try { + const response = await server.post('/profile/changePassword', { + currentPassword: currentPass, + newPassword: confirmNewPass + }) + + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw new Error(errObject); + } + + // Success case + dispatch(logout(true, (result) => { + if (result instanceof JSONResponse && !result.isErrorStatus()) { + navigate('/auth/login'); + } else { + ToastWizard.standard("error", "Logout failed", "Please try again."); + } + return; + })); + ToastWizard.standard("success", "Password changed successfully.", "Please log in again with your new password."); + } else { + throw new Error("Unexpected response format"); + } + } catch (err) { + if (err.response && err.response.data instanceof JSONResponse) { + console.log("Error response in change password request:", err.response.data.fullMessage()); + if (err.response.data.userErrorType()) { + ToastWizard.standard("error", err.response.data.message); + } else { + ToastWizard.standard("error", "Password update failed.", "Failed to change your password. Please try again."); + } + } else { + console.log("Unexpected error in change password request:", err); + ToastWizard.standard("error", "Password update failed.", "Failed to change your password. Please try again."); + } + } + + setChanging(false); + } + return ( @@ -36,17 +108,17 @@ function ChangePasswordDialogButton() { Verify your identity before changing your password. Current Password - setCurrentPass(e.target.value)} /> + setCurrentPass(e.target.value)} /> New Password - setNewPass(e.target.value)} /> + setNewPass(e.target.value)} /> Confirm New Password - setConfirmNewPass(e.target.value)} /> + setConfirmNewPass(e.target.value)} /> {startedTyping && newPass !== confirmNewPass && ( Passwords do not match. @@ -58,7 +130,7 @@ function ChangePasswordDialogButton() { {/* */} - + From af65b3c07d8bd42be797fed8c309d47e791a8932 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Fri, 1 Aug 2025 06:37:30 +0800 Subject: [PATCH 17/20] completed profile avatar uploading --- src/components/Identity/ProfileAvatar.jsx | 101 ++++++++++++++++++++++ src/components/Navbar.jsx | 3 +- src/networking.js | 2 +- src/pages/Profile.jsx | 21 ++--- 4 files changed, 109 insertions(+), 18 deletions(-) create mode 100644 src/components/Identity/ProfileAvatar.jsx diff --git a/src/components/Identity/ProfileAvatar.jsx b/src/components/Identity/ProfileAvatar.jsx new file mode 100644 index 0000000..0b974af --- /dev/null +++ b/src/components/Identity/ProfileAvatar.jsx @@ -0,0 +1,101 @@ +import React, { useRef, useState } from 'react' +import { useSelector } from 'react-redux'; +import ToastWizard from '../toastWizard'; +import server, { JSONResponse } from '../../networking'; +import { Avatar, Spinner } from '@chakra-ui/react'; + +function ProfileAvatar({ accountInfo }) { + const { username, accountID } = useSelector(state => state.auth); + + const [avatarSrc, setAvatarSrc] = useState(accountID && `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${accountID}`); + const [avatarUploading, setAvatarUploading] = useState(false); + const fileInputRef = useRef(null); + + const handleFileChange = async (e) => { + if (e.target.files.length === 0) { + ToastWizard.standard("warning", "No avatar image selected for upload") + return; + } + + const file = e.target.files ? e.target.files[0] : null; + if (!file) { + ToastWizard.standard("warning", "No avatar image selected for upload") + return; + } + + const MAX_SIZE = 5 * 1024 * 1024; // 5MB + if (file.size > MAX_SIZE) { + ToastWizard.standard("error", "Avatar image size exceeds 5MB limit"); + return; + } + + // Create a preview URL + const previewUrl = URL.createObjectURL(file); + setAvatarSrc(previewUrl); + + const formData = new FormData() + + formData.append('file', file) + const config = { + 'Content-Type': 'multipart/form-data' + } + + setAvatarUploading(true); + try { + const response = await server.post("/profile/uploadPicture", formData, { headers: config, transformRequest: formData => formData }) + + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw new Error(errObject); + } + + // Success case + ToastWizard.standard("success", "Avatar updated successfully!"); + } else { + throw new Error("Unexpected response format"); + } + } catch (err) { + if (err.response && err.response.data instanceof JSONResponse) { + console.log("Error response in avatar update request:", err.response.data.fullMessage()); + if (err.response.data.userErrorType()) { + ToastWizard.standard("error", "Avatar update failed.", err.response.data.message); + } else { + ToastWizard.standard("error", "Something went wrong in updating your avatar.", "Please try again."); + } + } else { + console.log("Unexpected error in login request:", err); + ToastWizard.standard("error", "Something went wrong in updating your avatar.", "Please try again."); + } + } + + setAvatarSrc(accountID && `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${accountID}?t=${Date.now()}`); + setAvatarUploading(false); + } + + const handleAvatarClick = (e) => { + fileInputRef.current.click(); + }; + + return <> + + + + + + {avatarUploading && } + +} + +export default ProfileAvatar \ No newline at end of file diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index c4c031a..54c63c5 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -9,7 +9,7 @@ import { useSelector } from 'react-redux' function Navbar() { const navigate = useNavigate(); const [isOpen, setIsOpen] = useState(false); - const { username } = useSelector(state => state.auth); + const { username, accountID } = useSelector(state => state.auth); const handleLogoClick = () => { navigate('/'); @@ -31,6 +31,7 @@ function Navbar() { + setIsOpen(e.open)} /> diff --git a/src/networking.js b/src/networking.js index 9225523..c6dd94d 100644 --- a/src/networking.js +++ b/src/networking.js @@ -42,7 +42,7 @@ const instance = axios.create({ }) instance.interceptors.request.use((config) => { - if (config.method == 'post') { + if (config.method == 'post' && !config.headers.get('Content-Type')) { config.headers["Content-Type"] = "application/json"; } diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index c06363d..4b50a17 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -1,6 +1,6 @@ -import { Avatar, Box, Button, CloseButton, Dialog, Field, Flex, Grid, GridItem, HStack, Input, Portal, Spacer, Stack, Text, useMediaQuery, VStack } from '@chakra-ui/react' -import { useEffect, useState } from 'react' -import { useDispatch } from 'react-redux'; +import { Avatar, Box, Button, CloseButton, Dialog, Field, FileUpload, Flex, Grid, GridItem, HStack, Input, Portal, Spacer, Spinner, Stack, Text, useMediaQuery, VStack } from '@chakra-ui/react' +import { useEffect, useRef, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import { withMask } from 'use-mask-input'; import { logout } from '../slices/AuthState'; @@ -11,6 +11,7 @@ import { IoLogOutOutline } from 'react-icons/io5'; import ProfileActionBar from '../components/Identity/ProfileActionBar'; import AccountActivityCard from '../components/Identity/AccountActivityCard'; import ChangePasswordDialogButton from '../components/Identity/ChangePasswordDialogButton'; +import ProfileAvatar from '../components/Identity/ProfileAvatar'; function Profile() { const navigate = useNavigate(); @@ -31,7 +32,6 @@ function Profile() { const [isSmallerThan800] = useMediaQuery("(max-width: 800px)"); const handleLogout = () => { - // ToastWizard.standard("success", "Logout successful.", "See you soon!"); setLoggingOut(true); const logoutPromise = new Promise((resolve, reject) => { dispatch(logout(true, (msg) => { @@ -143,26 +143,15 @@ function Profile() { return <> - - - + {fullName} {originalAccountInfo.role} From 724b8896f853265eb236d3b55fee9e48f4769f37 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Fri, 1 Aug 2025 06:47:45 +0800 Subject: [PATCH 18/20] added skeleton-based loading to all profile page elements --- .../Identity/AccountActivityCard.jsx | 48 ++++++------ src/pages/Profile.jsx | 73 ++++++++++--------- 2 files changed, 62 insertions(+), 59 deletions(-) diff --git a/src/components/Identity/AccountActivityCard.jsx b/src/components/Identity/AccountActivityCard.jsx index 4ea36a4..2f5bea3 100644 --- a/src/components/Identity/AccountActivityCard.jsx +++ b/src/components/Identity/AccountActivityCard.jsx @@ -1,7 +1,7 @@ -import { Box, Card, CardBody, CardHeader, Heading, Spinner, Stack, StackSeparator, Text, useMediaQuery } from '@chakra-ui/react' +import { Box, Card, CardBody, CardHeader, Heading, Skeleton, Spinner, Stack, StackSeparator, Text, useMediaQuery } from '@chakra-ui/react' import React from 'react' -function AccountActivityCard({ auditLogsData }) { +function AccountActivityCard({ infoLoading, auditLogsData }) { const [isSmallerThan800] = useMediaQuery("(max-width: 800px)"); return ( @@ -10,28 +10,30 @@ function AccountActivityCard({ auditLogsData }) { Account Activity - - {auditLogsData.length == 0 ? ( - <> - {/* + + + {auditLogsData.length == 0 ? ( + <> + {/* Retrieving... */} - 💤 Oops! No account activity yet. - - ) : ( - } gap={4}> - {auditLogsData.map((log, index) => ( - - - {log.title} - - {log.created} - - {log.description} - - - ))} - - )} + 💤 Oops! No account activity yet. + + ) : ( + } gap={4}> + {auditLogsData.map((log, index) => ( + + + {log.title} + + {log.created} + + {log.description} + + + ))} + + )} + ) diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 4b50a17..248e551 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -1,4 +1,4 @@ -import { Avatar, Box, Button, CloseButton, Dialog, Field, FileUpload, Flex, Grid, GridItem, HStack, Input, Portal, Spacer, Spinner, Stack, Text, useMediaQuery, VStack } from '@chakra-ui/react' +import { Avatar, Box, Button, CloseButton, Dialog, Field, FileUpload, Flex, Grid, GridItem, HStack, Input, Portal, Skeleton, SkeletonText, Spacer, Spinner, Stack, Text, useMediaQuery, VStack } from '@chakra-ui/react' import { useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; @@ -153,55 +153,54 @@ function Profile() { - {fullName} - {originalAccountInfo.role} + + {fullName} + {originalAccountInfo.role} + Name & Account Details - - - First Name - setAccountInfo({ ...accountInfo, fname: e.target.value })} /> - + + + + First Name + setAccountInfo({ ...accountInfo, fname: e.target.value })} /> + - + - - Last Name - setAccountInfo({ ...accountInfo, lname: e.target.value })} /> - + + Last Name + setAccountInfo({ ...accountInfo, lname: e.target.value })} /> + - + - - Username - setAccountInfo({ ...accountInfo, username: e.target.value })} /> - + + Username + setAccountInfo({ ...accountInfo, username: e.target.value })} /> + - + - - Email - setAccountInfo({ ...accountInfo, email: e.target.value })} /> - + + Email + setAccountInfo({ ...accountInfo, email: e.target.value })} /> + - + - - Contact - setAccountInfo({ ...accountInfo, contact: e.target.value })} /> - - + + Contact + setAccountInfo({ ...accountInfo, contact: e.target.value })} /> + + + - - {/* - Account Activity - - - */} + @@ -209,7 +208,9 @@ function Profile() { Security & Authentication - Last Login: {originalAccountInfo.lastLogin} + + Last Login: {originalAccountInfo.lastLogin} + From b4baa6a2e38a222866df2778dc35698e1484b870 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Fri, 1 Aug 2025 06:57:00 +0800 Subject: [PATCH 19/20] added some QoL improvements redirection on login when already logged in, usage of fname and lname in login success toast --- src/pages/Login.jsx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index 6308a50..872a032 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -1,17 +1,20 @@ import { Box, Button, Field, Fieldset, Flex, For, Image, Input, NativeSelect, Spacer, Stack, Text, useMediaQuery, VStack } from '@chakra-ui/react' import { PasswordInput } from "../components/ui/password-input" -import { useState } from 'react' +import { useEffect, useState } from 'react' import sccciBuildingImage from '../assets/sccciBuildingOpening1964.png' import logoVisual from '../assets/logoVisual.svg' import server, { JSONResponse } from '../networking' import ToastWizard from '../components/toastWizard' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { useNavigate } from 'react-router-dom' import { fetchSession } from '../slices/AuthState' function Login() { const dispatch = useDispatch(); const navigate = useNavigate(); + + const { username, loaded } = useSelector(state => state.auth); + const [isSmallerThan800] = useMediaQuery("(max-width: 800px)"); const [usernameOrEmail, setUsernameOrEmail] = useState(''); @@ -52,7 +55,13 @@ function Login() { // Success case dispatch(fetchSession()); navigate("/profile"); - ToastWizard.standard("success", "Login successful.", "Welcome back to ArchAIve!") + ToastWizard.standard( + "success", + (res.data.raw.fname && res.data.raw.lname) ? `Welcome back, ${res.data.raw.fname} ${res.data.raw.lname}!` : `Welcome back, ${res.data.raw.username}!`, + "Login successful.", + 5000, + true + ) } else { console.log("Unexpected response in login request:", res.data); ambiguousErrorToast(); @@ -76,6 +85,13 @@ function Login() { }) } + useEffect(() => { + if (loaded && username) { + navigate('/profile') + ToastWizard.standard("default", `You are already logged in, ${username}`, "Please logout to switch accounts.", 2000, true); + } + }, [loaded]) + return ( From 7dd7367541d58f3ad6ebfb5e4a13366bc8b1c2dd Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Fri, 1 Aug 2025 06:58:21 +0800 Subject: [PATCH 20/20] changed login success redirect to catalogue browser --- src/main.jsx | 1 - src/pages/Login.jsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main.jsx b/src/main.jsx index ee62501..13a35cb 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -16,7 +16,6 @@ import Health from './pages/Health.jsx' import Homepage from './pages/Homepage.jsx' import Login from './pages/Login.jsx'; import DefaultLayout from './DefaultLayout.jsx'; -import SampleProtected from './pages/SampleProtected.jsx'; import ProtectedLayout from './ProtectedLayout.jsx'; import AnimateIn from './AnimateIn.jsx'; import PublicGallery from './pages/PublicGallery.jsx'; diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index 872a032..f8a7763 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -54,7 +54,7 @@ function Login() { // Success case dispatch(fetchSession()); - navigate("/profile"); + navigate("/catalogue"); ToastWizard.standard( "success", (res.data.raw.fname && res.data.raw.lname) ? `Welcome back, ${res.data.raw.fname} ${res.data.raw.lname}!` : `Welcome back, ${res.data.raw.username}!`,