feat: add 'AddChainToWallet' button and integrate it into ChainPageLayout (#2666)
- Created a new component 'AddChainToWallet' to facilitate adding chains to wallet.
- Integrated 'AddChainToWallet' button into ChainPageLayout.
- Updated layout.tsx to include 'AddChainToWallet' button and its behavior.
- Refactored chain verification and metadata handling to accommodate the new feature.
- Adjusted related utility functions in utils.ts.
<!-- start pr-codex -->
---
## PR-Codex overview
The focus of this PR is to update the chainlist components by introducing a new `isVerified` flag and refactoring the rendering logic based on it.
### Detailed summary
- Introduces `isVerified` flag to determine if chain metadata is available
- Updates rendering logic in `ChainListRow` and `ChainListCard` components based on `isVerified`
- Adds `AddChainToWallet` component for connecting to chains
- Adjusts layout and styling based on `isVerified` flag and chain metadata
> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`
<!-- end pr-codex -->
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAddress, useChainId, useSwitchChain } from "@thirdweb-dev/react";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { useDebounce } from "use-debounce";
|
||||
|
||||
type AddChainToWalletProps = {
|
||||
chainId: number;
|
||||
};
|
||||
|
||||
export const AddChainToWallet: React.FC<AddChainToWalletProps> = (props) => {
|
||||
const address = useAddress();
|
||||
const switchChain = useSwitchChain();
|
||||
const activeWalletChainId = useChainId();
|
||||
|
||||
const switchChainMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await switchChain(props.chainId);
|
||||
},
|
||||
});
|
||||
|
||||
// debounce the loading state to prevent flickering
|
||||
const [debouncedLoading] = useDebounce(switchChainMutation.isLoading, 50);
|
||||
|
||||
const buttonContent = (
|
||||
<Button
|
||||
disabled={
|
||||
!address || debouncedLoading || activeWalletChainId === props.chainId
|
||||
}
|
||||
className="w-full md:min-w-40"
|
||||
onClick={() => switchChainMutation.mutate()}
|
||||
>
|
||||
{debouncedLoading && <Spinner />}
|
||||
<span>Add to wallet</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (address && activeWalletChainId !== props.chainId) {
|
||||
return buttonContent;
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolTipLabel
|
||||
label={
|
||||
activeWalletChainId === props.chainId
|
||||
? "You are already connected to this chain"
|
||||
: "Connect your wallet first"
|
||||
}
|
||||
>
|
||||
<div className="cursor-not-allowed">{buttonContent}</div>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
};
|
||||
@@ -21,6 +21,8 @@ import { ChainIcon } from "../components/server/chain-icon";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { getChain, getChainMetadata } from "../utils";
|
||||
import { ChainCTA } from "./components/server/cta-card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AddChainToWallet } from "./components/client/add-chain-to-wallet";
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params }: { params: { chain_id: string } },
|
||||
@@ -66,6 +68,8 @@ export default async function ChainPageLayout({
|
||||
redirect(chain.slug);
|
||||
}
|
||||
const chainMetadata = await getChainMetadata(chain.chainId);
|
||||
// true if we *have* chainMetadata for the chain
|
||||
const isVerified = !!chainMetadata;
|
||||
const isDeprecated = chain.status === "deprecated";
|
||||
|
||||
return (
|
||||
@@ -89,9 +93,7 @@ export default async function ChainPageLayout({
|
||||
<header
|
||||
className={cn(
|
||||
"py-6 md:py-8 border-b relative overflow-hidden",
|
||||
!chainMetadata?.gasSponsored &&
|
||||
!chainMetadata?.verified &&
|
||||
"md:pb-2",
|
||||
!chainMetadata?.gasSponsored && !isVerified && "md:pb-2",
|
||||
chainMetadata?.headerImgUrl && "md:py-10",
|
||||
)}
|
||||
>
|
||||
@@ -111,56 +113,78 @@ export default async function ChainPageLayout({
|
||||
</div>
|
||||
|
||||
{/* end header shaningans */}
|
||||
|
||||
<div className="container px-4 flex flex-col gap-2 md:gap-6">
|
||||
<Link
|
||||
href="/chainlist"
|
||||
className="inline-flex items-center gap-1 text-foreground hover:underline mt-4"
|
||||
>
|
||||
<ArrowLeftIcon className="size-5" />
|
||||
Chainlist
|
||||
</Link>
|
||||
|
||||
<div className="flex gap-3 md:gap-5 items-center">
|
||||
{chain.icon?.url && (
|
||||
<ChainIcon
|
||||
iconUrl={chain.icon.url}
|
||||
className="size-16 md:size-20 bg-secondary p-2 border-2 rounded-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Chain Name */}
|
||||
|
||||
<h1
|
||||
className={cn(
|
||||
"font-semibold tracking-tighter text-4xl md:text-6xl",
|
||||
)}
|
||||
<div className="container px-4 flex flex-col md:flex-row justify-between md:items-center">
|
||||
<div className="flex flex-col gap-2 md:gap-6">
|
||||
<Link
|
||||
href="/chainlist"
|
||||
className="inline-flex items-center gap-1 text-foreground hover:underline mt-4"
|
||||
>
|
||||
{chain.name}
|
||||
</h1>
|
||||
<StarButton chainId={chain.chainId} variant="secondary" />
|
||||
</div>
|
||||
<ArrowLeftIcon className="size-5" />
|
||||
Chainlist
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-row gap-2 md:gap-4 items-center h-8 mb-4 md:mb-6">
|
||||
{chainMetadata?.verified && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-accent-foreground pointer-events-none flex flex-row items-center h-full gap-1.5 border border-border"
|
||||
<div className="flex gap-3 md:gap-5 items-center">
|
||||
{chain.icon?.url && (
|
||||
<ChainIcon
|
||||
iconUrl={chain.icon.url}
|
||||
className="size-16 md:size-20 bg-secondary p-2 border-2 rounded-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Chain Name */}
|
||||
|
||||
<h1
|
||||
className={cn(
|
||||
"font-semibold tracking-tighter text-4xl md:text-6xl",
|
||||
)}
|
||||
>
|
||||
<VerifiedIcon className="size-5" />
|
||||
<span className="font-bold text-xs uppercase">verified</span>
|
||||
</Badge>
|
||||
)}
|
||||
{chainMetadata?.gasSponsored && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-accent-foreground pointer-events-none flex flex-row items-center h-full gap-1.5 border border-border"
|
||||
{chain.name}
|
||||
</h1>
|
||||
<StarButton chainId={chain.chainId} variant="secondary" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 md:gap-4 items-center h-8 mb-4 md:mb-6">
|
||||
{isVerified && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-accent-foreground pointer-events-none flex flex-row items-center h-full gap-1.5 border border-border"
|
||||
>
|
||||
<VerifiedIcon className="size-5" />
|
||||
<span className="font-bold text-xs uppercase">
|
||||
verified
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
{chainMetadata?.gasSponsored && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-accent-foreground pointer-events-none flex flex-row items-center h-full gap-1.5 border border-border"
|
||||
>
|
||||
<TicketCheckIcon className="size-5" />
|
||||
<span className="font-bold text-xs uppercase">
|
||||
gas sponsored
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row md:flex-col gap-4">
|
||||
<AddChainToWallet chainId={chain.chainId} />
|
||||
{isVerified ? null : (
|
||||
<Button
|
||||
className="w-full md:min-w-40"
|
||||
variant="outline"
|
||||
asChild
|
||||
>
|
||||
<TicketCheckIcon className="size-5" />
|
||||
<span className="font-bold text-xs uppercase">
|
||||
gas sponsored
|
||||
</span>
|
||||
</Badge>
|
||||
<Link
|
||||
className="gap-2"
|
||||
href="https://share.hsforms.com/1o01TyfsZRAao2eCrzuXSVgea58c"
|
||||
target="_blank"
|
||||
>
|
||||
<span>Claim this chain</span>
|
||||
<ExternalLinkIcon className="size-3" />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,7 @@ export async function ChainListCard({
|
||||
iconUrl,
|
||||
}: ChainListCardProps) {
|
||||
const chainMetadata = await getChainMetadata(chainId);
|
||||
const isVerified = !!chainMetadata;
|
||||
return (
|
||||
<div className="relative h-full">
|
||||
<Card className="h-full w-full hover:bg-muted">
|
||||
@@ -70,13 +71,11 @@ export async function ChainListCard({
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{(isDeprecated ||
|
||||
chainMetadata?.gasSponsored ||
|
||||
chainMetadata?.verified) && (
|
||||
{(isDeprecated || chainMetadata?.gasSponsored || isVerified) && (
|
||||
<div className="mt-5 flex gap-5 border-t pt-4">
|
||||
{!isDeprecated && (
|
||||
<>
|
||||
{chainMetadata?.verified && (
|
||||
{isVerified && (
|
||||
<div className="gap-1.5 flex items-center">
|
||||
<VerifiedIcon className="text-primary-foreground size-5" />
|
||||
<p className="text-sm">Verified</p>
|
||||
|
||||
@@ -35,6 +35,7 @@ export async function ChainListRow({
|
||||
iconUrl,
|
||||
}: ChainListRowProps) {
|
||||
const chainMetadata = await getChainMetadata(chainId);
|
||||
const isVerified = !!chainMetadata;
|
||||
return (
|
||||
<tr className="border-b relative hover:bg-secondary">
|
||||
<TableData>{favoriteButton}</TableData>
|
||||
@@ -50,7 +51,7 @@ export async function ChainListRow({
|
||||
{chainName}
|
||||
</Link>
|
||||
|
||||
{chainMetadata?.verified && (
|
||||
{isVerified && (
|
||||
<ToolTipLabel label="Verified">
|
||||
<VerifiedIcon className="text-primary-foreground size-5 z-10 " />
|
||||
</ToolTipLabel>
|
||||
|
||||
@@ -45,7 +45,6 @@ type ChainMetadata = Partial<{
|
||||
headerImgUrl: string;
|
||||
about: string;
|
||||
gasSponsored: boolean;
|
||||
verified: boolean;
|
||||
cta: ChainCTAProps;
|
||||
}>;
|
||||
|
||||
@@ -66,7 +65,7 @@ const chainMetaRecord = {
|
||||
headerImgUrl: xaiBanner.src,
|
||||
about:
|
||||
"Xai was developed to enable real economies and open trade in the next generation of video games. With Xai, potentially billions of traditional gamers can own and trade valuable in-game items in their favorite games for the first time, without the need to use crypto-wallets.",
|
||||
verified: true,
|
||||
|
||||
cta: {
|
||||
title: "Unlock ultimate possibility with Xai Connect",
|
||||
backgroundImageUrl: xaiCTABg.src,
|
||||
@@ -80,7 +79,7 @@ const chainMetaRecord = {
|
||||
about:
|
||||
"Base is a secure, low-cost, builder-friendly Ethereum L2 built to bring the next billion users onchain.",
|
||||
gasSponsored: true,
|
||||
verified: true,
|
||||
|
||||
cta: OP_CTA,
|
||||
},
|
||||
// optimism
|
||||
|
||||
Reference in New Issue
Block a user