Merge branch 'feature/add-basic-authentication' of https://github.com/ajnart/homarr into feature/add-basic-authentication

This commit is contained in:
Meier Lukas
2023-07-31 00:02:18 +02:00
4 changed files with 142 additions and 101 deletions

View File

@@ -1,76 +1,25 @@
import { Alert, Stack, Switch } from '@mantine/core';
import { IconInfoCircle } from '@tabler/icons-react';
import { BaseSyntheticEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store';
import { useFormContext } from '~/pages/user/preferences';
export const AccessibilitySettings = () => {
const { t } = useTranslation('settings/customization/accessibility');
const { updateConfig } = useConfigStore();
const { config, name: configName } = useConfigContext();
const form = useFormContext();
return (
<Stack>
<Switch
label={t('disablePulse.label')}
description={t('disablePulse.description')}
defaultChecked={config?.settings.customization.accessibility?.disablePingPulse ?? false}
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
);
}}
{...form.getInputProps('disablePingPulse')}
/>
<Switch
label={t('replaceIconsWithDots.label')}
description={t('replaceIconsWithDots.description')}
defaultChecked={config?.settings.customization.accessibility?.disablePingPulse ?? false}
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
);
}}
{...form.getInputProps('replaceDotsWithIcons')}
/>
<Alert icon={<IconInfoCircle size="1rem" />} color="blue">

View File

@@ -1,19 +1,21 @@
import { Group, Select, Stack, Text, Title } from '@mantine/core';
import Head from 'next/head';
import { Button, Group, Select, Stack, Text, Title } from '@mantine/core';
import { createFormContext } from '@mantine/form';
import type { InferGetServerSidePropsType } from 'next';
import { GetServerSidePropsContext } from 'next';
import { forwardRef } from 'react';
import { z } from 'zod';
import { AccessibilitySettings } from '~/components/Settings/Customization/Accessibility/AccessibilitySettings';
import { MainLayout } from '~/components/layout/admin/main-admin.layout';
import { CommonHeader } from '~/components/layout/common-header';
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 (
<MainLayout>
<CommonHeader>
@@ -21,6 +23,49 @@ const PreferencesPage = () => {
</CommonHeader>
<Title mb="xl">Preferences</Title>
{data && <SettingsComponent settings={data.settings} />}
</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
@@ -29,14 +74,17 @@ const PreferencesPage = () => {
<Select
label="Language"
itemComponent={SelectItem}
data={data}
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
@@ -53,8 +101,13 @@ const PreferencesPage = () => {
</Title>
<AccessibilitySettings />
<Button type="submit" fullWidth mt="md">
Save
</Button>
</Stack>
</MainLayout>
</form>
</FormProvider>
);
};
@@ -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;

View File

@@ -2,7 +2,11 @@ import { TRPCError } from '@trpc/server';
import bcrypt from 'bcrypt';
import { z } from 'zod';
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 { 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
.input(
z.object({
@@ -183,7 +206,7 @@ export const userRouter = createTRPCRouter({
password: hashedPassword,
salt: salt,
settings: {
create: {}
create: {},
},
},
});

View File

@@ -23,3 +23,9 @@ export const colorSchemeParser = z
.enum(['light', 'dark', 'environment'])
.default('environment')
.catch('environment');
export const updateSettingsValidationSchema = z.object({
disablePingPulse: z.boolean(),
replaceDotsWithIcons: z.boolean(),
language: z.string(),
});