Skip to content

Commit

Permalink
[CORE-652] Add chain icon in SingleNetworkSelector + other chain icon…
Browse files Browse the repository at this point in the history
… related fixes
  • Loading branch information
MananTank committed Dec 18, 2024
1 parent 7d8566e commit 8ad0c17
Show file tree
Hide file tree
Showing 19 changed files with 170 additions and 61 deletions.
1 change: 1 addition & 0 deletions apps/dashboard/src/@/components/blocks/Img.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export function Img(props: imgElementProps) {
"fade-in-0 object-cover transition-opacity duration-300",
className,
)}
decoding="async"
/>

{status !== "loaded" && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { MultiNetworkSelector } from "./NetworkSelectors";

const meta = {
title: "blocks/Cards/MultiNetworkSelector",
component: Story,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Story>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Desktop: Story = {
args: {},
};

export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};

function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
<Variant label="No Chains selected by default" selectedChainIds={[]} />
<Variant
label="Polygon, Ethereum selected by default"
selectedChainIds={[1, 137]}
/>
</div>
);
}

function Variant(props: {
label: string;
selectedChainIds: number[];
}) {
const [chainIds, setChainIds] = useState<number[]>(props.selectedChainIds);
return (
<BadgeContainer label={props.label}>
<MultiNetworkSelector
selectedChainIds={chainIds}
onChange={setChainIds}
/>
</BadgeContainer>
);
}
14 changes: 11 additions & 3 deletions apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MultiSelect } from "@/components/blocks/multi-select";
import { SelectWithSearch } from "@/components/blocks/select-with-search";
import { Badge } from "@/components/ui/badge";
import { useCallback, useMemo } from "react";
import { ChainIcon } from "../../../components/icons/ChainIcon";
import { useAllChainsData } from "../../../hooks/chains/allChains";

