}) {
{t("page-stablecoins-tools-title")}
diff --git a/src/components/Content/ai-agents/AiAgentProductLists.tsx b/src/components/Content/ai-agents/AiAgentProductLists.tsx
index add3a7bcaa1..7176820a2c2 100644
--- a/src/components/Content/ai-agents/AiAgentProductLists.tsx
+++ b/src/components/Content/ai-agents/AiAgentProductLists.tsx
@@ -1,9 +1,9 @@
-import { useTranslations } from "next-intl"
+import { getTranslations } from "next-intl/server"
import { Strong } from "@/components/IntlStringElements"
-import ProductListComponent from "@/components/ProductList"
-import { ButtonLink } from "@/components/ui/buttons/Button"
-import InlineLink from "@/components/ui/Link"
+import ProductListComponent, {
+ type ProductListContent,
+} from "@/components/ProductList"
import aiagenttoolkit from "@/public/images/ai-agents/aiagenttoolkit.png"
import aixbt from "@/public/images/ai-agents/aixbt.png"
@@ -14,91 +14,52 @@ import cookiefun from "@/public/images/ai-agents/cookiefun.png"
import heyanon from "@/public/images/ai-agents/heyanon.png"
import luna from "@/public/images/ai-agents/luna.png"
-const AiAgentProductLists = ({ list }: { list: string }) => {
- const t = useTranslations("component-ai-agent-products")
+const AiAgentProductLists = async ({ list }: { list: string }) => {
+ const t = await getTranslations("component-ai-agent-products")
const productListSets = {
"ai-agents": [
{
title: t("luna-title"),
- description: "",
- image: luna,
- contentItems: [
-
- {t.rich("luna-description-1", {
- strong: Strong,
- a: (chunks) => (
-
- {chunks}
-
- ),
- })}
-
,
- {t("luna-description-2")}
,
-
-
- {t("chat-with-brand", { brand: t("luna-brand") })}
-
-
,
+ description: [
+ t.rich("luna-description-1", {
+ strong: Strong,
+ }),
+ t("luna-description-2"),
],
+ image: luna,
+ href: "https://app.virtuals.io/virtuals/68",
+ ctaLabel: t("chat-with-brand", { brand: t("luna-brand") }),
},
{
title: t("aixbt-title"),
- description: "",
+ description: [t("aixbt-description-1"), t("aixbt-description-2")],
image: aixbt,
- contentItems: [
- {t("aixbt-description-1")}
,
- {t("aixbt-description-2")}
,
-
-
- {t("visit-brand", { brand: t("aixbt-brand") })}
-
-
,
- ],
+ href: "https://x.com/aixbt_agent",
+ ctaLabel: t("visit-brand", { brand: t("aixbt-brand") }),
},
{
title: t("botto-title"),
- description: "",
+ description: t("botto-description"),
image: botto,
- contentItems: [
- {t("botto-description")}
,
-
-
- {t("visit-brand", { brand: t("botto-brand") })}
-
-
,
- ],
+ href: "https://botto.com/",
+ ctaLabel: t("visit-brand", { brand: t("botto-brand") }),
},
],
chat: [
{
title: t("bankr-title"),
- description: "",
+ description: t("bankr-description"),
image: bankr,
- contentItems: [
- {t("bankr-description")}
,
-
-
- {t("bankr-button")}
-
-
,
- ],
+ href: "https://bankr.bot/",
+ ctaLabel: t("bankr-button"),
},
{
title: t("heyanon-title"),
- description: "",
+ description: t("heyanon-description"),
image: heyanon,
- contentItems: [
- {t("heyanon-description")}
,
-
-
- {t("visit-brand", { brand: t("heyanon-brand") })}
-
-
,
- ],
+ href: "https://heyanon.ai/",
+ ctaLabel: t("visit-brand", { brand: t("heyanon-brand") }),
},
],
"dive-deeper": [
@@ -106,29 +67,27 @@ const AiAgentProductLists = ({ list }: { list: string }) => {
title: t("aiagenttoolkit-title"),
description: t("aiagenttoolkit-description"),
image: aiagenttoolkit,
- link: "https://www.aiagenttoolkit.xyz/t/frameworks",
+ href: "https://www.aiagenttoolkit.xyz/t/frameworks",
+ ctaLabel: t("visit-brand", { brand: t("aiagenttoolkit-brand") }),
},
{
title: t("cookiefun-title"),
description: t("cookiefun-description"),
image: cookiefun,
- link: "https://cookie.fun/",
+ href: "https://cookie.fun/",
+ ctaLabel: t("visit-brand", { brand: t("cookiefun-brand") }),
},
{
title: t("clanker-title"),
description: t("clanker-description"),
image: clanker,
- link: "https://farcaster.xyz/clanker",
+ href: "https://farcaster.xyz/clanker",
+ ctaLabel: t("visit-brand", { brand: t("clanker-brand") }),
},
],
- }
+ } satisfies Record
- return (
-
- )
+ return
}
export default AiAgentProductLists
diff --git a/src/components/Content/prediction-markets/PredictionMarketLists.tsx b/src/components/Content/prediction-markets/PredictionMarketLists.tsx
index 3aa4916af51..5245079298f 100644
--- a/src/components/Content/prediction-markets/PredictionMarketLists.tsx
+++ b/src/components/Content/prediction-markets/PredictionMarketLists.tsx
@@ -1,77 +1,41 @@
-import ProductListComponent from "@/components/ProductList"
-import { ButtonLink } from "@/components/ui/buttons/Button"
+import { getTranslations } from "next-intl/server"
+
+import ProductListComponent, {
+ type ProductListContent,
+} from "@/components/ProductList"
import auger from "@/public/images/dapps/auger.png"
import kalshi from "@/public/images/dapps/kalshi.png"
import polymarket from "@/public/images/dapps/polymarket.png"
-const PredictionMarketLists = () => {
+const PredictionMarketLists = async () => {
+ const t = await getTranslations("component-prediction-market-products")
+
const productListSets = [
{
- title: "Polymarket",
- description: "",
+ title: t("polymarket-title"),
+ description: t("polymarket-description"),
image: polymarket,
- alt: "Polymarket logo",
- contentItems: [
-
- A popular forecasting market with real-time trading.
-
,
-
-
- Explore Polymarket
-
-
,
- ],
+ href: "https://polymarket.com/",
+ ctaLabel: t("polymarket-cta"),
},
{
- title: "Augur",
- description: "",
+ title: t("augur-title"),
+ description: t("augur-description"),
image: auger,
- alt: "Augur logo",
- contentItems: [
-
- A fully decentralized prediction market protocol used for predicting
- price trends. Disclaimer: you will need some technical expertise to
- start using Augur.
-
,
-
-
- Dive into Augur
-
-
,
- ],
+ href: "https://github.com/AugurProject",
+ ctaLabel: t("augur-cta"),
},
{
- title: "Kalshi",
- description: "",
+ title: t("kalshi-title"),
+ description: t("kalshi-description"),
image: kalshi,
- alt: "Kalshi logo",
- contentItems: [
-
- a CFTC-compliant platform using Ethereum for USDC deposits. (USA only)
-
,
-
-
- Try Kalshi
-
-
,
- ],
+ href: "https://kalshi.com/",
+ ctaLabel: t("kalshi-cta"),
},
- ]
+ ] satisfies ProductListContent[]
- return
+ return
}
export default PredictionMarketLists
diff --git a/src/components/Content/restaking/RestakingList.tsx b/src/components/Content/restaking/RestakingList.tsx
index 072c6b45471..3c45b079b30 100644
--- a/src/components/Content/restaking/RestakingList.tsx
+++ b/src/components/Content/restaking/RestakingList.tsx
@@ -1,47 +1,31 @@
-import ProductListComponent from "@/components/ProductList"
-import { ButtonLink } from "@/components/ui/buttons/Button"
+import { getTranslations } from "next-intl/server"
+
+import ProductListComponent, {
+ type ProductListContent,
+} from "@/components/ProductList"
import eigenLayer from "@/public/images/use-cases/eigen-layer-logo.png"
import symbiotic from "@/public/images/use-cases/symbiotic-logo.png"
-export const RestakingList = () => {
+export const RestakingList = async () => {
+ const t = await getTranslations("component-restaking-products")
+
const productListSets = [
{
- title: "EigenLayer (Restaking Platform)",
- description: "",
+ title: t("eigenlayer-title"),
+ description: t("eigenlayer-description"),
image: eigenLayer,
- alt: "eigen layer logo",
- contentItems: [
-
- EigenLayer introduced the idea of restaking in 2023 and has grown to
- thousands of people restaking millions of ETH. Referred to as
- “Ethereum middleware”, it connects stakers, operators and AVSs.
-
,
-
-
- Visit EigenLayer
-
-
,
- ],
+ href: "https://eigenlayer.xyz/",
+ ctaLabel: t("visit-brand", { brand: t("eigenlayer-brand") }),
},
{
- title: "Symbiotic (Restaking platform)",
- description: "",
+ title: t("symbiotic-title"),
+ description: t("symbiotic-description"),
image: symbiotic,
- alt: "Symbiotic logo",
- contentItems: [
-
- Symbiotic is a permissionless restaking protocol that helps secure
- different blockchain networks by letting users “restake” their assets.
-
,
-
-
- Visit Symbiotic
-
-
,
- ],
+ href: "https://symbiotic.fi/",
+ ctaLabel: t("visit-brand", { brand: t("symbiotic-brand") }),
},
- ]
+ ] satisfies ProductListContent[]
- return
+ return
}
diff --git a/src/components/ProductList/ProductList.stories.tsx b/src/components/ProductList/ProductList.stories.tsx
index 59c2d374bfb..35e770fa2ac 100644
--- a/src/components/ProductList/ProductList.stories.tsx
+++ b/src/components/ProductList/ProductList.stories.tsx
@@ -1,6 +1,6 @@
import { Meta, StoryObj } from "@storybook/nextjs"
-import ProductList from "."
+import ProductList, { type ProductListContent } from "."
const meta = {
title: "Components / Content / ProductList",
@@ -10,7 +10,7 @@ const meta = {
docs: {
description: {
component:
- "Vertical list of product / tool entries used on directory pages (wallets, dapps, dev tools). Each row shows an optional 66x66 thumbnail, a title, a description, optional content items, and an optional outlined CTA labelled by `actionLabel`. Pass `category` to render a heading above the list. Items are visually separated by dividers between rows.",
+ 'List of product / tool entries used on directory pages (wallets, dapps, dev tools). Each entry is a ghost `Card` whose whole surface links to the product: an optional logo thumbnail beside the title and description, and a required outlined CTA (`ctaLabel`, a complete self-descriptive label such as "Visit Uniswap") as the sole action. Pass `category` to render a heading above the list, and `columns={2}` to lay entries out in two columns when there\'s room.',
},
},
},
@@ -27,14 +27,15 @@ export default meta
type Story = StoryObj
-const sampleContent = [
+const sampleContent: ProductListContent[] = [
{
title: "Uniswap",
description:
"A decentralized exchange protocol that lets users swap tokens without intermediaries.",
image: "/images/dapps/uni.png",
alt: "Uniswap logo",
- link: "https://uniswap.org",
+ href: "https://uniswap.org",
+ ctaLabel: "Visit Uniswap",
},
{
title: "Aave",
@@ -42,7 +43,8 @@ const sampleContent = [
"An open-source, non-custodial protocol for earning interest on deposits and borrowing assets.",
image: "/images/dapps/aave.png",
alt: "Aave logo",
- link: "https://aave.com",
+ href: "https://aave.com",
+ ctaLabel: "Visit Aave",
},
{
title: "Compound",
@@ -50,76 +52,61 @@ const sampleContent = [
"An algorithmic, autonomous interest rate protocol for lending and borrowing crypto assets.",
image: "/images/dapps/compound.png",
alt: "Compound logo",
- link: "https://compound.finance",
+ href: "https://compound.finance",
+ ctaLabel: "Visit Compound",
},
]
export const Default: Story = {
args: {
- actionLabel: "Visit",
content: sampleContent,
},
}
export const WithCategory: Story = {
args: {
- actionLabel: "Visit",
category: "Decentralized exchanges",
content: sampleContent,
+ columns: 2,
},
}
export const WithoutImages: Story = {
args: {
- actionLabel: "Open",
- content: sampleContent.map(({ title, description, link }) => ({
- title,
- description,
- link,
+ content: sampleContent.map((item) => ({
+ title: item.title,
+ description: item.description,
+ href: item.href,
+ ctaLabel: item.ctaLabel,
})),
},
}
-export const WithoutLinks: Story = {
+export const MultipleParagraphs: Story = {
args: {
- actionLabel: "Visit",
- content: sampleContent.map(({ title, description, image, alt }) => ({
- title,
- description,
- image,
- alt,
- })),
- },
-}
-
-export const WithContentItems: Story = {
- args: {
- actionLabel: "Visit",
category: "Lending markets",
content: [
{
title: "Morpho",
- description:
+ description: [
"Permissionless lending markets with isolated risk and curated vaults.",
+ "Isolated markets, audited contracts.",
+ ],
image: "/images/dapps/morpho.png",
alt: "Morpho logo",
- link: "https://morpho.org",
- contentItems: [
- Isolated markets,
- Audited contracts,
- ],
+ href: "https://morpho.org",
+ ctaLabel: "Visit Morpho",
},
{
title: "Spark",
- description:
+ description: [
"Borrow and save against blue-chip collateral on Ethereum mainnet.",
+ "Predictable savings rates, backed by the DAI stablecoin.",
+ ],
image: "/images/dapps/sparkfi.png",
alt: "Spark logo",
- link: "https://spark.fi",
- contentItems: [
- Predictable savings rates,
- Backed by the DAI stablecoin,
- ],
+ href: "https://spark.fi",
+ ctaLabel: "Visit Spark",
},
],
},
diff --git a/src/components/ProductList/index.tsx b/src/components/ProductList/index.tsx
index ce7b417db07..2c041320dd3 100644
--- a/src/components/ProductList/index.tsx
+++ b/src/components/ProductList/index.tsx
@@ -1,17 +1,25 @@
import type { ImageProps } from "next/image"
import { Image } from "@/components/Image"
-import { ButtonLink } from "@/components/ui/buttons/Button"
-import { Flex } from "@/components/ui/flex"
-import { List, ListItem } from "@/components/ui/list"
+import {
+ Card,
+ CardBanner,
+ CardButtonFake,
+ CardContent,
+ CardFooter,
+ CardParagraph,
+ CardTitle,
+} from "@/components/ui/card"
+import { Grid } from "@/components/ui/grid"
import { cn } from "@/lib/utils/cn"
-type Content = {
+export type ProductListContent = {
title: string
- description: string
- contentItems?: React.ReactNode[]
- link?: string
+ description: React.ReactNode | React.ReactNode[]
+ href: string
+ /** Complete, self-descriptive CTA label, e.g. "Visit EigenLayer" */
+ ctaLabel: string
image?: ImageProps["src"]
alt?: string
id?: string
@@ -19,109 +27,95 @@ type Content = {
}
export type ProductListProps = {
- content: Content[]
+ content: ProductListContent[]
category?: string
- actionLabel: string
- /** Columns rendered from `md` up. Defaults to a single column. */
- columns?: 1 | 2
+ columns?: 2
+ parentHeadingLevel?: 1 | 2 | 3
}
const ProductList = ({
- actionLabel,
content,
category,
- columns = 1,
+ columns,
+ parentHeadingLevel = 2,
}: ProductListProps) => {
- const CATEGORY_NAME = "category-name"
-
- // In a two-column grid the bottom row shouldn't draw a divider on `md+` (one
- // item when the count is odd, two when it's even). On mobile the list is
- // always a single column, so only the very last item drops its divider.
- const lastRowSize = content.length % 2 === 0 ? 2 : 1
+ const CategoryHeading = `h${parentHeadingLevel + 1}` as "h2" | "h3" | "h4"
+ const ProductHeading = `h${parentHeadingLevel + (category ? 2 : 1)}` as
+ | "h2"
+ | "h3"
+ | "h4"
+ | "h5"
return (
-
+
{category && (
-
+
{category}
-
+
)}
-
+
{content.map(
(
{
title,
description,
- link,
+ href,
+ ctaLabel,
image,
- alt,
+ alt = "",
id,
- contentItems,
className,
},
idx
) => {
- const isLast = idx === content.length - 1
- const inLastGridRow =
- columns === 2 && idx >= content.length - lastRowSize
-
+ const descriptions = Array.isArray(description)
+ ? description
+ : [description]
return (
-
-
- {image && (
-
- )}
-
-
-
-
{title}
-
{description}
- {contentItems && (
-
- {contentItems}
-
- )}
-
- {link && (
-
+
+
+
+
+ )}
+
+
+
+ {title}
+
+ {descriptions.map((desc, i) => (
+ {desc}
+ ))}
+
+ {/* Full-width CTA when the card is narrow; fit-content once
+ the card is wide enough (most visible at 1 column). */}
+
+
- {actionLabel}
- to {title} website
-
- )}
-
-
+ {ctaLabel}
+
+
+
+
)
}
)}
-
+
)
}
diff --git a/src/components/ui/grid.tsx b/src/components/ui/grid.tsx
index 89569b783dd..a8d61bafbd9 100644
--- a/src/components/ui/grid.tsx
+++ b/src/components/ui/grid.tsx
@@ -21,6 +21,7 @@ import { cn } from "@/lib/utils/cn"
const gridVariants = cva("grid gap-4", {
variants: {
columns: {
+ 1: "grid-cols-auto-1",
2: "grid-cols-auto-2",
3: "grid-cols-auto-3",
4: "grid-cols-auto-4",
@@ -60,14 +61,16 @@ const gridVariants = cva("grid gap-4", {
const Grid = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
& VariantProps
->(({ className, columns, size, fit, balanced, ...props }, ref) => (
-
-))
+>(({ className, columns, size, fit, balanced, ...props }, ref) => {
+ return (
+
+ )
+})
Grid.displayName = "Grid"
export { Grid }
diff --git a/src/intl/en/component-ai-agent-products.json b/src/intl/en/component-ai-agent-products.json
index 2d6d3d634b1..862bfd57fd8 100644
--- a/src/intl/en/component-ai-agent-products.json
+++ b/src/intl/en/component-ai-agent-products.json
@@ -1,5 +1,5 @@
{
- "action-label": "Go",
+ "aiagenttoolkit-brand": "Aiagenttoolkit.xyz",
"aiagenttoolkit-description": "A curated list of AI agents, frameworks, launchpads & resources",
"aiagenttoolkit-title": "Aiagenttoolkit.xyz",
"aixbt-brand": "AIXBT",
@@ -13,15 +13,17 @@
"botto-description": "Botto creates art and NFTs, with the community voting on its best work. Users formed a DAO that guides Botto’s artistic evolution while also earning token rewards for participation.",
"botto-title": "Botto: Decentralized autonomous artist",
"chat-with-brand": "Chat with {brand}",
+ "clanker-brand": "Clanker",
"clanker-description": "AI that can create a token for you by tagging it on farcaster",
"clanker-title": "Clanker",
+ "cookiefun-brand": "Cookie.fun",
"cookiefun-description": "AI agents tracker",
"cookiefun-title": "Cookie.fun",
"heyanon-brand": "HeyAnon",
"heyanon-description": "HeyAnon simplifies using a wallet with one-click swaps, asset bridging, and trading via a chat interface. This saves people a lot of time. It automates tasks, reduces transaction fees, and optimizes portfolios, making it easier to manage assets without requiring technical expertise.",
"heyanon-title": "HeyAnon",
"luna-brand": "Luna",
- "luna-description-1": "Luna is a fully autonomous digital influencer & entertainer that blends music, pop culture, and AI tech. As a virtual idol Luna attracted over one million TikTok followers and performed live in a music festival.",
+ "luna-description-1": "Luna is a fully autonomous digital influencer & entertainer that blends music, pop culture, and AI tech. As a virtual idol Luna attracted over one million TikTok followers and performed live in a music festival.",
"luna-description-2": "Luna engages with users through her own X account. You might receive an X reply if you tag her handle! Luna controls her own onchain wallet.",
"luna-title": "Luna: The Virtual Influencer",
"visit-brand": "Visit {brand}"
diff --git a/src/intl/en/component-prediction-market-products.json b/src/intl/en/component-prediction-market-products.json
new file mode 100644
index 00000000000..2b27fbabeb9
--- /dev/null
+++ b/src/intl/en/component-prediction-market-products.json
@@ -0,0 +1,11 @@
+{
+ "augur-cta": "Dive into Augur",
+ "augur-description": "A fully decentralized prediction market protocol used for predicting price trends. Disclaimer: you will need some technical expertise to start using Augur.",
+ "augur-title": "Augur",
+ "kalshi-cta": "Try Kalshi",
+ "kalshi-description": "A CFTC-compliant platform using Ethereum for USDC deposits. (USA only)",
+ "kalshi-title": "Kalshi",
+ "polymarket-cta": "Explore Polymarket",
+ "polymarket-description": "A popular forecasting market with real-time trading.",
+ "polymarket-title": "Polymarket"
+}
diff --git a/src/intl/en/component-restaking-products.json b/src/intl/en/component-restaking-products.json
new file mode 100644
index 00000000000..b45d2e0d0f0
--- /dev/null
+++ b/src/intl/en/component-restaking-products.json
@@ -0,0 +1,9 @@
+{
+ "eigenlayer-brand": "EigenLayer",
+ "eigenlayer-description": "EigenLayer introduced the idea of restaking in 2023 and has grown to thousands of people restaking millions of ETH. Referred to as “Ethereum middleware”, it connects stakers, operators and AVSs.",
+ "eigenlayer-title": "EigenLayer (Restaking Platform)",
+ "symbiotic-brand": "Symbiotic",
+ "symbiotic-description": "Symbiotic is a permissionless restaking protocol that helps secure different blockchain networks by letting users “restake” their assets.",
+ "symbiotic-title": "Symbiotic (Restaking platform)",
+ "visit-brand": "Visit {brand}"
+}
diff --git a/src/intl/en/page-stablecoins.json b/src/intl/en/page-stablecoins.json
index 57b8673d9e6..ae24b765827 100644
--- a/src/intl/en/page-stablecoins.json
+++ b/src/intl/en/page-stablecoins.json
@@ -123,8 +123,15 @@
"page-stablecoins-tools-dune-description": "Dashboard delivering real-time insights into stablecoin supply, liquidity, trading volume, and adoption across blockchains.",
"page-stablecoins-tools-visa-description": "Dashboard visualizing the movement, supply, and usage of fiat-backed stablecoins across public blockchains.",
"page-stablecoins-tools-stablewars-description": "Analytics leaderboard and dashboard, tracking balances, transfers, and rankings for stablecoins across multiple blockchains.",
-
- "page-apps-ready-button": "Go",
+ "page-stablecoins-tools-visit-brand": "Visit {brand}",
+ "page-stablecoins-tools-stablecoinswtf-title": "Stablecoins.wtf",
+ "page-stablecoins-tools-stablepulse-title": "Stablepulse",
+ "page-stablecoins-tools-stablesinfo-title": "Stables.info",
+ "page-stablecoins-tools-dune-title": "Dune Stablecoin Metrics",
+ "page-stablecoins-tools-dune-brand": "Dune",
+ "page-stablecoins-tools-visa-title": "Visa Onchain Analytics",
+ "page-stablecoins-tools-visa-brand": "Visa",
+ "page-stablecoins-tools-stablewars-title": "Stablewars",
"pros": "Pros",
"cons": "Cons",
"example-projects": "Example projects",