💄 Polish layouts
This commit is contained in:
196
src/components/layout/Templates/BoardLayout.tsx
Normal file
196
src/components/layout/Templates/BoardLayout.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
28
src/components/layout/Templates/MainLayout.tsx
Normal file
28
src/components/layout/Templates/MainLayout.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
188
src/components/layout/Templates/ManageLayout.tsx
Normal file
188
src/components/layout/Templates/ManageLayout.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user