import { AppShell, Burger, Drawer, Flex, Footer, Group, Indicator, NavLink, Navbar, Paper, Text, ThemeIcon, useMantineTheme, } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { IconBook2, IconBrandDiscord, IconBrandDocker, IconBrandGithub, IconGitFork, IconHome, IconInfoCircle, IconInfoSmall, IconLayoutDashboard, IconMailForward, IconQuestionMark, IconTool, IconUser, IconUsers, TablerIconsProps, } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { useSession } from 'next-auth/react'; import { useTranslation } from 'next-i18next'; import Image from 'next/image'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { ReactNode, RefObject, forwardRef } from 'react'; import { useScreenLargerThan } from '~/hooks/useScreenLargerThan'; import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore'; import { ConditionalWrapper } from '~/utils/security'; import { REPO_URL } from '../../../../data/constants'; import { type navigation } from '../../../../public/locales/en/layout/manage.json'; import { MainHeader } from '../header/Header'; interface ManageLayoutProps { children: ReactNode; } export const ManageLayout = ({ children }: ManageLayoutProps) => { const packageVersion = usePackageAttributesStore((x) => x.attributes.packageVersion); const { data: newVersion } = useQuery({ queryKey: ['github/latest'], cacheTime: 1000 * 60 * 60 * 24, staleTime: 1000 * 60 * 60 * 5, queryFn: () => fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`, { cache: 'force-cache', }).then((res) => res.json()), }); const { attributes } = usePackageAttributesStore(); const newVersionAvailable = newVersion?.tag_name > `v${attributes.packageVersion}` ? newVersion?.tag_name : undefined; const screenLargerThanMd = useScreenLargerThan('md'); const [burgerMenuOpen, { toggle: toggleBurgerMenu, close: closeBurgerMenu }] = useDisclosure(false); const data = useSession(); const isAdmin = data.data?.user.isAdmin ?? false; const navigationLinks: NavigationLinks = { home: { icon: IconHome, href: '/manage', }, boards: { icon: IconLayoutDashboard, href: '/manage/boards', }, users: { icon: IconUser, onlyAdmin: true, items: { manage: { icon: IconUsers, href: '/manage/users', }, invites: { icon: IconMailForward, href: '/manage/users/invites', }, }, }, tools: { icon: IconTool, onlyAdmin: true, items: { docker: { icon: IconBrandDocker, href: '/manage/tools/docker', }, }, }, help: { icon: IconQuestionMark, items: { documentation: { icon: IconBook2, href: 'https://homarr.dev/docs/about', target: '_blank', }, report: { icon: IconBrandGithub, href: 'https://github.com/ajnart/homarr/issues/new/choose', target: '_blank', }, discord: { icon: IconBrandDiscord, href: 'https://discord.com/invite/aCsmEV5RgA', target: '_blank', }, contribute: { icon: IconGitFork, href: 'https://github.com/ajnart/homarr', target: '_blank', }, }, }, about: { icon: IconInfoSmall, displayUpdate: newVersionAvailable !== undefined, href: '/manage/about', }, }; type CustomNavigationLinkProps = { name: keyof typeof navigationLinks; navigationLink: (typeof navigationLinks)[keyof typeof navigationLinks]; }; const CustomNavigationLink = forwardRef< HTMLAnchorElement | HTMLButtonElement, CustomNavigationLinkProps >(({ name, navigationLink }, ref) => { const { t } = useTranslation('layout/manage'); const router = useRouter(); const commonProps = { label: t(`navigation.${name}.title`), icon: ( ( {children} )} > ), defaultOpened: false, }; if ('href' in navigationLink) { const isActive = router.pathname.endsWith(navigationLink.href); return ( } component={Link} href={navigationLink.href} active={isActive} /> ); } const isAnyActive = Object.entries(navigationLink.items) .map(([_, item]) => item.href) .some((href) => router.pathname.endsWith(href)); return ( } > {Object.entries(navigationLink.items).map(([itemName, item], index) => { const commonItemProps = { label: t(`navigation.${name}.items.${itemName}`), icon: , href: item.href, }; const matchesActive = router.pathname.endsWith(item.href); if (item.href.startsWith('http')) { return ( ); } return ( ); })} ); }); type NavigationLinks = { [key in keyof typeof navigation]: (typeof navigation)[key] extends { items: Record; } ? NavigationLinkItems<(typeof navigation)[key]['items']> : NavigationLinkHref; }; const navigationLinkComponents = Object.entries(navigationLinks).map(([name, navigationLink]) => { if (navigationLink.onlyAdmin && !isAdmin) { return null; } return ( ); }); const burgerMenu = screenLargerThanMd ? undefined : ( ); return ( <> {navigationLinkComponents} ); }; type Icon = (props: TablerIconsProps) => JSX.Element; type NavigationLinkHref = { icon: Icon; href: string; target?: '_self' | '_blank'; onlyAdmin?: boolean; displayUpdate?: boolean; }; type NavigationLinkItems = { icon: Icon; items: Record; onlyAdmin?: boolean; displayUpdate?: boolean; };