Completely rework the about page

This commit is contained in:
ajnart
2023-11-01 02:53:39 +01:00
parent 6f12863863
commit 58e57492e2
12 changed files with 513 additions and 812 deletions

View File

@@ -5,6 +5,7 @@ import {
Flex,
Footer,
Group,
Indicator,
NavLink,
Navbar,
Paper,
@@ -20,6 +21,8 @@ import {
IconBrandGithub,
IconGitFork,
IconHome,
IconInfoCircle,
IconInfoSmall,
IconLayoutDashboard,
IconMailForward,
IconQuestionMark,
@@ -28,6 +31,7 @@ import {
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';
@@ -36,7 +40,9 @@ 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';
@@ -46,7 +52,18 @@ interface ManageLayoutProps {
export const ManageLayout = ({ children }: ManageLayoutProps) => {
const packageVersion = usePackageAttributesStore((x) => x.attributes.packageVersion);
const theme = useMantineTheme();
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');
@@ -56,6 +73,162 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
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: (
<ConditionalWrapper
condition={navigationLink.displayUpdate === true}
wrapper={(children) => (
<Indicator withBorder offset={2} color="blue" processing size={12}>
{children}
</Indicator>
)}
>
<ThemeIcon size="md" variant="light" color="red">
<navigationLink.icon size={16} />
</ThemeIcon>
</ConditionalWrapper>
),
defaultOpened: false,
};
if ('href' in navigationLink) {
const isActive = router.pathname.endsWith(navigationLink.href);
return (
<NavLink
{...commonProps}
ref={ref as RefObject<HTMLAnchorElement>}
component={Link}
href={navigationLink.href}
active={isActive}
/>
);
}
const isAnyActive = Object.entries(navigationLink.items)
.map(([_, item]) => item.href)
.some((href) => router.pathname.endsWith(href));
return (
<NavLink
{...commonProps}
defaultOpened={isAnyActive}
ref={ref as RefObject<HTMLButtonElement>}
>
{Object.entries(navigationLink.items).map(([itemName, item], index) => {
const commonItemProps = {
label: t(`navigation.${name}.items.${itemName}`),
icon: <item.icon size={16} />,
href: item.href,
};
const matchesActive = router.pathname.endsWith(item.href);
if (item.href.startsWith('http')) {
return (
<NavLink
{...commonItemProps}
active={matchesActive}
target={item.target}
key={index}
component="a"
/>
);
}
return (
<NavLink {...commonItemProps} active={matchesActive} component={Link} key={index} />
);
})}
</NavLink>
);
});
type NavigationLinks = {
[key in keyof typeof navigation]: (typeof navigation)[key] extends {
items: Record<string, string>;
}
? NavigationLinkItems<(typeof navigation)[key]['items']>
: NavigationLinkHref;
};
const navigationLinkComponents = Object.entries(navigationLinks).map(([name, navigationLink]) => {
if (navigationLink.onlyAdmin && !isAdmin) {
return null;
@@ -77,11 +250,6 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
return (
<>
<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>
@@ -108,9 +276,7 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
</Footer>
}
>
<Paper p="xl" mih="100%" withBorder>
{children}
</Paper>
{children}
</AppShell>
<Drawer
opened={burgerMenuOpen}
@@ -132,146 +298,12 @@ type NavigationLinkHref = {
href: string;
target?: '_self' | '_blank';
onlyAdmin?: boolean;
displayUpdate?: boolean;
};
type NavigationLinkItems<TItemsObject> = {
icon: Icon;
items: Record<keyof TItemsObject, NavigationLinkHref>;
onlyAdmin?: boolean;
};
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: (
<ThemeIcon size="md" variant="light" color="red">
<navigationLink.icon size={16} />
</ThemeIcon>
),
defaultOpened: false,
};
if ('href' in navigationLink) {
const isActive = router.pathname.endsWith(navigationLink.href);
return (
<NavLink
{...commonProps}
ref={ref as RefObject<HTMLAnchorElement>}
component={Link}
href={navigationLink.href}
active={isActive}
/>
);
}
const isAnyActive = Object.entries(navigationLink.items)
.map(([_, item]) => item.href)
.some((href) => router.pathname.endsWith(href));
return (
<NavLink {...commonProps} defaultOpened={isAnyActive} ref={ref as RefObject<HTMLButtonElement>}>
{Object.entries(navigationLink.items).map(([itemName, item], index) => {
const commonItemProps = {
label: t(`navigation.${name}.items.${itemName}`),
icon: <item.icon size={16} />,
href: item.href,
};
const matchesActive = router.pathname.endsWith(item.href);
if (item.href.startsWith('http')) {
return (
<NavLink
{...commonItemProps}
active={matchesActive}
target={item.target}
key={index}
component="a"
/>
);
}
return <NavLink {...commonItemProps} active={matchesActive} component={Link} key={index} />;
})}
</NavLink>
);
});
type NavigationLinks = {
[key in keyof typeof navigation]: (typeof navigation)[key] extends {
items: Record<string, string>;
}
? NavigationLinkItems<(typeof navigation)[key]['items']>
: NavigationLinkHref;
};
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',
},
},
},
displayUpdate?: boolean;
};