mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
Each step's lines come from lib/headless and are highlighted by the server (components/code), so the window stands about as tall as the install beside it and no highlighter reaches the browser.
124 lines
5.1 KiB
TypeScript
124 lines
5.1 KiB
TypeScript
'use client'
|
|
|
|
import { Suspense, useEffect, type ReactNode } from 'react'
|
|
import dynamic from 'next/dynamic'
|
|
import { phrase } from '@hanzo/i18n'
|
|
import { useI18n } from '@hanzo/i18n/react'
|
|
import { XStack, YStack } from '@hanzo/ui'
|
|
import { More } from '@hanzo/ui/marketing'
|
|
import { track } from '@hanzo/app/lib/tags'
|
|
import { Box } from '@/components/ui'
|
|
import { SiteFooter, SiteHeader } from './shell'
|
|
import { Beta } from './beta'
|
|
import { Guard } from './guard'
|
|
import Start from './Start'
|
|
import Backing from './Backing'
|
|
import { Onward } from './label'
|
|
|
|
// Below the first screen, so each is prerendered into the HTML and its script
|
|
// loads at low priority, outside the first load perf.budget.json holds. The
|
|
// boundary keeps the first screen from waiting on their chunks to hydrate.
|
|
const Plans = dynamic(() => import('@/components/pricing/Plans'))
|
|
const Pareto = dynamic(() => import('./Pareto'))
|
|
const Faster = dynamic(() => import('./Faster'))
|
|
const Cheaper = dynamic(() => import('./Cheaper'))
|
|
const Compatibility = dynamic(() => import('./Compatibility'))
|
|
const Models = dynamic(() => import('./Models'))
|
|
const Products = dynamic(() => import('./Products'))
|
|
const Platform = dynamic(() => import('./Platform'))
|
|
const Trusted = dynamic(() => import('./Trusted'))
|
|
const OpenSource = dynamic(() => import('./OpenSource'))
|
|
|
|
/**
|
|
* Where a link goes, as the funnel names it: a path on this site, origin and
|
|
* path elsewhere, with its own query kept and the visitor ids another Hanzo
|
|
* host is handed (`hz_aid`, `hz_sid`, `hz_ft`, written onto the link as it is
|
|
* pressed) taken off, so a target is one value however many people press it.
|
|
*/
|
|
function target(a: HTMLAnchorElement): string {
|
|
const u = new URL(a.href)
|
|
for (const k of ['hz_aid', 'hz_sid', 'hz_ft']) u.searchParams.delete(k)
|
|
const q = u.searchParams.toString()
|
|
return `${u.origin === window.location.origin ? '' : u.origin}${u.pathname}${q ? `?${q}` : ''}${u.hash}`
|
|
}
|
|
|
|
/**
|
|
* Every link followed on the landing, as one funnel event through `track` (our
|
|
* stream always, the ad tags as consent allows): `cta_clicked { page, section,
|
|
* cta, target }`. The section is the band the link stands in (its
|
|
* `data-hz-view`, else the hero's slot); `cta` is the link's own `data-cta` or
|
|
* its words. A link marked `data-ga` sends its own moment (a plan card's
|
|
* `plan_clicked`), so it is not counted twice. Where on the page the press
|
|
* landed is @hanzo/observe's `$click`, for the heat map.
|
|
*/
|
|
function useCtas() {
|
|
useEffect(() => {
|
|
const click = (e: MouseEvent) => {
|
|
const a = (e.target as Element | null)?.closest?.('main a[href]') as HTMLAnchorElement | null
|
|
if (!a || a.hasAttribute('data-ga')) return
|
|
const section = a.closest('[data-hz-view]')?.getAttribute('data-hz-view') ?? a.closest('[data-slot]')?.getAttribute('data-slot') ?? 'page'
|
|
const cta = a.getAttribute('data-cta') ?? (a.getAttribute('aria-label') || a.textContent || '').trim().slice(0, 80)
|
|
track('cta_clicked', { page: '/', section, cta, target: target(a) })
|
|
}
|
|
document.addEventListener('click', click, { capture: true })
|
|
return () => document.removeEventListener('click', click, { capture: true })
|
|
}, [])
|
|
}
|
|
|
|
/**
|
|
* Under the plans: every rate. Its
|
|
* own component so the words are its own: Home stays a component that never
|
|
* re-renders after hydration, because an update to it would reach the
|
|
* boundary below before that boundary has hydrated, and React would throw its
|
|
* server HTML away and draw it again from nothing.
|
|
*/
|
|
function Unsure() {
|
|
const { t } = useI18n()
|
|
return (
|
|
<XStack justify="center" flexWrap="wrap" columnGap={28} rowGap={8}>
|
|
<More href="/pricing">
|
|
<Onward>{t('Every plan, model rate and tool price')}</Onward>
|
|
</More>
|
|
</XStack>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* The landing at `/`: the promise and who backs us, then the apps, every
|
|
* model, and the proof: Smarter, Faster, Cheaper. Then
|
|
* what we ship in the open, the agents it works with (and how an agent signs
|
|
* itself up), the stack underneath, pricing, and who trusts us; then the
|
|
* footer. The questions that used to end the page are on /learn.
|
|
*/
|
|
export default function Home({ samples }: { samples?: readonly ReactNode[] }) {
|
|
useCtas()
|
|
return (
|
|
<Box minH="100vh" $platform-web={{ color: 'var(--foreground)', overflowX: 'clip' }}>
|
|
<SiteHeader />
|
|
<main>
|
|
<Beta />
|
|
<Start />
|
|
<Backing />
|
|
<Suspense>
|
|
<Guard><Products /></Guard>
|
|
<Guard><Models /></Guard>
|
|
<Guard><Pareto /></Guard>
|
|
<Guard><Faster /></Guard>
|
|
<Guard><Cheaper /></Guard>
|
|
<Guard><OpenSource /></Guard>
|
|
<Guard><Compatibility samples={samples} /></Guard>
|
|
<Guard><Platform /></Guard>
|
|
<Guard>
|
|
<YStack id="plans" data-hz-view="plans" width="100%" maxW={1240} mx="auto" px="var(--page-gutter)" py={56} gap="$6" $md={{ py: 80 }}>
|
|
<Plans heading="h2" label="Hanzo.Pricing" title={phrase('Pricing')} ask={phrase('Try Hanzo')} footer={false} />
|
|
<Unsure />
|
|
</YStack>
|
|
</Guard>
|
|
<Guard><Trusted /></Guard>
|
|
</Suspense>
|
|
</main>
|
|
<SiteFooter />
|
|
</Box>
|
|
)
|
|
}
|