✨ Completely rework the about page
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user