mirror of
https://github.com/hanzoai/billing.git
synced 2026-10-30 18:46:54 +00:00
THE BUILD WAS BROKEN, and not by any of this: `@hanzogui/shell` is imported by lib/components/billing-shell.tsx and was declared in package.json but never installed, so every build died — and Next's own code-frame highlighter panicked while formatting the error, which is why it read as an internal crash rather than a missing module. Installed and pinned exactly at 8.1.44. THE CHROME IS A RAIL. This is an account surface — you come to do one thing to your own billing and leave — and a bar across the top spends the dimension a page has least of on four links that never change, while pushing the thing you came for down. A column spends the dimension there is plenty of, and it is what hanzo.app and hanzo.chat already do, so moving between them costs no relearning. Hidden below `md`, where 240px of navigation is most of a phone and the footer already carries every link. `display` lives in the CLASS, not the inline style. It is the one property the media query has to change, and stating it inline then fighting that with `!important` would leave two homes for one property. ANNUAL IS THE DEFAULT. It is the cheaper of the two and the one most buyers take, so it is the price the page should answer "what does this cost?" with — Team reads $20 per user rather than $25, and monthly is one click away. The same choice hanzo.ai's team strip already made, so two surfaces stop quoting one plan at two different numbers. The wordmark says "Hanzo AI". Verified on the built export at its real base path (/admin/billing — serving it at root 404s every stylesheet and reads as a CSS bug): rail 240px and flex at 1440 and 1024, display:none at 767 and 390, no sideways scroll at any width, and Annual selected on load. Co-authored-by: Hanzo Dev <[email protected]>
86 lines
3.0 KiB
TypeScript
86 lines
3.0 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { PlanCard, type PricingPlan } from './plan-card'
|
|
|
|
interface BillingToggleProps {
|
|
plans: PricingPlan[]
|
|
}
|
|
|
|
export function BillingToggle({ plans }: BillingToggleProps) {
|
|
// ANNUAL first. It is the cheaper of the two and the one most buyers take, so
|
|
// it is the price the page should be answering "what does this cost?" with —
|
|
// Team reads $20 per user rather than $25, and the monthly rate is one click
|
|
// away for anyone who wants it.
|
|
//
|
|
// The same choice hanzo.ai's team strip already made, for the same reason
|
|
// stated there: annual is the headline, month-to-month is the note under it.
|
|
// Two surfaces quoting the same plan at two different numbers is the thing
|
|
// worth avoiding.
|
|
const [interval, setInterval] = useState<'monthly' | 'annual'>('annual')
|
|
|
|
return (
|
|
<>
|
|
{/* Toggle */}
|
|
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: '40px' }}>
|
|
<div style={{
|
|
display: 'inline-flex',
|
|
background: 'color-mix(in oklab, var(--color-text) 4%, transparent)',
|
|
border: '1px solid color-mix(in oklab, var(--color-text) 10%, transparent)',
|
|
borderRadius: '10px',
|
|
padding: '4px',
|
|
gap: '4px',
|
|
}}>
|
|
{(['monthly', 'annual'] as const).map((iv) => (
|
|
<button
|
|
key={iv}
|
|
type="button"
|
|
onClick={() => setInterval(iv)}
|
|
style={{
|
|
background: interval === iv ? 'color-mix(in oklab, var(--color-text) 10%, transparent)' : 'transparent',
|
|
border: interval === iv ? '1px solid color-mix(in oklab, var(--color-text) 15%, transparent)' : '1px solid transparent',
|
|
borderRadius: '7px',
|
|
padding: '6px 16px',
|
|
color: interval === iv ? 'var(--color-text)' : 'color-mix(in oklab, var(--color-text) 40%, transparent)',
|
|
fontSize: '13px',
|
|
fontWeight: 500,
|
|
cursor: 'pointer',
|
|
transition: 'all 0.15s',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '6px',
|
|
}}
|
|
>
|
|
{iv === 'monthly' ? 'Monthly' : 'Annual'}
|
|
{iv === 'annual' && (
|
|
<span style={{
|
|
background: 'rgba(74,222,128,0.15)',
|
|
color: '#4ade80',
|
|
fontSize: '10px',
|
|
fontWeight: 700,
|
|
padding: '1px 5px',
|
|
borderRadius: '4px',
|
|
letterSpacing: '0.03em',
|
|
}}>
|
|
Save 20%
|
|
</span>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Plans grid */}
|
|
<div style={{
|
|
display: 'grid',
|
|
gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))',
|
|
gap: '20px',
|
|
}}>
|
|
{plans.map((plan) => (
|
|
<PlanCard key={plan.id} plan={plan} billingInterval={interval} />
|
|
))}
|
|
</div>
|
|
</>
|
|
)
|
|
}
|