mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-14 01:15: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 Credits from './Credits';
|
||||
import Tip from './Tip';
|
||||
import { TranslatorsTable } from './Translators';
|
||||
import { ContributorsTable } from './Contributors';
|
||||
|
||||
interface AboutModalProps {
|
||||
opened: boolean;
|
||||
@@ -175,6 +177,8 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<Credits />
|
||||
<TranslatorsTable />
|
||||
<ContributorsTable />
|
||||
</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