diff --git a/public/locales/en/layout/header.json b/public/locales/en/layout/header.json index 7ef7f92f8..9c3f86088 100644 --- a/public/locales/en/layout/header.json +++ b/public/locales/en/layout/header.json @@ -17,10 +17,6 @@ "preferences": "User preferences", "defaultBoard": "Default dashboard", "manage": "Manage", - "about": { - "label": "About", - "new": "New" - }, "logout": "Logout from {{username}}", "login": "Login" } diff --git a/public/locales/en/layout/manage.json b/public/locales/en/layout/manage.json index 1af1c5bf1..013532bda 100644 --- a/public/locales/en/layout/manage.json +++ b/public/locales/en/layout/manage.json @@ -27,6 +27,9 @@ "items": { "docker": "Docker" } + }, + "about": { + "title": "About" } } } \ No newline at end of file diff --git a/public/locales/en/layout/modals/about.json b/public/locales/en/layout/modals/about.json index fdca91f6d..3d8b7b8aa 100644 --- a/public/locales/en/layout/modals/about.json +++ b/public/locales/en/layout/modals/about.json @@ -1,12 +1,14 @@ { "description": "Homarr is a sleek, modern 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 here.", + "contributors": "Contributors ({{count}})", + "contributorsDescription": "These people have built the code that makes homarr work! Want to help build Homarr? Read how to do so here", "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", diff --git a/src/components/layout/Templates/ManageLayout.tsx b/src/components/layout/Templates/ManageLayout.tsx index a412e3076..1c7f4731a 100644 --- a/src/components/layout/Templates/ManageLayout.tsx +++ b/src/components/layout/Templates/ManageLayout.tsx @@ -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: ( + ( + + {children} + + )} + > + + + + + ), + defaultOpened: false, + }; + + if ('href' in navigationLink) { + const isActive = router.pathname.endsWith(navigationLink.href); + return ( + } + component={Link} + href={navigationLink.href} + active={isActive} + /> + ); + } + + const isAnyActive = Object.entries(navigationLink.items) + .map(([_, item]) => item.href) + .some((href) => router.pathname.endsWith(href)); + + return ( + } + > + {Object.entries(navigationLink.items).map(([itemName, item], index) => { + const commonItemProps = { + label: t(`navigation.${name}.items.${itemName}`), + icon: , + href: item.href, + }; + + const matchesActive = router.pathname.endsWith(item.href); + + if (item.href.startsWith('http')) { + return ( + + ); + } + + return ( + + ); + })} + + ); + }); + + type NavigationLinks = { + [key in keyof typeof navigation]: (typeof navigation)[key] extends { + items: Record; + } + ? 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 ( <> = { icon: Icon; items: Record; 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: ( - - - - ), - defaultOpened: false, - }; - - if ('href' in navigationLink) { - const isActive = router.pathname.endsWith(navigationLink.href); - return ( - } - component={Link} - href={navigationLink.href} - active={isActive} - /> - ); - } - - const isAnyActive = Object.entries(navigationLink.items) - .map(([_, item]) => item.href) - .some((href) => router.pathname.endsWith(href)); - - return ( - }> - {Object.entries(navigationLink.items).map(([itemName, item], index) => { - const commonItemProps = { - label: t(`navigation.${name}.items.${itemName}`), - icon: , - href: item.href, - }; - - const matchesActive = router.pathname.endsWith(item.href); - - if (item.href.startsWith('http')) { - return ( - - ); - } - - return ; - })} - - ); -}); - -type NavigationLinks = { - [key in keyof typeof navigation]: (typeof navigation)[key] extends { - items: Record; - } - ? 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; }; diff --git a/src/components/layout/header/About/AboutModal.tsx b/src/components/layout/header/About/AboutModal.tsx deleted file mode 100644 index e4cd642b0..000000000 --- a/src/components/layout/header/About/AboutModal.tsx +++ /dev/null @@ -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) => ( - - - {element.key} - - - {element.shortcut} - - - )); - - return ( - closeModal()} - opened={opened} - title={ - - Homarr logo - - {t('about')} Homarr - - - } - size="xl" - > - - - - - - - {informations.map((item, index) => ( - - - - - ))} - -
- - - {item.icon} - - - }} - /> - - - - {item.content} -
- - - }> - {t('layout/modals/about:keybinds')} - - - - - - - - - - {rows} -
{t('layout/modals/about:key')}{t('layout/modals/about:action')}
- {t('layout/modals/about:tip')} -
-
-
- - - {t('layout/modals/about:contact')} - - - - - - - - - - - - - - - - - -
- ); -}; - -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: , - label: 'i18n', - content: ( - - {usedI18nNamespaces.length} - - ), - }, - { - icon: , - label: 'locales', - content: ( - - {initOptions.locales.length} - - ), - }, - ]; - } - - items = [ - { - icon: , - label: 'configurationSchemaVersion', - content: ( - - {configVersion} - - ), - }, - { - icon: , - label: 'configurationsCount', - content: ( - - {configs.length} - - ), - }, - { - icon: , - label: 'version', - content: ( - - - {attributes.packageVersion ?? 'Unknown'} - - {newVersionAvailable && ( - - - - - {t('version.new', { newVersion: newVersionAvailable })} - - - - - - { - t('version.dropdown', { currentVersion: attributes.packageVersion }).split( - '{{newVersion}}' - )[0] - } - - - {newVersionAvailable} - - - { - t('version.dropdown', { currentVersion: attributes.packageVersion }).split( - '{{newVersion}}' - )[1] - } - - - - )} - - ), - }, - { - icon: , - label: 'nodeEnvironment', - content: ( - - {attributes.environment} - - ), - }, - ...items, - ]; - - return items; -}; - -const useStyles = createStyles(() => ({ - informationTableColumn: { - textAlign: 'right', - }, - informationIcon: { - cursor: 'default', - }, -})); diff --git a/src/components/layout/header/About/Contributors.tsx b/src/components/layout/header/About/Contributors.tsx index 021749982..2feab7fe5 100644 --- a/src/components/layout/header/About/Contributors.tsx +++ b/src/components/layout/header/About/Contributors.tsx @@ -2,16 +2,14 @@ import { Anchor, Avatar, Group, - Loader, - ScrollArea, + Pagination, Stack, Table, Text, - createStyles, + Title, } from '@mantine/core'; -import { useQuery } from '@tanstack/react-query'; -import { cache } from 'react'; -import { REPO_URL } from '../../../../../data/constants'; +import { usePagination } from '@mantine/hooks'; +import { Trans, useTranslation } from 'next-i18next'; // Generated by https://quicktype.io @@ -42,53 +40,77 @@ export enum Type { 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, - }); - if (isFetching || !data) return ; +const PAGINATION_ITEMS = 20; - const rows = data.map((contributor) => ( - - - - - - {contributor.login} - - - - {contributor.contributions} - - )); +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) => ( + + + + + + {contributor.login} + + + + {contributor.contributions} + + )); return ( -
Credits to our amazing contributors
+ {t('contributors', { count: contributors.length })} + + , + }} + /> + - - - - - - - - - {rows} -
ContributorContributions
-
+ + + + + + + + {rows} +
+ Contributor + + Contributions +
+ pagination.next()} + onPreviousPage={() => pagination.previous()} + onChange={(targetPage) => pagination.setPage(targetPage)} + />
); } diff --git a/src/components/layout/header/About/Credits.tsx b/src/components/layout/header/About/Credits.tsx index e361b1cfa..e155a15f0 100644 --- a/src/components/layout/header/About/Credits.tsx +++ b/src/components/layout/header/About/Credits.tsx @@ -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 ( - + + {' '} and you! - - + ); } const DependencyTable = () => { const { t } = useTranslation('settings/common'); - const [opened, { toggle }] = useDisclosure(false); const { attributes } = usePackageAttributesStore(); return ( - <> - - {t('credits.thirdPartyContent')} - - - - ({ - backgroundColor: - theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0], - padding: theme.spacing.xl, - borderRadius: theme.radius.md, - })} - mt="md" - > - - - - - - - - {Object.keys(attributes.dependencies).map((key, index) => ( - +
{t('credits.thirdPartyContentTable.dependencyName')}{t('credits.thirdPartyContentTable.dependencyVersion')}
+ - - + + + + + {Object.keys(attributes.dependencies).map((key, index) => ( + + + + + ))} - ))} -
{key}{attributes.dependencies[key]}{t('credits.thirdPartyContentTable.dependencyName')}{t('credits.thirdPartyContentTable.dependencyVersion')}
{key}{attributes.dependencies[key]}
-
-
- + + ), + }) + } + > + {t('credits.thirdPartyContent')} + ); }; diff --git a/src/components/layout/header/About/Translators.module.css b/src/components/layout/header/About/Translators.module.css deleted file mode 100644 index 8f01dad6a..000000000 --- a/src/components/layout/header/About/Translators.module.css +++ /dev/null @@ -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); -} \ No newline at end of file diff --git a/src/components/layout/header/About/Translators.tsx b/src/components/layout/header/About/Translators.tsx index 81bf9cd7f..0ccd7ba43 100644 --- a/src/components/layout/header/About/Translators.tsx +++ b/src/components/layout/header/About/Translators.tsx @@ -1,60 +1,123 @@ -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 { + 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'; -export function TranslatorsTable() { - // Only the first 30 translators are shown - const translators = CrowdinReport.data.slice(0, 30); +const PAGINATION_ITEMS = 20; - const rows = translators.map((translator) => ( - - - - - - {translator.user.username} - - - - {translator.translated} - {translator.approved} - {translator.target} - - - {translator.languages.map((language) => ( - {language.name}, - ))} - - - - )); +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) => ( + + + + + + {translator.user.fullName} + + + + + {translator.translated} + + + {translator.approved} + + + {translator.target} + + + + {translator.languages.map((language) => ( + {language.name}, + ))} + + + + )); return ( -
Credits to our amazing translators
- - - - - - - - - - - - - {rows} -
TranslatorTranslatedApprovedTargetLanguages
-
+ {t('translators', { count: translators.length })} + + , + }} + /> + + + + + + + + + + + + {rows} +
NameTranslatedApprovedTargetLanguages
+ pagination.next()} + onPreviousPage={() => pagination.previous()} + onChange={(targetPage) => pagination.setPage(targetPage)} + />
); } diff --git a/src/components/layout/header/AvatarMenu.tsx b/src/components/layout/header/AvatarMenu.tsx index 31158cf75..e0ea38d01 100644 --- a/src/components/layout/header/AvatarMenu.tsx +++ b/src/components/layout/header/AvatarMenu.tsx @@ -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 = () => { - + { )} - } - rightSection={ - newVersionAvailable && ( - - {t('actions.avatar.about.new')} - - ) - } - onClick={() => aboutModal.open()} - > - {t('actions.avatar.about.label')} - {sessionData?.user ? ( } @@ -109,35 +85,18 @@ export const AvatarMenu = () => { - - ); }; type CurrentUserAvatarProps = { - newVersionAvailable: boolean; user: User | null; }; const CurrentUserAvatar = forwardRef( - ({ user, newVersionAvailable, ...others }, ref) => { + ({ user, ...others }, ref) => { const { primaryColor } = useMantineTheme(); if (!user) return ; - - if (newVersionAvailable) - return ( - - - {user.name?.slice(0, 2).toUpperCase()} - - - ); - return ( {user.name?.slice(0, 2).toUpperCase()} @@ -145,15 +104,3 @@ const CurrentUserAvatar = forwardRef( ); } ); - -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; -}; diff --git a/src/pages/manage/about.tsx b/src/pages/manage/about.tsx index 9db71cf4c..36ff2b343 100644 --- a/src/pages/manage/about.tsx +++ b/src/pages/manage/about.tsx @@ -4,6 +4,7 @@ import { Anchor, Badge, Button, + Divider, Grid, Group, HoverCard, @@ -28,12 +29,14 @@ import { IconVocabulary, IconWorldWww, } from '@tabler/icons-react'; +import { useQuery } from '@tanstack/react-query'; 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 { ReactElement, 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'; @@ -44,6 +47,8 @@ import { useColorTheme } from '~/tools/color'; import { queryClient } from '~/tools/server/configurations/tanstack/queryClient.tool'; import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; +import { REPO_URL } from '../../../data/constants'; + interface InformationTableItem { icon: ReactNode; label: string; @@ -60,7 +65,6 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl const { t } = useTranslation(['layout/modals/about']); const { configVersion } = useConfigContext(); - const { configs } = useConfigStore(); let items: InformationTableItem[] = []; @@ -101,15 +105,6 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl ), }, - { - icon: , - label: 'configurationsCount', - content: ( - - {configs.length} - - ), - }, { icon: , label: 'version', @@ -121,18 +116,9 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl {newVersionAvailable && ( - - - {t('version.new', { newVersion: newVersionAvailable })} - - + + {t('version.new', { newVersion: newVersionAvailable })} + @@ -185,10 +171,26 @@ const useStyles = createStyles(() => ({ }, })); -const AboutPage = () => { - const newVersionAvailable = queryClient.getQueryData(['github/latest']); +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 { t } = useTranslation(['layout/modals/about']); const { classes } = useStyles(); const keybinds = [ @@ -209,109 +211,78 @@ const AboutPage = () => { )); return ( - - - - + + + + + - - - {informations.map((item, index) => ( - - - - - ))} - -
- - - {item.icon} - - - }} - /> - - - - {item.content} -
- - - }> - {t('layout/modals/about:keybinds')} - - - - - - - - - - {rows} -
{t('layout/modals/about:key')}{t('layout/modals/about:action')}
- {t('layout/modals/about:tip')} -
-
-
+ + + {informations.map((item, index) => ( + + + + + ))} + +
+ + + {item.icon} + + + }} + /> + + + + {item.content} +
+ + + }> + {t('layout/modals/about:keybinds')} + + + + + + + + + + {rows} +
{t('layout/modals/about:key')}{t('layout/modals/about:action')}
+ {t('layout/modals/about:tip')} +
+
+
- - {t('layout/modals/about:contact')} - - - - - - - - - - - - - - - - - -
+ + + + +
+ ); }; -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 { - ...(await getServerSideTranslations(['authentication/login'], locale)), - }; + props: { + contributors, + ...(await getServerSideTranslations(['layout/manage', 'manage/index'], locale)), + }, + }; } -export default AboutPage; +export default Page; diff --git a/src/utils/security.ts b/src/utils/security.ts index 9e0d6d2df..6adc28751 100644 --- a/src/utils/security.ts +++ b/src/utils/security.ts @@ -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 = ({ condition, wrapper, children }) => + condition ? wrapper(children) : children; \ No newline at end of file