change basic form to form context #9
@@ -93,6 +93,7 @@
|
|||||||
"settings_form_description": "Description",
|
"settings_form_description": "Description",
|
||||||
"settings_form_keywords": "keywords",
|
"settings_form_keywords": "keywords",
|
||||||
"settings_form_language": "Language",
|
"settings_form_language": "Language",
|
||||||
|
"settings_form_select_language": "Please select language",
|
||||||
"settings_ui_title": "Settings",
|
"settings_ui_title": "Settings",
|
||||||
"settings_messages_update_success": "Updated settings successfully!",
|
"settings_messages_update_success": "Updated settings successfully!",
|
||||||
"settings_messages_update_fail": "Update fail!",
|
"settings_messages_update_fail": "Update fail!",
|
||||||
|
|||||||
@@ -93,6 +93,7 @@
|
|||||||
"settings_form_description": "Mô tả website",
|
"settings_form_description": "Mô tả website",
|
||||||
"settings_form_keywords": "Từ khóa",
|
"settings_form_keywords": "Từ khóa",
|
||||||
"settings_form_language": "Ngôn ngữ",
|
"settings_form_language": "Ngôn ngữ",
|
||||||
|
"settings_form_select_language": "Hãy chọn ngôn ngữ",
|
||||||
"settings_ui_title": "Cài đặt",
|
"settings_ui_title": "Cài đặt",
|
||||||
"settings_messages_update_success": "Cập nhật cài đặt thành công!",
|
"settings_messages_update_success": "Cập nhật cài đặt thành công!",
|
||||||
"settings_messages_update_fail": "Cập nhật cài đặt thất bại!",
|
"settings_messages_update_fail": "Cập nhật cài đặt thất bại!",
|
||||||
|
|||||||
@@ -1,21 +1,12 @@
|
|||||||
|
import { useAppForm } from '@/hooks/use-app-form';
|
||||||
import { m } from '@/paraglide/messages';
|
import { m } from '@/paraglide/messages';
|
||||||
import { userBanSchema } from '@/service/user.schema';
|
import { userBanSchema } from '@/service/user.schema';
|
||||||
import { WarningIcon } from '@phosphor-icons/react';
|
import { WarningIcon } from '@phosphor-icons/react';
|
||||||
import { useForm } from '@tanstack/react-form';
|
|
||||||
import { UserWithRole } from 'better-auth/plugins';
|
import { UserWithRole } from 'better-auth/plugins';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { DialogClose, DialogFooter } from '../ui/dialog';
|
import { DialogClose, DialogFooter } from '../ui/dialog';
|
||||||
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
|
import { Field, FieldGroup } from '../ui/field';
|
||||||
import { Input } from '../ui/input';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '../ui/select';
|
|
||||||
import { Textarea } from '../ui/textarea';
|
|
||||||
import { useBanContext } from '../user/ban-user-dialog';
|
import { useBanContext } from '../user/ban-user-dialog';
|
||||||
|
|
||||||
type FormProps = {
|
type FormProps = {
|
||||||
@@ -25,7 +16,7 @@ type FormProps = {
|
|||||||
const BanUserForm = ({ data }: FormProps) => {
|
const BanUserForm = ({ data }: FormProps) => {
|
||||||
const { setSubmitData, setOpen, setOpenConfirm } = useBanContext();
|
const { setSubmitData, setOpen, setOpenConfirm } = useBanContext();
|
||||||
|
|
||||||
const form = useForm({
|
const form = useAppForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
banReason: '',
|
banReason: '',
|
||||||
@@ -59,99 +50,33 @@ const BanUserForm = ({ data }: FormProps) => {
|
|||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
{m.profile_form_name()}: {data.name}
|
{m.profile_form_name()}: {data.name}
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
<AlertDescription className="sr-only">adá</AlertDescription>
|
<AlertDescription className="sr-only">{data.name}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
<form.Field
|
<form.AppField name="id">
|
||||||
name="id"
|
{(field) => <field.HiddenField />}
|
||||||
children={(field) => {
|
</form.AppField>
|
||||||
const isInvalid =
|
<form.AppField name="banReason">
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
{(field) => (
|
||||||
return (
|
<field.TextArea label={m.users_page_ui_form_ban_reason()} />
|
||||||
<Field data-invalid={isInvalid}>
|
)}
|
||||||
<Input
|
</form.AppField>
|
||||||
type="hidden"
|
<form.AppField name="banExp">
|
||||||
name={field.name}
|
{(field) => (
|
||||||
id={field.name}
|
<field.SelectNumber
|
||||||
value={field.state.value}
|
label={m.users_page_ui_form_ban_exp()}
|
||||||
aria-invalid={isInvalid}
|
values={[
|
||||||
/>
|
{ label: m.exp_time({ time: '1' }), value: '1' },
|
||||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
{ label: m.exp_time({ time: '7' }), value: '7' },
|
||||||
</Field>
|
{ label: m.exp_time({ time: '15' }), value: '15' },
|
||||||
);
|
{ label: m.exp_time({ time: '30' }), value: '30' },
|
||||||
}}
|
{ label: m.exp_time({ time: '180' }), value: '180' },
|
||||||
/>
|
{ label: m.exp_time({ time: '365' }), value: '365' },
|
||||||
<form.Field
|
{ label: m.exp_time({ time: '99999' }), value: '99999' },
|
||||||
name="banReason"
|
]}
|
||||||
children={(field) => {
|
placeholder={m.users_page_ui_select_placeholder_ban_exp()}
|
||||||
const isInvalid =
|
/>
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
)}
|
||||||
return (
|
</form.AppField>
|
||||||
<Field data-invalid={isInvalid} className="col-span-2">
|
|
||||||
<FieldLabel htmlFor={field.name}>
|
|
||||||
{m.users_page_ui_form_ban_reason()}:
|
|
||||||
</FieldLabel>
|
|
||||||
<Textarea
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<form.Field
|
|
||||||
name="banExp"
|
|
||||||
children={(field) => {
|
|
||||||
const isInvalid =
|
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
|
||||||
return (
|
|
||||||
<Field data-invalid={isInvalid}>
|
|
||||||
<FieldLabel htmlFor={field.name}>
|
|
||||||
{m.users_page_ui_form_ban_exp()}
|
|
||||||
</FieldLabel>
|
|
||||||
<Select
|
|
||||||
name={field.name}
|
|
||||||
value={String(field.state.value)}
|
|
||||||
onValueChange={(value) => field.handleChange(Number(value))}
|
|
||||||
>
|
|
||||||
<SelectTrigger aria-invalid={isInvalid}>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={m.users_page_ui_select_placeholder_ban_exp()}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="1">
|
|
||||||
{m.exp_time({ time: '1' })}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="7">
|
|
||||||
{m.exp_time({ time: '7' })}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="15">
|
|
||||||
{m.exp_time({ time: '15' })}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="30">
|
|
||||||
{m.exp_time({ time: '30' })}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="180">
|
|
||||||
{m.exp_time({ time: '180' })}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="365">
|
|
||||||
{m.exp_time({ time: '365' })}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="99999">
|
|
||||||
{m.exp_time({ time: '99999' })}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Field>
|
<Field>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
@@ -159,9 +84,12 @@ const BanUserForm = ({ data }: FormProps) => {
|
|||||||
{m.ui_cancel_btn()}
|
{m.ui_cancel_btn()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button type="submit" variant="destructive">
|
<form.AppForm>
|
||||||
{m.ui_ban_btn()}
|
<form.SubscribeButton
|
||||||
</Button>
|
label={m.ui_ban_btn()}
|
||||||
|
variant="destructive"
|
||||||
|
/>
|
||||||
|
</form.AppForm>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|||||||
@@ -1,22 +1,14 @@
|
|||||||
|
import { useAppForm } from '@/hooks/use-app-form';
|
||||||
import { m } from '@/paraglide/messages';
|
import { m } from '@/paraglide/messages';
|
||||||
import { usersQueries } from '@/service/queries';
|
import { usersQueries } from '@/service/queries';
|
||||||
import { createUser } from '@/service/user.api';
|
import { createUser } from '@/service/user.api';
|
||||||
import { RoleEnum, userCreateSchema } from '@/service/user.schema';
|
import { userCreateSchema } from '@/service/user.schema';
|
||||||
import { ReturnError } from '@/types/common';
|
import { ReturnError } from '@/types/common';
|
||||||
import { useForm } from '@tanstack/react-form';
|
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { DialogClose, DialogFooter } from '../ui/dialog';
|
import { DialogClose, DialogFooter } from '../ui/dialog';
|
||||||
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
|
import { Field, FieldGroup } from '../ui/field';
|
||||||
import { Input } from '../ui/input';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '../ui/select';
|
|
||||||
|
|
||||||
type FormProps = {
|
type FormProps = {
|
||||||
onSubmit: (open: boolean) => void;
|
onSubmit: (open: boolean) => void;
|
||||||
@@ -44,7 +36,7 @@ const AdminCreateUserForm = ({ onSubmit }: FormProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useAppForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
@@ -70,114 +62,33 @@ const AdminCreateUserForm = ({ onSubmit }: FormProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<form.Field
|
<form.AppField name="email">
|
||||||
name="email"
|
{(field) => <field.TextField label={m.login_page_form_email()} />}
|
||||||
children={(field) => {
|
</form.AppField>
|
||||||
const isInvalid =
|
<form.AppField name="password">
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
{(field) => (
|
||||||
return (
|
<field.TextField
|
||||||
<Field data-invalid={isInvalid} className="col-span-2">
|
label={m.login_page_form_password()}
|
||||||
<FieldLabel htmlFor={field.name}>
|
type="password"
|
||||||
{m.login_page_form_email()}:
|
/>
|
||||||
</FieldLabel>
|
)}
|
||||||
<Input
|
</form.AppField>
|
||||||
id={field.name}
|
<form.AppField name="name">
|
||||||
name={field.name}
|
{(field) => <field.TextField label={m.profile_form_name()} />}
|
||||||
value={field.state.value}
|
</form.AppField>
|
||||||
onBlur={field.handleBlur}
|
<form.AppField name="role">
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
{(field) => (
|
||||||
aria-invalid={isInvalid}
|
<field.Select
|
||||||
type="email"
|
label={m.profile_form_role()}
|
||||||
/>
|
placeholder={m.users_page_ui_select_placeholder_role()}
|
||||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
isRole
|
||||||
</Field>
|
values={[
|
||||||
);
|
{ value: 'admin', label: m.role_tags({ role: 'admin' }) },
|
||||||
}}
|
{ value: 'user', label: m.role_tags({ role: 'user' }) },
|
||||||
/>
|
]}
|
||||||
<form.Field
|
/>
|
||||||
name="password"
|
)}
|
||||||
children={(field) => {
|
</form.AppField>
|
||||||
const isInvalid =
|
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
|
||||||
return (
|
|
||||||
<Field data-invalid={isInvalid}>
|
|
||||||
<FieldLabel htmlFor={field.name}>
|
|
||||||
{m.login_page_form_password()}
|
|
||||||
</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<form.Field
|
|
||||||
name="name"
|
|
||||||
children={(field) => {
|
|
||||||
const isInvalid =
|
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
|
||||||
return (
|
|
||||||
<Field data-invalid={isInvalid}>
|
|
||||||
<FieldLabel htmlFor={field.name}>
|
|
||||||
{m.profile_form_name()}
|
|
||||||
</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<form.Field
|
|
||||||
name="role"
|
|
||||||
children={(field) => {
|
|
||||||
const isInvalid =
|
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
|
||||||
return (
|
|
||||||
<Field data-invalid={isInvalid}>
|
|
||||||
<FieldLabel htmlFor={field.name}>
|
|
||||||
{m.profile_form_role()}
|
|
||||||
</FieldLabel>
|
|
||||||
<Select
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
field.handleChange(RoleEnum.parse(value))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger aria-invalid={isInvalid}>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={m.users_page_ui_select_placeholder_role()}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="admin">
|
|
||||||
{m.role_tags({ role: 'admin' })}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="user">
|
|
||||||
{m.role_tags({ role: 'user' })}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Field>
|
<Field>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
@@ -185,9 +96,9 @@ const AdminCreateUserForm = ({ onSubmit }: FormProps) => {
|
|||||||
{m.ui_cancel_btn()}
|
{m.ui_cancel_btn()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button type="submit" variant="destructive">
|
<form.AppForm>
|
||||||
{m.ui_signup_btn()}
|
<form.SubscribeButton label={m.ui_signup_btn()} />
|
||||||
</Button>
|
</form.AppForm>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
|
import { useAppForm } from '@/hooks/use-app-form';
|
||||||
import { m } from '@/paraglide/messages';
|
import { m } from '@/paraglide/messages';
|
||||||
import { usersQueries } from '@/service/queries';
|
import { usersQueries } from '@/service/queries';
|
||||||
import { setUserPassword } from '@/service/user.api';
|
import { setUserPassword } from '@/service/user.api';
|
||||||
import { userSetPasswordSchema } from '@/service/user.schema';
|
import { userSetPasswordSchema } from '@/service/user.schema';
|
||||||
import { ReturnError } from '@/types/common';
|
import { ReturnError } from '@/types/common';
|
||||||
import { useForm } from '@tanstack/react-form';
|
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { UserWithRole } from 'better-auth/plugins';
|
import { UserWithRole } from 'better-auth/plugins';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { DialogClose, DialogFooter } from '../ui/dialog';
|
import { DialogClose, DialogFooter } from '../ui/dialog';
|
||||||
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
|
import { Field, FieldGroup } from '../ui/field';
|
||||||
import { Input } from '../ui/input';
|
|
||||||
|
|
||||||
type FormProps = {
|
type FormProps = {
|
||||||
data: UserWithRole;
|
data: UserWithRole;
|
||||||
@@ -39,7 +38,7 @@ const AdminSetPasswordForm = ({ data, onSubmit }: FormProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useAppForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
password: '',
|
password: '',
|
||||||
@@ -62,49 +61,14 @@ const AdminSetPasswordForm = ({ data, onSubmit }: FormProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<form.Field
|
<form.AppField name="id">
|
||||||
name="id"
|
{(field) => <field.HiddenField />}
|
||||||
children={(field) => {
|
</form.AppField>
|
||||||
const isInvalid =
|
<form.AppField name="password">
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
{(field) => (
|
||||||
return (
|
<field.TextField label={m.change_password_form_new_password()} />
|
||||||
<Field data-invalid={isInvalid}>
|
)}
|
||||||
<Input
|
</form.AppField>
|
||||||
type="hidden"
|
|
||||||
name={field.name}
|
|
||||||
id={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
/>
|
|
||||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<form.Field
|
|
||||||
name="password"
|
|
||||||
children={(field) => {
|
|
||||||
const isInvalid =
|
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
|
||||||
return (
|
|
||||||
<Field data-invalid={isInvalid}>
|
|
||||||
<FieldLabel htmlFor={field.name}>
|
|
||||||
{m.change_password_form_new_password()}
|
|
||||||
</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Field>
|
<Field>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
@@ -112,7 +76,9 @@ const AdminSetPasswordForm = ({ data, onSubmit }: FormProps) => {
|
|||||||
{m.ui_cancel_btn()}
|
{m.ui_cancel_btn()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button type="submit">{m.ui_save_btn()}</Button>
|
<form.AppForm>
|
||||||
|
<form.SubscribeButton label={m.ui_save_btn()} />
|
||||||
|
</form.AppForm>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
|
import { useAppForm } from '@/hooks/use-app-form';
|
||||||
import { m } from '@/paraglide/messages';
|
import { m } from '@/paraglide/messages';
|
||||||
import { usersQueries } from '@/service/queries';
|
import { usersQueries } from '@/service/queries';
|
||||||
import { setUserRole } from '@/service/user.api';
|
import { setUserRole } from '@/service/user.api';
|
||||||
import { RoleEnum, userUpdateRoleSchema } from '@/service/user.schema';
|
import { userUpdateRoleSchema } from '@/service/user.schema';
|
||||||
import { ReturnError } from '@/types/common';
|
import { ReturnError } from '@/types/common';
|
||||||
import { useForm } from '@tanstack/react-form';
|
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { UserWithRole } from 'better-auth/plugins';
|
import { UserWithRole } from 'better-auth/plugins';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { DialogClose, DialogFooter } from '../ui/dialog';
|
import { DialogClose, DialogFooter } from '../ui/dialog';
|
||||||
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
|
import { Field, FieldGroup } from '../ui/field';
|
||||||
import { Input } from '../ui/input';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '../ui/select';
|
|
||||||
|
|
||||||
type SetRoleFormProps = {
|
type SetRoleFormProps = {
|
||||||
data: UserWithRole;
|
data: UserWithRole;
|
||||||
@@ -51,7 +43,7 @@ const AdminSetUserRoleForm = ({ data, onSubmit }: SetRoleFormProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useAppForm({
|
||||||
defaultValues: userUpdateRoleSchema.parse(defaultFormValues),
|
defaultValues: userUpdateRoleSchema.parse(defaultFormValues),
|
||||||
validators: {
|
validators: {
|
||||||
onChange: userUpdateRoleSchema,
|
onChange: userUpdateRoleSchema,
|
||||||
@@ -72,61 +64,21 @@ const AdminSetUserRoleForm = ({ data, onSubmit }: SetRoleFormProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<form.Field
|
<form.AppField name="id">
|
||||||
name="id"
|
{(field) => <field.HiddenField />}
|
||||||
children={(field) => {
|
</form.AppField>
|
||||||
const isInvalid =
|
<form.AppField name="role">
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
{(field) => (
|
||||||
return (
|
<field.Select
|
||||||
<Field data-invalid={isInvalid}>
|
label={m.profile_form_role()}
|
||||||
<Input
|
placeholder={m.users_page_ui_select_placeholder_role()}
|
||||||
type="hidden"
|
values={[
|
||||||
name={field.name}
|
{ value: 'admin', label: m.role_tags({ role: 'admin' }) },
|
||||||
id={field.name}
|
{ value: 'user', label: m.role_tags({ role: 'user' }) },
|
||||||
value={field.state.value}
|
]}
|
||||||
aria-invalid={isInvalid}
|
/>
|
||||||
/>
|
)}
|
||||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
</form.AppField>
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<form.Field
|
|
||||||
name="role"
|
|
||||||
children={(field) => {
|
|
||||||
const isInvalid =
|
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
|
||||||
return (
|
|
||||||
<Field data-invalid={isInvalid}>
|
|
||||||
<FieldLabel htmlFor={field.name}>
|
|
||||||
{m.profile_form_role()}
|
|
||||||
</FieldLabel>
|
|
||||||
<Select
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
field.handleChange(RoleEnum.parse(value))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger aria-invalid={isInvalid}>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={m.users_page_ui_select_placeholder_role()}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="admin">
|
|
||||||
{m.role_tags({ role: 'admin' })}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="user">
|
|
||||||
{m.role_tags({ role: 'user' })}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Field>
|
<Field>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
@@ -134,7 +86,9 @@ const AdminSetUserRoleForm = ({ data, onSubmit }: SetRoleFormProps) => {
|
|||||||
{m.ui_cancel_btn()}
|
{m.ui_cancel_btn()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button type="submit">{m.ui_save_btn()}</Button>
|
<form.AppForm>
|
||||||
|
<form.SubscribeButton label={m.ui_save_btn()} />
|
||||||
|
</form.AppForm>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
|
import { useAppForm } from '@/hooks/use-app-form';
|
||||||
import { m } from '@/paraglide/messages';
|
import { m } from '@/paraglide/messages';
|
||||||
import { usersQueries } from '@/service/queries';
|
import { usersQueries } from '@/service/queries';
|
||||||
import { updateUserInformation } from '@/service/user.api';
|
import { updateUserInformation } from '@/service/user.api';
|
||||||
import { userUpdateInfoSchema } from '@/service/user.schema';
|
import { userUpdateInfoSchema } from '@/service/user.schema';
|
||||||
import { ReturnError } from '@/types/common';
|
import { ReturnError } from '@/types/common';
|
||||||
import { useForm } from '@tanstack/react-form';
|
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { UserWithRole } from 'better-auth/plugins';
|
import { UserWithRole } from 'better-auth/plugins';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { DialogClose, DialogFooter } from '../ui/dialog';
|
import { DialogClose, DialogFooter } from '../ui/dialog';
|
||||||
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
|
import { Field, FieldGroup } from '../ui/field';
|
||||||
import { Input } from '../ui/input';
|
|
||||||
|
|
||||||
type UpdateUserFormProps = {
|
type UpdateUserFormProps = {
|
||||||
data: UserWithRole;
|
data: UserWithRole;
|
||||||
@@ -38,7 +37,7 @@ const AdminUpdateUserInfoForm = ({ data, onSubmit }: UpdateUserFormProps) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const form = useForm({
|
const form = useAppForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -61,49 +60,12 @@ const AdminUpdateUserInfoForm = ({ data, onSubmit }: UpdateUserFormProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<form.Field
|
<form.AppField name="id">
|
||||||
name="id"
|
{(field) => <field.HiddenField />}
|
||||||
children={(field) => {
|
</form.AppField>
|
||||||
const isInvalid =
|
<form.AppField name="name">
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
{(field) => <field.TextField label={m.profile_form_name()} />}
|
||||||
return (
|
</form.AppField>
|
||||||
<Field data-invalid={isInvalid}>
|
|
||||||
<Input
|
|
||||||
type="hidden"
|
|
||||||
name={field.name}
|
|
||||||
id={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
/>
|
|
||||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<form.Field
|
|
||||||
name="name"
|
|
||||||
children={(field) => {
|
|
||||||
const isInvalid =
|
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
|
||||||
return (
|
|
||||||
<Field data-invalid={isInvalid}>
|
|
||||||
<FieldLabel htmlFor={field.name}>
|
|
||||||
{m.profile_form_name()}
|
|
||||||
</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Field>
|
<Field>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
@@ -111,7 +73,9 @@ const AdminUpdateUserInfoForm = ({ data, onSubmit }: UpdateUserFormProps) => {
|
|||||||
{m.ui_cancel_btn()}
|
{m.ui_cancel_btn()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button type="submit">{m.ui_save_btn()}</Button>
|
<form.AppForm>
|
||||||
|
<form.SubscribeButton label={m.ui_save_btn()} />
|
||||||
|
</form.AppForm>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|||||||
@@ -1,43 +1,14 @@
|
|||||||
|
import { useAppForm } from '@/hooks/use-app-form';
|
||||||
import { authClient } from '@/lib/auth-client';
|
import { authClient } from '@/lib/auth-client';
|
||||||
import { m } from '@/paraglide/messages';
|
import { m } from '@/paraglide/messages';
|
||||||
|
import {
|
||||||
|
ChangePassword,
|
||||||
|
ChangePasswordFormSchema,
|
||||||
|
} from '@/service/user.schema';
|
||||||
import { KeyIcon } from '@phosphor-icons/react';
|
import { KeyIcon } from '@phosphor-icons/react';
|
||||||
import { useForm } from '@tanstack/react-form';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import z from 'zod';
|
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||||||
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
|
import { Field, FieldGroup } from '../ui/field';
|
||||||
import { Input } from '../ui/input';
|
|
||||||
|
|
||||||
const ChangePasswordFormSchema = z
|
|
||||||
.object({
|
|
||||||
currentPassword: z.string().nonempty(
|
|
||||||
m.common_is_required({
|
|
||||||
field: m.change_password_form_current_password(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
newPassword: z.string().nonempty(
|
|
||||||
m.common_is_required({
|
|
||||||
field: m.change_password_form_new_password(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
confirmPassword: z.string().nonempty(
|
|
||||||
m.common_is_required({
|
|
||||||
field: m.change_password_form_confirm_password(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.superRefine((data, ctx) => {
|
|
||||||
if (data.newPassword !== data.confirmPassword) {
|
|
||||||
ctx.addIssue({
|
|
||||||
path: ['confirmPassword'],
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: m.change_password_messages_password_not_match(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
type ChangePassword = z.infer<typeof ChangePasswordFormSchema>;
|
|
||||||
|
|
||||||
const defaultValues: ChangePassword = {
|
const defaultValues: ChangePassword = {
|
||||||
currentPassword: '',
|
currentPassword: '',
|
||||||
@@ -46,7 +17,7 @@ const defaultValues: ChangePassword = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ChangePasswordForm = () => {
|
const ChangePasswordForm = () => {
|
||||||
const form = useForm({
|
const form = useAppForm({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
validators: {
|
validators: {
|
||||||
onSubmit: ChangePasswordFormSchema,
|
onSubmit: ChangePasswordFormSchema,
|
||||||
@@ -98,86 +69,33 @@ const ChangePasswordForm = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<form.Field
|
<form.AppField name="currentPassword">
|
||||||
name="currentPassword"
|
{(field) => (
|
||||||
children={(field) => {
|
<field.TextField
|
||||||
const isInvalid =
|
label={m.change_password_form_current_password()}
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
/>
|
||||||
return (
|
)}
|
||||||
<Field data-invalid={isInvalid}>
|
</form.AppField>
|
||||||
<FieldLabel htmlFor={field.name}>
|
<form.AppField name="newPassword">
|
||||||
{m.change_password_form_current_password()}:
|
{(field) => (
|
||||||
</FieldLabel>
|
<field.TextField
|
||||||
<Input
|
label={m.change_password_form_new_password()}
|
||||||
id={field.name}
|
type="password"
|
||||||
name={field.name}
|
/>
|
||||||
value={field.state.value}
|
)}
|
||||||
onBlur={field.handleBlur}
|
</form.AppField>
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
<form.AppField name="confirmPassword">
|
||||||
aria-invalid={isInvalid}
|
{(field) => (
|
||||||
type="password"
|
<field.TextField
|
||||||
/>
|
label={m.change_password_form_confirm_password()}
|
||||||
{isInvalid && (
|
type="password"
|
||||||
<FieldError errors={field.state.meta.errors} />
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</form.AppField>
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<form.Field
|
|
||||||
name="newPassword"
|
|
||||||
children={(field) => {
|
|
||||||
const isInvalid =
|
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
|
||||||
return (
|
|
||||||
<Field data-invalid={isInvalid}>
|
|
||||||
<FieldLabel htmlFor={field.name}>
|
|
||||||
{m.change_password_form_new_password()}:
|
|
||||||
</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
{isInvalid && (
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<form.Field
|
|
||||||
name="confirmPassword"
|
|
||||||
children={(field) => {
|
|
||||||
const isInvalid =
|
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
|
||||||
return (
|
|
||||||
<Field data-invalid={isInvalid}>
|
|
||||||
<FieldLabel htmlFor={field.name}>
|
|
||||||
{m.change_password_form_confirm_password()}:
|
|
||||||
</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
{isInvalid && (
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Field>
|
<Field>
|
||||||
<Button type="submit">{m.ui_change_password_btn()}</Button>
|
<form.AppForm>
|
||||||
|
<form.SubscribeButton label={m.ui_change_password_btn()} />
|
||||||
|
</form.AppForm>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
218
src/components/form/form-components.tsx
Normal file
218
src/components/form/form-components.tsx
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import { useFieldContext, useFormContext } from '@/hooks/use-app-form';
|
||||||
|
import { RoleEnum } from '@/service/user.schema';
|
||||||
|
import { useStore } from '@tanstack/react-form';
|
||||||
|
import { type VariantProps } from 'class-variance-authority';
|
||||||
|
import { Button, buttonVariants } from '../ui/button';
|
||||||
|
import { Field, FieldError, FieldLabel } from '../ui/field';
|
||||||
|
import { Input } from '../ui/input';
|
||||||
|
import * as ShadcnSelect from '../ui/select';
|
||||||
|
import { Textarea } from '../ui/textarea';
|
||||||
|
|
||||||
|
export function SubscribeButton({
|
||||||
|
label,
|
||||||
|
variant = 'default',
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
} & VariantProps<typeof buttonVariants>) {
|
||||||
|
const form = useFormContext();
|
||||||
|
return (
|
||||||
|
<form.Subscribe selector={(state) => state.isSubmitting}>
|
||||||
|
{(isSubmitting) => (
|
||||||
|
<Button type="submit" disabled={isSubmitting} variant={variant}>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</form.Subscribe>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HiddenField() {
|
||||||
|
const field = useFieldContext<string>();
|
||||||
|
const errors = useStore(field.store, (state) => state.meta.errors);
|
||||||
|
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field data-invalid={isInvalid}>
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
aria-invalid={isInvalid}
|
||||||
|
type="hidden"
|
||||||
|
/>
|
||||||
|
{isInvalid && <FieldError errors={errors} />}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TextField({
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
type = 'text',
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
placeholder?: string;
|
||||||
|
type?: string;
|
||||||
|
}) {
|
||||||
|
const field = useFieldContext<string>();
|
||||||
|
const errors = useStore(field.store, (state) => state.meta.errors);
|
||||||
|
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field data-invalid={isInvalid}>
|
||||||
|
<FieldLabel htmlFor={field.name}>{label}:</FieldLabel>
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
aria-invalid={isInvalid}
|
||||||
|
type={type}
|
||||||
|
/>
|
||||||
|
{isInvalid && <FieldError errors={errors} />}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FileField({
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
className?: string;
|
||||||
|
} & React.ComponentProps<'input'>) {
|
||||||
|
const field = useFieldContext<string>();
|
||||||
|
const errors = useStore(field.store, (state) => state.meta.errors);
|
||||||
|
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field data-invalid={isInvalid} className={className}>
|
||||||
|
<FieldLabel htmlFor={field.name}>{label}:</FieldLabel>
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
aria-invalid={isInvalid}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{isInvalid && <FieldError errors={errors} />}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TextArea({
|
||||||
|
label,
|
||||||
|
rows = 4,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
rows?: number;
|
||||||
|
}) {
|
||||||
|
const field = useFieldContext<string>();
|
||||||
|
const errors = useStore(field.store, (state) => state.meta.errors);
|
||||||
|
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field data-invalid={isInvalid} className="col-span-2">
|
||||||
|
<FieldLabel htmlFor={field.name}>{label}:</FieldLabel>
|
||||||
|
<Textarea
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
aria-invalid={isInvalid}
|
||||||
|
rows={rows}
|
||||||
|
/>
|
||||||
|
{isInvalid && <FieldError errors={errors} />}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Select({
|
||||||
|
label,
|
||||||
|
values,
|
||||||
|
placeholder,
|
||||||
|
isRole = false,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
values: Array<{ label: string; value: string }>;
|
||||||
|
placeholder?: string;
|
||||||
|
isRole?: boolean;
|
||||||
|
}) {
|
||||||
|
const field = useFieldContext<string>();
|
||||||
|
const errors = useStore(field.store, (state) => state.meta.errors);
|
||||||
|
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field data-invalid={isInvalid} className="col-span-2">
|
||||||
|
<FieldLabel htmlFor={field.name}>{label}:</FieldLabel>
|
||||||
|
<ShadcnSelect.Select
|
||||||
|
name={field.name}
|
||||||
|
value={String(field.state.value)}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
isRole
|
||||||
|
? field.handleChange(RoleEnum.parse(value))
|
||||||
|
: field.handleChange(value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ShadcnSelect.SelectTrigger aria-invalid={isInvalid}>
|
||||||
|
<ShadcnSelect.SelectValue placeholder={placeholder} />
|
||||||
|
</ShadcnSelect.SelectTrigger>
|
||||||
|
<ShadcnSelect.SelectContent>
|
||||||
|
<ShadcnSelect.SelectGroup>
|
||||||
|
<ShadcnSelect.SelectLabel>{label}</ShadcnSelect.SelectLabel>
|
||||||
|
{values.map((value) => (
|
||||||
|
<ShadcnSelect.SelectItem key={value.value} value={value.value}>
|
||||||
|
{value.label}
|
||||||
|
</ShadcnSelect.SelectItem>
|
||||||
|
))}
|
||||||
|
</ShadcnSelect.SelectGroup>
|
||||||
|
</ShadcnSelect.SelectContent>
|
||||||
|
</ShadcnSelect.Select>
|
||||||
|
{isInvalid && <FieldError errors={errors} />}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectNumber({
|
||||||
|
label,
|
||||||
|
values,
|
||||||
|
placeholder,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
values: Array<{ label: string; value: string }>;
|
||||||
|
placeholder?: string;
|
||||||
|
}) {
|
||||||
|
const field = useFieldContext<number>();
|
||||||
|
const errors = useStore(field.store, (state) => state.meta.errors);
|
||||||
|
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field data-invalid={isInvalid} className="col-span-2">
|
||||||
|
<FieldLabel htmlFor={field.name}>{label}:</FieldLabel>
|
||||||
|
<ShadcnSelect.Select
|
||||||
|
name={field.name}
|
||||||
|
value={field.state.value === 0 ? String('') : String(field.state.value)}
|
||||||
|
onValueChange={(value) => field.handleChange(Number(value))}
|
||||||
|
>
|
||||||
|
<ShadcnSelect.SelectTrigger aria-invalid={isInvalid}>
|
||||||
|
<ShadcnSelect.SelectValue placeholder={placeholder} />
|
||||||
|
</ShadcnSelect.SelectTrigger>
|
||||||
|
<ShadcnSelect.SelectContent>
|
||||||
|
<ShadcnSelect.SelectGroup>
|
||||||
|
<ShadcnSelect.SelectLabel>{label}</ShadcnSelect.SelectLabel>
|
||||||
|
{values.map((value) => (
|
||||||
|
<ShadcnSelect.SelectItem key={value.value} value={value.value}>
|
||||||
|
{value.label}
|
||||||
|
</ShadcnSelect.SelectItem>
|
||||||
|
))}
|
||||||
|
</ShadcnSelect.SelectGroup>
|
||||||
|
</ShadcnSelect.SelectContent>
|
||||||
|
</ShadcnSelect.Select>
|
||||||
|
{isInvalid && <FieldError errors={errors} />}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
|
import { useAppForm } from '@/hooks/use-app-form';
|
||||||
import { authClient } from '@/lib/auth-client';
|
import { authClient } from '@/lib/auth-client';
|
||||||
import { m } from '@/paraglide/messages';
|
import { m } from '@/paraglide/messages';
|
||||||
import { uploadProfileImage } from '@/service/profile.api';
|
import { uploadProfileImage } from '@/service/profile.api';
|
||||||
import { ProfileInput, profileUpdateSchema } from '@/service/profile.schema';
|
import { ProfileInput, profileUpdateSchema } from '@/service/profile.schema';
|
||||||
import { UserCircleIcon } from '@phosphor-icons/react';
|
import { UserCircleIcon } from '@phosphor-icons/react';
|
||||||
import { useForm } from '@tanstack/react-form';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useAuth } from '../auth/auth-provider';
|
import { useAuth } from '../auth/auth-provider';
|
||||||
import AvatarUser from '../avatar/avatar-user';
|
import AvatarUser from '../avatar/avatar-user';
|
||||||
import RoleBadge from '../avatar/role-badge';
|
import RoleBadge from '../avatar/role-badge';
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||||||
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
|
import { Field, FieldGroup, FieldLabel } from '../ui/field';
|
||||||
import { Input } from '../ui/input';
|
import { Input } from '../ui/input';
|
||||||
|
|
||||||
const defaultValues: ProfileInput = {
|
const defaultValues: ProfileInput = {
|
||||||
@@ -25,7 +24,7 @@ const ProfileForm = () => {
|
|||||||
const { data: session, isPending } = useAuth();
|
const { data: session, isPending } = useAuth();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const form = useForm({
|
const form = useAppForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
name: session?.user?.name || '',
|
name: session?.user?.name || '',
|
||||||
@@ -102,58 +101,20 @@ const ProfileForm = () => {
|
|||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
<AvatarUser className="h-20 w-20" textSize="2xl" />
|
<AvatarUser className="h-20 w-20" textSize="2xl" />
|
||||||
<form.Field
|
<form.AppField name="image">
|
||||||
name="image"
|
{(field) => (
|
||||||
children={(field) => {
|
<field.FileField
|
||||||
const isInvalid =
|
label="Avatar"
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
className="col-span-2"
|
||||||
return (
|
accept=".jpg, .jpeg, .png, .webp"
|
||||||
<Field data-invalid={isInvalid} className="col-span-2">
|
onChange={(e) => field.handleChange(e.target.files?.[0])}
|
||||||
<FieldLabel htmlFor={field.name}>Avatar</FieldLabel>
|
/>
|
||||||
<Input
|
)}
|
||||||
type="file"
|
</form.AppField>
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
accept=".jpg, .jpeg, .png, .webp"
|
|
||||||
ref={fileInputRef}
|
|
||||||
onChange={(e) =>
|
|
||||||
field.handleChange(e.target.files?.[0])
|
|
||||||
}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
/>
|
|
||||||
{isInvalid && (
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<form.Field
|
<form.AppField name="name">
|
||||||
name="name"
|
{(field) => <field.TextField label={m.profile_form_name()} />}
|
||||||
children={(field) => {
|
</form.AppField>
|
||||||
const isInvalid =
|
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
|
||||||
return (
|
|
||||||
<Field data-invalid={isInvalid}>
|
|
||||||
<FieldLabel htmlFor={field.name}>
|
|
||||||
{m.profile_form_name()}
|
|
||||||
</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
/>
|
|
||||||
{isInvalid && (
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel htmlFor="name">{m.profile_form_email()}</FieldLabel>
|
<FieldLabel htmlFor="name">{m.profile_form_email()}</FieldLabel>
|
||||||
<Input
|
<Input
|
||||||
@@ -171,7 +132,9 @@ const ProfileForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<Button type="submit">{m.ui_update_btn()}</Button>
|
<form.AppForm>
|
||||||
|
<form.SubscribeButton label={m.ui_update_btn()} />
|
||||||
|
</form.AppForm>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
|
import { useAppForm } from '@/hooks/use-app-form';
|
||||||
import { m } from '@/paraglide/messages';
|
import { m } from '@/paraglide/messages';
|
||||||
import { settingQueries } from '@/service/queries';
|
import { settingQueries } from '@/service/queries';
|
||||||
import { updateAdminSettings } from '@/service/setting.api';
|
import { updateAdminSettings } from '@/service/setting.api';
|
||||||
import { settingSchema, SettingsInput } from '@/service/setting.schema';
|
import { settingSchema, SettingsInput } from '@/service/setting.schema';
|
||||||
import { ReturnError } from '@/types/common';
|
import { ReturnError } from '@/types/common';
|
||||||
import { GearIcon } from '@phosphor-icons/react';
|
import { GearIcon } from '@phosphor-icons/react';
|
||||||
import { useForm } from '@tanstack/react-form';
|
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||||||
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
|
import { Field, FieldGroup } from '../ui/field';
|
||||||
import { Input } from '../ui/input';
|
|
||||||
import { Skeleton } from '../ui/skeleton';
|
import { Skeleton } from '../ui/skeleton';
|
||||||
import { Textarea } from '../ui/textarea';
|
|
||||||
|
|
||||||
const defaultValues: SettingsInput = {
|
const defaultValues: SettingsInput = {
|
||||||
site_name: '',
|
site_name: '',
|
||||||
@@ -41,7 +38,7 @@ const SettingsForm = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useAppForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
site_name: settings?.site_name?.value || '',
|
site_name: settings?.site_name?.value || '',
|
||||||
@@ -78,85 +75,21 @@ const SettingsForm = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<form.Field
|
<form.AppField name="site_name">
|
||||||
name="site_name"
|
{(field) => <field.TextField label={m.settings_form_name()} />}
|
||||||
children={(field) => {
|
</form.AppField>
|
||||||
const isInvalid =
|
<form.AppField name="site_description">
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
{(field) => (
|
||||||
return (
|
<field.TextArea label={m.settings_form_description()} />
|
||||||
<Field data-invalid={isInvalid} className="col-span-2">
|
)}
|
||||||
<FieldLabel htmlFor={field.name}>
|
</form.AppField>
|
||||||
{m.settings_form_name()}
|
<form.AppField name="site_keywords">
|
||||||
</FieldLabel>
|
{(field) => <field.TextArea label={m.settings_form_keywords()} />}
|
||||||
<Input
|
</form.AppField>
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
/>
|
|
||||||
{isInvalid && (
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<form.Field
|
|
||||||
name="site_description"
|
|
||||||
children={(field) => {
|
|
||||||
const isInvalid =
|
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
|
||||||
return (
|
|
||||||
<Field data-invalid={isInvalid} className="col-span-2">
|
|
||||||
<FieldLabel htmlFor={field.name}>
|
|
||||||
{m.settings_form_description()}
|
|
||||||
</FieldLabel>
|
|
||||||
<Textarea
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
{isInvalid && (
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<form.Field
|
|
||||||
name="site_keywords"
|
|
||||||
children={(field) => {
|
|
||||||
const isInvalid =
|
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
|
||||||
return (
|
|
||||||
<Field data-invalid={isInvalid} className="col-span-2">
|
|
||||||
<FieldLabel htmlFor={field.name}>
|
|
||||||
{m.settings_form_keywords()}
|
|
||||||
</FieldLabel>
|
|
||||||
<Textarea
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
{isInvalid && (
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Field>
|
<Field>
|
||||||
<Button type="submit">{m.ui_update_btn()}</Button>
|
<form.AppForm>
|
||||||
|
<form.SubscribeButton label={m.ui_update_btn()} />
|
||||||
|
</form.AppForm>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
|
import { useAppForm } from '@/hooks/use-app-form';
|
||||||
import { authClient } from '@/lib/auth-client';
|
import { authClient } from '@/lib/auth-client';
|
||||||
import { m } from '@/paraglide/messages';
|
import { m } from '@/paraglide/messages';
|
||||||
import { useForm } from '@tanstack/react-form';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { createLink, useNavigate } from '@tanstack/react-router';
|
import { createLink, useNavigate } from '@tanstack/react-router';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||||||
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
|
import { Field, FieldGroup } from '../ui/field';
|
||||||
import { Input } from '../ui/input';
|
|
||||||
|
|
||||||
const SignInFormSchema = z.object({
|
const SignInFormSchema = z.object({
|
||||||
email: z
|
email: z
|
||||||
@@ -27,7 +26,8 @@ const ButtonLink = createLink(Button);
|
|||||||
const SignInForm = () => {
|
const SignInForm = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const form = useForm({
|
|
||||||
|
const form = useAppForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
@@ -91,62 +91,26 @@ const SignInForm = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Field>
|
</Field>
|
||||||
<FieldSeparator>Or continue with</FieldSeparator> */}
|
<FieldSeparator>Or continue with</FieldSeparator> */}
|
||||||
<form.Field
|
<form.AppField name="email">
|
||||||
name="email"
|
{(field) => (
|
||||||
children={(field) => {
|
<field.TextField
|
||||||
const isInvalid =
|
label={m.login_page_form_email()}
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
type="email"
|
||||||
return (
|
/>
|
||||||
<Field data-invalid={isInvalid}>
|
)}
|
||||||
<FieldLabel htmlFor={field.name}>
|
</form.AppField>
|
||||||
{m.login_page_form_email()}
|
<form.AppField name="password">
|
||||||
</FieldLabel>
|
{(field) => (
|
||||||
<Input
|
<field.TextField
|
||||||
id={field.name}
|
type="password"
|
||||||
name={field.name}
|
label={m.login_page_form_password()}
|
||||||
value={field.state.value}
|
/>
|
||||||
onBlur={field.handleBlur}
|
)}
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
</form.AppField>
|
||||||
aria-invalid={isInvalid}
|
|
||||||
type="email"
|
|
||||||
placeholder="m@example.com"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
{isInvalid && (
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<form.Field
|
|
||||||
name="password"
|
|
||||||
children={(field) => {
|
|
||||||
const isInvalid =
|
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
|
||||||
return (
|
|
||||||
<Field data-invalid={isInvalid}>
|
|
||||||
<FieldLabel htmlFor={field.name}>
|
|
||||||
{m.login_page_form_password()}
|
|
||||||
</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
{isInvalid && (
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Field>
|
<Field>
|
||||||
<Button type="submit">{m.ui_login_btn()}</Button>
|
<form.AppForm>
|
||||||
|
<form.SubscribeButton label={m.ui_login_btn()} />
|
||||||
|
</form.AppForm>
|
||||||
<ButtonLink to="/" variant="outline">
|
<ButtonLink to="/" variant="outline">
|
||||||
{m.ui_cancel_btn()}
|
{m.ui_cancel_btn()}
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useAppForm } from '@/hooks/use-app-form';
|
||||||
import { m } from '@/paraglide/messages';
|
import { m } from '@/paraglide/messages';
|
||||||
import { Locale, setLocale } from '@/paraglide/runtime';
|
import { Locale, setLocale } from '@/paraglide/runtime';
|
||||||
import { settingQueries } from '@/service/queries';
|
import { settingQueries } from '@/service/queries';
|
||||||
@@ -5,20 +6,11 @@ import { updateUserSettings } from '@/service/setting.api';
|
|||||||
import { UserSettingInput, userSettingSchema } from '@/service/setting.schema';
|
import { UserSettingInput, userSettingSchema } from '@/service/setting.schema';
|
||||||
import { ReturnError } from '@/types/common';
|
import { ReturnError } from '@/types/common';
|
||||||
import { GearIcon } from '@phosphor-icons/react';
|
import { GearIcon } from '@phosphor-icons/react';
|
||||||
import { useForm } from '@tanstack/react-form';
|
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||||||
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
|
import { Field, FieldGroup } from '../ui/field';
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '../ui/select';
|
|
||||||
import { Skeleton } from '../ui/skeleton';
|
import { Skeleton } from '../ui/skeleton';
|
||||||
|
|
||||||
const defaultValues: UserSettingInput = {
|
const defaultValues: UserSettingInput = {
|
||||||
@@ -49,7 +41,7 @@ const UserSettingsForm = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useAppForm({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
validators: {
|
validators: {
|
||||||
onSubmit: userSettingSchema,
|
onSubmit: userSettingSchema,
|
||||||
@@ -68,6 +60,13 @@ const UserSettingsForm = () => {
|
|||||||
}
|
}
|
||||||
}, [data, form]);
|
}, [data, form]);
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Skeleton className="h-40 w-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="@container/card col-span-1 @xl/main:col-span-2">
|
<Card className="@container/card col-span-1 @xl/main:col-span-2">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -86,47 +85,22 @@ const UserSettingsForm = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
{isLoading ? (
|
<form.AppField name="language">
|
||||||
<div className="col-span-2 space-y-2">
|
{(field) => (
|
||||||
<Skeleton className="h-4 w-20" />
|
<field.Select
|
||||||
<Skeleton className="h-10 w-full" />
|
label={m.settings_form_language()}
|
||||||
</div>
|
placeholder={m.settings_form_select_language()}
|
||||||
) : (
|
values={[
|
||||||
<form.Field
|
{ value: 'en', label: 'English' },
|
||||||
name="language"
|
{ value: 'vi', label: 'Tiếng Việt' },
|
||||||
children={(field) => {
|
]}
|
||||||
const isInvalid =
|
/>
|
||||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
)}
|
||||||
return (
|
</form.AppField>
|
||||||
<Field data-invalid={isInvalid} className="col-span-2">
|
|
||||||
<FieldLabel htmlFor={field.name}>
|
|
||||||
{m.settings_form_language()}
|
|
||||||
</FieldLabel>
|
|
||||||
<Select
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onValueChange={(value) => field.handleChange(value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger aria-invalid={isInvalid}>
|
|
||||||
<SelectValue placeholder="Select Language" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="en">English</SelectItem>
|
|
||||||
<SelectItem value="vi">Vietnamese</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{isInvalid && (
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Field>
|
<Field>
|
||||||
<Button type="submit" disabled={isLoading}>
|
<form.AppForm>
|
||||||
{m.ui_update_btn()}
|
<form.SubscribeButton label={m.ui_update_btn()} />
|
||||||
</Button>
|
</form.AppForm>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ const AddNewUserButton = () => {
|
|||||||
const prevent = usePreventAutoFocus();
|
const prevent = usePreventAutoFocus();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={_open} onOpenChange={_setOpen}>
|
||||||
<DialogTrigger>
|
<DialogTrigger asChild>
|
||||||
<Button type="button" variant="default">
|
<Button type="button" variant="default">
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
{m.nav_add_new()}
|
{m.nav_add_new()}
|
||||||
@@ -39,7 +39,7 @@ const AddNewUserButton = () => {
|
|||||||
{m.nav_add_new()}
|
{m.nav_add_new()}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<AdminCreateUserForm />
|
<AdminCreateUserForm onSubmit={_setOpen} />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
29
src/hooks/use-app-form.ts
Normal file
29
src/hooks/use-app-form.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import {
|
||||||
|
FileField,
|
||||||
|
HiddenField,
|
||||||
|
Select,
|
||||||
|
SelectNumber,
|
||||||
|
SubscribeButton,
|
||||||
|
TextArea,
|
||||||
|
TextField,
|
||||||
|
} from '@/components/form/form-components';
|
||||||
|
import { createFormHook, createFormHookContexts } from '@tanstack/react-form';
|
||||||
|
|
||||||
|
export const { fieldContext, useFieldContext, formContext, useFormContext } =
|
||||||
|
createFormHookContexts();
|
||||||
|
|
||||||
|
export const { useAppForm } = createFormHook({
|
||||||
|
fieldComponents: {
|
||||||
|
HiddenField,
|
||||||
|
TextField,
|
||||||
|
TextArea,
|
||||||
|
Select,
|
||||||
|
SelectNumber,
|
||||||
|
FileField,
|
||||||
|
},
|
||||||
|
formComponents: {
|
||||||
|
SubscribeButton,
|
||||||
|
},
|
||||||
|
fieldContext,
|
||||||
|
formContext,
|
||||||
|
});
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
import { Route as appRouteRouteImport } from './routes/(app)/route'
|
import { Route as appRouteRouteImport } from './routes/(app)/route'
|
||||||
import { Route as appIndexRouteImport } from './routes/(app)/index'
|
import { Route as appIndexRouteImport } from './routes/(app)/index'
|
||||||
import { Route as authSignUpRouteImport } from './routes/(auth)/sign-up'
|
|
||||||
import { Route as authSignInRouteImport } from './routes/(auth)/sign-in'
|
import { Route as authSignInRouteImport } from './routes/(auth)/sign-in'
|
||||||
import { Route as appauthRouteRouteImport } from './routes/(app)/(auth)/route'
|
import { Route as appauthRouteRouteImport } from './routes/(app)/(auth)/route'
|
||||||
import { Route as ApiAuthSplatRouteImport } from './routes/api.auth.$'
|
import { Route as ApiAuthSplatRouteImport } from './routes/api.auth.$'
|
||||||
@@ -36,11 +35,6 @@ const appIndexRoute = appIndexRouteImport.update({
|
|||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => appRouteRoute,
|
getParentRoute: () => appRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const authSignUpRoute = authSignUpRouteImport.update({
|
|
||||||
id: '/(auth)/sign-up',
|
|
||||||
path: '/sign-up',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const authSignInRoute = authSignInRouteImport.update({
|
const authSignInRoute = authSignInRouteImport.update({
|
||||||
id: '/(auth)/sign-in',
|
id: '/(auth)/sign-in',
|
||||||
path: '/sign-in',
|
path: '/sign-in',
|
||||||
@@ -114,7 +108,6 @@ const appauthAccountChangePasswordRoute =
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/sign-in': typeof authSignInRoute
|
'/sign-in': typeof authSignInRoute
|
||||||
'/sign-up': typeof authSignUpRoute
|
|
||||||
'/': typeof appIndexRoute
|
'/': typeof appIndexRoute
|
||||||
'/account': typeof appauthAccountRouteRouteWithChildren
|
'/account': typeof appauthAccountRouteRouteWithChildren
|
||||||
'/kanri': typeof appauthKanriRouteRouteWithChildren
|
'/kanri': typeof appauthKanriRouteRouteWithChildren
|
||||||
@@ -131,7 +124,6 @@ export interface FileRoutesByFullPath {
|
|||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/sign-in': typeof authSignInRoute
|
'/sign-in': typeof authSignInRoute
|
||||||
'/sign-up': typeof authSignUpRoute
|
|
||||||
'/': typeof appIndexRoute
|
'/': typeof appIndexRoute
|
||||||
'/dashboard': typeof appauthDashboardRoute
|
'/dashboard': typeof appauthDashboardRoute
|
||||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||||
@@ -149,7 +141,6 @@ export interface FileRoutesById {
|
|||||||
'/(app)': typeof appRouteRouteWithChildren
|
'/(app)': typeof appRouteRouteWithChildren
|
||||||
'/(app)/(auth)': typeof appauthRouteRouteWithChildren
|
'/(app)/(auth)': typeof appauthRouteRouteWithChildren
|
||||||
'/(auth)/sign-in': typeof authSignInRoute
|
'/(auth)/sign-in': typeof authSignInRoute
|
||||||
'/(auth)/sign-up': typeof authSignUpRoute
|
|
||||||
'/(app)/': typeof appIndexRoute
|
'/(app)/': typeof appIndexRoute
|
||||||
'/(app)/(auth)/account': typeof appauthAccountRouteRouteWithChildren
|
'/(app)/(auth)/account': typeof appauthAccountRouteRouteWithChildren
|
||||||
'/(app)/(auth)/kanri': typeof appauthKanriRouteRouteWithChildren
|
'/(app)/(auth)/kanri': typeof appauthKanriRouteRouteWithChildren
|
||||||
@@ -168,7 +159,6 @@ export interface FileRouteTypes {
|
|||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths:
|
fullPaths:
|
||||||
| '/sign-in'
|
| '/sign-in'
|
||||||
| '/sign-up'
|
|
||||||
| '/'
|
| '/'
|
||||||
| '/account'
|
| '/account'
|
||||||
| '/kanri'
|
| '/kanri'
|
||||||
@@ -185,7 +175,6 @@ export interface FileRouteTypes {
|
|||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/sign-in'
|
| '/sign-in'
|
||||||
| '/sign-up'
|
|
||||||
| '/'
|
| '/'
|
||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/api/auth/$'
|
| '/api/auth/$'
|
||||||
@@ -202,7 +191,6 @@ export interface FileRouteTypes {
|
|||||||
| '/(app)'
|
| '/(app)'
|
||||||
| '/(app)/(auth)'
|
| '/(app)/(auth)'
|
||||||
| '/(auth)/sign-in'
|
| '/(auth)/sign-in'
|
||||||
| '/(auth)/sign-up'
|
|
||||||
| '/(app)/'
|
| '/(app)/'
|
||||||
| '/(app)/(auth)/account'
|
| '/(app)/(auth)/account'
|
||||||
| '/(app)/(auth)/kanri'
|
| '/(app)/(auth)/kanri'
|
||||||
@@ -221,7 +209,6 @@ export interface FileRouteTypes {
|
|||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
appRouteRoute: typeof appRouteRouteWithChildren
|
appRouteRoute: typeof appRouteRouteWithChildren
|
||||||
authSignInRoute: typeof authSignInRoute
|
authSignInRoute: typeof authSignInRoute
|
||||||
authSignUpRoute: typeof authSignUpRoute
|
|
||||||
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
|
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,13 +228,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof appIndexRouteImport
|
preLoaderRoute: typeof appIndexRouteImport
|
||||||
parentRoute: typeof appRouteRoute
|
parentRoute: typeof appRouteRoute
|
||||||
}
|
}
|
||||||
'/(auth)/sign-up': {
|
|
||||||
id: '/(auth)/sign-up'
|
|
||||||
path: '/sign-up'
|
|
||||||
fullPath: '/sign-up'
|
|
||||||
preLoaderRoute: typeof authSignUpRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/(auth)/sign-in': {
|
'/(auth)/sign-in': {
|
||||||
id: '/(auth)/sign-in'
|
id: '/(auth)/sign-in'
|
||||||
path: '/sign-in'
|
path: '/sign-in'
|
||||||
@@ -416,7 +396,6 @@ const appRouteRouteWithChildren = appRouteRoute._addFileChildren(
|
|||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
appRouteRoute: appRouteRouteWithChildren,
|
appRouteRoute: appRouteRouteWithChildren,
|
||||||
authSignInRoute: authSignInRoute,
|
authSignInRoute: authSignInRoute,
|
||||||
authSignUpRoute: authSignUpRoute,
|
|
||||||
ApiAuthSplatRoute: ApiAuthSplatRoute,
|
ApiAuthSplatRoute: ApiAuthSplatRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
|
|||||||
@@ -2,13 +2,25 @@ import { AuthProvider } from '@/components/auth/auth-provider';
|
|||||||
import Header from '@/components/Header';
|
import Header from '@/components/Header';
|
||||||
import AppSidebar from '@/components/sidebar/app-sidebar';
|
import AppSidebar from '@/components/sidebar/app-sidebar';
|
||||||
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
|
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
|
||||||
|
import { Locale, setLocale } from '@/paraglide/runtime';
|
||||||
|
import { settingQueries } from '@/service/queries';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { createFileRoute, Outlet } from '@tanstack/react-router';
|
import { createFileRoute, Outlet } from '@tanstack/react-router';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export const Route = createFileRoute('/(app)')({
|
export const Route = createFileRoute('/(app)')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
|
const { data: language } = useQuery(
|
||||||
|
settingQueries.getCurrentUserLanguageSetting(),
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (language) {
|
||||||
|
setLocale(language as Locale);
|
||||||
|
}
|
||||||
|
}, [language]);
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<SidebarProvider defaultOpen={false}>
|
<SidebarProvider defaultOpen={false}>
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import SignupForm from '@/components/form/signup-form'
|
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/(auth)/sign-up')({
|
|
||||||
component: RouteComponent,
|
|
||||||
})
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
return (
|
|
||||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
|
||||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
|
||||||
<h1 className="text-xl font-semibold flex items-center gap-2 self-center">
|
|
||||||
<img src="/logo.svg" alt="Fuware Logo" className="h-8" />
|
|
||||||
Fuware
|
|
||||||
</h1>
|
|
||||||
<SignupForm />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import { getSession } from '@/lib/auth/session';
|
import { getSession } from '@/lib/auth/session';
|
||||||
import { queryOptions } from '@tanstack/react-query';
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
import { getAllAudit } from './audit.api';
|
import { getAllAudit } from './audit.api';
|
||||||
import { getAdminSettings, getUserSettings } from './setting.api';
|
import {
|
||||||
|
getAdminSettings,
|
||||||
|
getCurrentUserLanguage,
|
||||||
|
getUserSettings,
|
||||||
|
} from './setting.api';
|
||||||
import { getAllUser } from './user.api';
|
import { getAllUser } from './user.api';
|
||||||
|
|
||||||
export const sessionQueries = {
|
export const sessionQueries = {
|
||||||
@@ -27,6 +31,11 @@ export const settingQueries = {
|
|||||||
queryKey: [...settingQueries.all, 'listUser'],
|
queryKey: [...settingQueries.all, 'listUser'],
|
||||||
queryFn: () => getUserSettings(),
|
queryFn: () => getUserSettings(),
|
||||||
}),
|
}),
|
||||||
|
getCurrentUserLanguageSetting: () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: [...settingQueries.all, 'language'],
|
||||||
|
queryFn: () => getCurrentUserLanguage(),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const auditQueries = {
|
export const auditQueries = {
|
||||||
|
|||||||
@@ -12,6 +12,24 @@ export const getAdminSettings = createServerFn({ method: 'GET' })
|
|||||||
return await getAllAdminSettings();
|
return await getAllAdminSettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getCurrentUserLanguage = createServerFn({ method: 'GET' })
|
||||||
|
.middleware([authMiddleware])
|
||||||
|
.handler(async ({ context }) => {
|
||||||
|
try {
|
||||||
|
const setting = await prisma.setting.findUniqueOrThrow({
|
||||||
|
where: { key: context.user.id, relation: 'user' },
|
||||||
|
select: { value: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const value = JSON.parse(setting.value) as UserSetting;
|
||||||
|
|
||||||
|
return value.language;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const updateAdminSettings = createServerFn({ method: 'POST' })
|
export const updateAdminSettings = createServerFn({ method: 'POST' })
|
||||||
.inputValidator(settingSchema)
|
.inputValidator(settingSchema)
|
||||||
.middleware([authMiddleware])
|
.middleware([authMiddleware])
|
||||||
@@ -84,6 +102,7 @@ export const getUserSettings = createServerFn({ method: 'GET' })
|
|||||||
value: JSON.parse(settings.value) as UserSetting,
|
value: JSON.parse(settings.value) as UserSetting,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -128,6 +147,7 @@ export const updateUserSettings = createServerFn({ method: 'POST' })
|
|||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,36 @@ export const baseUser = z.object({
|
|||||||
id: z.string().nonempty(m.users_page_message_user_not_found()),
|
id: z.string().nonempty(m.users_page_message_user_not_found()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ChangePasswordFormSchema = z
|
||||||
|
.object({
|
||||||
|
currentPassword: z.string().nonempty(
|
||||||
|
m.common_is_required({
|
||||||
|
field: m.change_password_form_current_password(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
newPassword: z.string().nonempty(
|
||||||
|
m.common_is_required({
|
||||||
|
field: m.change_password_form_new_password(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
confirmPassword: z.string().nonempty(
|
||||||
|
m.common_is_required({
|
||||||
|
field: m.change_password_form_confirm_password(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.newPassword !== data.confirmPassword) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ['confirmPassword'],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: m.change_password_messages_password_not_match(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ChangePassword = z.infer<typeof ChangePasswordFormSchema>;
|
||||||
|
|
||||||
export const userListSchema = z.object({
|
export const userListSchema = z.object({
|
||||||
page: z.coerce.number().min(1).default(1),
|
page: z.coerce.number().min(1).default(1),
|
||||||
limit: z.coerce.number().min(10).max(100).default(10),
|
limit: z.coerce.number().min(10).max(100).default(10),
|
||||||
|
|||||||
Reference in New Issue
Block a user