Add user settings, improve color scheme

This commit is contained in:
Meier Lukas
2023-07-29 11:19:40 +02:00
parent d8562e2990
commit ed76afbce8
8 changed files with 129 additions and 31 deletions

View File

@@ -42,16 +42,17 @@ model Session {
}
model User {
id String @id @default(cuid())
id String @id @default(cuid())
name String?
email String? @unique
email String? @unique
emailVerified DateTime?
image String?
password String?
salt String?
isAdmin Boolean @default(false)
isAdmin Boolean @default(false)
accounts Account[]
sessions Session[]
settings UserSettings?
}
model VerificationToken {
@@ -67,3 +68,18 @@ model RegistrationToken {
token String @unique
expires DateTime
}
model UserSettings {
id String @id @default(cuid())
userId String
colorScheme String @default("environment") // environment, light, dark
language String @default("en")
searchTemplate String @default("https://google.com/search?q=%s")
openSearchInNewTab Boolean @default(true)
disablePingPulse Boolean @default(false)
replacePingWithIcons Boolean @default(false)
useDebugLanguage Boolean @default(false)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId])
}

View File

@@ -35,7 +35,6 @@ export function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: str
</Menu.Target>
<Menu.Dropdown>
<ColorSchemeSwitch />
<Menu.Divider />
{!editModeEnabled && (
<Menu.Item icon={<IconSettings strokeWidth={1.2} size={18} />} onClick={drawer.open}>
{t('sections.settings')}

View File

@@ -0,0 +1,27 @@
import { ColorScheme } from '@mantine/core';
import { useHotkeys } from '@mantine/hooks';
import { setCookie } from 'cookies-next';
import { Session } from 'next-auth';
import { useState } from 'react';
import { api } from '~/utils/api';
export const useColorScheme = (defaultValue: ColorScheme, session: Session) => {
const [colorScheme, setColorScheme] = useState(defaultValue);
const { mutateAsync } = api.user.changeColorScheme.useMutation();
const toggleColorScheme = async () => {
const newColorScheme = colorScheme === 'dark' ? 'light' : 'dark';
setColorScheme(newColorScheme);
setCookie('color-scheme', newColorScheme);
if (session && new Date(session.expires) > new Date()) {
await mutateAsync({ colorScheme: newColorScheme });
}
};
useHotkeys([['mod+J', () => void toggleColorScheme()]]);
return {
colorScheme,
toggleColorScheme,
};
};

View File

@@ -1,5 +1,4 @@
import { ColorScheme, ColorSchemeProvider, MantineProvider, MantineTheme } from '@mantine/core';
import { useColorScheme, useHotkeys, useLocalStorage } from '@mantine/hooks';
import { ModalsProvider } from '@mantine/modals';
import { Notifications } from '@mantine/notifications';
import AsyncStorage from '@react-native-async-storage/async-storage';
@@ -7,7 +6,7 @@ import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persi
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import Consola from 'consola';
import { getCookie } from 'cookies-next';
import { getCookie, setCookie } from 'cookies-next';
import { GetServerSidePropsContext } from 'next';
import { Session } from 'next-auth';
import { SessionProvider, getSession } from 'next-auth/react';
@@ -17,8 +16,10 @@ import Head from 'next/head';
import { useEffect, useState } from 'react';
import 'video.js/dist/video-js.css';
import { env } from '~/env.js';
import { useColorScheme } from '~/hooks/use-colorscheme';
import { ConfigType } from '~/types/config';
import { api } from '~/utils/api';
import { colorSchemeParser } from '~/validations/user';
import nextI18nextConfig from '../../next-i18next.config.js';
import { ChangeAppPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal';
@@ -46,7 +47,6 @@ function App(
colorScheme: ColorScheme;
packageAttributes: ServerSidePackageAttributesType;
editModeEnabled: boolean;
defaultColorScheme: ColorScheme;
config?: ConfigType;
configName?: string;
session: Session;
@@ -72,34 +72,20 @@ function App(
setPrimaryShade,
};
// hook will return either 'dark' or 'light' on client
// and always 'light' during ssr as window.matchMedia is not available
const preferredColorScheme = useColorScheme(props.pageProps.defaultColorScheme);
const [colorScheme, setColorScheme] = useLocalStorage<ColorScheme>({
key: 'mantine-color-scheme',
defaultValue: preferredColorScheme,
getInitialValueInEffect: true,
});
const { setInitialPackageAttributes } = usePackageAttributesStore();
const { setDisabled } = useEditModeInformationStore();
useEffect(() => {
setInitialPackageAttributes(props.pageProps.packageAttributes);
if (!props.pageProps.editModeEnabled) {
setDisabled();
}
}, []);
const toggleColorScheme = (value?: ColorScheme) =>
setColorScheme(value || (colorScheme === 'dark' ? 'light' : 'dark'));
const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage,
});
useHotkeys([['mod+J', () => toggleColorScheme()]]);
const { colorScheme, toggleColorScheme } = useColorScheme(
pageProps.colorScheme,
pageProps.session
);
return (
<>
@@ -178,13 +164,25 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
return {
pageProps: {
colorScheme: getCookie('color-scheme', ctx) || 'light',
colorScheme: getActiveColorScheme(session, ctx),
packageAttributes: getServiceSidePackageAttributes(),
editModeEnabled: process.env.DISABLE_EDIT_MODE !== 'true',
defaultColorScheme: env.DEFAULT_COLOR_SCHEME,
session,
},
};
};
export default appWithTranslation<any>(api.withTRPC(App), nextI18nextConfig as any);
const getActiveColorScheme = (session: Session | null, ctx: GetServerSidePropsContext) => {
const environmentColorScheme = env.DEFAULT_COLOR_SCHEME ?? 'light';
const cookieValue = getCookie('color-scheme', ctx);
const activeColorScheme = colorSchemeParser.parse(
session?.user?.colorScheme ?? cookieValue ?? environmentColorScheme
);
if (cookieValue !== activeColorScheme) {
setCookie('color-scheme', activeColorScheme, ctx);
}
return activeColorScheme === 'environment' ? environmentColorScheme : activeColorScheme;
};

View File

@@ -2,9 +2,9 @@ import { TRPCError } from '@trpc/server';
import bcrypt from 'bcrypt';
import { z } from 'zod';
import { hashPassword } from '~/utils/security';
import { signUpFormSchema } from '~/validations/user';
import { colorSchemeParser, signUpFormSchema } from '~/validations/user';
import { createTRPCRouter, publicProcedure } from '../trpc';
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
export const userRouter = createTRPCRouter({
register: publicProcedure
@@ -50,6 +50,12 @@ export const userRouter = createTRPCRouter({
name: input.username,
password: hashedPassword,
salt: salt,
settings: {
create: {
colorScheme: colorSchemeParser.parse(ctx.cookies['color-scheme']),
language: ctx.cookies['config-locale'] ?? 'en',
},
},
},
});
await ctx.prisma.registrationToken.delete({
@@ -63,4 +69,24 @@ export const userRouter = createTRPCRouter({
name: user.name,
};
}),
changeColorScheme: protectedProcedure
.input(
z.object({
colorScheme: colorSchemeParser,
})
)
.mutation(async ({ ctx, input }) => {
await ctx.prisma.user.update({
where: {
id: ctx.session?.user?.id,
},
data: {
settings: {
update: {
colorScheme: input.colorScheme,
},
},
},
});
}),
});

View File

@@ -25,6 +25,7 @@ import { prisma } from '../db';
interface CreateContextOptions {
session: Session | null;
cookies: Partial<Record<string, string>>;
}
/**
@@ -39,6 +40,7 @@ interface CreateContextOptions {
*/
const createInnerTRPCContext = (opts: CreateContextOptions) => ({
session: opts.session,
cookies: opts.cookies,
prisma,
});
@@ -56,6 +58,7 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
return createInnerTRPCContext({
session,
cookies: req.cookies,
});
};

View File

@@ -8,7 +8,7 @@ import Credentials from 'next-auth/providers/credentials';
import { prisma } from '~/server/db';
import EmptyNextAuthProvider from '~/utils/empty-provider';
import { fromDate, generateSessionToken } from '~/utils/session';
import { signInSchema } from '~/validations/user';
import { colorSchemeParser, signInSchema } from '~/validations/user';
/**
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
@@ -21,6 +21,8 @@ declare module 'next-auth' {
user: DefaultSession['user'] & {
id: string;
isAdmin: boolean;
colorScheme: 'light' | 'dark' | 'environment';
language: string;
// ...other properties
// role: UserRole;
};
@@ -28,6 +30,8 @@ declare module 'next-auth' {
interface User {
isAdmin: boolean;
colorScheme: 'light' | 'dark' | 'environment';
language: string;
// ...other properties
// role: UserRole;
}
@@ -64,9 +68,19 @@ export const constructAuthOptions = (
where: {
id: user.id,
},
include: {
settings: {
select: {
colorScheme: true,
language: true,
},
},
},
});
session.user.isAdmin = userFromDatabase.isAdmin;
session.user.colorScheme = colorSchemeParser.parse(userFromDatabase.settings?.colorScheme);
session.user.language = userFromDatabase.settings?.language ?? 'en';
}
return session;
@@ -122,6 +136,14 @@ export const constructAuthOptions = (
where: {
name: data.name,
},
include: {
settings: {
select: {
colorScheme: true,
language: true,
},
},
},
});
if (!user || !user.password) {
@@ -142,6 +164,8 @@ export const constructAuthOptions = (
id: user.id,
name: user.name,
isAdmin: false,
colorScheme: colorSchemeParser.parse(user.settings?.colorScheme),
language: user.settings?.language ?? 'en',
};
},
}),

View File

@@ -18,3 +18,8 @@ export const signUpFormSchema = z
} satisfies CustomErrorParams,
path: ['passwordConfirmation'],
});
export const colorSchemeParser = z
.enum(['light', 'dark', 'environment'])
.default('environment')
.catch('environment');