diff --git a/002_source/cms/src/components/dashboard/settings/account-details/_GUIDELINES.md b/002_source/cms/src/components/dashboard/settings/account-details/_GUIDELINES.md new file mode 100644 index 0000000..ace1e5c --- /dev/null +++ b/002_source/cms/src/components/dashboard/settings/account-details/_GUIDELINES.md @@ -0,0 +1,3 @@ +# GUIDELINES + +- use i18n diff --git a/002_source/cms/src/components/dashboard/settings/account-details/index.tsx b/002_source/cms/src/components/dashboard/settings/account-details/index.tsx new file mode 100644 index 0000000..1063ad6 --- /dev/null +++ b/002_source/cms/src/components/dashboard/settings/account-details/index.tsx @@ -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; + +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(false); + const [showLoading, setShowLoading] = React.useState(false); + const [showError, setShowError] = React.useState({ show: false, detail: '' }); + + const onSubmit = React.useCallback( + async (values: Values): Promise => { + 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({ 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 ; + if (!user) return <>loading; + + if (showError.show) + return ( + + ); + + return ( +
+ + + + + } + title="Basic details" + /> + + + + + + + + + + {t('select')} + + + + + + + + + + ( + + Name + + {errors.name ? {errors.name.message} : null} + + )} + /> + + ( + + Email + + {errors.email ? {errors.email.message} : null} + + )} + /> + + + ( + + Phone + + {errors.phone ? {errors.phone.message} : null} + + )} + /> + + + Title + + + + Biography (optional) + + 0/200 characters + + + + + + + {/* */} + + {t('edit.updateButton')} + + + +
+ ); +}