mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-14 09:25:47 +01:00
🚧 WIP on about page
This commit is contained in:
2732
data/crowdin-report.json
Normal file
2732
data/crowdin-report.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -39,6 +39,8 @@ import { useColorTheme } from '~/tools/color';
|
|||||||
import { usePrimaryGradient } from '../../Common/useGradient';
|
import { usePrimaryGradient } from '../../Common/useGradient';
|
||||||
import Credits from './Credits';
|
import Credits from './Credits';
|
||||||
import Tip from './Tip';
|
import Tip from './Tip';
|
||||||
|
import { TranslatorsTable } from './Translators';
|
||||||
|
import { ContributorsTable } from './Contributors';
|
||||||
|
|
||||||
interface AboutModalProps {
|
interface AboutModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
@@ -175,6 +177,8 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
|
|||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Credits />
|
<Credits />
|
||||||
|
<TranslatorsTable />
|
||||||
|
<ContributorsTable />
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
94
src/components/layout/header/About/Contributors.tsx
Normal file
94
src/components/layout/header/About/Contributors.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import {
|
||||||
|
Anchor,
|
||||||
|
Avatar,
|
||||||
|
Group,
|
||||||
|
Loader,
|
||||||
|
ScrollArea,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
createStyles,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { cache } from 'react';
|
||||||
|
import { REPO_URL } from '../../../../../data/constants';
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContributorsTable() {
|
||||||
|
// 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) => (
|
||||||
|
<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>
|
||||||
|
<h5>Credits to our amazing contributors</h5>
|
||||||
|
|
||||||
|
<ScrollArea h={800}>
|
||||||
|
<Table miw={700}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Contributor</th>
|
||||||
|
<th>Contributions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{rows}</tbody>
|
||||||
|
</Table>
|
||||||
|
</ScrollArea>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
src/components/layout/header/About/Translators.module.css
Normal file
20
src/components/layout/header/About/Translators.module.css
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
.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);
|
||||||
|
}
|
||||||
60
src/components/layout/header/About/Translators.tsx
Normal file
60
src/components/layout/header/About/Translators.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { Anchor, Avatar, Group, ScrollArea, Stack, Table, Text, createStyles } from '@mantine/core';
|
||||||
|
import cx from 'clsx';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import CrowdinReport from '../../../../../data/crowdin-report.json';
|
||||||
|
|
||||||
|
export function TranslatorsTable() {
|
||||||
|
// Only the first 30 translators are shown
|
||||||
|
const translators = CrowdinReport.data.slice(0, 30);
|
||||||
|
|
||||||
|
const rows = translators.map((translator) => (
|
||||||
|
<tr key={translator.user.id}>
|
||||||
|
<td>
|
||||||
|
<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.username}
|
||||||
|
</Group>
|
||||||
|
</Anchor>
|
||||||
|
</td>
|
||||||
|
<td>{translator.translated}</td>
|
||||||
|
<td>{translator.approved}</td>
|
||||||
|
<td>{translator.target}</td>
|
||||||
|
<td>
|
||||||
|
<Text lineClamp={1}>
|
||||||
|
{translator.languages.map((language) => (
|
||||||
|
<span key={language.id}>{language.name}, </span>
|
||||||
|
))}
|
||||||
|
</Text>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<h5>Credits to our amazing translators</h5>
|
||||||
|
|
||||||
|
<ScrollArea h={800}>
|
||||||
|
<Table miw={700}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Translator</th>
|
||||||
|
<th>Translated</th>
|
||||||
|
<th>Approved</th>
|
||||||
|
<th>Target</th>
|
||||||
|
<th>Languages</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{rows}</tbody>
|
||||||
|
</Table>
|
||||||
|
</ScrollArea>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
317
src/pages/manage/about.tsx
Normal file
317
src/pages/manage/about.tsx
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
ActionIcon,
|
||||||
|
Anchor,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
HoverCard,
|
||||||
|
Image,
|
||||||
|
Kbd,
|
||||||
|
Modal,
|
||||||
|
Stack,
|
||||||
|
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 { GetStaticPropsContext } from 'next';
|
||||||
|
import { Trans, i18n, useTranslation } from 'next-i18next';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { 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 { useConfigStore } from '~/config/store';
|
||||||
|
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
||||||
|
import { useColorTheme } from '~/tools/color';
|
||||||
|
import { queryClient } from '~/tools/server/configurations/tanstack/queryClient.tool';
|
||||||
|
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const AboutPage = () => {
|
||||||
|
const newVersionAvailable = queryClient.getQueryData<string>(['github/latest']);
|
||||||
|
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 (
|
||||||
|
<Stack>
|
||||||
|
<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 />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getStaticProps({ locale }: GetStaticPropsContext) {
|
||||||
|
return {
|
||||||
|
...(await getServerSideTranslations(['authentication/login'], locale)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AboutPage;
|
||||||
Reference in New Issue
Block a user