mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 15:35:55 +01:00
✨ Add procedure for updating user settings
This commit is contained in:
@@ -1,76 +1,25 @@
|
|||||||
import { Alert, Stack, Switch } from '@mantine/core';
|
import { Alert, Stack, Switch } from '@mantine/core';
|
||||||
import { IconInfoCircle } from '@tabler/icons-react';
|
import { IconInfoCircle } from '@tabler/icons-react';
|
||||||
import { BaseSyntheticEvent } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useFormContext } from '~/pages/user/preferences';
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
|
||||||
import { useConfigStore } from '../../../../config/store';
|
|
||||||
|
|
||||||
export const AccessibilitySettings = () => {
|
export const AccessibilitySettings = () => {
|
||||||
const { t } = useTranslation('settings/customization/accessibility');
|
const { t } = useTranslation('settings/customization/accessibility');
|
||||||
const { updateConfig } = useConfigStore();
|
|
||||||
const { config, name: configName } = useConfigContext();
|
const form = useFormContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Switch
|
<Switch
|
||||||
label={t('disablePulse.label')}
|
label={t('disablePulse.label')}
|
||||||
description={t('disablePulse.description')}
|
description={t('disablePulse.description')}
|
||||||
defaultChecked={config?.settings.customization.accessibility?.disablePingPulse ?? false}
|
{...form.getInputProps('disablePingPulse')}
|
||||||
onChange={(value: BaseSyntheticEvent) => {
|
|
||||||
if (!configName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateConfig(
|
|
||||||
configName,
|
|
||||||
(previousConfig) => ({
|
|
||||||
...previousConfig,
|
|
||||||
settings: {
|
|
||||||
...previousConfig.settings,
|
|
||||||
customization: {
|
|
||||||
...previousConfig.settings.customization,
|
|
||||||
accessibility: {
|
|
||||||
...previousConfig.settings.customization.accessibility,
|
|
||||||
disablePingPulse: value.target.checked,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
false,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
label={t('replaceIconsWithDots.label')}
|
label={t('replaceIconsWithDots.label')}
|
||||||
description={t('replaceIconsWithDots.description')}
|
description={t('replaceIconsWithDots.description')}
|
||||||
defaultChecked={config?.settings.customization.accessibility?.disablePingPulse ?? false}
|
{...form.getInputProps('replaceDotsWithIcons')}
|
||||||
onChange={(value: BaseSyntheticEvent) => {
|
|
||||||
if (!configName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateConfig(
|
|
||||||
configName,
|
|
||||||
(previousConfig) => ({
|
|
||||||
...previousConfig,
|
|
||||||
settings: {
|
|
||||||
...previousConfig.settings,
|
|
||||||
customization: {
|
|
||||||
...previousConfig.settings.customization,
|
|
||||||
accessibility: {
|
|
||||||
...previousConfig.settings.customization.accessibility,
|
|
||||||
replacePingDotsWithIcons: value.target.checked,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
false,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Alert icon={<IconInfoCircle size="1rem" />} color="blue">
|
<Alert icon={<IconInfoCircle size="1rem" />} color="blue">
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import { Group, Select, Stack, Text, Title } from '@mantine/core';
|
import { Button, Group, Select, Stack, Text, Title } from '@mantine/core';
|
||||||
import Head from 'next/head';
|
import { createFormContext } from '@mantine/form';
|
||||||
|
import type { InferGetServerSidePropsType } from 'next';
|
||||||
|
import { GetServerSidePropsContext } from 'next';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
|
import { z } from 'zod';
|
||||||
import { AccessibilitySettings } from '~/components/Settings/Customization/Accessibility/AccessibilitySettings';
|
import { AccessibilitySettings } from '~/components/Settings/Customization/Accessibility/AccessibilitySettings';
|
||||||
import { MainLayout } from '~/components/layout/admin/main-admin.layout';
|
import { MainLayout } from '~/components/layout/admin/main-admin.layout';
|
||||||
import { CommonHeader } from '~/components/layout/common-header';
|
import { CommonHeader } from '~/components/layout/common-header';
|
||||||
import { languages } from '~/tools/language';
|
import { languages } from '~/tools/language';
|
||||||
|
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||||
|
import { RouterOutputs, api } from '~/utils/api';
|
||||||
|
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||||
|
import { updateSettingsValidationSchema } from '~/validations/user';
|
||||||
|
|
||||||
|
const PreferencesPage = ({ locale }: InferGetServerSidePropsType<typeof getServerSideProps>) => {
|
||||||
|
const { data } = api.user.getWithSettings.useQuery();
|
||||||
|
|
||||||
const PreferencesPage = () => {
|
|
||||||
const data = languages.map((language) => ({
|
|
||||||
image: 'https://img.icons8.com/clouds/256/000000/futurama-bender.png',
|
|
||||||
label: language.originalName,
|
|
||||||
description: language.translatedName,
|
|
||||||
value: language.shortName,
|
|
||||||
country: language.country,
|
|
||||||
}));
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<CommonHeader>
|
<CommonHeader>
|
||||||
@@ -21,43 +23,94 @@ const PreferencesPage = () => {
|
|||||||
</CommonHeader>
|
</CommonHeader>
|
||||||
<Title mb="xl">Preferences</Title>
|
<Title mb="xl">Preferences</Title>
|
||||||
|
|
||||||
<Stack spacing={5}>
|
{data && <SettingsComponent settings={data.settings} />}
|
||||||
<Title order={2} size="lg">
|
|
||||||
Localization
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
label="Language"
|
|
||||||
itemComponent={SelectItem}
|
|
||||||
data={data}
|
|
||||||
searchable
|
|
||||||
maxDropdownHeight={400}
|
|
||||||
filter={(value, item) =>
|
|
||||||
item.label.toLowerCase().includes(value.toLowerCase().trim()) ||
|
|
||||||
item.description.toLowerCase().includes(value.toLowerCase().trim())
|
|
||||||
}
|
|
||||||
withAsterisk
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
label="First day of the week"
|
|
||||||
data={[
|
|
||||||
{ value: 'monday', label: 'Monday' },
|
|
||||||
{ value: 'sunday', label: 'Sunday' },
|
|
||||||
{ value: 'saturday', label: 'Saturday' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Title order={2} size="lg" mt="lg" mb="md">
|
|
||||||
Accessibility
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
<AccessibilitySettings />
|
|
||||||
</Stack>
|
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const [FormProvider, useFormContext, useForm] =
|
||||||
|
createFormContext<z.infer<typeof updateSettingsValidationSchema>>();
|
||||||
|
|
||||||
|
const SettingsComponent = ({
|
||||||
|
settings,
|
||||||
|
}: {
|
||||||
|
settings: RouterOutputs['user']['getWithSettings']['settings'];
|
||||||
|
}) => {
|
||||||
|
const languagesData = languages.map((language) => ({
|
||||||
|
image: 'https://img.icons8.com/clouds/256/000000/futurama-bender.png',
|
||||||
|
label: language.originalName,
|
||||||
|
description: language.translatedName,
|
||||||
|
value: language.shortName,
|
||||||
|
country: language.country,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { i18nZodResolver } = useI18nZodResolver();
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
disablePingPulse: settings.disablePingPulse,
|
||||||
|
replaceDotsWithIcons: settings.replacePingWithIcons,
|
||||||
|
language: settings.language,
|
||||||
|
},
|
||||||
|
validate: i18nZodResolver(updateSettingsValidationSchema),
|
||||||
|
validateInputOnBlur: true,
|
||||||
|
validateInputOnChange: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate } = api.user.updateSettings.useMutation();
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
mutate(form.values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider form={form}>
|
||||||
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
|
<Stack spacing={5}>
|
||||||
|
<Title order={2} size="lg">
|
||||||
|
Localization
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Language"
|
||||||
|
itemComponent={SelectItem}
|
||||||
|
data={languagesData}
|
||||||
|
searchable
|
||||||
|
maxDropdownHeight={400}
|
||||||
|
filter={(value, item) =>
|
||||||
|
item.label.toLowerCase().includes(value.toLowerCase().trim()) ||
|
||||||
|
item.description.toLowerCase().includes(value.toLowerCase().trim())
|
||||||
|
}
|
||||||
|
defaultValue={settings.language}
|
||||||
|
withAsterisk
|
||||||
|
mb="xs"
|
||||||
|
{...form.getInputProps('language')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="First day of the week"
|
||||||
|
data={[
|
||||||
|
{ value: 'monday', label: 'Monday' },
|
||||||
|
{ value: 'sunday', label: 'Sunday' },
|
||||||
|
{ value: 'saturday', label: 'Saturday' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Title order={2} size="lg" mt="lg" mb="md">
|
||||||
|
Accessibility
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<AccessibilitySettings />
|
||||||
|
|
||||||
|
<Button type="submit" fullWidth mt="md">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
|
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||||
image: string;
|
image: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -82,4 +135,14 @@ const SelectItem = forwardRef<HTMLDivElement, ItemProps>(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export async function getServerSideProps({ req, res, locale }: GetServerSidePropsContext) {
|
||||||
|
const translations = await getServerSideTranslations([], locale, undefined, undefined);
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
...translations,
|
||||||
|
locale: locale,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default PreferencesPage;
|
export default PreferencesPage;
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { TRPCError } from '@trpc/server';
|
|||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { hashPassword } from '~/utils/security';
|
import { hashPassword } from '~/utils/security';
|
||||||
import { colorSchemeParser, signUpFormSchema } from '~/validations/user';
|
import {
|
||||||
|
colorSchemeParser,
|
||||||
|
signUpFormSchema,
|
||||||
|
updateSettingsValidationSchema,
|
||||||
|
} from '~/validations/user';
|
||||||
|
|
||||||
import { COOKIE_COLOR_SCHEME_KEY, COOKIE_LOCALE_KEY } from '../../../../data/constants';
|
import { COOKIE_COLOR_SCHEME_KEY, COOKIE_LOCALE_KEY } from '../../../../data/constants';
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
||||||
@@ -134,6 +138,25 @@ export const userRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
updateSettings: protectedProcedure
|
||||||
|
.input(updateSettingsValidationSchema)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
await ctx.prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: ctx.session.user.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
settings: {
|
||||||
|
update: {
|
||||||
|
disablePingPulse: input.disablePingPulse,
|
||||||
|
replacePingWithIcons: input.replaceDotsWithIcons,
|
||||||
|
language: input.language,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
getAll: publicProcedure
|
getAll: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -183,7 +206,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
salt: salt,
|
salt: salt,
|
||||||
settings: {
|
settings: {
|
||||||
create: {}
|
create: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,3 +23,9 @@ export const colorSchemeParser = z
|
|||||||
.enum(['light', 'dark', 'environment'])
|
.enum(['light', 'dark', 'environment'])
|
||||||
.default('environment')
|
.default('environment')
|
||||||
.catch('environment');
|
.catch('environment');
|
||||||
|
|
||||||
|
export const updateSettingsValidationSchema = z.object({
|
||||||
|
disablePingPulse: z.boolean(),
|
||||||
|
replaceDotsWithIcons: z.boolean(),
|
||||||
|
language: z.string(),
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user