146 lines
4.4 KiB
TypeScript
146 lines
4.4 KiB
TypeScript
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { updateProfile } from '@/service/profile.api';
|
|
import { useAuth } from '@components/auth/auth-provider';
|
|
import AvatarUser, { AvatarUserType } from '@components/avatar/avatar-user';
|
|
import RoleBadge from '@components/avatar/role-badge';
|
|
import { useAppForm } from '@hooks/use-app-form';
|
|
import { m } from '@paraglide/messages';
|
|
import { UserCircleIcon } from '@phosphor-icons/react';
|
|
import { ProfileInput, profileUpdateSchema } from '@service/profile.schema';
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
|
|
import { Field, FieldGroup, FieldLabel } from '@ui/field';
|
|
import { Input } from '@ui/input';
|
|
import { useRef } from 'react';
|
|
import { toast } from 'sonner';
|
|
|
|
const defaultValues: ProfileInput = {
|
|
name: '',
|
|
image: undefined,
|
|
};
|
|
|
|
const ProfileForm = () => {
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
const { session, isPending } = useAuth();
|
|
const queryClient = useQueryClient();
|
|
|
|
const { mutate: updateProfileMutation, isPending: isRunning } = useMutation({
|
|
mutationFn: updateProfile,
|
|
onSuccess: () => {
|
|
form.reset();
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.value = '';
|
|
}
|
|
queryClient.refetchQueries({
|
|
queryKey: ['auth', 'session'],
|
|
});
|
|
toast.success(m.profile_messages_update_success(), {
|
|
richColors: true,
|
|
});
|
|
},
|
|
onError: (error: ReturnError) => {
|
|
console.error(error);
|
|
const code = error.code as Parameters<
|
|
typeof m.backend_message
|
|
>[0]['code'];
|
|
toast.error(m.backend_message({ code }), {
|
|
richColors: true,
|
|
});
|
|
},
|
|
});
|
|
|
|
const form = useAppForm({
|
|
defaultValues: {
|
|
...defaultValues,
|
|
name: session?.user?.name || '',
|
|
},
|
|
validators: {
|
|
onSubmit: profileUpdateSchema,
|
|
onChange: profileUpdateSchema,
|
|
},
|
|
onSubmit: async ({ value }) => {
|
|
const formData = new FormData();
|
|
formData.set('name', value.name);
|
|
if (value.image) {
|
|
formData.set('file', value.image);
|
|
}
|
|
|
|
updateProfileMutation({ data: formData });
|
|
},
|
|
});
|
|
|
|
if (isPending || !session?.user?.name) {
|
|
return <Skeleton className="h-100 col-span-1" />;
|
|
}
|
|
|
|
return (
|
|
<Card className="@container/card col-span-1">
|
|
<CardHeader>
|
|
<CardTitle className="text-xl flex items-center gap-2">
|
|
<UserCircleIcon size={20} />
|
|
{m.profile_ui_title()}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form
|
|
id="profile-form"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
form.handleSubmit();
|
|
}}
|
|
>
|
|
<FieldGroup>
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<AvatarUser
|
|
className="h-20 w-20"
|
|
textSize="2xl"
|
|
user={session.user as AvatarUserType}
|
|
/>
|
|
<form.AppField name="image">
|
|
{(field) => (
|
|
<field.FileField
|
|
label="Avatar"
|
|
className="col-span-2"
|
|
accept=".jpg, .jpeg, .png, .webp"
|
|
onChange={(e) => field.handleChange(e.target.files?.[0])}
|
|
/>
|
|
)}
|
|
</form.AppField>
|
|
</div>
|
|
<form.AppField name="name">
|
|
{(field) => <field.TextField label={m.profile_form_name()} />}
|
|
</form.AppField>
|
|
<Field>
|
|
<FieldLabel htmlFor="name">{m.profile_form_email()}</FieldLabel>
|
|
<Input
|
|
id="email"
|
|
name="email"
|
|
value={session?.user?.email || ''}
|
|
disabled
|
|
className="disabled:opacity-80"
|
|
/>
|
|
</Field>
|
|
<Field>
|
|
<FieldLabel htmlFor="name">{m.profile_form_role()}</FieldLabel>
|
|
<div className="flex gap-2">
|
|
<RoleBadge type={session?.user?.role} />
|
|
</div>
|
|
</Field>
|
|
<Field>
|
|
<form.AppForm>
|
|
<form.SubscribeButton
|
|
label={m.ui_update_btn()}
|
|
disabled={isRunning}
|
|
/>
|
|
</form.AppForm>
|
|
</Field>
|
|
</FieldGroup>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default ProfileForm;
|