✨ Completely rework the about page
This commit is contained in:
@@ -17,10 +17,6 @@
|
|||||||
"preferences": "User preferences",
|
"preferences": "User preferences",
|
||||||
"defaultBoard": "Default dashboard",
|
"defaultBoard": "Default dashboard",
|
||||||
"manage": "Manage",
|
"manage": "Manage",
|
||||||
"about": {
|
|
||||||
"label": "About",
|
|
||||||
"new": "New"
|
|
||||||
},
|
|
||||||
"logout": "Logout from {{username}}",
|
"logout": "Logout from {{username}}",
|
||||||
"login": "Login"
|
"login": "Login"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"docker": "Docker"
|
"docker": "Docker"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "About"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
{
|
{
|
||||||
"description": "Homarr is a <strong>sleek</strong>, <strong>modern</strong> dashboard that puts all of your apps and services at your fingertips. With Homarr, you can access and control everything in one convenient location. Homarr seamlessly integrates with the apps you've added, providing you with valuable information and giving you complete control. Installation is a breeze, and Homarr supports a wide range of deployment methods.",
|
"description": "Homarr is a <strong>sleek</strong>, <strong>modern</strong> dashboard that puts all of your apps and services at your fingertips. With Homarr, you can access and control everything in one convenient location. Homarr seamlessly integrates with the apps you've added, providing you with valuable information and giving you complete control. Installation is a breeze, and Homarr supports a wide range of deployment methods.",
|
||||||
"contact": "Having trouble or questions? Connect with us!",
|
|
||||||
"addToDashboard": "Add to Dashboard",
|
"addToDashboard": "Add to Dashboard",
|
||||||
"tip": "Mod refers to your modifier key, it is Ctrl and Command/Super/Windows key",
|
"tip": "Mod refers to your modifier key, it is Ctrl and Command/Super/Windows key",
|
||||||
"key": "Shortcut key",
|
"key": "Shortcut key",
|
||||||
"action": "Action",
|
"action": "Action",
|
||||||
"keybinds": "Keybinds",
|
"keybinds": "Keybinds",
|
||||||
"documentation": "Documentation",
|
"translators": "Translators ({{count}})",
|
||||||
|
"translatorsDescription": "Thanks to these people, Homarr is available in {{languages}} languages! Want to help translate Homarr into your language? Read how to do so <a>here</a>.",
|
||||||
|
"contributors": "Contributors ({{count}})",
|
||||||
|
"contributorsDescription": "These people have built the code that makes homarr work! Want to help build Homarr? Read how to do so <a>here</a>",
|
||||||
"actions": {
|
"actions": {
|
||||||
"toggleTheme": "Toggle light/dark mode",
|
"toggleTheme": "Toggle light/dark mode",
|
||||||
"focusSearchBar": "Focus on search bar",
|
"focusSearchBar": "Focus on search bar",
|
||||||
@@ -15,7 +17,6 @@
|
|||||||
},
|
},
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"configurationSchemaVersion": "Configuration schema version",
|
"configurationSchemaVersion": "Configuration schema version",
|
||||||
"configurationsCount": "Available configurations",
|
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
"nodeEnvironment": "Node environment",
|
"nodeEnvironment": "Node environment",
|
||||||
"i18n": "Loaded I18n translation namespaces",
|
"i18n": "Loaded I18n translation namespaces",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
Footer,
|
Footer,
|
||||||
Group,
|
Group,
|
||||||
|
Indicator,
|
||||||
NavLink,
|
NavLink,
|
||||||
Navbar,
|
Navbar,
|
||||||
Paper,
|
Paper,
|
||||||
@@ -20,6 +21,8 @@ import {
|
|||||||
IconBrandGithub,
|
IconBrandGithub,
|
||||||
IconGitFork,
|
IconGitFork,
|
||||||
IconHome,
|
IconHome,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconInfoSmall,
|
||||||
IconLayoutDashboard,
|
IconLayoutDashboard,
|
||||||
IconMailForward,
|
IconMailForward,
|
||||||
IconQuestionMark,
|
IconQuestionMark,
|
||||||
@@ -28,6 +31,7 @@ import {
|
|||||||
IconUsers,
|
IconUsers,
|
||||||
TablerIconsProps,
|
TablerIconsProps,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
@@ -36,7 +40,9 @@ import { useRouter } from 'next/router';
|
|||||||
import { ReactNode, RefObject, forwardRef } from 'react';
|
import { ReactNode, RefObject, forwardRef } from 'react';
|
||||||
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
|
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
|
||||||
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
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 { type navigation } from '../../../../public/locales/en/layout/manage.json';
|
||||||
import { MainHeader } from '../header/Header';
|
import { MainHeader } from '../header/Header';
|
||||||
|
|
||||||
@@ -46,7 +52,18 @@ interface ManageLayoutProps {
|
|||||||
|
|
||||||
export const ManageLayout = ({ children }: ManageLayoutProps) => {
|
export const ManageLayout = ({ children }: ManageLayoutProps) => {
|
||||||
const packageVersion = usePackageAttributesStore((x) => x.attributes.packageVersion);
|
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');
|
const screenLargerThanMd = useScreenLargerThan('md');
|
||||||
|
|
||||||
@@ -56,6 +73,162 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
|
|||||||
const data = useSession();
|
const data = useSession();
|
||||||
const isAdmin = data.data?.user.isAdmin ?? false;
|
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]) => {
|
const navigationLinkComponents = Object.entries(navigationLinks).map(([name, navigationLink]) => {
|
||||||
if (navigationLink.onlyAdmin && !isAdmin) {
|
if (navigationLink.onlyAdmin && !isAdmin) {
|
||||||
return null;
|
return null;
|
||||||
@@ -77,11 +250,6 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppShell
|
<AppShell
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
background: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1],
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
navbar={
|
navbar={
|
||||||
<Navbar width={{ base: !screenLargerThanMd ? 0 : 220 }} hidden={!screenLargerThanMd}>
|
<Navbar width={{ base: !screenLargerThanMd ? 0 : 220 }} hidden={!screenLargerThanMd}>
|
||||||
<Navbar.Section pt="xs" grow>
|
<Navbar.Section pt="xs" grow>
|
||||||
@@ -108,9 +276,7 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
|
|||||||
</Footer>
|
</Footer>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Paper p="xl" mih="100%" withBorder>
|
{children}
|
||||||
{children}
|
|
||||||
</Paper>
|
|
||||||
</AppShell>
|
</AppShell>
|
||||||
<Drawer
|
<Drawer
|
||||||
opened={burgerMenuOpen}
|
opened={burgerMenuOpen}
|
||||||
@@ -132,146 +298,12 @@ type NavigationLinkHref = {
|
|||||||
href: string;
|
href: string;
|
||||||
target?: '_self' | '_blank';
|
target?: '_self' | '_blank';
|
||||||
onlyAdmin?: boolean;
|
onlyAdmin?: boolean;
|
||||||
|
displayUpdate?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type NavigationLinkItems<TItemsObject> = {
|
type NavigationLinkItems<TItemsObject> = {
|
||||||
icon: Icon;
|
icon: Icon;
|
||||||
items: Record<keyof TItemsObject, NavigationLinkHref>;
|
items: Record<keyof TItemsObject, NavigationLinkHref>;
|
||||||
onlyAdmin?: boolean;
|
onlyAdmin?: boolean;
|
||||||
};
|
displayUpdate?: 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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,325 +0,0 @@
|
|||||||
import {
|
|
||||||
Accordion,
|
|
||||||
ActionIcon,
|
|
||||||
Anchor,
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Grid,
|
|
||||||
Group,
|
|
||||||
HoverCard,
|
|
||||||
Image,
|
|
||||||
Kbd,
|
|
||||||
Modal,
|
|
||||||
Table,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
createStyles,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import {
|
|
||||||
IconAnchor,
|
|
||||||
IconBrandDiscord,
|
|
||||||
IconBrandGithub,
|
|
||||||
IconFile,
|
|
||||||
IconKey,
|
|
||||||
IconLanguage,
|
|
||||||
IconSchema,
|
|
||||||
IconVersions,
|
|
||||||
IconVocabulary,
|
|
||||||
IconWorldWww,
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { InitOptions } from 'i18next';
|
|
||||||
import { Trans, i18n, useTranslation } from 'next-i18next';
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { useConfigContext } from '~/config/provider';
|
|
||||||
import { useConfigStore } from '~/config/store';
|
|
||||||
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
|
||||||
import { useColorTheme } from '~/tools/color';
|
|
||||||
|
|
||||||
import { usePrimaryGradient } from '../../Common/useGradient';
|
|
||||||
import Credits from './Credits';
|
|
||||||
import Tip from './Tip';
|
|
||||||
import { TranslatorsTable } from './Translators';
|
|
||||||
import { ContributorsTable } from './Contributors';
|
|
||||||
|
|
||||||
interface AboutModalProps {
|
|
||||||
opened: boolean;
|
|
||||||
closeModal: () => void;
|
|
||||||
newVersionAvailable?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutModalProps) => {
|
|
||||||
const { classes } = useStyles();
|
|
||||||
const colorGradiant = usePrimaryGradient();
|
|
||||||
const informations = useInformationTableItems(newVersionAvailable);
|
|
||||||
const { t } = useTranslation(['common', 'layout/modals/about']);
|
|
||||||
|
|
||||||
const keybinds = [
|
|
||||||
{ key: 'Mod + J', shortcut: t('layout/modals/about:actions.toggleTheme') },
|
|
||||||
{ key: 'Mod + K', shortcut: t('layout/modals/about:actions.focusSearchBar') },
|
|
||||||
{ key: 'Mod + B', shortcut: t('layout/modals/about:actions.openDocker') },
|
|
||||||
{ key: 'Mod + E', shortcut: t('layout/modals/about:actions.toggleEdit') },
|
|
||||||
];
|
|
||||||
const rows = keybinds.map((element) => (
|
|
||||||
<tr key={element.key}>
|
|
||||||
<td>
|
|
||||||
<Kbd>{element.key}</Kbd>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Text>{element.shortcut}</Text>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
onClose={() => closeModal()}
|
|
||||||
opened={opened}
|
|
||||||
title={
|
|
||||||
<Group spacing="sm">
|
|
||||||
<Image alt="Homarr logo" src="/imgs/logo/logo.png" width={30} height={30} fit="contain" />
|
|
||||||
<Title order={3} variant="gradient" gradient={colorGradiant}>
|
|
||||||
{t('about')} Homarr
|
|
||||||
</Title>
|
|
||||||
</Group>
|
|
||||||
}
|
|
||||||
size="xl"
|
|
||||||
>
|
|
||||||
<Text mb="lg">
|
|
||||||
<Trans i18nKey="layout/modals/about:description" />
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Table mb="lg" highlightOnHover withBorder>
|
|
||||||
<tbody>
|
|
||||||
{informations.map((item, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>
|
|
||||||
<Group spacing="xs">
|
|
||||||
<ActionIcon className={classes.informationIcon} variant="default">
|
|
||||||
{item.icon}
|
|
||||||
</ActionIcon>
|
|
||||||
<Text>
|
|
||||||
<Trans
|
|
||||||
i18nKey={`layout/modals/about:metrics.${item.label}`}
|
|
||||||
components={{ b: <b /> }}
|
|
||||||
/>
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</td>
|
|
||||||
<td className={classes.informationTableColumn} style={{ maxWidth: 200 }}>
|
|
||||||
{item.content}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
<Accordion mb={5} variant="contained" radius="md">
|
|
||||||
<Accordion.Item value="keybinds">
|
|
||||||
<Accordion.Control icon={<IconKey size={20} />}>
|
|
||||||
{t('layout/modals/about:keybinds')}
|
|
||||||
</Accordion.Control>
|
|
||||||
<Accordion.Panel>
|
|
||||||
<Table mb={5}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{t('layout/modals/about:key')}</th>
|
|
||||||
<th>{t('layout/modals/about:action')}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>{rows}</tbody>
|
|
||||||
</Table>
|
|
||||||
<Tip>{t('layout/modals/about:tip')}</Tip>
|
|
||||||
</Accordion.Panel>
|
|
||||||
</Accordion.Item>
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Title order={6} mb="xs" align="center">
|
|
||||||
{t('layout/modals/about:contact')}
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
<Grid grow>
|
|
||||||
<Grid.Col md={4} xs={12}>
|
|
||||||
<Button
|
|
||||||
component="a"
|
|
||||||
href="https://github.com/ajnart/homarr"
|
|
||||||
target="_blank"
|
|
||||||
leftIcon={<IconBrandGithub size={20} />}
|
|
||||||
variant="default"
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</Button>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col md={4} xs={12}>
|
|
||||||
<Button
|
|
||||||
component="a"
|
|
||||||
href="https://homarr.dev/"
|
|
||||||
target="_blank"
|
|
||||||
leftIcon={<IconWorldWww size={20} />}
|
|
||||||
variant="default"
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
{t('layout/modals/about:documentation')}
|
|
||||||
</Button>
|
|
||||||
</Grid.Col>
|
|
||||||
|
|
||||||
<Grid.Col md={4} xs={12}>
|
|
||||||
<Button
|
|
||||||
component="a"
|
|
||||||
href="https://discord.gg/aCsmEV5RgA"
|
|
||||||
target="_blank"
|
|
||||||
leftIcon={<IconBrandDiscord size={20} />}
|
|
||||||
variant="default"
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
Discord
|
|
||||||
</Button>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
<Credits />
|
|
||||||
<TranslatorsTable />
|
|
||||||
<ContributorsTable />
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface InformationTableItem {
|
|
||||||
icon: ReactNode;
|
|
||||||
label: string;
|
|
||||||
content: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExtendedInitOptions extends InitOptions {
|
|
||||||
locales: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
|
|
||||||
const { attributes } = usePackageAttributesStore();
|
|
||||||
const { primaryColor } = useColorTheme();
|
|
||||||
const { t } = useTranslation(['layout/modals/about']);
|
|
||||||
|
|
||||||
const { configVersion } = useConfigContext();
|
|
||||||
const { configs } = useConfigStore();
|
|
||||||
|
|
||||||
let items: InformationTableItem[] = [];
|
|
||||||
|
|
||||||
if (i18n?.reportNamespaces) {
|
|
||||||
const usedI18nNamespaces = i18n.reportNamespaces.getUsedNamespaces();
|
|
||||||
const initOptions = i18n.options as ExtendedInitOptions;
|
|
||||||
|
|
||||||
items = [
|
|
||||||
...items,
|
|
||||||
{
|
|
||||||
icon: <IconLanguage size={20} />,
|
|
||||||
label: 'i18n',
|
|
||||||
content: (
|
|
||||||
<Badge variant="light" color={primaryColor}>
|
|
||||||
{usedI18nNamespaces.length}
|
|
||||||
</Badge>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconVocabulary size={20} />,
|
|
||||||
label: 'locales',
|
|
||||||
content: (
|
|
||||||
<Badge variant="light" color={primaryColor}>
|
|
||||||
{initOptions.locales.length}
|
|
||||||
</Badge>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
items = [
|
|
||||||
{
|
|
||||||
icon: <IconSchema size={20} />,
|
|
||||||
label: 'configurationSchemaVersion',
|
|
||||||
content: (
|
|
||||||
<Badge variant="light" color={primaryColor}>
|
|
||||||
{configVersion}
|
|
||||||
</Badge>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconFile size={20} />,
|
|
||||||
label: 'configurationsCount',
|
|
||||||
content: (
|
|
||||||
<Badge variant="light" color={primaryColor}>
|
|
||||||
{configs.length}
|
|
||||||
</Badge>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconVersions size={20} />,
|
|
||||||
label: 'version',
|
|
||||||
content: (
|
|
||||||
<Group position="right">
|
|
||||||
<Badge variant="light" color={primaryColor}>
|
|
||||||
{attributes.packageVersion ?? 'Unknown'}
|
|
||||||
</Badge>
|
|
||||||
{newVersionAvailable && (
|
|
||||||
<HoverCard shadow="md" position="top" withArrow>
|
|
||||||
<HoverCard.Target>
|
|
||||||
<motion.div
|
|
||||||
initial={{ scale: 1.2 }}
|
|
||||||
animate={{
|
|
||||||
scale: [0.8, 1.1, 1],
|
|
||||||
rotate: [0, 10, 0],
|
|
||||||
}}
|
|
||||||
transition={{ duration: 0.8, ease: 'easeInOut' }}
|
|
||||||
>
|
|
||||||
<Badge color="green" variant="filled">
|
|
||||||
{t('version.new', { newVersion: newVersionAvailable })}
|
|
||||||
</Badge>
|
|
||||||
</motion.div>
|
|
||||||
</HoverCard.Target>
|
|
||||||
<HoverCard.Dropdown>
|
|
||||||
<Text>
|
|
||||||
{
|
|
||||||
t('version.dropdown', { currentVersion: attributes.packageVersion }).split(
|
|
||||||
'{{newVersion}}'
|
|
||||||
)[0]
|
|
||||||
}
|
|
||||||
<b>
|
|
||||||
<Anchor
|
|
||||||
target="_blank"
|
|
||||||
href={`https://github.com/ajnart/homarr/releases/tag/${newVersionAvailable}`}
|
|
||||||
>
|
|
||||||
{newVersionAvailable}
|
|
||||||
</Anchor>
|
|
||||||
</b>
|
|
||||||
{
|
|
||||||
t('version.dropdown', { currentVersion: attributes.packageVersion }).split(
|
|
||||||
'{{newVersion}}'
|
|
||||||
)[1]
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
</HoverCard.Dropdown>
|
|
||||||
</HoverCard>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconAnchor size={20} />,
|
|
||||||
label: 'nodeEnvironment',
|
|
||||||
content: (
|
|
||||||
<Badge variant="light" color={primaryColor}>
|
|
||||||
{attributes.environment}
|
|
||||||
</Badge>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
...items,
|
|
||||||
];
|
|
||||||
|
|
||||||
return items;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useStyles = createStyles(() => ({
|
|
||||||
informationTableColumn: {
|
|
||||||
textAlign: 'right',
|
|
||||||
},
|
|
||||||
informationIcon: {
|
|
||||||
cursor: 'default',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
@@ -2,16 +2,14 @@ import {
|
|||||||
Anchor,
|
Anchor,
|
||||||
Avatar,
|
Avatar,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Pagination,
|
||||||
ScrollArea,
|
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
Text,
|
Text,
|
||||||
createStyles,
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { usePagination } from '@mantine/hooks';
|
||||||
import { cache } from 'react';
|
import { Trans, useTranslation } from 'next-i18next';
|
||||||
import { REPO_URL } from '../../../../../data/constants';
|
|
||||||
|
|
||||||
// Generated by https://quicktype.io
|
// Generated by https://quicktype.io
|
||||||
|
|
||||||
@@ -42,53 +40,77 @@ export enum Type {
|
|||||||
User = 'User',
|
User = 'User',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContributorsTable() {
|
const PAGINATION_ITEMS = 20;
|
||||||
// Type data as Contributors
|
|
||||||
const { data, isFetching } = useQuery({
|
|
||||||
queryKey: ['contributors'],
|
|
||||||
cacheTime: 1000 * 60 * 60 * 24,
|
|
||||||
staleTime: 1000 * 60 * 60 * 5,
|
|
||||||
queryFn: () =>
|
|
||||||
fetch(`https://api.github.com/repos/${REPO_URL}/contributors?per_page=25`, {
|
|
||||||
cache: 'force-cache',
|
|
||||||
}).then((res) => res.json()) as Promise<Contributors[]>,
|
|
||||||
});
|
|
||||||
if (isFetching || !data) return <Loader />;
|
|
||||||
|
|
||||||
const rows = data.map((contributor) => (
|
export function ContributorsTable({ contributors }: { contributors: Contributors[] }) {
|
||||||
<tr key={contributor.id}>
|
const pagination = usePagination({
|
||||||
<td>
|
total: contributors.length / PAGINATION_ITEMS,
|
||||||
<Anchor href={`https://github.com/${contributor.login}`} target="_blank">
|
initialPage: 1,
|
||||||
<Group noWrap>
|
});
|
||||||
<Avatar
|
const { t } = useTranslation(['layout/modals/about']);
|
||||||
size={25}
|
|
||||||
radius="lg"
|
const rows = contributors
|
||||||
src={contributor.avatar_url}
|
.slice(
|
||||||
alt={contributor.login}
|
(pagination.active - 1) * PAGINATION_ITEMS,
|
||||||
/>
|
(pagination.active - 1) * PAGINATION_ITEMS + PAGINATION_ITEMS
|
||||||
{contributor.login}
|
)
|
||||||
</Group>
|
.map((contributor) => (
|
||||||
</Anchor>
|
<tr key={contributor.id}>
|
||||||
</td>
|
<td>
|
||||||
<td>{contributor.contributions}</td>
|
<Anchor href={`https://github.com/${contributor.login}`} target="_blank">
|
||||||
</tr>
|
<Group noWrap>
|
||||||
));
|
<Avatar size={25} radius="lg" src={contributor.avatar_url} alt={contributor.login} />
|
||||||
|
{contributor.login}
|
||||||
|
</Group>
|
||||||
|
</Anchor>
|
||||||
|
</td>
|
||||||
|
<td>{contributor.contributions}</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<h5>Credits to our amazing contributors</h5>
|
<Title order={3}>{t('contributors', { count: contributors.length })}</Title>
|
||||||
|
<Text>
|
||||||
|
<Trans
|
||||||
|
i18nKey="layout/modals/about:contributorsDescription"
|
||||||
|
components={{
|
||||||
|
a: <Anchor href="https://homarr.dev/docs/community/developer-guides" target="_blank" />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
|
||||||
<ScrollArea h={800}>
|
<Table withBorder>
|
||||||
<Table miw={700}>
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th
|
||||||
<th>Contributor</th>
|
style={{
|
||||||
<th>Contributions</th>
|
width: 400,
|
||||||
</tr>
|
}}
|
||||||
</thead>
|
>
|
||||||
<tbody>{rows}</tbody>
|
Contributor
|
||||||
</Table>
|
</th>
|
||||||
</ScrollArea>
|
<th
|
||||||
|
style={{
|
||||||
|
width: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Contributions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{rows}</tbody>
|
||||||
|
</Table>
|
||||||
|
<Pagination
|
||||||
|
style={{
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
total={contributors.length / PAGINATION_ITEMS}
|
||||||
|
value={pagination.active}
|
||||||
|
onNextPage={() => pagination.next()}
|
||||||
|
onPreviousPage={() => pagination.previous()}
|
||||||
|
onChange={(targetPage) => pagination.setPage(targetPage)}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Anchor, Box, Collapse, Flex, Table, Text } from '@mantine/core';
|
import { Anchor, Box, Button, Collapse, Container, Flex, Stack, Table, Text } from '@mantine/core';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
|
import { modals } from '@mantine/modals';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
||||||
|
|
||||||
@@ -7,7 +8,8 @@ export default function Credits() {
|
|||||||
const { t } = useTranslation('settings/common');
|
const { t } = useTranslation('settings/common');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex justify="center" direction="column" mt="xs">
|
<Stack>
|
||||||
|
<DependencyTable />
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: '0.90rem',
|
fontSize: '0.90rem',
|
||||||
@@ -24,49 +26,48 @@ export default function Credits() {
|
|||||||
</Anchor>{' '}
|
</Anchor>{' '}
|
||||||
and you!
|
and you!
|
||||||
</Text>
|
</Text>
|
||||||
<DependencyTable />
|
</Stack>
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DependencyTable = () => {
|
const DependencyTable = () => {
|
||||||
const { t } = useTranslation('settings/common');
|
const { t } = useTranslation('settings/common');
|
||||||
const [opened, { toggle }] = useDisclosure(false);
|
|
||||||
const { attributes } = usePackageAttributesStore();
|
const { attributes } = usePackageAttributesStore();
|
||||||
return (
|
return (
|
||||||
<>
|
<Button
|
||||||
<Text variant="link" mx="auto" size="xs" opacity={0.3} onClick={toggle}>
|
style={{
|
||||||
{t('credits.thirdPartyContent')}
|
justifyContent: 'start',
|
||||||
</Text>
|
}}
|
||||||
|
variant="light"
|
||||||
<Collapse in={opened}>
|
mx="auto"
|
||||||
<Box
|
size="xs"
|
||||||
sx={(theme) => ({
|
opacity={0.3}
|
||||||
backgroundColor:
|
onClick={() =>
|
||||||
theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0],
|
modals.open({
|
||||||
padding: theme.spacing.xl,
|
title: t('credits.thirdPartyContent'),
|
||||||
borderRadius: theme.radius.md,
|
size: 'xl',
|
||||||
})}
|
children: (
|
||||||
mt="md"
|
<Table>
|
||||||
>
|
<thead>
|
||||||
<Table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{t('credits.thirdPartyContentTable.dependencyName')}</th>
|
|
||||||
<th>{t('credits.thirdPartyContentTable.dependencyVersion')}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
{Object.keys(attributes.dependencies).map((key, index) => (
|
|
||||||
<tbody key={`dependency-${index}`}>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{key}</td>
|
<th>{t('credits.thirdPartyContentTable.dependencyName')}</th>
|
||||||
<td>{attributes.dependencies[key]}</td>
|
<th>{t('credits.thirdPartyContentTable.dependencyVersion')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Object.keys(attributes.dependencies).map((key, index) => (
|
||||||
|
<tr>
|
||||||
|
<td>{key}</td>
|
||||||
|
<td>{attributes.dependencies[key]}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
))}
|
</Table>
|
||||||
</Table>
|
),
|
||||||
</Box>
|
})
|
||||||
</Collapse>
|
}
|
||||||
</>
|
>
|
||||||
|
{t('credits.thirdPartyContent')}
|
||||||
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
.header {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background-color: var(--mantine-color-body);
|
|
||||||
transition: box-shadow 150ms ease;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
border-bottom: rem(1px) solid
|
|
||||||
light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-3));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrolled {
|
|
||||||
box-shadow: var(--mantine-shadow-sm);
|
|
||||||
}
|
|
||||||
@@ -1,60 +1,123 @@
|
|||||||
import { Anchor, Avatar, Group, ScrollArea, Stack, Table, Text, createStyles } from '@mantine/core';
|
import {
|
||||||
import cx from 'clsx';
|
Anchor,
|
||||||
import Link from 'next/link';
|
Avatar,
|
||||||
import { useState } from 'react';
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { usePagination } from '@mantine/hooks';
|
||||||
|
import { Trans, useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
import CrowdinReport from '../../../../../data/crowdin-report.json';
|
import CrowdinReport from '../../../../../data/crowdin-report.json';
|
||||||
|
|
||||||
export function TranslatorsTable() {
|
const PAGINATION_ITEMS = 20;
|
||||||
// Only the first 30 translators are shown
|
|
||||||
const translators = CrowdinReport.data.slice(0, 30);
|
|
||||||
|
|
||||||
const rows = translators.map((translator) => (
|
export function TranslatorsTable({ loadedLanguages }: { loadedLanguages: number }) {
|
||||||
<tr key={translator.user.id}>
|
const { t } = useTranslation(['layout/modals/about']);
|
||||||
<td>
|
const translators = CrowdinReport.data;
|
||||||
<Anchor href={`https://crowdin.com/profile/${translator.user.username}`} target="_blank">
|
const pagination = usePagination({
|
||||||
<Group noWrap>
|
total: translators.length / PAGINATION_ITEMS,
|
||||||
<Avatar
|
initialPage: 1,
|
||||||
size={25}
|
});
|
||||||
radius="lg"
|
|
||||||
src={translator.user.avatarUrl}
|
const rows = translators
|
||||||
alt={translator.user.username}
|
.slice(
|
||||||
/>
|
(pagination.active - 1) * PAGINATION_ITEMS,
|
||||||
{translator.user.username}
|
(pagination.active - 1) * PAGINATION_ITEMS + PAGINATION_ITEMS
|
||||||
</Group>
|
)
|
||||||
</Anchor>
|
.map((translator) => (
|
||||||
</td>
|
<tr key={translator.user.id}>
|
||||||
<td>{translator.translated}</td>
|
<td
|
||||||
<td>{translator.approved}</td>
|
style={{
|
||||||
<td>{translator.target}</td>
|
width: 400,
|
||||||
<td>
|
}}
|
||||||
<Text lineClamp={1}>
|
>
|
||||||
{translator.languages.map((language) => (
|
<Anchor href={`https://crowdin.com/profile/${translator.user.username}`} target="_blank">
|
||||||
<span key={language.id}>{language.name}, </span>
|
<Group noWrap>
|
||||||
))}
|
<Avatar
|
||||||
</Text>
|
size={25}
|
||||||
</td>
|
radius="lg"
|
||||||
</tr>
|
src={translator.user.avatarUrl}
|
||||||
));
|
alt={translator.user.username}
|
||||||
|
/>
|
||||||
|
{translator.user.fullName}
|
||||||
|
</Group>
|
||||||
|
</Anchor>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
width: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{translator.translated}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
width: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{translator.approved}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
width: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{translator.target}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
width: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text lineClamp={1}>
|
||||||
|
{translator.languages.map((language) => (
|
||||||
|
<span key={language.id}>{language.name}, </span>
|
||||||
|
))}
|
||||||
|
</Text>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<h5>Credits to our amazing translators</h5>
|
<Title order={3}>{t('translators', { count: translators.length })}</Title>
|
||||||
|
<Text>
|
||||||
<ScrollArea h={800}>
|
<Trans
|
||||||
<Table miw={700}>
|
i18nKey="layout/modals/about:translatorsDescription"
|
||||||
<thead>
|
values={{
|
||||||
<tr>
|
languages: loadedLanguages,
|
||||||
<th>Translator</th>
|
}}
|
||||||
<th>Translated</th>
|
components={{
|
||||||
<th>Approved</th>
|
a: <Anchor href="https://homarr.dev/docs/community/translations" target="_blank" />,
|
||||||
<th>Target</th>
|
}}
|
||||||
<th>Languages</th>
|
/>
|
||||||
</tr>
|
</Text>
|
||||||
</thead>
|
<Table withBorder>
|
||||||
<tbody>{rows}</tbody>
|
<thead>
|
||||||
</Table>
|
<tr>
|
||||||
</ScrollArea>
|
<th>Name</th>
|
||||||
|
<th>Translated</th>
|
||||||
|
<th>Approved</th>
|
||||||
|
<th>Target</th>
|
||||||
|
<th>Languages</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{rows}</tbody>
|
||||||
|
</Table>
|
||||||
|
<Pagination
|
||||||
|
style={{
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
total={translators.length / PAGINATION_ITEMS}
|
||||||
|
value={pagination.active}
|
||||||
|
onNextPage={() => pagination.next()}
|
||||||
|
onPreviousPage={() => pagination.previous()}
|
||||||
|
onChange={(targetPage) => pagination.setPage(targetPage)}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,26 @@
|
|||||||
import { Avatar, Badge, Indicator, Menu, UnstyledButton, useMantineTheme } from '@mantine/core';
|
import { Avatar, Menu, UnstyledButton, useMantineTheme } from '@mantine/core';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
|
||||||
import {
|
import {
|
||||||
IconDashboard,
|
IconDashboard,
|
||||||
IconHomeShare,
|
IconHomeShare,
|
||||||
IconInfoCircle,
|
|
||||||
IconLogin,
|
IconLogin,
|
||||||
IconLogout,
|
IconLogout,
|
||||||
IconMoonStars,
|
IconMoonStars,
|
||||||
IconSun,
|
IconSun,
|
||||||
IconUserCog,
|
IconUserCog,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { User } from 'next-auth';
|
import { User } from 'next-auth';
|
||||||
import { signOut, useSession } from 'next-auth/react';
|
import { signOut, useSession } from 'next-auth/react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { AboutModal } from '~/components/layout/header/About/AboutModal';
|
|
||||||
import { useColorScheme } from '~/hooks/use-colorscheme';
|
import { useColorScheme } from '~/hooks/use-colorscheme';
|
||||||
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
|
||||||
|
|
||||||
import { REPO_URL } from '../../../../data/constants';
|
|
||||||
import { useBoardLink } from '../Templates/BoardLayout';
|
import { useBoardLink } from '../Templates/BoardLayout';
|
||||||
|
|
||||||
export const AvatarMenu = () => {
|
export const AvatarMenu = () => {
|
||||||
const { t } = useTranslation('layout/header');
|
const { t } = useTranslation('layout/header');
|
||||||
const [aboutModalOpened, aboutModal] = useDisclosure(false);
|
|
||||||
const { data: sessionData } = useSession();
|
const { data: sessionData } = useSession();
|
||||||
const { colorScheme, toggleColorScheme } = useColorScheme();
|
const { colorScheme, toggleColorScheme } = useColorScheme();
|
||||||
const newVersionAvailable = useNewVersionAvailable();
|
|
||||||
|
|
||||||
const Icon = colorScheme === 'dark' ? IconSun : IconMoonStars;
|
const Icon = colorScheme === 'dark' ? IconSun : IconMoonStars;
|
||||||
const defaultBoardHref = useBoardLink('/board');
|
const defaultBoardHref = useBoardLink('/board');
|
||||||
@@ -38,10 +30,7 @@ export const AvatarMenu = () => {
|
|||||||
<UnstyledButton>
|
<UnstyledButton>
|
||||||
<Menu width={256}>
|
<Menu width={256}>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<CurrentUserAvatar
|
<CurrentUserAvatar user={sessionData?.user ?? null} />
|
||||||
newVersionAvailable={newVersionAvailable ? true : false}
|
|
||||||
user={sessionData?.user ?? null}
|
|
||||||
/>
|
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
@@ -74,19 +63,6 @@ export const AvatarMenu = () => {
|
|||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Menu.Item
|
|
||||||
icon={<IconInfoCircle size="1rem" />}
|
|
||||||
rightSection={
|
|
||||||
newVersionAvailable && (
|
|
||||||
<Badge variant="light" color="blue">
|
|
||||||
{t('actions.avatar.about.new')}
|
|
||||||
</Badge>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onClick={() => aboutModal.open()}
|
|
||||||
>
|
|
||||||
{t('actions.avatar.about.label')}
|
|
||||||
</Menu.Item>
|
|
||||||
{sessionData?.user ? (
|
{sessionData?.user ? (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
icon={<IconLogout size="1rem" />}
|
icon={<IconLogout size="1rem" />}
|
||||||
@@ -109,35 +85,18 @@ export const AvatarMenu = () => {
|
|||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
|
|
||||||
<AboutModal
|
|
||||||
opened={aboutModalOpened}
|
|
||||||
closeModal={aboutModal.close}
|
|
||||||
newVersionAvailable={newVersionAvailable}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type CurrentUserAvatarProps = {
|
type CurrentUserAvatarProps = {
|
||||||
newVersionAvailable: boolean;
|
|
||||||
user: User | null;
|
user: User | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CurrentUserAvatar = forwardRef<HTMLDivElement, CurrentUserAvatarProps>(
|
const CurrentUserAvatar = forwardRef<HTMLDivElement, CurrentUserAvatarProps>(
|
||||||
({ user, newVersionAvailable, ...others }, ref) => {
|
({ user, ...others }, ref) => {
|
||||||
const { primaryColor } = useMantineTheme();
|
const { primaryColor } = useMantineTheme();
|
||||||
if (!user) return <Avatar ref={ref} {...others} />;
|
if (!user) return <Avatar ref={ref} {...others} />;
|
||||||
|
|
||||||
if (newVersionAvailable)
|
|
||||||
return (
|
|
||||||
<Indicator withBorder offset={2} color="blue" processing size={15}>
|
|
||||||
<Avatar ref={ref} color={primaryColor} {...others}>
|
|
||||||
{user.name?.slice(0, 2).toUpperCase()}
|
|
||||||
</Avatar>
|
|
||||||
</Indicator>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Avatar ref={ref} color={primaryColor} {...others}>
|
<Avatar ref={ref} color={primaryColor} {...others}>
|
||||||
{user.name?.slice(0, 2).toUpperCase()}
|
{user.name?.slice(0, 2).toUpperCase()}
|
||||||
@@ -145,15 +104,3 @@ const CurrentUserAvatar = forwardRef<HTMLDivElement, CurrentUserAvatarProps>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const useNewVersionAvailable = () => {
|
|
||||||
const { attributes } = usePackageAttributesStore();
|
|
||||||
const { data } = 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`).then((res) => res.json()),
|
|
||||||
});
|
|
||||||
return data?.tag_name > `v${attributes.packageVersion}` ? data?.tag_name : undefined;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Anchor,
|
Anchor,
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
|
Divider,
|
||||||
Grid,
|
Grid,
|
||||||
Group,
|
Group,
|
||||||
HoverCard,
|
HoverCard,
|
||||||
@@ -28,12 +29,14 @@ import {
|
|||||||
IconVocabulary,
|
IconVocabulary,
|
||||||
IconWorldWww,
|
IconWorldWww,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { InitOptions } from 'i18next';
|
import { InitOptions } from 'i18next';
|
||||||
import { GetStaticPropsContext } from 'next';
|
import { GetStaticPropsContext } from 'next';
|
||||||
import { Trans, i18n, useTranslation } from 'next-i18next';
|
import { Trans, i18n, useTranslation } from 'next-i18next';
|
||||||
import { ReactNode } from 'react';
|
import { ReactElement, ReactNode } from 'react';
|
||||||
import { ContributorsTable } from '~/components/layout/header/About/Contributors';
|
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||||
|
import { Contributors, ContributorsTable } from '~/components/layout/header/About/Contributors';
|
||||||
import Credits from '~/components/layout/header/About/Credits';
|
import Credits from '~/components/layout/header/About/Credits';
|
||||||
import Tip from '~/components/layout/header/About/Tip';
|
import Tip from '~/components/layout/header/About/Tip';
|
||||||
import { TranslatorsTable } from '~/components/layout/header/About/Translators';
|
import { TranslatorsTable } from '~/components/layout/header/About/Translators';
|
||||||
@@ -44,6 +47,8 @@ import { useColorTheme } from '~/tools/color';
|
|||||||
import { queryClient } from '~/tools/server/configurations/tanstack/queryClient.tool';
|
import { queryClient } from '~/tools/server/configurations/tanstack/queryClient.tool';
|
||||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||||
|
|
||||||
|
import { REPO_URL } from '../../../data/constants';
|
||||||
|
|
||||||
interface InformationTableItem {
|
interface InformationTableItem {
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -60,7 +65,6 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
const { t } = useTranslation(['layout/modals/about']);
|
const { t } = useTranslation(['layout/modals/about']);
|
||||||
|
|
||||||
const { configVersion } = useConfigContext();
|
const { configVersion } = useConfigContext();
|
||||||
const { configs } = useConfigStore();
|
|
||||||
|
|
||||||
let items: InformationTableItem[] = [];
|
let items: InformationTableItem[] = [];
|
||||||
|
|
||||||
@@ -101,15 +105,6 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: <IconFile size={20} />,
|
|
||||||
label: 'configurationsCount',
|
|
||||||
content: (
|
|
||||||
<Badge variant="light" color={primaryColor}>
|
|
||||||
{configs.length}
|
|
||||||
</Badge>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: <IconVersions size={20} />,
|
icon: <IconVersions size={20} />,
|
||||||
label: 'version',
|
label: 'version',
|
||||||
@@ -121,18 +116,9 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
{newVersionAvailable && (
|
{newVersionAvailable && (
|
||||||
<HoverCard shadow="md" position="top" withArrow>
|
<HoverCard shadow="md" position="top" withArrow>
|
||||||
<HoverCard.Target>
|
<HoverCard.Target>
|
||||||
<motion.div
|
<Badge color="teal" variant="light">
|
||||||
initial={{ scale: 1.2 }}
|
{t('version.new', { newVersion: newVersionAvailable })}
|
||||||
animate={{
|
</Badge>
|
||||||
scale: [0.8, 1.1, 1],
|
|
||||||
rotate: [0, 10, 0],
|
|
||||||
}}
|
|
||||||
transition={{ duration: 0.8, ease: 'easeInOut' }}
|
|
||||||
>
|
|
||||||
<Badge color="green" variant="filled">
|
|
||||||
{t('version.new', { newVersion: newVersionAvailable })}
|
|
||||||
</Badge>
|
|
||||||
</motion.div>
|
|
||||||
</HoverCard.Target>
|
</HoverCard.Target>
|
||||||
<HoverCard.Dropdown>
|
<HoverCard.Dropdown>
|
||||||
<Text>
|
<Text>
|
||||||
@@ -185,10 +171,26 @@ const useStyles = createStyles(() => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const AboutPage = () => {
|
export const Page = ({ contributors }: { contributors: Contributors[] }) => {
|
||||||
const newVersionAvailable = queryClient.getQueryData<string>(['github/latest']);
|
const { data } = 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();
|
||||||
|
if (!i18n) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const initOptions = i18n.options as ExtendedInitOptions;
|
||||||
|
|
||||||
|
const newVersionAvailable =
|
||||||
|
data?.tag_name > `v${attributes.packageVersion}` ? data?.tag_name : undefined;
|
||||||
const informations = useInformationTableItems(newVersionAvailable);
|
const informations = useInformationTableItems(newVersionAvailable);
|
||||||
const { t } = useTranslation(['layout/modals/about']);
|
const { t } = useTranslation(['layout/modals/about']);
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
|
|
||||||
const keybinds = [
|
const keybinds = [
|
||||||
@@ -209,109 +211,78 @@ const AboutPage = () => {
|
|||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<ManageLayout>
|
||||||
<Text mb="lg">
|
<Stack>
|
||||||
<Trans i18nKey="layout/modals/about:description" />
|
<Text>
|
||||||
</Text>
|
<Trans i18nKey="layout/modals/about:description" />
|
||||||
|
</Text>
|
||||||
|
|
||||||
<Table mb="lg" highlightOnHover withBorder>
|
<Table withBorder>
|
||||||
<tbody>
|
<tbody>
|
||||||
{informations.map((item, index) => (
|
{informations.map((item, index) => (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td>
|
<td>
|
||||||
<Group spacing="xs">
|
<Group spacing="xs">
|
||||||
<ActionIcon className={classes.informationIcon} variant="default">
|
<ActionIcon className={classes.informationIcon} variant="default">
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<Text>
|
<Text>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={`layout/modals/about:metrics.${item.label}`}
|
i18nKey={`layout/modals/about:metrics.${item.label}`}
|
||||||
components={{ b: <b /> }}
|
components={{ b: <b /> }}
|
||||||
/>
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</td>
|
</td>
|
||||||
<td className={classes.informationTableColumn} style={{ maxWidth: 200 }}>
|
<td className={classes.informationTableColumn} style={{ maxWidth: 200 }}>
|
||||||
{item.content}
|
{item.content}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
<Accordion mb={5} variant="contained" radius="md">
|
<Accordion mb={5} variant="contained" radius="md">
|
||||||
<Accordion.Item value="keybinds">
|
<Accordion.Item value="keybinds">
|
||||||
<Accordion.Control icon={<IconKey size={20} />}>
|
<Accordion.Control icon={<IconKey size={20} />}>
|
||||||
{t('layout/modals/about:keybinds')}
|
{t('layout/modals/about:keybinds')}
|
||||||
</Accordion.Control>
|
</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<Table mb={5}>
|
<Table mb={5}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t('layout/modals/about:key')}</th>
|
<th>{t('layout/modals/about:key')}</th>
|
||||||
<th>{t('layout/modals/about:action')}</th>
|
<th>{t('layout/modals/about:action')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>{rows}</tbody>
|
<tbody>{rows}</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
<Tip>{t('layout/modals/about:tip')}</Tip>
|
<Tip>{t('layout/modals/about:tip')}</Tip>
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Title order={6} mb="xs" align="center">
|
<TranslatorsTable loadedLanguages={initOptions.locales.length} />
|
||||||
{t('layout/modals/about:contact')}
|
<Divider />
|
||||||
</Title>
|
<ContributorsTable contributors={contributors} />
|
||||||
|
<Credits />
|
||||||
<Grid grow>
|
</Stack>
|
||||||
<Grid.Col md={4} xs={12}>
|
</ManageLayout>
|
||||||
<Button
|
|
||||||
component="a"
|
|
||||||
href="https://github.com/ajnart/homarr"
|
|
||||||
target="_blank"
|
|
||||||
leftIcon={<IconBrandGithub size={20} />}
|
|
||||||
variant="default"
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</Button>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col md={4} xs={12}>
|
|
||||||
<Button
|
|
||||||
component="a"
|
|
||||||
href="https://homarr.dev/"
|
|
||||||
target="_blank"
|
|
||||||
leftIcon={<IconWorldWww size={20} />}
|
|
||||||
variant="default"
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
{t('layout/modals/about:documentation')}
|
|
||||||
</Button>
|
|
||||||
</Grid.Col>
|
|
||||||
|
|
||||||
<Grid.Col md={4} xs={12}>
|
|
||||||
<Button
|
|
||||||
component="a"
|
|
||||||
href="https://discord.gg/aCsmEV5RgA"
|
|
||||||
target="_blank"
|
|
||||||
leftIcon={<IconBrandDiscord size={20} />}
|
|
||||||
variant="default"
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
Discord
|
|
||||||
</Button>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
<Credits />
|
|
||||||
<TranslatorsTable />
|
|
||||||
<ContributorsTable />
|
|
||||||
</Stack>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getStaticProps({ locale }: GetStaticPropsContext) {
|
export async function getStaticProps({ locale }: GetStaticPropsContext) {
|
||||||
|
const contributors = (await fetch(
|
||||||
|
`https://api.github.com/repos/${REPO_URL}/contributors?per_page=100`,
|
||||||
|
{
|
||||||
|
cache: 'force-cache',
|
||||||
|
}
|
||||||
|
).then((res) => res.json())) as Contributors[];
|
||||||
return {
|
return {
|
||||||
...(await getServerSideTranslations(['authentication/login'], locale)),
|
props: {
|
||||||
};
|
contributors,
|
||||||
|
...(await getServerSideTranslations(['layout/manage', 'manage/index'], locale)),
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AboutPage;
|
export default Page;
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
export const hashPassword = (password: string, salt: string) => {
|
export const hashPassword = (password: string, salt: string) => {
|
||||||
return bcrypt.hashSync(password, salt);
|
return bcrypt.hashSync(password, salt);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface ConditionalWrapperProps {
|
||||||
|
condition: boolean;
|
||||||
|
wrapper: (children: ReactNode) => JSX.Element;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConditionalWrapper: React.FC<ConditionalWrapperProps> = ({ condition, wrapper, children }) =>
|
||||||
|
condition ? wrapper(children) : children;
|
||||||
Reference in New Issue
Block a user