🚧 WIP on about page

This commit is contained in:
ajnart
2023-10-31 23:48:24 +01:00
parent 4072ebc5a5
commit ea8c8ffee2
6 changed files with 3227 additions and 0 deletions

2732
data/crowdin-report.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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>
); );
}; };

View 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>
);
}

View 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);
}

View 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
View 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;