update,
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
# GUIDELINES
|
||||||
|
|
||||||
|
- use i18n
|
@@ -0,0 +1,348 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import RouterLink from 'next/link';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { COL_USER_METAS } from '@/constants';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { LoadingButton } from '@mui/lab';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardActions from '@mui/material/CardActions';
|
||||||
|
import CardContent from '@mui/material/CardContent';
|
||||||
|
import CardHeader from '@mui/material/CardHeader';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||||
|
import Select from '@mui/material/Select';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
|
||||||
|
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { z as zod } from 'zod';
|
||||||
|
|
||||||
|
import { paths } from '@/paths';
|
||||||
|
import { logger } from '@/lib/default-logger';
|
||||||
|
import { fileToBase64 } from '@/lib/file-to-base64';
|
||||||
|
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
|
||||||
|
import { pb } from '@/lib/pb';
|
||||||
|
import { useUser } from '@/hooks/use-user';
|
||||||
|
import { Option } from '@/components/core/option';
|
||||||
|
import { toast } from '@/components/core/toaster';
|
||||||
|
import FormLoading from '@/components/loading';
|
||||||
|
|
||||||
|
import ErrorDisplay from '../../error';
|
||||||
|
|
||||||
|
const schema = zod.object({
|
||||||
|
name: zod.string().min(1, 'Name is required').max(255),
|
||||||
|
email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255),
|
||||||
|
phone: zod.string().min(1, 'Phone is required').max(25),
|
||||||
|
company: zod.string().max(255).optional(),
|
||||||
|
billingAddress: zod.object({
|
||||||
|
country: zod.string().min(1, 'Country is required').max(255),
|
||||||
|
state: zod.string().min(1, 'State is required').max(255),
|
||||||
|
city: zod.string().min(1, 'City is required').max(255),
|
||||||
|
zipCode: zod.string().min(1, 'Zip code is required').max(255),
|
||||||
|
line1: zod.string().min(1, 'Street line 1 is required').max(255),
|
||||||
|
line2: zod.string().max(255).optional(),
|
||||||
|
}),
|
||||||
|
taxId: zod.string().max(255).optional(),
|
||||||
|
timezone: zod.string().min(1, 'Timezone is required').max(255),
|
||||||
|
language: zod.string().min(1, 'Language is required').max(255),
|
||||||
|
currency: zod.string().min(1, 'Currency is required').max(255),
|
||||||
|
avatar: zod.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Values = zod.infer<typeof schema>;
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
company: '',
|
||||||
|
billingAddress: {
|
||||||
|
country: '',
|
||||||
|
state: '',
|
||||||
|
city: '',
|
||||||
|
zipCode: '',
|
||||||
|
line1: '',
|
||||||
|
line2: '',
|
||||||
|
},
|
||||||
|
taxId: '',
|
||||||
|
timezone: '',
|
||||||
|
language: '',
|
||||||
|
currency: '',
|
||||||
|
avatar: '',
|
||||||
|
} satisfies Values;
|
||||||
|
|
||||||
|
export function AccountDetails(): React.JSX.Element {
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { user, isLoading } = useUser();
|
||||||
|
const [isUpdating, setIsUpdating] = React.useState<boolean>(false);
|
||||||
|
const [showLoading, setShowLoading] = React.useState<boolean>(false);
|
||||||
|
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||||
|
|
||||||
|
const onSubmit = React.useCallback(
|
||||||
|
async (values: Values): Promise<void> => {
|
||||||
|
setIsUpdating(true);
|
||||||
|
|
||||||
|
// const updateData = {
|
||||||
|
// name: values.name,
|
||||||
|
// email: values.email,
|
||||||
|
// phone: values.phone,
|
||||||
|
// company: values.company,
|
||||||
|
// billingAddress: values.billingAddress,
|
||||||
|
// taxId: values.taxId,
|
||||||
|
// timezone: values.timezone,
|
||||||
|
// language: values.language,
|
||||||
|
// currency: values.currency,
|
||||||
|
// avatar: values.avatar ? await base64ToFile(values.avatar) : null,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// await pb.collection(COL_CUSTOMERS).update(customerId, updateData);
|
||||||
|
// toast.success('Customer updated successfully');
|
||||||
|
// router.push(paths.dashboard.students.list);
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.error(error);
|
||||||
|
// toast.error('Failed to update customer');
|
||||||
|
// } finally {
|
||||||
|
// setIsUpdating(false);
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
setValue,
|
||||||
|
reset,
|
||||||
|
watch,
|
||||||
|
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
|
||||||
|
|
||||||
|
const loadExistingData = React.useCallback(async () => {
|
||||||
|
setShowLoading(true);
|
||||||
|
if (user) {
|
||||||
|
try {
|
||||||
|
const result = await pb.collection(COL_USER_METAS).getOne(user.id);
|
||||||
|
reset({ ...defaultValues, ...result });
|
||||||
|
console.log({ result });
|
||||||
|
|
||||||
|
if (result.avatar) {
|
||||||
|
// TODO: remove me
|
||||||
|
// const fetchResult = await fetch(
|
||||||
|
// `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.avatar}`
|
||||||
|
// );
|
||||||
|
const fetchResult = await fetch(getImageUrlFromFile(result.collectionId, result.id, result.avatar));
|
||||||
|
const blob = await fetchResult.blob();
|
||||||
|
const url = await fileToBase64(blob);
|
||||||
|
setValue('avatar', url);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
|
||||||
|
// TODO: add i18n here
|
||||||
|
toast.error('Failed to load customer data');
|
||||||
|
|
||||||
|
setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
|
||||||
|
} finally {
|
||||||
|
setShowLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [user, reset, setValue]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
void loadExistingData();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (showLoading) return <FormLoading />;
|
||||||
|
if (!user) return <>loading</>;
|
||||||
|
|
||||||
|
if (showError.show)
|
||||||
|
return (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t('error.unable-to-process-request')}
|
||||||
|
code="500"
|
||||||
|
details={showError.detail}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader
|
||||||
|
avatar={
|
||||||
|
<Avatar>
|
||||||
|
<UserIcon fontSize="var(--Icon-fontSize)" />
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
title="Basic details"
|
||||||
|
/>
|
||||||
|
<CardContent>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
sx={{ alignItems: 'center' }}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
border: '1px dashed var(--mui-palette-divider)',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'inline-flex',
|
||||||
|
p: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ borderRadius: 'inherit', position: 'relative' }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
alignItems: 'center',
|
||||||
|
bgcolor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
borderRadius: 'inherit',
|
||||||
|
bottom: 0,
|
||||||
|
color: 'var(--mui-palette-common-white)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
left: 0,
|
||||||
|
opacity: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
zIndex: 1,
|
||||||
|
'&:hover': { opacity: 1 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={1}
|
||||||
|
sx={{ alignItems: 'center' }}
|
||||||
|
>
|
||||||
|
<CameraIcon fontSize="var(--icon-fontSize-md)" />
|
||||||
|
<Typography
|
||||||
|
color="inherit"
|
||||||
|
variant="subtitle2"
|
||||||
|
>
|
||||||
|
{t('select')}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
<Avatar
|
||||||
|
src={getImageUrlFromFile(user.collectionId, user.id, user.avatar)}
|
||||||
|
sx={{ '--Avatar-size': '100px' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{t('remove')}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl
|
||||||
|
error={Boolean(errors.name)}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<InputLabel required>Name</InputLabel>
|
||||||
|
<OutlinedInput {...field} />
|
||||||
|
{errors.name ? <FormHelperText>{errors.name.message}</FormHelperText> : null}
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
disabled
|
||||||
|
control={control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl
|
||||||
|
error={Boolean(errors.email)}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<InputLabel required>Email</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
{...field}
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
|
{errors.email ? <FormHelperText>{errors.email.message}</FormHelperText> : null}
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="phone"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl
|
||||||
|
error={Boolean(errors.phone)}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<InputLabel required>Phone</InputLabel>
|
||||||
|
<OutlinedInput {...field} />
|
||||||
|
{errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null}
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<FormControl>
|
||||||
|
<InputLabel>Title</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
name="title"
|
||||||
|
placeholder="e.g Golang Developer"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<InputLabel>Biography (optional)</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
name="bio"
|
||||||
|
placeholder="Describe yourself..."
|
||||||
|
/>
|
||||||
|
<FormHelperText>0/200 characters</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
<CardActions sx={{ justifyContent: 'flex-end' }}>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
component={RouterLink}
|
||||||
|
href={paths.dashboard.overview}
|
||||||
|
>
|
||||||
|
{t('edit.cancelButton')}
|
||||||
|
</Button>
|
||||||
|
{/* <Button variant="contained">Save changes</Button> */}
|
||||||
|
<LoadingButton
|
||||||
|
disabled={isUpdating}
|
||||||
|
loading={isUpdating}
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
{t('edit.updateButton')}
|
||||||
|
</LoadingButton>
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
Reference in New Issue
Block a user