mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
Calculator, captable, careers, cloud, compare, contact and contact-sales, customers, dashboards, dataroom, datastore, dev, download, enso, enterprise, extension, family, integrations, investors, kai, learn, legal and their components say their words through t() in client code, <T> in server components and phrase() where copy is held as data; one sentence is one key, figures and links its arguments. Rebased on main: in every conflict main's file stands (its copy, layout and styling) and only the wrapping is added back, including copy main added since. Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH
316 lines
11 KiB
TypeScript
316 lines
11 KiB
TypeScript
/**
|
|
* /integrations/<slug> — one integration, its attribution, and its code.
|
|
*
|
|
* NO TAILWIND, and it is the same idiom as the index it hangs off
|
|
* (`app/(marketing)/integrations/page.tsx`): a SERVER page, `Box` for the
|
|
* frame, @hanzo/ui's marketing components for the type and the controls — `Line`
|
|
* for every run of small print — and gui props over @hanzo/design's tokens.
|
|
*
|
|
* The two controls at the foot are `Action`, the site's ONE button shape at
|
|
* its two weights — the loud one keeps the brand ground it was drawn with,
|
|
* because the `data-fill` hover is an OPACITY and an inline colour does not
|
|
* fight it. The chips beneath are `Chip`, which owns its own hover; padding is
|
|
* the caller's, which is the split that rule states.
|
|
*
|
|
* Two ink lifts on hover are deliberately gone rather than moved, and both were
|
|
* on a link inside a `<p>`: @hanzo/design's base already underlines those on
|
|
* approach, so the affordance stays and only the colour change is dropped. A
|
|
* role would have brought its own size and weight with it, which is a bigger
|
|
* change than the one it repairs.
|
|
*/
|
|
|
|
import type { Metadata } from 'next'
|
|
import { notFound } from 'next/navigation'
|
|
import Link from 'next/link'
|
|
import { KEYS } from '@/components/home/nav-data'
|
|
import { INTEGRATIONS, getIntegration, INTEGRATION_CATEGORIES } from '@/lib/integrations'
|
|
import { Models } from '@/components/api/count'
|
|
import { Box } from '@/components/ui'
|
|
import { Code } from '@/components/code'
|
|
import { Action, Chip, Display, Lede, Line, type LineProps } from '@hanzo/ui/marketing'
|
|
import { T } from '@hanzo/i18n/react'
|
|
|
|
interface Props {
|
|
params: Promise<{ slug: string }>
|
|
}
|
|
|
|
export function generateStaticParams() {
|
|
return INTEGRATIONS.map((i) => ({ slug: i.slug }))
|
|
}
|
|
|
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
const { slug } = await params
|
|
const integration = getIntegration(slug)
|
|
|
|
if (!integration) {
|
|
return { title: 'Integration Not Found — Hanzo AI' }
|
|
}
|
|
|
|
const title = `Use Hanzo AI with ${integration.name}`
|
|
const description = integration.description
|
|
|
|
return {
|
|
title: `${title} — Hanzo AI`,
|
|
description,
|
|
openGraph: {
|
|
title,
|
|
description,
|
|
url: `https://hanzo.ai/integrations/${slug}`,
|
|
siteName: 'Hanzo AI',
|
|
type: 'website',
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
title,
|
|
description,
|
|
},
|
|
}
|
|
}
|
|
|
|
/** The two quiet greys this page sets its small print in, and full ink. */
|
|
const QUIET = { fontSize: '$2', color: 'var(--muted-foreground)' } as const
|
|
const SMALL = { fontSize: '$1', color: 'var(--muted-foreground)' } as const
|
|
const INK = 'var(--foreground)'
|
|
|
|
/**
|
|
* A heading beneath the page's own. These label a sample and a shelf rather
|
|
* than making a section's argument, so they state their rung instead of taking
|
|
* a role and overriding two thirds of it — the same call the index page makes.
|
|
*/
|
|
const Sub = (p: LineProps) => (
|
|
<Line render="h2" display="block" fontSize="$6" lineHeight="var(--leading-snug)" fontWeight="600" color="var(--foreground)" {...p} />
|
|
)
|
|
|
|
/** A sample under its language, highlighted at build (components/code). */
|
|
function CodeBlock({ code, lang }: { code: string; lang: string }) {
|
|
return (
|
|
<Box rounded="var(--radius-md)" borderWidth={1} borderColor="var(--border)" overflow="hidden">
|
|
<Box
|
|
display="flex"
|
|
flexDirection="row"
|
|
items="center"
|
|
justify="space-between"
|
|
px={16}
|
|
py={8}
|
|
borderBottomWidth={1}
|
|
borderColor="var(--border)"
|
|
>
|
|
<Line fontFamily="$mono" {...SMALL}>
|
|
{lang}
|
|
</Line>
|
|
</Box>
|
|
<Code code={code} lang={lang} />
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
export default async function IntegrationPage({ params }: Props) {
|
|
const { slug } = await params
|
|
const integration = getIntegration(slug)
|
|
|
|
if (!integration) {
|
|
notFound()
|
|
}
|
|
|
|
const jsonLd = {
|
|
'@context': 'https://schema.org',
|
|
'@type': ['HowTo', 'SoftwareApplication'],
|
|
name: `Use Hanzo AI with ${integration.name}`,
|
|
description: integration.description,
|
|
applicationCategory: 'DeveloperApplication',
|
|
operatingSystem: 'Any',
|
|
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
|
author: { '@type': 'Organization', name: integration.creator, url: integration.creatorUrl },
|
|
license: integration.upstream,
|
|
step: integration.examples.map((ex, i) => ({
|
|
'@type': 'HowToStep',
|
|
position: i + 1,
|
|
name: ex.label,
|
|
text: ex.code,
|
|
})),
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<script
|
|
type="application/ld+json"
|
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
/>
|
|
<Box minH="100vh">
|
|
<Box
|
|
width="100%"
|
|
maxW={896}
|
|
mx="auto"
|
|
px={16}
|
|
py={80}
|
|
>
|
|
{/* Breadcrumb */}
|
|
<Line render="nav" display="flex" flexDirection="row" items="center" gap={8} mb={40} {...QUIET}>
|
|
<Link href="/integrations"><T>Integrations</T></Link>
|
|
<span>/</span>
|
|
<Line color={INK}>{integration.name}</Line>
|
|
</Line>
|
|
|
|
{/* Header */}
|
|
<Box mb={48}>
|
|
<Box display="flex" flexDirection="row" items="center" gap={16} mb={16}>
|
|
<Line fontSize="$10" color="inherit">
|
|
{integration.icon}
|
|
</Line>
|
|
<div>
|
|
<Line {...SMALL} fontWeight="500" textTransform="uppercase" letterSpacing="0.05em">
|
|
<T>{INTEGRATION_CATEGORIES[integration.category]}</T>
|
|
</Line>
|
|
<Display><T values={{ name: integration.name }}>{'Use Hanzo AI with {name}'}</T></Display>
|
|
</div>
|
|
</Box>
|
|
<Lede maxW={672}><T>{integration.description}</T></Lede>
|
|
</Box>
|
|
|
|
{/* API base info */}
|
|
<Box
|
|
mb={40}
|
|
rounded="var(--radius-lg)"
|
|
borderWidth={1}
|
|
p={20}
|
|
/* A `color-mix()` ink is not a value gui's colour props type, so the
|
|
brand wash rides `$platform-web`, which gui still writes as a class. */
|
|
$platform-web={{ borderColor: 'color-mix(in oklab, var(--brand) 20%, transparent)', backgroundColor: 'color-mix(in oklab, var(--brand) 5%, transparent)' }}
|
|
>
|
|
<Line render="p" display="block" mb={8} {...QUIET}>
|
|
<Line render="strong" color={INK}><T>Base URL:</T></Line>{' '}
|
|
<Line render="code" color="var(--brand)">https://api.hanzo.ai/v1</Line>
|
|
</Line>
|
|
<Line render="p" display="block" {...QUIET}>
|
|
<Line render="strong" color={INK}><T>API Key:</T></Line>{' '}
|
|
<T
|
|
values={{
|
|
link: (
|
|
<Line href={KEYS} color="var(--brand)">
|
|
platform.hanzo.ai/api-keys
|
|
</Line>
|
|
),
|
|
}}
|
|
>
|
|
{'Get yours at {link}'}
|
|
</T>
|
|
{' · '}
|
|
<T>Fully OpenAI-compatible</T>
|
|
{' · '}
|
|
<T values={{ models: <Models /> }}>{'{models} available'}</T>
|
|
</Line>
|
|
</Box>
|
|
|
|
{/* Attribution */}
|
|
<Box
|
|
mb={40}
|
|
rounded="var(--radius-lg)"
|
|
borderWidth={1}
|
|
borderColor="var(--border)"
|
|
bg="$panel"
|
|
p={20}
|
|
display="flex"
|
|
flexDirection="row"
|
|
items="flex-start"
|
|
gap={16}
|
|
>
|
|
<Line render="div" display="block" fontSize="$7" color="inherit">
|
|
{integration.icon}
|
|
</Line>
|
|
<Box grow={1} shrink={1} flexBasis="0%" minW={0}>
|
|
<Line render="p" display="block" mb={2} fontSize="$2" fontWeight="500" color="var(--foreground)">
|
|
<T
|
|
values={{
|
|
creator: (
|
|
<Line href={integration.creatorUrl} target="_blank" rel="noopener noreferrer" color="var(--state-error)">
|
|
{integration.creator}
|
|
</Line>
|
|
),
|
|
}}
|
|
>
|
|
{'Created by {creator}'}
|
|
</T>
|
|
</Line>
|
|
<Line render="p" display="block" mb={8} {...SMALL}>
|
|
<T>License:</T>{' '}
|
|
<Line fontFamily="$mono" color={INK} bg="var(--muted)" px={6} py={2} rounded="var(--radius)">
|
|
{integration.license}
|
|
</Line>
|
|
{' · '}
|
|
<a href={integration.upstream} target="_blank" rel="noopener noreferrer">
|
|
<T>{'View source on GitHub →'}</T>
|
|
</a>
|
|
</Line>
|
|
<Line render="p" display="block" {...SMALL}>
|
|
<T values={{ name: integration.name, creator: integration.creator }}>
|
|
{'Hanzo AI is OpenAI-compatible, so existing {name} code works with zero refactoring. We deeply appreciate the {creator} team for building and maintaining this open-source project.'}
|
|
</T>
|
|
</Line>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Code examples */}
|
|
<Box display="flex" flexDirection="column" gap={40}>
|
|
{integration.examples.map((example, i) => (
|
|
<div key={i}>
|
|
<Sub mb={16}><T>{example.label}</T></Sub>
|
|
<CodeBlock code={example.code} lang={example.lang} />
|
|
</div>
|
|
))}
|
|
</Box>
|
|
|
|
{/* Footer CTA */}
|
|
<Box
|
|
mt={64}
|
|
rounded="var(--radius-lg)"
|
|
borderWidth={1}
|
|
borderColor="var(--border)"
|
|
bg="var(--card)"
|
|
p={32}
|
|
$platform-web={{ textAlign: 'center' }}
|
|
>
|
|
<Sub mb={12} fontSize="$7" fontWeight="700">
|
|
<T>Ready to get started?</T>
|
|
</Sub>
|
|
<Line render="p" display="block" mb={24} color="var(--muted-foreground)">
|
|
<T>Create a free account and get your API key. The Free plan includes 50 free requests a day, no card.</T>
|
|
</Line>
|
|
<Box display="flex" flexDirection="row" items="center" justify="center" gap={16}>
|
|
{/* The brand ground is a prop and the fill's hover is an
|
|
OPACITY, so the two do not fight. */}
|
|
<Action
|
|
href={KEYS}
|
|
fill
|
|
bg="var(--brand)"
|
|
color="var(--brand-foreground)"
|
|
>
|
|
<T>Get API key — Free</T>
|
|
</Action>
|
|
<Action href="/models">
|
|
<T>Browse Models</T>
|
|
</Action>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Other integrations */}
|
|
<Box mt={48}>
|
|
<Sub mb={16}><T>More integrations</T></Sub>
|
|
<Box display="flex" flexDirection="row" flexWrap="wrap" gap={8}>
|
|
{INTEGRATIONS.filter((i) => i.slug !== slug).map((i) => (
|
|
<Chip
|
|
href={`/integrations/${i.slug}`}
|
|
key={i.slug}
|
|
px={12}
|
|
py={6}
|
|
>
|
|
{i.name}
|
|
</Chip>
|
|
))}
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</>
|
|
)
|
|
}
|