Commit 4065e478 authored by tom goriunov's avatar tom goriunov Committed by GitHub

SVG sprite page (#2056)

* create page with all icons from the sprite

* add info about file size

* refactoring
parent 48d6f5b5
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.392 5.45c.252-.288.592-.45.948-.45h8.038a.63.63 0 0 1 .474.225l4.689 5.385a.83.83 0 0 1 .196.544v8.574l-.985 1.055-.355-.38v-8.666h-4.485a.702.702 0 0 1-.702-.702V6.538H7.34v16.924h9.44L18.217 25H7.34c-.356 0-.696-.162-.948-.45A1.661 1.661 0 0 1 6 23.461V6.538c0-.408.141-.799.392-1.087Zm9.222 1.678 2.791 3.205h-2.791V7.128ZM8.85 15.5a.65.65 0 0 1 .65-.65h7.2a.65.65 0 1 1 0 1.3H9.5a.65.65 0 0 1-.65-.65Zm0 2.4a.65.65 0 0 1 .65-.65h7.2a.65.65 0 1 1 0 1.3H9.5a.65.65 0 0 1-.65-.65Z" fill="currentColor"/>
<path d="m17.552 21.357 2.2 2.357 4.4-4.714" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
...@@ -49,6 +49,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -49,6 +49,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': 'Regular page', '/login': 'Regular page',
'/sprite': 'Regular page',
'/api/metrics': 'Regular page', '/api/metrics': 'Regular page',
'/api/log': 'Regular page', '/api/log': 'Regular page',
'/api/media-type': 'Regular page', '/api/media-type': 'Regular page',
...@@ -56,6 +57,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -56,6 +57,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/api/csrf': 'Regular page', '/api/csrf': 'Regular page',
'/api/healthz': 'Regular page', '/api/healthz': 'Regular page',
'/api/config': 'Regular page', '/api/config': 'Regular page',
'/api/sprite': 'Regular page',
'/auth/auth0': 'Regular page', '/auth/auth0': 'Regular page',
'/auth/unverified-email': 'Regular page', '/auth/unverified-email': 'Regular page',
}; };
......
...@@ -53,6 +53,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -53,6 +53,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': DEFAULT_TEMPLATE, '/login': DEFAULT_TEMPLATE,
'/sprite': DEFAULT_TEMPLATE,
'/api/metrics': DEFAULT_TEMPLATE, '/api/metrics': DEFAULT_TEMPLATE,
'/api/log': DEFAULT_TEMPLATE, '/api/log': DEFAULT_TEMPLATE,
'/api/media-type': DEFAULT_TEMPLATE, '/api/media-type': DEFAULT_TEMPLATE,
...@@ -60,6 +61,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -60,6 +61,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/api/csrf': DEFAULT_TEMPLATE, '/api/csrf': DEFAULT_TEMPLATE,
'/api/healthz': DEFAULT_TEMPLATE, '/api/healthz': DEFAULT_TEMPLATE,
'/api/config': DEFAULT_TEMPLATE, '/api/config': DEFAULT_TEMPLATE,
'/api/sprite': DEFAULT_TEMPLATE,
'/auth/auth0': DEFAULT_TEMPLATE, '/auth/auth0': DEFAULT_TEMPLATE,
'/auth/unverified-email': DEFAULT_TEMPLATE, '/auth/unverified-email': DEFAULT_TEMPLATE,
}; };
......
...@@ -49,13 +49,15 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -49,13 +49,15 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': '%network_name% login', '/login': '%network_name% login',
'/sprite': '%network_name% SVG sprite',
'/api/metrics': '%network_name% node API prometheus metrics', '/api/metrics': '%network_name% node API prometheus metrics',
'/api/log': '%network_name% node API request log', '/api/log': '%network_name% node API request log',
'/api/media-type': '%network_name% node API media type', '/api/media-type': '%network_name% node API media type',
'/api/proxy': '%network_name% node API proxy', '/api/proxy': '%network_name% node API proxy',
'/api/csrf': '%network_name% node API CSRF token', '/api/csrf': '%network_name% node API CSRF token',
'/api/healthz': '%network_name% node API health check', '/api/healthz': '%network_name% node API health check',
'/api/config': '%network_name% node API health check', '/api/config': '%network_name% node API app config',
'/api/sprite': '%network_name% node API SVG sprite content',
'/auth/auth0': '%network_name% authentication', '/auth/auth0': '%network_name% authentication',
'/auth/unverified-email': '%network_name% unverified email', '/auth/unverified-email': '%network_name% unverified email',
}; };
......
...@@ -47,13 +47,15 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -47,13 +47,15 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': 'Login', '/login': 'Login',
'/sprite': 'Sprite',
'/api/metrics': 'Node API: Prometheus metrics', '/api/metrics': 'Node API: Prometheus metrics',
'/api/log': 'Node API: Request log', '/api/log': 'Node API: Request log',
'/api/media-type': 'Node API: Media type', '/api/media-type': 'Node API: Media type',
'/api/proxy': 'Node API: Proxy', '/api/proxy': 'Node API: Proxy',
'/api/csrf': 'Node API: CSRF token', '/api/csrf': 'Node API: CSRF token',
'/api/healthz': 'Node API: Health check', '/api/healthz': 'Node API: Health check',
'/api/config': 'Node API: Health check', '/api/config': 'Node API: App config',
'/api/sprite': 'Node API: SVG sprite content',
'/auth/auth0': 'Auth', '/auth/auth0': 'Auth',
'/auth/unverified-email': 'Unverified email', '/auth/unverified-email': 'Unverified email',
}; };
......
...@@ -245,6 +245,16 @@ export const login: GetServerSideProps<Props> = async(context) => { ...@@ -245,6 +245,16 @@ export const login: GetServerSideProps<Props> = async(context) => {
return base(context); return base(context);
}; };
export const dev: GetServerSideProps<Props> = async(context) => {
if (!config.app.isDev) {
return {
notFound: true,
};
}
return base(context);
};
export const publicTagsSubmit: GetServerSideProps<Props> = async(context) => { export const publicTagsSubmit: GetServerSideProps<Props> = async(context) => {
if (!config.features.publicTagsSubmission.isEnabled) { if (!config.features.publicTagsSubmission.isEnabled) {
......
...@@ -22,6 +22,7 @@ declare module "nextjs-routes" { ...@@ -22,6 +22,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/api/media-type"> | StaticRoute<"/api/media-type">
| StaticRoute<"/api/metrics"> | StaticRoute<"/api/metrics">
| StaticRoute<"/api/proxy"> | StaticRoute<"/api/proxy">
| StaticRoute<"/api/sprite">
| StaticRoute<"/api-docs"> | StaticRoute<"/api-docs">
| DynamicRoute<"/apps/[id]", { "id": string }> | DynamicRoute<"/apps/[id]", { "id": string }>
| StaticRoute<"/apps"> | StaticRoute<"/apps">
...@@ -48,6 +49,7 @@ declare module "nextjs-routes" { ...@@ -48,6 +49,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/output-roots"> | StaticRoute<"/output-roots">
| StaticRoute<"/public-tags/submit"> | StaticRoute<"/public-tags/submit">
| StaticRoute<"/search-results"> | StaticRoute<"/search-results">
| StaticRoute<"/sprite">
| StaticRoute<"/stats"> | StaticRoute<"/stats">
| DynamicRoute<"/token/[hash]", { "hash": string }> | DynamicRoute<"/token/[hash]", { "hash": string }>
| DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }> | DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }>
......
import fs from 'fs';
import type { NextApiRequest, NextApiResponse } from 'next';
import path from 'path';
import config from 'configs/app';
const ROOT_DIR = './icons';
const NAME_PREFIX = ROOT_DIR.replace('./', '') + '/';
interface IconInfo {
name: string;
fileSize: number;
}
const getIconName = (filePath: string) => filePath.replace(NAME_PREFIX, '').replace('.svg', '');
function collectIconNames(dir: string) {
const files = fs.readdirSync(dir, { withFileTypes: true });
let icons: Array<IconInfo> = [];
files.forEach((file) => {
const filePath = path.join(dir, file.name);
const stats = fs.statSync(filePath);
file.name.endsWith('.svg') && icons.push({
name: getIconName(filePath),
fileSize: stats.size,
});
if (file.isDirectory()) {
icons = [ ...icons, ...collectIconNames(filePath) ];
}
});
return icons;
}
export default async function spriteHandler(req: NextApiRequest, res: NextApiResponse) {
if (!config.app.isDev) {
return res.status(404).json({ error: 'Not found' });
}
const icons = collectIconNames(ROOT_DIR);
res.status(200).json({
icons,
});
}
import type { NextPage } from 'next';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
import Sprite from 'ui/pages/Sprite';
const Page: NextPage = () => {
return (
<PageNextJs pathname="/sprite">
<Sprite/>
</PageNextJs>
);
};
export default Page;
export { dev as getServerSideProps } from 'nextjs/getServerSideProps';
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
| "arrows/north-east" | "arrows/north-east"
| "arrows/south-east" | "arrows/south-east"
| "arrows/up-down" | "arrows/up-down"
| "arrows/up-head"
| "beta_xs" | "beta_xs"
| "beta" | "beta"
| "blob" | "blob"
...@@ -30,10 +31,10 @@ ...@@ -30,10 +31,10 @@
| "clock" | "clock"
| "coins/bitcoin" | "coins/bitcoin"
| "collection" | "collection"
| "contract_verified" | "contracts/regular_many"
| "contract" | "contracts/regular"
| "contracts_verified" | "contracts/verified_many"
| "contracts" | "contracts/verified"
| "copy" | "copy"
| "cross" | "cross"
| "delete" | "delete"
...@@ -59,7 +60,6 @@ ...@@ -59,7 +60,6 @@
| "files/sol" | "files/sol"
| "files/yul" | "files/yul"
| "filter" | "filter"
| "finalized"
| "flame" | "flame"
| "games" | "games"
| "gas_xl" | "gas_xl"
...@@ -99,7 +99,7 @@ ...@@ -99,7 +99,7 @@
| "publictags" | "publictags"
| "qr_code" | "qr_code"
| "refresh" | "refresh"
| "repeat_arrow" | "repeat"
| "restAPI" | "restAPI"
| "rocket_xl" | "rocket_xl"
| "rocket" | "rocket"
...@@ -146,14 +146,13 @@ ...@@ -146,14 +146,13 @@
| "transactions" | "transactions"
| "txn_batches_slim" | "txn_batches_slim"
| "txn_batches" | "txn_batches"
| "unfinalized"
| "uniswap" | "uniswap"
| "up"
| "user_op_slim" | "user_op_slim"
| "user_op" | "user_op"
| "validator" | "validator"
| "verification-steps/finalized"
| "verification-steps/unfinalized"
| "verified" | "verified"
| "verify-contract"
| "wallet" | "wallet"
| "wallets/coinbase" | "wallets/coinbase"
| "wallets/metamask" | "wallets/metamask"
......
...@@ -222,7 +222,7 @@ const ContractMethodForm = ({ data, attempt, onSubmit, onReset, isOpen }: Props) ...@@ -222,7 +222,7 @@ const ContractMethodForm = ({ data, attempt, onSubmit, onReset, isOpen }: Props)
onClick={ onReset } onClick={ onReset }
ml={ 1 } ml={ 1 }
> >
<IconSvg name="repeat_arrow" boxSize={ 5 } mr={ 1 }/> <IconSvg name="repeat" boxSize={ 5 } mr={ 1 }/>
Reset Reset
</Button> </Button>
) } ) }
......
...@@ -68,7 +68,7 @@ const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onC ...@@ -68,7 +68,7 @@ const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onC
return ( return (
<Skeleton isLoaded={ !stats.isPlaceholderData } ml={ 3 } display="flex" alignItems="center" color={ diffColor }> <Skeleton isLoaded={ !stats.isPlaceholderData } ml={ 3 } display="flex" alignItems="center" color={ diffColor }>
<IconSvg name="up" boxSize={ 5 } mr={ 1 } transform={ diff < 0 ? 'rotate(180deg)' : 'rotate(0)' }/> <IconSvg name="arrows/up-head" boxSize={ 5 } mr={ 1 } transform={ diff < 0 ? 'rotate(180deg)' : 'rotate(0)' }/>
<Text color={ diffColor } fontWeight={ 600 }>{ diff }%</Text> <Text color={ diffColor } fontWeight={ 600 }>{ diff }%</Text>
</Skeleton> </Skeleton>
); );
......
...@@ -77,7 +77,7 @@ const ChainIndicators = () => { ...@@ -77,7 +77,7 @@ const ChainIndicators = () => {
return ( return (
<Skeleton isLoaded={ !statsQueryResult.isPlaceholderData } display="flex" alignItems="center" color={ diffColor } ml={ 2 }> <Skeleton isLoaded={ !statsQueryResult.isPlaceholderData } display="flex" alignItems="center" color={ diffColor } ml={ 2 }>
<IconSvg name="up" boxSize={ 5 } mr={ 1 } transform={ diff < 0 ? 'rotate(180deg)' : 'rotate(0)' }/> <IconSvg name="arrows/up-head" boxSize={ 5 } mr={ 1 } transform={ diff < 0 ? 'rotate(180deg)' : 'rotate(0)' }/>
<Text color={ diffColor } fontWeight={ 600 }>{ diff }%</Text> <Text color={ diffColor } fontWeight={ 600 }>{ diff }%</Text>
</Skeleton> </Skeleton>
); );
......
...@@ -76,7 +76,7 @@ const AppSecurityReport = ({ ...@@ -76,7 +76,7 @@ const AppSecurityReport = ({
<Text fontWeight="500" fontSize="xs" mb={ 2 } variant="secondary">Smart contracts info</Text> <Text fontWeight="500" fontSize="xs" mb={ 2 } variant="secondary">Smart contracts info</Text>
<Flex alignItems="center" justifyContent="space-between" py={ 1.5 }> <Flex alignItems="center" justifyContent="space-between" py={ 1.5 }>
<Flex alignItems="center"> <Flex alignItems="center">
<IconSvg name="contracts_verified" boxSize={ 5 } color="green.500" mr={ 1 }/> <IconSvg name="contracts/verified_many" boxSize={ 5 } color="green.500" mr={ 1 }/>
<Text>Verified contracts</Text> <Text>Verified contracts</Text>
</Flex> </Flex>
<Link fontSize="sm" fontWeight="500" onClick={ showAllContracts }> <Link fontSize="sm" fontWeight="500" onClick={ showAllContracts }>
......
...@@ -191,7 +191,7 @@ const MarketplaceAppModal = ({ ...@@ -191,7 +191,7 @@ const MarketplaceAppModal = ({
mb={ 6 } mb={ 6 }
> >
<Flex alignItems="center" gap={ 2 } flexWrap="wrap"> <Flex alignItems="center" gap={ 2 } flexWrap="wrap">
<IconSvg name="contracts_verified" boxSize={ 5 } color="green.500"/> <IconSvg name="contracts/verified_many" boxSize={ 5 } color="green.500"/>
<Text>Verified contracts</Text> <Text>Verified contracts</Text>
<Text fontWeight="500"> <Text fontWeight="500">
{ securityReport?.overallInfo.verifiedNumber ?? 0 } of { securityReport?.overallInfo.totalContractsNumber ?? 0 } { securityReport?.overallInfo.verifiedNumber ?? 0 } of { securityReport?.overallInfo.totalContractsNumber ?? 0 }
......
import { Flex, Box, Tooltip, useClipboard, useColorModeValue } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { StaticRoute } from 'nextjs-routes';
import { route } from 'nextjs-routes';
import useFetch from 'lib/hooks/useFetch';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import EmptySearchResult from 'ui/shared/EmptySearchResult';
import FilterInput from 'ui/shared/filters/FilterInput';
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;
fileSize: number;
}
const Item = ({ name, fileSize, bgColor }: IconInfo & { bgColor: string }) => {
const { hasCopied, onCopy } = 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={ onCopy }
cursor="pointer"
>
<IconSvg name={ name as IconName } boxSize="100px" bgColor={ bgColor } borderRadius="base"/>
<Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } isOpen={ 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 bgColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100');
const fetch = useFetch();
const { data, isFetching, isError } = useQuery({
queryKey: [ 'sprite' ],
queryFn: () => {
const url = route({ pathname: '/node-api/sprite' as StaticRoute<'/api/sprite'>['pathname'] });
return fetch<{ icons: Array<IconInfo> }, unknown>(url);
},
});
const content = (() => {
if (isFetching) {
return <ContentLoader/>;
}
if (isError || !data || !('icons' in data)) {
return <DataFetchAlert/>;
}
const items = data.icons.filter((icon) => icon.name.includes(searchTerm));
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={ bgColor }/>) }
</Flex>
);
})();
const total = React.useMemo(() => {
if (!data || !('icons' in data)) {
return;
}
return data?.icons.reduce((result, item) => {
result.num++;
result.fileSize += item.fileSize;
return result;
}, { num: 0, fileSize: 0 });
}, [ data ]);
const searchInput = <FilterInput placeholder="Search by name..." onChange={ setSearchTerm } isLoading={ 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);
...@@ -198,7 +198,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError, ...@@ -198,7 +198,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
size="sm" size="sm"
variant="outline" variant="outline"
onClick={ handleZoomResetClick } onClick={ handleZoomResetClick }
icon={ <IconSvg name="repeat_arrow" w={ 4 } h={ 4 }/> } icon={ <IconSvg name="repeat" w={ 4 } h={ 4 }/> }
/> />
</Tooltip> </Tooltip>
......
...@@ -71,7 +71,7 @@ const FullscreenChartModal = ({ ...@@ -71,7 +71,7 @@ const FullscreenChartModal = ({
{ !isZoomResetInitial && ( { !isZoomResetInitial && (
<Button <Button
leftIcon={ <IconSvg name="repeat_arrow" w={ 4 } h={ 4 }/> } leftIcon={ <IconSvg name="repeat" w={ 4 } h={ 4 }/> }
colorScheme="blue" colorScheme="blue"
gridColumn={ 2 } gridColumn={ 2 }
justifySelf="end" justifySelf="end"
......
...@@ -63,7 +63,7 @@ const Icon = (props: IconProps) => { ...@@ -63,7 +63,7 @@ const Icon = (props: IconProps) => {
<span> <span>
<EntityBase.Icon <EntityBase.Icon
{ ...props } { ...props }
name="contract_verified" name="contracts/verified"
color="green.500" color="green.500"
borderRadius={ 0 } borderRadius={ 0 }
/> />
...@@ -77,7 +77,7 @@ const Icon = (props: IconProps) => { ...@@ -77,7 +77,7 @@ const Icon = (props: IconProps) => {
<span> <span>
<EntityBase.Icon <EntityBase.Icon
{ ...props } { ...props }
name="contract" name="contracts/regular"
borderRadius={ 0 } borderRadius={ 0 }
/> />
</span> </span>
......
...@@ -22,7 +22,7 @@ const VerificationStep = ({ step, isLast, isPassed, isPending }: Props) => { ...@@ -22,7 +22,7 @@ const VerificationStep = ({ step, isLast, isPassed, isPending }: Props) => {
return ( return (
<HStack gap={ 2 } color={ stepColor }> <HStack gap={ 2 } color={ stepColor }>
<IconSvg name={ isPassed ? 'finalized' : 'unfinalized' } boxSize={ 5 }/> <IconSvg name={ isPassed ? 'verification-steps/finalized' : 'verification-steps/unfinalized' } boxSize={ 5 }/>
<Box color={ stepColor }>{ typeof step === 'string' ? step : step.content }</Box> <Box color={ stepColor }>{ typeof step === 'string' ? step : step.content }</Box>
{ !isLast && <IconSvg name="arrows/east" boxSize={ 5 }/> } { !isLast && <IconSvg name="arrows/east" boxSize={ 5 }/> }
</HStack> </HStack>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment