mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
Every word the landing, the header menus, the phone sheet, the footer,
the help corner and the way in (sign-in, sign-up, the reset, the
invitation, the callback, onboarding, the 404) write goes through
@hanzo/i18n's t(): one sentence is one message, its figures, names and
links arguments; copy written as data is phrase('…') where written and
t(label) where drawn. Plans, the chart, the API states and the lines
lib/kai and lib/enso build for the landing do the same.
Right to left: <html lang dir> is set before the first paint (the
package's bootScript first in the head); a figure, clock, range or
price outside a sentence is isolated; sides are logical gui props, the
layout's [data-join] corners logical, a bar grows from the start;
arrows along the line mirror (Onward for "words ->" links). The repo
dates read in the reader's language.
Gates select by slot, never by an English label (rtl, help). New gate
translated.spec.ts: in Arabic the landing at 1440 and 390, every menu
open, the phone sheet, sign-in, sign-up and the app signed in hold no
authored English, nothing scrolls sideways, every range is isolated;
French the same for English's commonest words. What another package
draws with no words prop is listed by name (@hanzo/ui's Footer label,
@hanzo/build's composer row and suggestions).
@hanzo/i18n 0.5.3 (the catalogs: 1,443 keys, every offered locale
complete), @hanzo/app 0.2.1 (the app's words). The link gate imports
the terms versions from @hanzo/app, the one SDK main now resolves.
88 lines
3.3 KiB
TypeScript
88 lines
3.3 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* A coupon's offer, said across the top of the page as pay's strip says it
|
|
* (hanzo-inc/pay components/Offer `Applied`, `.pay-strip`): "50% off your first
|
|
* 3 months of Pro", from commerce's quote for the plan the page is about, or the
|
|
* first plan the code covers when it names none. The strip holds its height
|
|
* while the quote is asked, so nothing under it moves when it is answered; a
|
|
* code commerce refuses draws no strip, and pay says why where it is charged.
|
|
*
|
|
* Said as one sentence in the reader's language (@hanzo/i18n): the share, the
|
|
* months and the plan are its arguments, and one month or several, with a plan
|
|
* named or not, are four sentences, so no translation assembles one from parts.
|
|
*/
|
|
|
|
import { useEffect, useRef } from 'react'
|
|
import { Sparkles } from 'lucide-react'
|
|
import { useI18n } from '@hanzo/i18n/react'
|
|
import { Text, XStack, type GuiElement } from '@hanzo/ui'
|
|
import { useQuote } from '@/hooks/useQuote'
|
|
|
|
export function Offer({ coupon, plan = null }: { coupon: string | null; plan?: string | null }) {
|
|
const { t } = useI18n()
|
|
const strip = useRef<GuiElement>(null)
|
|
const quote = useQuote(coupon)
|
|
|
|
useEffect(() => {
|
|
const el = strip.current as HTMLElement | null
|
|
if (!el) return
|
|
let want = 0.5
|
|
let wantY = 0.5
|
|
let at = 0.5
|
|
let atY = 0.5
|
|
let frame = 0
|
|
const step = () => {
|
|
at += (want - at) * 0.42
|
|
atY += (wantY - atY) * 0.42
|
|
el.style.setProperty('--tilt', at.toFixed(4))
|
|
el.style.setProperty('--lift', atY.toFixed(4))
|
|
frame = Math.abs(want - at) > 0.0005 || Math.abs(wantY - atY) > 0.0005 ? requestAnimationFrame(step) : 0
|
|
}
|
|
const move = (e: PointerEvent) => {
|
|
want = e.clientX / Math.max(1, window.innerWidth)
|
|
wantY = e.clientY / Math.max(1, window.innerHeight)
|
|
if (!frame) frame = requestAnimationFrame(step)
|
|
}
|
|
window.addEventListener('pointermove', move, { passive: true })
|
|
return () => {
|
|
window.removeEventListener('pointermove', move)
|
|
cancelAnimationFrame(frame)
|
|
}
|
|
}, [])
|
|
|
|
if (!coupon || quote === null) return null
|
|
const deals = quote && 'deals' in quote ? quote.deals : undefined
|
|
const deal = deals ? (plan ? deals[plan] : Object.values(deals)[0]) : undefined
|
|
if (quote && !deal) return null
|
|
const said = !deal
|
|
? t('Your discount applies at checkout')
|
|
: deal.months > 1
|
|
? deal.name
|
|
? t('{percent}% off your first {months} months of {plan}', { percent: deal.percent, months: deal.months, plan: deal.name })
|
|
: t('{percent}% off your first {months} months', { percent: deal.percent, months: deal.months })
|
|
: deal.name
|
|
? t('{percent}% off your first month of {plan}', { percent: deal.percent, plan: deal.name })
|
|
: t('{percent}% off your first month', { percent: deal.percent })
|
|
return (
|
|
<XStack
|
|
ref={strip}
|
|
role="status"
|
|
data-slot="beta"
|
|
items="center"
|
|
justify="center"
|
|
gap={8}
|
|
minH={40}
|
|
px="var(--page-gutter)"
|
|
py={8}
|
|
position="relative"
|
|
overflow="hidden"
|
|
>
|
|
<Sparkles size={14} strokeWidth={2.2} color="var(--pure-white)" aria-hidden />
|
|
<Text fontSize="$2" lineHeight="1.25rem" fontWeight="600" color="var(--pure-white)" text="center">
|
|
{said}
|
|
</Text>
|
|
</XStack>
|
|
)
|
|
}
|