Files
zooqueen 7e12e34f7d the plan a buyer arrives with is the plan they check out with
`?plan=<slug>` is what every "Get started" on the marketing site links to, and
the grid now continues that choice through the same door a click uses: the tab
the plan lives on, its price from the catalog, and the hop to the Pay card form.

It cannot charge on its own. A click may charge a card already on file in one
step; a plan carried in on a link stops at the card form, because arriving
somewhere is not consent to be charged.

Sign-in keeps it too — startLogin remembers the query and hash to come back to,
and the callback lands there instead of at the bare page. Only a suffix is ever
stored, so there is no path or host in it to point somewhere else.

The CTA on the commerce page names its own tier rather than dropping it, and
`make test` runs the suite the repo actually has.
2026-08-16 01:29:15 -07:00

214 lines
7.5 KiB
TypeScript

'use client'
import { SALES_URL } from '@/lib/config'
export interface PricingPlan {
id: string
name: string
description: string
/** DOLLARS. `null` on a tier sold by a person — not zero, and not free. */
priceMonthly: number | null
priceAnnual?: number
category?: string
popular?: boolean
features: string[]
payouts?: { idleResalePercent?: number }
contactSales?: boolean
}
interface PlanCardProps {
plan: PricingPlan
billingInterval: 'monthly' | 'annual'
}
const BILLING_URL = 'https://billing.hanzo.ai'
const CAL_URL = SALES_URL
/**
* Sold by a person — no self-serve price, and the CTA books a conversation.
*
* This test was written out four times (the price, the CTA's words, the CTA's
* destination, and the card's own layout), which is how they came to disagree.
* Stated once, they cannot.
*/
function bySales(plan: PricingPlan): boolean {
return !!plan.contactSales || plan.priceMonthly == null || plan.priceMonthly >= 9999
}
/**
* Costs nothing — which is NOT the same fact as having no price.
*
* Enterprise advertised itself as "Free". The catalog gives it
* `priceMonthly: null`, the projection turned that into 0, and 0 read as free —
* on the one tier nobody may self-serve. Only one of those two is a discount.
*/
function isFreePlan(plan: PricingPlan): boolean {
return !bySales(plan) && plan.priceMonthly === 0
}
function price(plan: PricingPlan, interval: 'monthly' | 'annual'): string {
if (bySales(plan)) return 'Contact sales'
if (isFreePlan(plan)) return 'Free'
const amt = interval === 'annual' && plan.priceAnnual ? plan.priceAnnual : plan.priceMonthly!
return `$${amt}/mo`
}
// `priceAnnual` is the MONTHLY rate when billed for a year, not a yearly total,
// so a year's saving is twelve times the difference.
function annualSavings(plan: PricingPlan): string | null {
if (bySales(plan) || !plan.priceAnnual || !plan.priceMonthly) return null
const saved = (plan.priceMonthly - plan.priceAnnual) * 12
if (saved <= 0) return null
return `Save $${saved}/yr`
}
function ctaLabel(plan: PricingPlan): string {
return bySales(plan) ? 'Talk to sales' : 'Get started'
}
// The CTA names the tier it sits under, so the grid it opens continues that
// choice instead of asking for it again.
function ctaHref(plan: PricingPlan): string {
return bySales(plan) ? CAL_URL : `${BILLING_URL}/?plan=${encodeURIComponent(plan.id)}#pricing`
}
export function PlanCard({ plan, billingInterval }: PlanCardProps) {
const isPopular = plan.popular
const isFree = isFreePlan(plan)
const isSales = bySales(plan)
const savings = annualSavings(plan)
return (
<div
style={{
position: 'relative',
background: isPopular ? 'color-mix(in oklab, var(--color-text) 5%, transparent)' : 'color-mix(in oklab, var(--color-text) 2%, transparent)',
border: isPopular ? '1px solid color-mix(in oklab, var(--color-text) 20%, transparent)' : '1px solid color-mix(in oklab, var(--color-text) 8%, transparent)',
borderRadius: '16px',
padding: '28px 24px 24px',
display: 'flex',
flexDirection: 'column',
gap: '20px',
transition: 'border-color 0.15s',
}}
>
{/* Popular badge */}
{isPopular && (
<div style={{
position: 'absolute',
top: '-12px',
left: '50%',
transform: 'translateX(-50%)',
background: 'var(--color-brand)',
color: 'var(--color-brand-foreground)',
fontSize: '11px',
fontWeight: 700,
letterSpacing: '0.06em',
textTransform: 'uppercase',
padding: '3px 12px',
borderRadius: '99px',
whiteSpace: 'nowrap',
}}>
Most Popular
</div>
)}
{/* Header */}
<div>
<h3 style={{ fontSize: '15px', fontWeight: 600, color: 'var(--color-text)', margin: '0 0 6px', letterSpacing: '-0.01em' }}>
{plan.name}
</h3>
<p style={{ fontSize: '13px', color: 'color-mix(in oklab, var(--color-text) 45%, transparent)', margin: 0, lineHeight: 1.5 }}>
{plan.description}
</p>
</div>
{/* Price */}
<div style={{ display: 'flex', alignItems: 'flex-end', gap: '8px' }}>
<span style={{ fontSize: isFree || isSales ? '28px' : '36px', fontWeight: 700, color: 'var(--color-text)', letterSpacing: '-0.03em', lineHeight: 1 }}>
{price(plan, billingInterval)}
</span>
{!isFree && !isSales && (
<span style={{ fontSize: '13px', color: 'color-mix(in oklab, var(--color-text) 35%, transparent)', marginBottom: '4px' }}>
{billingInterval === 'annual' ? 'billed annually' : 'billed monthly'}
</span>
)}
</div>
{/* Annual savings badge */}
{billingInterval === 'annual' && savings && (
<div style={{
display: 'inline-flex',
alignItems: 'center',
background: 'rgba(74,222,128,0.1)',
border: '1px solid rgba(74,222,128,0.2)',
borderRadius: '6px',
padding: '3px 8px',
fontSize: '12px',
color: '#4ade80',
fontWeight: 500,
marginTop: '-12px',
width: 'fit-content',
}}>
{savings}
</div>
)}
{/* CTA */}
<a
href={ctaHref(plan)}
style={{
display: 'block',
textAlign: 'center',
background: isPopular ? 'var(--color-brand)' : 'color-mix(in oklab, var(--color-text) 6%, transparent)',
color: isPopular ? 'var(--color-brand-foreground)' : 'var(--color-text)',
border: isPopular ? 'none' : '1px solid color-mix(in oklab, var(--color-text) 10%, transparent)',
borderRadius: '8px',
padding: '10px 16px',
fontSize: '14px',
fontWeight: 600,
textDecoration: 'none',
transition: 'opacity 0.15s',
}}
onMouseEnter={(e) => { (e.currentTarget as HTMLAnchorElement).style.opacity = '0.85' }}
onMouseLeave={(e) => { (e.currentTarget as HTMLAnchorElement).style.opacity = '1' }}
>
{ctaLabel(plan)}
</a>
{/* Features */}
<ul style={{ margin: 0, padding: 0, listStyle: 'none', display: 'flex', flexDirection: 'column', gap: '10px' }}>
{plan.features.map((f, i) => (
<li key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: '10px', fontSize: '13px', color: 'color-mix(in oklab, var(--color-text) 60%, transparent)', lineHeight: 1.4 }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0, marginTop: '1px', color: '#4ade80' }}>
<polyline points="20 6 9 17 4 12" />
</svg>
{f}
</li>
))}
</ul>
{/* Payout badge */}
{plan.payouts?.idleResalePercent && (
<div style={{
marginTop: 'auto',
background: 'rgba(251,191,36,0.08)',
border: '1px solid rgba(251,191,36,0.15)',
borderRadius: '8px',
padding: '10px 12px',
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '12px',
color: 'rgba(251,191,36,0.85)',
}}>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" />
</svg>
Earn {plan.payouts.idleResalePercent}% on idle compute resale
</div>
)}
</div>
)
}