change basic form to form context

This commit is contained in:
2026-01-23 16:49:24 +07:00
parent 9766684e03
commit ff2140b9ef
21 changed files with 558 additions and 804 deletions

View File

@@ -93,6 +93,7 @@
"settings_form_description": "Description",
"settings_form_keywords": "keywords",
"settings_form_language": "Language",
"settings_form_select_language": "Please select language",
"settings_ui_title": "Settings",
"settings_messages_update_success": "Updated settings successfully!",
"settings_messages_update_fail": "Update fail!",

View File

@@ -93,6 +93,7 @@
"settings_form_description": "Mô tả website",
"settings_form_keywords": "Từ khóa",
"settings_form_language": "Ngôn ngữ",
"settings_form_select_language": "Hãy chọn ngôn ngữ",
"settings_ui_title": "Cài đặt",
"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!",

View File

@@ -1,21 +1,12 @@
import { useAppForm } from '@/hooks/use-app-form';
import { m } from '@/paraglide/messages';
import { userBanSchema } from '@/service/user.schema';
import { WarningIcon } from '@phosphor-icons/react';
import { useForm } from '@tanstack/react-form';
import { UserWithRole } from 'better-auth/plugins';
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
import { Button } from '../ui/button';
import { DialogClose, DialogFooter } from '../ui/dialog';
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
import { Input } from '../ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '../ui/select';
import { Textarea } from '../ui/textarea';
import { Field, FieldGroup } from '../ui/field';
import { useBanContext } from '../user/ban-user-dialog';
type FormProps = {
@@ -25,7 +16,7 @@ type FormProps = {
const BanUserForm = ({ data }: FormProps) => {
const { setSubmitData, setOpen, setOpenConfirm } = useBanContext();
const form = useForm({
const form = useAppForm({
defaultValues: {
id: data.id,
banReason: '',
@@ -59,99 +50,33 @@ const BanUserForm = ({ data }: FormProps) => {
<AlertTitle>
{m.profile_form_name()}: {data.name}
</AlertTitle>
<AlertDescription className="sr-only">adá</AlertDescription>
<AlertDescription className="sr-only">{data.name}</AlertDescription>
</Alert>
<form.Field
name="id"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid;
return (
<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="banReason"
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.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
<form.AppField name="id">
{(field) => <field.HiddenField />}
</form.AppField>
<form.AppField name="banReason">
{(field) => (
<field.TextArea label={m.users_page_ui_form_ban_reason()} />
)}
</form.AppField>
<form.AppField name="banExp">
{(field) => (
<field.SelectNumber
label={m.users_page_ui_form_ban_exp()}
values={[
{ label: m.exp_time({ time: '1' }), value: '1' },
{ label: m.exp_time({ time: '7' }), value: '7' },
{ 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' },
{ label: m.exp_time({ time: '99999' }), value: '99999' },
]}
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>
);
}}
/>
)}
</form.AppField>
<Field>
<DialogFooter>
<DialogClose asChild>
@@ -159,9 +84,12 @@ const BanUserForm = ({ data }: FormProps) => {
{m.ui_cancel_btn()}
</Button>
</DialogClose>
<Button type="submit" variant="destructive">
{m.ui_ban_btn()}
</Button>
<form.AppForm>
<form.SubscribeButton
label={m.ui_ban_btn()}
variant="destructive"
/>
</form.AppForm>
</DialogFooter>
</Field>
</FieldGroup>

View File

@@ -1,22 +1,14 @@
import { useAppForm } from '@/hooks/use-app-form';
import { m } from '@/paraglide/messages';
import { usersQueries } from '@/service/queries';
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 { useForm } from '@tanstack/react-form';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { Button } from '../ui/button';
import { DialogClose, DialogFooter } from '../ui/dialog';
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
import { Input } from '../ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '../ui/select';
import { Field, FieldGroup } from '../ui/field';
type FormProps = {
onSubmit: (open: boolean) => void;
@@ -44,7 +36,7 @@ const AdminCreateUserForm = ({ onSubmit }: FormProps) => {
},
});
const form = useForm({
const form = useAppForm({
defaultValues: {
email: '',
password: '',
@@ -70,114 +62,33 @@ const AdminCreateUserForm = ({ onSubmit }: FormProps) => {
}}
>
<FieldGroup>
<form.Field
name="email"
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.login_page_form_email()}:
</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="email"
/>
{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}
<form.AppField name="email">
{(field) => <field.TextField label={m.login_page_form_email()} />}
</form.AppField>
<form.AppField name="password">
{(field) => (
<field.TextField
label={m.login_page_form_password()}
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
)}
</form.AppField>
<form.AppField name="name">
{(field) => <field.TextField label={m.profile_form_name()} />}
</form.AppField>
<form.AppField name="role">
{(field) => (
<field.Select
label={m.profile_form_role()}
placeholder={m.users_page_ui_select_placeholder_role()}
isRole
values={[
{ value: 'admin', label: m.role_tags({ role: 'admin' }) },
{ value: 'user', label: m.role_tags({ role: 'user' }) },
]}
/>
</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>
);
}}
/>
)}
</form.AppField>
<Field>
<DialogFooter>
<DialogClose asChild>
@@ -185,9 +96,9 @@ const AdminCreateUserForm = ({ onSubmit }: FormProps) => {
{m.ui_cancel_btn()}
</Button>
</DialogClose>
<Button type="submit" variant="destructive">
{m.ui_signup_btn()}
</Button>
<form.AppForm>
<form.SubscribeButton label={m.ui_signup_btn()} />
</form.AppForm>
</DialogFooter>
</Field>
</FieldGroup>

View File

@@ -1,16 +1,15 @@
import { useAppForm } from '@/hooks/use-app-form';
import { m } from '@/paraglide/messages';
import { usersQueries } from '@/service/queries';
import { setUserPassword } from '@/service/user.api';
import { userSetPasswordSchema } from '@/service/user.schema';
import { ReturnError } from '@/types/common';
import { useForm } from '@tanstack/react-form';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { UserWithRole } from 'better-auth/plugins';
import { toast } from 'sonner';
import { Button } from '../ui/button';
import { DialogClose, DialogFooter } from '../ui/dialog';
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
import { Input } from '../ui/input';
import { Field, FieldGroup } from '../ui/field';
type FormProps = {
data: UserWithRole;
@@ -39,7 +38,7 @@ const AdminSetPasswordForm = ({ data, onSubmit }: FormProps) => {
},
});
const form = useForm({
const form = useAppForm({
defaultValues: {
id: data.id,
password: '',
@@ -62,49 +61,14 @@ const AdminSetPasswordForm = ({ data, onSubmit }: FormProps) => {
}}
>
<FieldGroup>
<form.Field
name="id"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid;
return (
<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="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>
);
}}
/>
<form.AppField name="id">
{(field) => <field.HiddenField />}
</form.AppField>
<form.AppField name="password">
{(field) => (
<field.TextField label={m.change_password_form_new_password()} />
)}
</form.AppField>
<Field>
<DialogFooter>
<DialogClose asChild>
@@ -112,7 +76,9 @@ const AdminSetPasswordForm = ({ data, onSubmit }: FormProps) => {
{m.ui_cancel_btn()}
</Button>
</DialogClose>
<Button type="submit">{m.ui_save_btn()}</Button>
<form.AppForm>
<form.SubscribeButton label={m.ui_save_btn()} />
</form.AppForm>
</DialogFooter>
</Field>
</FieldGroup>

View File

@@ -1,23 +1,15 @@
import { useAppForm } from '@/hooks/use-app-form';
import { m } from '@/paraglide/messages';
import { usersQueries } from '@/service/queries';
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 { useForm } from '@tanstack/react-form';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { UserWithRole } from 'better-auth/plugins';
import { toast } from 'sonner';
import { Button } from '../ui/button';
import { DialogClose, DialogFooter } from '../ui/dialog';
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
import { Input } from '../ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '../ui/select';
import { Field, FieldGroup } from '../ui/field';
type SetRoleFormProps = {
data: UserWithRole;
@@ -51,7 +43,7 @@ const AdminSetUserRoleForm = ({ data, onSubmit }: SetRoleFormProps) => {
},
});
const form = useForm({
const form = useAppForm({
defaultValues: userUpdateRoleSchema.parse(defaultFormValues),
validators: {
onChange: userUpdateRoleSchema,
@@ -72,61 +64,21 @@ const AdminSetUserRoleForm = ({ data, onSubmit }: SetRoleFormProps) => {
}}
>
<FieldGroup>
<form.Field
name="id"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid;
return (
<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="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
<form.AppField name="id">
{(field) => <field.HiddenField />}
</form.AppField>
<form.AppField name="role">
{(field) => (
<field.Select
label={m.profile_form_role()}
placeholder={m.users_page_ui_select_placeholder_role()}
values={[
{ value: 'admin', label: m.role_tags({ role: 'admin' }) },
{ value: 'user', label: m.role_tags({ role: 'user' }) },
]}
/>
</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>
);
}}
/>
)}
</form.AppField>
<Field>
<DialogFooter>
<DialogClose asChild>
@@ -134,7 +86,9 @@ const AdminSetUserRoleForm = ({ data, onSubmit }: SetRoleFormProps) => {
{m.ui_cancel_btn()}
</Button>
</DialogClose>
<Button type="submit">{m.ui_save_btn()}</Button>
<form.AppForm>
<form.SubscribeButton label={m.ui_save_btn()} />
</form.AppForm>
</DialogFooter>
</Field>
</FieldGroup>

View File

@@ -1,16 +1,15 @@
import { useAppForm } from '@/hooks/use-app-form';
import { m } from '@/paraglide/messages';
import { usersQueries } from '@/service/queries';
import { updateUserInformation } from '@/service/user.api';
import { userUpdateInfoSchema } from '@/service/user.schema';
import { ReturnError } from '@/types/common';
import { useForm } from '@tanstack/react-form';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { UserWithRole } from 'better-auth/plugins';
import { toast } from 'sonner';
import { Button } from '../ui/button';
import { DialogClose, DialogFooter } from '../ui/dialog';
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
import { Input } from '../ui/input';
import { Field, FieldGroup } from '../ui/field';
type UpdateUserFormProps = {
data: UserWithRole;
@@ -38,7 +37,7 @@ const AdminUpdateUserInfoForm = ({ data, onSubmit }: UpdateUserFormProps) => {
});
},
});
const form = useForm({
const form = useAppForm({
defaultValues: {
id: data.id,
name: data.name,
@@ -61,49 +60,12 @@ const AdminUpdateUserInfoForm = ({ data, onSubmit }: UpdateUserFormProps) => {
}}
>
<FieldGroup>
<form.Field
name="id"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid;
return (
<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>
);
}}
/>
<form.AppField name="id">
{(field) => <field.HiddenField />}
</form.AppField>
<form.AppField name="name">
{(field) => <field.TextField label={m.profile_form_name()} />}
</form.AppField>
<Field>
<DialogFooter>
<DialogClose asChild>
@@ -111,7 +73,9 @@ const AdminUpdateUserInfoForm = ({ data, onSubmit }: UpdateUserFormProps) => {
{m.ui_cancel_btn()}
</Button>
</DialogClose>
<Button type="submit">{m.ui_save_btn()}</Button>
<form.AppForm>
<form.SubscribeButton label={m.ui_save_btn()} />
</form.AppForm>
</DialogFooter>
</Field>
</FieldGroup>

View File

@@ -1,43 +1,14 @@
import { useAppForm } from '@/hooks/use-app-form';
import { authClient } from '@/lib/auth-client';
import { m } from '@/paraglide/messages';
import {
ChangePassword,
ChangePasswordFormSchema,
} from '@/service/user.schema';
import { KeyIcon } from '@phosphor-icons/react';
import { useForm } from '@tanstack/react-form';
import { toast } from 'sonner';
import z from 'zod';
import { Button } from '../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Field, FieldError, FieldGroup, FieldLabel } 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>;
import { Field, FieldGroup } from '../ui/field';
const defaultValues: ChangePassword = {
currentPassword: '',
@@ -46,7 +17,7 @@ const defaultValues: ChangePassword = {
};
const ChangePasswordForm = () => {
const form = useForm({
const form = useAppForm({
defaultValues,
validators: {
onSubmit: ChangePasswordFormSchema,
@@ -98,86 +69,33 @@ const ChangePasswordForm = () => {
}}
>
<FieldGroup>
<form.Field
name="currentPassword"
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_current_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}
<form.AppField name="currentPassword">
{(field) => (
<field.TextField
label={m.change_password_form_current_password()}
/>
)}
</form.AppField>
<form.AppField name="newPassword">
{(field) => (
<field.TextField
label={m.change_password_form_new_password()}
type="password"
/>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
);
}}
/>
<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}
</form.AppField>
<form.AppField name="confirmPassword">
{(field) => (
<field.TextField
label={m.change_password_form_confirm_password()}
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>
);
}}
/>
</form.AppField>
<Field>
<Button type="submit">{m.ui_change_password_btn()}</Button>
<form.AppForm>
<form.SubscribeButton label={m.ui_change_password_btn()} />
</form.AppForm>
</Field>
</FieldGroup>
</form>

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

View File

@@ -1,18 +1,17 @@
import { useAppForm } from '@/hooks/use-app-form';
import { authClient } from '@/lib/auth-client';
import { m } from '@/paraglide/messages';
import { uploadProfileImage } from '@/service/profile.api';
import { ProfileInput, profileUpdateSchema } from '@/service/profile.schema';
import { UserCircleIcon } from '@phosphor-icons/react';
import { useForm } from '@tanstack/react-form';
import { useQueryClient } from '@tanstack/react-query';
import { useRef } from 'react';
import { toast } from 'sonner';
import { useAuth } from '../auth/auth-provider';
import AvatarUser from '../avatar/avatar-user';
import RoleBadge from '../avatar/role-badge';
import { Button } from '../ui/button';
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';
const defaultValues: ProfileInput = {
@@ -25,7 +24,7 @@ const ProfileForm = () => {
const { data: session, isPending } = useAuth();
const queryClient = useQueryClient();
const form = useForm({
const form = useAppForm({
defaultValues: {
...defaultValues,
name: session?.user?.name || '',
@@ -102,58 +101,20 @@ const ProfileForm = () => {
<FieldGroup>
<div className="grid grid-cols-3 gap-3">
<AvatarUser className="h-20 w-20" textSize="2xl" />
<form.Field
name="image"
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}>Avatar</FieldLabel>
<Input
type="file"
id={field.name}
name={field.name}
<form.AppField name="image">
{(field) => (
<field.FileField
label="Avatar"
className="col-span-2"
accept=".jpg, .jpeg, .png, .webp"
ref={fileInputRef}
onChange={(e) =>
field.handleChange(e.target.files?.[0])
}
aria-invalid={isInvalid}
onChange={(e) => field.handleChange(e.target.files?.[0])}
/>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
);
}}
/>
</form.AppField>
</div>
<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}
/>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
);
}}
/>
<form.AppField name="name">
{(field) => <field.TextField label={m.profile_form_name()} />}
</form.AppField>
<Field>
<FieldLabel htmlFor="name">{m.profile_form_email()}</FieldLabel>
<Input
@@ -171,7 +132,9 @@ const ProfileForm = () => {
</div>
</Field>
<Field>
<Button type="submit">{m.ui_update_btn()}</Button>
<form.AppForm>
<form.SubscribeButton label={m.ui_update_btn()} />
</form.AppForm>
</Field>
</FieldGroup>
</form>

View File

@@ -1,18 +1,15 @@
import { useAppForm } from '@/hooks/use-app-form';
import { m } from '@/paraglide/messages';
import { settingQueries } from '@/service/queries';
import { updateAdminSettings } from '@/service/setting.api';
import { settingSchema, SettingsInput } from '@/service/setting.schema';
import { ReturnError } from '@/types/common';
import { GearIcon } from '@phosphor-icons/react';
import { useForm } from '@tanstack/react-form';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { Button } from '../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
import { Input } from '../ui/input';
import { Field, FieldGroup } from '../ui/field';
import { Skeleton } from '../ui/skeleton';
import { Textarea } from '../ui/textarea';
const defaultValues: SettingsInput = {
site_name: '',
@@ -41,7 +38,7 @@ const SettingsForm = () => {
},
});
const form = useForm({
const form = useAppForm({
defaultValues: {
...defaultValues,
site_name: settings?.site_name?.value || '',
@@ -78,85 +75,21 @@ const SettingsForm = () => {
}}
>
<FieldGroup>
<form.Field
name="site_name"
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_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} />
<form.AppField name="site_name">
{(field) => <field.TextField label={m.settings_form_name()} />}
</form.AppField>
<form.AppField name="site_description">
{(field) => (
<field.TextArea label={m.settings_form_description()} />
)}
</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>
);
}}
/>
</form.AppField>
<form.AppField name="site_keywords">
{(field) => <field.TextArea label={m.settings_form_keywords()} />}
</form.AppField>
<Field>
<Button type="submit">{m.ui_update_btn()}</Button>
<form.AppForm>
<form.SubscribeButton label={m.ui_update_btn()} />
</form.AppForm>
</Field>
</FieldGroup>
</form>

View File

@@ -1,14 +1,13 @@
import { useAppForm } from '@/hooks/use-app-form';
import { authClient } from '@/lib/auth-client';
import { m } from '@/paraglide/messages';
import { useForm } from '@tanstack/react-form';
import { useQueryClient } from '@tanstack/react-query';
import { createLink, useNavigate } from '@tanstack/react-router';
import { toast } from 'sonner';
import z from 'zod';
import { Button } from '../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
import { Input } from '../ui/input';
import { Field, FieldGroup } from '../ui/field';
const SignInFormSchema = z.object({
email: z
@@ -27,7 +26,8 @@ const ButtonLink = createLink(Button);
const SignInForm = () => {
const navigate = useNavigate();
const queryClient = useQueryClient();
const form = useForm({
const form = useAppForm({
defaultValues: {
email: '',
password: '',
@@ -91,62 +91,26 @@ const SignInForm = () => {
</Button>
</Field>
<FieldSeparator>Or continue with</FieldSeparator> */}
<form.Field
name="email"
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_email()}
</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}
<form.AppField name="email">
{(field) => (
<field.TextField
label={m.login_page_form_email()}
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}
</form.AppField>
<form.AppField name="password">
{(field) => (
<field.TextField
type="password"
label={m.login_page_form_password()}
/>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
);
}}
/>
</form.AppField>
<Field>
<Button type="submit">{m.ui_login_btn()}</Button>
<form.AppForm>
<form.SubscribeButton label={m.ui_login_btn()} />
</form.AppForm>
<ButtonLink to="/" variant="outline">
{m.ui_cancel_btn()}
</ButtonLink>

View File

@@ -1,3 +1,4 @@
import { useAppForm } from '@/hooks/use-app-form';
import { m } from '@/paraglide/messages';
import { Locale, setLocale } from '@/paraglide/runtime';
import { settingQueries } from '@/service/queries';
@@ -5,20 +6,11 @@ import { updateUserSettings } from '@/service/setting.api';
import { UserSettingInput, userSettingSchema } from '@/service/setting.schema';
import { ReturnError } from '@/types/common';
import { GearIcon } from '@phosphor-icons/react';
import { useForm } from '@tanstack/react-form';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
import { toast } from 'sonner';
import { Button } from '../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '../ui/select';
import { Field, FieldGroup } from '../ui/field';
import { Skeleton } from '../ui/skeleton';
const defaultValues: UserSettingInput = {
@@ -49,7 +41,7 @@ const UserSettingsForm = () => {
},
});
const form = useForm({
const form = useAppForm({
defaultValues,
validators: {
onSubmit: userSettingSchema,
@@ -68,6 +60,13 @@ const UserSettingsForm = () => {
}
}, [data, form]);
if (isLoading)
return (
<div>
<Skeleton className="h-40 w-full" />
</div>
);
return (
<Card className="@container/card col-span-1 @xl/main:col-span-2">
<CardHeader>
@@ -86,47 +85,22 @@ const UserSettingsForm = () => {
}}
>
<FieldGroup>
{isLoading ? (
<div className="col-span-2 space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-10 w-full" />
</div>
) : (
<form.Field
name="language"
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_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>
);
}}
<form.AppField name="language">
{(field) => (
<field.Select
label={m.settings_form_language()}
placeholder={m.settings_form_select_language()}
values={[
{ value: 'en', label: 'English' },
{ value: 'vi', label: 'Tiếng Việt' },
]}
/>
)}
</form.AppField>
<Field>
<Button type="submit" disabled={isLoading}>
{m.ui_update_btn()}
</Button>
<form.AppForm>
<form.SubscribeButton label={m.ui_update_btn()} />
</form.AppForm>
</Field>
</FieldGroup>
</form>

View File

@@ -18,8 +18,8 @@ const AddNewUserButton = () => {
const prevent = usePreventAutoFocus();
return (
<Dialog>
<DialogTrigger>
<Dialog open={_open} onOpenChange={_setOpen}>
<DialogTrigger asChild>
<Button type="button" variant="default">
<PlusIcon />
{m.nav_add_new()}
@@ -39,7 +39,7 @@ const AddNewUserButton = () => {
{m.nav_add_new()}
</DialogDescription>
</DialogHeader>
<AdminCreateUserForm />
<AdminCreateUserForm onSubmit={_setOpen} />
</DialogContent>
</Dialog>
);

29
src/hooks/use-app-form.ts Normal file
View 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,
});

View File

@@ -11,7 +11,6 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as appRouteRouteImport } from './routes/(app)/route'
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 appauthRouteRouteImport } from './routes/(app)/(auth)/route'
import { Route as ApiAuthSplatRouteImport } from './routes/api.auth.$'
@@ -36,11 +35,6 @@ const appIndexRoute = appIndexRouteImport.update({
path: '/',
getParentRoute: () => appRouteRoute,
} as any)
const authSignUpRoute = authSignUpRouteImport.update({
id: '/(auth)/sign-up',
path: '/sign-up',
getParentRoute: () => rootRouteImport,
} as any)
const authSignInRoute = authSignInRouteImport.update({
id: '/(auth)/sign-in',
path: '/sign-in',
@@ -114,7 +108,6 @@ const appauthAccountChangePasswordRoute =
export interface FileRoutesByFullPath {
'/sign-in': typeof authSignInRoute
'/sign-up': typeof authSignUpRoute
'/': typeof appIndexRoute
'/account': typeof appauthAccountRouteRouteWithChildren
'/kanri': typeof appauthKanriRouteRouteWithChildren
@@ -131,7 +124,6 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/sign-in': typeof authSignInRoute
'/sign-up': typeof authSignUpRoute
'/': typeof appIndexRoute
'/dashboard': typeof appauthDashboardRoute
'/api/auth/$': typeof ApiAuthSplatRoute
@@ -149,7 +141,6 @@ export interface FileRoutesById {
'/(app)': typeof appRouteRouteWithChildren
'/(app)/(auth)': typeof appauthRouteRouteWithChildren
'/(auth)/sign-in': typeof authSignInRoute
'/(auth)/sign-up': typeof authSignUpRoute
'/(app)/': typeof appIndexRoute
'/(app)/(auth)/account': typeof appauthAccountRouteRouteWithChildren
'/(app)/(auth)/kanri': typeof appauthKanriRouteRouteWithChildren
@@ -168,7 +159,6 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/sign-in'
| '/sign-up'
| '/'
| '/account'
| '/kanri'
@@ -185,7 +175,6 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/sign-in'
| '/sign-up'
| '/'
| '/dashboard'
| '/api/auth/$'
@@ -202,7 +191,6 @@ export interface FileRouteTypes {
| '/(app)'
| '/(app)/(auth)'
| '/(auth)/sign-in'
| '/(auth)/sign-up'
| '/(app)/'
| '/(app)/(auth)/account'
| '/(app)/(auth)/kanri'
@@ -221,7 +209,6 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
appRouteRoute: typeof appRouteRouteWithChildren
authSignInRoute: typeof authSignInRoute
authSignUpRoute: typeof authSignUpRoute
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
}
@@ -241,13 +228,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof appIndexRouteImport
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': {
id: '/(auth)/sign-in'
path: '/sign-in'
@@ -416,7 +396,6 @@ const appRouteRouteWithChildren = appRouteRoute._addFileChildren(
const rootRouteChildren: RootRouteChildren = {
appRouteRoute: appRouteRouteWithChildren,
authSignInRoute: authSignInRoute,
authSignUpRoute: authSignUpRoute,
ApiAuthSplatRoute: ApiAuthSplatRoute,
}
export const routeTree = rootRouteImport

View File

@@ -2,13 +2,25 @@ import { AuthProvider } from '@/components/auth/auth-provider';
import Header from '@/components/Header';
import AppSidebar from '@/components/sidebar/app-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 { useEffect } from 'react';
export const Route = createFileRoute('/(app)')({
component: RouteComponent,
});
function RouteComponent() {
const { data: language } = useQuery(
settingQueries.getCurrentUserLanguageSetting(),
);
useEffect(() => {
if (language) {
setLocale(language as Locale);
}
}, [language]);
return (
<AuthProvider>
<SidebarProvider defaultOpen={false}>

View File

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

View File

@@ -1,7 +1,11 @@
import { getSession } from '@/lib/auth/session';
import { queryOptions } from '@tanstack/react-query';
import { getAllAudit } from './audit.api';
import { getAdminSettings, getUserSettings } from './setting.api';
import {
getAdminSettings,
getCurrentUserLanguage,
getUserSettings,
} from './setting.api';
import { getAllUser } from './user.api';
export const sessionQueries = {
@@ -27,6 +31,11 @@ export const settingQueries = {
queryKey: [...settingQueries.all, 'listUser'],
queryFn: () => getUserSettings(),
}),
getCurrentUserLanguageSetting: () =>
queryOptions({
queryKey: [...settingQueries.all, 'language'],
queryFn: () => getCurrentUserLanguage(),
}),
};
export const auditQueries = {

View File

@@ -12,6 +12,24 @@ export const getAdminSettings = createServerFn({ method: 'GET' })
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' })
.inputValidator(settingSchema)
.middleware([authMiddleware])
@@ -84,6 +102,7 @@ export const getUserSettings = createServerFn({ method: 'GET' })
value: JSON.parse(settings.value) as UserSetting,
};
} catch (error) {
console.log(error);
throw error;
}
});
@@ -128,6 +147,7 @@ export const updateUserSettings = createServerFn({ method: 'POST' })
return { success: true };
} catch (error) {
console.log(error);
throw error;
}
});

View File

@@ -5,6 +5,36 @@ export const baseUser = z.object({
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({
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(10).max(100).default(10),