Redesign search chain filters

pull/114/head
J M Rossy 2 months ago
parent 1f2434b503
commit 5bf09c83df
  1. 1
      package.json
  2. 111
      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",
"bignumber.js": "^9.1.2",
"buffer": "^6.0.3",
"clsx": "^2.1.1",
"ethers": "^5.7.2",
"formik": "^2.2.9",
"graphql": "^16.6.0",

@ -1,12 +1,20 @@
import Image from 'next/image';
import clsx from 'clsx';
import Link from 'next/link';
import { useState } from 'react';
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 GearIcon from '../../images/icons/gear.svg';
import { getChainDisplayName } from '../../features/chains/utils';
import { useMultiProvider } from '../../store';
import { Color } from '../../styles/Color';
import { SolidButton } from '../buttons/SolidButton';
@ -37,19 +45,11 @@ export function SearchFilterBar({
}: Props) {
return (
<div className="flex items-center space-x-2 md:space-x-4">
<ChainSelector
text="Origin"
header="Origin Chains"
value={originChain}
onChangeValue={onChangeOrigin}
position="-right-32"
/>
<ChainSelector text="Origin" value={originChain} onChangeValue={onChangeOrigin} />
<ChainSelector
text="Destination"
header="Destination Chains"
value={destinationChain}
onChangeValue={onChangeDestination}
position="-right-28"
/>
<DatetimeSelector
startValue={startTimestamp}
@ -57,9 +57,9 @@ export function SearchFilterBar({
endValue={endTimestamp}
onChangeEndValue={onChangeEndTimestamp}
/>
<Link href="/settings" title="View explorer settings">
<div className="p-1.5 bg-pink-500 rounded-full active:opacity-90 hover:rotate-90 transition-all">
<Image src={GearIcon} width={16} height={16} className="invert" alt="Settings" />
<Link href="/settings" title="View explorer settings" className="hidden sm:block">
<div className="active:opacity-90 hover:rotate-90 transition-all">
<GearIcon color={Color.pink} height={18} width={18} />
</div>
</Link>
</div>
@ -68,52 +68,64 @@ export function SearchFilterBar({
function ChainSelector({
text,
header,
value,
onChangeValue,
position,
}: {
text: string;
header: string;
value: string | null; // comma separated list of checked chains
value: ChainId | null;
onChangeValue: (value: string | null) => void;
position?: string;
}) {
const multiProvider = useMultiProvider();
const { chains } = useScrapedEvmChains(multiProvider);
// const [checkedChain, setCheckedChain] = useState<ChainId|null>(value);
const [showModal, setShowModal] = useState(false);
const closeModal = () => {
setShowModal(false);
};
const onClickChain = (c: ChainMetadata) => {
// setCheckedChain(c.chainId);
onChangeValue(c.chainId.toString());
closeModal();
};
const [showModal, setShowModal] = useState(false);
const closeModal = () => {
setShowModal(false);
const onClear = () => {
onChangeValue(null);
};
const chainName = value
? trimToLength(getChainDisplayName(multiProvider, value, true), 12)
: undefined;
return (
<>
<div className="relative">
<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)}
>
<span className="text-white font-medium py-px">{text}</span>
<span>{chainName || text} </span>
{!value && (
<ChevronIcon
direction="s"
width={9}
height={5}
classes="ml-2 opacity-80"
color={Color.white}
color={Color.pink}
/>
)}
</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} />
</Modal>
</>
</div>
);
}
@ -137,27 +149,41 @@ function DatetimeSelector({
setEndTime(null);
};
const onClickDirectClear = () => {
onClickClear();
onChangeStartValue(null);
onChangeEndValue(null);
};
const onClickApply = (closeDropdown?: () => void) => {
onChangeStartValue(startTime);
onChangeEndValue(endTime);
if (closeDropdown) closeDropdown();
};
const hasValue = !!startTime || !!endTime;
return (
<div className="relative">
<Popover
button={
<>
<span className="text-white font-medium py-px px-2">Time</span>
<span>Time</span>
{!hasValue && (
<ChevronIcon
direction="s"
width={9}
height={5}
classes="ml-2 opacity-80"
color={Color.white}
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"
buttonClassname={clsx(
'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',
hasValue ? ' bg-pink-500 text-white pr-7 sm:pr-8' : 'text-pink-500',
)}
panelClassname="w-60"
>
{({ close }) => (
@ -176,11 +202,26 @@ function DatetimeSelector({
<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)}>
<SolidButton
classes="mt-4 text-sm px-2 py-1 w-full"
onClick={() => onClickApply(close)}
>
Apply
</SolidButton>
</div>
)}
</Popover>
{hasValue && <ClearButton onClick={onClickDirectClear} />}
</div>
);
}
function ClearButton({ onClick }: { onClick: () => void }) {
return (
<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">
<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
const scrapedEvmChains = objFilter(
multiProvider.metadata,
(chainName, chainMetadata): chainMetadata is ChainMetadata =>
(_, chainMetadata): chainMetadata is ChainMetadata =>
isEvmChain(multiProvider, chainMetadata.chainId) &&
!isPiChain(multiProvider, scrapedChains, chainMetadata.chainId) &&
!isUnscrapedDbChain(multiProvider, chainMetadata.chainId),

@ -23,9 +23,9 @@ export function getChainDisplayName(
chainOrDomainId?: ChainId | DomainId,
shortName = false,
fallbackToId = true,
) {
): string {
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;
return toTitleCase(displayName || metadata.displayName || metadata.name);
}

@ -2,7 +2,8 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
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 { 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
await registry.listRegistryContent();
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],
primary: themeColors.blue[500],
accent: themeColors.pink[500],
blue: themeColors.blue[200],
pink: themeColors.pink[200],
blue: themeColors.blue[500],
pink: themeColors.pink[500],
red: themeColors.red[500],
} as const;

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

Loading…
Cancel
Save