diff --git a/src/components/layout/header/SettingsMenu/ColorSchemeSwitch.tsx b/src/components/layout/header/SettingsMenu/ColorSchemeSwitch.tsx index fb926496a..8adba3333 100644 --- a/src/components/layout/header/SettingsMenu/ColorSchemeSwitch.tsx +++ b/src/components/layout/header/SettingsMenu/ColorSchemeSwitch.tsx @@ -1,9 +1,10 @@ -import { Menu, useMantineColorScheme } from '@mantine/core'; +import { Menu } from '@mantine/core'; import { IconMoonStars, IconSun } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; +import { useColorScheme } from '~/hooks/use-colorscheme'; export const ColorSchemeSwitch = () => { - const { colorScheme, toggleColorScheme } = useMantineColorScheme(); + const { colorScheme, toggleColorScheme } = useColorScheme(); const { t } = useTranslation('settings/general/theme-selector'); const Icon = colorScheme === 'dark' ? IconSun : IconMoonStars; diff --git a/src/env.js b/src/env.js index c45f748cb..8d208def5 100644 --- a/src/env.js +++ b/src/env.js @@ -1,8 +1,8 @@ const { z } = require('zod'); const { createEnv } = require('@t3-oss/env-nextjs'); -const portSchema = z.string().regex(/\d+/).transform(Number).optional() -const envSchema = z.enum(["development", "test", "production"]); +const portSchema = z.string().regex(/\d+/).transform(Number).optional(); +const envSchema = z.enum(['development', 'test', 'production']); const env = createEnv({ /** @@ -11,22 +11,17 @@ const env = createEnv({ */ server: { DATABASE_URL: z.string().url(), - NODE_ENV: envSchema, NEXTAUTH_SECRET: - process.env.NODE_ENV === "production" - ? z.string().min(1) - : z.string().min(1).optional(), + process.env.NODE_ENV === 'production' ? z.string().min(1) : z.string().min(1).optional(), NEXTAUTH_URL: z.preprocess( // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL // Since NextAuth.js automatically uses the VERCEL_URL if present. (str) => process.env.VERCEL_URL ?? str, // VERCEL_URL doesn't include `https` so it cant be validated as a URL - process.env.VERCEL ? z.string().min(1) : z.string().url(), + process.env.VERCEL ? z.string().min(1) : z.string().url() ), - DEFAULT_COLOR_SCHEME: z.enum(['light', 'dark']).optional().default('light'), DOCKER_HOST: z.string().optional(), DOCKER_PORT: z.string().regex(/\d+/).transform(Number).optional(), - PORT: portSchema }, /** @@ -36,8 +31,9 @@ const env = createEnv({ */ client: { // NEXT_PUBLIC_CLIENTVAR: z.string().min(1), + NEXT_PUBLIC_DEFAULT_COLOR_SCHEME: z.enum(['light', 'dark']).optional().default('light'), NEXT_PUBLIC_PORT: portSchema, - NEXT_PUBLIC_NODE_ENV: envSchema + NEXT_PUBLIC_NODE_ENV: envSchema, }, /** @@ -46,21 +42,17 @@ const env = createEnv({ */ runtimeEnv: { DATABASE_URL: process.env.DATABASE_URL, - NODE_ENV: process.env.NODE_ENV, NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, NEXTAUTH_URL: process.env.NEXTAUTH_URL, - NEXT_PUBLIC_DISABLE_EDIT_MODE: process.env.DISABLE_EDIT_MODE, - DISABLE_EDIT_MODE: process.env.DISABLE_EDIT_MODE, - DEFAULT_COLOR_SCHEME: process.env.DEFAULT_COLOR_SCHEME, DOCKER_HOST: process.env.DOCKER_HOST, DOCKER_PORT: process.env.DOCKER_PORT, VERCEL_URL: process.env.VERCEL_URL, - PORT: process.env.PORT, + NEXT_PUBLIC_DEFAULT_COLOR_SCHEME: process.env.DEFAULT_COLOR_SCHEME, NEXT_PUBLIC_PORT: process.env.PORT, - NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV + NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV, }, }); module.exports = { - env -} \ No newline at end of file + env, +}; diff --git a/src/hooks/use-colorscheme.ts b/src/hooks/use-colorscheme.ts deleted file mode 100644 index 3f325111b..000000000 --- a/src/hooks/use-colorscheme.ts +++ /dev/null @@ -1,29 +0,0 @@ -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'; - -import { COOKIE_COLOR_SCHEME_KEY } from '../../data/constants'; - -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(COOKIE_COLOR_SCHEME_KEY, newColorScheme); - if (session && new Date(session.expires) > new Date()) { - await mutateAsync({ colorScheme: newColorScheme }); - } - }; - - useHotkeys([['mod+J', () => void toggleColorScheme()]]); - - return { - colorScheme, - toggleColorScheme, - }; -}; diff --git a/src/hooks/use-colorscheme.tsx b/src/hooks/use-colorscheme.tsx new file mode 100644 index 000000000..c766a7bbb --- /dev/null +++ b/src/hooks/use-colorscheme.tsx @@ -0,0 +1,70 @@ +import { ColorScheme as MantineColorScheme } from '@mantine/core'; +import { useHotkeys } from '@mantine/hooks'; +import { setCookie } from 'cookies-next'; +import { Session } from 'next-auth'; +import { createContext, useContext, useState } from 'react'; +import { api } from '~/utils/api'; + +import { COOKIE_COLOR_SCHEME_KEY } from '../../data/constants'; + +export type ColorScheme = 'dark' | 'light' | 'environment'; + +export const ColorSchemeContext = createContext<{ + colorScheme: MantineColorScheme; + settings: ColorScheme; + toggleColorScheme: () => Promise; + setColorScheme: (colorScheme: ColorScheme) => void; +} | null>(null); + +type ColorSchemeProviderProps = { + activeColorScheme: ColorScheme; + environmentColorScheme: MantineColorScheme; + session: Session; + children: (colorScheme: MantineColorScheme) => React.ReactNode; +}; + +export const ColorSchemeProvider = ({ + activeColorScheme, + environmentColorScheme, + session, + children, +}: ColorSchemeProviderProps) => { + const [colorScheme, setColorScheme] = useState(activeColorScheme); + const { mutateAsync } = api.user.changeColorScheme.useMutation(); + + const toggleColorScheme = async () => { + const newColorScheme = colorScheme === 'dark' ? 'light' : 'dark'; + setColorScheme(newColorScheme); + setCookie(COOKIE_COLOR_SCHEME_KEY, newColorScheme); + if (session && new Date(session.expires) > new Date()) { + await mutateAsync({ colorScheme: newColorScheme }); + } + }; + + const changeColorScheme = (colorScheme: ColorScheme) => setColorScheme(colorScheme); + + useHotkeys([['mod+J', () => void toggleColorScheme()]]); + + const mantineColorScheme = colorScheme === 'environment' ? environmentColorScheme : colorScheme; + + return ( + + {children(mantineColorScheme)} + + ); +}; + +export const useColorScheme = () => { + const context = useContext(ColorSchemeContext); + if (!context) { + throw new Error('useColorScheme must be used within a ColorSchemeProvider'); + } + return context; +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 4c9a4c0d1..bac770a93 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,4 +1,4 @@ -import { ColorScheme, ColorSchemeProvider, MantineProvider, MantineTheme } from '@mantine/core'; +import { ColorScheme as MantineColorScheme, MantineProvider, MantineTheme } from '@mantine/core'; import { ModalsProvider } from '@mantine/modals'; import { Notifications } from '@mantine/notifications'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -16,7 +16,7 @@ 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 { ColorScheme, ColorSchemeProvider } from '~/hooks/use-colorscheme'; import { ConfigType } from '~/types/config'; import { api } from '~/utils/api'; import { colorSchemeParser } from '~/validations/user'; @@ -45,7 +45,8 @@ import { theme } from '../tools/server/theme/theme'; function App( this: any, props: AppProps<{ - colorScheme: ColorScheme; + activeColorScheme: MantineColorScheme; + environmentColorScheme: MantineColorScheme; packageAttributes: ServerSidePackageAttributesType; editModeEnabled: boolean; config?: ConfigType; @@ -83,11 +84,6 @@ function App( storage: AsyncStorage, }); - const { colorScheme, toggleColorScheme } = useColorScheme( - pageProps.colorScheme, - pageProps.session - ); - return ( <> @@ -98,50 +94,52 @@ function App( client={queryClient} persistOptions={{ persister: asyncStoragePersister }} > - - - + {(colorScheme) => ( + + - - - - - - - - + primaryColor, + primaryShade, + colorScheme, + }} + withGlobalStyles + withNormalizeCSS + > + + + + + + + + + )} @@ -151,16 +149,12 @@ function App( } App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => { - if (process.env.DISABLE_EDIT_MODE === 'true') { - Consola.warn( - 'EXPERIMENTAL: You have disabled the edit mode. Modifications are no longer possible and any requests on the API will be dropped. If you want to disable this, unset the DISABLE_EDIT_MODE environment variable. This behaviour may be removed in future versions of Homarr' + if (env.NEXT_PUBLIC_DEFAULT_COLOR_SCHEME !== 'light') { + Consola.debug( + `Overriding the default color scheme with ${env.NEXT_PUBLIC_DEFAULT_COLOR_SCHEME}` ); } - if (env.DEFAULT_COLOR_SCHEME !== 'light') { - Consola.debug(`Overriding the default color scheme with ${env.DEFAULT_COLOR_SCHEME}`); - } - const session = await getSession(ctx); // Set the cookie language to the user language if it is not set correctly @@ -171,7 +165,7 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => { return { pageProps: { - colorScheme: getActiveColorScheme(session, ctx), + ...getActiveColorScheme(session, ctx), packageAttributes: getServiceSidePackageAttributes(), session, }, @@ -181,7 +175,7 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => { export default appWithTranslation(api.withTRPC(App), nextI18nextConfig as any); const getActiveColorScheme = (session: Session | null, ctx: GetServerSidePropsContext) => { - const environmentColorScheme = env.DEFAULT_COLOR_SCHEME ?? 'light'; + const environmentColorScheme = env.NEXT_PUBLIC_DEFAULT_COLOR_SCHEME ?? 'light'; const cookieColorScheme = getCookie(COOKIE_COLOR_SCHEME_KEY, ctx); const activeColorScheme = colorSchemeParser.parse( session?.user?.colorScheme ?? cookieColorScheme ?? environmentColorScheme @@ -191,5 +185,8 @@ const getActiveColorScheme = (session: Session | null, ctx: GetServerSidePropsCo setCookie(COOKIE_COLOR_SCHEME_KEY, activeColorScheme, ctx); } - return activeColorScheme === 'environment' ? environmentColorScheme : activeColorScheme; + return { + activeColorScheme: activeColorScheme, + environmentColorScheme, + }; }; diff --git a/src/pages/api/trpc/[trpc].ts b/src/pages/api/trpc/[trpc].ts index 7bec97bf0..8b7cd11a4 100644 --- a/src/pages/api/trpc/[trpc].ts +++ b/src/pages/api/trpc/[trpc].ts @@ -9,7 +9,7 @@ export default createNextApiHandler({ router: rootRouter, createContext: createTRPCContext, onError: - env.NODE_ENV === 'development' + env.NEXT_PUBLIC_NODE_ENV === 'development' ? ({ path, error }) => { Consola.error(`❌ tRPC failed on ${path ?? ''}: ${error.message}`); } diff --git a/src/pages/migrate.tsx b/src/pages/migrate.tsx index 9b13a79b8..5c8c7e3d8 100644 --- a/src/pages/migrate.tsx +++ b/src/pages/migrate.tsx @@ -20,7 +20,6 @@ import { ThemeIcon, Title, createStyles, - useMantineColorScheme, useMantineTheme, } from '@mantine/core'; import { @@ -37,6 +36,7 @@ import fs from 'fs'; import { GetServerSidePropsContext } from 'next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import React, { useEffect, useState } from 'react'; +import { useColorScheme } from '~/hooks/use-colorscheme'; import { Logo } from '../components/layout/Logo'; import { usePrimaryGradient } from '../components/layout/useGradient'; @@ -229,7 +229,7 @@ export default function ServerError({ configs }: { configs: any }) { } function SwitchToggle() { - const { colorScheme, toggleColorScheme } = useMantineColorScheme(); + const { colorScheme, toggleColorScheme } = useColorScheme(); const theme = useMantineTheme(); return ( diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts index 6ba36d93a..d5b5e5d09 100644 --- a/src/server/api/routers/user.ts +++ b/src/server/api/routers/user.ts @@ -110,4 +110,27 @@ export const userRouter = createTRPCRouter({ }, }); }), + getWithSettings: protectedProcedure.query(async ({ ctx }) => { + const user = await ctx.prisma.user.findUnique({ + where: { + id: ctx.session?.user?.id, + }, + include: { + settings: true, + }, + }); + + if (!user || !user.settings) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'User not found', + }); + } + + return { + id: user.id, + name: user.name, + settings: user.settings, + }; + }), }); diff --git a/src/server/db.ts b/src/server/db.ts index 963fba477..9fb47cb40 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -8,7 +8,7 @@ const globalForPrisma = globalThis as unknown as { export const prisma = globalForPrisma.prisma ?? new PrismaClient({ - log: env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], + log: env.NEXT_PUBLIC_NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], }); -if (env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; +if (env.NEXT_PUBLIC_NODE_ENV !== 'production') globalForPrisma.prisma = prisma; diff --git a/src/tools/server/getPackageVersion.ts b/src/tools/server/getPackageVersion.ts index cb7986d54..3197a3ca3 100644 --- a/src/tools/server/getPackageVersion.ts +++ b/src/tools/server/getPackageVersion.ts @@ -4,7 +4,7 @@ import packageJson from '../../../package.json'; const getServerPackageVersion = (): string | undefined => packageJson.version; -const getServerNodeEnvironment = () => env.NODE_ENV; +const getServerNodeEnvironment = () => env.NEXT_PUBLIC_NODE_ENV; const getDependencies = (): PackageJsonDependencies => packageJson.dependencies;