mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
The product pages from /s3 to /zap, /university and its course data, /trust, /security, /sdks, /support, /tasks, /startups, /sms-opt-in, the buying specialist, Zen, Kai's guide, Studio, the settings pages, and the shared sales, solutions, zap and ui pieces. Client code draws with t(), server components with <T>, copy held as data is marked with phrase() and drawn through t() where it lands. A link or a figure inside a sentence is a value, so a translator reorders the whole sentence. Rebased on main: every page main moved onto Fold or Steps keeps main's layout, with its words wrapped (label, title, lede, points). Steps now draws a step's body through t, as it already drew the title. Settings keeps main's usage panel, refresh, rewards wallet and sign-in wallet rows, each label wrapped. Left English: the skills page's install line (a command and two URLs), and the solution eyebrow and sibling heading built from a kind label. Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH
260 lines
8.3 KiB
TypeScript
260 lines
8.3 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import { Input } from '@hanzo/ui'
|
|
import { Box, Text, XStack, YStack } from '@/components/ui'
|
|
import { Action, Chip } from '@hanzo/ui/marketing'
|
|
import { Tag, Check, X, Sparkles, ArrowRight } from 'lucide-react'
|
|
import { checkoutUrl } from '@hanzo/app/lib/pay'
|
|
import { validateCoupon, type CouponResult } from './courses-data'
|
|
import { useI18n } from '@hanzo/i18n/react'
|
|
|
|
interface CouponInputProps {
|
|
originalPrice: number
|
|
planId: string
|
|
courseSlug: string
|
|
courseCode: string
|
|
rebateCredits: number
|
|
onCouponChange?: (result: CouponResult) => void
|
|
returnPath?: string
|
|
}
|
|
|
|
export function CouponInput({
|
|
originalPrice,
|
|
planId,
|
|
courseSlug,
|
|
courseCode,
|
|
rebateCredits,
|
|
onCouponChange,
|
|
returnPath,
|
|
}: CouponInputProps) {
|
|
const { t } = useI18n()
|
|
const [code, setCode] = useState('')
|
|
const [applied, setApplied] = useState<CouponResult | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const activePrice = applied?.valid ? applied.finalPrice : originalPrice
|
|
const activeRebate = applied?.valid ? applied.rebateCredits : rebateCredits
|
|
const backUrl = `https://hanzo.ai${returnPath || `/university/${courseSlug}`}`
|
|
|
|
const checkoutHref = applied?.valid
|
|
? `${checkoutUrl(planId, backUrl)}&coupon=${encodeURIComponent(applied.code)}&amount=${activePrice}`
|
|
: checkoutUrl(planId, backUrl)
|
|
|
|
const handleApply = (e?: React.FormEvent) => {
|
|
if (e) e.preventDefault()
|
|
if (!code.trim()) {
|
|
setError(t('Please enter a coupon code.'))
|
|
return
|
|
}
|
|
|
|
const res = validateCoupon(code, originalPrice)
|
|
if (res.valid) {
|
|
setApplied(res)
|
|
setError(null)
|
|
onCouponChange?.(res)
|
|
} else {
|
|
setError(t(res.message, { code: res.code }))
|
|
setApplied(null)
|
|
}
|
|
}
|
|
|
|
const handleRemove = () => {
|
|
setCode('')
|
|
setApplied(null)
|
|
setError(null)
|
|
onCouponChange?.({
|
|
valid: false,
|
|
code: '',
|
|
discountAmount: 0,
|
|
originalPrice,
|
|
finalPrice: originalPrice,
|
|
rebateCredits,
|
|
message: '',
|
|
})
|
|
}
|
|
|
|
return (
|
|
<YStack gap="$3" width="100%">
|
|
{/* Price Display with Strikethrough if discounted */}
|
|
<YStack
|
|
p="$4"
|
|
rounded="var(--radius-lg)"
|
|
bg="var(--pure-black)"
|
|
borderWidth={1}
|
|
borderColor={applied?.valid ? 'var(--border-selected)' : 'var(--border)'}
|
|
gap="$3"
|
|
>
|
|
<XStack items="baseline" justify="space-between" flexWrap="wrap" gap="$2">
|
|
<YStack>
|
|
<Text fontSize="$1" color="var(--muted-foreground)" fontFamily="$mono">
|
|
{t('Tuition due today')}
|
|
</Text>
|
|
<XStack items="baseline" gap="$2" mt="$1">
|
|
{applied?.valid && (
|
|
<Text
|
|
fontSize="$4"
|
|
fontWeight="600"
|
|
color="var(--muted-foreground)"
|
|
$platform-web={{ textDecoration: 'line-through' }}
|
|
>
|
|
${originalPrice}
|
|
</Text>
|
|
)}
|
|
<Text fontSize="$6" fontWeight="800" color="var(--white)">
|
|
${activePrice}
|
|
</Text>
|
|
<Text fontSize="$1" color="var(--muted-foreground)">
|
|
USD
|
|
</Text>
|
|
{applied?.valid && applied.discountPercent && (
|
|
<Chip px={8} py={2} fontSize="$1" fontFamily="$mono" color="var(--state-success)">
|
|
{t('{percent}% OFF', { percent: applied.discountPercent })}
|
|
</Chip>
|
|
)}
|
|
</XStack>
|
|
</YStack>
|
|
|
|
<YStack items="flex-end">
|
|
<Text fontSize="$1" color="var(--muted-foreground)" fontFamily="$mono">
|
|
{t('COMPUTE REBATE (25%)')}
|
|
</Text>
|
|
<Chip px={10} py={3} fontSize="$1" fontFamily="$mono" color="var(--white)" mt="$1">
|
|
{t('+${amount} Credits', { amount: activeRebate })}
|
|
</Chip>
|
|
</YStack>
|
|
</XStack>
|
|
|
|
{/* Applied Coupon Banner */}
|
|
{applied?.valid && (
|
|
<XStack
|
|
items="center"
|
|
justify="space-between"
|
|
p="$2"
|
|
px="$3"
|
|
rounded="var(--radius-md)"
|
|
bg="$panel"
|
|
$platform-web={{ backgroundColor: 'color-mix(in srgb, var(--state-success) 12%, transparent)' }}
|
|
borderWidth={1}
|
|
borderColor="var(--border-selected)"
|
|
>
|
|
<XStack items="center" gap="$2">
|
|
<Check size={14} color="var(--state-success)" />
|
|
<Text fontSize="$1" color="var(--state-success)" fontWeight="600">
|
|
{t(applied.message, { label: applied.label ? t(applied.label) : '', amount: applied.discountAmount })}
|
|
</Text>
|
|
</XStack>
|
|
<Box
|
|
render="button"
|
|
onClick={handleRemove}
|
|
p="$1"
|
|
rounded="var(--radius-sm)"
|
|
$platform-web={{
|
|
cursor: 'pointer',
|
|
background: 'transparent',
|
|
border: 'none',
|
|
color: 'var(--muted-foreground)',
|
|
}}
|
|
>
|
|
<X size={14} />
|
|
</Box>
|
|
</XStack>
|
|
)}
|
|
|
|
{/* Coupon Input Form */}
|
|
{!applied?.valid ? (
|
|
<XStack render={<form onSubmit={handleApply} />} gap="$2" width="100%" mt="$1">
|
|
<Box flex={1} position="relative">
|
|
<Input
|
|
type="text"
|
|
placeholder={t('Coupon code (e.g. STUDENT50, HANZO20)')}
|
|
value={code}
|
|
onChange={(e) => {
|
|
setCode(e.target.value.toUpperCase())
|
|
if (error) setError(null)
|
|
}}
|
|
width="100%"
|
|
pl={34}
|
|
borderColor={error ? 'var(--state-error)' : 'var(--border)'}
|
|
fontSize="$1"
|
|
fontFamily="$mono"
|
|
/>
|
|
<Box position="absolute" l={10} t={10} pointerEvents="none" opacity={0.6}>
|
|
<Tag size={14} color="var(--white)" />
|
|
</Box>
|
|
</Box>
|
|
<Action
|
|
render="button"
|
|
type="submit"
|
|
px={14}
|
|
py={8}
|
|
fontSize="$1"
|
|
>
|
|
{t('Apply')}
|
|
</Action>
|
|
</XStack>
|
|
) : null}
|
|
|
|
{error && (
|
|
<Text fontSize="$1" color="var(--state-error)" mt={-4}>
|
|
{error}
|
|
</Text>
|
|
)}
|
|
|
|
{/* Suggested Quick Promos */}
|
|
{!applied?.valid && (
|
|
<XStack items="center" gap="$2" flexWrap="wrap" pt="$1">
|
|
<Text fontSize="$1" color="var(--muted-foreground)">
|
|
{t('Suggested codes:')}
|
|
</Text>
|
|
{['STUDENT50', 'HANZO20', 'EARLYBIRD'].map((sample) => (
|
|
<Text
|
|
key={sample}
|
|
render="button"
|
|
onClick={() => {
|
|
setCode(sample)
|
|
const res = validateCoupon(sample, originalPrice)
|
|
if (res.valid) {
|
|
setApplied(res)
|
|
setError(null)
|
|
onCouponChange?.(res)
|
|
}
|
|
}}
|
|
px={8}
|
|
py={2}
|
|
rounded="var(--radius-sm)"
|
|
bg="var(--card)"
|
|
borderWidth={1}
|
|
borderColor="var(--border)"
|
|
fontSize="$1"
|
|
$platform-web={{
|
|
cursor: 'pointer',
|
|
fontFamily: 'monospace',
|
|
color: 'var(--muted-foreground)',
|
|
}}
|
|
hoverStyle={{ borderColor: 'var(--white)' }}
|
|
>
|
|
{sample}
|
|
</Text>
|
|
))}
|
|
</XStack>
|
|
)}
|
|
|
|
{/* Primary Checkout Action Button */}
|
|
<YStack gap="$2" mt="$2">
|
|
<Action href={checkoutHref} fill>
|
|
{t('Enroll in {course} — ${price} USD →', { course: courseCode, price: activePrice })}
|
|
</Action>
|
|
<Text
|
|
fontSize="$1"
|
|
color="var(--muted-foreground)"
|
|
$platform-web={{ textAlign: 'center', display: 'block' }}
|
|
>
|
|
{t('Includes {amount} compute credit rebate, Hanzo Visor sandbox pod & HACE Credential', { amount: activeRebate })}
|
|
</Text>
|
|
</YStack>
|
|
</YStack>
|
|
</YStack>
|
|
)
|
|
}
|