mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
The owner's spec for a landing page is the models bento's: monochrome, the words left and the art right, no extra prose. The folds carried two to four sentences under each heading, and some a claim, a lede and a list besides. Each now says one line: ProductLanding the tagline where a page states one (the lede still describes it to search), a claim alone where the claim is the line (/payments, /treasury, /zap, the extension), and elsewhere the sentence that says what the thing is. /philosophy's two points, /dns' claim that repeated its title, /calculator's capabilities line and the Spare clauses on /research and /learn go.
141 lines
6.0 KiB
TypeScript
141 lines
6.0 KiB
TypeScript
/**
|
|
* /integrations — every SDK, framework and language Hanzo answers as.
|
|
*
|
|
* NO TAILWIND. This is a SERVER page and stays one. Its own sections take no
|
|
* `Band`/`Head`, because `Head` carries a scroll entrance and they have never
|
|
* had one — giving them motion would be a change to the picture smuggled in as
|
|
* a conversion. The frame is `Box render="section"` with the paddings the page
|
|
* already stated; the type is @hanzo/ui's marketing components. The agent prompt
|
|
* and the closing block follow the column rather than sit inside it: each is a
|
|
* full-bleed `Band` carrying its own rule, gutter and measure, and nesting one
|
|
* in this page's 1152 column doubles the gutter and stops the rule short of the
|
|
* viewport.
|
|
*
|
|
* A card is `Leaf`, the site's one card-that-IS-a-link: it owns the resting
|
|
* edge, the hover and the reduced-motion opt-out. The rest of the type is
|
|
* `Line`, one rung of the ramp per call.
|
|
*/
|
|
|
|
import { INTEGRATIONS, INTEGRATION_CATEGORIES, type Integration } from '@/lib/integrations'
|
|
import { Box } from '@/components/ui'
|
|
import { Action, Leaf, Lift, Line, type LineProps } from '@hanzo/ui/marketing'
|
|
import { Grid } from '@hanzo/ui/grid'
|
|
import { AgentPrompt } from '@/components/product/AgentPrompt'
|
|
import { Related } from '@/components/product/Related'
|
|
import { product } from '@/lib/product'
|
|
import { Code } from '@/components/code'
|
|
import { Fold } from '@/components/marketing/fold'
|
|
import { Samples } from '@/components/models/Samples'
|
|
import { Win } from '@/components/home/chrome'
|
|
import { KEYS } from '@/components/home/nav-data'
|
|
import { phrase } from '@hanzo/i18n'
|
|
import { T } from '@hanzo/i18n/react'
|
|
|
|
/** What the agent prompt and the closing block read, off the one table that
|
|
* holds a product's name, route, audited command and docs page. */
|
|
const PRODUCT = product('integrations')
|
|
|
|
const CARD = { p: 20, borderColor: "var(--border)", display: "flex", flexDirection: "column", gap: 8, bg: "var(--card)", color: "var(--foreground)", hoverStyle: { borderColor: "color-mix(in oklab, var(--brand) 50%, transparent)" } } as const
|
|
|
|
/**
|
|
* A heading BENEATH the page's own, and under `Title`'s rung: these are a
|
|
* card's label and a shelf's label, not a section's argument, so they state
|
|
* their size rather than borrowing a role and then overriding two thirds of it.
|
|
*/
|
|
const Sub = (p: LineProps) => (
|
|
<Line render="h2" display="block" fontSize="$4" lineHeight="var(--leading-snug)" fontWeight="600" color="var(--foreground)" {...p} />
|
|
)
|
|
|
|
function IntegrationCard({ integration }: { integration: Integration }) {
|
|
return (
|
|
<Leaf href={`/integrations/${integration.slug}`} {...CARD} hoverStyle={{ borderColor: "color-mix(in oklab, var(--brand) 50%, transparent)" }}>
|
|
<Box display="flex" flexDirection="row" items="center" gap={12}>
|
|
<Line fontSize="$7" color="inherit">{integration.icon}</Line>
|
|
<Lift fontWeight="600">
|
|
{integration.name}
|
|
</Lift>
|
|
</Box>
|
|
{/* Two lines of description, and no more — the card is a fixed size in a grid. */}
|
|
<Line render="p" numberOfLines={2} fontSize="$2" color="var(--muted-foreground)">
|
|
<T>{integration.description}</T>
|
|
</Line>
|
|
<Line fontSize="$1" textTransform="capitalize" color="color-mix(in oklab, var(--muted-foreground) 60%, transparent)">
|
|
<T>{INTEGRATION_CATEGORIES[integration.category]}</T>
|
|
</Line>
|
|
</Leaf>
|
|
)
|
|
}
|
|
|
|
/** The same client, pointed at Hanzo, in the three languages the page names. */
|
|
const CLIENTS = [
|
|
{ lang: 'Python', grammar: 'python', code: 'from openai import OpenAI\n\nclient = OpenAI(\n base_url="https://api.hanzo.ai/v1",\n api_key=HANZO_API_KEY,\n)' },
|
|
{ lang: 'TypeScript', grammar: 'typescript', code: 'import OpenAI from "openai";\n\nconst client = new OpenAI({\n baseURL: "https://api.hanzo.ai/v1",\n apiKey: HANZO_API_KEY,\n});' },
|
|
{ lang: 'Go', grammar: 'go', code: 'client := openai.NewClient(\n\toption.WithBaseURL("https://api.hanzo.ai/v1"),\n\toption.WithAPIKey(HANZO_API_KEY),\n)' },
|
|
] as const
|
|
|
|
export default function IntegrationsPage() {
|
|
const categories = Object.entries(INTEGRATION_CATEGORIES) as [Integration['category'], string][]
|
|
|
|
return (
|
|
<Box minH="100vh">
|
|
{/* The first screen: one base URL beside the same client in three languages, switchable. */}
|
|
<Fold
|
|
label={phrase('Integrations')}
|
|
title={<T>Works with everything</T>}
|
|
lede={
|
|
<T
|
|
values={{
|
|
url: <Line render="code" fontSize="$2" bg="var(--muted)" px={6} py={2} rounded="var(--radius)" color="inherit">https://api.hanzo.ai/v1</Line>,
|
|
}}
|
|
>
|
|
{'Drop in {url} as the base URL and your Hanzo API key.'}
|
|
</T>
|
|
}
|
|
actions={
|
|
<>
|
|
<Action href={KEYS} fill>
|
|
<T>Get an API key</T>
|
|
</Action>
|
|
<Action href="https://docs.hanzo.ai/docs/sdks"><T>SDK docs</T></Action>
|
|
</>
|
|
}
|
|
preview={
|
|
<Win title="https://api.hanzo.ai/v1">
|
|
<Box p={12} $md={{ p: 16 }}>
|
|
<Samples samples={CLIENTS.map((c) => ({ lang: c.lang, code: c.code, sample: <Code lang={c.grammar} code={c.code} /> }))} />
|
|
</Box>
|
|
</Win>
|
|
}
|
|
/>
|
|
<Box
|
|
width="100%"
|
|
maxW={1152}
|
|
mx="auto"
|
|
px={16}
|
|
py={80}
|
|
>
|
|
{/* Categories */}
|
|
{categories.map(([category, label]) => {
|
|
const items = INTEGRATIONS.filter((i) => i.category === category)
|
|
if (items.length === 0) return null
|
|
return (
|
|
<Box key={category} render="section" mb={48}>
|
|
<Sub mb={24} fontSize="$7" fontWeight="700">
|
|
<T>{label}</T>
|
|
</Sub>
|
|
<Grid columns={{ min: 280, max: 3 }} gap={16}>
|
|
{items.map((integration) => (
|
|
<IntegrationCard key={integration.slug} integration={integration} />
|
|
))}
|
|
</Grid>
|
|
</Box>
|
|
)
|
|
})}
|
|
</Box>
|
|
|
|
{PRODUCT && <AgentPrompt product={PRODUCT} />}
|
|
{PRODUCT && <Related product={PRODUCT} />}
|
|
</Box>
|
|
)
|
|
}
|