Merge pull request 'Added settings for user' (#5) from feature/account into main
Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
@@ -38,7 +38,7 @@ async function main() {
|
|||||||
...settingsData,
|
...settingsData,
|
||||||
{
|
{
|
||||||
key: admin ? (admin?.user?.id as string) : (mailExists?.id as string),
|
key: admin ? (admin?.user?.id as string) : (mailExists?.id as string),
|
||||||
value: 'en',
|
value: '{ "language": "en" }',
|
||||||
description: 'User Settings',
|
description: 'User Settings',
|
||||||
relation: 'user',
|
relation: 'user',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { m } from '@/paraglide/messages';
|
import { m } from '@/paraglide/messages';
|
||||||
import { settingQueries } from '@/service/queries';
|
import { settingQueries } from '@/service/queries';
|
||||||
import { updateSettings } 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 { GearIcon } from '@phosphor-icons/react';
|
import { GearIcon } from '@phosphor-icons/react';
|
||||||
import { useForm } from '@tanstack/react-form';
|
import { useForm } from '@tanstack/react-form';
|
||||||
@@ -10,6 +10,7 @@ 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, FieldError, FieldGroup, FieldLabel } from '../ui/field';
|
||||||
import { Input } from '../ui/input';
|
import { Input } from '../ui/input';
|
||||||
|
import { Skeleton } from '../ui/skeleton';
|
||||||
import { Textarea } from '../ui/textarea';
|
import { Textarea } from '../ui/textarea';
|
||||||
|
|
||||||
const defaultValues: SettingsInput = {
|
const defaultValues: SettingsInput = {
|
||||||
@@ -21,13 +22,15 @@ const defaultValues: SettingsInput = {
|
|||||||
const SettingsForm = () => {
|
const SettingsForm = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { data: settings } = useQuery(settingQueries.list());
|
const { data: settings, isLoading } = useQuery(settingQueries.listAdmin());
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: updateSettings,
|
mutationFn: updateAdminSettings,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
// setLocale(variables.data.site_language as Locale);
|
// setLocale(variables.data.site_language as Locale);
|
||||||
queryClient.invalidateQueries({ queryKey: settingQueries.all });
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [...settingQueries.all, 'list'],
|
||||||
|
});
|
||||||
toast.success(m.settings_messages_update_success(), {
|
toast.success(m.settings_messages_update_success(), {
|
||||||
richColors: true,
|
richColors: true,
|
||||||
});
|
});
|
||||||
@@ -50,6 +53,9 @@ const SettingsForm = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return <Skeleton className="h-96.25 col-span-1 @xl/main:col-span-2" />;
|
||||||
|
|
||||||
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>
|
||||||
@@ -145,37 +151,6 @@ const SettingsForm = () => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* <form.Field
|
|
||||||
name="site_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)}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<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">{m.ui_update_btn()}</Button>
|
<Button type="submit">{m.ui_update_btn()}</Button>
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
131
src/components/form/user-settings-form.tsx
Normal file
131
src/components/form/user-settings-form.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { m } from '@/paraglide/messages';
|
||||||
|
import { Locale, setLocale } from '@/paraglide/runtime';
|
||||||
|
import { settingQueries } from '@/service/queries';
|
||||||
|
import { updateUserSettings } from '@/service/setting.api';
|
||||||
|
import { UserSettingInput, userSettingSchema } from '@/service/setting.schema';
|
||||||
|
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 { Skeleton } from '../ui/skeleton';
|
||||||
|
|
||||||
|
const defaultValues: UserSettingInput = {
|
||||||
|
language: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserSettingsForm = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery(settingQueries.listUser());
|
||||||
|
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: updateUserSettings,
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
setLocale(variables.data.language as Locale);
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [...settingQueries.all, 'listUser'],
|
||||||
|
});
|
||||||
|
toast.success(m.settings_messages_update_success(), {
|
||||||
|
richColors: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues,
|
||||||
|
validators: {
|
||||||
|
onSubmit: userSettingSchema,
|
||||||
|
onChange: userSettingSchema,
|
||||||
|
},
|
||||||
|
onSubmit: ({ value }) => {
|
||||||
|
updateMutation.mutate({ data: value as UserSettingInput });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.value?.language) {
|
||||||
|
setTimeout(() => {
|
||||||
|
form.setFieldValue('language', data.value.language);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}, [data, form]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="@container/card col-span-1 @xl/main:col-span-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl flex items-center gap-2">
|
||||||
|
<GearIcon size={20} />
|
||||||
|
{m.settings_ui_title()}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form
|
||||||
|
id="user-settings-form"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Field>
|
||||||
|
<Button type="submit" disabled={isLoading}>
|
||||||
|
{m.ui_update_btn()}
|
||||||
|
</Button>
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserSettingsForm;
|
||||||
@@ -17,11 +17,8 @@ export type BreadcrumbValue =
|
|||||||
const RouterBreadcrumb = () => {
|
const RouterBreadcrumb = () => {
|
||||||
const matches = useMatches()
|
const matches = useMatches()
|
||||||
|
|
||||||
console.log(matches);
|
|
||||||
|
|
||||||
const breadcrumbs = matches.flatMap((match) => {
|
const breadcrumbs = matches.flatMap((match) => {
|
||||||
const staticData = match.staticData;
|
const staticData = match.staticData;
|
||||||
console.log(staticData);
|
|
||||||
if (!staticData?.breadcrumb) return [];
|
if (!staticData?.breadcrumb) return [];
|
||||||
|
|
||||||
const breadcrumbValue =
|
const breadcrumbValue =
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import * as React from "react"
|
import { Select as SelectPrimitive } from 'radix-ui';
|
||||||
import { Select as SelectPrimitive } from "radix-ui"
|
import * as React from 'react';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
import { CaretDownIcon, CheckIcon, CaretUpIcon } from "@phosphor-icons/react"
|
import { CaretDownIcon, CaretUpIcon, CheckIcon } from '@phosphor-icons/react';
|
||||||
|
|
||||||
function Select({
|
function Select({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectGroup({
|
function SelectGroup({
|
||||||
@@ -17,33 +17,33 @@ function SelectGroup({
|
|||||||
return (
|
return (
|
||||||
<SelectPrimitive.Group
|
<SelectPrimitive.Group
|
||||||
data-slot="select-group"
|
data-slot="select-group"
|
||||||
className={cn("scroll-my-1 p-1", className)}
|
className={cn('scroll-my-1 p-1', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectValue({
|
function SelectValue({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectTrigger({
|
function SelectTrigger({
|
||||||
className,
|
className,
|
||||||
size = "default",
|
size = 'default',
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
size?: "sm" | "default"
|
size?: 'sm' | 'default';
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
data-slot="select-trigger"
|
data-slot="select-trigger"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-input data-[placeholder]:text-muted-foreground bg-input/20 dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-md border px-2 py-1.5 text-xs/relaxed transition-colors focus-visible:ring-[2px] aria-invalid:ring-[2px] data-[size=default]:h-7 data-[size=sm]:h-6 *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-3.5 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"border-input data-placeholder:text-muted-foreground bg-input/20 dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-md border px-2 py-1.5 text-xs/relaxed transition-colors focus-visible:ring-2 aria-invalid:ring-2 data-[size=default]:h-7 data-[size=sm]:h-6 *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-3.5 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -52,21 +52,26 @@ function SelectTrigger({
|
|||||||
<CaretDownIcon className="text-muted-foreground size-3.5 pointer-events-none" />
|
<CaretDownIcon className="text-muted-foreground size-3.5 pointer-events-none" />
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Icon>
|
||||||
</SelectPrimitive.Trigger>
|
</SelectPrimitive.Trigger>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectContent({
|
function SelectContent({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
position = "item-aligned",
|
position = 'item-aligned',
|
||||||
align = "center",
|
align = 'center',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
data-slot="select-content"
|
data-slot="select-content"
|
||||||
className={cn("bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-32 rounded-lg shadow-md ring-1 duration-100 relative z-50 max-h-(--radix-select-content-available-height) origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
|
className={cn(
|
||||||
|
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-32 rounded-lg shadow-md ring-1 duration-100 relative z-50 max-h-(--radix-select-content-available-height) origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto',
|
||||||
|
position === 'popper' &&
|
||||||
|
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
position={position}
|
position={position}
|
||||||
align={align}
|
align={align}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -75,8 +80,8 @@ function SelectContent({
|
|||||||
<SelectPrimitive.Viewport
|
<SelectPrimitive.Viewport
|
||||||
data-position={position}
|
data-position={position}
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[position=popper]:h-[var(--radix-select-trigger-height)] data-[position=popper]:w-full data-[position=popper]:min-w-[var(--radix-select-trigger-width)]",
|
'data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)',
|
||||||
position === "popper" && ""
|
position === 'popper' && '',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -84,7 +89,7 @@ function SelectContent({
|
|||||||
<SelectScrollDownButton />
|
<SelectScrollDownButton />
|
||||||
</SelectPrimitive.Content>
|
</SelectPrimitive.Content>
|
||||||
</SelectPrimitive.Portal>
|
</SelectPrimitive.Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectLabel({
|
function SelectLabel({
|
||||||
@@ -94,10 +99,10 @@ function SelectLabel({
|
|||||||
return (
|
return (
|
||||||
<SelectPrimitive.Label
|
<SelectPrimitive.Label
|
||||||
data-slot="select-label"
|
data-slot="select-label"
|
||||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectItem({
|
function SelectItem({
|
||||||
@@ -109,8 +114,8 @@ function SelectItem({
|
|||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
data-slot="select-item"
|
data-slot="select-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground min-h-7 gap-2 rounded-md px-2 py-1 text-xs/relaxed [&_svg:not([class*='size-'])]:size-3.5 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground min-h-7 gap-2 rounded-md px-2 py-1 text-xs/relaxed [&_svg:not([class*='size-'])]:size-3.5 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -121,7 +126,7 @@ function SelectItem({
|
|||||||
</span>
|
</span>
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
</SelectPrimitive.Item>
|
</SelectPrimitive.Item>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectSeparator({
|
function SelectSeparator({
|
||||||
@@ -131,10 +136,13 @@ function SelectSeparator({
|
|||||||
return (
|
return (
|
||||||
<SelectPrimitive.Separator
|
<SelectPrimitive.Separator
|
||||||
data-slot="select-separator"
|
data-slot="select-separator"
|
||||||
className={cn("bg-border/50 -mx-1 my-1 h-px pointer-events-none", className)}
|
className={cn(
|
||||||
|
'bg-border/50 -mx-1 my-1 h-px pointer-events-none',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectScrollUpButton({
|
function SelectScrollUpButton({
|
||||||
@@ -144,13 +152,15 @@ function SelectScrollUpButton({
|
|||||||
return (
|
return (
|
||||||
<SelectPrimitive.ScrollUpButton
|
<SelectPrimitive.ScrollUpButton
|
||||||
data-slot="select-scroll-up-button"
|
data-slot="select-scroll-up-button"
|
||||||
className={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-3.5", className)}
|
className={cn(
|
||||||
|
"bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<CaretUpIcon
|
<CaretUpIcon />
|
||||||
/>
|
|
||||||
</SelectPrimitive.ScrollUpButton>
|
</SelectPrimitive.ScrollUpButton>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectScrollDownButton({
|
function SelectScrollDownButton({
|
||||||
@@ -160,13 +170,15 @@ function SelectScrollDownButton({
|
|||||||
return (
|
return (
|
||||||
<SelectPrimitive.ScrollDownButton
|
<SelectPrimitive.ScrollDownButton
|
||||||
data-slot="select-scroll-down-button"
|
data-slot="select-scroll-down-button"
|
||||||
className={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-3.5", className)}
|
className={cn(
|
||||||
|
"bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<CaretDownIcon
|
<CaretDownIcon />
|
||||||
/>
|
|
||||||
</SelectPrimitive.ScrollDownButton>
|
</SelectPrimitive.ScrollDownButton>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -180,4 +192,4 @@ export {
|
|||||||
SelectSeparator,
|
SelectSeparator,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|||||||
<textarea
|
<textarea
|
||||||
data-slot="textarea"
|
data-slot="textarea"
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-input bg-input/20 dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 resize-none rounded-md border px-2 py-2 text-sm transition-colors focus-visible:ring-[2px] aria-invalid:ring-[2px] md:text-xs/relaxed placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
'border-input bg-input/20 dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 resize-none rounded-md border px-2 py-2 text-sm transition-colors focus-visible:ring-2 aria-invalid:ring-2 md:text-xs/relaxed placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Textarea }
|
export { Textarea }
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import UserSettingsForm from '@/components/form/user-settings-form';
|
||||||
import { m } from '@/paraglide/messages';
|
import { m } from '@/paraglide/messages';
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ function RouteComponent() {
|
|||||||
return (
|
return (
|
||||||
<div className="@container/main flex flex-1 flex-col gap-2 p-4">
|
<div className="@container/main flex flex-1 flex-col gap-2 p-4">
|
||||||
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card *:data-[slot=card]:bg-linear-to-br *:data-[slot=card]:shadow-xs grid grid-cols-1 @xl/main:grid-cols-2 @5xl/main:grid-cols-3 gap-4">
|
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card *:data-[slot=card]:bg-linear-to-br *:data-[slot=card]:shadow-xs grid grid-cols-1 @xl/main:grid-cols-2 @5xl/main:grid-cols-3 gap-4">
|
||||||
abc
|
<UserSettingsForm />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { getSession } from '@/lib/auth/session';
|
import { getSession } from '@/lib/auth/session';
|
||||||
// import { sessionPush } from '@/lib/auth/session';
|
// import { sessionPush } from '@/lib/auth/session';
|
||||||
import { queryOptions } from '@tanstack/react-query';
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
import { getSettings } from './setting.api';
|
import { getAdminSettings, getUserSettings } from './setting.api';
|
||||||
|
|
||||||
export const sessionQueries = {
|
export const sessionQueries = {
|
||||||
all: ['auth'],
|
all: ['auth'],
|
||||||
@@ -17,9 +17,14 @@ export const sessionQueries = {
|
|||||||
|
|
||||||
export const settingQueries = {
|
export const settingQueries = {
|
||||||
all: ['setting'],
|
all: ['setting'],
|
||||||
list: () =>
|
listAdmin: () =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: [...settingQueries.all, 'list'],
|
queryKey: [...settingQueries.all, 'list'],
|
||||||
queryFn: () => getSettings(),
|
queryFn: () => getAdminSettings(),
|
||||||
|
}),
|
||||||
|
listUser: () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: [...settingQueries.all, 'listUser'],
|
||||||
|
queryFn: () => getUserSettings(),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +1,16 @@
|
|||||||
import { prisma } from '@/db';
|
import { prisma } from '@/db';
|
||||||
import { Setting } from '@/generated/prisma/client';
|
import { Setting } from '@/generated/prisma/client';
|
||||||
import { authMiddleware } from '@/lib/middleware';
|
import { authMiddleware } from '@/lib/middleware';
|
||||||
import { createIsomorphicFn, createServerFn } from '@tanstack/react-start';
|
import { createServerFn } from '@tanstack/react-start';
|
||||||
import { settingSchema } from './setting.schema';
|
import { settingSchema, userSettingSchema } from './setting.schema';
|
||||||
// import { settingSchema } from './setting.schema';
|
// import { settingSchema } from './setting.schema';
|
||||||
|
|
||||||
export type SettingReturn = {
|
type AdminSettingReturn = {
|
||||||
[key: string]: Setting;
|
[key: string]: Setting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLanguage = createIsomorphicFn().server(async () => {
|
// Settings for admin
|
||||||
const language = await prisma.setting.findUnique({
|
export const getAdminSettings = createServerFn({ method: 'GET' })
|
||||||
where: {
|
|
||||||
key: 'site_language',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return language?.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getSettings = createServerFn({ method: 'GET' })
|
|
||||||
.middleware([authMiddleware])
|
.middleware([authMiddleware])
|
||||||
.handler(async () => {
|
.handler(async () => {
|
||||||
const settings = await prisma.setting.findMany({
|
const settings = await prisma.setting.findMany({
|
||||||
@@ -28,7 +19,7 @@ export const getSettings = createServerFn({ method: 'GET' })
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const results: SettingReturn = {};
|
const results: AdminSettingReturn = {};
|
||||||
|
|
||||||
settings.forEach((setting) => {
|
settings.forEach((setting) => {
|
||||||
results[setting.key] = setting;
|
results[setting.key] = setting;
|
||||||
@@ -37,7 +28,7 @@ export const getSettings = createServerFn({ method: 'GET' })
|
|||||||
return results;
|
return results;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateSettings = createServerFn({ method: 'POST' })
|
export const updateAdminSettings = createServerFn({ method: 'POST' })
|
||||||
.inputValidator(settingSchema)
|
.inputValidator(settingSchema)
|
||||||
.middleware([authMiddleware])
|
.middleware([authMiddleware])
|
||||||
.handler(async ({ data }) => {
|
.handler(async ({ data }) => {
|
||||||
@@ -59,3 +50,51 @@ export const updateSettings = createServerFn({ method: 'POST' })
|
|||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Setting for user
|
||||||
|
type UserSetting = {
|
||||||
|
language: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserSettings = createServerFn({ method: 'GET' })
|
||||||
|
.middleware([authMiddleware])
|
||||||
|
.handler(async ({ context }) => {
|
||||||
|
try {
|
||||||
|
const settings = await prisma.setting.findUniqueOrThrow({
|
||||||
|
where: {
|
||||||
|
relation: 'user',
|
||||||
|
key: context.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
settings: settings as Setting,
|
||||||
|
value: JSON.parse(settings.value) as UserSetting,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateUserSettings = createServerFn({ method: 'POST' })
|
||||||
|
.inputValidator(userSettingSchema)
|
||||||
|
.middleware([authMiddleware])
|
||||||
|
.handler(async ({ data, context }) => {
|
||||||
|
// Update each setting
|
||||||
|
try {
|
||||||
|
await prisma.setting.upsert({
|
||||||
|
where: { key: context.user.id },
|
||||||
|
update: { value: JSON.stringify(data) },
|
||||||
|
create: {
|
||||||
|
key: context.user.id,
|
||||||
|
value: JSON.stringify(data),
|
||||||
|
description: 'User settings', // or provide proper descriptions
|
||||||
|
relation: 'user',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -20,3 +20,13 @@ export const settingSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type SettingsInput = z.infer<typeof settingSchema>;
|
export type SettingsInput = z.infer<typeof settingSchema>;
|
||||||
|
|
||||||
|
export const userSettingSchema = z.object({
|
||||||
|
language: z.string().nonempty(
|
||||||
|
m.common_is_required({
|
||||||
|
field: m.settings_form_language(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UserSettingInput = z.infer<typeof userSettingSchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user