♻️ Improved code structure for layout, remove most settings components

This commit is contained in:
Meier Lukas
2023-08-01 15:23:31 +02:00
parent 6b8d94b6b5
commit 65d0b31a1a
48 changed files with 103 additions and 1575 deletions

View File

@@ -1,25 +0,0 @@
import { Global } from '@mantine/core';
import { useConfigContext } from '../../config/provider';
export function Background() {
const { config } = useConfigContext();
if (!config?.settings.customization.backgroundImageUrl) {
return null;
}
return (
<Global
styles={{
body: {
minHeight: '100vh',
backgroundImage: `url('${config?.settings.customization.backgroundImageUrl}')`,
backgroundPosition: 'center center',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
},
}}
/>
);
}

View File

@@ -1,7 +1,7 @@
import { Group, Image, Text } from '@mantine/core';
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
import { useConfigContext } from '../../config/provider';
import { useConfigContext } from '../../../config/provider';
import { usePrimaryGradient } from './useGradient';
interface LogoProps {

View File

@@ -1,6 +1,6 @@
import { createStyles } from '@mantine/core';
import { useConfigContext } from '../../config/provider';
import { useConfigContext } from '../../../config/provider';
export const useCardStyles = (isCategory: boolean) => {
const { config } = useConfigContext();

View File

@@ -1,6 +1,6 @@
import { MantineGradient } from '@mantine/core';
import { useColorTheme } from '../../tools/color';
import { useColorTheme } from '../../../tools/color';
export const usePrimaryGradient = (): MantineGradient => {
const { primaryColor, secondaryColor } = useColorTheme();

View File

@@ -0,0 +1,31 @@
import Head from 'next/head';
import React from 'react';
import { useConfigContext } from '../../../config/provider';
export const BoardHeadOverride = () => {
const { config } = useConfigContext();
if (!config) return null;
const { metaTitle, faviconUrl } = config.settings.customization;
return (
<Head>
{metaTitle && metaTitle.length > 0 && (
<>
<title>{metaTitle}</title>
<meta name="apple-mobile-web-app-title" content={metaTitle} />
</>
)}
{faviconUrl && faviconUrl.length > 0 && (
<>
<link rel="shortcut icon" href={faviconUrl} />
<link rel="apple-touch-icon" href={faviconUrl} />
</>
)}
</Head>
);
};

View File

@@ -1,13 +1,12 @@
import { useMantineTheme } from '@mantine/core';
import Head from 'next/head';
import { ReactNode } from 'react';
interface CommonHeaderProps {
children?: ReactNode;
}
export const CommonHead = () => {
const { colorScheme } = useMantineTheme();
export const CommonHeader = ({ children }: CommonHeaderProps) => {
return (
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<link rel="shortcut icon" href="/imgs/favicon/favicon.svg" />
<link rel="manifest" href="/site.webmanifest" />
@@ -18,7 +17,10 @@ export const CommonHeader = ({ children }: CommonHeaderProps) => {
<meta name="apple-mobile-web-app-capable" content="yes" />
{children}
<meta
name="apple-mobile-web-app-status-bar-style"
content={colorScheme === 'dark' ? 'white-translucent' : 'black-translucent'}
/>
</Head>
);
};

View File

@@ -1,35 +0,0 @@
/* eslint-disable react/no-invalid-html-attribute */
import NextHead from 'next/head';
import React from 'react';
import { useConfigContext } from '../../../config/provider';
import { SafariStatusBarStyle } from './SafariStatusBarStyle';
export function Head() {
const { config } = useConfigContext();
return (
<NextHead>
<title>{config?.settings.customization.metaTitle || 'Homarr 🦞'}</title>
<link
rel="shortcut icon"
href={config?.settings.customization.faviconUrl || '/imgs/favicon/favicon.svg'}
/>
<link rel="manifest" href="/site.webmanifest" />
{/* configure apple splash screen & touch icon */}
<link
rel="apple-touch-icon"
href={config?.settings.customization.faviconUrl || '/imgs/favicon/favicon.svg'}
/>
<meta
name="apple-mobile-web-app-title"
content={config?.settings.customization.metaTitle || 'Homarr'}
/>
<SafariStatusBarStyle />
<meta name="apple-mobile-web-app-capable" content="yes" />
</NextHead>
);
}

View File

@@ -1,12 +0,0 @@
import { useMantineTheme } from '@mantine/core';
export const SafariStatusBarStyle = () => {
const { colorScheme } = useMantineTheme();
const isDark = colorScheme === 'dark';
return (
<meta
name="apple-mobile-web-app-status-bar-style"
content={isDark ? 'white-translucent' : 'black-translucent'}
/>
);
};

View File

@@ -1,4 +1,4 @@
import { Button, Text, Title, Tooltip, clsx } from '@mantine/core';
import { Button, Global, Text, Title, Tooltip, clsx } from '@mantine/core';
import { useHotkeys, useWindowEvent } from '@mantine/hooks';
import { openContextModal } from '@mantine/modals';
import { hideNotification, showNotification } from '@mantine/notifications';
@@ -19,8 +19,8 @@ import { useConfigContext } from '~/config/provider';
import { env } from '~/env';
import { api } from '~/utils/api';
import { Background } from '../Background';
import { HeaderActionButton } from '../Header/ActionButton';
import { BoardHeadOverride } from '../Meta/BoardHead';
import { MainLayout } from './MainLayout';
type BoardLayoutProps = {
@@ -32,7 +32,8 @@ export const BoardLayout = ({ children }: BoardLayoutProps) => {
return (
<MainLayout headerActions={<HeaderActions />}>
<Background />
<BoardHeadOverride />
<BackgroundImage />
{children}
<style>{clsx(config?.settings.customization.customCss)}</style>
</MainLayout>
@@ -195,3 +196,25 @@ const AddElementButton = () => {
</Tooltip>
);
};
const BackgroundImage = () => {
const { config } = useConfigContext();
if (!config?.settings.customization.backgroundImageUrl) {
return null;
}
return (
<Global
styles={{
body: {
minHeight: '100vh',
backgroundImage: `url('${config?.settings.customization.backgroundImageUrl}')`,
backgroundPosition: 'center center',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
},
}}
/>
);
};

View File

@@ -1,7 +1,6 @@
import { AppShell, useMantineTheme } from '@mantine/core';
import { MainHeader } from '../Header/Header';
import { Head } from '../Meta/Head';
type MainLayoutProps = {
headerActions?: React.ReactNode;
@@ -21,7 +20,6 @@ export const MainLayout = ({ headerActions, children }: MainLayoutProps) => {
header={<MainHeader headerActions={headerActions} />}
className="dashboard-app-shell"
>
<Head />
{children}
</AppShell>
);

View File

@@ -33,7 +33,6 @@ import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
import { MainHeader } from '../Header/Header';
import { CommonHeader } from '../common-header';
interface ManageLayoutProps {
children: ReactNode;
@@ -143,7 +142,6 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
return (
<>
<CommonHeader />
<AppShell
styles={{
root: {

View File

@@ -0,0 +1,318 @@
import {
Accordion,
ActionIcon,
Anchor,
Badge,
Button,
Grid,
Group,
HoverCard,
Kbd,
Modal,
Table,
Text,
Title,
createStyles,
} from '@mantine/core';
import {
IconAnchor,
IconBrandDiscord,
IconBrandGithub,
IconFile,
IconKey,
IconLanguage,
IconSchema,
IconVersions,
IconVocabulary,
IconWorldWww,
} from '@tabler/icons-react';
import { motion } from 'framer-motion';
import { InitOptions } from 'i18next';
import { Trans, i18n, useTranslation } from 'next-i18next';
import Image from 'next/image';
import { ReactNode } from 'react';
import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store';
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
import { useColorTheme } from '../../../../tools/color';
import { usePrimaryGradient } from '../../Common/useGradient';
import Credits from './Credits';
import Tip from './Tip';
interface AboutModalProps {
opened: boolean;
closeModal: () => void;
newVersionAvailable?: string;
}
export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutModalProps) => {
const { classes } = useStyles();
const colorGradiant = usePrimaryGradient();
const informations = useInformationTableItems(newVersionAvailable);
const { t } = useTranslation(['common', 'layout/modals/about']);
const keybinds = [
{ key: 'Mod + J', shortcut: 'Toggle light/dark mode' },
{ key: 'Mod + K', shortcut: 'Focus on search bar' },
{ key: 'Mod + B', shortcut: 'Open docker widget' },
{ key: 'Mod + E', shortcut: 'Toggle Edit mode' },
];
const rows = keybinds.map((element) => (
<tr key={element.key}>
<td>
<Kbd>{element.key}</Kbd>
</td>
<td>
<Text>{element.shortcut}</Text>
</td>
</tr>
));
return (
<Modal
onClose={() => closeModal()}
opened={opened}
title={
<Group spacing="sm">
<Image
alt="Homarr logo"
src="/imgs/logo/logo.png"
width={30}
height={30}
style={{
objectFit: 'contain',
}}
/>
<Title order={3} variant="gradient" gradient={colorGradiant}>
{t('about')} Homarr
</Title>
</Group>
}
size="xl"
>
<Text mb="lg">
<Trans i18nKey="layout/modals/about:description" />
</Text>
<Table mb="lg" highlightOnHover withBorder>
<tbody>
{informations.map((item, index) => (
<tr key={index}>
<td>
<Group spacing="xs">
<ActionIcon className={classes.informationIcon} variant="default">
{item.icon}
</ActionIcon>
<Text>
<Trans
i18nKey={`layout/modals/about:metrics.${item.label}`}
components={{ b: <b /> }}
/>
</Text>
</Group>
</td>
<td className={classes.informationTableColumn} style={{ maxWidth: 200 }}>
{item.content}
</td>
</tr>
))}
</tbody>
</Table>
<Accordion mb={5} variant="contained" radius="md">
<Accordion.Item value="keybinds">
<Accordion.Control icon={<IconKey size={20} />}>
{t('layout/modals/about:keybinds')}
</Accordion.Control>
<Accordion.Panel>
<Table mb={5}>
<thead>
<tr>
<th>{t('layout/modals/about:key')}</th>
<th>{t('layout/modals/about:action')}</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
<Tip>{t('layout/modals/about:tip')}</Tip>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
<Title order={6} mb="xs" align="center">
{t('layout/modals/about:contact')}
</Title>
<Grid grow>
<Grid.Col md={4} xs={12}>
<Button
component="a"
href="https://github.com/ajnart/homarr"
target="_blank"
leftIcon={<IconBrandGithub size={20} />}
variant="default"
fullWidth
>
GitHub
</Button>
</Grid.Col>
<Grid.Col md={4} xs={12}>
<Button
component="a"
href="https://homarr.dev/"
target="_blank"
leftIcon={<IconWorldWww size={20} />}
variant="default"
fullWidth
>
Documentation
</Button>
</Grid.Col>
<Grid.Col md={4} xs={12}>
<Button
component="a"
href="https://discord.gg/aCsmEV5RgA"
target="_blank"
leftIcon={<IconBrandDiscord size={20} />}
variant="default"
fullWidth
>
Discord
</Button>
</Grid.Col>
</Grid>
<Credits />
</Modal>
);
};
interface InformationTableItem {
icon: ReactNode;
label: string;
content: ReactNode;
}
interface ExtendedInitOptions extends InitOptions {
locales: string[];
}
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
const { attributes } = usePackageAttributesStore();
const { primaryColor } = useColorTheme();
const { configVersion } = useConfigContext();
const { configs } = useConfigStore();
let items: InformationTableItem[] = [];
if (i18n?.reportNamespaces) {
const usedI18nNamespaces = i18n.reportNamespaces.getUsedNamespaces();
const initOptions = i18n.options as ExtendedInitOptions;
items = [
...items,
{
icon: <IconLanguage size={20} />,
label: 'i18n',
content: (
<Badge variant="light" color={primaryColor}>
{usedI18nNamespaces.length}
</Badge>
),
},
{
icon: <IconVocabulary size={20} />,
label: 'locales',
content: (
<Badge variant="light" color={primaryColor}>
{initOptions.locales.length}
</Badge>
),
},
];
}
items = [
{
icon: <IconSchema size={20} />,
label: 'configurationSchemaVersion',
content: (
<Badge variant="light" color={primaryColor}>
{configVersion}
</Badge>
),
},
{
icon: <IconFile size={20} />,
label: 'configurationsCount',
content: (
<Badge variant="light" color={primaryColor}>
{configs.length}
</Badge>
),
},
{
icon: <IconVersions size={20} />,
label: 'version',
content: (
<Group position="right">
<Badge variant="light" color={primaryColor}>
{attributes.packageVersion ?? 'Unknown'}
</Badge>
{newVersionAvailable && (
<HoverCard shadow="md" position="top" withArrow>
<HoverCard.Target>
<motion.div
initial={{ scale: 1.2 }}
animate={{
scale: [0.8, 1.1, 1],
rotate: [0, 10, 0],
}}
transition={{ duration: 0.8, ease: 'easeInOut' }}
>
<Badge color="green" variant="filled">
new: {newVersionAvailable}
</Badge>
</motion.div>
</HoverCard.Target>
<HoverCard.Dropdown>
Version{' '}
<b>
<Anchor
target="_blank"
href={`https://github.com/ajnart/homarr/releases/tag/${newVersionAvailable}`}
>
{newVersionAvailable}
</Anchor>
</b>{' '}
is available ! Current version: {attributes.packageVersion}
</HoverCard.Dropdown>
</HoverCard>
)}
</Group>
),
},
{
icon: <IconAnchor size={20} />,
label: 'nodeEnvironment',
content: (
<Badge variant="light" color={primaryColor}>
{attributes.environment}
</Badge>
),
},
...items,
];
return items;
};
const useStyles = createStyles(() => ({
informationTableColumn: {
textAlign: 'right',
},
informationIcon: {
cursor: 'default',
},
}));

View File

@@ -0,0 +1,73 @@
import { Anchor, Box, Collapse, Flex, Table, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useTranslation } from 'next-i18next';
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
export default function Credits() {
const { t } = useTranslation('settings/common');
return (
<Flex justify="center" direction="column" mt="xs">
<Text
style={{
fontSize: '0.90rem',
textAlign: 'center',
color: 'gray',
}}
>
{t('credits.madeWithLove')}
<Anchor
href="https://github.com/ajnart"
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
>
ajnart
</Anchor>{' '}
and you!
</Text>
<DependencyTable />
</Flex>
);
}
const DependencyTable = () => {
const { t } = useTranslation('settings/common');
const [opened, { toggle }] = useDisclosure(false);
const { attributes } = usePackageAttributesStore();
return (
<>
<Text variant="link" mx="auto" size="xs" opacity={0.3} onClick={toggle}>
{t('credits.thirdPartyContent')}
</Text>
<Collapse in={opened}>
<Box
sx={(theme) => ({
backgroundColor:
theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0],
padding: theme.spacing.xl,
borderRadius: theme.radius.md,
})}
mt="md"
>
<Table>
<thead>
<tr>
<th>{t('credits.thirdPartyContentTable.dependencyName')}</th>
<th>{t('credits.thirdPartyContentTable.dependencyVersion')}</th>
</tr>
</thead>
{Object.keys(attributes.dependencies).map((key, index) => (
<tbody key={`dependency-${index}`}>
<tr>
<td>{key}</td>
<td>{attributes.dependencies[key]}</td>
</tr>
</tbody>
))}
</Table>
</Box>
</Collapse>
</>
);
};

View File

@@ -2,7 +2,7 @@ import { Button, ButtonProps } from '@mantine/core';
import Link from 'next/link';
import { ForwardedRef, forwardRef } from 'react';
import { useCardStyles } from '../useCardStyles';
import { useCardStyles } from '../Common/useCardStyles';
type SpecificLinkProps = {
component: typeof Link;

View File

@@ -14,7 +14,7 @@ import { User } from 'next-auth';
import { signOut, useSession } from 'next-auth/react';
import Link from 'next/link';
import { forwardRef } from 'react';
import { AboutModal } from '~/components/Dashboard/Modals/AboutModal/AboutModal';
import { AboutModal } from '~/components/layout/Header/About/AboutModal';
import { useColorScheme } from '~/hooks/use-colorscheme';
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';

View File

@@ -13,7 +13,7 @@ import { IconAlertTriangle } from '@tabler/icons-react';
import Link from 'next/link';
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
import { Logo } from '../Logo';
import { Logo } from '../Common/Logo';
import { AvatarMenu } from './AvatarMenu';
import { Search } from './Search';

View File

@@ -13,7 +13,7 @@ import { ReactNode, forwardRef, useEffect, useMemo, useRef, useState } from 'rea
import { useConfigContext } from '~/config/provider';
import { api } from '~/utils/api';
import { MovieModal } from './MovieModal';
import { MovieModal } from './Search/MovieModal';
type SearchProps = {
isMobile?: boolean;