mirror of
https://github.com/ajnart/homarr.git
synced 2026-01-26 09:19:18 +01:00
Merge pull request #1539 from ajnart/about-page
Turn about modal into a static page
This commit is contained in:
14
.vscode/settings.json
vendored
Normal file
14
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": "public/locales",
|
||||
"i18n-ally.enabledFrameworks": [
|
||||
"react-i18next"
|
||||
],
|
||||
"i18n-ally.namespace": true,
|
||||
"i18n-ally.pathMatcher": "{locale}/{namespaces}.json",
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.keysInUse": [
|
||||
"modules.**",
|
||||
"layout.manage.navigation.**",
|
||||
],
|
||||
// "i18n-ally.defaultNamespace": "translation"
|
||||
}
|
||||
2732
data/crowdin-report.json
Normal file
2732
data/crowdin-report.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -17,10 +17,6 @@
|
||||
"preferences": "User preferences",
|
||||
"defaultBoard": "Default dashboard",
|
||||
"manage": "Manage",
|
||||
"about": {
|
||||
"label": "About",
|
||||
"new": "New"
|
||||
},
|
||||
"logout": "Logout from {{username}}",
|
||||
"login": "Login"
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
"items": {
|
||||
"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.",
|
||||
"contact": "Having trouble or questions? Connect with us!",
|
||||
"addToDashboard": "Add to Dashboard",
|
||||
"tip": "Mod refers to your modifier key, it is Ctrl and Command/Super/Windows key",
|
||||
"key": "Shortcut key",
|
||||
"action": "Action",
|
||||
"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": {
|
||||
"toggleTheme": "Toggle light/dark mode",
|
||||
"focusSearchBar": "Focus on search bar",
|
||||
@@ -15,7 +17,6 @@
|
||||
},
|
||||
"metrics": {
|
||||
"configurationSchemaVersion": "Configuration schema version",
|
||||
"configurationsCount": "Available configurations",
|
||||
"version": "Version",
|
||||
"nodeEnvironment": "Node environment",
|
||||
"i18n": "Loaded I18n translation namespaces",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -1,321 +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';
|
||||
|
||||
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 />
|
||||
</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',
|
||||
},
|
||||
}));
|
||||
116
src/components/layout/header/About/Contributors.tsx
Normal file
116
src/components/layout/header/About/Contributors.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
Anchor,
|
||||
Avatar,
|
||||
Group,
|
||||
Pagination,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { usePagination } from '@mantine/hooks';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
|
||||
// Generated by https://quicktype.io
|
||||
|
||||
export interface Contributors {
|
||||
login: string;
|
||||
id: number;
|
||||
node_id: string;
|
||||
avatar_url: string;
|
||||
gravatar_id: string;
|
||||
url: string;
|
||||
html_url: string;
|
||||
followers_url: string;
|
||||
following_url: string;
|
||||
gists_url: string;
|
||||
starred_url: string;
|
||||
subscriptions_url: string;
|
||||
organizations_url: string;
|
||||
repos_url: string;
|
||||
events_url: string;
|
||||
received_events_url: string;
|
||||
type: Type;
|
||||
site_admin: boolean;
|
||||
contributions: number;
|
||||
}
|
||||
|
||||
export enum Type {
|
||||
Bot = 'Bot',
|
||||
User = 'User',
|
||||
}
|
||||
|
||||
const PAGINATION_ITEMS = 20;
|
||||
|
||||
export function ContributorsTable({ contributors }: { contributors: Contributors[] }) {
|
||||
const pagination = usePagination({
|
||||
total: contributors.length / PAGINATION_ITEMS,
|
||||
initialPage: 1,
|
||||
});
|
||||
const { t } = useTranslation(['layout/modals/about']);
|
||||
|
||||
const rows = contributors
|
||||
.slice(
|
||||
(pagination.active - 1) * PAGINATION_ITEMS,
|
||||
(pagination.active - 1) * PAGINATION_ITEMS + PAGINATION_ITEMS
|
||||
)
|
||||
.map((contributor) => (
|
||||
<tr key={contributor.id}>
|
||||
<td>
|
||||
<Anchor href={`https://github.com/${contributor.login}`} target="_blank">
|
||||
<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 (
|
||||
<Stack>
|
||||
<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>
|
||||
|
||||
<Table withBorder>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
style={{
|
||||
width: 400,
|
||||
}}
|
||||
>
|
||||
Contributor
|
||||
</th>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -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 { modals } from '@mantine/modals';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
||||
|
||||
@@ -7,7 +8,8 @@ export default function Credits() {
|
||||
const { t } = useTranslation('settings/common');
|
||||
|
||||
return (
|
||||
<Flex justify="center" direction="column" mt="xs">
|
||||
<Stack>
|
||||
<DependencyTable />
|
||||
<Text
|
||||
style={{
|
||||
fontSize: '0.90rem',
|
||||
@@ -24,49 +26,47 @@ export default function Credits() {
|
||||
</Anchor>{' '}
|
||||
and you!
|
||||
</Text>
|
||||
<DependencyTable />
|
||||
</Flex>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const DependencyTable = () => {
|
||||
const { t } = useTranslation('settings/common');
|
||||
const [opened, { toggle }] = useDisclosure(false);
|
||||
const { attributes } = usePackageAttributesStore();
|
||||
return (
|
||||
<>
|
||||
<Text variant="link" mx="auto" size="xs" opacity={0.3} onClick={toggle}>
|
||||
{t('credits.thirdPartyContent')}
|
||||
</Text>
|
||||
|
||||
<Collapse in={opened}>
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
backgroundColor:
|
||||
theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0],
|
||||
padding: theme.spacing.xl,
|
||||
borderRadius: theme.radius.md,
|
||||
})}
|
||||
mt="md"
|
||||
>
|
||||
<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}`}>
|
||||
<Button
|
||||
style={{
|
||||
justifyContent: 'start',
|
||||
}}
|
||||
variant="light"
|
||||
mx="auto"
|
||||
size="xs"
|
||||
onClick={() =>
|
||||
modals.open({
|
||||
title: t('credits.thirdPartyContent'),
|
||||
size: 'xl',
|
||||
children: (
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{key}</td>
|
||||
<td>{attributes.dependencies[key]}</td>
|
||||
<th>{t('credits.thirdPartyContentTable.dependencyName')}</th>
|
||||
<th>{t('credits.thirdPartyContentTable.dependencyVersion')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(attributes.dependencies).map((key, index) => (
|
||||
<tr>
|
||||
<td>{key}</td>
|
||||
<td>{attributes.dependencies[key]}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
))}
|
||||
</Table>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</>
|
||||
</Table>
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('credits.thirdPartyContent')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
123
src/components/layout/header/About/Translators.tsx
Normal file
123
src/components/layout/header/About/Translators.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
Anchor,
|
||||
Avatar,
|
||||
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';
|
||||
|
||||
const PAGINATION_ITEMS = 20;
|
||||
|
||||
export function TranslatorsTable({ loadedLanguages }: { loadedLanguages: number }) {
|
||||
const { t } = useTranslation(['layout/modals/about']);
|
||||
const translators = CrowdinReport.data;
|
||||
const pagination = usePagination({
|
||||
total: translators.length / PAGINATION_ITEMS,
|
||||
initialPage: 1,
|
||||
});
|
||||
|
||||
const rows = translators
|
||||
.slice(
|
||||
(pagination.active - 1) * PAGINATION_ITEMS,
|
||||
(pagination.active - 1) * PAGINATION_ITEMS + PAGINATION_ITEMS
|
||||
)
|
||||
.map((translator) => (
|
||||
<tr key={translator.user.id}>
|
||||
<td
|
||||
style={{
|
||||
width: 400,
|
||||
}}
|
||||
>
|
||||
<Anchor href={`https://crowdin.com/profile/${translator.user.username}`} target="_blank">
|
||||
<Group noWrap>
|
||||
<Avatar
|
||||
size={25}
|
||||
radius="lg"
|
||||
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 (
|
||||
<Stack>
|
||||
<Title order={3}>{t('translators', { count: translators.length })}</Title>
|
||||
<Text>
|
||||
<Trans
|
||||
i18nKey="layout/modals/about:translatorsDescription"
|
||||
values={{
|
||||
languages: loadedLanguages,
|
||||
}}
|
||||
components={{
|
||||
a: <Anchor href="https://homarr.dev/docs/community/translations" target="_blank" />,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
<Table withBorder>
|
||||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +1,26 @@
|
||||
import { Avatar, Badge, Indicator, Menu, UnstyledButton, useMantineTheme } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { Avatar, Menu, UnstyledButton, useMantineTheme } from '@mantine/core';
|
||||
import {
|
||||
IconDashboard,
|
||||
IconHomeShare,
|
||||
IconInfoCircle,
|
||||
IconLogin,
|
||||
IconLogout,
|
||||
IconMoonStars,
|
||||
IconSun,
|
||||
IconUserCog,
|
||||
} from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { User } from 'next-auth';
|
||||
import { signOut, useSession } from 'next-auth/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Link from 'next/link';
|
||||
import { forwardRef } from 'react';
|
||||
import { AboutModal } from '~/components/layout/header/About/AboutModal';
|
||||
import { useColorScheme } from '~/hooks/use-colorscheme';
|
||||
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
||||
|
||||
import { REPO_URL } from '../../../../data/constants';
|
||||
import { useBoardLink } from '../Templates/BoardLayout';
|
||||
|
||||
export const AvatarMenu = () => {
|
||||
const { t } = useTranslation('layout/header');
|
||||
const [aboutModalOpened, aboutModal] = useDisclosure(false);
|
||||
const { data: sessionData } = useSession();
|
||||
const { colorScheme, toggleColorScheme } = useColorScheme();
|
||||
const newVersionAvailable = useNewVersionAvailable();
|
||||
|
||||
const Icon = colorScheme === 'dark' ? IconSun : IconMoonStars;
|
||||
const defaultBoardHref = useBoardLink('/board');
|
||||
@@ -38,10 +30,7 @@ export const AvatarMenu = () => {
|
||||
<UnstyledButton>
|
||||
<Menu width={256}>
|
||||
<Menu.Target>
|
||||
<CurrentUserAvatar
|
||||
newVersionAvailable={newVersionAvailable ? true : false}
|
||||
user={sessionData?.user ?? null}
|
||||
/>
|
||||
<CurrentUserAvatar user={sessionData?.user ?? null} />
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
@@ -74,19 +63,6 @@ export const AvatarMenu = () => {
|
||||
<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 ? (
|
||||
<Menu.Item
|
||||
icon={<IconLogout size="1rem" />}
|
||||
@@ -109,35 +85,18 @@ export const AvatarMenu = () => {
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</UnstyledButton>
|
||||
|
||||
<AboutModal
|
||||
opened={aboutModalOpened}
|
||||
closeModal={aboutModal.close}
|
||||
newVersionAvailable={newVersionAvailable}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type CurrentUserAvatarProps = {
|
||||
newVersionAvailable: boolean;
|
||||
user: User | null;
|
||||
};
|
||||
|
||||
const CurrentUserAvatar = forwardRef<HTMLDivElement, CurrentUserAvatarProps>(
|
||||
({ user, newVersionAvailable, ...others }, ref) => {
|
||||
({ user, ...others }, ref) => {
|
||||
const { primaryColor } = useMantineTheme();
|
||||
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 (
|
||||
<Avatar ref={ref} color={primaryColor} {...others}>
|
||||
{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;
|
||||
};
|
||||
|
||||
280
src/pages/manage/about.tsx
Normal file
280
src/pages/manage/about.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
import {
|
||||
Accordion,
|
||||
ActionIcon,
|
||||
Anchor,
|
||||
Badge,
|
||||
Divider,
|
||||
Group,
|
||||
HoverCard,
|
||||
Kbd,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
createStyles
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconAnchor,
|
||||
IconKey,
|
||||
IconLanguage,
|
||||
IconSchema,
|
||||
IconVersions,
|
||||
IconVocabulary
|
||||
} from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { InitOptions } from 'i18next';
|
||||
import { GetServerSidePropsContext } from 'next';
|
||||
import { Trans, i18n, useTranslation } from 'next-i18next';
|
||||
import Head from 'next/head';
|
||||
import { ReactNode } from 'react';
|
||||
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 Tip from '~/components/layout/header/About/Tip';
|
||||
import { TranslatorsTable } from '~/components/layout/header/About/Translators';
|
||||
import { useConfigContext } from '~/config/provider';
|
||||
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
||||
import { useColorTheme } from '~/tools/color';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
|
||||
import { REPO_URL } from '../../../data/constants';
|
||||
|
||||
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();
|
||||
|
||||
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: <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>
|
||||
<Badge color="teal" variant="light">
|
||||
{t('version.new', { newVersion: newVersionAvailable })}
|
||||
</Badge>
|
||||
</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',
|
||||
},
|
||||
}));
|
||||
|
||||
export const Page = ({ contributors }: { contributors: Contributors[] }) => {
|
||||
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 { t } = useTranslation(['layout/modals/about']);
|
||||
const { classes } = useStyles();
|
||||
|
||||
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 (
|
||||
<ManageLayout>
|
||||
<Head>
|
||||
<title>About • Homarr</title>
|
||||
</Head>
|
||||
<Stack>
|
||||
<Text>
|
||||
<Trans i18nKey="layout/modals/about:description" />
|
||||
</Text>
|
||||
|
||||
<Table 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>
|
||||
|
||||
<TranslatorsTable loadedLanguages={initOptions.locales.length} />
|
||||
<Divider />
|
||||
<ContributorsTable contributors={contributors} />
|
||||
<Credits />
|
||||
</Stack>
|
||||
</ManageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps({ locale }: GetServerSidePropsContext) {
|
||||
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 {
|
||||
props: {
|
||||
contributors,
|
||||
...(await getServerSideTranslations(['layout/manage', 'manage/index'], locale)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -1,5 +1,15 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export const hashPassword = (password: string, salt: string) => {
|
||||
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