Files
hanzo.ai/app/(marketing)/university/CouponInput.tsx
zeekay 6c65e3e941 @hanzo/app 0.2.0: the app SDK, renamed from @hanzo/rooms
Every import, the catalog pin and the docs name @hanzo/app and
github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and
components/rooms.tsx is components/app.tsx.
2026-10-22 19:07:20 -07:00

258 lines
8.0 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'
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 [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('Please enter a coupon code.')
return
}
const res = validateCoupon(code, originalPrice)
if (res.valid) {
setApplied(res)
setError(null)
onCouponChange?.(res)
} else {
setError(res.message)
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">
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)">
{applied.discountPercent}% OFF
</Chip>
)}
</XStack>
</YStack>
<YStack items="flex-end">
<Text fontSize="$1" color="var(--muted-foreground)" fontFamily="$mono">
COMPUTE REBATE (25%)
</Text>
<Chip px={10} py={3} fontSize="$1" fontFamily="$mono" color="var(--white)" mt="$1">
+${activeRebate} Credits
</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">
{applied.message}
</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="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"
>
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)">
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>
Enroll in {courseCode} — ${activePrice} USD →
</Action>
<Text
fontSize="$1"
color="var(--muted-foreground)"
$platform-web={{ textAlign: 'center', display: 'block' }}
>
Includes {activeRebate} compute credit rebate, Hanzo Visor sandbox pod & HACE Credential
</Text>
</YStack>
</YStack>
</YStack>
)
}