Files
Checkmate/client/src/Components/TabPanels/Account/PasswordPanel.jsx
T

257 lines
6.6 KiB
React
Raw Normal View History

2024-06-18 20:16:32 -04:00
import TabPanel from "@mui/lab/TabPanel";
import { useState } from "react";
2024-06-18 20:16:32 -04:00
import { useTheme } from "@emotion/react";
2025-02-13 15:20:12 -08:00
import { Box, Stack, Typography, Button } from "@mui/material";
2024-11-26 13:56:36 +08:00
import { PasswordEndAdornment } from "../../Inputs/TextInput/Adornments";
import TextInput from "../../Inputs/TextInput";
import { credentials } from "../../../Validation/validation";
2024-07-06 14:33:36 -04:00
import Alert from "../../Alert";
2024-07-10 18:26:05 -04:00
import { update } from "../../../Features/Auth/authSlice";
import { useDispatch, useSelector } from "react-redux";
2024-07-11 19:00:31 -04:00
import { createToast } from "../../../Utils/toastUtils";
2024-12-04 23:52:38 +08:00
import { getTouchedFieldErrors } from "../../../Validation/error";
2025-05-06 22:08:36 -04:00
import { useTranslation } from "react-i18next";
2024-12-04 23:52:38 +08:00
const defaultPasswordsState = {
password: "",
newPassword: "",
confirm: "",
};
2024-06-18 20:16:32 -04:00
/**
* PasswordPanel component manages the form for editing password.
*
* @returns {JSX.Element}
*/
const PasswordPanel = () => {
const theme = useTheme();
const dispatch = useDispatch();
2025-05-06 22:08:36 -04:00
const { t } = useTranslation();
2024-07-10 18:26:05 -04:00
2025-01-07 21:27:52 -05:00
const SPACING_GAP = theme.spacing(12);
2024-12-04 15:27:02 -07:00
//redux state
const { isLoading } = useSelector((state) => state.auth);
2024-06-18 20:16:32 -04:00
const idToName = {
"edit-current-password": "password",
"edit-new-password": "newPassword",
"edit-confirm-password": "confirm",
};
2024-07-10 18:26:05 -04:00
2024-12-04 23:52:38 +08:00
const [localData, setLocalData] = useState(defaultPasswordsState);
const [errors, setErrors] = useState(defaultPasswordsState);
const [touchedFields, setTouchedFields] = useState({
password: false,
newPassword: false,
confirm: false,
});
2024-12-04 23:52:38 +08:00
const handleChange = (event) => {
const { value, id } = event.target;
const name = idToName[id];
2024-12-04 23:52:38 +08:00
const updatedData = {
...localData,
[name]: value,
2024-12-04 23:52:38 +08:00
};
const updatedTouchedFields = {
...touchedFields,
[name]: true,
};
const validation = credentials.validate(
2024-12-04 23:52:38 +08:00
{ ...updatedData },
{ abortEarly: false, context: { password: updatedData.newPassword } }
);
2024-12-04 23:52:38 +08:00
const updatedErrors = getTouchedFieldErrors(validation, updatedTouchedFields);
2024-06-23 17:41:40 -04:00
2024-12-04 23:52:38 +08:00
if (!touchedFields[name]) {
setTouchedFields(updatedTouchedFields);
}
setLocalData(updatedData);
setErrors(updatedErrors);
};
const handleSubmit = async (event) => {
event.preventDefault();
2024-06-23 17:41:40 -04:00
const { error } = credentials.validate(localData, {
abortEarly: false,
context: { password: localData.newPassword },
});
2024-06-23 17:41:40 -04:00
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
} else {
const action = await dispatch(update({ localData }));
if (action.payload.success) {
createToast({
body: t("passwordPanel.passwordChangedSuccess", "Your password was changed successfully."),
});
setLocalData({
password: "",
newPassword: "",
confirm: "",
});
} else {
// TODO: Check for other errors?
createToast({
body: t("passwordPanel.passwordInputIncorrect", "Your password input was incorrect."),
});
setErrors({ password: "*" + action.payload.msg + "." });
}
}
};
2024-06-23 17:41:40 -04:00
return (
<TabPanel
value="password"
sx={{
"& h1, & input": {
2025-01-12 09:39:31 -05:00
color: theme.palette.primary.contrastTextTertiary,
},
}}
>
<Stack
component="form"
onSubmit={handleSubmit}
noValidate
spellCheck="false"
2024-10-31 14:43:09 -04:00
gap={theme.spacing(26)}
2025-01-07 21:27:52 -05:00
maxWidth={"80ch"} // Keep maxWidth
>
2024-11-26 13:56:36 +08:00
<TextInput
2024-10-31 14:43:09 -04:00
type="text"
id="hidden-username"
name="username"
autoComplete="username"
hidden={true}
value=""
/>
<Stack
direction="row"
justifyContent={"flex-start"}
alignItems={"center"}
2025-01-07 21:27:52 -05:00
gap={SPACING_GAP}
2024-10-31 14:43:09 -04:00
flexWrap={"wrap"}
>
<Typography
component="h1"
width="20ch"
>
{t("passwordPanel.currentPassword", "Current password")}
2024-10-31 14:43:09 -04:00
</Typography>
2024-11-26 13:56:36 +08:00
<TextInput
type="password"
id="edit-current-password"
placeholder={t("passwordPanel.enterCurrentPassword", "Enter your current password")}
autoComplete="current-password"
value={localData.password}
onChange={handleChange}
2024-11-26 13:56:36 +08:00
error={errors[idToName["edit-current-password"]] ? true : false}
helperText={errors[idToName["edit-current-password"]]}
endAdornment={<PasswordEndAdornment />}
flex={1}
/>
</Stack>
2024-10-31 14:43:09 -04:00
<Stack
direction="row"
2025-01-03 14:30:12 -05:00
alignItems={"flex-start"}
2025-01-07 21:27:52 -05:00
gap={SPACING_GAP}
2024-10-31 14:43:09 -04:00
flexWrap={"wrap"}
>
<Typography
component="h1"
width="20ch"
>
{t("passwordPanel.newPassword", "New password")}
2024-10-31 14:43:09 -04:00
</Typography>
2024-11-26 13:56:36 +08:00
<TextInput
type="password"
id="edit-new-password"
placeholder={t("passwordPanel.enterNewPassword", "Enter your new password")}
autoComplete="new-password"
value={localData.newPassword}
onChange={handleChange}
2024-11-26 13:56:36 +08:00
error={errors[idToName["edit-new-password"]] ? true : false}
helperText={errors[idToName["edit-new-password"]]}
endAdornment={<PasswordEndAdornment />}
flex={1}
/>
</Stack>
2024-10-31 14:43:09 -04:00
<Stack
direction="row"
2025-01-03 14:30:12 -05:00
alignItems={"flex-start"}
2025-01-07 21:27:52 -05:00
gap={SPACING_GAP}
2024-10-31 14:43:09 -04:00
flexWrap={"wrap"}
>
<Typography
component="h1"
width="20ch"
>
{t("passwordPanel.confirmNewPassword", "Confirm new password")}
2024-10-31 14:43:09 -04:00
</Typography>
2024-11-26 13:56:36 +08:00
<TextInput
type="password"
id="edit-confirm-password"
placeholder={t("confirmPassword", "Confirm password")}
autoComplete="new-password"
value={localData.confirm}
onChange={handleChange}
2024-11-26 13:56:36 +08:00
error={errors[idToName["edit-confirm-password"]] ? true : false}
helperText={errors[idToName["edit-confirm-password"]]}
endAdornment={<PasswordEndAdornment />}
flex={1}
/>
</Stack>
2024-10-31 14:43:09 -04:00
{Object.keys(errors).length > 0 && (
<Box sx={{ maxWidth: "70ch" }}>
<Alert
variant="warning"
body={t("passwordPanel.passwordRequirements", "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.")}
/>
</Box>
2024-10-31 14:43:09 -04:00
)}
<Stack
direction="row"
justifyContent="flex-end"
>
2025-02-13 15:20:12 -08:00
<Button
variant="contained"
2025-01-12 13:03:09 -05:00
color="accent"
2024-10-31 14:43:09 -04:00
type="submit"
loading={isLoading}
2025-05-12 21:04:32 -04:00
loadingIndicator={t("commonSaving", "Saving...")}
2024-11-03 06:50:49 -05:00
disabled={
2024-12-04 23:52:38 +08:00
Object.keys(errors).length > 0 ||
Object.values(localData).filter((value) => value === "").length > 0
2024-11-03 06:50:49 -05:00
}
sx={{
px: theme.spacing(12),
mt: theme.spacing(20),
}}
>
{t("commonSave", "Save")}
2025-02-13 15:20:12 -08:00
</Button>
</Stack>
</Stack>
</TabPanel>
);
2024-06-18 20:16:32 -04:00
};
PasswordPanel.propTypes = {
// No props are being passed to this component, hence no specific PropTypes are defined.
};
2024-06-18 20:16:32 -04:00
2025-01-07 21:27:52 -05:00
export default PasswordPanel;