diff --git a/prisma/seed.ts b/prisma/seed.ts
index 777c947..a4f34eb 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -38,7 +38,7 @@ async function main() {
...settingsData,
{
key: admin ? (admin?.user?.id as string) : (mailExists?.id as string),
- value: 'en',
+ value: '{ "language": "en" }',
description: 'User Settings',
relation: 'user',
},
diff --git a/src/components/form/settings-form.tsx b/src/components/form/settings-form.tsx
index f88306c..8e05e37 100644
--- a/src/components/form/settings-form.tsx
+++ b/src/components/form/settings-form.tsx
@@ -1,6 +1,6 @@
import { m } from '@/paraglide/messages';
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 { GearIcon } from '@phosphor-icons/react';
import { useForm } from '@tanstack/react-form';
@@ -10,6 +10,7 @@ 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 { Skeleton } from '../ui/skeleton';
import { Textarea } from '../ui/textarea';
const defaultValues: SettingsInput = {
@@ -21,13 +22,15 @@ const defaultValues: SettingsInput = {
const SettingsForm = () => {
const queryClient = useQueryClient();
- const { data: settings } = useQuery(settingQueries.list());
+ const { data: settings, isLoading } = useQuery(settingQueries.listAdmin());
const updateMutation = useMutation({
- mutationFn: updateSettings,
+ mutationFn: updateAdminSettings,
onSuccess: () => {
// 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(), {
richColors: true,
});
@@ -50,6 +53,9 @@ const SettingsForm = () => {
},
});
+ if (isLoading)
+ return ;
+
return (
@@ -145,37 +151,6 @@ const SettingsForm = () => {
);
}}
/>
- {/* {
- const isInvalid =
- field.state.meta.isTouched && !field.state.meta.isValid;
- return (
-
-
- {m.settings_form_language()}
-
-
- {isInvalid && (
-
- )}
-
- );
- }}
- /> */}
diff --git a/src/components/form/user-settings-form.tsx b/src/components/form/user-settings-form.tsx
new file mode 100644
index 0000000..d2f6330
--- /dev/null
+++ b/src/components/form/user-settings-form.tsx
@@ -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 (
+
+
+
+
+ {m.settings_ui_title()}
+
+
+
+
+
+
+ );
+};
+
+export default UserSettingsForm;
diff --git a/src/components/sidebar/RouterBreadcrumb.tsx b/src/components/sidebar/RouterBreadcrumb.tsx
index 1cad4b0..37c6a4d 100644
--- a/src/components/sidebar/RouterBreadcrumb.tsx
+++ b/src/components/sidebar/RouterBreadcrumb.tsx
@@ -17,11 +17,8 @@ export type BreadcrumbValue =
const RouterBreadcrumb = () => {
const matches = useMatches()
- console.log(matches);
-
const breadcrumbs = matches.flatMap((match) => {
const staticData = match.staticData;
- console.log(staticData);
if (!staticData?.breadcrumb) return [];
const breadcrumbValue =
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
index cba1d26..7641172 100644
--- a/src/components/ui/select.tsx
+++ b/src/components/ui/select.tsx
@@ -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 { CaretDownIcon, CheckIcon, CaretUpIcon } from "@phosphor-icons/react"
+import { cn } from '@/lib/utils';
+import { CaretDownIcon, CaretUpIcon, CheckIcon } from '@phosphor-icons/react';
function Select({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function SelectGroup({
@@ -17,33 +17,33 @@ function SelectGroup({
return (
- )
+ );
}
function SelectValue({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function SelectTrigger({
className,
- size = "default",
+ size = 'default',
children,
...props
}: React.ComponentProps & {
- size?: "sm" | "default"
+ size?: 'sm' | 'default';
}) {
return (
@@ -52,21 +52,26 @@ function SelectTrigger({
- )
+ );
}
function SelectContent({
className,
children,
- position = "item-aligned",
- align = "center",
+ position = 'item-aligned',
+ align = 'center',
...props
}: React.ComponentProps) {
return (
{children}
@@ -84,7 +89,7 @@ function SelectContent({
- )
+ );
}
function SelectLabel({
@@ -94,10 +99,10 @@ function SelectLabel({
return (
- )
+ );
}
function SelectItem({
@@ -109,8 +114,8 @@ function SelectItem({
@@ -121,7 +126,7 @@ function SelectItem({
{children}
- )
+ );
}
function SelectSeparator({
@@ -131,10 +136,13 @@ function SelectSeparator({
return (
- )
+ );
}
function SelectScrollUpButton({
@@ -144,13 +152,15 @@ function SelectScrollUpButton({
return (
-
+
- )
+ );
}
function SelectScrollDownButton({
@@ -160,13 +170,15 @@ function SelectScrollDownButton({
return (
-
+
- )
+ );
}
export {
@@ -180,4 +192,4 @@ export {
SelectSeparator,
SelectTrigger,
SelectValue,
-}
+};
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
index d8c3757..753b5af 100644
--- a/src/components/ui/textarea.tsx
+++ b/src/components/ui/textarea.tsx
@@ -7,12 +7,12 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
- )
+ );
}
export { Textarea }
diff --git a/src/routes/(app)/(auth)/account/settings.tsx b/src/routes/(app)/(auth)/account/settings.tsx
index ce2c0c7..9257ca8 100644
--- a/src/routes/(app)/(auth)/account/settings.tsx
+++ b/src/routes/(app)/(auth)/account/settings.tsx
@@ -1,3 +1,4 @@
+import UserSettingsForm from '@/components/form/user-settings-form';
import { m } from '@/paraglide/messages';
import { createFileRoute } from '@tanstack/react-router';
@@ -10,7 +11,7 @@ function RouteComponent() {
return (
);
diff --git a/src/service/queries.ts b/src/service/queries.ts
index 4ec9d0c..54bb7d5 100644
--- a/src/service/queries.ts
+++ b/src/service/queries.ts
@@ -1,7 +1,7 @@
import { getSession } from '@/lib/auth/session';
// import { sessionPush } from '@/lib/auth/session';
import { queryOptions } from '@tanstack/react-query';
-import { getSettings } from './setting.api';
+import { getAdminSettings, getUserSettings } from './setting.api';
export const sessionQueries = {
all: ['auth'],
@@ -17,9 +17,14 @@ export const sessionQueries = {
export const settingQueries = {
all: ['setting'],
- list: () =>
+ listAdmin: () =>
queryOptions({
queryKey: [...settingQueries.all, 'list'],
- queryFn: () => getSettings(),
+ queryFn: () => getAdminSettings(),
+ }),
+ listUser: () =>
+ queryOptions({
+ queryKey: [...settingQueries.all, 'listUser'],
+ queryFn: () => getUserSettings(),
}),
};
diff --git a/src/service/setting.api.ts b/src/service/setting.api.ts
index 54155e5..679654f 100644
--- a/src/service/setting.api.ts
+++ b/src/service/setting.api.ts
@@ -1,25 +1,16 @@
import { prisma } from '@/db';
import { Setting } from '@/generated/prisma/client';
import { authMiddleware } from '@/lib/middleware';
-import { createIsomorphicFn, createServerFn } from '@tanstack/react-start';
-import { settingSchema } from './setting.schema';
+import { createServerFn } from '@tanstack/react-start';
+import { settingSchema, userSettingSchema } from './setting.schema';
// import { settingSchema } from './setting.schema';
-export type SettingReturn = {
+type AdminSettingReturn = {
[key: string]: Setting;
};
-export const getLanguage = createIsomorphicFn().server(async () => {
- const language = await prisma.setting.findUnique({
- where: {
- key: 'site_language',
- },
- });
-
- return language?.value;
-});
-
-export const getSettings = createServerFn({ method: 'GET' })
+// Settings for admin
+export const getAdminSettings = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.handler(async () => {
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) => {
results[setting.key] = setting;
@@ -37,7 +28,7 @@ export const getSettings = createServerFn({ method: 'GET' })
return results;
});
-export const updateSettings = createServerFn({ method: 'POST' })
+export const updateAdminSettings = createServerFn({ method: 'POST' })
.inputValidator(settingSchema)
.middleware([authMiddleware])
.handler(async ({ data }) => {
@@ -59,3 +50,51 @@ export const updateSettings = createServerFn({ method: 'POST' })
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;
+ }
+ });
diff --git a/src/service/setting.schema.ts b/src/service/setting.schema.ts
index c1e71dc..75cfe4a 100644
--- a/src/service/setting.schema.ts
+++ b/src/service/setting.schema.ts
@@ -20,3 +20,13 @@ export const settingSchema = z.object({
});
export type SettingsInput = z.infer;
+
+export const userSettingSchema = z.object({
+ language: z.string().nonempty(
+ m.common_is_required({
+ field: m.settings_form_language(),
+ }),
+ ),
+});
+
+export type UserSettingInput = z.infer;