Files
rentalcalculator/src/app/main/navigationPages/profile/Profile.js

383 lines
12 KiB
JavaScript

import FusePageSimple from '@fuse/core/FusePageSimple';
import _ from '@lodash';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon/FuseSvgIcon';
import { yupResolver } from '@hookform/resolvers/yup';
import Button from '@mui/material/Button';
import Paper from '@mui/material/Paper';
import TextField from '@mui/material/TextField';
import { styled } from '@mui/material/styles';
import { selectUser, updateUserSettings } from 'app/store/userSlice';
import { Controller, useForm } from 'react-hook-form';
import { withTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import * as yup from 'yup';
import { toBase64 } from 'src/app/utils';
import { forwardRef } from 'react';
const MAX_PICTURE_SIZE = 5000000;
const AVAILABLE_MEDIA_TYPES = ['image/png', 'image/jpeg'];
const Root = styled(FusePageSimple)(({ theme }) => ({
'& .FusePageSimple-header': {},
'& .FusePageSimple-toolbar': {},
'& .FusePageSimple-content': {
backgroundColor: theme.palette.background.default,
},
'& .FusePageSimple-sidebarHeader': {},
'& .FusePageSimple-sidebarContent': {},
}));
const StyledTextField = forwardRef((props, ref) => (
<TextField
InputProps={{
sx: {
background: (theme) => theme.palette.background.paper,
borderRadius: '10px',
},
}}
{...props}
ref={ref}
/>
));
function ProfilePage({ t }) {
const dispatch = useDispatch();
const user = useSelector(selectUser);
const defaultValues = {
photoURL: '',
firstName: '',
lastName: '',
displayName: '',
email: '',
mobileNumber: '',
information: '',
address: '',
...user.data,
};
const schema = yup.object().shape({
photoURL: yup.string().notRequired(),
firstName: yup
.string()
.max(150, t('max_length_error', { length: 150 }))
.trim()
.notRequired(),
lastName: yup
.string()
.max(150, t('max_length_error', { length: 150 }))
.trim()
.notRequired(),
displayName: yup
.string()
.required(t('display_name_error'))
.max(30, t('max_length_error', { length: 30 }))
.trim(),
email: yup
.string()
.matches(/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/g, t('email_error'))
.required(t('email_error'))
.max(40, t('max_length_error', { length: 40 })),
mobileNumber: yup
.string()
.matches(/^(?=(.*\d){7,})\+?(\d[\d-. ]+)?(\([\d-. ]+\))?[\d-. ]+\d$/, {
message: t('mobile_number_error'),
excludeEmptyString: true,
})
.max(20, t('max_length_error', { length: 20 }))
.notRequired(),
information: yup
.string()
.max(300, t('max_length_error', { length: 300 }))
.trim()
.notRequired(),
address: yup
.string()
.max(150, t('max_length_error', { length: 150 }))
.trim()
.notRequired(),
});
const { control, formState, watch, setValue, handleSubmit, setError, reset } = useForm({
mode: 'onChange',
defaultValues,
resolver: yupResolver(schema),
});
const { dirtyFields, errors } = formState;
const uploadPicture = async (event) => {
const { target } = event;
if (target.files && target.files[0]) {
const file = target.files[0];
if (file.size > MAX_PICTURE_SIZE) {
return setError('photoURL', { type: 'custom', message: t('picture_size_error') });
}
if (!AVAILABLE_MEDIA_TYPES.includes(file.type)) {
return setError('photoURL', {
type: 'custom',
message: t('picture_extensions_error'),
});
}
const base64 = await toBase64(file);
setValue('photoURL', base64, { shouldDirty: true });
} else {
setError('photoURL', { type: 'custom', message: 'Choose a file please' });
}
};
const deletePicture = () => setValue('photoURL', '', { shouldDirty: true });
const onSubmit = (data) => {
dispatch(updateUserSettings(data)).catch((error) => {
setError('root', {
type: 'manual',
message: error.message,
});
});
reset(data);
};
return (
<Root
content={
<div className="w-full p-60">
<form name="profileForm" noValidate onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-end mb-68">
<Paper className="relative w-[240px] h-[240px] mr-20 bg-common-disabled overflow-hidden">
{watch('photoURL') && (
<img
src={watch('photoURL')}
alt="user"
className="absolute w-full h-full object-cover"
/>
)}
</Paper>
<div className="grid grid-cols-[200px_minmax(0,_1fr)] gap-x-40 gap-y-28">
<Controller
name="photoURL"
control={control}
render={({ field: { name, ref, onBlur } }) => (
<label htmlFor="input-file">
<input
id="input-file"
name={name}
ref={ref}
type="file"
accept={AVAILABLE_MEDIA_TYPES.join(', ')}
style={{ display: 'none' }}
onBlur={onBlur}
onChange={uploadPicture}
/>
<Button
variant="outlined"
component="span"
color="secondary"
className="text-lg rounded-xl border-2 border-secondary-main shadow hover:shadow-hover hover:shadow-secondary-main hover:border-2 ease-in-out duration-300"
aria-label={t('upload_picture_btn')}
size="small"
fullWidth
>
{t('upload_picture_btn')}
</Button>
</label>
)}
/>
<Button
variant="text"
color="secondary"
className="justify-self-start min-w-fit text-base"
aria-label={t('delete_picture')}
disabled={!watch('photoURL')}
size="small"
onClick={deletePicture}
>
<FuseSvgIcon className="mr-10">heroicons-outline:trash</FuseSvgIcon>
{t('delete_picture')}
</Button>
<ul className="col-span-2 text-lg">
<li className="bullet">{t('first_picture_req')}</li>
<li className="bullet">{t('second_picture_req')}</li>
</ul>
</div>
</div>
<div className="grid grid-cols-6 gap-x-24 gap-y-32 w-full mb-48">
<Controller
name="firstName"
control={control}
render={({ field }) => (
<StyledTextField
{...field}
className="col-span-2"
label={t('first_name')}
type="text"
error={!!errors.firstName}
helperText={errors?.firstName?.message}
variant="outlined"
fullWidth
/>
)}
/>
<Controller
name="lastName"
control={control}
render={({ field }) => (
<StyledTextField
{...field}
className="col-span-2"
label={t('last_name')}
type="text"
error={!!errors.lastName}
helperText={errors?.lastName?.message}
variant="outlined"
fullWidth
/>
)}
/>
<Controller
name="displayName"
control={control}
render={({ field }) => (
<StyledTextField
{...field}
className="col-span-2"
label={t('display_name')}
type="text"
error={!!errors.displayName}
helperText={errors?.displayName?.message}
variant="outlined"
required
fullWidth
/>
)}
/>
<Controller
name="email"
control={control}
render={({ field }) => (
<StyledTextField
{...field}
className="col-span-3"
label={t('email')}
type="email"
error={!!errors.email}
helperText={errors?.email?.message}
variant="outlined"
required
fullWidth
/>
)}
/>
<Controller
name="mobileNumber"
control={control}
render={({ field }) => (
<StyledTextField
{...field}
className="col-span-3"
label={t('mobile_number')}
type="tel"
error={!!errors.mobileNumber}
helperText={errors?.mobileNumber?.message}
variant="outlined"
fullWidth
/>
)}
/>
<Controller
name="information"
control={control}
render={({ field }) => (
<TextField
{...field}
className="col-span-3"
label={t('information')}
type="text"
error={!!errors.information}
helperText={errors?.information?.message}
variant="outlined"
multiline
rows={7}
fullWidth
InputProps={{
sx: {
padding: 0,
background: (theme) => theme.palette.background.paper,
borderRadius: '10px',
},
}}
// eslint-disable-next-line react/jsx-no-duplicate-props
inputProps={{
sx: {
padding: '16.5px 14px',
},
}}
/>
)}
/>
<Controller
name="address"
control={control}
render={({ field }) => (
<TextField
{...field}
className="col-span-3"
label={t('address')}
type="text"
error={!!errors.address}
helperText={errors?.address?.message}
variant="outlined"
multiline
rows={7}
fullWidth
InputProps={{
sx: {
padding: 0,
background: (theme) => theme.palette.background.paper,
borderRadius: '10px',
},
}}
// eslint-disable-next-line react/jsx-no-duplicate-props
inputProps={{
sx: {
padding: '16.5px 14px',
},
}}
/>
)}
/>
</div>
<div className="flex flex-col items-center justify-center gap-10 w-full">
<Button
variant="contained"
color="secondary"
className="max-w-320 text-base uppercase rounded-xl"
aria-label={t('save_changes')}
disabled={_.isEmpty(dirtyFields) || !_.isEmpty(errors)}
type="submit"
size="large"
>
{t('save_changes')}
</Button>
{errors.root?.message && (
<p className="text-l text-error-main">{errors.root?.message}</p>
)}
</div>
</form>
</div>
}
scroll="content"
/>
);
}
export default withTranslation('profilePage')(ProfilePage);