Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b098a89
Merge pull request #19 from ArchAIve-Project/main
Prakhar896 Jul 26, 2025
a5b0f43
working on new Profile page
Prakhar896 Jul 29, 2025
29c207b
changed to grid-based layout instead
Prakhar896 Jul 29, 2025
7e755d8
slight UI refinements
Prakhar896 Jul 29, 2025
0c2fd0b
added logout functionality to profile
Prakhar896 Jul 29, 2025
e4fdf4b
added getAccountInfo request to Profile
Prakhar896 Jul 29, 2025
3b0f283
added detection of changes and trigger of new ActionBar component
Prakhar896 Jul 29, 2025
4b00672
refinements to ProfileActionBar component
Prakhar896 Jul 29, 2025
f2f4e42
added profile updating functionality
Prakhar896 Jul 29, 2025
7fae0a1
working on change password fields
Prakhar896 Jul 29, 2025
9b3ed02
moved contact details into main card on the left
Prakhar896 Jul 30, 2025
d27026b
added AccountActivity card with rendering of audit logs
Prakhar896 Jul 30, 2025
474496a
changed grid layout such that account activity has more space
Prakhar896 Jul 30, 2025
34b89cf
enter key now opens save dialog if changes are made
Prakhar896 Jul 30, 2025
570b11f
fixed bug where details not updated overwrote database updates due to…
Prakhar896 Jul 30, 2025
2bd078b
added ChangePasswordDialogButton
Prakhar896 Jul 31, 2025
90acc26
change password dialog is now functioning
Prakhar896 Jul 31, 2025
af65b3c
completed profile avatar uploading
Prakhar896 Jul 31, 2025
724b889
added skeleton-based loading to all profile page elements
Prakhar896 Jul 31, 2025
b4baa6a
added some QoL improvements
Prakhar896 Jul 31, 2025
7dd7367
changed login success redirect to catalogue browser
Prakhar896 Jul 31, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
"@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",
"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",
Expand Down
42 changes: 42 additions & 0 deletions src/components/Identity/AccountActivityCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Box, Card, CardBody, CardHeader, Heading, Skeleton, Spinner, Stack, StackSeparator, Text, useMediaQuery } from '@chakra-ui/react'
import React from 'react'

function AccountActivityCard({ infoLoading, auditLogsData }) {
const [isSmallerThan800] = useMediaQuery("(max-width: 800px)");

return (
<Card.Root border={'thin solid lightgrey'} borderRadius={'20px'} h={'100%'} w={'100%'} shadow={'lg'}>
<Card.Header>
<Text alignSelf={'flex-start'} fontSize={'xl'} fontWeight={'bolder'} mt={'10px'}>Account Activity</Text>
</Card.Header>

<Card.Body maxH={isSmallerThan800 ? '500px' : '400px'} overflow={"auto"}>
<Skeleton loading={infoLoading} w={'100%'} h={isSmallerThan800 ? '400px': '350px'}>
{auditLogsData.length == 0 ? (
<>
{/* <Spinner />
<Text>Retrieving...</Text> */}
<Text color={'grey'}>💤 Oops! No account activity yet.</Text>
</>
) : (
<Stack divider={<StackSeparator />} gap={4}>
{auditLogsData.map((log, index) => (
<Box key={index} p='5' shadow='md' borderWidth='1px' borderRadius='md'>
<Heading size='md' textTransform='uppercase'>
{log.title}
</Heading>
<Heading size='sm' color='gray.500' textTransform='uppercase'>{log.created}</Heading>
<Text pt='2' fontSize='sm'>
{log.description}
</Text>
</Box>
))}
</Stack>
)}
</Skeleton>
</Card.Body>
</Card.Root>
)
}

export default AccountActivityCard
145 changes: 145 additions & 0 deletions src/components/Identity/ChangePasswordDialogButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { Button, CloseButton, Dialog, Field, Input, Portal, Text } from '@chakra-ui/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) {
setStartedTyping(false);
setConfirmNewPass('');
}
}, [newPass]);

