Files
tom goriunovandGitHub 5e80806796 UI toolkit package (#2691)
* first version of the pack

* single entry point

* add custom components to the package

* move form components to toolkit

* add more utils and hooks to the package

* minor fixes

* add CI workflow

* update readme and package.json

* fix ts

* fix build of env-validator

* fix docker build

* fix icons size

* test fixes

* more test fixes
2025-04-21 19:44:22 +02:00

121 lines
3.5 KiB
TypeScript

import type { HTMLChakraProps } from '@chakra-ui/react';
import { Flex, Box } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import useFetch from 'lib/hooks/useFetch';
import { Tooltip } from 'toolkit/chakra/tooltip';
import { FilterInput } from 'toolkit/components/filters/FilterInput';
import { useClipboard } from 'toolkit/hooks/useClipboard';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import EmptySearchResult from 'ui/shared/EmptySearchResult';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
import PageTitle from 'ui/shared/Page/PageTitle';
const formatFileSize = (fileSizeInBytes: number) => `${ (fileSizeInBytes / 1_024).toFixed(2) } Kb`;
interface IconInfo {
name: string;
file_size: number;
}
const Item = ({ name, file_size: fileSize, bgColor }: IconInfo & HTMLChakraProps<'div'>) => {
const { hasCopied, copy } = useClipboard(name, 1000);
const [ copied, setCopied ] = React.useState(false);
React.useEffect(() => {
if (hasCopied) {
setCopied(true);
} else {
setCopied(false);
}
}, [ hasCopied ]);
return (
<Flex
flexDir="column"
alignItems="center"
whiteSpace="pre-wrap"
wordBreak="break-word"
maxW="100px"
textAlign="center"
onClick={ copy }
cursor="pointer"
>
<IconSvg name={ name.replace('.svg', '') as IconName } boxSize="100px" bgColor={ bgColor } borderRadius="base"/>
<Tooltip content={ copied ? 'Copied' : 'Copy to clipboard' } open={ copied }>
<Box fontWeight={ 500 } mt={ 2 }>{ name }</Box>
</Tooltip>
<Box color="text.secondary">{ formatFileSize(fileSize) }</Box>
</Flex>
);
};
const Sprite = () => {
const [ searchTerm, setSearchTerm ] = React.useState('');
const fetch = useFetch();
const { data, isFetching, isError } = useQuery({
queryKey: [ 'sprite' ],
queryFn: () => {
return fetch<Array<IconInfo>, unknown>('/icons/registry.json');
},
});
const content = (() => {
if (isFetching) {
return <ContentLoader/>;
}
if (isError || !data || !Array.isArray(data)) {
return <DataFetchAlert/>;
}
const items = data
.filter((icon) => icon.name.includes(searchTerm))
.sort((a, b) => a.name.localeCompare(b.name));
if (items.length === 0) {
return <EmptySearchResult text="No icons found"/>;
}
return (
<Flex flexWrap="wrap" fontSize="sm" columnGap={ 5 } rowGap={ 5 } justifyContent="flex-start">
{ items.map((item) => <Item key={ item.name } { ...item } bgColor={{ _light: 'blackAlpha.100', _dark: 'whiteAlpha.100' }}/>) }
</Flex>
);
})();
const total = React.useMemo(() => {
if (!data || !Array.isArray(data)) {
return;
}
return data.reduce((result, item) => {
result.num++;
result.fileSize += item.file_size;
return result;
}, { num: 0, fileSize: 0 });
}, [ data ]);
const searchInput = <FilterInput placeholder="Search by name..." onChange={ setSearchTerm } loading={ isFetching } minW={{ base: '100%', lg: '300px' }}/>;
const totalEl = total ? <Box ml="auto">Items: { total.num } / Size: { formatFileSize(total.fileSize) }</Box> : null;
const contentAfter = (
<>
{ totalEl }
{ searchInput }
</>
);
return (
<div>
<PageTitle title="SVG sprite 🥤" contentAfter={ contentAfter }/>
{ content }
</div>
);
};
export default React.memo(Sprite);