Added settings for user #5

Merged
sam merged 1 commits from feature/account into main 2026-01-08 10:56:28 +00:00
10 changed files with 267 additions and 97 deletions

View File

@@ -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',
}, },

View File

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

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

View File

@@ -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 =

View File

@@ -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,
} };

View File

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

View File

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

View File

@@ -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(),
}), }),
}; };

View File

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

View File

@@ -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>;