useEffect(() => {
if (confirmNewPass.length > 0 && !startedTyping) {
setStartedTyping(true);
}
}, [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 (
<Dialog.Root placement={'center'}>
<Dialog.Trigger asChild>
<Button bgColor={'primaryColour'} color={'white'}>Change Password</Button>
</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Change Password</Dialog.Title>
</Dialog.Header>
<Dialog.Body>
<Text>Verify your identity before changing your password.</Text>
<Field.Root mt={'30px'} w='100%'>
<Field.Label fontSize={'md'}>Current Password</Field.Label>
<Input placeholder="Enter here" type='password' onKeyDown={handleKeyDown} value={currentPass} onChange={e => setCurrentPass(e.target.value)} />
</Field.Root>

<Field.Root mt={'30px'} w='100%'>
<Field.Label fontSize={'md'}>New Password</Field.Label>
<Input placeholder="Enter here" type='password' onKeyDown={handleKeyDown} value={newPass} onChange={e => setNewPass(e.target.value)} />
</Field.Root>

<Field.Root mt={'10px'} w='100%'>
<Field.Label fontSize={'md'}>Confirm New Password</Field.Label>
<Input placeholder={newPass ? 'Enter here' : 'Type New Password First.'} type='password' onKeyDown={handleKeyDown} disabled={!newPass} value={confirmNewPass} onChange={e => setConfirmNewPass(e.target.value)} />
{startedTyping && newPass !== confirmNewPass && (
<Text color={'red.500'} fontSize={'sm'} mt={'5px'}>
Passwords do not match.
</Text>
)}
</Field.Root>
</Dialog.Body>
<Dialog.Footer>
{/* <Dialog.ActionTrigger asChild>
<Button variant={'outline'} colorPalette={'red'}>Close</Button>
</Dialog.ActionTrigger> */}
<Button variant={'ArchPrimary'} disabled={!currentPass || !newPass || !confirmNewPass || newPass !== confirmNewPass} onClick={handleChangePassword} loading={changing} loadingText='Changing...'>Change</Button>
</Dialog.Footer>
<Dialog.CloseTrigger asChild>
<CloseButton size="sm" />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
)
}

export default ChangePasswordDialogButton
113 changes: 113 additions & 0 deletions src/components/Identity/ProfileActionBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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, getAccountInfo, enterKeyHit }) {
const [changesMade, setChangesMade] = useState(false);
const [isConfirmSaveOpen, setIsConfirmSaveOpen] = useState(false);
const [saveLoading, setSaveLoading] = useState(false);

useEffect(() => {
setChangesMade(!isEqual(accountInfo, originalAccountInfo));
}, [accountInfo, originalAccountInfo])

useEffect(() => {
if (changesMade) {
setIsConfirmSaveOpen(true);
}
}, [enterKeyHit]);

const handleCancelChanges = () => {
setAccountInfo(originalAccountInfo);
}

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', updateDict);

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 (
<ActionBar.Root open={changesMade}>
<Portal>
<ActionBar.Positioner>
<ActionBar.Content>
<Button variant={'outline'} colorPalette={'red'} onClick={handleCancelChanges}>Reset</Button>

<Dialog.Root placement="center" open={isConfirmSaveOpen} onOpenChange={(e) => setIsConfirmSaveOpen(e.open)}>
<Dialog.Trigger asChild>
<Button variant={"outline"} colorPalette={'green'} size="sm">Save Changes</Button>
</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Update Profile</Dialog.Title>
</Dialog.Header>
<Dialog.Body>
<Dialog.Description>
Are you sure you'd like to save these changes? This action cannot be undone.
</Dialog.Description>
</Dialog.Body>
<Dialog.Footer>
<Button variant={'solid'} colorPalette={'red'} onClick={() => setIsConfirmSaveOpen(false)}>Cancel</Button>
<Button bgColor={'primaryColour'} color={'white'} onClick={handleSaveChanges} loading={saveLoading} loadingText={'Saving...'}>Save Changes</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
</ActionBar.Content>
</ActionBar.Positioner>
</Portal>
</ActionBar.Root>
)
}

export default ProfileActionBar
Loading