mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
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.')}
|
|
</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;
|