refactor and restructure ui components

This commit is contained in:
thatmattlove 2021-12-19 22:57:00 -07:00
parent 9a04ef1dfc
commit 227da64c9a
53 changed files with 200 additions and 136 deletions

View file

@ -13,7 +13,7 @@ import {
ModalCloseButton,
} from '@chakra-ui/react';
import { useConfig, useColorValue, useBreakpointValue } from '~/context';
import { CodeBlock, DynamicIcon } from '~/components';
import { CodeBlock, DynamicIcon } from '~/elements';
import { useHyperglassConfig } from '~/hooks';
import type { UseDisclosureReturn } from '@chakra-ui/react';

View file

@ -9,8 +9,8 @@ import {
useDisclosure,
ModalCloseButton,
} from '@chakra-ui/react';
import { DynamicIcon, Markdown } from '~/components';
import { useColorValue } from '~/context';
import { DynamicIcon, Markdown } from '~/elements';
import type { ModalContentProps } from '@chakra-ui/react';

View file

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { Button, Menu, MenuButton, MenuList } from '@chakra-ui/react';
import { Markdown } from '~/components';
import { useColorValue, useBreakpointValue, useConfig } from '~/context';
import { Markdown } from '~/elements';
import { useOpposingColor, useStrf } from '~/hooks';
import type { MenuListProps } from '@chakra-ui/react';

View file

@ -1,8 +1,8 @@
import { forwardRef } from 'react';
import { Button, Tooltip } from '@chakra-ui/react';
import { Switch, Case } from 'react-if';
import { DynamicIcon } from '~/components';
import { useColorMode, useColorValue, useBreakpointValue } from '~/context';
import { DynamicIcon } from '~/elements';
import { useOpposingColor } from '~/hooks';
import type { ButtonProps } from '@chakra-ui/react';

View file

@ -1,8 +1,8 @@
import { useMemo } from 'react';
import { Flex, HStack, useToken } from '@chakra-ui/react';
import { If, Then } from 'react-if';
import { DynamicIcon } from '~/components';
import { useConfig, useMobile, useColorValue, useBreakpointValue } from '~/context';
import { DynamicIcon } from '~/elements';
import { useStrf } from '~/hooks';
import { FooterButton } from './button';
import { ColorModeToggle } from './color-mode';

View file

@ -10,7 +10,7 @@ import {
ModalCloseButton,
} from '@chakra-ui/react';
import { If, Then } from 'react-if';
import { Markdown } from '~/components';
import { Markdown } from '~/elements';
import { useConfig, useColorValue } from '~/context';
import { useGreeting, useOpposingColor } from '~/hooks';

View file

@ -1,6 +1,6 @@
import { Flex, ScaleFade } from '@chakra-ui/react';
import { motionChakra } from '~/components';
import { useBreakpointValue } from '~/context';
import { motionChakra } from '~/elements';
import { useBooleanValue, useFormInteractive } from '~/hooks';
import { Title } from './title';

View file

@ -1,24 +1,19 @@
export * from './animated';
export * from './card';
export * from './code-block';
export * from './countdown';
export * from './custom';
/**
* The components directory contains React components that handle logic.
*
* Generally, components that call hooks or reference configuration, or API types should be in
* components.
*/
export * from './debugger';
export * from './directive-info-modal';
export * from './dynamic-icon';
export * from './favicon';
export * from './footer';
export * from './form-field';
export * from './form-row';
export * from './greeting';
export * from './header';
export * from './label';
export * from './layout';
export * from './load-error';
export * from './loading';
export * from './location-card';
export * from './looking-glass-form';
export * from './markdown';
export * from './meta';
export * from './output';
export * from './path';
@ -26,6 +21,7 @@ export * from './prompt';
export * from './query-location';
export * from './query-target';
export * from './query-type';
export * from './reset-button';
export * from './resolved-target';
export * from './results';
export * from './select';

View file

