Skip to content

Commit

Permalink
feat: add server settings for default board, default color scheme and…
Browse files Browse the repository at this point in the history
… default locale
  • Loading branch information
Meierschlumpf committed Oct 25, 2024
1 parent 0c41241 commit 979794a
Show file tree
Hide file tree
Showing 24 changed files with 439 additions and 132 deletions.
10 changes: 7 additions & 3 deletions apps/nextjs/src/app/[locale]/_client-providers/mantine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<DirectionProvider>
<MantineProvider
defaultColorScheme="dark"
defaultColorScheme={defaultColorScheme}
colorSchemeManager={manager}
theme={createTheme({
primaryColor: "red",
Expand All @@ -28,7 +32,7 @@ export const CustomMantineProvider = ({ children }: PropsWithChildren) => {
);
};

function useColorSchemeManager(): MantineColorSchemeManager {
export function useColorSchemeManager(): MantineColorSchemeManager {
const key = "homarr-color-scheme";
const { data: session } = useSession();

Expand Down
15 changes: 13 additions & 2 deletions apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -18,7 +25,11 @@ export const BoardMantineProvider = ({ children }: PropsWithChildren) => {
autoContrast: true,
});

return <MantineProvider theme={theme}>{children}</MantineProvider>;
return (
<MantineProvider defaultColorScheme={defaultColorScheme} theme={theme} colorSchemeManager={colorSchemeManager}>
{children}
</MantineProvider>
);
};

export const generateColors = (hex: string) => {
Expand Down
4 changes: 3 additions & 1 deletion apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -37,10 +38,11 @@ export const createBoardLayout = <TParams extends Params>({

throw error;
});
const colorScheme = await getCurrentColorSchemeAsync();

return (
<BoardProvider initialBoard={initialBoard}>
<BoardMantineProvider>
<BoardMantineProvider defaultColorScheme={colorScheme}>
<CustomCss />
<ClientShell hasNavigation={false}>
<MainHeader
Expand Down
16 changes: 6 additions & 10 deletions apps/nextjs/src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import "@homarr/spotlight/styles.css";
import "@homarr/ui/styles.css";
import "~/styles/scroll-area.scss";

import { cookies } from "next/headers";

import { env } from "@homarr/auth/env.mjs";
import { auth } from "@homarr/auth/next";
import { ModalProvider } from "@homarr/modals";
Expand All @@ -16,6 +14,7 @@ import { getScopedI18n } from "@homarr/translation/server";

import { Analytics } from "~/components/layout/analytics";
import { SearchEngineOptimization } from "~/components/layout/search-engine-optimization";
import { getCurrentColorSchemeAsync } from "~/theme/color-scheme";
import { JotaiProvider } from "./_client-providers/jotai";
import { CustomMantineProvider } from "./_client-providers/mantine";
import { NextInternationalProvider } from "./_client-providers/next-international";
Expand All @@ -28,7 +27,8 @@ const fontSans = Inter({
variable: "--font-sans",
});

export const generateMetadata = (): Metadata => ({
// eslint-disable-next-line no-restricted-syntax
export const generateMetadata = async (): Promise<Metadata> => ({
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.",
Expand All @@ -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",
},
});

Expand All @@ -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");

Expand All @@ -71,7 +71,7 @@ export default async function Layout(props: { children: React.ReactNode; params:
(innerProps) => <JotaiProvider {...innerProps} />,
(innerProps) => <TRPCReactProvider {...innerProps} />,
(innerProps) => <NextInternationalProvider {...innerProps} locale={props.params.locale} />,
(innerProps) => <CustomMantineProvider {...innerProps} />,
(innerProps) => <CustomMantineProvider {...innerProps} defaultColorScheme={colorScheme} />,
(innerProps) => <ModalProvider {...innerProps} />,
]);

Expand Down Expand Up @@ -99,7 +99,3 @@ export default async function Layout(props: { children: React.ReactNode; params:
</html>
);
}

const getColorScheme = () => {
return cookies().get("homarr-color-scheme")?.value ?? "dark";
};
Original file line number Diff line number Diff line change
@@ -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 (
<CommonSettingsForm settingKey="appearance" defaultValues={defaultValues}>
{(form) => (
<>
<SelectWithCustomItems
label="Default color scheme"
data={colorSchemes.map((scheme) => ({
value: scheme,
label: scheme,
}))}
{...form.getInputProps("defaultColorScheme")}
SelectOption={ApperanceCustomOption}
/>
</>
)}
</CommonSettingsForm>
);
};

const appearanceIcons = {
light: IconSun,
dark: IconMoon,
};

const ApperanceCustomOption = ({ value, label }: { value: ColorScheme; label: string }) => {
const Icon = appearanceIcons[value];

return (
<Group>
<Icon size={16} stroke={1.5} />
<Text fz="sm" fw={500}>
{label}
</Text>
</Group>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<CommonSettingsForm settingKey="board" defaultValues={defaultValues}>
{(form) => (
<>
<SelectWithCustomItems
label="Global default board"
description="Only public boards are available for selection"
data={selectableBoards.map((board) => ({
value: board.id,
label: board.name,
image: board.logoImageUrl,
}))}
SelectOption={({ label, image }: { value: string; label: string; image: string | null }) => (
<Group>
{/* eslint-disable-next-line @next/next/no-img-element */}
{image ? <img width={16} height={16} src={image} alt={label} /> : <IconLayoutDashboard size={16} />}
<Text fz="sm" fw={500}>
{label}
</Text>
</Group>
)}
{...form.getInputProps("defaultBoardId")}
/>
</>
)}
</CommonSettingsForm>
);
};
Original file line number Diff line number Diff line change
@@ -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 = <TKey extends keyof ServerSettings>({
settingKey,
defaultValues,
children,
}: {
settingKey: TKey;
defaultValues: ServerSettings[TKey];
children: (form: ReturnType<typeof useForm<ServerSettings[TKey]>>) => 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 (
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
<Stack gap="sm">
{children(form)}
<Group justify="end">
<Button type="submit" loading={isPending}>
Save
</Button>
</Group>
</Stack>
</form>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<CommonSettingsForm settingKey="culture" defaultValues={defaultValues}>
{(form) => (
<>
<SelectWithCustomItems
label="Default locale"
data={objectEntries(localeAttributes).map(([value, { name }]) => ({
value,
label: name,
}))}
defaultValue={defaultValues.defaultLocale}
SelectOption={({ value, label }: { value: SupportedLanguage; label: string }) => {
const currentConfig = localeAttributes[value];

return (
<Group>
<span className={`fi fi-${currentConfig.flagIcon}`} style={{ borderRadius: 4 }}></span>
<Text fz="sm" fw={500}>
{label}
</Text>
</Group>
);
}}
{...form.getInputProps("defaultLocale")}
/>
</>
)}
</CommonSettingsForm>
);
};
15 changes: 15 additions & 0 deletions apps/nextjs/src/app/[locale]/manage/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -26,6 +29,18 @@ export default async function SettingsPage() {
<Title order={1}>{t("title")}</Title>
<AnalyticsSettings initialData={serverSettings.analytics} />
<CrawlingAndIndexingSettings initialData={serverSettings.crawlingAndIndexing} />
<Stack>
<Title order={2}>Boards</Title>
<BoardSettingsForm defaultValues={serverSettings.board} />
</Stack>
<Stack>
<Title order={2}>Appearance</Title>
<AppearanceSettingsForm defaultValues={serverSettings.appearance} />
</Stack>
<Stack>
<Title order={2}>Culture</Title>
<CultureSettingsForm defaultValues={serverSettings.culture} />
</Stack>
</Stack>
</>
);
Expand Down
5 changes: 3 additions & 2 deletions apps/nextjs/src/components/layout/analytics.tsx
Original file line number Diff line number Diff line change
@@ -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 <Script src="https://umami.homarr.dev/script.js" data-website-id={UMAMI_WEBSITE_ID} defer />;
Expand Down
Loading

0 comments on commit 979794a

Please sign in to comment.