💄 Polish layouts

This commit is contained in:
Meier Lukas
2023-08-01 01:13:21 +02:00
parent fff6e6c077
commit df890b8c0a
30 changed files with 556 additions and 1345 deletions

View File

@@ -0,0 +1,196 @@
import { Button, Text, Title, Tooltip, clsx } from '@mantine/core';
import { useHotkeys, useWindowEvent } from '@mantine/hooks';
import { openContextModal } from '@mantine/modals';
import { hideNotification, showNotification } from '@mantine/notifications';
import {
IconApps,
IconBrandDocker,
IconEditCircle,
IconEditCircleOff,
IconSettings,
} from '@tabler/icons-react';
import Consola from 'consola';
import { useSession } from 'next-auth/react';
import { Trans, useTranslation } from 'next-i18next';
import Link from 'next/link';
import { useEditModeStore } from '~/components/Dashboard/Views/useEditModeStore';
import { useNamedWrapperColumnCount } from '~/components/Dashboard/Wrappers/gridstack/store';
import { useConfigContext } from '~/config/provider';
import { env } from '~/env';
import { api } from '~/utils/api';
import { Background } from '../Background';
import { HeaderActionButton } from '../Header/ActionButton';
import { MainLayout } from './MainLayout';
type BoardLayoutProps = {
children: React.ReactNode;
};
export const BoardLayout = ({ children }: BoardLayoutProps) => {
const { config } = useConfigContext();
return (
<MainLayout headerActions={<HeaderActions />}>
<Background />
{children}
<style>{clsx(config?.settings.customization.customCss)}</style>
</MainLayout>
);
};
export const HeaderActions = () => {
const { data: sessionData } = useSession();
if (!sessionData?.user?.isAdmin) return null;
return (
<>
{env.NEXT_PUBLIC_DOCKER_ENABLED && <DockerButton />}
<ToggleEditModeButton />
<CustomizeBoardButton />
</>
);
};
const DockerButton = () => {
const { t } = useTranslation('modules/docker');
return (
<Tooltip label={t('actionIcon.tooltip')}>
<HeaderActionButton component={Link} href="/docker">
<IconBrandDocker size={20} stroke={1.5} />
</HeaderActionButton>
</Tooltip>
);
};
const CustomizeBoardButton = () => {
const { name } = useConfigContext();
return (
<Tooltip label="Customize board">
<HeaderActionButton component={Link} href={`/board/${name}/customize`}>
<IconSettings size={20} stroke={1.5} />
</HeaderActionButton>
</Tooltip>
);
};
const beforeUnloadEventText = 'Exit the edit mode to save your changes';
const editModeNotificationId = 'toggle-edit-mode';
const ToggleEditModeButton = () => {
const { enabled, toggleEditMode } = useEditModeStore();
const { config, name: configName } = useConfigContext();
const { mutateAsync: saveConfig } = api.config.save.useMutation();
const namedWrapperColumnCount = useNamedWrapperColumnCount();
const { t } = useTranslation(['layout/header/actions/toggle-edit-mode', 'common']);
const translatedSize =
namedWrapperColumnCount !== null
? t(`common:breakPoints.${namedWrapperColumnCount}`)
: t('common:loading');
useHotkeys([['mod+E', toggleEditMode]]);
useWindowEvent('beforeunload', (event: BeforeUnloadEvent) => {
if (enabled) {
// eslint-disable-next-line no-param-reassign
event.returnValue = beforeUnloadEventText;
return beforeUnloadEventText;
}
return undefined;
});
const save = async () => {
toggleEditMode();
if (!config || !configName) return;
await saveConfig({ name: configName, config });
Consola.log('Saved config to server', configName);
hideNotification(editModeNotificationId);
};
const enableEditMode = () => {
toggleEditMode();
showNotification({
styles: (theme) => ({
root: {
backgroundColor: theme.colors.orange[7],
borderColor: theme.colors.orange[7],
'&::before': { backgroundColor: theme.white },
},
title: { color: theme.white },
description: { color: theme.white },
closeButton: {
color: theme.white,
'&:hover': { backgroundColor: theme.colors.orange[7] },
},
}),
radius: 'md',
id: 'toggle-edit-mode',
autoClose: 10000,
title: (
<Title order={4}>
<Trans
i18nKey="layout/header/actions/toggle-edit-mode:popover.title"
values={{ size: translatedSize }}
components={{
1: (
<Text
component="a"
style={{ color: 'inherit', textDecoration: 'underline' }}
href="https://homarr.dev/docs/customizations/layout"
target="_blank"
/>
),
}}
/>
</Title>
),
message: <Trans i18nKey="layout/header/actions/toggle-edit-mode:popover.text" />,
});
};
if (enabled) {
return (
<Button.Group>
<Tooltip label={t('button.disabled')}>
<HeaderActionButton onClick={save}>
<IconEditCircleOff size={20} stroke={1.5} />
</HeaderActionButton>
</Tooltip>
<AddElementButton />
</Button.Group>
);
}
return (
<Tooltip label={t('button.disabled')}>
<HeaderActionButton onClick={enableEditMode}>
<IconEditCircle size={20} stroke={1.5} />
</HeaderActionButton>
</Tooltip>
);
};
const AddElementButton = () => {
const { t } = useTranslation('layout/element-selector/selector');
return (
<Tooltip label={t('actionIcon.tooltip')}>
<HeaderActionButton
onClick={() =>
openContextModal({
modal: 'selectElement',
title: t('modal.title'),
size: 'xl',
innerProps: {},
})
}
>
<IconApps size={20} stroke={1.5} />
</HeaderActionButton>
</Tooltip>
);
};