function cleanChainName(chainName: string) {
Expand Down Expand Up @@ -51,7 +52,7 @@ export function MultiNetworkSelector(props: {

return (
<div className="flex justify-between gap-4">
<span className="grow truncate text-left">
<span className="flex grow gap-2 truncate text-left">
{cleanChainName(chain.name)}
</span>
<Badge variant="outline" className="gap-2">
Expand Down Expand Up @@ -133,8 +134,15 @@ export function SingleNetworkSelector(props: {

return (
<div className="flex justify-between gap-4">
<span className="grow truncate text-left">{chain.name}</span>
<Badge variant="outline" className="gap-2">
<span className="flex grow gap-2 truncate text-left">
<ChainIcon
className="size-5"
ipfsSrc={chain.icon?.url}
loading="lazy"
/>
{chain.name}
</span>
<Badge variant="outline" className="gap-2 max-sm:hidden">
<span className="text-muted-foreground">Chain ID</span>
{chain.chainId}
</Badge>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { SingleNetworkSelector } from "./NetworkSelectors";

const meta = {
title: "blocks/Cards/SingleNetworkSelector",
component: Story,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Story>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Desktop: Story = {
args: {},
};

export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};

function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
<Variant label="No Chain ID selected by default" chainId={undefined} />
<Variant label="Polygon selected by default" chainId={137} />
<Variant
label="Show certain chains only"
chainId={undefined}
chainIds={[1, 137, 10]}
/>
</div>
);
}

function Variant(props: {
label: string;
chainId: number | undefined;
chainIds?: number[];
}) {
const [chainId, setChainId] = useState<number | undefined>(props.chainId);
return (
<BadgeContainer label={props.label}>
<SingleNetworkSelector
chainId={chainId}
onChange={setChainId}
chainIds={props.chainIds}
/>
</BadgeContainer>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const MetadataHeader: React.FC<MetadataHeaderProps> = ({
href={`/${chain.slug}`}
className="flex w-fit shrink-0 items-center gap-2 rounded-3xl border border-border bg-muted/50 px-2.5 py-1.5 hover:bg-muted"
>
<ChainIcon ipfsSrc={chain.icon?.url} size={16} />
<ChainIcon ipfsSrc={chain.icon?.url} className="size-4" />
{cleanedChainName && (
<span className="text-xs">{cleanedChainName}</span>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/env";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
import { cn } from "@/lib/utils";

const fallbackChainIcon =
"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOTYiIGhlaWdodD0iOTYiIHZpZXdCb3g9IjAgMCA5NiA5NiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTY4LjE1MTkgNzUuNzM3MkM2Mi4yOTQzIDc5Ljk5MyA1NS4yMzk3IDgyLjI4NTIgNDcuOTk5MyA4Mi4yODUyQzQwLjc1ODkgODIuMjg1MiAzMy43MDQzIDc5Ljk5MyAyNy44NDY2IDc1LjczNzJNNjMuMDI5MSAxNy4xODM3QzY5LjUzNjggMjAuMzU3NyA3NC44NzI2IDI1LjUxMDQgNzguMjcxOCAzMS45MDMzQzgxLjY3MDkgMzguMjk2MiA4Mi45NTkgNDUuNjAxMiA4MS45NTEzIDUyLjc3MTFNMTQuMDQ3NiA1Mi43NzA4QzEzLjAzOTkgNDUuNjAwOCAxNC4zMjggMzguMjk1OSAxNy43MjcxIDMxLjkwM0MyMS4xMjYzIDI1LjUxMDEgMjYuNDYyMSAyMC4zNTczIDMyLjk2OTggMTcuMTgzM000Ni4wNTk4IDI5LjM2NzVMMjkuMzY3MyA0Ni4wNkMyOC42ODg1IDQ2LjczODkgMjguMzQ5IDQ3LjA3ODMgMjguMjIxOCA0Ny40Njk3QzI4LjExIDQ3LjgxNCAyOC4xMSA0OC4xODQ5IDI4LjIyMTggNDguNTI5MkMyOC4zNDkgNDguOTIwNiAyOC42ODg1IDQ5LjI2MDEgMjkuMzY3MyA0OS45MzlMNDYuMDU5OCA2Ni42MzE0QzQ2LjczODcgNjcuMzEwMyA0Ny4wNzgxIDY3LjY0OTcgNDcuNDY5NSA2Ny43NzY5QzQ3LjgxMzggNjcuODg4OCA0OC4xODQ3IDY3Ljg4ODggNDguNTI5IDY3Ljc3NjlDNDguOTIwNCA2Ny42NDk3IDQ5LjI1OTkgNjcuMzEwMyA0OS45Mzg4IDY2LjYzMTRMNjYuNjMxMiA0OS45MzlDNjcuMzEwMSA0OS4yNjAxIDY3LjY0OTUgNDguOTIwNiA2Ny43NzY3IDQ4LjUyOTJDNjcuODg4NiA0OC4xODQ5IDY3Ljg4ODYgNDcuODE0IDY3Ljc3NjcgNDcuNDY5N0M2Ny42NDk1IDQ3LjA3ODMgNjcuMzEwMSA0Ni43Mzg5IDY2LjYzMTIgNDYuMDZMNDkuOTM4OCAyOS4zNjc1QzQ5LjI1OTkgMjguNjg4NyA0OC45MjA0IDI4LjM0OTIgNDguNTI5IDI4LjIyMkM0OC4xODQ3IDI4LjExMDIgNDcuODEzOCAyOC4xMTAyIDQ3LjQ2OTUgMjguMjIyQzQ3LjA3ODEgMjguMzQ5MiA0Ni43Mzg3IDI4LjY4ODcgNDYuMDU5OCAyOS4zNjc1WiIgc3Ryb2tlPSIjNDA0MDQwIiBzdHJva2Utd2lkdGg9IjYuODU3MTQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K";
import { fallbackChainIcon } from "../../../../../utils/chain-icons";

export async function ChainIcon(props: {
iconUrl?: string;
Expand Down Expand Up @@ -35,6 +33,11 @@ export async function ChainIcon(props: {

if (res?.status === 200) {
imageLink = resolved;
// check that its an image
const contentType = res.headers.get("content-type");
if (!contentType?.startsWith("image")) {
imageLink = fallbackChainIcon;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const ContractSubscriptionTable: React.FC<
const chain = idToChain.get(cell.getValue());
return (
<Flex align="center" gap={2}>
<ChainIcon size={12} ipfsSrc={chain?.icon?.url} />
<ChainIcon className="size-3" ipfsSrc={chain?.icon?.url} />
<Text>{chain?.name ?? "N/A"}</Text>
</Flex>
);
Expand Down Expand Up @@ -399,7 +399,7 @@ const RemoveModal = ({
<FormControl>
<FormLabel>Chain</FormLabel>
<Flex align="center" gap={2}>
<ChainIcon size={12} ipfsSrc={chain?.icon?.url} />
<ChainIcon className="size-3" ipfsSrc={chain?.icon?.url} />
<Text>{chain?.name ?? "N/A"}</Text>
</Flex>
</FormControl>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ const SendFundsModal = ({
<FormControl>
<FormLabel>Chain</FormLabel>
<Flex align="center" gap={2}>
<ChainIcon size={12} ipfsSrc={chain?.icon?.url} />
<ChainIcon className="size-3" ipfsSrc={chain?.icon?.url} />
<Text>{chain?.name}</Text>
</Flex>
</FormControl>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export const TransactionsTable: React.FC<TransactionsTableProps> = ({
if (chain) {
return (
<Flex align="center" gap={2} className="py-2">
<ChainIcon size={12} ipfsSrc={chain?.icon?.url} />
<ChainIcon className="size-3" ipfsSrc={chain?.icon?.url} />
<Text maxW={150} isTruncated>
{chain?.name ?? "N/A"}
</Text>
Expand Down Expand Up @@ -359,7 +359,7 @@ const TransactionDetailsDrawer = ({
<FormControl>
<FormLabel>Chain</FormLabel>
<Flex align="center" gap={2}>
<ChainIcon size={12} ipfsSrc={chain?.icon?.url} />
<ChainIcon className="size-3" ipfsSrc={chain?.icon?.url} />
<Text>{chain?.name}</Text>
</Flex>
</FormControl>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const RelayersTable: React.FC<RelayersTableProps> = ({
const chain = idToChain.get(Number.parseInt(cell.getValue()));
return (
<Flex align="center" gap={2}>
<ChainIcon size={12} ipfsSrc={chain?.icon?.url} />
<ChainIcon className="size-3" ipfsSrc={chain?.icon?.url} />
<Text>{chain?.name ?? "N/A"}</Text>
</Flex>
);
Expand Down Expand Up @@ -405,7 +405,7 @@ const RemoveModal = ({
<FormLabel>Chain</FormLabel>
<Flex align="center" gap={2}>
<ChainIcon
size={12}
className="size-3"
ipfsSrc={
idToChain.get(Number.parseInt(relayer.chainId))?.icon?.url
}
Expand Down
3 changes: 1 addition & 2 deletions apps/dashboard/src/components/cmd-k-search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,8 @@ const SearchResult: React.FC<SearchResultProps> = ({
)}
>
<ChainIcon
size={24}
className="size-6 shrink-0"
ipfsSrc={result.chainMetadata?.icon?.url}
className="shrink-0"
/>
<div className="flex flex-col gap-1">
<h3 className="line-clamp-2 font-semibold text-foreground">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
label="Icon"
>
<div className="flex items-center gap-1">
<ChainIcon size={20} ipfsSrc={form.watch("icon")} />
<ChainIcon className="size-5" ipfsSrc={form.watch("icon")} />
<IconUpload
onUpload={(uri) => {
form.setValue("icon", uri, { shouldDirty: true });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ const ContractTable: React.FC<ContractTableProps> = ({
`Unknown Network (#${cell.row.original.chainId})`;
return (
<div className="flex items-center gap-2">
<ChainIcon size={24} ipfsSrc={data?.icon?.url} />
<ChainIcon className="size-5" ipfsSrc={data?.icon?.url} />
<SkeletonContainer
loadedData={data ? cleanedChainName : undefined}
skeletonData={`Chain ID ${cell.row.original.chainId}`}
Expand Down
56 changes: 19 additions & 37 deletions apps/dashboard/src/components/icons/ChainIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,31 @@
"use client";

import { Img } from "@/components/blocks/Img";
/* eslint-disable @next/next/no-img-element */
import { replaceIpfsUrl } from "lib/sdk";
import { forwardRef } from "react";

const fallbackIcon = replaceIpfsUrl(
"ipfs://QmU1r24UsmGg2w2RePz98zV5hR3CnjvakLZzB6yH4prPFh/globe.svg",
);
import { cn } from "../../@/lib/utils";
import { fallbackChainIcon } from "../../utils/chain-icons";

type ImageProps = React.ComponentProps<"img">;

type ChainIconProps = ImageProps & {
ipfsSrc?: string;
size: ImageProps["width"];
};

export const ChainIcon = forwardRef<HTMLImageElement, ChainIconProps>(
({ ipfsSrc, size, ...restProps }, ref) => {
const src = ipfsSrc ? replaceIpfsUrl(ipfsSrc) : fallbackIcon;
// treat size of number as "px"
size = typeof size === "number" ? `${size}px` : size;
export const ChainIcon = ({ ipfsSrc, ...restProps }: ChainIconProps) => {
const src = ipfsSrc ? replaceIpfsUrl(ipfsSrc) : fallbackChainIcon;

return (
<img
{...restProps}
ref={ref}
// render different image element if src changes to avoid showing old image while loading new one
key={src}
src={src}
width={size}
height={size}
style={{
objectFit: "contain",
width: size,
height: size,
}}
loading={restProps.loading || "lazy"}
decoding="async"
alt=""
// fallbackSrc is not working
onError={(event) => {
event.currentTarget.srcset = `${fallbackIcon} 1x`;
event.currentTarget.src = fallbackIcon;
}}
/>
);
},
);
return (
<Img
{...restProps}
// render different image element if src changes to avoid showing old image while loading new one
key={src}
className={cn("object-contain", restProps.className)}
src={src}
loading={restProps.loading || "lazy"}
alt=""
fallback={<img src={fallbackChainIcon} alt="" />}
skeleton={<div className="animate-pulse rounded-full bg-border" />}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const CustomChainRenderer = ({
}
}}
>
<ChainIcon ipfsSrc={chain.icon?.url} size={32} />
<ChainIcon ipfsSrc={chain.icon?.url} className="size-8" />
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<p
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,21 @@ export const NetworkSelectDropdown: React.FC<NetworkSelectDropdownProps> = ({
onSelect(v === "all-chains" ? undefined : v);
}}
>
<SelectTrigger className="-translate-x-3 !h-auto inline-flex w-auto border-none bg-transparent py-1 font-medium hover:bg-muted">
<SelectTrigger className="-translate-x-3 !h-auto inline-flex w-auto border-none bg-transparent px-1 py-0.5 font-medium hover:bg-muted focus:ring-0 focus:ring-offset-0">
<SelectValue />
</SelectTrigger>

<SelectContent align="center" className="rounded-lg shadow-lg">
<SelectItem value="all-chains">
<div className="flex items-center gap-2 py-1" data-all-chains>
<ChainIcon ipfsSrc={undefined} size={24} />
<ChainIcon ipfsSrc={undefined} className="size-5" />
All Networks
</div>
</SelectItem>
{chains.map((chain) => (
<SelectItem key={chain.chainId} value={String(chain.chainId)}>
<div className="flex items-center gap-2 py-1">
<ChainIcon ipfsSrc={chain.icon?.url} size={24} />
<ChainIcon ipfsSrc={chain.icon?.url} className="size-5" />
{useCleanChainName ? cleanChainName(chain.name) : chain.name}
</div>
</SelectItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export const NetworkSelectorButton: React.FC<NetworkSelectorButtonProps> = ({
});
}}
>
<ChainIcon ipfsSrc={chain?.icon?.url} size={20} />
<ChainIcon ipfsSrc={chain?.icon?.url} className="size-5" />
{chain?.name || "Select Network"}

<ChevronDownIcon className="ml-auto size-4" />
Expand Down
Loading

0 comments on commit 8ad0c17

Please sign in to comment.