Commit 1ece2b7f authored by tom goriunov's avatar tom goriunov Committed by GitHub

Replace system selectors with Blockscout selectors style (#2451)

* migrate from native select to the custom one

* migrate popover radio filter to select component

* migrate stats menu to select

* migrate Sort component

* screenshot updates
parent b1b4d81d
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 320" shape-rendering="crispEdges"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 320" shape-rendering="crispEdges">
<rect width="100%" height="100%" fill="#d5d7e1"/> <rect width="100%" height="100%" fill="#d5d7e1"/>
<path fill="#caeff9" d="M90 210h140v10H90zM90 220h140v10H90zM90 230h140v10H90zM90 240h140v10H90zM90 250h20v10H90zM120 250h110v10H120zM90 260h20v10H90zM120 260h110v10H120zM90 270h20v10H90zM120 270h110v10H120zM90 280h20v10H90zM120 280h110v10H120zM90 290h20v10H90zM120 290h110v10H120zM90 300h20v10H90zM120 300h110v10H120zM90 310h20v10H90zM120 310h110v10H120z"/> <path fill="#caeff9" d="M90 210h140v10H90zm0 10h140v10H90zm0 10h140v10H90zm0 10h140v10H90zm0 10h20v10H90zm30 0h110v10H120zm-30 10h20v10H90zm30 0h110v10H120zm-30 10h20v10H90zm30 0h110v10H120zm-30 10h20v10H90zm30 0h110v10H120zm-30 10h20v10H90zm30 0h110v10H120zm-30 10h20v10H90zm30 0h110v10H120zm-30 10h20v10H90zm30 0h110v10H120z"/>
<path fill="#9f21a0" d="M200 230h30v10h-30z"/> <path fill="#9f21a0" d="M200 230h30v10h-30z"/>
<path fill="#d22209" d="M160 240h40v10h-40z"/> <path fill="#d22209" d="M160 240h40v10h-40z"/>
<path fill="#9f21a0" d="M200 240h30v10h-30z"/> <path fill="#9f21a0" d="M200 240h30v10h-30z"/>
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
<path fill="#ffc925" d="M90 270h20v10H90z"/> <path fill="#ffc925" d="M90 270h20v10H90z"/>
<path fill="#fe500c" d="M120 270h40v10h-40z"/> <path fill="#fe500c" d="M120 270h40v10h-40z"/>
<path fill="#ffc925" d="M90 280h20v10H90z"/> <path fill="#ffc925" d="M90 280h20v10H90z"/>
<path fill="#ffc110" d="M160 30h80v10h-80zM140 40h50v10h-50z"/> <path fill="#ffc110" d="M160 30h80v10h-80zm-20 10h50v10h-50z"/>
<path fill="#fff0ee" d="M190 40h40v10h-40z"/> <path fill="#fff0ee" d="M190 40h40v10h-40z"/>
<path fill="#ffc110" d="M230 40h20v10h-20zM130 50h50v10h-50z"/> <path fill="#ffc110" d="M230 40h20v10h-20zM130 50h50v10h-50z"/>
<path fill="#fff0ee" d="M180 50h20v10h-20z"/> <path fill="#fff0ee" d="M180 50h20v10h-20z"/>
...@@ -29,17 +29,17 @@ ...@@ -29,17 +29,17 @@
<path fill="#fff0ee" d="M190 70h40v10h-40z"/> <path fill="#fff0ee" d="M190 70h40v10h-40z"/>
<path fill="#ffc110" d="M230 70h20v10h-20z"/> <path fill="#ffc110" d="M230 70h20v10h-20z"/>
<path fill="#fe500c" d="M250 70h30v10h-30z"/> <path fill="#fe500c" d="M250 70h30v10h-30z"/>
<path fill="#ffc110" d="M50 80h20v10H50zM140 80h100v10H140z"/> <path fill="#ffc110" d="M50 80h20v10H50zm90 0h100v10H140z"/>
<path fill="#fe500c" d="M240 80h30v10h-30z"/> <path fill="#fe500c" d="M240 80h30v10h-30z"/>
<path fill="#ffc110" d="M50 90h50v10H50zM150 90h100v10H150zM40 100h220v10H40zM40 110h230v10H40zM40 120h240v10H40zM40 130h240v10H40zM40 140h240v10H40zM50 150h230v10H50zM50 160h230v10H50zM60 170h220v10H60zM60 180h110v10H60z"/> <path fill="#ffc110" d="M50 90h50v10H50zm100 0h100v10H150zM40 100h220v10H40zm0 10h230v10H40zm0 10h240v10H40zm0 10h240v10H40zm0 10h240v10H40zm10 10h230v10H50zm0 10h230v10H50zm10 10h220v10H60zm0 10h110v10H60z"/>
<path fill="#d08b11" d="M170 180h10v10h-10z"/> <path fill="#d08b11" d="M170 180h10v10h-10z"/>
<path fill="#ffc110" d="M180 180h90v10h-90zM70 190h70v10H70z"/> <path fill="#ffc110" d="M180 180h90v10h-90zM70 190h70v10H70z"/>
<path fill="#d08b11" d="M140 190h30v10h-30z"/> <path fill="#d08b11" d="M140 190h30v10h-30z"/>
<path fill="#ffc110" d="M170 190h90v10h-90zM90 200h160v10H90z"/> <path fill="#ffc110" d="M170 190h90v10h-90zm-80 10h160v10H90z"/>
<path fill="#ff638d" d="M100 110h60v10h-60zM170 110h60v10h-60zM100 120h10v10h-10z"/> <path fill="#ff638d" d="M100 110h60v10h-60zm70 0h60v10h-60zm-70 10h10v10h-10z"/>
<path fill="#fff" d="M110 120h20v10h-20z"/> <path fill="#fff" d="M110 120h20v10h-20z"/>
<path d="M130 120h20v10h-20z"/> <path d="M130 120h20v10h-20z"/>
<path fill="#ff638d" d="M150 120h10v10h-10zM170 120h10v10h-10z"/> <path fill="#ff638d" d="M150 120h10v10h-10zm20 0h10v10h-10z"/>
<path fill="#fff" d="M180 120h20v10h-20z"/> <path fill="#fff" d="M180 120h20v10h-20z"/>
<path d="M200 120h20v10h-20z"/> <path d="M200 120h20v10h-20z"/>
<path fill="#ff638d" d="M220 120h10v10h-10zM70 130h40v10H70z"/> <path fill="#ff638d" d="M220 120h10v10h-10zM70 130h40v10H70z"/>
...@@ -54,11 +54,11 @@ ...@@ -54,11 +54,11 @@
<path fill="#ff638d" d="M150 140h30v10h-30z"/> <path fill="#ff638d" d="M150 140h30v10h-30z"/>
<path fill="#fff" d="M180 140h20v10h-20z"/> <path fill="#fff" d="M180 140h20v10h-20z"/>
<path d="M200 140h20v10h-20z"/> <path d="M200 140h20v10h-20z"/>
<path fill="#ff638d" d="M220 140h10v10h-10zM70 150h10v10H70zM100 150h10v10h-10z"/> <path fill="#ff638d" d="M220 140h10v10h-10zM70 150h10v10H70zm30 0h10v10h-10z"/>
<path fill="#fff" d="M110 150h20v10h-20z"/> <path fill="#fff" d="M110 150h20v10h-20z"/>
<path d="M130 150h20v10h-20z"/> <path d="M130 150h20v10h-20z"/>
<path fill="#ff638d" d="M150 150h10v10h-10zM170 150h10v10h-10z"/> <path fill="#ff638d" d="M150 150h10v10h-10zm20 0h10v10h-10z"/>
<path fill="#fff" d="M180 150h20v10h-20z"/> <path fill="#fff" d="M180 150h20v10h-20z"/>
<path d="M200 150h20v10h-20z"/> <path d="M200 150h20v10h-20z"/>
<path fill="#ff638d" d="M220 150h10v10h-10zM100 160h60v10h-60zM170 160h60v10h-60z"/> <path fill="#ff638d" d="M220 150h10v10h-10zm-120 10h60v10h-60zm70 0h60v10h-60z"/>
</svg> </svg>
...@@ -24,6 +24,7 @@ import ContractDetailsInfo from './info/ContractDetailsInfo'; ...@@ -24,6 +24,7 @@ import ContractDetailsInfo from './info/ContractDetailsInfo';
import useContractDetailsTabs from './useContractDetailsTabs'; import useContractDetailsTabs from './useContractDetailsTabs';
const TAB_LIST_PROPS = { flexWrap: 'wrap', rowGap: 2 }; const TAB_LIST_PROPS = { flexWrap: 'wrap', rowGap: 2 };
const LEFT_SLOT_PROPS = { w: { base: '100%', lg: 'auto' } };
type Props = { type Props = {
addressHash: string; addressHash: string;
...@@ -117,6 +118,7 @@ const ContractDetails = ({ addressHash, channel, mainContractQuery }: Props) => ...@@ -117,6 +118,7 @@ const ContractDetails = ({ addressHash, channel, mainContractQuery }: Props) =>
size="sm" size="sm"
leftSlot={ addressSelector } leftSlot={ addressSelector }
tabListProps={ TAB_LIST_PROPS } tabListProps={ TAB_LIST_PROPS }
leftSlotProps={ LEFT_SLOT_PROPS }
/> />
) : ( ) : (
<> <>
......
import { chakra, Flex, Select, Skeleton } from '@chakra-ui/react'; import { chakra, Flex, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
...@@ -6,6 +6,7 @@ import { route } from 'nextjs-routes'; ...@@ -6,6 +6,7 @@ import { route } from 'nextjs-routes';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import LinkNewTab from 'ui/shared/links/LinkNewTab'; import LinkNewTab from 'ui/shared/links/LinkNewTab';
import Select from 'ui/shared/select/Select';
export interface Item { export interface Item {
address: string; address: string;
...@@ -23,13 +24,17 @@ interface Props { ...@@ -23,13 +24,17 @@ interface Props {
const ContractSourceAddressSelector = ({ className, selectedItem, onItemSelect, items, isLoading, label }: Props) => { const ContractSourceAddressSelector = ({ className, selectedItem, onItemSelect, items, isLoading, label }: Props) => {
const handleItemSelect = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => { const handleItemSelect = React.useCallback((value: string) => {
const nextOption = items.find(({ address }) => address === event.target.value); const nextOption = items.find(({ address }) => address === value);
if (nextOption) { if (nextOption) {
onItemSelect(nextOption); onItemSelect(nextOption);
} }
}, [ items, onItemSelect ]); }, [ items, onItemSelect ]);
const options = React.useMemo(() => {
return items.map(({ address, name }) => ({ label: name || address, value: address }));
}, [ items ]);
if (isLoading) { if (isLoading) {
return <Skeleton h={ 6 } w={{ base: '300px', lg: '500px' }} className={ className }/>; return <Skeleton h={ 6 } w={{ base: '300px', lg: '500px' }} className={ className }/>;
} }
...@@ -53,19 +58,14 @@ const ContractSourceAddressSelector = ({ className, selectedItem, onItemSelect, ...@@ -53,19 +58,14 @@ const ContractSourceAddressSelector = ({ className, selectedItem, onItemSelect,
<Flex columnGap={ 3 } rowGap={ 2 } alignItems="center" className={ className }> <Flex columnGap={ 3 } rowGap={ 2 } alignItems="center" className={ className }>
<chakra.span fontWeight={ 500 } fontSize="sm">{ label }</chakra.span> <chakra.span fontWeight={ 500 } fontSize="sm">{ label }</chakra.span>
<Select <Select
size="xs" options={ options }
value={ selectedItem.address } name="contract-source-address"
defaultValue={ options[0].value }
onChange={ handleItemSelect } onChange={ handleItemSelect }
w="auto" isLoading={ isLoading }
maxW={{ base: '180px', lg: 'none' }}
fontWeight={ 600 } fontWeight={ 600 }
borderRadius="base" />
>
{ items.map((item) => (
<option key={ item.address } value={ item.address }>
{ item.name }
</option>
)) }
</Select>
<Flex columnGap={ 2 } alignItems="center"> <Flex columnGap={ 2 } alignItems="center">
<CopyToClipboard text={ selectedItem.address } ml={ 0 }/> <CopyToClipboard text={ selectedItem.address } ml={ 0 }/>
<LinkNewTab <LinkNewTab
......
...@@ -12,7 +12,8 @@ test('text', async({ render }) => { ...@@ -12,7 +12,8 @@ test('text', async({ render }) => {
const data = '0xE2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A280E2A3A4E2A1B6E2A0BFE2A0BFE2A0B7E2A3B6E2A384E2A080E2A080E2A080E2A080E2A0800AE2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A3B0E2A1BFE2A081E2A080E2A080E2A280E2A380E2A180E2A099E2A3B7E2A180E2A080E2A080E2A0800AE2A080E2A080E2A080E2A180E2A080E2A080E2A080E2A080E2A080E2A2A0E2A3BFE2A081E2A080E2A080E2A080E2A098E2A0BFE2A083E2A080E2A2B8E2A3BFE2A3BFE2A3BFE2A3BF0AE2A080E2A3A0E2A1BFE2A09BE2A2B7E2A3A6E2A180E2A080E2A080E2A088E2A3BFE2A184E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A3B8E2A3BFE2A3BFE2A3BFE2A09F0AE2A2B0E2A1BFE2A081E2A080E2A080E2A099E2A2BFE2A3A6E2A3A4E2A3A4E2A3BCE2A3BFE2A384E2A080E2A080E2A080E2A080E2A080E2A2B4E2A19FE2A09BE2A08BE2A081E2A0800AE2A3BFE2A087E2A080E2A080E2A080E2A080E2A080E2A089E2A089E2A089E2A089E2A089E2A081E2A080E2A080E2A080E2A080E2A080E2A088E2A3BFE2A180E2A080E2A080E2A0800AE2A3BFE2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A2B9E2A187E2A080E2A080E2A0800AE2A3BFE2A186E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A3BCE2A187E2A080E2A080E2A0800AE2A0B8E2A3B7E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A2A0E2A1BFE2A080E2A080E2A080E2A0800AE2A080E2A0B9E2A3B7E2A3A4E2A380E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A380E2A3B0E2A1BFE2A081E2A080E2A080E2A080E2A0800AE2A080E2A080E2A080E2A089E2A099E2A09BE2A0BFE2A0B6E2A3B6E2A3B6E2A3B6E2A3B6E2A3B6E2A0B6E2A0BFE2A09FE2A09BE2A089E2A080E2A080E2A080E2A080E2A080E2A080'; const data = '0xE2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A280E2A3A4E2A1B6E2A0BFE2A0BFE2A0B7E2A3B6E2A384E2A080E2A080E2A080E2A080E2A0800AE2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A3B0E2A1BFE2A081E2A080E2A080E2A280E2A380E2A180E2A099E2A3B7E2A180E2A080E2A080E2A0800AE2A080E2A080E2A080E2A180E2A080E2A080E2A080E2A080E2A080E2A2A0E2A3BFE2A081E2A080E2A080E2A080E2A098E2A0BFE2A083E2A080E2A2B8E2A3BFE2A3BFE2A3BFE2A3BF0AE2A080E2A3A0E2A1BFE2A09BE2A2B7E2A3A6E2A180E2A080E2A080E2A088E2A3BFE2A184E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A3B8E2A3BFE2A3BFE2A3BFE2A09F0AE2A2B0E2A1BFE2A081E2A080E2A080E2A099E2A2BFE2A3A6E2A3A4E2A3A4E2A3BCE2A3BFE2A384E2A080E2A080E2A080E2A080E2A080E2A2B4E2A19FE2A09BE2A08BE2A081E2A0800AE2A3BFE2A087E2A080E2A080E2A080E2A080E2A080E2A089E2A089E2A089E2A089E2A089E2A081E2A080E2A080E2A080E2A080E2A080E2A088E2A3BFE2A180E2A080E2A080E2A0800AE2A3BFE2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A2B9E2A187E2A080E2A080E2A0800AE2A3BFE2A186E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A3BCE2A187E2A080E2A080E2A0800AE2A0B8E2A3B7E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A2A0E2A1BFE2A080E2A080E2A080E2A0800AE2A080E2A0B9E2A3B7E2A3A4E2A380E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A380E2A3B0E2A1BFE2A081E2A080E2A080E2A080E2A0800AE2A080E2A080E2A080E2A089E2A099E2A09BE2A0BFE2A0B6E2A3B6E2A3B6E2A3B6E2A3B6E2A3B6E2A0B6E2A0BFE2A09FE2A09BE2A089E2A080E2A080E2A080E2A080E2A080E2A080';
const component = await render(<BlobData hash="0x01" data={ data }/>); const component = await render(<BlobData hash="0x01" data={ data }/>);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
await component.locator('select').selectOption('UTF-8'); await component.getByRole('button', { name: 'Raw' }).click();
await component.getByText('UTF-8').click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -21,7 +22,8 @@ test('image', async({ render }) => { ...@@ -21,7 +22,8 @@ test('image', async({ render }) => {
const data = '0x89504E470D0A1A0A0000000D494844520000003C0000003C0403000000C8D2C4410000000467414D410000B18F0BFC6105000000017352474200AECE1CE900000027504C54454C69712B6CB02A6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB0F4205A540000000C74524E5300ED2F788CD91B99475C09B969CFA99D0000004F7A5458745261772070726F66696C65207479706520697074630000789CE3CA2C2849E6520003230B2E630B1323134B9314031320448034C3640323B35420CBD8D4C8C4CCC41CC407CB8048A04A2E0028950EE32A226D1F0000000970485973000084DF000084DF0195C81C33000000F24944415438CB636000018E983367CE482780D90CDA40F6991D0C4820152472A60ACCE6DA03629F4E40929E03961602B39964C09C0624691B24690E88F48461215D03160903B3D962C01C07842C2758C341A80643B0B40484C3646C6C5C78E6E016171723A8E215262EEE31670E161B1B7731304C05AB155EC08002C0D172E6F80206884DBB50651938CF4003FE0CBA4390E3C56064482F53525252C329CD562A2828283A0197340B22AAB0494332C311FCD2C747A547A58996C69998D8F12745B68DA0846C85331B2CEAE8E8681A81D91F8B348C4605D0527B02A4283FA88026CD05163EAAC0900ED21EC9800EC0C2110C002BBA9FE999B920330000000049454E44AE426082'; const data = '0x89504E470D0A1A0A0000000D494844520000003C0000003C0403000000C8D2C4410000000467414D410000B18F0BFC6105000000017352474200AECE1CE900000027504C54454C69712B6CB02A6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB0F4205A540000000C74524E5300ED2F788CD91B99475C09B969CFA99D0000004F7A5458745261772070726F66696C65207479706520697074630000789CE3CA2C2849E6520003230B2E630B1323134B9314031320448034C3640323B35420CBD8D4C8C4CCC41CC407CB8048A04A2E0028950EE32A226D1F0000000970485973000084DF000084DF0195C81C33000000F24944415438CB636000018E983367CE482780D90CDA40F6991D0C4820152472A60ACCE6DA03629F4E40929E03961602B39964C09C0624691B24690E88F48461215D03160903B3D962C01C07842C2758C341A80643B0B40484C3646C6C5C78E6E016171723A8E215262EEE31670E161B1B7731304C05AB155EC08002C0D172E6F80206884DBB50651938CF4003FE0CBA4390E3C56064482F53525252C329CD562A2828283A0197340B22AAB0494332C311FCD2C747A547A58996C69998D8F12745B68DA0846C85331B2CEAE8E8681A81D91F8B348C4605D0527B02A4283FA88026CD05163EAAC0900ED21EC9800EC0C2110C002BBA9FE999B920330000000049454E44AE426082';
const component = await render(<BlobData hash="0x01" data={ data }/>); const component = await render(<BlobData hash="0x01" data={ data }/>);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
await component.locator('select').selectOption('Base64'); await component.getByRole('button', { name: 'Image' }).click();
await component.getByText('Base64').click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
......
import { Flex, GridItem, Select, Skeleton, Button } from '@chakra-ui/react'; import { Flex, GridItem, Skeleton, Button } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import * as blobUtils from 'lib/blob'; import * as blobUtils from 'lib/blob';
...@@ -10,12 +10,18 @@ import hexToBytes from 'lib/hexToBytes'; ...@@ -10,12 +10,18 @@ import hexToBytes from 'lib/hexToBytes';
import hexToUtf8 from 'lib/hexToUtf8'; import hexToUtf8 from 'lib/hexToUtf8';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import RawDataSnippet from 'ui/shared/RawDataSnippet'; import RawDataSnippet from 'ui/shared/RawDataSnippet';
import Select from 'ui/shared/select/Select';
import BlobDataImage from './BlobDataImage'; import BlobDataImage from './BlobDataImage';
const FORMATS = [ 'Image', 'Raw', 'UTF-8', 'Base64' ] as const; const FORMATS = [
{ label: 'Image', value: 'Image' as const },
{ label: 'Raw', value: 'Raw' as const },
{ label: 'UTF-8', value: 'UTF-8' as const },
{ label: 'Base64', value: 'Base64' as const },
];
type Format = typeof FORMATS[number]; type Format = typeof FORMATS[number]['value'];
interface Props { interface Props {
data: string; data: string;
...@@ -34,7 +40,7 @@ const BlobData = ({ data, isLoading, hash }: Props) => { ...@@ -34,7 +40,7 @@ const BlobData = ({ data, isLoading, hash }: Props) => {
}, [ data, isLoading ]); }, [ data, isLoading ]);
const isImage = guessedType?.mime?.startsWith('image/'); const isImage = guessedType?.mime?.startsWith('image/');
const formats = isImage ? FORMATS : FORMATS.filter((format) => format !== 'Image'); const formats = isImage ? FORMATS : FORMATS.filter((format) => format.value !== 'Image');
React.useEffect(() => { React.useEffect(() => {
if (isImage) { if (isImage) {
...@@ -42,10 +48,6 @@ const BlobData = ({ data, isLoading, hash }: Props) => { ...@@ -42,10 +48,6 @@ const BlobData = ({ data, isLoading, hash }: Props) => {
} }
}, [ isImage ]); }, [ isImage ]);
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setFormat(event.target.value as Format);
}, []);
const handleDownloadButtonClick = React.useCallback(() => { const handleDownloadButtonClick = React.useCallback(() => {
const fileBlob = (() => { const fileBlob = (() => {
switch (format) { switch (format) {
...@@ -104,16 +106,13 @@ const BlobData = ({ data, isLoading, hash }: Props) => { ...@@ -104,16 +106,13 @@ const BlobData = ({ data, isLoading, hash }: Props) => {
</Skeleton> </Skeleton>
<Skeleton ml={ 5 } isLoaded={ !isLoading }> <Skeleton ml={ 5 } isLoaded={ !isLoading }>
<Select <Select
size="xs" options={ formats }
borderRadius="base" name="format"
value={ format } defaultValue={ format }
onChange={ handleSelectChange } onChange={ setFormat }
w="auto" isLoading={ isLoading }
> w="95px"
{ formats.map((format) => ( />
<option key={ format } value={ format }>{ format }</option>
)) }
</Select>
</Skeleton> </Skeleton>
<Skeleton ml="auto" mr={ 3 } isLoaded={ !isLoading }> <Skeleton ml="auto" mr={ 3 } isLoaded={ !isLoading }>
<Button <Button
......
import type { NextRouter } from 'next/router'; import type { NextRouter } from 'next/router';
import type { SelectOption } from 'ui/shared/select/types';
import config from 'configs/app'; import config from 'configs/app';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import removeQueryParam from 'lib/router/removeQueryParam'; import removeQueryParam from 'lib/router/removeQueryParam';
import type { TOption } from 'ui/shared/sort/Option';
const feature = config.features.marketplace; const feature = config.features.marketplace;
export type SortValue = 'rating_score' | 'rating_count' | 'security_score'; export type SortValue = 'rating_score' | 'rating_count' | 'security_score';
export const SORT_OPTIONS: Array<TOption<SortValue>> = [ export const SORT_OPTIONS: Array<SelectOption<SortValue>> = [
{ title: 'Default', id: undefined }, { label: 'Default', value: undefined },
(feature.isEnabled && feature.rating) && { title: 'Top rated', id: 'rating_score' }, (feature.isEnabled && feature.rating) && { label: 'Top rated', value: 'rating_score' },
(feature.isEnabled && feature.rating) && { title: 'Most rated', id: 'rating_count' }, (feature.isEnabled && feature.rating) && { label: 'Most rated', value: 'rating_count' },
(feature.isEnabled && feature.securityReportsUrl) && { title: 'Security score', id: 'security_score' }, (feature.isEnabled && feature.securityReportsUrl) && { label: 'Security score', value: 'security_score' },
].filter(Boolean) as Array<TOption<SortValue>>; ].filter(Boolean) as Array<SelectOption<SortValue>>;
export function getAppUrl(url: string | undefined, router: NextRouter) { export function getAppUrl(url: string | undefined, router: NextRouter) {
if (!url) { if (!url) {
......
import type { EnsLookupSorting } from 'types/api/ens'; import type { EnsLookupSorting } from 'types/api/ens';
import type { SelectOption } from 'ui/shared/select/types';
import getNextSortValueShared from 'ui/shared/sort/getNextSortValue'; import getNextSortValueShared from 'ui/shared/sort/getNextSortValue';
import type { TOption } from 'ui/shared/sort/Option';
export type SortField = EnsLookupSorting['sort']; export type SortField = EnsLookupSorting['sort'];
export type Sort = `${ EnsLookupSorting['sort'] }-${ EnsLookupSorting['order'] }`; export type Sort = `${ EnsLookupSorting['sort'] }-${ EnsLookupSorting['order'] }`;
export const SORT_OPTIONS: Array<TOption<Sort>> = [ export const SORT_OPTIONS: Array<SelectOption<Sort>> = [
{ title: 'Default', id: undefined }, { label: 'Default', value: undefined },
{ title: 'Registered on descending', id: 'registration_date-DESC' }, { label: 'Registered on descending', value: 'registration_date-DESC' },
{ title: 'Registered on ascending', id: 'registration_date-ASC' }, { label: 'Registered on ascending', value: 'registration_date-ASC' },
]; ];
const SORT_SEQUENCE: Record<SortField, Array<Sort | undefined>> = { const SORT_SEQUENCE: Record<SortField, Array<Sort | undefined>> = {
......
...@@ -18,13 +18,14 @@ import getQueryParamString from 'lib/router/getQueryParamString'; ...@@ -18,13 +18,14 @@ import getQueryParamString from 'lib/router/getQueryParamString';
import isCustomAppError from 'ui/shared/AppError/isCustomAppError'; import isCustomAppError from 'ui/shared/AppError/isCustomAppError';
import ChartIntervalSelect from 'ui/shared/chart/ChartIntervalSelect'; import ChartIntervalSelect from 'ui/shared/chart/ChartIntervalSelect';
import ChartMenu from 'ui/shared/chart/ChartMenu'; import ChartMenu from 'ui/shared/chart/ChartMenu';
import ChartResolutionSelect from 'ui/shared/chart/ChartResolutionSelect';
import ChartWidgetContent from 'ui/shared/chart/ChartWidgetContent'; import ChartWidgetContent from 'ui/shared/chart/ChartWidgetContent';
import useChartQuery from 'ui/shared/chart/useChartQuery'; import useChartQuery from 'ui/shared/chart/useChartQuery';
import useZoom from 'ui/shared/chart/useZoom'; import useZoom from 'ui/shared/chart/useZoom';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Select from 'ui/shared/select/Select';
import { STATS_RESOLUTIONS } from 'ui/stats/constants';
const DEFAULT_RESOLUTION = Resolution.DAY; const DEFAULT_RESOLUTION = Resolution.DAY;
...@@ -68,8 +69,9 @@ const Chart = () => { ...@@ -68,8 +69,9 @@ const Chart = () => {
const id = getQueryParamString(router.query.id); const id = getQueryParamString(router.query.id);
const intervalFromQuery = getIntervalFromQuery(router); const intervalFromQuery = getIntervalFromQuery(router);
const resolutionFromQuery = getResolutionFromQuery(router); const resolutionFromQuery = getResolutionFromQuery(router);
const defaultResolution = resolutionFromQuery || DEFAULT_RESOLUTION;
const [ intervalState, setIntervalState ] = React.useState<StatsIntervalIds | undefined>(intervalFromQuery); const [ intervalState, setIntervalState ] = React.useState<StatsIntervalIds | undefined>(intervalFromQuery);
const [ resolution, setResolution ] = React.useState<Resolution>(resolutionFromQuery || DEFAULT_RESOLUTION); const [ resolution, setResolution ] = React.useState<Resolution>(defaultResolution);
const { zoomRange, handleZoom, handleZoomReset } = useZoom(); const { zoomRange, handleZoom, handleZoomReset } = useZoom();
const interval = intervalState || getIntervalByResolution(resolution); const interval = intervalState || getIntervalByResolution(resolution);
...@@ -163,6 +165,13 @@ const Chart = () => { ...@@ -163,6 +165,13 @@ const Chart = () => {
</Button> </Button>
); );
const resolutionOptions = React.useMemo(() => {
const resolutions = lineQuery.data?.info?.resolutions || [];
return STATS_RESOLUTIONS
.filter((resolution) => resolutions.includes(resolution.id))
.map((resolution) => ({ value: resolution.id, label: resolution.title }));
}, [ lineQuery.data?.info?.resolutions ]);
return ( return (
<> <>
<PageTitle <PageTitle
...@@ -174,7 +183,7 @@ const Chart = () => { ...@@ -174,7 +183,7 @@ const Chart = () => {
withTextAd withTextAd
/> />
<Flex alignItems="center" justifyContent="space-between"> <Flex alignItems="center" justifyContent="space-between">
<Flex alignItems="center" gap={{ base: 3, lg: 6 }} maxW="100%" overflow="hidden"> <Flex alignItems="center" gap={{ base: 3, lg: 6 }} maxW="100%">
<Flex alignItems="center" gap={ 3 }> <Flex alignItems="center" gap={ 3 }>
{ !isMobile && <Text>Period</Text> } { !isMobile && <Text>Period</Text> }
<ChartIntervalSelect interval={ interval } onIntervalChange={ onIntervalChange }/> <ChartIntervalSelect interval={ interval } onIntervalChange={ onIntervalChange }/>
...@@ -187,11 +196,13 @@ const Chart = () => { ...@@ -187,11 +196,13 @@ const Chart = () => {
<Skeleton isLoaded={ !isInfoLoading }> <Skeleton isLoaded={ !isInfoLoading }>
{ isMobile ? 'Res.' : 'Resolution' } { isMobile ? 'Res.' : 'Resolution' }
</Skeleton> </Skeleton>
<ChartResolutionSelect <Select
resolution={ resolution } options={ resolutionOptions }
onResolutionChange={ onResolutionChange } defaultValue={ defaultResolution }
resolutions={ lineQuery.data?.info?.resolutions || [] } onChange={ onResolutionChange }
isLoading={ isInfoLoading } isLoading={ isInfoLoading }
w={{ base: 'fit-content', lg: '160px' }}
fontWeight={ 600 }
/> />
</Flex> </Flex>
) } ) }
......
import { Select, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import hexToUtf8 from 'lib/hexToUtf8'; import hexToUtf8 from 'lib/hexToUtf8';
import RawDataSnippet from 'ui/shared/RawDataSnippet'; import RawDataSnippet from 'ui/shared/RawDataSnippet';
import Select from 'ui/shared/select/Select';
export type DataType = 'Hex' | 'UTF-8'; const OPTIONS = [
const OPTIONS: Array<DataType> = [ 'Hex', 'UTF-8' ]; { label: 'Hex', value: 'Hex' as const },
{ label: 'UTF-8', value: 'UTF-8' as const },
];
export type DataType = (typeof OPTIONS)[number]['value'];
interface Props { interface Props {
hex: string; hex: string;
...@@ -18,17 +22,17 @@ interface Props { ...@@ -18,17 +22,17 @@ interface Props {
const RawInputData = ({ hex, rightSlot: rightSlotProp, defaultDataType = 'Hex', isLoading, minHeight }: Props) => { const RawInputData = ({ hex, rightSlot: rightSlotProp, defaultDataType = 'Hex', isLoading, minHeight }: Props) => {
const [ selectedDataType, setSelectedDataType ] = React.useState<DataType>(defaultDataType); const [ selectedDataType, setSelectedDataType ] = React.useState<DataType>(defaultDataType);
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedDataType(event.target.value as DataType);
}, []);
const rightSlot = ( const rightSlot = (
<> <>
<Skeleton isLoaded={ !isLoading } borderRadius="base" w="auto" mr="auto"> <Select
<Select size="xs" borderRadius="base" value={ selectedDataType } onChange={ handleSelectChange }> options={ OPTIONS }
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) } name="data-type"
</Select> defaultValue={ defaultDataType }
</Skeleton> onChange={ setSelectedDataType }
isLoading={ isLoading }
w="90px"
mr="auto"
/>
{ rightSlotProp } { rightSlotProp }
</> </>
); );
......
...@@ -3,15 +3,16 @@ import { Skeleton } from '@chakra-ui/react'; ...@@ -3,15 +3,16 @@ import { Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { StatsInterval, StatsIntervalIds } from 'types/client/stats'; import type { StatsInterval, StatsIntervalIds } from 'types/client/stats';
import type { SelectOption } from 'ui/shared/select/types';
import Select from 'ui/shared/select/Select';
import TagGroupSelect from 'ui/shared/tagGroupSelect/TagGroupSelect'; import TagGroupSelect from 'ui/shared/tagGroupSelect/TagGroupSelect';
import { STATS_INTERVALS } from 'ui/stats/constants'; import { STATS_INTERVALS } from 'ui/stats/constants';
import StatsDropdownMenu from 'ui/stats/StatsDropdownMenu';
const intervalList = Object.keys(STATS_INTERVALS).map((id: string) => ({ const intervalList = Object.keys(STATS_INTERVALS).map((id: string) => ({
id: id, value: id,
title: STATS_INTERVALS[id as StatsIntervalIds].title, label: STATS_INTERVALS[id as StatsIntervalIds].title,
})) as Array<StatsInterval>; })) as Array<SelectOption>;
const intervalListShort = Object.keys(STATS_INTERVALS).map((id: string) => ({ const intervalListShort = Object.keys(STATS_INTERVALS).map((id: string) => ({
id: id, id: id,
...@@ -31,13 +32,16 @@ const ChartIntervalSelect = ({ interval, onIntervalChange, isLoading, selectTagS ...@@ -31,13 +32,16 @@ const ChartIntervalSelect = ({ interval, onIntervalChange, isLoading, selectTagS
<Skeleton display={{ base: 'none', lg: 'flex' }} borderRadius="base" isLoaded={ !isLoading }> <Skeleton display={{ base: 'none', lg: 'flex' }} borderRadius="base" isLoaded={ !isLoading }>
<TagGroupSelect<StatsIntervalIds> items={ intervalListShort } onChange={ onIntervalChange } value={ interval } tagSize={ selectTagSize }/> <TagGroupSelect<StatsIntervalIds> items={ intervalListShort } onChange={ onIntervalChange } value={ interval } tagSize={ selectTagSize }/>
</Skeleton> </Skeleton>
<Skeleton display={{ base: 'block', lg: 'none' }} borderRadius="base" isLoaded={ !isLoading }> <Select
<StatsDropdownMenu options={ intervalList }
items={ intervalList } defaultValue={ interval }
selectedId={ interval } onChange={ onIntervalChange }
onSelect={ onIntervalChange } isLoading={ isLoading }
/> w={{ base: '100%', lg: '136px' }}
</Skeleton> display={{ base: 'flex', lg: 'none' }}
flexShrink={ 0 }
fontWeight={ 600 }
/>
</> </>
); );
}; };
......
import { Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { Resolution } from '@blockscout/stats-types';
import { STATS_RESOLUTIONS } from 'ui/stats/constants';
import StatsDropdownMenu from 'ui/stats/StatsDropdownMenu';
type Props = {
resolution: Resolution;
resolutions: Array<string>;
onResolutionChange: (resolution: Resolution) => void;
isLoading?: boolean;
};
const ChartResolutionSelect = ({ resolution, resolutions, onResolutionChange, isLoading }: Props) => {
return (
<Skeleton borderRadius="base" isLoaded={ !isLoading } w={{ base: 'auto', lg: '160px' }}>
<StatsDropdownMenu
items={ STATS_RESOLUTIONS.filter(r => resolutions.includes(r.id)) }
selectedId={ resolution }
onSelect={ onResolutionChange }
/>
</Skeleton>
);
};
export default React.memo(ChartResolutionSelect);
import {
PopoverTrigger,
PopoverContent,
PopoverBody,
useDisclosure,
useRadio,
Box,
useRadioGroup,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import Popover from 'ui/shared/chakra/Popover'; import type { SelectOption } from 'ui/shared/select/types';
import FilterButton from 'ui/shared/filters/FilterButton';
import IconSvg from 'ui/shared/IconSvg';
// OPTION
export interface TOption {
value: string;
label: string;
}
type OptionProps = ReturnType<ReturnType<typeof useRadioGroup>['getRadioProps']>;
const Option = (props: OptionProps) => {
const { getInputProps, getRadioProps } = useRadio(props);
const input = getInputProps();
const checkbox = getRadioProps();
const bgColorHover = useColorModeValue('blue.50', 'whiteAlpha.100');
return (
<Box
as="label"
px={ 4 }
py={ 2 }
cursor="pointer"
display="flex"
columnGap={ 3 }
alignItems="center"
_hover={{
bgColor: bgColorHover,
}}
>
<input { ...input }/>
<Box { ...checkbox }>
{ props.children }
</Box>
{ props.isChecked && <IconSvg name="check" boxSize={ 4 }/> }
</Box>
);
};
// FILTER
import FilterButton from 'ui/shared/filters/FilterButton';
import Select from 'ui/shared/select/Select';
interface Props { interface Props {
name: string; name: string;
options: Array<TOption>; options: Array<SelectOption>;
hasActiveFilter: boolean; hasActiveFilter: boolean;
defaultValue?: string; defaultValue?: string;
isLoading?: boolean; isLoading?: boolean;
...@@ -63,39 +14,22 @@ interface Props { ...@@ -63,39 +14,22 @@ interface Props {
} }
const PopoverFilterRadio = ({ name, hasActiveFilter, options, isLoading, onChange, defaultValue }: Props) => { const PopoverFilterRadio = ({ name, hasActiveFilter, options, isLoading, onChange, defaultValue }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const { getRootProps, getRadioProps } = useRadioGroup({
name,
defaultValue,
onChange,
});
const root = getRootProps();
return ( return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy> <Select
<PopoverTrigger> options={ options }
name={ name }
defaultValue={ defaultValue }
onChange={ onChange }
>
{ ({ isOpen, onToggle }) => (
<FilterButton <FilterButton
isActive={ isOpen } isActive={ isOpen }
onClick={ onToggle } onClick={ onToggle }
appliedFiltersNum={ hasActiveFilter ? 1 : 0 } appliedFiltersNum={ hasActiveFilter ? 1 : 0 }
isLoading={ isLoading } isLoading={ isLoading }
/> />
</PopoverTrigger> ) }
<PopoverContent w="fit-content" minW="150px"> </Select>
<PopoverBody { ...root } py={ 2 } px={ 0 } display="flex" flexDir="column">
{ options.map((option) => {
const radio = getRadioProps({ value: option.value });
return (
<Option key={ option.value } { ...radio }>
{ option.label }
</Option>
);
}) }
</PopoverBody>
</PopoverContent>
</Popover>
); );
}; };
......
import { PopoverTrigger, chakra, useDisclosure, useRadioGroup } from '@chakra-ui/react';
import React from 'react';
import type { SelectOption } from './types';
import Popover from 'ui/shared/chakra/Popover';
import SelectButton from './SelectButton';
import SelectContent from './SelectContent';
interface InjectedProps<Value extends string> {
isOpen: boolean;
onToggle: () => void;
value: Value;
}
export interface Props<Value extends string> {
className?: string;
isLoading?: boolean;
options: Array<SelectOption<Value>>;
name: string;
defaultValue?: Value;
onChange: (value: Value) => void;
children?: (props: InjectedProps<Value>) => React.ReactNode;
}
const Select = <Value extends string>({ className, isLoading, options, name, defaultValue, onChange, children }: Props<Value>) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const handleChange = React.useCallback((value: Value) => {
onChange(value);
onClose();
}, [ onChange, onClose ]);
const { value, getRootProps, getRadioProps, setValue } = useRadioGroup({
name,
defaultValue,
onChange: handleChange,
});
React.useEffect(() => {
if (defaultValue) {
setValue(defaultValue);
}
}, [ defaultValue, setValue ]);
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
{ children?.({ isOpen, onToggle, value: value as Value }) || (
<SelectButton
className={ className }
onClick={ onToggle }
isOpen={ isOpen }
isLoading={ isLoading }
label={ options.find((option) => option.value === value)?.label || String(value) }
/>
) }
</PopoverTrigger>
<SelectContent options={ options } getRootProps={ getRootProps } getRadioProps={ getRadioProps } value={ value }/>
</Popover>
);
};
export default React.memo(chakra(Select));
import { Box, Button, Skeleton } from '@chakra-ui/react';
import React from 'react';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
className?: string;
onClick: () => void;
isOpen: boolean;
isLoading?: boolean;
label: string;
}
const SelectButton = ({ className, onClick, isOpen, isLoading, label }: Props, ref: React.Ref<HTMLButtonElement>) => {
if (isLoading) {
return <Skeleton className={ className } h={ 8 } borderRadius="base" flexShrink={ 0 }/>;
}
return (
<Button
ref={ ref }
className={ className }
variant="outline"
size="sm"
colorScheme="gray"
fontWeight="500"
lineHeight={ 5 }
display="flex"
alignItems="center"
justifyContent="space-between"
columnGap={ 1 }
onClick={ onClick }
isActive={ isOpen }
pl={ 2 }
pr={ 1 }
py={ 1 }
flexShrink={ 0 }
>
<Box maxW="calc(100% - 20px)" overflow="hidden" textOverflow="ellipsis">{ label }</Box>
<IconSvg name="arrows/east-mini" boxSize={ 5 } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } flexShrink={ 0 }/>
</Button>
);
};
export default React.forwardRef(SelectButton);
import type { useRadioGroup } from '@chakra-ui/react';
import { PopoverBody, PopoverContent } from '@chakra-ui/react';
import React from 'react';
import type { SelectOption as TSelectOption } from './types';
import SelectOption from './SelectOption';
interface Props {
options: Array<TSelectOption>;
getRootProps: ReturnType<typeof useRadioGroup>['getRootProps'];
getRadioProps: ReturnType<typeof useRadioGroup>['getRadioProps'];
value: string | number;
}
const SelectContent = ({ options, getRootProps, getRadioProps, value }: Props) => {
const root = getRootProps();
return (
<PopoverContent w="fit-content" minW="150px">
<PopoverBody { ...root } py={ 2 } px={ 0 } display="flex" flexDir="column">
{ options.map((option) => {
const radio = getRadioProps({ value: option.value });
return (
<SelectOption key={ option.value } { ...radio } isChecked={ radio.isChecked || (!option.value && !value) }>
{ option.label }
</SelectOption>
);
}) }
</PopoverBody>
</PopoverContent>
);
};
export default React.memo(SelectContent);
import { import type { UseRadioProps } from '@chakra-ui/react';
useRadio, import { Box, useRadio, useColorModeValue } from '@chakra-ui/react';
Box,
useColorModeValue,
} from '@chakra-ui/react';
import type { useRadioGroup } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from '../IconSvg';
export interface TOption<Sort extends string> { interface Props extends UseRadioProps {
id: Sort | undefined; children: React.ReactNode;
title: string;
} }
type OptionProps = ReturnType<ReturnType<typeof useRadioGroup>['getRadioProps']>; const SelectOption = (props: Props) => {
const Option = (props: OptionProps) => {
const { getInputProps, getRadioProps } = useRadio(props); const { getInputProps, getRadioProps } = useRadio(props);
const input = getInputProps(); const input = getInputProps();
...@@ -35,13 +28,13 @@ const Option = (props: OptionProps) => { ...@@ -35,13 +28,13 @@ const Option = (props: OptionProps) => {
bgColor: bgColorHover, bgColor: bgColorHover,
}} }}
> >
{ props.isChecked ? <IconSvg name="check" boxSize={ 5 }/> : <Box boxSize={ 5 }/> }
<input { ...input }/> <input { ...input }/>
<Box { ...checkbox }> <Box { ...checkbox }>
{ props.children } { props.children }
</Box> </Box>
{ props.isChecked && <IconSvg name="check" boxSize={ 4 } color="blue.600"/> }
</Box> </Box>
); );
}; };
export default Option; export default React.memo(SelectOption);
export interface SelectOption<Value extends string = string> {
value: Value | undefined;
label: string;
}
import { import { chakra } from '@chakra-ui/react';
PopoverTrigger,
PopoverContent,
PopoverBody,
useDisclosure,
useRadioGroup,
chakra,
} from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SelectOption } from 'ui/shared/select/types';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import Popover from 'ui/shared/chakra/Popover'; import Select, { type Props as SelectProps } from 'ui/shared/select/Select';
import SortButtonDesktop from './ButtonDesktop'; import SortButtonDesktop from './ButtonDesktop';
import SortButtonMobile from './ButtonMobile'; import SortButtonMobile from './ButtonMobile';
import Option from './Option';
import type { TOption } from './Option';
interface Props<Sort extends string> { type Props<Value extends string> = Omit<SelectProps<Value>, 'children'>;
name: string;
options: Array<TOption<Sort>>;
defaultValue?: Sort;
isLoading?: boolean;
onChange: (value: Sort | undefined) => void;
}
const Sort = <Sort extends string>({ name, options, isLoading, onChange, defaultValue }: Props<Sort>) => { const Sort = <Sort extends string>({ name, options, isLoading, onChange, defaultValue }: Props<Sort>) => {
const isMobile = useIsMobile(false); const isMobile = useIsMobile(false);
const { isOpen, onToggle, onClose } = useDisclosure();
const handleChange = (value: Sort) => {
onChange(value);
onClose();
};
const { value, getRootProps, getRadioProps } = useRadioGroup({
name,
defaultValue,
onChange: handleChange,
});
const root = getRootProps();
return ( return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy> <Select
<PopoverTrigger> options={ options }
{ isMobile ? ( name={ name }
<SortButtonMobile isActive={ isOpen || Boolean(value) } onClick={ onToggle } isLoading={ isLoading }/> defaultValue={ defaultValue }
) : ( onChange={ onChange }
<SortButtonDesktop isActive={ isOpen } isLoading={ isLoading } onClick={ onToggle }> >
{ options.find((option: TOption<Sort>) => option.id === value || (!option.id && !value))?.title } { ({ isOpen, onToggle, value }) => {
</SortButtonDesktop> return (
) } isMobile ? (
</PopoverTrigger> <SortButtonMobile isActive={ isOpen || Boolean(value) } onClick={ onToggle } isLoading={ isLoading }/>
<PopoverContent w="fit-content" minW="165px"> ) : (
<PopoverBody { ...root } py={ 2 } px={ 0 } display="flex" flexDir="column"> <SortButtonDesktop isActive={ isOpen } isLoading={ isLoading } onClick={ onToggle }>
{ options.map((option, index) => { { options.find((option: SelectOption<Sort>) => option.value === value || (!option.value && !value))?.label }
const radio = getRadioProps({ value: option.id }); </SortButtonDesktop>
return ( )
<Option key={ index } { ...radio } isChecked={ radio.isChecked || (!option.id && !value) }> );
{ option.title } } }
</Option> </Select>
);
}) }
</PopoverBody>
</PopoverContent>
</Popover>
); );
}; };
......
import type { Query } from 'nextjs-routes'; import type { SelectOption } from 'ui/shared/select/types';
import type { TOption } from 'ui/shared/sort/Option'; import type { Query } from 'nextjs-routes';
export default function getSortValueFromQuery<SortValue extends string>(query: Query, sortOptions: Array<TOption<SortValue>>) { export default function getSortValueFromQuery<SortValue extends string>(query: Query, sortOptions: Array<SelectOption<SortValue>>) {
if (!query.sort || !query.order) { if (!query.sort || !query.order) {
return undefined; return undefined;
} }
const str = query.sort + '-' + query.order; const str = query.sort + '-' + query.order;
if (sortOptions.map(option => option.id).includes(str as SortValue)) { if (sortOptions.map(option => option.value).includes(str as SortValue)) {
return str as SortValue; return str as SortValue;
} }
} }
import { Box, Button, MenuButton, MenuItemOption, MenuList, MenuOptionGroup, chakra } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import Menu from 'ui/shared/chakra/Menu';
import IconSvg from 'ui/shared/IconSvg';
type Props<T extends string> = {
items: ReadonlyArray<{ id: T; title: string }>;
selectedId: T;
onSelect: (id: T) => void;
};
export function StatsDropdownMenu<T extends string>({ items, selectedId, onSelect }: Props<T>) {
const selectedCategory = items.find(category => category.id === selectedId);
const handleSelection = useCallback((id: string | Array<string>) => {
const selectedId = Array.isArray(id) ? id[0] : id;
onSelect(selectedId as T);
}, [ onSelect ]);
return (
<Menu
>
<MenuButton
as={ Button }
size="sm"
variant="outline"
colorScheme="gray"
w="100%"
>
<Box
as="span"
display="flex"
alignItems="center"
>
<chakra.span
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
>
{ selectedCategory?.title }
</chakra.span>
<IconSvg transform="rotate(-90deg)" ml="auto" name="arrows/east-mini" w={ 5 } h={ 5 }/>
</Box>
</MenuButton>
<MenuList zIndex={ 3 }>
<MenuOptionGroup
value={ selectedId }
type="radio"
onChange={ handleSelection }
>
{ items.map((item) => (
<MenuItemOption
key={ item.id }
value={ item.id }
>
{ item.title }
</MenuItemOption>
)) }
</MenuOptionGroup>
</MenuList>
</Menu>
);
}
export default StatsDropdownMenu;
import { Grid, GridItem, Skeleton } from '@chakra-ui/react'; import { Grid, GridItem } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type * as stats from '@blockscout/stats-types'; import type * as stats from '@blockscout/stats-types';
...@@ -6,8 +6,7 @@ import type { StatsIntervalIds } from 'types/client/stats'; ...@@ -6,8 +6,7 @@ import type { StatsIntervalIds } from 'types/client/stats';
import ChartIntervalSelect from 'ui/shared/chart/ChartIntervalSelect'; import ChartIntervalSelect from 'ui/shared/chart/ChartIntervalSelect';
import FilterInput from 'ui/shared/filters/FilterInput'; import FilterInput from 'ui/shared/filters/FilterInput';
import Select from 'ui/shared/select/Select';
import StatsDropdownMenu from './StatsDropdownMenu';
type Props = { type Props = {
sections?: Array<stats.LineChartSection>; sections?: Array<stats.LineChartSection>;
...@@ -30,10 +29,13 @@ const StatsFilters = ({ ...@@ -30,10 +29,13 @@ const StatsFilters = ({
isLoading, isLoading,
initialFilterValue, initialFilterValue,
}: Props) => { }: Props) => {
const sectionsList = [ {
id: 'all', const options = React.useMemo(() => {
title: 'All stats', return [
}, ... (sections || []) ]; { value: 'all', label: 'All stats' },
...(sections || []).map((section) => ({ value: section.id, label: section.title })),
];
}, [ sections ]);
return ( return (
<Grid <Grid
...@@ -50,13 +52,14 @@ const StatsFilters = ({ ...@@ -50,13 +52,14 @@ const StatsFilters = ({
w={{ base: '100%', lg: 'auto' }} w={{ base: '100%', lg: 'auto' }}
area="section" area="section"
> >
{ isLoading ? <Skeleton w={{ base: '100%', lg: '103px' }} h="32px" borderRadius="base"/> : ( <Select
<StatsDropdownMenu options={ options }
items={ sectionsList } defaultValue={ currentSection }
selectedId={ currentSection } onChange={ onSectionChange }
onSelect={ onSectionChange } isLoading={ isLoading }
/> w={{ base: '100%', lg: '136px' }}
) } fontWeight={ 600 }
/>
</GridItem> </GridItem>
<GridItem <GridItem
......
...@@ -23,6 +23,7 @@ test('base view +@mobile', async({ render }) => { ...@@ -23,6 +23,7 @@ test('base view +@mobile', async({ render }) => {
test('raw view', async({ render }) => { test('raw view', async({ render }) => {
const component = await render(<TokenInstanceMetadata data={ tokenInstanceMock.withRichMetadata.metadata }/>); const component = await render(<TokenInstanceMetadata data={ tokenInstanceMock.withRichMetadata.metadata }/>);
await component.locator('select').selectOption('JSON'); await component.getByRole('button', { name: 'Table' }).click();
await component.getByText('JSON').click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
import { Alert, Box, Flex, Select, chakra } from '@chakra-ui/react'; import { Alert, Box, Flex, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInstance } from 'types/api/token'; import type { TokenInstance } from 'types/api/token';
...@@ -6,11 +6,17 @@ import type { TokenInstance } from 'types/api/token'; ...@@ -6,11 +6,17 @@ import type { TokenInstance } from 'types/api/token';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import RawDataSnippet from 'ui/shared/RawDataSnippet'; import RawDataSnippet from 'ui/shared/RawDataSnippet';
import Select from 'ui/shared/select/Select';
import { useMetadataUpdateContext } from './contexts/metadataUpdate'; import { useMetadataUpdateContext } from './contexts/metadataUpdate';
import MetadataAccordion from './metadata/MetadataAccordion'; import MetadataAccordion from './metadata/MetadataAccordion';
type Format = 'JSON' | 'Table'; const OPTIONS = [
{ label: 'Table', value: 'Table' as const },
{ label: 'JSON', value: 'JSON' as const },
];
type Format = (typeof OPTIONS)[number]['value'];
interface Props { interface Props {
data: TokenInstance['metadata'] | undefined; data: TokenInstance['metadata'] | undefined;
...@@ -22,10 +28,6 @@ const TokenInstanceMetadata = ({ data, isPlaceholderData }: Props) => { ...@@ -22,10 +28,6 @@ const TokenInstanceMetadata = ({ data, isPlaceholderData }: Props) => {
const { status: refetchStatus } = useMetadataUpdateContext() || {}; const { status: refetchStatus } = useMetadataUpdateContext() || {};
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setFormat(event.target.value as Format);
}, []);
if (isPlaceholderData || refetchStatus === 'WAITING_FOR_RESPONSE') { if (isPlaceholderData || refetchStatus === 'WAITING_FOR_RESPONSE') {
return <ContentLoader/>; return <ContentLoader/>;
} }
...@@ -48,10 +50,14 @@ const TokenInstanceMetadata = ({ data, isPlaceholderData }: Props) => { ...@@ -48,10 +50,14 @@ const TokenInstanceMetadata = ({ data, isPlaceholderData }: Props) => {
) } ) }
<Flex alignItems="center" mb={ 6 }> <Flex alignItems="center" mb={ 6 }>
<chakra.span fontWeight={ 500 }>Metadata</chakra.span> <chakra.span fontWeight={ 500 }>Metadata</chakra.span>
<Select size="xs" borderRadius="base" value={ format } onChange={ handleSelectChange } w="auto" ml={ 5 }> <Select
<option value="Table">Table</option> options={ OPTIONS }
<option value="JSON">JSON</option> name="metadata-format"
</Select> defaultValue="Table"
onChange={ setFormat }
w="85px"
ml={ 5 }
/>
{ format === 'JSON' && <CopyToClipboard text={ JSON.stringify(data) } ml="auto"/> } { format === 'JSON' && <CopyToClipboard text={ JSON.stringify(data) } ml="auto"/> }
</Flex> </Flex>
{ content } { content }
......
import type { TokenType } from 'types/api/token'; import type { TokenType } from 'types/api/token';
import type { TokensSortingValue } from 'types/api/tokens'; import type { TokensSortingValue } from 'types/api/tokens';
import type { SelectOption } from 'ui/shared/select/types';
import config from 'configs/app'; import config from 'configs/app';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import { TOKEN_TYPE_IDS } from 'lib/token/tokenTypes'; import { TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import type { TOption } from 'ui/shared/sort/Option';
export const SORT_OPTIONS: Array<TOption<TokensSortingValue>> = [ export const SORT_OPTIONS: Array<SelectOption<TokensSortingValue>> = [
{ title: 'Default', id: undefined }, { label: 'Default', value: undefined },
{ title: 'Price ascending', id: 'fiat_value-asc' }, { label: 'Price ascending', value: 'fiat_value-asc' },
{ title: 'Price descending', id: 'fiat_value-desc' }, { label: 'Price descending', value: 'fiat_value-desc' },
{ title: 'Holders ascending', id: 'holder_count-asc' }, { label: 'Holders ascending', value: 'holder_count-asc' },
{ title: 'Holders descending', id: 'holder_count-desc' }, { label: 'Holders descending', value: 'holder_count-desc' },
{ title: 'On-chain market cap ascending', id: 'circulating_market_cap-asc' }, { label: 'On-chain market cap ascending', value: 'circulating_market_cap-asc' },
{ title: 'On-chain market cap descending', id: 'circulating_market_cap-desc' }, { label: 'On-chain market cap descending', value: 'circulating_market_cap-desc' },
]; ];
export const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPE_IDS); export const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPE_IDS);
......
...@@ -2,19 +2,19 @@ import type { UseQueryResult } from '@tanstack/react-query'; ...@@ -2,19 +2,19 @@ import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { TransactionsSortingValue, TxsResponse } from 'types/api/transaction'; import type { TransactionsSortingValue, TxsResponse } from 'types/api/transaction';
import type { SelectOption } from 'ui/shared/select/types';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import type { TOption } from 'ui/shared/sort/Option';
import sortTxs from './sortTxs'; import sortTxs from './sortTxs';
export const SORT_OPTIONS: Array<TOption<TransactionsSortingValue>> = [ export const SORT_OPTIONS: Array<SelectOption<TransactionsSortingValue>> = [
{ title: 'Default', id: undefined }, { label: 'Default', value: undefined },
{ title: 'Value ascending', id: 'value-asc' }, { label: 'Value ascending', value: 'value-asc' },
{ title: 'Value descending', id: 'value-desc' }, { label: 'Value descending', value: 'value-desc' },
{ title: 'Fee ascending', id: 'fee-asc' }, { label: 'Fee ascending', value: 'fee-asc' },
{ title: 'Fee descending', id: 'fee-desc' }, { label: 'Fee descending', value: 'fee-desc' },
]; ];
type SortingValue = TransactionsSortingValue | undefined; type SortingValue = TransactionsSortingValue | undefined;
......
...@@ -2,13 +2,12 @@ import type { ...@@ -2,13 +2,12 @@ import type {
ValidatorsBlackfortSortingValue, ValidatorsBlackfortSortingValue,
ValidatorsBlackfortSortingField, ValidatorsBlackfortSortingField,
} from 'types/api/validators'; } from 'types/api/validators';
import type { SelectOption } from 'ui/shared/select/types';
import type { TOption } from 'ui/shared/sort/Option'; export const VALIDATORS_BLACKFORT_SORT_OPTIONS: Array<SelectOption<ValidatorsBlackfortSortingValue>> = [
{ label: 'Default', value: undefined },
export const VALIDATORS_BLACKFORT_SORT_OPTIONS: Array<TOption<ValidatorsBlackfortSortingValue>> = [ { label: 'Address descending', value: 'address_hash-desc' },
{ title: 'Default', id: undefined }, { label: 'Address ascending', value: 'address_hash-asc' },
{ title: 'Address descending', id: 'address_hash-desc' },
{ title: 'Address ascending', id: 'address_hash-asc' },
]; ];
export const VALIDATORS_BLACKFORT_SORT_SEQUENCE: Record<ValidatorsBlackfortSortingField, Array<ValidatorsBlackfortSortingValue | undefined>> = { export const VALIDATORS_BLACKFORT_SORT_SEQUENCE: Record<ValidatorsBlackfortSortingField, Array<ValidatorsBlackfortSortingValue | undefined>> = {
......
...@@ -2,15 +2,14 @@ import type { ...@@ -2,15 +2,14 @@ import type {
ValidatorsStabilitySortingValue, ValidatorsStabilitySortingValue,
ValidatorsStabilitySortingField, ValidatorsStabilitySortingField,
} from 'types/api/validators'; } from 'types/api/validators';
import type { SelectOption } from 'ui/shared/select/types';
import type { TOption } from 'ui/shared/sort/Option'; export const VALIDATORS_STABILITY_SORT_OPTIONS: Array<SelectOption<ValidatorsStabilitySortingValue>> = [
{ label: 'Default', value: undefined },
export const VALIDATORS_STABILITY_SORT_OPTIONS: Array<TOption<ValidatorsStabilitySortingValue>> = [ { label: 'Status descending', value: 'state-desc' },
{ title: 'Default', id: undefined }, { label: 'Status ascending', value: 'state-asc' },
{ title: 'Status descending', id: 'state-desc' }, { label: 'Blocks validated descending', value: 'blocks_validated-desc' },
{ title: 'Status ascending', id: 'state-asc' }, { label: 'Blocks validated ascending', value: 'blocks_validated-asc' },
{ title: 'Blocks validated descending', id: 'blocks_validated-desc' },
{ title: 'Blocks validated ascending', id: 'blocks_validated-asc' },
]; ];
export const VALIDATORS_STABILITY_SORT_SEQUENCE: Record<ValidatorsStabilitySortingField, Array<ValidatorsStabilitySortingValue | undefined>> = { export const VALIDATORS_STABILITY_SORT_SEQUENCE: Record<ValidatorsStabilitySortingField, Array<ValidatorsStabilitySortingValue | undefined>> = {
......
import type { VerifiedContractsSortingValue, VerifiedContractsSortingField } from 'types/api/verifiedContracts'; import type { VerifiedContractsSortingValue, VerifiedContractsSortingField } from 'types/api/verifiedContracts';
import type { SelectOption } from 'ui/shared/select/types';
import type { TOption } from 'ui/shared/sort/Option'; export const SORT_OPTIONS: Array<SelectOption<VerifiedContractsSortingValue>> = [
{ label: 'Default', value: undefined },
export const SORT_OPTIONS: Array<TOption<VerifiedContractsSortingValue>> = [ { label: 'Balance descending', value: 'balance-desc' },
{ title: 'Default', id: undefined }, { label: 'Balance ascending', value: 'balance-asc' },
{ title: 'Balance descending', id: 'balance-desc' }, { label: 'Txs count descending', value: 'txs_count-desc' },
{ title: 'Balance ascending', id: 'balance-asc' }, { label: 'Txs count ascending', value: 'txs_count-asc' },
{ title: 'Txs count descending', id: 'txs_count-desc' },
{ title: 'Txs count ascending', id: 'txs_count-asc' },
]; ];
export const SORT_SEQUENCE: Record<VerifiedContractsSortingField, Array<VerifiedContractsSortingValue | undefined>> = { export const SORT_SEQUENCE: Record<VerifiedContractsSortingField, Array<VerifiedContractsSortingValue | undefined>> = {
......
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