Files
Checkmate/Client/src/Components/TabPanels/Account/ProfilePanel.jsx
T

511 lines
15 KiB
React
Raw Normal View History

2024-06-18 19:59:23 -04:00
import { useTheme } from "@emotion/react";
2024-07-05 14:43:29 -04:00
import { useRef, useState } from "react";
2024-06-18 19:59:23 -04:00
import TabPanel from "@mui/lab/TabPanel";
2024-06-26 19:06:10 -04:00
import { Box, Divider, Modal, Stack, Typography } from "@mui/material";
2024-06-18 19:59:23 -04:00
import ButtonSpinner from "../../ButtonSpinner";
import Button from "../../Button";
import Avatar from "../../Avatar";
import Field from "../../Inputs/Field";
import ImageField from "../../Inputs/Image";
import { credentials, imageValidation } from "../../../Validation/validation";
2024-06-24 21:32:30 -04:00
import { useDispatch, useSelector } from "react-redux";
2024-07-09 19:11:16 -04:00
import {
clearAuthState,
deleteUser,
update,
} from "../../../Features/Auth/authSlice";
2024-06-26 19:06:10 -04:00
import ImageIcon from "@mui/icons-material/Image";
import ProgressUpload from "../../ProgressBars";
2024-07-04 20:47:23 -04:00
import { formatBytes } from "../../../Utils/fileUtils";
2024-07-30 11:25:14 -07:00
import { clearUptimeMonitorState } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
2024-07-11 18:53:14 -04:00
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
2024-06-18 19:59:23 -04:00
/**
* ProfilePanel component displays a form for editing user profile information
* and allows for actions like updating profile picture, credentials,
* and deleting account.
*
* @returns {JSX.Element}
*/
const ProfilePanel = () => {
const theme = useTheme();
2024-06-24 21:32:30 -04:00
const dispatch = useDispatch();
2024-07-05 19:52:29 -04:00
//redux state
const { user, authToken, isLoading } = useSelector((state) => state.auth);
2024-07-05 19:52:29 -04:00
2024-06-23 17:41:40 -04:00
const idToName = {
2024-07-22 12:40:44 -04:00
"edit-first-name": "firstName",
"edit-last-name": "lastName",
2024-07-04 20:47:23 -04:00
// Disabled for now, will revisit in the future
// "edit-email": "email",
2024-06-23 17:41:40 -04:00
};
2024-07-05 19:52:29 -04:00
// Local state for form data, errors, and file handling
const [localData, setLocalData] = useState({
2024-07-22 12:40:44 -04:00
firstName: user.firstName,
lastName: user.lastName,
2024-07-05 19:52:29 -04:00
// email: user.email, // Disabled for now
});
const [errors, setErrors] = useState({});
2024-07-05 19:52:29 -04:00
const [file, setFile] = useState();
const intervalRef = useRef(null);
const [progress, setProgress] = useState({ value: 0, isLoading: false });
2024-07-04 20:47:23 -04:00
2024-07-05 19:52:29 -04:00
// Handles input field changes and performs validation
const handleChange = (event) => {
errors["unchanged"] && clearError("unchanged");
const { value, id } = event.target;
2024-06-23 17:41:40 -04:00
const name = idToName[id];
setLocalData((prev) => ({
...prev,
2024-06-23 17:41:40 -04:00
[name]: value,
}));
2024-06-24 21:32:30 -04:00
validateField({ [name]: value }, credentials, name);
};
2024-07-04 20:47:23 -04:00
2024-07-05 19:52:29 -04:00
// Handles image file
2024-06-26 19:06:10 -04:00
const handlePicture = (event) => {
const pic = event.target.files[0];
2024-07-04 20:47:23 -04:00
let error = validateField(
{ type: pic.type, size: pic.size },
imageValidation
);
if (error) return;
2024-07-05 14:43:29 -04:00
setProgress((prev) => ({ ...prev, isLoading: true }));
2024-07-05 18:48:32 -04:00
setFile({
src: URL.createObjectURL(pic),
name: pic.name,
size: formatBytes(pic.size),
2024-07-09 16:46:24 -04:00
delete: false,
2024-07-05 18:48:32 -04:00
});
2024-06-26 19:06:10 -04:00
2024-07-11 19:00:31 -04:00
//TODO - potentitally remove, will revisit in the future
2024-07-05 14:43:29 -04:00
intervalRef.current = setInterval(() => {
const buffer = 12;
setProgress((prev) => {
if (prev.value + buffer >= 100) {
clearInterval(intervalRef.current);
return { value: 100, isLoading: false };
}
return { ...prev, value: prev.value + buffer };
});
}, 120);
2024-06-26 19:06:10 -04:00
};
2024-07-04 20:47:23 -04:00
2024-07-05 19:52:29 -04:00
// Validates input against provided schema and updates error state
2024-07-04 20:47:23 -04:00
const validateField = (toValidate, schema, name = "picture") => {
const { error } = schema.validate(toValidate, { abortEarly: false });
setErrors((prev) => {
const prevErrors = { ...prev };
if (error) prevErrors[name] = error.details[0].message;
else delete prevErrors[name];
return prevErrors;
});
if (error) return true;
};
2024-07-05 19:52:29 -04:00
// Clears specific error from errors state
const clearError = (err) => {
setErrors((prev) => {
const updatedErrors = { ...prev };
if (updatedErrors[err]) delete updatedErrors[err];
return updatedErrors;
});
};
// Resets picture-related states and clears interval
const removePicture = () => {
2024-07-05 19:52:29 -04:00
errors["picture"] && clearError("picture");
2024-07-09 16:46:24 -04:00
setFile({ delete: true });
2024-07-05 19:52:29 -04:00
clearInterval(intervalRef.current); // interrupt interval if image upload is canceled prior to completing the process
setProgress({ value: 0, isLoading: false });
2024-06-26 19:06:10 -04:00
};
2024-07-05 19:52:29 -04:00
2024-07-09 16:46:24 -04:00
// Opens the picture update modal
const openPictureModal = () => {
setIsOpen("picture");
setFile({ delete: localData.deleteProfileImage });
};
2024-07-09 13:54:59 -04:00
2024-07-05 19:52:29 -04:00
// Closes the picture update modal and resets related states
const closePictureModal = () => {
2024-07-09 16:46:24 -04:00
errors["picture"] && clearError("picture");
setFile(); //reset file
clearInterval(intervalRef.current); // interrupt interval if image upload is canceled prior to completing the process
setProgress({ value: 0, isLoading: false });
2024-06-26 19:06:10 -04:00
setIsOpen("");
2024-06-18 19:59:23 -04:00
};
2024-07-05 14:43:29 -04:00
2024-07-05 19:52:29 -04:00
// Updates profile image displayed on UI
2024-06-26 19:06:10 -04:00
const handleUpdatePicture = () => {
setProgress({ value: 0, isLoading: false });
2024-07-04 20:47:23 -04:00
setLocalData((prev) => ({
...prev,
2024-07-05 19:52:29 -04:00
file: file.src,
2024-07-09 16:46:24 -04:00
deleteProfileImage: false,
2024-07-04 20:47:23 -04:00
}));
2024-06-26 19:06:10 -04:00
setIsOpen("");
errors["unchanged"] && clearError("unchanged");
2024-06-26 19:06:10 -04:00
};
2024-07-05 14:43:29 -04:00
2024-07-05 19:52:29 -04:00
// Handles form submission to update user profile
2024-07-11 18:53:14 -04:00
const handleSaveProfile = async (event) => {
2024-06-24 21:32:30 -04:00
event.preventDefault();
2024-07-09 16:46:24 -04:00
if (
2024-07-22 12:40:44 -04:00
localData.firstName === user.firstName &&
localData.lastName === user.lastName &&
2024-07-09 16:46:24 -04:00
localData.deleteProfileImage === undefined &&
localData.file === undefined
) {
2024-07-11 18:53:14 -04:00
createToast({
body: "Unable to update profile — no changes detected.",
2024-07-11 18:53:14 -04:00
});
setErrors({ unchanged: "unable to update profile" });
2024-07-09 16:46:24 -04:00
return;
}
2024-07-11 18:53:14 -04:00
const action = await dispatch(update({ authToken, localData }));
if (action.payload.success) {
createToast({
body: "Your profile data was changed successfully.",
});
} else {
createToast({
body: "There was an error updating your profile data.",
});
}
2024-06-18 19:59:23 -04:00
};
2024-07-05 19:52:29 -04:00
// Removes current profile image from UI
const handleDeletePicture = () => {
2024-07-09 13:02:33 -04:00
setLocalData((prev) => ({
...prev,
deleteProfileImage: true,
}));
errors["unchanged"] && clearError("unchanged");
2024-07-05 19:52:29 -04:00
};
// Initiates the account deletion process
const handleDeleteAccount = async () => {
const action = await dispatch(deleteUser(authToken));
if (action.payload.success) {
dispatch(clearAuthState());
2024-07-30 11:25:14 -07:00
dispatch(clearUptimeMonitorState());
2024-07-15 18:47:23 -04:00
} else {
if (action.payload) {
// dispatch errors
createToast({
body: action.payload.msg,
});
} else {
// unknown errors
createToast({
body: "Unknown error.",
});
}
}
2024-07-05 19:52:29 -04:00
};
// Modal state and control functions
2024-07-05 18:48:32 -04:00
const [isOpen, setIsOpen] = useState("");
const isModalOpen = (name) => isOpen === name;
2024-06-18 19:59:23 -04:00
return (
2024-08-27 20:11:59 -04:00
<TabPanel
value="profile"
sx={{
"& h1, & p, & input": {
color: theme.palette.text.tertiary,
},
}}
>
2024-08-20 19:34:56 -04:00
<Stack
component="form"
className="edit-profile-form"
noValidate
spellCheck="false"
2024-08-27 20:11:59 -04:00
gap={theme.spacing(20)}
2024-08-20 19:34:56 -04:00
>
<Stack direction="row">
<Box flex={0.9}>
<Typography component="h1">First name</Typography>
2024-08-20 19:34:56 -04:00
</Box>
<Field
id="edit-first-name"
2024-07-22 12:40:44 -04:00
value={localData.firstName}
placeholder="Enter your first name"
autoComplete="given-name"
onChange={handleChange}
error={errors[idToName["edit-first-name"]]}
/>
2024-08-20 19:34:56 -04:00
</Stack>
<Stack direction="row">
<Box flex={0.9}>
<Typography component="h1">Last name</Typography>
2024-08-20 19:34:56 -04:00
</Box>
<Field
id="edit-last-name"
placeholder="Enter your last name"
autoComplete="family-name"
2024-07-22 12:40:44 -04:00
value={localData.lastName}
onChange={handleChange}
error={errors[idToName["edit-last-name"]]}
/>
2024-08-20 19:34:56 -04:00
</Stack>
<Stack direction="row">
<Stack flex={0.9}>
<Typography component="h1">Email</Typography>
2024-08-20 19:34:56 -04:00
<Typography component="p" sx={{ opacity: 0.6 }}>
This is your current email address — it cannot be changed.
2024-06-18 19:59:23 -04:00
</Typography>
</Stack>
<Field
id="edit-email"
value={user.email}
placeholder="Enter your email"
autoComplete="email"
onChange={() => logger.warn("disabled")}
// error={errors[idToName["edit-email"]]}
disabled={true}
/>
2024-08-20 19:34:56 -04:00
</Stack>
<Stack direction="row">
<Stack flex={0.9}>
<Typography component="h1">Your photo</Typography>
2024-08-20 19:34:56 -04:00
<Typography component="p" sx={{ opacity: 0.6 }}>
2024-06-18 19:59:23 -04:00
This photo will be displayed in your profile page.
</Typography>
</Stack>
2024-08-20 19:34:56 -04:00
<Stack direction="row" alignItems="center" flex={1}>
2024-07-09 13:54:59 -04:00
<Avatar
src={
localData?.deleteProfileImage
? "/static/images/avatar/2.jpg"
2024-07-09 16:46:24 -04:00
: localData?.file
? localData.file
2024-07-09 13:54:59 -04:00
: ""
}
sx={{ mr: "8px" }}
/>
<Button
2024-06-18 19:59:23 -04:00
level="tertiary"
label="Delete"
2024-07-03 17:49:40 -04:00
onClick={handleDeletePicture}
2024-06-18 19:59:23 -04:00
/>
<Button
level="tertiary"
label="Update"
2024-07-09 16:46:24 -04:00
onClick={openPictureModal}
2024-06-18 19:59:23 -04:00
sx={{
color: theme.palette.primary.main,
2024-06-18 19:59:23 -04:00
}}
/>
</Stack>
2024-08-20 19:34:56 -04:00
</Stack>
2024-06-21 13:16:11 -04:00
<Divider
aria-hidden="true"
width="0"
2024-08-27 20:11:59 -04:00
sx={{
marginY: theme.spacing(1),
}}
2024-06-21 13:16:11 -04:00
/>
<Stack direction="row" justifyContent="flex-end">
<Box width="fit-content">
<ButtonSpinner
level="primary"
label="Save"
onClick={handleSaveProfile}
isLoading={isLoading}
2024-06-21 13:16:11 -04:00
loadingText="Saving..."
disabled={
Object.keys(errors).length !== 0 && !errors?.picture && true
}
2024-06-21 13:16:11 -04:00
sx={{
2024-08-27 20:11:59 -04:00
paddingX: theme.spacing(12),
2024-06-21 13:16:11 -04:00
}}
/>
</Box>
</Stack>
2024-08-20 19:34:56 -04:00
</Stack>
2024-08-27 20:11:59 -04:00
<Divider
aria-hidden="true"
sx={{
marginY: theme.spacing(20),
borderColor: theme.palette.border.light,
}}
/>
2024-08-20 19:34:56 -04:00
<Stack
component="form"
noValidate
spellCheck="false"
2024-08-27 20:11:59 -04:00
gap={theme.spacing(4)}
2024-08-20 19:34:56 -04:00
>
<Box>
<Typography component="h1">Delete account</Typography>
<Typography component="p" sx={{ opacity: 0.6 }}>
Note that deleting your account will remove all data from our
system. This is permanent and non-recoverable.
</Typography>
</Box>
<Button
level="error"
label="Delete account"
onClick={() => setIsOpen("delete")}
2024-08-27 20:11:59 -04:00
sx={{ width: "fit-content", mt: theme.spacing(4) }}
2024-08-20 19:34:56 -04:00
/>
</Stack>
2024-06-18 19:59:23 -04:00
{/* TODO - Update ModalPopup Component with @mui for reusability */}
<Modal
aria-labelledby="modal-delete-account"
aria-describedby="delete-account-confirmation"
2024-06-25 17:38:40 -04:00
open={isModalOpen("delete")}
onClose={() => setIsOpen("")}
2024-06-18 19:59:23 -04:00
disablePortal
>
<Stack
2024-08-27 20:11:59 -04:00
gap={theme.spacing(5)}
2024-06-18 19:59:23 -04:00
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
2024-08-08 16:33:32 -04:00
width: 500,
2024-08-27 20:11:59 -04:00
bgcolor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
2024-06-18 19:59:23 -04:00
boxShadow: 24,
2024-08-27 20:11:59 -04:00
p: theme.spacing(15),
2024-06-18 19:59:23 -04:00
"&:focus": {
outline: "none",
},
}}
>
<Typography id="modal-delete-account" component="h1">
2024-06-18 19:59:23 -04:00
Really delete this account?
</Typography>
<Typography id="delete-account-confirmation" component="p">
2024-06-18 19:59:23 -04:00
If you delete your account, you will no longer be able to sign in,
and all of your data will be deleted. Deleting your account is
permanent and non-recoverable action.
</Typography>
2024-08-27 20:11:59 -04:00
<Stack
direction="row"
gap={theme.spacing(5)}
mt={theme.spacing(5)}
justifyContent="flex-end"
>
2024-06-18 19:59:23 -04:00
<Button
level="tertiary"
label="Cancel"
2024-06-25 17:38:40 -04:00
onClick={() => setIsOpen("")}
2024-06-18 19:59:23 -04:00
/>
<ButtonSpinner
level="error"
label="Delete account"
onClick={handleDeleteAccount}
isLoading={isLoading}
/>
</Stack>
</Stack>
</Modal>
2024-06-25 17:38:40 -04:00
<Modal
aria-labelledby="modal-update-picture"
aria-describedby="update-profile-picture"
open={isModalOpen("picture")}
onClose={closePictureModal}
2024-06-25 17:38:40 -04:00
>
<Stack
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
2024-08-20 17:03:19 -04:00
width: 450,
2024-08-27 20:11:59 -04:00
bgcolor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
2024-06-25 17:38:40 -04:00
boxShadow: 24,
2024-08-27 20:11:59 -04:00
p: theme.spacing(15),
2024-06-25 17:38:40 -04:00
"&:focus": {
outline: "none",
},
}}
>
2024-08-27 20:11:59 -04:00
<Typography
id="modal-update-picture"
component="h1"
color={theme.palette.text.secondary}
>
2024-06-26 19:06:10 -04:00
Upload Image
</Typography>
<ImageField
id="update-profile-picture"
2024-07-09 13:54:59 -04:00
src={
2024-07-09 16:46:24 -04:00
file?.delete
2024-07-09 13:54:59 -04:00
? ""
: file?.src
? file.src
2024-07-09 16:46:24 -04:00
: localData?.file
? localData.file
2024-07-09 13:54:59 -04:00
: user?.avatarImage
? `data:image/png;base64,${user.avatarImage}`
: ""
}
loading={progress.isLoading && progress.value !== 100}
2024-06-26 19:06:10 -04:00
onChange={handlePicture}
/>
2024-07-04 20:47:23 -04:00
{progress.isLoading || progress.value !== 0 || errors["picture"] ? (
2024-06-26 19:06:10 -04:00
<ProgressUpload
icon={<ImageIcon />}
2024-07-05 19:52:29 -04:00
label={file?.name}
size={file?.size}
progress={progress.value}
onClick={removePicture}
2024-07-02 14:51:31 -04:00
error={errors["picture"]}
2024-06-26 19:06:10 -04:00
/>
) : (
""
)}
2024-08-27 20:11:59 -04:00
<Stack
direction="row"
mt={theme.spacing(10)}
gap={theme.spacing(5)}
justifyContent="flex-end"
>
2024-06-26 19:06:10 -04:00
<Button
level="secondary"
label="Edit"
disabled
sx={{ mr: "auto" }}
/>
2024-07-03 17:49:40 -04:00
<Button level="tertiary" label="Remove" onClick={removePicture} />
<Button
2024-06-26 19:06:10 -04:00
level="primary"
label="Update"
onClick={handleUpdatePicture}
disabled={
(Object.keys(errors).length !== 0 && errors?.picture) ||
progress.value !== 100
? true
: false
}
2024-06-26 19:06:10 -04:00
/>
2024-06-25 17:38:40 -04:00
</Stack>
</Stack>
</Modal>
2024-06-18 19:59:23 -04:00
</TabPanel>
);
};
ProfilePanel.propTypes = {
// No props are being passed to this component, hence no specific PropTypes are defined.
};
export default ProfilePanel;