Files
hanzo.ai/components/blockchain/BlockchainProductLayout.tsx

430 lines
17 KiB
TypeScript

'use client'
/**
* The shared layout every `/blockchain/<product>` page renders through.
*
* The six section frames are `components/marketing/band`'s `Band` and `Head`
* — the rule, the rhythm, the gutter, the measure and the centred head — the
* type and the controls are `@hanzo/ui/marketing`, the tile, the disc
* and a card's lines are `./surface`, and the rest is gui props.
*
* The page's one accent is white, tinted by alpha for a plate, an edge or the
* availability dot, which breathes with @hanzo/ui's `Pulse`.
*/
import React, { useState, type ReactNode } from "react";
import { motion } from "@/components/motion";
import Link from "next/link";
import {
ArrowRight,
LucideIcon,
} from "lucide-react";
import { phrase } from "@hanzo/i18n";
import { useI18n } from "@hanzo/i18n/react";
import { ProductFooter } from "@/components/products/ProductFooter";
import { KEYS } from "@/components/home/nav-data";
import { Box, Pulse, Text, XStack, YStack } from '@hanzo/ui'
import { Grid as Columns } from '@hanzo/ui/grid'
import { Band, Head } from '@/components/marketing/band'
import { Action, Chip, Claim, Display, Lede, More, Title } from '@hanzo/ui/marketing'
import { Card, CardBody, CardTitle, GROUND, SOFT, Tile, white } from './surface'
export interface CodeExample {
language: string;
filename: string;
/** The sample, highlighted where the page is built (components/blockchain/sample). */
code: ReactNode;
}
export interface BlockchainProductProps {
name: string;
tagline: string;
description: string;
icon: LucideIcon;
features: Array<{
icon: LucideIcon;
title: string;
description: string;
}>;
useCases: Array<{
title: string;
description: string;
}>;
chains?: string[];
codeExamples?: CodeExample[];
/**
* Product slug for the canonical OSS + Deploy footer — the BARE slug, matching
* the key in products-metadata and this page's own directory (`nft`, not
* `blockchain/nft`; the page composes the prefix itself).
*
* REQUIRED, and it used to default to `name`'s last word lowercased. That
* derivation read a marketing headline as a product identity, and the two are
* different values that merely coincide most of the time:
*
* "Hanzo DeFi" -> defi -> /docs/defi 404s, never written
* "Gas Manager" -> manager -> /docs/manager 404s; there is no such product
* "Hanzo NFT API"-> api -> /docs/api EXISTS, so four products
* "Hanzo Tokens API" -> api (nft, tokens, bundler, transfers) all
* ... silently addressed generic API docs
*
* The last case is the reason this is required rather than merely discouraged:
* `nft` and `tokens` were declared in products-metadata with the correct
* `docs_slug`, and the derivation routed around their own curated metadata.
* A wrong 200 reports nothing and outlives every link check.
*/
slug: string;
}
/** How narrow a column may get before a grid drops one, per shape. */
const PAIR = { min: 400, max: 2 } as const
const DECK = { min: 304, max: 3 } as const
const DUO = { min: 320, max: 2 } as const
/** The page's one accent: white, tinted by alpha for a plate, an edge or a glow. */
const ACCENT = 'var(--pure-white)'
/** The two strengths of the secondary ground the panels sit on. */
const ground = (pct: number) => ({ backgroundColor: `color-mix(in oklab, var(--secondary) ${pct}%, transparent)` })
/** The plate a mark sits on, in the accent at 10%. */
function Plate({ size, radius, mb, children }: { size: number; radius: 'var(--radius-lg)' | 'var(--radius-xl)'; mb?: number; children: ReactNode }) {
return (
<Tile size={size} rounded={radius} mb={mb}>
{children}
</Tile>
)
}
/** A window's three lamps. */
function Lamps({ size }: { size: number }) {
return (
<XStack gap={6}>
{[0, 1, 2].map((k) => (
<Box key={k} width={size} height={size} rounded={9999} $platform-web={{ backgroundColor: white(10) }} />
))}
</XStack>
)
}
/** A file's name in the frame's head. */
function File({ children }: { children: ReactNode }) {
return (
<Text ml={8} fontFamily="$mono" fontSize="$1" lineHeight="0.9167rem" color="var(--muted-foreground)">
{children}
</Text>
)
}
/** The highlighted source in a frame, scrolling rather than wrapping a line. */
function Source({ max, children }: { max?: number; children: ReactNode }) {
return (
<Box overflowX="auto" overflowY="auto" maxH={max}>
{children}
</Box>
)
}
/** The frame a code sample sits in. */
function Frame({ children }: { children: ReactNode }) {
return (
<Box overflow="hidden" rounded="var(--radius-xl)" borderWidth={1} borderColor="var(--border)" $platform-web={ground(80)}>
{children}
</Box>
)
}
const BlockchainProductLayout: React.FC<BlockchainProductProps> = ({
name,
tagline,
description,
icon: ProductIcon,
features,
useCases,
chains,
codeExamples,
slug,
}) => {
const [activeTab, setActiveTab] = useState(0);
const { t } = useI18n();
return (
<Box minH="100vh" $platform-web={{ color: 'var(--foreground)' }}>
{/* Hero Section */}
<Box render="section" position="relative" overflow="hidden" pt={96} pb={64} px={16} $md={{ px: 32 }}>
<Box position="relative" z="var(--z-raised)" width="100%" maxW={1152} mx="auto">
<Columns columns={PAIR} gap={48} items="flex-start">
{/* Left: Product Info */}
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }}>
{/* Available Badge */}
<Chip mb={24} px={16} py={8} borderColor={white(30)} bg={white(10)}>
<Pulse render="span" width={8} height={8} rounded={9999} bg={ACCENT} />
<Text fontSize="$2" lineHeight="1.1607rem" fontWeight="500" color={ACCENT}>
{t('Available Now')}
</Text>
</Chip>
<Display mb={16}>{t(name)}</Display>
<Claim mb={24} color="var(--muted-foreground)">
{t(tagline)}
</Claim>
<Lede mb={32}>{t(description)}</Lede>
{/* Quick Features */}
<Box mb={32}>
<Columns columns={{ min: 240, max: 2 }} gap={16}>
{features.slice(0, 4).map((feature, idx) => {
const Icon = feature.icon;
return (
<XStack key={idx} items="flex-start" gap={12}>
<Plate size={32} radius="var(--radius-lg)">
<Icon width={16} height={16} color={ACCENT} />
</Plate>
<Box>
<Text render="h3" display="block" fontSize="$2" lineHeight="1.1607rem" fontWeight="500" color="var(--foreground)">
{t(feature.title)}
</Text>
<Text render="p" display="block" fontSize="$1" lineHeight="0.9167rem" color="var(--muted-foreground)">
{t(feature.description)}
</Text>
</Box>
</XStack>
);
})}
</Columns>
</Box>
<More href="/blockchain">
<XStack rotate="180deg">
<ArrowRight width={16} height={16} data-i18n-mirror />
</XStack>
{t('Back to Web3 Overview')}
</More>
</motion.div>
{/* Right: Signup Form */}
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.1 }}>
<Box p={24} $md={{ p: 32 }} rounded="var(--radius-2xl)" borderWidth={1} borderColor="var(--border)" $platform-web={ground(80)}>
{/* Quick Start */}
<XStack items="center" gap={12} mb={24}>
<Plate size={48} radius="var(--radius-xl)">
<ProductIcon width={24} height={24} color={ACCENT} />
</Plate>
<Box>
<Text render="h2" display="block" fontSize="$4" lineHeight="1.4583rem" fontWeight="600" color="var(--foreground)">
{t('Get Your API Key')}
</Text>
<Text render="p" display="block" fontSize="$2" lineHeight="1.1607rem" color="var(--muted-foreground)">
{t('Start building in under {n} minutes', { n: 5 })}
</Text>
</Box>
</XStack>
{/* Performance Stats */}
<Box mb={24}>
<Columns columns={3} gap={16}>
{[
{ value: "HA", label: phrase("Failover") },
{ value: "<50ms", label: phrase("Latency") },
{ value: "100+", label: phrase("Chains") },
].map((stat) => (
<Box key={stat.label} $platform-web={{ textAlign: 'center' }}>
<Text display="block" fontSize="$6" lineHeight="1.4875rem" fontWeight="700" color="var(--foreground)">
{stat.value}
</Text>
<Text display="block" fontSize="$1" lineHeight="0.9167rem" color="var(--muted-foreground)">
{t(stat.label)}
</Text>
</Box>
))}
</Columns>
</Box>
{/* CTA Buttons */}
<YStack gap={12}>
<Action render="button" type="button" onClick={() => window.location.assign('/login')} fill width="100%" py={12}>
{t('Try Hanzo')}
<ArrowRight width={16} height={16} data-i18n-mirror />
</Action>
<Action
render="button" type="button" onClick={() => window.open('https://docs.hanzo.ai/docs/network', '_blank')}
width="100%"
py={12}
borderColor="var(--border)"
>
{t('View Documentation')}
</Action>
</YStack>
<Text render="p" display="block" mt={16} text="center" fontSize="$1" lineHeight="0.9167rem" color="var(--muted-foreground)">
{t('No credit card required.')}
</Text>
{/* Trust Signals */}
<Box mt={24} pt={24} borderTopWidth={1} borderColor="var(--border)">
<Text render="p" display="block" mb={12} fontSize="$1" lineHeight="0.9167rem" color="var(--muted-foreground)">
{t('Trusted by developers building:')}
</Text>
<XStack flexWrap="wrap" gap={8}>
{["DeFi", "NFTs", phrase("Payments"), phrase("Gaming"), phrase("AI Agents")].map((tag) => (
<Chip key={tag} px={8} py={4} fontSize="$1" lineHeight="0.9167rem" borderColor="var(--border)" color="var(--muted-foreground)">
{t(tag)}
</Chip>
))}
</XStack>
</Box>
</Box>
</motion.div>
</Columns>
</Box>
</Box>
{/* Features Section */}
<Band rule={false} measure={1152} ground="var(--background)">
<Head title={t('Key Capabilities')} lede={t("Everything you need, nothing you don't.")} />
<Columns columns={DECK} gap={24}>
{features.map((feature, index) => {
const Icon = feature.icon;
return (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: index * 0.05 }}
>
<Card height="100%" p={24}>
<Plate size={40} radius="var(--radius-lg)" mb={16}>
<Icon width={20} height={20} color={ACCENT} />
</Plate>
<CardTitle mb={8} color="var(--foreground)">
{t(feature.title)}
</CardTitle>
<CardBody>{t(feature.description)}</CardBody>
</Card>
</motion.div>
);
})}
</Columns>
</Band>
{/* Code Examples - Polyglot */}
{(codeExamples && codeExamples.length > 0) && (
<Band rule={false} measure={896}>
<Head title={t('Simple to Integrate')} lede={t('Get started with just a few lines of code. SDKs for every language.')} />
<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}>
<Frame>
{/* Language Tabs */}
<XStack items="center" gap={4} px={16} py={8} borderBottomWidth={1} borderColor="var(--border)" bg="var(--background)" overflowX="auto">
{codeExamples.map((example, idx) => {
const on = activeTab === idx
return (
<More
key={example.language}
render="button" onClick={() => setActiveTab(idx)}
px={12}
py={6}
rounded={4}
fontSize="$1"
lineHeight="0.9821rem"
bg={on ? 'var(--neutral-800)' : undefined}
color={on ? 'var(--foreground)' : undefined}
hoverStyle={on ? { color: 'var(--foreground)' } : undefined}
$theme-light={on ? { color: 'var(--foreground)' } : { color: 'var(--muted-foreground)', hoverStyle: { color: 'var(--foreground)' } }}
$touchable={undefined}
>
{example.language}
</More>
)
})}
</XStack>
{/* File Header */}
<XStack items="center" gap={8} px={16} py={8} borderBottomWidth={1} borderColor="var(--border)" bg="$hover">
<Lamps size={10} />
<File>{codeExamples[activeTab]?.filename}</File>
</XStack>
<Source max={500}>{codeExamples[activeTab]?.code}</Source>
</Frame>
</motion.div>
</Band>
)}
{/* Use Cases */}
<Band rule={false} measure={1152} ground="var(--background)">
<Head title={t('Built For')} />
<Columns columns={DUO} gap={24}>
{useCases.map((useCase, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: index * 0.1 }}
>
<Card height="100%" p={24} $platform-web={ground(30)}>
<Text render="h3" display="block" mb={8} fontSize="$4" lineHeight="1.4583rem" fontWeight="600" color="var(--foreground)">
{t(useCase.title)}
</Text>
<Text render="p" display="block" fontSize="$3" lineHeight="1.25rem" color="var(--muted-foreground)">
{t(useCase.description)}
</Text>
</Card>
</motion.div>
))}
</Columns>
</Band>
{/* Supported Chains */}
{chains && chains.length > 0 && (
<Band rule={false} measure={1152}>
<Head title={t('Supported Chains')} />
<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}>
<XStack flexWrap="wrap" justify="center" gap={12}>
{chains.map((chain) => (
<Chip
key={chain}
px={16}
py={8} borderColor="var(--border)" bg={GROUND} color={SOFT}
>
{chain}
</Chip>
))}
</XStack>
</motion.div>
</Band>
)}
{/* Bottom CTA */}
<Band measure={896}>
<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}>
<Box $platform-web={{ textAlign: 'center' }}>
<Title quiet mb={16} color="var(--foreground)">
{t('Start Building with {name}', { name: t(name) })}
</Title>
<Lede mb={32}>{t('Get your free API key and ship your first request in under {n} minutes. No credit card required.', { n: 5 })}</Lede>
<XStack flexWrap="wrap" items="center" justify="center" gap={16}>
<Action href={KEYS} fill>
{t('Get Your API Key')}
<ArrowRight width={16} height={16} data-i18n-mirror />
</Action>
<Action href="/blockchain" borderColor="var(--border)">
{t('Explore All Web3 Products')}
</Action>
</XStack>
</Box>
</motion.div>
</Band>
{/* Canonical OSS + Deploy footer — one block for every product page. */}
<ProductFooter slug={slug} name={t(name)} />
</Box>
);
};
export default BlockchainProductLayout;