From 979794adaa993dfa432baed2eec84e250396eb54 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sat, 26 Oct 2024 00:14:47 +0200 Subject: [PATCH 1/7] feat: add server settings for default board, default color scheme and default locale --- .../[locale]/_client-providers/mantine.tsx | 10 ++-- .../app/[locale]/boards/(content)/_theme.tsx | 15 +++++- .../app/[locale]/boards/_layout-creator.tsx | 4 +- apps/nextjs/src/app/[locale]/layout.tsx | 16 +++--- .../_components/appearance-settings-form.tsx | 49 +++++++++++++++++ .../_components/board-settings-form.tsx | 42 +++++++++++++++ .../settings/_components/common-form.tsx | 54 +++++++++++++++++++ .../_components/culture-settings-form.tsx | 43 +++++++++++++++ .../src/app/[locale]/manage/settings/page.tsx | 15 ++++++ .../src/components/layout/analytics.tsx | 5 +- .../layout/search-engine-optimization.tsx | 36 ++++++------- apps/nextjs/src/middleware.ts | 9 ++-- apps/nextjs/src/theme/color-scheme.ts | 17 ++++++ .../analytics/src/send-server-analytics.ts | 19 ++----- packages/api/src/router/board.ts | 26 ++++++++- packages/api/src/router/serverSettings.ts | 53 ++++-------------- packages/cron-jobs/src/jobs/analytics.ts | 19 ++----- packages/db/migrations/seed.ts | 35 ++++++++---- packages/db/queries/index.ts | 1 + packages/db/queries/server-setting.ts | 52 ++++++++++++++++++ packages/server-settings/package.json | 4 ++ packages/server-settings/src/index.ts | 20 ++++++- packages/translation/src/middleware.ts | 20 ++++--- pnpm-lock.yaml | 7 +++ 24 files changed, 439 insertions(+), 132 deletions(-) create mode 100644 apps/nextjs/src/app/[locale]/manage/settings/_components/appearance-settings-form.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/settings/_components/board-settings-form.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/settings/_components/common-form.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/settings/_components/culture-settings-form.tsx create mode 100644 apps/nextjs/src/theme/color-scheme.ts create mode 100644 packages/db/queries/server-setting.ts diff --git a/apps/nextjs/src/app/[locale]/_client-providers/mantine.tsx b/apps/nextjs/src/app/[locale]/_client-providers/mantine.tsx index 867ecfd94..edd5952ac 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/mantine.tsx +++ b/apps/nextjs/src/app/[locale]/_client-providers/mantine.tsx @@ -8,14 +8,18 @@ import dayjs from "dayjs"; import { clientApi } from "@homarr/api/client"; import { useSession } from "@homarr/auth/client"; import { parseCookies, setClientCookie } from "@homarr/common"; +import type { ColorScheme } from "@homarr/definitions"; -export const CustomMantineProvider = ({ children }: PropsWithChildren) => { +export const CustomMantineProvider = ({ + children, + defaultColorScheme, +}: PropsWithChildren<{ defaultColorScheme: ColorScheme }>) => { const manager = useColorSchemeManager(); return ( { ); }; -function useColorSchemeManager(): MantineColorSchemeManager { +export function useColorSchemeManager(): MantineColorSchemeManager { const key = "homarr-color-scheme"; const { data: session } = useSession(); diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx index e3c87aa05..755c0a42b 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx @@ -4,10 +4,17 @@ import type { PropsWithChildren } from "react"; import type { MantineColorsTuple } from "@mantine/core"; import { createTheme, darken, lighten, MantineProvider } from "@mantine/core"; +import type { ColorScheme } from "@homarr/definitions"; + +import { useColorSchemeManager } from "../../_client-providers/mantine"; import { useRequiredBoard } from "./_context"; -export const BoardMantineProvider = ({ children }: PropsWithChildren) => { +export const BoardMantineProvider = ({ + children, + defaultColorScheme, +}: PropsWithChildren<{ defaultColorScheme: ColorScheme }>) => { const board = useRequiredBoard(); + const colorSchemeManager = useColorSchemeManager(); const theme = createTheme({ colors: { @@ -18,7 +25,11 @@ export const BoardMantineProvider = ({ children }: PropsWithChildren) => { autoContrast: true, }); - return {children}; + return ( + + {children} + + ); }; export const generateColors = (hex: string) => { diff --git a/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx b/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx index 4e8edd149..e6737e14e 100644 --- a/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx +++ b/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx @@ -8,6 +8,7 @@ import { logger } from "@homarr/log"; import { MainHeader } from "~/components/layout/header"; import { BoardLogoWithTitle } from "~/components/layout/logo/board-logo"; import { ClientShell } from "~/components/layout/shell"; +import { getCurrentColorSchemeAsync } from "~/theme/color-scheme"; import type { Board } from "./_types"; import { BoardProvider } from "./(content)/_context"; import type { Params } from "./(content)/_creator"; @@ -37,10 +38,11 @@ export const createBoardLayout = ({ throw error; }); + const colorScheme = await getCurrentColorSchemeAsync(); return ( - + ({ +// eslint-disable-next-line no-restricted-syntax +export const generateMetadata = async (): Promise => ({ title: "Homarr", description: "Simplify the management of your server with Homarr - a sleek, modern dashboard that puts all of your apps and services at your fingertips.", @@ -47,7 +47,7 @@ export const generateMetadata = (): Metadata => ({ title: "Homarr", capable: true, startupImage: { url: "/logo/logo.png" }, - statusBarStyle: getColorScheme() === "dark" ? "black-translucent" : "default", + statusBarStyle: (await getCurrentColorSchemeAsync()) === "dark" ? "black-translucent" : "default", }, }); @@ -60,7 +60,7 @@ export const viewport: Viewport = { export default async function Layout(props: { children: React.ReactNode; params: { locale: string } }) { const session = await auth(); - const colorScheme = getColorScheme(); + const colorScheme = await getCurrentColorSchemeAsync(); const tCommon = await getScopedI18n("common"); const direction = tCommon("direction"); @@ -71,7 +71,7 @@ export default async function Layout(props: { children: React.ReactNode; params: (innerProps) => , (innerProps) => , (innerProps) => , - (innerProps) => , + (innerProps) => , (innerProps) => , ]); @@ -99,7 +99,3 @@ export default async function Layout(props: { children: React.ReactNode; params: ); } - -const getColorScheme = () => { - return cookies().get("homarr-color-scheme")?.value ?? "dark"; -}; diff --git a/apps/nextjs/src/app/[locale]/manage/settings/_components/appearance-settings-form.tsx b/apps/nextjs/src/app/[locale]/manage/settings/_components/appearance-settings-form.tsx new file mode 100644 index 000000000..aa8ad6eb7 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/settings/_components/appearance-settings-form.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { Group, Text } from "@mantine/core"; +import { IconMoon, IconSun } from "@tabler/icons-react"; +import { SelectWithCustomItems } from "node_modules/@homarr/ui/src/components/select-with-custom-items"; + +import type { ColorScheme } from "@homarr/definitions"; +import { colorSchemes } from "@homarr/definitions"; +import type { ServerSettings } from "@homarr/server-settings"; + +import { CommonSettingsForm } from "./common-form"; + +export const AppearanceSettingsForm = ({ defaultValues }: { defaultValues: ServerSettings["appearance"] }) => { + return ( + + {(form) => ( + <> + ({ + value: scheme, + label: scheme, + }))} + {...form.getInputProps("defaultColorScheme")} + SelectOption={ApperanceCustomOption} + /> + + )} + + ); +}; + +const appearanceIcons = { + light: IconSun, + dark: IconMoon, +}; + +const ApperanceCustomOption = ({ value, label }: { value: ColorScheme; label: string }) => { + const Icon = appearanceIcons[value]; + + return ( + + + + {label} + + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/manage/settings/_components/board-settings-form.tsx b/apps/nextjs/src/app/[locale]/manage/settings/_components/board-settings-form.tsx new file mode 100644 index 000000000..4f4c5c226 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/settings/_components/board-settings-form.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { Group, Text } from "@mantine/core"; +import { IconLayoutDashboard } from "@tabler/icons-react"; +import { SelectWithCustomItems } from "node_modules/@homarr/ui/src/components/select-with-custom-items"; + +import { clientApi } from "@homarr/api/client"; +import type { ServerSettings } from "@homarr/server-settings"; + +import { CommonSettingsForm } from "./common-form"; + +export const BoardSettingsForm = ({ defaultValues }: { defaultValues: ServerSettings["board"] }) => { + const [selectableBoards] = clientApi.board.getPublicBoards.useSuspenseQuery(); + + return ( + + {(form) => ( + <> + ({ + value: board.id, + label: board.name, + image: board.logoImageUrl, + }))} + SelectOption={({ label, image }: { value: string; label: string; image: string | null }) => ( + + {/* eslint-disable-next-line @next/next/no-img-element */} + {image ? {label} : } + + {label} + + + )} + {...form.getInputProps("defaultBoardId")} + /> + + )} + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/manage/settings/_components/common-form.tsx b/apps/nextjs/src/app/[locale]/manage/settings/_components/common-form.tsx new file mode 100644 index 000000000..8cc7d4601 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/settings/_components/common-form.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { Button, Group, Stack } from "@mantine/core"; + +import { clientApi } from "@homarr/api/client"; +import { useForm } from "@homarr/form"; +import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; +import type { ServerSettings } from "@homarr/server-settings"; + +export const CommonSettingsForm = ({ + settingKey, + defaultValues, + children, +}: { + settingKey: TKey; + defaultValues: ServerSettings[TKey]; + children: (form: ReturnType>) => React.ReactNode; +}) => { + const { mutateAsync, isPending } = clientApi.serverSettings.saveSettings.useMutation({ + onSuccess() { + showSuccessNotification({ + message: "Settings saved successfully", + }); + }, + onError() { + showErrorNotification({ + message: "Failed to save settings", + }); + }, + }); + const form = useForm({ + initialValues: defaultValues, + }); + + const handleSubmitAsync = async (values: ServerSettings[TKey]) => { + await mutateAsync({ + settingsKey: settingKey, + value: values, + }); + }; + + return ( +
void handleSubmitAsync(values))}> + + {children(form)} + + + + +
+ ); +}; diff --git a/apps/nextjs/src/app/[locale]/manage/settings/_components/culture-settings-form.tsx b/apps/nextjs/src/app/[locale]/manage/settings/_components/culture-settings-form.tsx new file mode 100644 index 000000000..88b104ab4 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/settings/_components/culture-settings-form.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { Group, Text } from "@mantine/core"; +import { SelectWithCustomItems } from "node_modules/@homarr/ui/src/components/select-with-custom-items"; + +import { objectEntries } from "@homarr/common"; +import type { ServerSettings } from "@homarr/server-settings"; +import type { SupportedLanguage } from "@homarr/translation"; +import { localeAttributes } from "@homarr/translation"; + +import { CommonSettingsForm } from "./common-form"; + +export const CultureSettingsForm = ({ defaultValues }: { defaultValues: ServerSettings["culture"] }) => { + return ( + + {(form) => ( + <> + ({ + value, + label: name, + }))} + defaultValue={defaultValues.defaultLocale} + SelectOption={({ value, label }: { value: SupportedLanguage; label: string }) => { + const currentConfig = localeAttributes[value]; + + return ( + + + + {label} + + + ); + }} + {...form.getInputProps("defaultLocale")} + /> + + )} + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/manage/settings/page.tsx b/apps/nextjs/src/app/[locale]/manage/settings/page.tsx index 26ef0b3c1..abe38ed43 100644 --- a/apps/nextjs/src/app/[locale]/manage/settings/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/settings/page.tsx @@ -6,6 +6,9 @@ import { getScopedI18n } from "@homarr/translation/server"; import { CrawlingAndIndexingSettings } from "~/app/[locale]/manage/settings/_components/crawling-and-indexing.settings"; import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; import { AnalyticsSettings } from "./_components/analytics.settings"; +import { AppearanceSettingsForm } from "./_components/appearance-settings-form"; +import { BoardSettingsForm } from "./_components/board-settings-form"; +import { CultureSettingsForm } from "./_components/culture-settings-form"; export async function generateMetadata() { const t = await getScopedI18n("management"); @@ -26,6 +29,18 @@ export default async function SettingsPage() { {t("title")} + + Boards + + + + Appearance + + + + Culture + + ); diff --git a/apps/nextjs/src/components/layout/analytics.tsx b/apps/nextjs/src/components/layout/analytics.tsx index 322d7a0a6..8d81fa13c 100644 --- a/apps/nextjs/src/components/layout/analytics.tsx +++ b/apps/nextjs/src/components/layout/analytics.tsx @@ -1,11 +1,12 @@ import Script from "next/script"; import { UMAMI_WEBSITE_ID } from "@homarr/analytics"; -import { api } from "@homarr/api/server"; +import { db } from "@homarr/db"; +import { getServerSettingByKeyAsync } from "@homarr/db/queries"; export const Analytics = async () => { // For static pages it will not find any analytics data so we do not include the script on them - const analytics = await api.serverSettings.getAnalytics().catch(() => null); + const analytics = await getServerSettingByKeyAsync(db, "analytics").catch(() => null); if (analytics?.enableGeneral) { return