mirror of
				https://github.com/checktheroads/hyperglass
				synced 2024-05-11 05:55:08 +00:00 
			
		
		
		
	Closes #140: Genericize footer links and menus and allow multiple definitions
This commit is contained in:
		@@ -1,16 +1,37 @@
 | 
			
		||||
import { useMemo } from 'react';
 | 
			
		||||
import { Button, Menu, MenuButton, MenuList } from '@chakra-ui/react';
 | 
			
		||||
import { Markdown } from '~/components';
 | 
			
		||||
import { useColorValue, useBreakpointValue } from '~/context';
 | 
			
		||||
import { useOpposingColor } from '~/hooks';
 | 
			
		||||
import { useColorValue, useBreakpointValue, useConfig } from '~/context';
 | 
			
		||||
import { useOpposingColor, useStrf } from '~/hooks';
 | 
			
		||||
 | 
			
		||||
import type { IConfig } from '~/types';
 | 
			
		||||
import type { TFooterButton } from './types';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Filter the configuration object based on values that are strings for formatting.
 | 
			
		||||
 */
 | 
			
		||||
function getConfigFmt(config: IConfig): Record<string, string> {
 | 
			
		||||
  const fmt = {} as Record<string, string>;
 | 
			
		||||
  for (const [k, v] of Object.entries(config)) {
 | 
			
		||||
    if (typeof v === 'string') {
 | 
			
		||||
      fmt[k] = v;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return fmt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const FooterButton: React.FC<TFooterButton> = (props: TFooterButton) => {
 | 
			
		||||
  const { content, title, side, ...rest } = props;
 | 
			
		||||
 | 
			
		||||
  const config = useConfig();
 | 
			
		||||
  const fmt = useMemo(() => getConfigFmt(config), []);
 | 
			
		||||
  const fmtContent = useStrf(content, fmt);
 | 
			
		||||
 | 
			
		||||
  const placement = side === 'left' ? 'top' : side === 'right' ? 'top-end' : undefined;
 | 
			
		||||
  const bg = useColorValue('white', 'gray.900');
 | 
			
		||||
  const color = useOpposingColor(bg);
 | 
			
		||||
  const size = useBreakpointValue({ base: 'xs', lg: 'sm' });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Menu placement={placement} preventOverflow isLazy>
 | 
			
		||||
      <MenuButton
 | 
			
		||||
@@ -32,11 +53,12 @@ export const FooterButton: React.FC<TFooterButton> = (props: TFooterButton) => {
 | 
			
		||||
        boxShadow="2xl"
 | 
			
		||||
        textAlign="left"
 | 
			
		||||
        overflowY="auto"
 | 
			
		||||
        whiteSpace="normal"
 | 
			
		||||
        mx={{ base: 1, lg: 2 }}
 | 
			
		||||
        maxW={{ base: '100%', lg: '50vw' }}
 | 
			
		||||
        {...rest}
 | 
			
		||||
      >
 | 
			
		||||
        <Markdown content={content} />
 | 
			
		||||
        <Markdown content={fmtContent} />
 | 
			
		||||
      </MenuList>
 | 
			
		||||
    </Menu>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +1,43 @@
 | 
			
		||||
import { useMemo } from 'react';
 | 
			
		||||
import dynamic from 'next/dynamic';
 | 
			
		||||
import { Button, Flex, Link, Icon, HStack, useToken } from '@chakra-ui/react';
 | 
			
		||||
import { Flex, Icon, HStack, useToken } from '@chakra-ui/react';
 | 
			
		||||
import { If } from '~/components';
 | 
			
		||||
import { useConfig, useMobile, useColorValue, useBreakpointValue } from '~/context';
 | 
			
		||||
import { useStrf } from '~/hooks';
 | 
			
		||||
import { FooterButton } from './button';
 | 
			
		||||
import { ColorModeToggle } from './colorMode';
 | 
			
		||||
import { FooterLink } from './link';
 | 
			
		||||
import { isLink, isMenu } from './types';
 | 
			
		||||
 | 
			
		||||
import type { ButtonProps, LinkProps } from '@chakra-ui/react';
 | 
			
		||||
import type { TLink, TMenu } from '~/types';
 | 
			
		||||
 | 
			
		||||
const CodeIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiCode));
 | 
			
		||||
const ExtIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/go').then(i => i.GoLinkExternal));
 | 
			
		||||
 | 
			
		||||
function buildItems(links: TLink[], menus: TMenu[]): [(TLink | TMenu)[], (TLink | TMenu)[]] {
 | 
			
		||||
  const leftLinks = links.filter(link => link.side === 'left');
 | 
			
		||||
  const leftMenus = menus.filter(menu => menu.side === 'left');
 | 
			
		||||
  const rightLinks = links.filter(link => link.side === 'right');
 | 
			
		||||
  const rightMenus = menus.filter(menu => menu.side === 'right');
 | 
			
		||||
 | 
			
		||||
  const left = [...leftLinks, ...leftMenus].sort((a, b) => (a.order > b.order ? 1 : -1));
 | 
			
		||||
  const right = [...rightLinks, ...rightMenus].sort((a, b) => (a.order > b.order ? 1 : -1));
 | 
			
		||||
  return [left, right];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Footer: React.FC = () => {
 | 
			
		||||
  const { web, content, primary_asn } = useConfig();
 | 
			
		||||
 | 
			
		||||
  const footerBg = useColorValue('blackAlpha.50', 'whiteAlpha.100');
 | 
			
		||||
  const footerColor = useColorValue('black', 'white');
 | 
			
		||||
 | 
			
		||||
  const extUrl = useStrf(web.external_link.url, { primary_asn }) ?? '/';
 | 
			
		||||
 | 
			
		||||
  const size = useBreakpointValue({ base: useToken('sizes', 4), lg: useToken('sizes', 6) });
 | 
			
		||||
  const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' });
 | 
			
		||||
 | 
			
		||||
  const isMobile = useMobile();
 | 
			
		||||
 | 
			
		||||
  const [left, right] = useMemo(() => buildItems(web.links, web.menus), []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <HStack
 | 
			
		||||
      px={6}
 | 
			
		||||
@@ -30,30 +46,42 @@ export const Footer: React.FC = () => {
 | 
			
		||||
      zIndex={1}
 | 
			
		||||
      as="footer"
 | 
			
		||||
      bg={footerBg}
 | 
			
		||||
      whiteSpace="nowrap"
 | 
			
		||||
      color={footerColor}
 | 
			
		||||
      spacing={{ base: 8, lg: 6 }}
 | 
			
		||||
      d={{ base: 'inline-block', lg: 'flex' }}
 | 
			
		||||
      overflowY={{ base: 'auto', lg: 'unset' }}
 | 
			
		||||
      justifyContent={{ base: 'center', lg: 'space-between' }}
 | 
			
		||||
    >
 | 
			
		||||
      <If c={web.terms.enable}>
 | 
			
		||||
        <FooterButton side="left" content={content.terms} title={web.terms.title} />
 | 
			
		||||
      </If>
 | 
			
		||||
      <If c={web.help_menu.enable}>
 | 
			
		||||
        <FooterButton side="left" content={content.help_menu} title={web.help_menu.title} />
 | 
			
		||||
      </If>
 | 
			
		||||
      <If c={web.external_link.enable}>
 | 
			
		||||
        <Button
 | 
			
		||||
          as={Link}
 | 
			
		||||
          isExternal
 | 
			
		||||
          href={extUrl}
 | 
			
		||||
          size={btnSize}
 | 
			
		||||
          variant="ghost"
 | 
			
		||||
          rightIcon={<ExtIcon />}
 | 
			
		||||
          aria-label={web.external_link.title}
 | 
			
		||||
        >
 | 
			
		||||
          {web.external_link.title}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </If>
 | 
			
		||||
      {left.map(item => {
 | 
			
		||||
        if (isLink(item)) {
 | 
			
		||||
          const url = useStrf(item.url, { primary_asn }) ?? '/';
 | 
			
		||||
          const icon: Partial<ButtonProps & LinkProps> = {};
 | 
			
		||||
 | 
			
		||||
          if (item.show_icon) {
 | 
			
		||||
            icon.rightIcon = <ExtIcon />;
 | 
			
		||||
          }
 | 
			
		||||
          return <FooterLink key={item.title} href={url} title={item.title} {...icon} />;
 | 
			
		||||
        } else if (isMenu(item)) {
 | 
			
		||||
          return (
 | 
			
		||||
            <FooterButton key={item.title} side="left" content={item.content} title={item.title} />
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      })}
 | 
			
		||||
      {!isMobile && <Flex p={0} flex="1 0 auto" maxWidth="100%" mr="auto" />}
 | 
			
		||||
      {right.map(item => {
 | 
			
		||||
        if (isLink(item)) {
 | 
			
		||||
          const url = useStrf(item.url, { primary_asn }) ?? '/';
 | 
			
		||||
          const icon: Partial<ButtonProps & LinkProps> = {};
 | 
			
		||||
 | 
			
		||||
          if (item.show_icon) {
 | 
			
		||||
            icon.rightIcon = <ExtIcon />;
 | 
			
		||||
          }
 | 
			
		||||
          return <FooterLink href={url} title={item.title} {...icon} />;
 | 
			
		||||
        } else if (isMenu(item)) {
 | 
			
		||||
          return <FooterButton side="right" content={item.content} title={item.title} />;
 | 
			
		||||
        }
 | 
			
		||||
      })}
 | 
			
		||||
      <If c={web.credit.enable}>
 | 
			
		||||
        <FooterButton
 | 
			
		||||
          side="right"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								hyperglass/ui/components/footer/link.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								hyperglass/ui/components/footer/link.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
import { Button, Link, useBreakpointValue } from '@chakra-ui/react';
 | 
			
		||||
 | 
			
		||||
import type { TFooterLink } from './types';
 | 
			
		||||
 | 
			
		||||
export const FooterLink: React.FC<TFooterLink> = (props: TFooterLink) => {
 | 
			
		||||
  const { title } = props;
 | 
			
		||||
  const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' });
 | 
			
		||||
  return (
 | 
			
		||||
    <Button as={Link} isExternal size={btnSize} variant="ghost" aria-label={title} {...props}>
 | 
			
		||||
      {title}
 | 
			
		||||
    </Button>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import type { ButtonProps, MenuListProps } from '@chakra-ui/react';
 | 
			
		||||
import type { ButtonProps, LinkProps, MenuListProps } from '@chakra-ui/react';
 | 
			
		||||
import type { TLink, TMenu } from '~/types';
 | 
			
		||||
 | 
			
		||||
type TFooterSide = 'left' | 'right';
 | 
			
		||||
 | 
			
		||||
@@ -8,8 +9,18 @@ export interface TFooterButton extends Omit<MenuListProps, 'title'> {
 | 
			
		||||
  content: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type TFooterLink = ButtonProps & LinkProps & { title: string };
 | 
			
		||||
 | 
			
		||||
export type TFooterItems = 'help' | 'credit' | 'terms';
 | 
			
		||||
 | 
			
		||||
export interface TColorModeToggle extends ButtonProps {
 | 
			
		||||
  size?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isLink(item: TLink | TMenu): item is TLink {
 | 
			
		||||
  return 'url' in item;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isMenu(item: TLink | TMenu): item is TMenu {
 | 
			
		||||
  return 'content' in item;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user