Here's the complete UI syntax for the loyalty wizard—copy the full SetupWizard component:
// ─── Setup Wizard ───────────────────────────────────────────────────────────
interface ProgramConfig {
name: string
earnRate: number
minimumRedemption: number
redemptionValue: number
}
function SetupWizard({ onComplete }: { onComplete: (config: ProgramConfig) => void }) {
const [step, setStep] = useState(0)
const [config, setConfig] = useState<ProgramConfig>({
name: "",
earnRate: 1,
minimumRedemption: 100,
redemptionValue: 5,
})
const steps = [
{ title: "Name", subtitle: "Identity" },
{ title: "Earning", subtitle: "Accumulation" },
{ title: "Redemption", subtitle: "Value" },
{ title: "Review", subtitle: "Launch" },
]
const canAdvance = useCallback(() => {
if (step === 0) return config.name.trim().length > 0
if (step === 1) return config.earnRate > 0
if (step === 2) return config.minimumRedemption > 0 && config.redemptionValue > 0
return true
}, [step, config])
const next = () => {
if (step < 3 && canAdvance()) setStep(step + 1)
}
const prev = () => {
if (step > 0) setStep(step - 1)
}
return (
<main className="flex-1 overflow-auto">
<div className="min-h-full flex flex-col">
{/* Top bar */}
<div className="px-4 pt-5 pb-0 md:p-8">
<p className="text-xs font-medium tracking-widest uppercase text-muted-foreground">
Set up loyalty
</p>
</div>
{/* Content container */}
<div className="flex-1 flex items-start md:items-center justify-center px-4 pt-6 pb-28 md:px-6 md:pb-20">
<div className="w-full max-w-xl">
{/* Step indicators */}
<div className="flex items-center gap-2 md:gap-3 mb-8 md:mb-12">
{steps.map((s, i) => (
<button
key={s.title}
type="button"
onClick={() => i < step && setStep(i)}
className={cn(
"flex-1 text-left transition-all",
i <= step ? "opacity-100" : "opacity-30"
)}
>
<div className={cn(
"h-0.5 w-full mb-2 md:mb-3 transition-all",
i <= step ? "bg-foreground" : "bg-border"
)} />
<p className="text-[10px] md:text-xs text-muted-foreground hidden md:block">{s.subtitle}</p>
<p className={cn(
"text-xs md:text-sm font-medium",
i === step ? "text-foreground" : "text-muted-foreground"
)}>{s.title}</p>
</button>
))}
</div>
{/* Step 0: Name */}
{step === 0 && (
<div className="space-y-5 md:space-y-8">
<div>
<h1 className="text-2xl md:text-3xl font-serif font-medium text-foreground text-balance leading-tight">
What should we call your loyalty program?
</h1>
<p className="text-muted-foreground mt-2 md:mt-3 text-sm md:text-base leading-relaxed">
This is the name your customers will see. Choose something that reflects your brand.
</p>
</div>
<div>
<Input
value={config.name}
onChange={(e) => setConfig({ ...config, name: e.target.value })}
placeholder="e.g. Point Coffee Rewards"
className="h-12 md:h-14 text-base md:text-lg px-4 bg-transparent border-border"
autoFocus
/>
<p className="text-[11px] text-muted-foreground mt-1.5">
You can change this later in program settings.
</p>
</div>
{/* Inspiration */}
<div className="bg-muted/50 rounded-lg p-4 md:p-6 space-y-3 md:space-y-4 border border-border">
<p className="text-[10px] md:text-xs font-medium text-muted-foreground uppercase tracking-wide">
Inspiration from industry leaders
</p>
<div className="grid grid-cols-2 gap-x-3 gap-y-2.5">
<div>
<p className="text-xs md:text-sm font-medium text-foreground">Starbucks Rewards</p>
<p className="text-[10px] md:text-xs text-muted-foreground">Emphasizes member status</p>
</div>
<div>
<p className="text-xs md:text-sm font-medium text-foreground">Kroger Rewards</p>
<p className="text-[10px] md:text-xs text-muted-foreground">Direct & clear</p>
</div>
<div>
<p className="text-xs md:text-sm font-medium text-foreground">Target Circle</p>
<p className="text-[10px] md:text-xs text-muted-foreground">Community-focused</p>
</div>
<div>
<p className="text-xs md:text-sm font-medium text-foreground">Meijer mPerks</p>
<p className="text-[10px] md:text-xs text-muted-foreground">Personal & playful</p>
</div>
</div>
</div>
</div>
)}
{/* Step 1: Earning */}
{step === 1 && (
<div className="space-y-5 md:space-y-8">
<div>
<h1 className="text-2xl md:text-3xl font-serif font-medium text-foreground text-balance leading-tight">
How do customers earn points?
</h1>
<p className="text-muted-foreground mt-2 md:mt-3 text-sm md:text-base leading-relaxed">
Set the rate at which customers accumulate points for every dollar spent.
</p>
</div>
<div className="bg-card border border-border rounded-xl p-4 md:p-6">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm md:text-base font-medium text-foreground">Points per dollar</p>
<p className="text-xs md:text-sm text-muted-foreground mt-0.5 md:mt-1">
Points earned for each $1 spent
</p>
</div>
<div className="flex items-center gap-2 md:gap-3">
<Button
variant="outline"
size="icon"
className="h-9 w-9 md:h-10 md:w-10 rounded-full bg-transparent"
onClick={() => setConfig({ ...config, earnRate: Math.max(0.5, config.earnRate - 0.5) })}
>
<Minus className="w-3.5 h-3.5 md:w-4 md:h-4" />
</Button>
<span className="text-2xl md:text-3xl font-semibold tabular-nums w-10 md:w-12 text-center">
{config.earnRate}
</span>
<Button
variant="outline"
size="icon"
className="h-9 w-9 md:h-10 md:w-10 rounded-full bg-transparent"
onClick={() => setConfig({ ...config, earnRate: config.earnRate + 0.5 })}
>
<Plus className="w-3.5 h-3.5 md:w-4 md:h-4" />
</Button>
</div>
</div>
<div className="mt-4 pt-4 md:mt-6 md:pt-6 border-t border-border">
<div className="flex items-center gap-2.5 md:gap-3 text-xs md:text-sm text-muted-foreground">
<div className="w-7 h-7 md:w-8 md:h-8 rounded-full bg-primary/10 flex items-center justify-center shrink-0">
<Zap className="w-3.5 h-3.5 md:w-4 md:h-4 text-primary" />
</div>
<span>
A $25 purchase earns <strong className="text-foreground">{(25 * config.earnRate).toFixed(0)} points</strong>
</span>
</div>
</div>
</div>
</div>
)}
{/* Step 2: Redemption */}
{step === 2 && (
<div className="space-y-5 md:space-y-8">
<div>
<h1 className="text-2xl md:text-3xl font-serif font-medium text-foreground text-balance leading-tight">
How do customers redeem points?
</h1>
<p className="text-muted-foreground mt-2 md:mt-3 text-sm md:text-base leading-relaxed">
Define the minimum balance required and how much each redemption is worth.
</p>
</div>
<div className="space-y-3 md:space-y-4">
<div className="bg-card border border-border rounded-xl p-4 md:p-6">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm md:text-base font-medium text-foreground">Minimum to redeem</p>
<p className="text-xs md:text-sm text-muted-foreground mt-0.5 md:mt-1">
Points needed to redeem
</p>
</div>
<div className="flex items-center gap-1.5">
<Input
type="number"
value={config.minimumRedemption}
onChange={(e) => setConfig({ ...config, minimumRedemption: Number(e.target.value) })}
className="w-20 md:w-24 h-9 md:h-10 text-right text-base md:text-lg font-semibold bg-transparent"
/>
<span className="text-xs md:text-sm text-muted-foreground">pts</span>
</div>
</div>
</div>
<div className="bg-card border border-border rounded-xl p-4 md:p-6">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm md:text-base font-medium text-foreground">Redemption value</p>
<p className="text-xs md:text-sm text-muted-foreground mt-0.5 md:mt-1">
Dollar discount applied
</p>
</div>
<div className="flex items-center gap-1">
<span className="text-base md:text-lg text-muted-foreground">$</span>
<Input
type="number"
value={config.redemptionValue}
onChange={(e) => setConfig({ ...config, redemptionValue: Number(e.target.value) })}
className="w-20 md:w-24 h-9 md:h-10 text-right text-base md:text-lg font-semibold bg-transparent"
/>
</div>
</div>
</div>
<div className="flex items-center gap-2.5 md:gap-3 px-1 text-xs md:text-sm text-muted-foreground">
<div className="w-7 h-7 md:w-8 md:h-8 rounded-full bg-primary/10 flex items-center justify-center shrink-0">
<Gift className="w-3.5 h-3.5 md:w-4 md:h-4 text-primary" />
</div>
<span>
{config.minimumRedemption} points = <strong className="text-foreground">${config.redemptionValue} off</strong> their next purchase
</span>
</div>
</div>
</div>
)}
{/* Step 3: Review */}
{step === 3 && (
<div className="space-y-5 md:space-y-8">
<div>
<h1 className="text-2xl md:text-3xl font-serif font-medium text-foreground text-balance leading-tight">
Review your program
</h1>
<p className="text-muted-foreground mt-2 md:mt-3 text-sm md:text-base leading-relaxed">
Everything looks good? You can adjust these settings at any time after launch.
</p>
</div>
<div className="bg-card border border-border rounded-xl divide-y divide-border overflow-hidden">
<div className="px-4 py-3.5 md:p-5 flex items-center justify-between">
<div>
<p className="text-xs md:text-sm text-muted-foreground">Program name</p>
<p className="text-sm md:text-base font-medium text-foreground mt-0.5">{config.name}</p>
</div>
<button
type="button"
onClick={() => setStep(0)}
className="text-xs md:text-sm text-primary hover:underline"
>
Edit
</button>
</div>
<div className="px-4 py-3.5 md:p-5 flex items-center justify-between">
<div>
<p className="text-xs md:text-sm text-muted-foreground">Earn rate</p>
<p className="text-sm md:text-base font-medium text-foreground mt-0.5">{config.earnRate} point{config.earnRate !== 1 ? "s" : ""} per $1</p>
</div>
<button
type="button"
onClick={() => setStep(1)}
className="text-xs md:text-sm text-primary hover:underline"
>
Edit
</button>
</div>
<div className="px-4 py-3.5 md:p-5 flex items-center justify-between">
<div>
<p className="text-xs md:text-sm text-muted-foreground">Redemption</p>
<p className="text-sm md:text-base font-medium text-foreground mt-0.5">{config.minimumRedemption} pts = ${config.redemptionValue} off</p>
</div>
<button
type="button"
onClick={() => setStep(2)}
className="text-xs md:text-sm text-primary hover:underline"
>
Edit
</button>
</div>
</div>
<div className="bg-muted/50 rounded-xl p-4 md:p-5">
<p className="text-xs md:text-sm text-muted-foreground leading-relaxed">
Once activated, customers will automatically earn points on eligible transactions. Points are calculated after discounts and before tax. Refunds will reverse earned points proportionally.
</p>
</div>
</div>
)}
{/* Navigation */}
<div className="flex items-center justify-between mt-8 md:mt-12">
<Button
variant="ghost"
onClick={prev}
className={cn("text-sm", step === 0 && "invisible")}
>
<ArrowLeft className="w-4 h-4 mr-1.5" />
Back
</Button>
{step < 3 ? (
<Button onClick={next} disabled={!canAdvance()} className="px-6 md:px-8 text-sm">
Continue
<ArrowRight className="w-4 h-4 ml-1.5" />
</Button>
) : (
<Button onClick={() => onComplete(config)} className="px-6 md:px-8 text-sm">
<Check className="w-4 h-4 mr-1.5" />
Launch Program
</Button>
)}
</div>
</div>
</div>
</div>
</main>
)
}Key responsive classes: md: prefix scales everything for desktop, text-2xl md:text-3xl, h-12 md:h-14, p-4 md:p-6 for padding, space-y-5 md:space-y-8 for spacing. All inputs are bg-transparent to blend with the card. Progress bar uses h-0.5 line with full-width fill. Step navigation uses keyboard-aware canAdvance() validation before moving forward.