mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 15:35:55 +01:00
🎨 Improve color scheme logic
This commit is contained in:
@@ -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;
|
||||
|
||||
28
src/env.js
28
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
|
||||
}
|
||||
env,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
70
src/hooks/use-colorscheme.tsx
Normal file
70
src/hooks/use-colorscheme.tsx
Normal file
@@ -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<void>;
|
||||
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 (
|
||||
<ColorSchemeContext.Provider
|
||||
value={{
|
||||
colorScheme: mantineColorScheme,
|
||||
settings: colorScheme,
|
||||
toggleColorScheme,
|
||||
setColorScheme: changeColorScheme,
|
||||
}}
|
||||
>
|
||||
{children(mantineColorScheme)}
|
||||
</ColorSchemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useColorScheme = () => {
|
||||
const context = useContext(ColorSchemeContext);
|
||||
if (!context) {
|
||||
throw new Error('useColorScheme must be used within a ColorSchemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -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 (
|
||||
<>
|
||||
<Head>
|
||||
@@ -98,7 +94,8 @@ function App(
|
||||
client={queryClient}
|
||||
persistOptions={{ persister: asyncStoragePersister }}
|
||||
>
|
||||
<ColorSchemeProvider colorScheme={colorScheme} toggleColorScheme={toggleColorScheme}>
|
||||
<ColorSchemeProvider {...pageProps}>
|
||||
{(colorScheme) => (
|
||||
<ColorTheme.Provider value={colorTheme}>
|
||||
<MantineProvider
|
||||
theme={{
|
||||
@@ -142,6 +139,7 @@ function App(
|
||||
</ConfigProvider>
|
||||
</MantineProvider>
|
||||
</ColorTheme.Provider>
|
||||
)}
|
||||
</ColorSchemeProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</PersistQueryClientProvider>
|
||||
@@ -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<any>(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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 ?? '<no-path>'}: ${error.message}`);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user