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-10-22 08:54:12 -04:00
import { Box , Button , Divider , Stack , Typography } from "@mui/material" ;
2024-06-22 18:39:11 -04:00
import Avatar from "../../Avatar" ;
2024-11-26 13:37:35 +08:00
import TextInput from "../../Inputs/TextInput" ;
2024-07-13 14:54:06 -04:00
import ImageField from "../../Inputs/Image" ;
2024-07-15 20:51:18 -04:00
import { credentials , imageValidation } from "../../../Validation/validation" ;
2024-06-24 21:32:30 -04:00
import { useDispatch , useSelector } from "react-redux" ;
2024-10-17 12:05:06 +08: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" ;
2024-08-19 14:20:55 -07:00
import { logger } from "../../../Utils/Logger" ;
2024-08-31 18:10:50 -04:00
import LoadingButton from "@mui/lab/LoadingButton" ;
2024-10-22 08:54:12 -04:00
import { GenericDialog } from "../../Dialog/genericDialog" ;
2024-10-22 10:09:03 -04:00
import Dialog from "../../Dialog" ;
2024-07-12 11:33:34 -04:00
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 = () => {
2024-10-17 12:05:06 +08:00
const theme = useTheme ();
const dispatch = useDispatch ();
2024-12-04 15:27:02 -07:00
const SPACING_GAP = theme . spacing ( 12 );
2024-06-22 18:39:11 -04:00
2024-10-17 12:05:06 +08:00
//redux state
const { user , authToken , isLoading } = useSelector (( state ) => state . auth );
2024-07-05 19:52:29 -04:00
2024-10-17 12:05:06 +08:00
const idToName = {
"edit-first-name" : "firstName" ,
"edit-last-name" : "lastName" ,
// Disabled for now, will revisit in the future
// "edit-email": "email",
};
2024-07-05 19:52:29 -04:00
2024-10-17 12:05:06 +08:00
// Local state for form data, errors, and file handling
const [ localData , setLocalData ] = useState ({
firstName : user . firstName ,
lastName : user . lastName ,
// email: user.email, // Disabled for now
});
const [ errors , setErrors ] = useState ({});
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-10-17 12:05:06 +08:00
// Handles input field changes and performs validation
const handleChange = ( event ) => {
errors [ "unchanged" ] && clearError ( "unchanged" );
const { value , id } = event . target ;
const name = idToName [ id ];
setLocalData (( prev ) => ({
... prev ,
[ name ] : value ,
}));
2024-06-24 21:32:30 -04:00
2024-10-17 12:05:06 +08:00
validateField ({ [ name ] : value }, credentials , name );
};
2024-07-04 20:47:23 -04:00
2024-10-17 12:05:06 +08:00
// Handles image file
const handlePicture = ( event ) => {
const pic = event . target . files [ 0 ];
let error = validateField ({ type : pic . type , size : pic . size }, imageValidation );
if ( error ) return ;
2024-07-04 20:47:23 -04:00
2024-10-17 12:05:06 +08:00
setProgress (( prev ) => ({ ... prev , isLoading : true }));
setFile ({
src : URL . createObjectURL ( pic ),
name : pic . name ,
size : formatBytes ( pic . size ),
delete : false ,
});
2024-06-26 19:06:10 -04:00
2024-10-17 12:05:06 +08:00
//TODO - potentitally remove, will revisit in the future
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-07-04 20:47:23 -04:00
2024-10-17 12:05:06 +08:00
// Validates input against provided schema and updates error state
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-04 20:47:23 -04:00
2024-10-17 12:05:06 +08:00
// Clears specific error from errors state
const clearError = ( err ) => {
setErrors (( prev ) => {
const updatedErrors = { ... prev };
if ( updatedErrors [ err ]) delete updatedErrors [ err ];
return updatedErrors ;
});
};
2024-07-05 19:52:29 -04:00
2024-10-17 12:05:06 +08:00
// Resets picture-related states and clears interval
const removePicture = () => {
errors [ "picture" ] && clearError ( "picture" );
setFile ({ delete : true });
clearInterval ( intervalRef . current ); // interrupt interval if image upload is canceled prior to completing the process
setProgress ({ value : 0 , isLoading : false });
};
2024-07-05 19:52:29 -04:00
2024-10-17 12:05:06 +08:00
// Opens the picture update modal
const openPictureModal = () => {
setIsOpen ( "picture" );
setFile ({ delete : localData . deleteProfileImage });
};
2024-07-09 13:54:59 -04:00
2024-10-17 12:05:06 +08:00
// Closes the picture update modal and resets related states
const closePictureModal = () => {
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 });
setIsOpen ( "" );
};
2024-07-05 14:43:29 -04:00
2024-10-17 12:05:06 +08:00
// Updates profile image displayed on UI
const handleUpdatePicture = () => {
setProgress ({ value : 0 , isLoading : false });
setLocalData (( prev ) => ({
... prev ,
file : file . src ,
deleteProfileImage : false ,
}));
setIsOpen ( "" );
errors [ "unchanged" ] && clearError ( "unchanged" );
};
2024-07-05 14:43:29 -04:00
2024-10-17 12:05:06 +08:00
// Handles form submission to update user profile
const handleSaveProfile = async ( event ) => {
event . preventDefault ();
if (
localData . firstName === user . firstName &&
localData . lastName === user . lastName &&
localData . deleteProfileImage === undefined &&
localData . file === undefined
) {
createToast ({
body : "Unable to update profile — no changes detected." ,
});
setErrors ({ unchanged : "unable to update profile" });
return ;
}
2024-06-25 13:01:21 -04:00
2024-10-17 12:05:06 +08: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-10-17 12:05:06 +08:00
// Removes current profile image from UI
const handleDeletePicture = () => {
setLocalData (( prev ) => ({
... prev ,
deleteProfileImage : true ,
}));
errors [ "unchanged" ] && clearError ( "unchanged" );
};
2024-07-05 19:52:29 -04:00
2024-10-17 12:05:06 +08:00
// Initiates the account deletion process
const handleDeleteAccount = async () => {
const action = await dispatch ( deleteUser ( authToken ));
if ( action . payload . success ) {
dispatch ( clearAuthState ());
dispatch ( clearUptimeMonitorState ());
} 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
2024-10-17 12:05:06 +08:00
// Modal state and control functions
const [ isOpen , setIsOpen ] = useState ( "" );
const isModalOpen = ( name ) => isOpen === name ;
2024-07-05 18:48:32 -04:00
2024-10-17 12:05:06 +08:00
return (
< TabPanel
value = "profile"
sx = {{
"& h1, & p, & input" : {
color : theme . palette . text . tertiary ,
},
}}
>
< Stack
component = "form"
className = "edit-profile-form"
noValidate
spellCheck = "false"
2024-12-06 01:47:47 -07:00
gap = { SPACING_GAP }
2024-10-17 12:05:06 +08:00
>
2024-12-06 01:47:47 -07:00
< Stack direction = "row" gap = { SPACING_GAP }>
2024-10-17 12:05:06 +08:00
< Box flex = { 0.9 }>
< Typography component = "h1" > First name </ Typography >
</ Box >
2024-11-26 13:37:35 +08:00
< TextInput
2024-10-17 12:05:06 +08:00
id = "edit-first-name"
value = { localData . firstName }
placeholder = "Enter your first name"
autoComplete = "given-name"
onChange = { handleChange }
2024-11-26 13:37:35 +08:00
error = { errors [ idToName [ "edit-first-name" ]] ? true : false }
helperText = { errors [ idToName [ "edit-first-name" ]]}
flex = { 1 }
2024-10-17 12:05:06 +08:00
/>
</ Stack >
2024-12-06 01:47:47 -07:00
< Stack direction = "row" gap = { SPACING_GAP }>
2024-10-17 12:05:06 +08:00
< Box flex = { 0.9 }>
< Typography component = "h1" > Last name </ Typography >
</ Box >
2024-11-26 13:37:35 +08:00
< TextInput
2024-10-17 12:05:06 +08:00
id = "edit-last-name"
placeholder = "Enter your last name"
autoComplete = "family-name"
value = { localData . lastName }
onChange = { handleChange }
2024-11-26 13:37:35 +08:00
error = { errors [ idToName [ "edit-last-name" ]] ? true : false }
helperText = { errors [ idToName [ "edit-last-name" ]]}
flex = { 1 }
2024-10-17 12:05:06 +08:00
/>
</ Stack >
2024-12-06 01:47:47 -07:00
< Stack direction = "row" gap = { SPACING_GAP }>
2024-10-17 12:05:06 +08:00
< Stack flex = { 0.9 }>
< Typography component = "h1" > Email </ Typography >
< Typography
component = "p"
sx = {{ opacity : 0.6 }}
>
This is your current email address — it cannot be changed .
</ Typography >
</ Stack >
2024-11-26 13:37:35 +08:00
< TextInput
2024-10-17 12:05:06 +08:00
id = "edit-email"
value = { user . email }
placeholder = "Enter your email"
autoComplete = "email"
onChange = {() => logger . warn ( "disabled" )}
disabled = { true }
2024-11-26 13:37:35 +08:00
flex = { 1 }
2024-10-17 12:05:06 +08:00
/>
</ Stack >
2024-12-06 01:47:47 -07:00
< Stack direction = "row" gap = { SPACING_GAP }>
2024-10-17 12:05:06 +08:00
< Stack flex = { 0.9 }>
< Typography component = "h1" > Your photo </ Typography >
< Typography
component = "p"
sx = {{ opacity : 0.6 }}
>
This photo will be displayed in your profile page .
</ Typography >
</ Stack >
< Stack
direction = "row"
alignItems = "center"
flex = { 1 }
>
< Avatar
src = {
localData ? . deleteProfileImage
? "/static/images/avatar/2.jpg"
: localData ? . file
? localData . file
: ""
}
sx = {{ mr : "8px" }}
/>
< Button
variant = "text"
color = "info"
onClick = { handleDeletePicture }
>
Delete
</ Button >
< Button
variant = "text"
color = "primary"
onClick = { openPictureModal }
>
Update
</ Button >
</ Stack >
</ Stack >
< Divider
aria - hidden = "true"
width = "0"
sx = {{
marginY : theme . spacing ( 1 ),
}}
/>
< Stack
direction = "row"
justifyContent = "flex-end"
>
< Box width = "fit-content" >
< LoadingButton
variant = "contained"
color = "primary"
onClick = { handleSaveProfile }
loading = { isLoading }
loadingIndicator = "Saving..."
disabled = { Object . keys ( errors ). length !== 0 && ! errors ? . picture && true }
sx = {{ px : theme . spacing ( 12 ) }}
>
Save
</ LoadingButton >
</ Box >
</ Stack >
</ Stack >
< Divider
aria - hidden = "true"
sx = {{
marginY : theme . spacing ( 20 ),
borderColor : theme . palette . border . light ,
}}
/>
{ ! user . role . includes ( "demo" ) && (
< Box
component = "form"
noValidate
spellCheck = "false"
>
< Box mb = { theme . spacing ( 6 )}>
< 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
variant = "contained"
color = "error"
onClick = {() => setIsOpen ( "delete" )}
>
Delete account
</ Button >
</ Box >
)}
2024-10-22 10:09:03 -04:00
< Dialog
2024-10-17 12:05:06 +08:00
open = { isModalOpen ( "delete" )}
2024-10-22 08:54:12 -04:00
theme = { theme }
2024-10-22 10:09:03 -04:00
title = { "Really delete this account?" }
description = {
"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."
}
onCancel = {() => setIsOpen ( "" )}
confirmationButtonLabel = { "Delete account" }
onConfirm = { handleDeleteAccount }
isLoading = { isLoading }
/>
2024-10-22 08:54:12 -04:00
< GenericDialog
2024-10-22 11:38:54 -04:00
title = { "Upload Image" }
2024-10-17 12:05:06 +08:00
open = { isModalOpen ( "picture" )}
2024-10-22 10:09:03 -04:00
onClose = { closePictureModal }
2024-10-22 08:54:12 -04:00
theme = { theme }
2024-10-17 12:05:06 +08:00
>
2024-10-22 08:54:12 -04:00
< ImageField
id = "update-profile-picture"
src = {
file ? . delete
? ""
: file ? . src
? file . src
: localData ? . file
? localData . file
: user ? . avatarImage
? `data:image/png;base64, ${ user . avatarImage } `
: ""
}
loading = { progress . isLoading && progress . value !== 100 }
onChange = { handlePicture }
/>
{ progress . isLoading || progress . value !== 0 || errors [ "picture" ] ? (
< ProgressUpload
icon = {< ImageIcon />}
label = { file ? . name }
size = { file ? . size }
progress = { progress . value }
onClick = { removePicture }
error = { errors [ "picture" ]}
/>
) : (
""
)}
2024-10-17 12:05:06 +08:00
< Stack
2024-10-22 08:54:12 -04:00
direction = "row"
mt = { theme . spacing ( 10 )}
gap = { theme . spacing ( 5 )}
justifyContent = "flex-end"
2024-10-17 12:05:06 +08:00
>
2024-10-22 08:54:12 -04:00
< Button
variant = "text"
color = "info"
onClick = { removePicture }
2024-10-17 12:05:06 +08:00
>
2024-10-22 08:54:12 -04:00
Remove
</ Button >
< Button
variant = "contained"
color = "primary"
onClick = { handleUpdatePicture }
disabled = {
( Object . keys ( errors ). length !== 0 && errors ? . picture ) ||
progress . value !== 100
? true
: false
2024-10-17 12:05:06 +08:00
}
>
2024-10-22 08:54:12 -04:00
Update
</ Button >
2024-10-17 12:05:06 +08:00
</ Stack >
2024-10-22 08:54:12 -04:00
</ GenericDialog >
2024-10-17 12:05:06 +08:00
</ TabPanel >
);
2024-06-18 19:59:23 -04:00
};
ProfilePanel . propTypes = {
2024-10-17 12:05:06 +08:00
// No props are being passed to this component, hence no specific PropTypes are defined.
2024-06-18 19:59:23 -04:00
};
export default ProfilePanel ;