Compare commits

...

2 Commits

Author SHA1 Message Date
louiscklaw
39a7d32fcd update, 2025-05-11 15:47:49 +08:00
louiscklaw
85d1ecdc90 update, 2025-05-11 15:44:18 +08:00
4 changed files with 359 additions and 140 deletions

View File

@@ -1,138 +0,0 @@
'use client';
import * as React from 'react';
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 { Option } from '@/components/core/option';
export function AccountDetails(): React.JSX.Element {
return (
<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">
Select
</Typography>
</Stack>
</Box>
<Avatar src="/assets/avatar.png" sx={{ '--Avatar-size': '100px' }} />
</Box>
</Box>
<Button color="secondary" size="small">
Remove
</Button>
</Stack>
<Stack spacing={2}>
<FormControl>
<InputLabel>Full name</InputLabel>
<OutlinedInput defaultValue="Sofia Rivers" name="fullName" />
</FormControl>
<FormControl disabled>
<InputLabel>Email address</InputLabel>
<OutlinedInput name="email" type="email" value="sofia@devias.io" />
<FormHelperText>
Please <Link variant="inherit">contact us</Link> to change your email
</FormHelperText>
</FormControl>
<Stack direction="row" spacing={2}>
<FormControl sx={{ width: '160px' }}>
<InputLabel>Dial code</InputLabel>
<Select
name="countryCode"
startAdornment={
<InputAdornment position="start">
<Box
alt="Spain"
component="img"
src="/assets/flag-es.svg"
sx={{ display: 'block', height: '20px', width: 'auto' }}
/>
</InputAdornment>
}
value="+34"
>
<Option value="+1">United States</Option>
<Option value="+49">Germany</Option>
<Option value="+34">Spain</Option>
</Select>
</FormControl>
<FormControl sx={{ flex: '1 1 auto' }}>
<InputLabel>Phone number</InputLabel>
<OutlinedInput defaultValue="965 245 7623" name="phone" />
</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">Cancel</Button>
<Button variant="contained">Save changes</Button>
</CardActions>
</Card>
);
}

View File

@@ -0,0 +1,3 @@
# GUIDELINES
- use i18n

View File

@@ -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>
);
}

View File

@@ -20,11 +20,17 @@ export function DeleteAccount(): React.JSX.Element {
title="Delete account"
/>
<CardContent>
<Stack spacing={3} sx={{ alignItems: 'flex-start' }}>
<Stack
spacing={3}
sx={{ alignItems: 'flex-start' }}
>
<Typography variant="subtitle1">
Delete your account and all of your source data. This is irreversible.
</Typography>
<Button color="error" variant="outlined">
<Button
color="error"
variant="outlined"
>
Delete account
</Button>
</Stack>