@ -1,18 +1,29 @@
import { useCallback, useRef } from 'react';
import { Flex } from '@chakra-ui/react';
import { motion } from 'framer-motion';
import { isSafari } from 'react-device-detect';
import { If, Then } from 'react-if';
import { Debugger, Greeting, Footer, Header } from '~/components';
import { Debugger, Greeting, Footer, Header, ResetButton } from '~/components';
import { useConfig } from '~/context';
import { motionChakra } from '~/elements';
import { useFormState } from '~/hooks';
import { ResetButton } from './reset-button';
import type { FlexProps } from '@chakra-ui/react';
const AnimatedFlex = motion(Flex);
const Main = motionChakra('main', {
baseStyle: {
px: 4,
py: 0,
w: '100%',
display: 'flex',
flex: '1 1 auto',
flexDir: 'column',
textAlign: 'center',
alignItems: 'center',
justifyContent: 'start',
},
});
export const Frame = (props: FlexProps): JSX.Element => {
export const Layout = (props: FlexProps): JSX.Element => {
const { developerMode } = useConfig();
const { setStatus, reset } = useFormState(
useCallback(({ setStatus, reset }) => ({ setStatus, reset }), []),
@ -42,24 +53,15 @@ export const Frame = (props: FlexProps): JSX.Element => {
minHeight={isSafari ? '-webkit-fill-available' : '100vh'}
>
<Header />
<AnimatedFlex
<Main
layout
px={4}
py={0}
w="100%"
as="main"
flex="1 1 auto"
flexDir="column"
textAlign="center"
alignItems="center"
justifyContent="start"
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
exit={{ opacity: 0, x: -300 }}
initial={{ opacity: 0, y: 300 }}
>
{props.children}
</AnimatedFlex>
</Main>
<Footer />
<If condition={developerMode}>
<Then>

View file

@ -1,2 +0,0 @@
export * from './frame';
export * from './layout';

View file

@ -1,19 +0,0 @@
import { AnimatePresence } from 'framer-motion';
import { LookingGlassForm, Results } from '~/components';
import { useView } from '~/hooks';
import { Frame } from './frame';
export const Layout = (): JSX.Element => {
const view = useView();
return (
<Frame>
{view === 'results' ? (
<Results />
) : (
<AnimatePresence>
<LookingGlassForm />
</AnimatePresence>
)}
</Frame>
);
};

View file

@ -5,7 +5,6 @@ import { FormProvider, useForm } from 'react-hook-form';
import { vestResolver } from '@hookform/resolvers/vest';
import vest, { test, enforce } from 'vest';
import {
FormRow,
FormField,
DirectiveInfoModal,
QueryType,
@ -14,6 +13,7 @@ import {
QueryLocation,
} from '~/components';
import { useConfig } from '~/context';
import { FormRow } from '~/elements';
import { useStrf, useGreeting, useDevice, useFormState } from '~/hooks';
import { isString, isQueryField, Directive } from '~/types';

View file

@ -4,8 +4,8 @@ import dayjs from 'dayjs';
import relativeTimePlugin from 'dayjs/plugin/relativeTime';
import utcPlugin from 'dayjs/plugin/utc';
import { If, Then, Else } from 'react-if';
import { DynamicIcon } from '~/components';
import { useConfig, useColorValue } from '~/context';
import { DynamicIcon } from '~/elements';
import { useOpposingColor } from '~/hooks';
import type { TextProps } from '@chakra-ui/react';

View file

@ -45,13 +45,7 @@ const _Highlighted = (props: HighlightedProps): JSX.Element => {
times++;
}
return (
<>
{result.map(r => (
<>{r}</>
))}
</>
);
return <>{result}</>;
};
export const Highlighted = memo(_Highlighted, isEqual);

View file

@ -1,6 +1,6 @@
import { ButtonGroup, IconButton } from '@chakra-ui/react';
import { useZoomPanHelper } from 'react-flow-renderer';
import { DynamicIcon } from '~/components';
import { DynamicIcon } from '~/elements';
export const Controls = (): JSX.Element => {
const { fitView, zoomIn, zoomOut } = useZoomPanHelper();

View file

@ -1,5 +1,5 @@
import { Button, Tooltip } from '@chakra-ui/react';
import { DynamicIcon } from '~/components';
import { DynamicIcon } from '~/elements';
interface PathButtonProps {
onOpen(): void;

View file

@ -1,7 +1,7 @@
import { Flex, IconButton } from '@chakra-ui/react';
import { AnimatePresence } from 'framer-motion';
import { AnimatedDiv, DynamicIcon } from '~/components';
import { useColorValue } from '~/context';
import { AnimatedDiv, DynamicIcon } from '~/elements';
import { useOpposingColor, useFormState } from '~/hooks';
import type { FlexProps } from '@chakra-ui/react';

View file

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { Button, Stack, Text, VStack } from '@chakra-ui/react';
import { DynamicIcon } from '~/components';
import { useConfig, useColorValue } from '~/context';
import { DynamicIcon } from '~/elements';
import { useStrf, useDNSQuery, useFormState } from '~/hooks';
import type { DnsOverHttps } from '~/types';

View file

@ -1,5 +1,5 @@
import { Button, Tooltip, useClipboard } from '@chakra-ui/react';
import { DynamicIcon } from '~/components';
import { DynamicIcon } from '~/elements';
import type { ButtonProps } from '@chakra-ui/react';

View file

@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { Accordion } from '@chakra-ui/react';
import { AnimatePresence } from 'framer-motion';
import { AnimatedDiv } from '~/components';
import { AnimatedDiv } from '~/elements';
import { useFormState } from '~/hooks';
import { Result } from './individual';
import { Tags } from './tags';

View file

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { AccordionIcon, Box, Spinner, HStack, Text, Tooltip } from '@chakra-ui/react';
import { DynamicIcon } from '~/components';
import { useConfig, useColorValue } from '~/context';
import { DynamicIcon } from '~/elements';
import { useOpposingColor, useStrf } from '~/hooks';
import type { ErrorLevels } from '~/types';

View file

@ -16,8 +16,9 @@ import { motion } from 'framer-motion';
import startCase from 'lodash/startCase';
import isEqual from 'react-fast-compare';
import { If, Then, Else } from 'react-if';
import { BGPTable, Countdown, DynamicIcon, Path, TextOutput } from '~/components';
import { BGPTable, Path, TextOutput } from '~/components';
import { useColorValue, useConfig, useMobile } from '~/context';
import { Countdown, DynamicIcon } from '~/elements';
import { useStrf, useLGQuery, useTableToString, useFormState, useDevice } from '~/hooks';
import { isStructuredOutput, isStringOutput } from '~/types';
import { isStackError, isFetchError, isLGError, isLGOutputOrError } from './guards';
@ -227,8 +228,8 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
>
<Box>
<Flex direction="column" flex="1 0 auto" maxW={error ? '100%' : undefined}>
{!isError && typeof data !== 'undefined' ? (
<>
<If condition={!isError && typeof data !== 'undefined'}>
<Then>
{isStructuredOutput(data) && data.level === 'success' && tableComponent ? (
<BGPTable>{data.output}</BGPTable>
) : isStringOutput(data) && data.level === 'success' && !tableComponent ? (
@ -242,12 +243,13 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
<FormattedError message={errorMsg} keywords={errorKeywords} />
</Alert>
)}
</>
) : (
<Alert rounded="lg" my={2} py={4} status={errorLevel} variant="solid">
<FormattedError message={errorMsg} keywords={errorKeywords} />
</Alert>
)}
</Then>
<Else>
<Alert rounded="lg" my={2} py={4} status={errorLevel} variant="solid">
<FormattedError message={errorMsg} keywords={errorKeywords} />
</Alert>
</Else>
</If>
</Flex>
</Box>
@ -260,24 +262,26 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
justifyContent={{ base: 'flex-start', lg: 'flex-end' }}
>
<If condition={cache.showText && !isError && isCached}>
<If condition={isMobile}>
<Then>
<Countdown timeout={cache.timeout} text={web.text.cachePrefix} />
<Tooltip hasArrow label={cacheLabel} placement="top">
<Box>
<DynamicIcon icon={{ bs: 'BsLightningFill' }} color={color} />
</Box>
</Tooltip>
</Then>
<Else>
<Tooltip hasArrow label={cacheLabel} placement="top">
<Box>
<DynamicIcon icon={{ bs: 'BsLightningFill' }} color={color} />
</Box>
</Tooltip>
<Countdown timeout={cache.timeout} text={web.text.cachePrefix} />
</Else>
</If>
<Then>
<If condition={isMobile}>
<Then>
<Countdown timeout={cache.timeout} text={web.text.cachePrefix} />
<Tooltip hasArrow label={cacheLabel} placement="top">
<Box>
<DynamicIcon icon={{ bs: 'BsLightningFill' }} color={color} />
</Box>
</Tooltip>
</Then>
<Else>
<Tooltip hasArrow label={cacheLabel} placement="top">
<Box>
<DynamicIcon icon={{ bs: 'BsLightningFill' }} color={color} />
</Box>
</Tooltip>
<Countdown timeout={cache.timeout} text={web.text.cachePrefix} />
</Else>
</If>
</Then>
</If>
</HStack>
</Flex>

View file

@ -1,6 +1,6 @@
import { forwardRef } from 'react';
import { Button, Tooltip } from '@chakra-ui/react';
import { DynamicIcon } from '~/components';
import { DynamicIcon } from '~/elements';
import type { ButtonProps } from '@chakra-ui/react';
import type { UseQueryResult } from 'react-query';

View file

@ -1,8 +1,8 @@
import { useMemo } from 'react';
import { Box, Stack, useToken } from '@chakra-ui/react';
import { motion, AnimatePresence } from 'framer-motion';
import { Label } from '~/components';
import { useConfig, useBreakpointValue } from '~/context';
import { Label } from '~/elements';
import { useFormState } from '~/hooks';
import type { Transition } from 'framer-motion';

View file

@ -15,8 +15,9 @@ import {
} from '@chakra-ui/react';
import { useFormContext } from 'react-hook-form';
import { If, Then, Else } from 'react-if';
import { DynamicIcon, ResolvedTarget } from '~/components';
import { ResolvedTarget } from '~/components';
import { useMobile, useColorValue } from '~/context';
import { DynamicIcon } from '~/elements';
import { useFormState } from '~/hooks';
import type { IconButtonProps } from '@chakra-ui/react';

View file

@ -4,7 +4,7 @@ import { Flex, Text } from '@chakra-ui/react';
import { usePagination, useSortBy, useTable } from 'react-table';
import { If, Then, Else } from 'react-if';
import { useMobile } from '~/context';
import { CardBody, CardFooter, CardHeader, DynamicIcon } from '~/components';
import { CardBody, CardFooter, CardHeader, DynamicIcon } from '~/elements';
import { TableMain } from './table';
import { TableCell } from './cell';
import { TableHead } from './head';

View file

@ -1,7 +1,8 @@
import { useMemo } from 'react';
import { Button, Stack, Text, VStack, useDisclosure } from '@chakra-ui/react';
import { DynamicIcon, Prompt } from '~/components';
import { Prompt } from '~/components';
import { useConfig, useColorValue } from '~/context';
import { DynamicIcon } from '~/elements';
import { useStrf, useWtf } from '~/hooks';
interface UserIPProps {

View file

@ -0,0 +1,21 @@
/**
* The elements directory contains React components that are stateless and contain no logic or
* references to configuration.
*
* Generally, elements should not call non-theme-related hooks, rely on context, or reference
* configuration/API types.
*/
export * from './animated';
export * from './card';
export * from './code-block';
export * from './countdown';
export * from './custom';
export * from './dynamic-icon';
export * from './favicon';
export * from './form-row';
export * from './label';
export * from './load-error';
export * from './loading';
export * from './markdown';
export * from './no-config';

View file

@ -7,7 +7,7 @@ import {
AlertTitle,
AlertDescription,
} from '@chakra-ui/react';
import { NoConfig } from './no-config';
import { NoConfig } from '~/elements';
import type { CenterProps } from '@chakra-ui/react';
import type { ConfigLoadError } from '~/util';

View file

@ -1,5 +1,5 @@
import { Spinner } from '@chakra-ui/react';
import { NoConfig } from './no-config';
import { NoConfig } from '~/elements';
export const Loading = (): JSX.Element => {
return (

View file

@ -18,7 +18,7 @@ import {
ListItem as ChakraListItem,
} from '@chakra-ui/react';
import { If, Then, Else } from 'react-if';
import { CodeBlock as CustomCodeBlock } from '~/components';
import { CodeBlock as CustomCodeBlock } from '~/elements';
import { useColorValue } from '~/context';
import type {

View file

@ -1,3 +1,4 @@
export * from './theme-hooks';
export * from './useASNDetail';
export * from './useBooleanValue';
export * from './useDevice';

View file

@ -0,0 +1,34 @@
import {
useBreakpointValue,
useTheme as useChakraTheme,
useColorModeValue,
useToken,
} from '@chakra-ui/react';
import type { Theme } from '~/types';
export {
useBreakpointValue,
useColorMode,
useColorModeValue as useColorValue,
useToken,
} from '@chakra-ui/react';
/**
* Determine if device is mobile or desktop based on Chakra UI theme breakpoints.
*/
export const useMobile = (): boolean =>
useBreakpointValue<boolean>({ base: true, md: true, lg: false, xl: false }) ?? true;
/**
* Get the current theme object.
*/
export const useTheme = (): Theme.Full => useChakraTheme();
/**
* Convenience function to combine Chakra UI's useToken & useColorModeValue.
*/
export const useColorToken = <L extends string, D extends string>(
token: keyof Theme.Full,
light: L,
dark: D,
): L | D => useColorModeValue<L, D>(useToken(token, light), useToken(token, dark));

View file

@ -1,15 +1,45 @@
import { QueryClient, QueryClientProvider } from 'react-query';
import { Switch, Case, Default } from 'react-if';
import { Meta, Layout } from '~/components';
import { HyperglassProvider } from '~/context';
import { LoadError, Loading } from '~/elements';
import { useHyperglassConfig } from '~/hooks';
import type { AppProps } from 'next/app';
const queryClient = new QueryClient();
const App = (props: AppProps): JSX.Element => {
const AppComponent = (props: AppProps) => {
const { Component, pageProps } = props;
const { data, error, isLoading, ready, refetch, showError, isLoadingInitial } =
useHyperglassConfig();
return (
<Switch>
<Case condition={isLoadingInitial}>
<Loading />
</Case>
<Case condition={showError}>
<LoadError error={error!} retry={refetch} inProgress={isLoading} />
</Case>
<Case condition={ready}>
<HyperglassProvider config={data!}>
<Meta />
<Layout>
<Component {...pageProps} />
</Layout>
</HyperglassProvider>
</Case>
<Default>
<LoadError error={error!} retry={refetch} inProgress={isLoading} />
</Default>
</Switch>
);
};
const App = (props: AppProps): JSX.Element => {
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
<AppComponent {...props} />
</QueryClientProvider>
);
};

View file

@ -1,6 +1,6 @@
import fs from 'fs';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { Favicon, CustomJavascript, CustomHtml } from '~/components';
import { CustomJavascript, CustomHtml, Favicon } from '~/elements';
import favicons from '../favicon-formats';
import type { DocumentContext, DocumentInitialProps } from 'next/document';

View file

@ -1,37 +1,36 @@
import dynamic from 'next/dynamic';
import { Switch, Case, Default } from 'react-if';
import { Meta, Loading, LoadError } from '~/components';
import { HyperglassProvider } from '~/context';
import { useHyperglassConfig } from '~/hooks';
import { If, Then, Else } from 'react-if';
import { Loading } from '~/elements';
import { useView } from '~/hooks';
import type { NextPage } from 'next';
import type { AnimatePresenceProps } from 'framer-motion';
const Layout = dynamic<Dict>(() => import('~/components').then(i => i.Layout), {
const AnimatePresence = dynamic<AnimatePresenceProps>(() =>
import('framer-motion').then(i => i.AnimatePresence),
);
const LookingGlassForm = dynamic<Dict>(() => import('~/components').then(i => i.LookingGlassForm), {
loading: Loading,
});
const Results = dynamic<Dict>(() => import('~/components').then(i => i.Results), {
loading: Loading,
});
const Index: NextPage = () => {
const { data, error, isLoading, ready, refetch, showError, isLoadingInitial } =
useHyperglassConfig();
const view = useView();
return (
<Switch>
<Case condition={isLoadingInitial}>
<Loading />
</Case>
<Case condition={showError}>
<LoadError error={error!} retry={refetch} inProgress={isLoading} />
</Case>
<Case condition={ready}>
<HyperglassProvider config={data!}>
<Meta />
<Layout />
</HyperglassProvider>
</Case>
<Default>
<LoadError error={error!} retry={refetch} inProgress={isLoading} />
</Default>
</Switch>
<If condition={view === 'results'}>
<Then>
<Results />
</Then>
<Else>
<AnimatePresence>
<LookingGlassForm />
</AnimatePresence>
</Else>
</If>
);
};

View file

@ -10,6 +10,8 @@
"~/components/*": ["components/*"],
"~/context": ["context/index"],
"~/context/*": ["context/*"],
"~/elements": ["elements/index"],
"~/elements/*": ["elements/*"],
"~/hooks": ["hooks/index"],
"~/hooks/*": ["hooks/*"],
"~/state": ["state/index"],

View file

@ -29,7 +29,7 @@ export function isStringOutput(data: unknown): data is StringQueryResponse {
* Determine if a form field name is a valid form key name.
*/
export function isQueryField(field: string): field is keyof FormData {
return ['queryLocation', 'queryType', 'queryGroup', 'queryTarget'].includes(field);
return ['queryLocation', 'queryType', 'queryTarget'].includes(field);
}
/**