Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import "server-only";
import { unstable_cache } from "next/cache";

export type ChainSeo = {
title: string;
description: string;
og: {
title: string;
description: string;
site_name: string;
url: string;
};
faqs: Array<{
title: string;
description: string;
}>;
chain: {
chainId: number;
name: string;
slug: string;
nativeCurrency: {
name: string;
symbol: string;
decimals: number;
};
testnet: boolean;
is_deprecated: boolean;
};
};

export const fetchChainSeo = unstable_cache(
async (chainId: number) => {
const url = new URL(
`https://seo-pages-generator-5814.zeet-nftlabs.zeet.app/chain/${chainId}`,
);
const res = await fetch(url, {
headers: {
"Content-Type": "application/json",
},
});

if (!res.ok) {
return undefined;
}

return res.json() as Promise<ChainSeo>;
},
["chain-seo"],
{ revalidate: 60 * 60 * 24 }, // 24 hours
);
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function ChainOverviewSection(props: { chain: ChainMetadata }) {
return (
<section>
<SectionTitle title="Chain Overview" />
<div className="grid grid-cols-1 gap-6 rounded-lg border bg-card p-4 md:grid-cols-2 md:p-6 lg:grid-cols-3 lg:gap-8">
<div className="grid grid-cols-1 gap-6 rounded-xl border bg-card p-4 md:grid-cols-2 md:p-6 lg:grid-cols-3 lg:gap-6">
{/* Info */}
{chain.infoURL && (
<PrimaryInfoItem title="Info">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { CircleCheckIcon } from "lucide-react";
import Link from "next/link";
import type { ChainMetadataWithServices } from "@/types/chain";
import { products } from "../../../../components/server/products";
Expand All @@ -24,15 +23,18 @@ export function SupportedProductsSection(props: {
{enabledProducts.map((product) => {
return (
<div
className="relative flex gap-3 rounded-lg border bg-card p-4 pr-8 transition-colors hover:border-active-border"
className="relative rounded-xl border bg-card p-4 hover:border-active-border"
key={product.id}
>
<CircleCheckIcon className="absolute top-4 right-4 size-5 text-success-text" />
<product.icon className="mt-0.5 size-5 shrink-0" />
<div className="flex mb-4">
<div className="p-2 rounded-full border bg-background">
<product.icon className="size-4 text-muted-foreground" />
</div>
</div>
<div>
<h3 className="mb-1.5 font-medium">
<h3 className="mb-1">
<Link
className="before:absolute before:inset-0"
className="before:absolute before:inset-0 text-base font-medium"
href={product.link}
rel="noopener noreferrer"
target="_blank"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function ExplorersSection(props: {
key={explorer.url}
>
<ExternalLinkIcon className="absolute top-4 right-4 size-4 text-muted-foreground" />
<h3 className="mb-1 font-semibold text-base capitalize">
<h3 className="mb-1 font-medium text-base capitalize">
{explorer.name}
</h3>
<Link
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"use client";
import { ChevronDownIcon } from "lucide-react";
import { useId, useState } from "react";
import { Button } from "@/components/ui/button";
import { DynamicHeight } from "@/components/ui/DynamicHeight";
import { cn } from "@/lib/utils";
import type { ChainSeo } from "../../apis/chain-seo";

export function FaqSection(props: { faqs: ChainSeo["faqs"] }) {
return (
<div className="py-10">
<section className="">
<h2 className="text-2xl md:text-3xl font-semibold mb-4 tracking-tight">
Frequently asked questions
</h2>
<div className="flex flex-col">
{props.faqs.map((faq, faqIndex) => (
<FaqItem
key={faq.title}
title={faq.title}
description={faq.description}
className={cn(faqIndex === props.faqs.length - 1 && "border-b-0")}
/>
))}
</div>
</section>
</div>
);
}

function FaqItem(props: {
title: string;
description: string;
className?: string;
}) {
const [isOpen, setIsOpenn] = useState(false);
const contentId = useId();
return (
<DynamicHeight>
<div className={cn("border-b border-dashed", props.className)}>
<h3>
<Button
variant="ghost"
onClick={() => setIsOpenn(!isOpen)}
aria-controls={contentId}
aria-expanded={isOpen}
className={cn(
"w-full justify-between h-auto py-5 text-base text-muted-foreground hover:bg-transparent pl-0 text-wrap text-left gap-6",
isOpen && "text-foreground",
)}
>
{props.title}
<ChevronDownIcon
className={cn(
"size-4 shrink-0 transition-transform duration-200",
isOpen && "rotate-180",
)}
/>
</Button>
</h3>

<p
className={cn(
"text-muted-foreground text-base leading-7 pb-6 max-w-4xl",
!isOpen && "hidden",
)}
id={contentId}
>
{props.description}
</p>
</div>
</DynamicHeight>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ export function PrimaryInfoItem(props: {
return (
<div>
<div className="flex items-center gap-2">
<h3 className="font-medium text-base text-muted-foreground">
{props.title}
</h3>
<h3 className="text-base text-muted-foreground">{props.title}</h3>
{props.titleIcon}
</div>
{props.children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,39 +22,51 @@ import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
import { TeamHeader } from "../../../../team/components/TeamHeader/team-header";
import { StarButton } from "../../components/client/star-button";
import { getChain, getChainMetadata } from "../../utils";
import { getChain, getCustomChainMetadata } from "../../utils";
import { fetchChainSeo } from "./apis/chain-seo";
import { AddChainToWallet } from "./components/client/add-chain-to-wallet";
import { ChainPageView } from "./components/client/chain-pageview";
import { ChainHeader } from "./components/server/chain-header";

// TODO: improve the behavior when clicking "Get started with thirdweb", currently just redirects to the dashboard
type Params = Promise<{ chain_id: string }>;

export async function generateMetadata(props: {
params: Promise<{ chain_id: string }>;
}): Promise<Metadata> {
params: Params;
}): Promise<Metadata | undefined> {
const params = await props.params;
const chain = await getChain(params.chain_id);
const sanitizedChainName = chain.name.replace("Mainnet", "").trim();
const title = `${sanitizedChainName}: RPC and Chain Settings`;
const chainSeo = await fetchChainSeo(Number(chain.chainId)).catch(
() => undefined,
);

const description = `Use the best ${sanitizedChainName} RPC and add to your wallet. Discover the chain ID, native token, explorers, and ${
chain.testnet && chain.faucets?.length ? "faucet options" : "more"
}.`;
if (!chainSeo) {
return undefined;
}

return {
description,
title: chainSeo.title,
description: chainSeo.description,
metadataBase: new URL("https://thirdweb.com"),
openGraph: {
description,
title,
title: chainSeo.og.title,
description: chainSeo.og.description,
siteName: "thirdweb",
type: "website",
url: "https://thirdweb.com",
},
twitter: {
title: chainSeo.og.title,
description: chainSeo.og.description,
card: "summary_large_image",
creator: "@thirdweb",
site: "@thirdweb",
},
title,
};
}

// this is the dashboard layout file
export default async function ChainPageLayout(props: {
children: React.ReactNode;
params: Promise<{ chain_id: string }>;
params: Params;
}) {
const params = await props.params;
const { children } = props;
Expand All @@ -67,8 +79,10 @@ export default async function ChainPageLayout(props: {
redirect(chain.slug);
}

const chainMetadata = await getChainMetadata(chain.chainId);
const customChainMetadata = getCustomChainMetadata(chain.chainId);
const chainSeo = await fetchChainSeo(chain.chainId);
const client = getClientThirdwebClient(undefined);
const description = customChainMetadata?.about || chainSeo?.description;

return (
<div className="flex grow flex-col">
Expand Down Expand Up @@ -123,15 +137,15 @@ export default async function ChainPageLayout(props: {
<ChainHeader
chain={chain}
client={client}
headerImageUrl={chainMetadata?.headerImgUrl}
headerImageUrl={customChainMetadata?.headerImgUrl}
logoUrl={chain.icon?.url}
/>

<div className="h-4 md:h-8" />

<div className="flex flex-col gap-3 md:gap-2">
{/* Gas Sponsored badge - Mobile */}
{chainMetadata?.gasSponsored && (
{customChainMetadata?.gasSponsored && (
<div className="flex md:hidden">
<GasSponsoredBadge />
</div>
Expand All @@ -153,17 +167,17 @@ export default async function ChainPageLayout(props: {
)}

{/* Gas Sponsored badge - Desktop */}
{chainMetadata?.gasSponsored && (
{customChainMetadata?.gasSponsored && (
<div className="hidden md:block">
<GasSponsoredBadge />
</div>
)}
</div>

{/* description */}
{chainMetadata?.about && (
<p className="mb-2 whitespace-pre-line text-muted-foreground text-sm lg:text-base">
{chainMetadata.about}
{description && (
<p className="mb-2 whitespace-pre-line text-muted-foreground text-sm lg:text-base text-pretty max-w-3xl">
{description}
</p>
)}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
import { CircleAlertIcon } from "lucide-react";
import { getRawAccount } from "@/api/account/get-account";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getChain, getChainMetadata } from "../../utils";
import { getChain, getCustomChainMetadata } from "../../utils";
import { fetchChainSeo } from "./apis/chain-seo";
import { BuyFundsSection } from "./components/client/BuyFundsSection";
import { ChainOverviewSection } from "./components/server/ChainOverviewSection";
import { ChainCTA } from "./components/server/cta-card";
import { ExplorersSection } from "./components/server/explorer-section";
import { FaucetSection } from "./components/server/FaucetSection";
import { FaqSection } from "./components/server/faq-section";
import { SupportedProductsSection } from "./components/server/SupportedProductsSection";

export default async function Page(props: {
type Props = {
params: Promise<{ chain_id: string }>;
}) {
};

export default async function Page(props: Props) {
const params = await props.params;
const chain = await getChain(params.chain_id);
const chainMetadata = await getChainMetadata(chain.chainId);
const customChainMetadata = getCustomChainMetadata(Number(params.chain_id));
const chainSeo = await fetchChainSeo(Number(chain.chainId)).catch(
() => undefined,
);
const client = getClientThirdwebClient();
const isDeprecated = chain.status === "deprecated";

const account = await getRawAccount();

return (
<div className="flex flex-col gap-10">
{/* Custom CTA */}
{(chainMetadata?.cta?.title || chainMetadata?.cta?.description) && (
<ChainCTA {...chainMetadata.cta} />
{(customChainMetadata?.cta?.title ||
customChainMetadata?.cta?.description) && (
<ChainCTA {...customChainMetadata.cta} />
)}

{/* Deprecated Alert */}
Expand Down Expand Up @@ -58,6 +65,10 @@ export default async function Page(props: {
{chain.services.filter((s) => s.enabled).length > 0 && (
<SupportedProductsSection services={chain.services} />
)}

{chainSeo?.faqs && chainSeo.faqs.length > 0 && (
<FaqSection faqs={chainSeo.faqs} />
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { ChainSupportedService } from "@/types/chain";
import { ChainIcon } from "../../../components/server/chain-icon";
import { getChainMetadata } from "../../../utils";
import { getCustomChainMetadata } from "../../../utils";

type ChainListCardProps = {
favoriteButton: JSX.Element | undefined;
Expand All @@ -18,7 +18,7 @@ type ChainListCardProps = {
iconUrl?: string;
};

export async function ChainListCard({
export function ChainListCard({
isDeprecated,
chainId,
chainName,
Expand All @@ -28,7 +28,7 @@ export async function ChainListCard({
favoriteButton,
iconUrl,
}: ChainListCardProps) {
const chainMetadata = await getChainMetadata(chainId);
const customChainMetadata = getCustomChainMetadata(chainId);

return (
<div className="relative h-full">
Expand Down Expand Up @@ -90,9 +90,9 @@ export async function ChainListCard({
</tbody>
</table>

{(isDeprecated || chainMetadata?.gasSponsored) && (
{(isDeprecated || customChainMetadata?.gasSponsored) && (
<div className="mt-5 flex gap-5 border-t pt-4">
{!isDeprecated && chainMetadata?.gasSponsored && (
{!isDeprecated && customChainMetadata?.gasSponsored && (
<div className="flex items-center gap-1.5">
<TicketCheckIcon className="size-5 text-foreground" />
<p className="text-sm">Gas Sponsored</p>
Expand Down
Loading
Loading