mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
BlockchainProductLayout draws every field it is handed with t(), and the fifteen product views mark their copy with phrase() where it is written. The index and the chain directory wrap their own words; chain names, code samples and figures stay as they are. The back arrow and the call-to-action arrows mirror under dir="rtl", and the two disclosure heads align to the start instead of the left.
430 lines
17 KiB
TypeScript
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. Free tier includes {units} compute units/month.', { units: '300M' })}
|
|
</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;
|