| @@ -0,0 +1,53 @@ | |||
| import { preloadClaims } from "@/app/api/claims"; | |||
| import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; | |||
| import ChangePassword from "@/components/ChangePassword"; | |||
| import StaffSearch from "@/components/StaffSearch"; | |||
| import TeamSearch from "@/components/TeamSearch"; | |||
| import UserGroupSearch from "@/components/UserGroupSearch"; | |||
| import UserSearch from "@/components/UserSearch"; | |||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||
| import Add from "@mui/icons-material/Add"; | |||
| import Button from "@mui/material/Button"; | |||
| import Stack from "@mui/material/Stack"; | |||
| import Typography from "@mui/material/Typography"; | |||
| import { Metadata } from "next"; | |||
| import Link from "next/link"; | |||
| import { Suspense } from "react"; | |||
| export const metadata: Metadata = { | |||
| title: "Change Password", | |||
| }; | |||
| const ChangePasswordPage: React.FC = async () => { | |||
| const { t } = await getServerI18n("User Group"); | |||
| // preloadTeamLeads(); | |||
| // preloadStaff(); | |||
| return ( | |||
| <> | |||
| <Stack | |||
| direction="row" | |||
| justifyContent="space-between" | |||
| flexWrap="wrap" | |||
| rowGap={2} | |||
| > | |||
| <Typography variant="h4" marginInlineEnd={2}> | |||
| {t("Change Password")} | |||
| </Typography> | |||
| </Stack> | |||
| {/* <I18nProvider namespaces={["User Group", "common"]}> | |||
| <Suspense fallback={<UserGroupSearch.Loading />}> | |||
| <UserGroupSearch /> | |||
| </Suspense> | |||
| </I18nProvider> */} | |||
| <I18nProvider namespaces={["User Group", "common"]}> | |||
| <Suspense fallback={<ChangePassword.Loading />}> | |||
| <ChangePassword /> | |||
| </Suspense> | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default ChangePasswordPage; | |||
| @@ -0,0 +1,22 @@ | |||
| import { Edit } from "@mui/icons-material"; | |||
| import { Metadata } from "next"; | |||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||
| import EditUser from "@/components/EditUser"; | |||
| import { Typography } from "@mui/material"; | |||
| import { Suspense } from "react"; | |||
| const User: React.FC = async () => { | |||
| const { t } = await getServerI18n("user"); | |||
| return ( | |||
| <> | |||
| <Typography variant="h4">{t("Edit User")}</Typography> | |||
| <I18nProvider namespaces={["user", "common"]}> | |||
| <Suspense fallback={<EditUser.Loading />}> | |||
| <EditUser /> | |||
| </Suspense> | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default User; | |||
| @@ -0,0 +1,107 @@ | |||
| "use client"; | |||
| import { PasswordInputs, changePassword } from "@/app/api/user/actions"; | |||
| import { Grid } from "@mui/material"; | |||
| import { useRouter } from "next/navigation"; | |||
| import { useCallback, useState } from "react"; | |||
| import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { Button, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material"; | |||
| import { Check, Close, Error } from "@mui/icons-material"; | |||
| import ChagnePasswordForm from "./ChangePasswordForm"; | |||
| import { ServerFetchError } from "@/app/utils/fetchUtil"; | |||
| // interface Props { | |||
| // // auth?: auth[] | |||
| // // users?: UserResult[] | |||
| // } | |||
| const ChangePassword: React.FC = () => { | |||
| const formProps = useForm<PasswordInputs>(); | |||
| const [serverError, setServerError] = useState(""); | |||
| const router = useRouter(); | |||
| // const [tabIndex, setTabIndex] = useState(0); | |||
| const { t } = useTranslation(); | |||
| const onSubmit = useCallback<SubmitHandler<PasswordInputs>>( | |||
| async (data) => { | |||
| try { | |||
| let haveError = false; | |||
| // Minimum eight characters, at least one uppercase letter, one lowercase letter, one number and one special character: | |||
| let regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/ | |||
| if (data.newPassword.length < 8 || data.newPassword.length > 20) { | |||
| haveError = true | |||
| formProps.setError("newPassword", { message: "The password requires 8-20 characters", type: "required" }) | |||
| } | |||
| if (!regex.test(data.newPassword)) { | |||
| haveError = true | |||
| formProps.setError("newPassword", { message: "A combination of uppercase letters, lowercase letters, numbers, and symbols is required.", type: "required" }) | |||
| } | |||
| if (data.password == data.newPassword) { | |||
| haveError = true | |||
| formProps.setError("newPassword", { message: "The new password cannot be the same as the old password", type: "required" }) | |||
| } | |||
| if (data.newPassword != data.newPasswordCheck) { | |||
| haveError = true | |||
| formProps.setError("newPassword", { message: "The new password has to be the same as the new password", type: "required" }) | |||
| formProps.setError("newPasswordCheck", { message: "The new password has to be the same as the new password", type: "required" }) | |||
| } | |||
| if (haveError) { | |||
| return | |||
| } | |||
| const postData = { | |||
| password: data.password, | |||
| newPassword: data.newPassword | |||
| } | |||
| // await changePassword(postData) | |||
| // router.replace("/home") | |||
| } catch (e) { | |||
| console.log(e) | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| } | |||
| }, | |||
| [router] | |||
| ); | |||
| const handleCancel = () => { | |||
| router.push(`/home`); | |||
| }; | |||
| const onSubmitError = useCallback<SubmitErrorHandler<PasswordInputs>>( | |||
| (errors) => { | |||
| console.log(errors); | |||
| }, | |||
| [] | |||
| ); | |||
| return ( | |||
| <FormProvider {...formProps}> | |||
| <Stack | |||
| spacing={2} | |||
| component="form" | |||
| onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||
| > | |||
| <ChagnePasswordForm /> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| variant="outlined" | |||
| startIcon={<Close />} | |||
| onClick={handleCancel} | |||
| > | |||
| {t("Cancel")} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| startIcon={<Check />} | |||
| type="submit" | |||
| // disabled={Boolean(formProps.watch("isGridEditing"))} | |||
| > | |||
| {t("Confirm")} | |||
| </Button> | |||
| </Stack> | |||
| </Stack> | |||
| </FormProvider> | |||
| ); | |||
| }; | |||
| export default ChangePassword; | |||
| @@ -0,0 +1,144 @@ | |||
| "use client"; | |||
| import Stack from "@mui/material/Stack"; | |||
| import Box from "@mui/material/Box"; | |||
| import Card from "@mui/material/Card"; | |||
| import CardContent from "@mui/material/CardContent"; | |||
| import Grid from "@mui/material/Grid"; | |||
| import TextField from "@mui/material/TextField"; | |||
| import Typography from "@mui/material/Typography"; | |||
| import { useFormContext } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { useCallback, useState } from "react"; | |||
| import { PasswordInputs } from "@/app/api/user/actions"; | |||
| import { Visibility, VisibilityOff } from "@mui/icons-material"; | |||
| import { IconButton, InputAdornment } from "@mui/material"; | |||
| const ChagnePasswordForm: React.FC = () => { | |||
| const { t } = useTranslation(); | |||
| const [showNewPassword, setShowNewPassword] = useState(false); | |||
| const handleClickShowNewPassword = () => setShowNewPassword(!showNewPassword); | |||
| const handleMouseDownNewPassword = () => setShowNewPassword(!showNewPassword); | |||
| const [showPassword, setShowPassword] = useState(false); | |||
| const handleClickShowPassword = () => setShowPassword(!showPassword); | |||
| const handleMouseDownPassword = () => setShowPassword(!showPassword); | |||
| const { | |||
| register, | |||
| formState: { errors, defaultValues }, | |||
| control, | |||
| reset, | |||
| resetField, | |||
| setValue, | |||
| } = useFormContext<PasswordInputs>(); | |||
| // const resetGroup = useCallback(() => { | |||
| // console.log(defaultValues); | |||
| // if (defaultValues !== undefined) { | |||
| // resetField("description"); | |||
| // } | |||
| // }, [defaultValues]); | |||
| return ( | |||
| <Card sx={{ display: "block" }}> | |||
| <CardContent component={Stack} spacing={4}> | |||
| <Box> | |||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||
| {t("Group Info")} | |||
| </Typography> | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Input Old Password")} | |||
| fullWidth | |||
| type={showPassword ? "text" : "password"} | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| <InputAdornment position="end"> | |||
| <IconButton | |||
| aria-label="toggle password visibility" | |||
| onClick={handleClickShowPassword} | |||
| onMouseDown={handleMouseDownPassword} | |||
| > | |||
| {showPassword ? <Visibility /> : <VisibilityOff />} | |||
| </IconButton> | |||
| </InputAdornment> | |||
| ) | |||
| }} | |||
| {...register("password", { | |||
| required: true, | |||
| })} | |||
| error={Boolean(errors.password)} | |||
| helperText={ | |||
| Boolean(errors.password) && | |||
| (errors.password?.message | |||
| ? t(errors.password.message) | |||
| : t("Please input correct password")) | |||
| } | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6} /> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Input New Password")} | |||
| fullWidth | |||
| type={showNewPassword ? "text" : "password"} | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| <InputAdornment position="end"> | |||
| <IconButton | |||
| aria-label="toggle password visibility" | |||
| onClick={handleClickShowNewPassword} | |||
| onMouseDown={handleMouseDownNewPassword} | |||
| > | |||
| {showNewPassword ? <Visibility /> : <VisibilityOff />} | |||
| </IconButton> | |||
| </InputAdornment> | |||
| ) | |||
| }} | |||
| {...register("newPassword")} | |||
| error={Boolean(errors.newPassword)} | |||
| helperText={ | |||
| Boolean(errors.newPassword) && | |||
| (errors.newPassword?.message | |||
| ? t(errors.newPassword.message) | |||
| : t("Please input correct newPassword")) | |||
| } | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Input New Password Again")} | |||
| fullWidth | |||
| type={showNewPassword ? "text" : "password"} | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| <InputAdornment position="end"> | |||
| <IconButton | |||
| aria-label="toggle password visibility" | |||
| onClick={handleClickShowNewPassword} | |||
| onMouseDown={handleMouseDownNewPassword} | |||
| > | |||
| {showNewPassword ? <Visibility /> : <VisibilityOff />} | |||
| </IconButton> | |||
| </InputAdornment> | |||
| ) | |||
| }} | |||
| {...register("newPasswordCheck")} | |||
| error={Boolean(errors.newPassword)} | |||
| helperText={ | |||
| Boolean(errors.newPassword) && | |||
| (errors.newPassword?.message | |||
| ? t(errors.newPassword.message) | |||
| : t("Please input correct newPassword")) | |||
| } | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| }; | |||
| export default ChagnePasswordForm; | |||
| @@ -0,0 +1,40 @@ | |||
| import Card from "@mui/material/Card"; | |||
| import CardContent from "@mui/material/CardContent"; | |||
| import Skeleton from "@mui/material/Skeleton"; | |||
| import Stack from "@mui/material/Stack"; | |||
| import React from "react"; | |||
| // Can make this nicer | |||
| export const ChangePasswordLoading: React.FC = () => { | |||
| return ( | |||
| <> | |||
| <Card> | |||
| <CardContent> | |||
| <Stack spacing={2}> | |||
| <Skeleton variant="rounded" height={60} /> | |||
| <Skeleton variant="rounded" height={60} /> | |||
| <Skeleton variant="rounded" height={60} /> | |||
| <Skeleton | |||
| variant="rounded" | |||
| height={50} | |||
| width={100} | |||
| sx={{ alignSelf: "flex-end" }} | |||
| /> | |||
| </Stack> | |||
| </CardContent> | |||
| </Card> | |||
| <Card>Change Password | |||
| <CardContent> | |||
| <Stack spacing={2}> | |||
| <Skeleton variant="rounded" height={40} /> | |||
| <Skeleton variant="rounded" height={40} /> | |||
| <Skeleton variant="rounded" height={40} /> | |||
| <Skeleton variant="rounded" height={40} /> | |||
| </Stack> | |||
| </CardContent> | |||
| </Card> | |||
| </> | |||
| ); | |||
| }; | |||
| export default ChangePasswordLoading; | |||
| @@ -0,0 +1,20 @@ | |||
| import React from "react"; | |||
| import ChangePasswordLoading from "./ChangePasswordLoading"; | |||
| import ChangePassword from "./ChangePassword"; | |||
| interface SubComponents { | |||
| Loading: typeof ChangePasswordLoading; | |||
| } | |||
| const ChangePasswordWrapper: React.FC & SubComponents = async () => { | |||
| // const records = await fetchAuth() | |||
| // const users = await fetchUser() | |||
| // console.log(users) | |||
| // const auth = records.records as auth[] | |||
| return <ChangePassword />; | |||
| }; | |||
| ChangePasswordWrapper.Loading = ChangePasswordLoading; | |||
| export default ChangePasswordWrapper; | |||
| @@ -0,0 +1 @@ | |||
| export { default } from "./ChangePasswordWrapper"; | |||