All Taste components
Commerce
Checkout summary
An order summary with line items, a coupon, GST and the total on the pay button.
line items · applied coupon · subtotal · discount · GST 18% · pay button
Order summary
- ₹30,000Growth plan, yearlyUp to 4 seats · billed once
- ₹10,000Extra seats4 × ₹2,500
WELCOME10 · 10% off
Subtotal₹40,000
Discount−₹4,000
GST (18%)₹6,480
Total₹42,480
Secure payment · GST invoice emailed to you
"use client";
import { useState } from "react";
import { Lock, ShieldCheck, Ticket } from "lucide-react";
export interface LineItem { id: string; name: string; detail?: string; qty: number; unitPrice: number; icon?: React.ReactNode }
const inr = (v: number) => "₹" + Math.round(v).toLocaleString("en-IN");
const sample: LineItem[] = [
{ id: "plan", name: "Growth plan, yearly", detail: "Up to 4 seats · billed once", qty: 1, unitPrice: 30000 },
{ id: "seats", name: "Extra seats", qty: 4, unitPrice: 2500 },
];
/** Coupons you accept: code → percent off. Validate on the server too. */
const coupons: Record<string, number> = { WELCOME10: 10 };
export function CheckoutSummary({ items = sample, gstRate = 0.18, onPay }:
{ items?: LineItem[]; gstRate?: number; onPay?: (total: number, coupon?: string) => void }) {
const [code, setCode] = useState("");
const [applied, setApplied] = useState<string | undefined>("WELCOME10");
const [error, setError] = useState<string | null>(null);
const subtotal = items.reduce((s, i) => s + i.qty * i.unitPrice, 0);
const discount = applied ? (subtotal * coupons[applied]) / 100 : 0;
const gst = (subtotal - discount) * gstRate;
const total = subtotal - discount + gst;
const apply = () => {
const c = code.trim().toUpperCase();
if (coupons[c]) { setApplied(c); setError(null); setCode(""); }
else setError("That code isn't valid.");
};
return (
<section className="w-full max-w-[420px] rounded-lg border border-border bg-bg p-5">
<h3 className="text-sm font-medium">Order summary</h3>
<ul className="mt-4 grid gap-3.5">
{items.map((i) => (
<li key={i.id} className="flex items-start gap-3 text-[13.5px]">
<span className="grid size-10 shrink-0 place-items-center rounded bg-surface-2 text-fg-muted">{i.icon ?? i.name[0]}</span>
<div className="min-w-0 flex-1">
<p className="font-medium">{i.name}</p>
<p className="text-[12.5px] tabular-nums text-fg-subtle">{i.detail ?? i.qty + " × " + inr(i.unitPrice)}</p>
</div>
<span className="tabular-nums">{inr(i.qty * i.unitPrice)}</span>
</li>
))}
</ul>
<div className="mt-4 flex gap-2">
<input value={code} onChange={(e) => setCode(e.target.value)} placeholder="Coupon code" aria-label="Coupon code"
aria-invalid={error ? true : undefined}
className="h-9 min-w-0 flex-1 rounded border border-border-strong bg-bg px-3 text-sm uppercase outline-none placeholder:normal-case placeholder:text-fg-subtle focus:border-primary focus:ring-[3px] focus:ring-primary/20" />
<button onClick={apply} className="h-9 rounded border border-border-strong px-3.5 text-sm font-medium hover:bg-surface">Apply</button>
</div>
{error && <p className="mt-1.5 text-xs text-danger">{error}</p>}
{applied && (
<button onClick={() => setApplied(undefined)} className="mt-2 inline-flex h-[22px] items-center gap-1 rounded-full bg-success-soft px-2 text-xs font-medium text-success-fg"
aria-label={"Remove coupon " + applied}>
<Ticket className="size-3" aria-hidden /> {applied} · {coupons[applied]}% off ×
</button>
)}
<dl className="mt-4 grid gap-2 border-t border-border pt-4 text-[13.5px] [&_dd]:tabular-nums">
<div className="flex justify-between"><dt className="text-fg-muted">Subtotal</dt><dd>{inr(subtotal)}</dd></div>
{discount > 0 && <div className="flex justify-between"><dt className="text-fg-muted">Discount</dt><dd className="text-success-fg">−{inr(discount)}</dd></div>}
<div className="flex justify-between"><dt className="text-fg-muted">GST ({gstRate * 100}%)</dt><dd>{inr(gst)}</dd></div>
<div className="mt-1 flex justify-between border-t border-border pt-3 text-base font-medium"><dt>Total</dt><dd>{inr(total)}</dd></div>
</dl>
<button onClick={() => onPay?.(total, applied)}
className="mt-4 inline-flex h-11 w-full items-center justify-center gap-2 rounded bg-primary text-[15px] font-medium text-primary-fg hover:bg-primary-hover">
<Lock className="size-4" aria-hidden /> Pay {inr(total)}
</button>
<p className="mt-2.5 flex items-center justify-center gap-1.5 text-[12.5px] text-fg-subtle">
<ShieldCheck className="size-3.5" aria-hidden /> Secure payment · GST invoice emailed to you
</p>
</section>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.