View File

@@ -0,0 +1,28 @@
import { AppShell, useMantineTheme } from '@mantine/core';
import { MainHeader } from '../Header/Header';
import { Head } from '../Meta/Head';
type MainLayoutProps = {
headerActions?: React.ReactNode;
children: React.ReactNode;
};
export const MainLayout = ({ headerActions, children }: MainLayoutProps) => {
const theme = useMantineTheme();
return (
<AppShell
styles={{
root: {
background: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1],
},
}}
header={<MainHeader headerActions={headerActions} />}
className="dashboard-app-shell"
>
<Head />
{children}
</AppShell>
);
};

View File

@@ -0,0 +1,188 @@
import {
AppShell,
Burger,
Drawer,
Flex,
Footer,
Group,
NavLink,
Navbar,
Paper,
Text,
ThemeIcon,
useMantineTheme,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import {
IconBook2,
IconBrandDiscord,
IconBrandGithub,
IconGitFork,
IconHome,
IconLayoutDashboard,
IconMailForward,
IconQuestionMark,
IconSettings2,
IconUser,
IconUsers,
} from '@tabler/icons-react';
import Image from 'next/image';
import Link from 'next/link';
import { ReactNode } from 'react';
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;
}
export const ManageLayout = ({ children }: ManageLayoutProps) => {
const { attributes } = usePackageAttributesStore();
const theme = useMantineTheme();
const screenLargerThanMd = useScreenLargerThan('md');
const [burgerMenuOpen, { toggle: toggleBurgerMenu, close: closeBurgerMenu }] =
useDisclosure(false);
const navigationLinks = (
<>
<NavLink
icon={
<ThemeIcon size="md" variant="light" color="red">
<IconHome size="1rem" />
</ThemeIcon>
}
label="Home"
component={Link}
href="/manage/"
/>
<NavLink
label="Boards"
icon={
<ThemeIcon size="md" variant="light" color="red">
<IconLayoutDashboard size="1rem" />
</ThemeIcon>
}
component={Link}
href="/manage/boards"
/>
<NavLink
label="Users"
icon={
<ThemeIcon size="md" variant="light" color="red">
<IconUser size="1rem" />
</ThemeIcon>
}
>
<NavLink
icon={<IconUsers size="1rem" />}
label="Manage"
component={Link}
href="/manage/users"
/>
<NavLink
icon={<IconMailForward size="1rem" />}
label="Invites"
component={Link}
href="/manage/users/invites"
/>
</NavLink>
<NavLink
label="Settings"
icon={
<ThemeIcon size="md" variant="light" color="red">
<IconSettings2 size="1rem" />
</ThemeIcon>
}
component={Link}
href="/manage/settings"
/>
<NavLink
label="Help"
icon={
<ThemeIcon size="md" variant="light" color="red">
<IconQuestionMark size="1rem" />
</ThemeIcon>
}
>
<NavLink
icon={<IconBook2 size="1rem" />}
component="a"
href="https://homarr.dev/docs/about"
label="Documentation"
/>
<NavLink
icon={<IconBrandGithub size="1rem" />}
component="a"
href="https://github.com/ajnart/homarr/issues/new/choose"
label="Report an issue / bug"
/>
<NavLink
icon={<IconBrandDiscord size="1rem" />}
component="a"
href="https://discord.com/invite/aCsmEV5RgA"
label="Community Discord"
/>
<NavLink
icon={<IconGitFork size="1rem" />}
component="a"
href="https://github.com/ajnart/homarr"
label="Contribute"
/>
</NavLink>
</>
);
const burgerMenu = screenLargerThanMd ? undefined : (
<Burger opened={burgerMenuOpen} onClick={toggleBurgerMenu} />
);
return (
<>
<CommonHeader />
<AppShell
styles={{
root: {
background: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1],
},
}}
navbar={
<Navbar width={{ base: !screenLargerThanMd ? 0 : 220 }} hidden={!screenLargerThanMd}>
<Navbar.Section pt="xs" grow>
{navigationLinks}
</Navbar.Section>
</Navbar>
}
header={<MainHeader showExperimental logoHref="/manage" leftIcon={burgerMenu} />}
footer={
<Footer height={25}>
<Group position="apart" px="md">
<Flex gap="md" align="center" columnGap={5}>
<Image src="/imgs/logo/logo.svg" width={20} height={20} alt="" />
<Text fw="bold" size={15}>
Homarr
</Text>
{attributes.packageVersion && (
<Text color="dimmed" size={13}>
{attributes.packageVersion}
</Text>
)}
</Flex>
</Group>
</Footer>
}
>
<Paper p="xl" mih="100%" withBorder>
{children}
</Paper>
</AppShell>
<Drawer opened={burgerMenuOpen} onClose={closeBurgerMenu}>
{navigationLinks}
</Drawer>
</>
);
};