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:
jnsdls
2024-06-14 17:30:48 +00:00
parent 9fcd00ae08
commit 4112dcf4cc
5 changed files with 136 additions and 57 deletions
@@ -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>
+2 -3
View File
@@ -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