Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CORE-652] Add chain icon in SingleNetworkSelector + other chain icon related fixes #5794

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw this is one of the things we should switch to using the react sdk components for!

{chain?.name || "Select Network"}

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