Redesign search chain filters

pull/114/head
J M Rossy 2 months ago
parent 1f2434b503
commit 5bf09c83df
  1. 1
      package.json
  2. 187
      src/components/search/SearchFilterBar.tsx
  3. 2
      src/features/chains/queries/useScrapedChains.ts
  4. 4
      src/features/chains/utils.ts
  5. 15
      src/store.ts
  6. 4
      src/styles/Color.ts
  7. 3
      yarn.lock

@ -12,6 +12,7 @@
"@tanstack/react-query": "^5.35.5", "@tanstack/react-query": "^5.35.5",
"bignumber.js": "^9.1.2", "bignumber.js": "^9.1.2",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"clsx": "^2.1.1",
"ethers": "^5.7.2", "ethers": "^5.7.2",
"formik": "^2.2.9", "formik": "^2.2.9",
"graphql": "^16.6.0", "graphql": "^16.6.0",

@ -1,12 +1,20 @@
import Image from 'next/image'; import clsx from 'clsx';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
import { ChainMetadata } from '@hyperlane-xyz/sdk'; import { ChainMetadata } from '@hyperlane-xyz/sdk';
import { ChainSearchMenu, Modal, Popover } from '@hyperlane-xyz/widgets'; import { trimToLength } from '@hyperlane-xyz/utils';
import {
ChainSearchMenu,
GearIcon,
IconButton,
Modal,
Popover,
XIcon,
} from '@hyperlane-xyz/widgets';
import { useScrapedEvmChains } from '../../features/chains/queries/useScrapedChains'; import { useScrapedEvmChains } from '../../features/chains/queries/useScrapedChains';
import GearIcon from '../../images/icons/gear.svg'; import { getChainDisplayName } from '../../features/chains/utils';
import { useMultiProvider } from '../../store'; import { useMultiProvider } from '../../store';
import { Color } from '../../styles/Color'; import { Color } from '../../styles/Color';
import { SolidButton } from '../buttons/SolidButton'; import { SolidButton } from '../buttons/SolidButton';
@ -37,19 +45,11 @@ export function SearchFilterBar({
}: Props) { }: Props) {
return ( return (
<div className="flex items-center space-x-2 md:space-x-4"> <div className="flex items-center space-x-2 md:space-x-4">
<ChainSelector <ChainSelector text="Origin" value={originChain} onChangeValue={onChangeOrigin} />
text="Origin"
header="Origin Chains"
value={originChain}
onChangeValue={onChangeOrigin}
position="-right-32"
/>
<ChainSelector <ChainSelector
text="Destination" text="Destination"
header="Destination Chains"
value={destinationChain} value={destinationChain}
onChangeValue={onChangeDestination} onChangeValue={onChangeDestination}
position="-right-28"
/> />
<DatetimeSelector <DatetimeSelector
startValue={startTimestamp} startValue={startTimestamp}
@ -57,9 +57,9 @@ export function SearchFilterBar({
endValue={endTimestamp} endValue={endTimestamp}
onChangeEndValue={onChangeEndTimestamp} onChangeEndValue={onChangeEndTimestamp}
/> />
<Link href="/settings" title="View explorer settings"> <Link href="/settings" title="View explorer settings" className="hidden sm:block">
<div className="p-1.5 bg-pink-500 rounded-full active:opacity-90 hover:rotate-90 transition-all"> <div className="active:opacity-90 hover:rotate-90 transition-all">
<Image src={GearIcon} width={16} height={16} className="invert" alt="Settings" /> <GearIcon color={Color.pink} height={18} width={18} />
</div> </div>
</Link> </Link>
</div> </div>
@ -68,52 +68,64 @@ export function SearchFilterBar({
function ChainSelector({ function ChainSelector({
text, text,
header,
value, value,
onChangeValue, onChangeValue,
position,
}: { }: {
text: string; text: string;
header: string; value: ChainId | null;
value: string | null; // comma separated list of checked chains
onChangeValue: (value: string | null) => void; onChangeValue: (value: string | null) => void;
position?: string;
}) { }) {
const multiProvider = useMultiProvider(); const multiProvider = useMultiProvider();
const { chains } = useScrapedEvmChains(multiProvider); const { chains } = useScrapedEvmChains(multiProvider);
// const [checkedChain, setCheckedChain] = useState<ChainId|null>(value); const [showModal, setShowModal] = useState(false);
const closeModal = () => {
setShowModal(false);
};
const onClickChain = (c: ChainMetadata) => { const onClickChain = (c: ChainMetadata) => {
// setCheckedChain(c.chainId);
onChangeValue(c.chainId.toString()); onChangeValue(c.chainId.toString());
closeModal();
}; };
const [showModal, setShowModal] = useState(false); const onClear = () => {
const closeModal = () => { onChangeValue(null);
setShowModal(false);
}; };
const chainName = value
? trimToLength(getChainDisplayName(multiProvider, value, true), 12)
: undefined;
return ( return (
<> <div className="relative">
<button <button
type="button" type="button"
className="text-sm sm:min-w-[5.8rem] px-1 sm:px-2.5 py-0.5 flex items-center justify-center rounded-full bg-pink-500 hover:opacity-80 active:opacity-70 transition-all" className={clsx(
'text-sm sm:min-w-[5.8rem] px-1.5 sm:px-2.5 py-1 flex items-center justify-center font-medium rounded-full border border-pink-500 hover:opacity-80 active:opacity-70 transition-all',
value ? 'bg-pink-500 text-white pr-7 sm:pr-8' : 'text-pink-500',
)}
onClick={() => setShowModal(!showModal)} onClick={() => setShowModal(!showModal)}
> >
<span className="text-white font-medium py-px">{text}</span> <span>{chainName || text} </span>
<ChevronIcon {!value && (
direction="s" <ChevronIcon
width={9} direction="s"
height={5} width={9}
classes="ml-2 opacity-80" height={5}
color={Color.white} classes="ml-2 opacity-80"
/> color={Color.pink}
/>
)}
</button> </button>
<Modal isOpen={showModal} close={closeModal} panelClassname="max-w-lg p-4 sm:p-5"> {value && <ClearButton onClick={onClear} />}
<Modal
isOpen={showModal}
close={closeModal}
panelClassname="p-4 sm:p-5 max-w-lg min-h-[50vh]"
>
<ChainSearchMenu chainMetadata={chains} onClickChain={onClickChain} /> <ChainSearchMenu chainMetadata={chains} onClickChain={onClickChain} />
</Modal> </Modal>
</> </div>
); );
} }
@ -137,50 +149,79 @@ function DatetimeSelector({
setEndTime(null); setEndTime(null);
}; };
const onClickDirectClear = () => {
onClickClear();
onChangeStartValue(null);
onChangeEndValue(null);
};
const onClickApply = (closeDropdown?: () => void) => { const onClickApply = (closeDropdown?: () => void) => {
onChangeStartValue(startTime); onChangeStartValue(startTime);
onChangeEndValue(endTime); onChangeEndValue(endTime);
if (closeDropdown) closeDropdown(); if (closeDropdown) closeDropdown();
}; };
const hasValue = !!startTime || !!endTime;
return ( return (
<Popover <div className="relative">
button={ <Popover
<> button={
<span className="text-white font-medium py-px px-2">Time</span> <>
<ChevronIcon <span>Time</span>
direction="s" {!hasValue && (
width={9} <ChevronIcon
height={5} direction="s"
classes="ml-2 opacity-80" width={9}
color={Color.white} height={5}
/> classes="ml-2 opacity-80"
</> color={Color.pink}
} />
buttonClassname="text-sm px-1 sm:px-2.5 py-0.5 flex items-center justify-center rounded-full bg-pink-500 hover:opacity-80 active:opacity-70 transition-all" )}
panelClassname="w-60" </>
> }
{({ close }) => ( buttonClassname={clsx(
<div className="p-4" key="date-time-selector"> 'text-sm px-2 sm:px-3 py-1 flex items-center justify-center font-medium border border-pink-500 rounded-full hover:opacity-80 active:opacity-70 transition-all',
<div className="flex items-center justify-between"> hasValue ? ' bg-pink-500 text-white pr-7 sm:pr-8' : 'text-pink-500',
<h3 className="text-blue-500 font-medium">Time Range</h3> )}
<div className="flex pt-1"> panelClassname="w-60"
<TextButton classes="text-sm font-medium text-pink-500" onClick={onClickClear}> >
Clear {({ close }) => (
</TextButton> <div className="p-4" key="date-time-selector">
<div className="flex items-center justify-between">
<h3 className="text-blue-500 font-medium">Time Range</h3>
<div className="flex pt-1">
<TextButton classes="text-sm font-medium text-pink-500" onClick={onClickClear}>
Clear
</TextButton>
</div>
</div> </div>
<div className="flex flex-col">
<h4 className="mt-3 mb-1 text-gray-500 text-sm font-medium">Start Time</h4>
<DatetimeField timestamp={startTime} onChange={setStartTime} />
<h4 className="mt-3 mb-1 text-gray-500 text-sm font-medium">End Time</h4>
<DatetimeField timestamp={endTime} onChange={setEndTime} />
</div>
<SolidButton
classes="mt-4 text-sm px-2 py-1 w-full"
onClick={() => onClickApply(close)}
>
Apply
</SolidButton>
</div> </div>
<div className="flex flex-col"> )}
<h4 className="mt-3 mb-1 text-gray-500 text-sm font-medium">Start Time</h4> </Popover>
<DatetimeField timestamp={startTime} onChange={setStartTime} /> {hasValue && <ClearButton onClick={onClickDirectClear} />}
<h4 className="mt-3 mb-1 text-gray-500 text-sm font-medium">End Time</h4> </div>
<DatetimeField timestamp={endTime} onChange={setEndTime} /> );
</div> }
<SolidButton classes="mt-4 text-sm px-2 py-1 w-full" onClick={() => onClickApply(close)}>
Apply function ClearButton({ onClick }: { onClick: () => void }) {
</SolidButton> return (
</div> <div className="absolute right-1.5 top-1/2 -translate-y-1/2">
)} <IconButton onClick={onClick} className="bg-pink-300 p-1.5 rounded-full">
</Popover> <XIcon color="white" height={9} width={9} />
</IconButton>
</div>
); );
} }

@ -42,7 +42,7 @@ export function useScrapedEvmChains(multiProvider: MultiProvider) {
// https://github.com/hyperlane-xyz/hyperlane-explorer/issues/61 // https://github.com/hyperlane-xyz/hyperlane-explorer/issues/61
const scrapedEvmChains = objFilter( const scrapedEvmChains = objFilter(
multiProvider.metadata, multiProvider.metadata,
(chainName, chainMetadata): chainMetadata is ChainMetadata => (_, chainMetadata): chainMetadata is ChainMetadata =>
isEvmChain(multiProvider, chainMetadata.chainId) && isEvmChain(multiProvider, chainMetadata.chainId) &&
!isPiChain(multiProvider, scrapedChains, chainMetadata.chainId) && !isPiChain(multiProvider, scrapedChains, chainMetadata.chainId) &&
!isUnscrapedDbChain(multiProvider, chainMetadata.chainId), !isUnscrapedDbChain(multiProvider, chainMetadata.chainId),

@ -23,9 +23,9 @@ export function getChainDisplayName(
chainOrDomainId?: ChainId | DomainId, chainOrDomainId?: ChainId | DomainId,
shortName = false, shortName = false,
fallbackToId = true, fallbackToId = true,
) { ): string {
const metadata = multiProvider.tryGetChainMetadata(chainOrDomainId || 0); const metadata = multiProvider.tryGetChainMetadata(chainOrDomainId || 0);
if (!metadata) return fallbackToId && chainOrDomainId ? chainOrDomainId : 'Unknown'; if (!metadata) return fallbackToId && chainOrDomainId ? chainOrDomainId.toString() : 'Unknown';
const displayName = shortName ? metadata.displayNameShort : metadata.displayName; const displayName = shortName ? metadata.displayNameShort : metadata.displayName;
return toTitleCase(displayName || metadata.displayName || metadata.name); return toTitleCase(displayName || metadata.displayName || metadata.name);
} }

@ -2,7 +2,8 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import { GithubRegistry, IRegistry } from '@hyperlane-xyz/registry'; import { GithubRegistry, IRegistry } from '@hyperlane-xyz/registry';
import { ChainMap, MultiProvider } from '@hyperlane-xyz/sdk'; import { ChainMap, ChainMetadata, MultiProvider } from '@hyperlane-xyz/sdk';
import { objMap, promiseObjAll } from '@hyperlane-xyz/utils';
import { config } from './consts/config'; import { config } from './consts/config';
import { ChainConfig } from './features/chains/chainConfig'; import { ChainConfig } from './features/chains/chainConfig';
@ -91,5 +92,15 @@ async function buildMultiProvider(registry: IRegistry, customChainConfigs: Chain
// TODO improve interface so this pre-cache isn't required // TODO improve interface so this pre-cache isn't required
await registry.listRegistryContent(); await registry.listRegistryContent();
const registryChainMetadata = await registry.getMetadata(); const registryChainMetadata = await registry.getMetadata();
return new MultiProvider({ ...registryChainMetadata, ...customChainConfigs }); // TODO have the registry do this automatically
const metadataWithLogos = await promiseObjAll(
objMap(
registryChainMetadata,
async (chainName, metadata): Promise<ChainMetadata> => ({
...metadata,
logoURI: (await registry.getChainLogoUri(chainName)) || undefined,
}),
),
);
return new MultiProvider({ ...metadataWithLogos, ...customChainConfigs });
} }

@ -10,7 +10,7 @@ export const Color = {
lightGray: themeColors.gray[200], lightGray: themeColors.gray[200],
primary: themeColors.blue[500], primary: themeColors.blue[500],
accent: themeColors.pink[500], accent: themeColors.pink[500],
blue: themeColors.blue[200], blue: themeColors.blue[500],
pink: themeColors.pink[200], pink: themeColors.pink[500],
red: themeColors.red[500], red: themeColors.red[500],
} as const; } as const;

@ -2189,6 +2189,7 @@ __metadata:
autoprefixer: "npm:^10.4.15" autoprefixer: "npm:^10.4.15"
bignumber.js: "npm:^9.1.2" bignumber.js: "npm:^9.1.2"
buffer: "npm:^6.0.3" buffer: "npm:^6.0.3"
clsx: "npm:^2.1.1"
eslint: "npm:^8.41.0" eslint: "npm:^8.41.0"
eslint-config-next: "npm:^13.4.19" eslint-config-next: "npm:^13.4.19"
eslint-config-prettier: "npm:^8.8.0" eslint-config-prettier: "npm:^8.8.0"
@ -5679,7 +5680,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"clsx@npm:^2.0.0": "clsx@npm:^2.0.0, clsx@npm:^2.1.1":
version: 2.1.1 version: 2.1.1
resolution: "clsx@npm:2.1.1" resolution: "clsx@npm:2.1.1"
checksum: cdfb57fa6c7649bbff98d9028c2f0de2f91c86f551179541cf784b1cfdc1562dcb951955f46d54d930a3879931a980e32a46b598acaea274728dbe068deca919 checksum: cdfb57fa6c7649bbff98d9028c2f0de2f91c86f551179541cf784b1cfdc1562dcb951955f46d54d930a3879931a980e32a46b598acaea274728dbe068deca919

Loading…
Cancel
Save