Ticketing — Checkout
A bold event-ticketing checkout page with a live order summary, seat-tier legend, quantity steppers, and a perforated ticket motif. Apply a promo code for instant discounts, watch the service-fee and facility-charge breakdown recalculate, and race a ten-minute seat-hold countdown. Inline-validated attendee and payment forms format card details as you type, and placing the order reveals an animated success ticket with a generated QR placeholder. Vanilla HTML, CSS, and JavaScript only.
MCP
Code
:root {
--brand: #ff2e63;
--brand-d: #d61f4e;
--ink: #0e0e16;
--ink-2: #3a3a4d;
--muted: #6c6c80;
--bg: #f5f4f9;
--surface: #ffffff;
--line: rgba(14, 14, 22, 0.1);
--ok: #16a34a;
--warn: #d97706;
--danger: #dc2626;
--accent: #7c3aed;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-sm: 0 2px 8px rgba(14, 14, 22, 0.08);
--sh-md: 0 12px 30px rgba(14, 14, 22, 0.12);
--sh-lg: 0 24px 60px rgba(14, 14, 22, 0.22);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1200px 500px at 100% -10%, rgba(255, 46, 99, 0.12), transparent 60%),
radial-gradient(1000px 480px at -10% 0%, rgba(124, 58, 237, 0.12), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.wrap { width: min(1080px, 100% - 40px); margin-inline: auto; }
/* Topbar */
.topbar {
position: sticky;
top: 0;
z-index: 20;
background: rgba(255, 255, 255, 0.82);
backdrop-filter: saturate(140%) blur(10px);
border-bottom: 1px solid var(--line);
}
.topbar-inner { display: flex; align-items: center; justify-content: space-between; gap: 16px; height: 64px; }
.brand { display: inline-flex; align-items: center; gap: 8px; text-decoration: none; font-weight: 800; color: var(--ink); }
.brand-mark { color: var(--brand); font-size: 20px; transform: translateY(-1px); }
.brand-name { letter-spacing: -0.02em; font-size: 18px; }
.hold {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--ink);
color: #fff;
padding: 7px 14px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
}
.hold-label { color: rgba(255, 255, 255, 0.7); font-weight: 500; }
.hold-time { font-variant-numeric: tabular-nums; font-weight: 800; letter-spacing: 0.02em; }
.hold.warn { background: var(--danger); animation: pulse 1s ease-in-out infinite; }
.hold-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 0 rgba(22, 163, 74, 0.6); animation: pulse 1.6s ease-out infinite; }
.hold.warn .hold-dot { background: #fff; animation: none; }
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(22, 163, 74, 0.5); } 70% { box-shadow: 0 0 0 8px rgba(22, 163, 74, 0); } 100% { box-shadow: 0 0 0 0 rgba(22, 163, 74, 0); } }
/* Layout */
.layout {
display: grid;
grid-template-columns: 1fr 400px;
gap: 28px;
align-items: start;
padding: 32px 0 64px;
}
.col-aside { position: sticky; top: 88px; }
/* Steps */
.steps ol { list-style: none; display: flex; gap: 10px; margin: 0 0 18px; padding: 0; flex-wrap: wrap; }
.steps li { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--muted); }
.steps .num { width: 22px; height: 22px; display: grid; place-items: center; border-radius: 50%; border: 1.5px solid var(--line); font-size: 12px; }
.steps li::after { content: ""; width: 18px; height: 1.5px; background: var(--line); margin-left: 4px; }
.steps li:last-child::after { display: none; }
.steps .done { color: var(--ok); }
.steps .done .num { background: var(--ok); border-color: var(--ok); color: #fff; }
.steps .current { color: var(--ink); }
.steps .current .num { background: var(--brand); border-color: var(--brand); color: #fff; }
.page-title { font-size: 28px; font-weight: 800; letter-spacing: -0.03em; margin: 0 0 20px; }
/* Cards */
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-sm);
padding: 22px;
margin: 0 0 18px;
}
fieldset.card { border: 1px solid var(--line); }
.card-title { font-size: 15px; font-weight: 700; letter-spacing: -0.01em; padding: 0; margin-bottom: 14px; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
/* Fields */
.field { display: block; margin-bottom: 14px; }
.field:last-child { margin-bottom: 0; }
.field-label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; color: var(--ink-2); }
.hint { color: var(--muted); font-weight: 500; }
.field input {
width: 100%;
font: inherit;
font-size: 15px;
padding: 11px 13px;
border: 1.5px solid var(--line);
border-radius: var(--r-sm);
background: #fff;
color: var(--ink);
transition: border-color 0.15s, box-shadow 0.15s;
}
.field input::placeholder { color: #aaa; }
.field input:focus-visible { outline: none; border-color: var(--brand); box-shadow: 0 0 0 4px rgba(255, 46, 99, 0.14); }
.field.invalid input { border-color: var(--danger); }
.field.invalid input:focus-visible { box-shadow: 0 0 0 4px rgba(220, 38, 38, 0.14); }
.field-error { display: block; min-height: 0; font-size: 12px; font-weight: 600; color: var(--danger); margin-top: 5px; }
.field-error:empty { display: none; }
.check { display: flex; align-items: flex-start; gap: 10px; font-size: 13.5px; color: var(--ink-2); margin-top: 4px; cursor: pointer; }
.check input { width: 17px; height: 17px; margin-top: 1px; accent-color: var(--brand); flex: none; }
.paychips { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
.paychip { font-size: 11px; font-weight: 800; letter-spacing: 0.04em; padding: 5px 9px; border-radius: 6px; background: var(--bg); border: 1px solid var(--line); color: var(--ink-2); }
.paychip.secure { color: var(--ok); border-color: rgba(22, 163, 74, 0.3); background: rgba(22, 163, 74, 0.08); }
/* Pay button */
.btn-pay {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font: inherit;
font-weight: 800;
font-size: 16px;
color: #fff;
background: linear-gradient(120deg, var(--brand), var(--brand-d));
border: none;
border-radius: var(--r-md);
padding: 16px 22px;
cursor: pointer;
box-shadow: 0 12px 26px rgba(255, 46, 99, 0.32);
transition: transform 0.12s, box-shadow 0.12s, filter 0.12s;
}
.btn-pay:hover { transform: translateY(-1px); box-shadow: 0 16px 34px rgba(255, 46, 99, 0.4); }
.btn-pay:active { transform: translateY(0); }
.btn-pay:disabled { filter: grayscale(0.4) brightness(0.95); cursor: progress; }
.btn-pay-amount { font-variant-numeric: tabular-nums; }
.btn-pay.loading .btn-pay-label { opacity: 0.6; }
.reassure { font-size: 12.5px; color: var(--muted); text-align: center; margin: 12px 0 0; }
/* Summary / event */
.summary { padding: 0; overflow: hidden; }
.event-hero {
position: relative;
height: 130px;
background:
radial-gradient(120px 120px at 80% 20%, rgba(255, 255, 255, 0.35), transparent 70%),
linear-gradient(135deg, var(--accent), var(--brand) 70%, #ff7a3d);
}
.event-hero::after {
content: "";
position: absolute;
inset: 0;
background-image: radial-gradient(rgba(255, 255, 255, 0.18) 1px, transparent 1.4px);
background-size: 14px 14px;
mix-blend-mode: overlay;
}
.event-hero-tag {
position: absolute;
left: 14px;
bottom: 12px;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.08em;
color: #fff;
background: rgba(14, 14, 22, 0.5);
padding: 5px 10px;
border-radius: 999px;
backdrop-filter: blur(4px);
}
.event-body { padding: 18px 20px 8px; }
.badge { display: inline-block; font-size: 11px; font-weight: 800; letter-spacing: 0.02em; padding: 4px 10px; border-radius: 999px; margin-bottom: 10px; }
.badge-low { color: var(--warn); background: rgba(217, 119, 6, 0.12); border: 1px solid rgba(217, 119, 6, 0.28); }
.badge-out { color: var(--danger); background: rgba(220, 38, 38, 0.1); border: 1px solid rgba(220, 38, 38, 0.28); }
.event-name { font-size: 20px; font-weight: 800; letter-spacing: -0.02em; margin: 0 0 8px; }
.event-meta { display: flex; flex-direction: column; gap: 3px; font-size: 13.5px; color: var(--ink-2); margin: 0 0 14px; }
.countdown { display: flex; gap: 8px; }
.cd-cell { background: var(--ink); color: #fff; border-radius: var(--r-sm); padding: 7px 0; width: 54px; text-align: center; }
.cd-cell b { display: block; font-size: 18px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1; }
.cd-cell i { font-style: normal; font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; color: rgba(255, 255, 255, 0.65); }
/* Perforation */
.perforation { position: relative; height: 22px; margin: 4px 0; }
.perforation::before { content: ""; position: absolute; left: 14px; right: 14px; top: 50%; border-top: 2px dashed var(--line); }
.perforation::after { content: ""; position: absolute; left: -11px; right: -11px; top: 50%; height: 22px; transform: translateY(-50%); }
.summary .perforation::after,
.summary .perforation { background: transparent; }
/* notch circles cut from card edges */
.summary > .perforation { background: transparent; }
.summary > .perforation::before { content: ""; position: absolute; left: 14px; right: 14px; top: 50%; border-top: 2px dashed var(--line); }
.summary > .perforation { box-shadow: none; }
.perforation .notch { display: none; }
.legend { list-style: none; display: flex; gap: 14px; flex-wrap: wrap; margin: 0; padding: 0 20px 8px; font-size: 12.5px; font-weight: 600; color: var(--ink-2); }
.legend li { display: inline-flex; align-items: center; gap: 6px; }
.legend .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--c, var(--brand)); }
/* order lines */
.lines { list-style: none; margin: 0; padding: 6px 20px; }
.line { display: grid; grid-template-columns: auto 1fr auto; grid-template-areas: "dot info qty" "dot info total"; column-gap: 10px; align-items: center; padding: 12px 0; border-bottom: 1px solid var(--line); }
.line:last-child { border-bottom: none; }
.line-dot { grid-area: dot; width: 10px; height: 38px; border-radius: 6px; background: var(--c, var(--brand)); }
.line-info { grid-area: info; min-width: 0; }
.line-name { display: block; font-size: 14px; font-weight: 700; }
.line-sub { display: block; font-size: 12px; color: var(--muted); }
.qty { grid-area: qty; display: inline-flex; align-items: center; gap: 4px; justify-self: end; }
.qbtn { width: 24px; height: 24px; border: 1.5px solid var(--line); background: #fff; border-radius: 6px; font-size: 15px; font-weight: 700; line-height: 1; color: var(--ink-2); cursor: pointer; transition: border-color 0.12s, color 0.12s, background 0.12s; }
.qbtn:hover { border-color: var(--brand); color: var(--brand); }
.qbtn:disabled { opacity: 0.4; cursor: not-allowed; }
.qnum { min-width: 20px; text-align: center; font-weight: 700; font-variant-numeric: tabular-nums; font-size: 14px; }
.line-total { grid-area: total; justify-self: end; font-weight: 700; font-size: 14px; font-variant-numeric: tabular-nums; }
/* promo */
.promo { padding: 6px 20px 2px; }
.promo-field { margin-bottom: 6px; }
.promo-row { display: flex; gap: 8px; }
.promo-row input { flex: 1; min-width: 0; text-transform: uppercase; }
.promo-row input::placeholder { text-transform: none; }
.btn-promo { font: inherit; font-weight: 700; font-size: 14px; padding: 0 16px; border-radius: var(--r-sm); border: 1.5px solid var(--ink); background: var(--ink); color: #fff; cursor: pointer; transition: opacity 0.12s, transform 0.12s; flex: none; }
.btn-promo:hover { opacity: 0.88; }
.btn-promo:active { transform: scale(0.97); }
.btn-promo.applied { background: var(--ok); border-color: var(--ok); }
#promoMsg.ok { color: var(--ok); }
/* totals */
.totals { margin: 0; padding: 8px 20px 22px; }
.trow { display: flex; justify-content: space-between; align-items: baseline; font-size: 14px; padding: 5px 0; color: var(--ink-2); }
.trow dt { margin: 0; }
.trow dd { margin: 0; font-variant-numeric: tabular-nums; font-weight: 600; }
.trow-discount { color: var(--ok); }
.trow-discount dd { color: var(--ok); }
#discountTag { font-size: 11px; font-weight: 800; background: rgba(22, 163, 74, 0.12); padding: 2px 7px; border-radius: 999px; margin-left: 4px; }
.trow-grand { border-top: 1px solid var(--line); margin-top: 6px; padding-top: 12px; font-size: 17px; font-weight: 800; color: var(--ink); }
.trow-grand dd { font-weight: 800; }
/* Overlay / success */
.overlay { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; padding: 20px; background: rgba(14, 14, 22, 0.62); backdrop-filter: blur(4px); animation: fade 0.2s ease; }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.ticket-success { width: min(420px, 100%); background: var(--surface); border-radius: var(--r-lg); overflow: hidden; box-shadow: var(--sh-lg); animation: pop 0.32s cubic-bezier(0.2, 0.9, 0.3, 1.2); }
@keyframes pop { from { transform: translateY(16px) scale(0.96); opacity: 0; } to { transform: none; opacity: 1; } }
.ts-top { text-align: center; padding: 28px 24px 18px; background: linear-gradient(135deg, var(--accent), var(--brand)); color: #fff; }
.ts-check { display: inline-grid; place-items: center; width: 52px; height: 52px; border-radius: 50%; background: rgba(255, 255, 255, 0.18); border: 2px solid rgba(255, 255, 255, 0.6); font-size: 26px; font-weight: 800; margin-bottom: 10px; }
.ts-top h2 { margin: 0; font-size: 24px; font-weight: 800; letter-spacing: -0.02em; }
.ts-sub { margin: 4px 0 0; font-size: 14px; color: rgba(255, 255, 255, 0.88); }
.perforation-dark { height: 18px; }
.perforation-dark::before { border-color: rgba(255, 255, 255, 0.4); }
.ticket-success .perforation-dark { background: var(--accent); }
.ticket-success .perforation-dark::before { content: ""; position: absolute; left: 14px; right: 14px; top: 0; border-top: 2px dashed rgba(255, 255, 255, 0.45); }
.ticket-success .perforation-dark { height: 0; }
.ts-body { padding: 22px 24px 24px; text-align: center; }
.ts-qr { width: 116px; height: 116px; margin: 0 auto 16px; padding: 8px; background: #fff; border: 1px solid var(--line); border-radius: var(--r-sm); box-shadow: var(--sh-sm); }
.qr-grid { width: 100%; height: 100%; display: grid; grid-template-columns: repeat(11, 1fr); grid-template-rows: repeat(11, 1fr); gap: 1px; }
.qr-grid span { background: var(--ink); border-radius: 1px; }
.qr-grid span.off { background: transparent; }
.ts-meta { display: grid; gap: 8px; text-align: left; margin: 0 0 14px; }
.ts-meta div { display: flex; justify-content: space-between; gap: 12px; font-size: 13.5px; padding-bottom: 8px; border-bottom: 1px solid var(--line); }
.ts-meta div:last-child { border-bottom: none; padding-bottom: 0; }
.ts-meta dt { color: var(--muted); margin: 0; }
.ts-meta dd { margin: 0; font-weight: 700; text-align: right; }
.ts-note { font-size: 12.5px; color: var(--muted); margin: 0 0 16px; }
.btn-done { width: 100%; font: inherit; font-weight: 800; font-size: 15px; color: #fff; background: var(--ink); border: none; border-radius: var(--r-md); padding: 13px; cursor: pointer; transition: opacity 0.12s; }
.btn-done:hover { opacity: 0.9; }
/* Toast */
.toast-stack { position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%); z-index: 80; display: flex; flex-direction: column; gap: 8px; align-items: center; pointer-events: none; width: max-content; max-width: calc(100% - 32px); }
.toast { background: var(--ink); color: #fff; font-size: 13.5px; font-weight: 600; padding: 11px 16px; border-radius: 999px; box-shadow: var(--sh-md); animation: toastIn 0.25s ease, toastOut 0.3s ease 2.6s forwards; }
.toast.ok { background: var(--ok); }
.toast.err { background: var(--danger); }
@keyframes toastIn { from { transform: translateY(12px); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes toastOut { to { transform: translateY(8px); opacity: 0; } }
@media (max-width: 920px) {
.layout { grid-template-columns: 1fr; }
.col-aside { position: static; order: -1; }
}
@media (max-width: 520px) {
.wrap { width: calc(100% - 28px); }
.topbar-inner { height: 58px; }
.hold-label { display: none; }
.page-title { font-size: 23px; }
.grid-2 { grid-template-columns: 1fr; }
.layout { padding-top: 22px; gap: 20px; }
.cd-cell { width: 48px; }
.steps li::after { width: 10px; }
}(function () {
"use strict";
var FEE_RATE = 0.08;
var FACILITY = 6.0;
var MAX_PER_TIER = 8;
// Promo codes (clearly fictional)
var PROMOS = {
PULSE15: { type: "pct", value: 0.15, label: "15% off" },
EARLYBIRD: { type: "flat", value: 40, label: "$40 off" },
FANCLUB: { type: "pct", value: 0.1, label: "10% off" }
};
var state = { promo: null };
var $ = function (sel, ctx) { return (ctx || document).querySelector(sel); };
var $$ = function (sel, ctx) { return Array.prototype.slice.call((ctx || document).querySelectorAll(sel)); };
var money = function (n) {
return "$" + n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
/* ---------- Toast ---------- */
function toast(msg, kind) {
var stack = $("#toastStack");
var el = document.createElement("div");
el.className = "toast" + (kind ? " " + kind : "");
el.textContent = msg;
stack.appendChild(el);
setTimeout(function () { el.remove(); }, 3000);
}
/* ---------- Totals ---------- */
function lineSubtotal() {
var sum = 0;
$$(".line").forEach(function (line) {
var price = parseFloat(line.dataset.price);
var qty = parseInt(line.dataset.qty, 10);
sum += price * qty;
});
return sum;
}
function ticketCount() {
return $$(".line").reduce(function (acc, line) {
return acc + parseInt(line.dataset.qty, 10);
}, 0);
}
function recompute() {
// update per-line totals + qty UI
$$(".line").forEach(function (line) {
var price = parseFloat(line.dataset.price);
var qty = parseInt(line.dataset.qty, 10);
$(".line-total", line).textContent = money(price * qty);
$(".qnum", line).textContent = String(qty);
var dec = $('[data-act="dec"]', line);
var inc = $('[data-act="inc"]', line);
if (dec) dec.disabled = qty <= 0;
if (inc) inc.disabled = qty >= MAX_PER_TIER;
});
var subtotal = lineSubtotal();
var discount = 0;
if (state.promo) {
var p = PROMOS[state.promo];
discount = p.type === "pct" ? subtotal * p.value : Math.min(p.value, subtotal);
}
var discounted = subtotal - discount;
var fee = discounted * FEE_RATE;
var facility = subtotal > 0 ? FACILITY : 0;
var grand = discounted + fee + facility;
$("#tSubtotal").textContent = money(subtotal);
$("#tFee").textContent = money(fee);
$("#tFacility").textContent = money(facility);
$("#tGrand").textContent = money(grand);
$("#payBtnAmount").textContent = money(grand);
var dRow = $("#discountRow");
if (discount > 0) {
dRow.hidden = false;
$("#tDiscount").textContent = "−" + money(discount);
$("#discountTag").textContent = PROMOS[state.promo].label;
} else {
dRow.hidden = true;
}
// stock badge based on GA Pit qty
var pit = $$(".line")[0];
var badge = $("#stockBadge");
if (pit) {
var left = 14 - parseInt(pit.dataset.qty, 10) + 2;
if (parseInt(pit.dataset.qty, 10) >= MAX_PER_TIER) {
badge.textContent = "Max " + MAX_PER_TIER + " per order at this tier";
badge.className = "badge badge-out";
} else {
badge.textContent = "Only " + left + " left at this tier";
badge.className = "badge badge-low";
}
}
return { subtotal: subtotal, grand: grand, count: ticketCount() };
}
/* ---------- Qty buttons ---------- */
$("#orderLines").addEventListener("click", function (e) {
var btn = e.target.closest(".qbtn");
if (!btn) return;
var line = btn.closest(".line");
var qty = parseInt(line.dataset.qty, 10);
if (btn.dataset.act === "inc") {
if (qty >= MAX_PER_TIER) { toast("Max " + MAX_PER_TIER + " tickets per tier", "err"); return; }
qty++;
} else {
if (qty <= 0) return;
qty--;
}
line.dataset.qty = qty;
recompute();
});
/* ---------- Promo ---------- */
function applyPromo() {
var input = $("#promoInput");
var msg = $("#promoMsg");
var btn = $("#promoBtn");
var code = input.value.trim().toUpperCase();
if (!code) { msg.className = "field-error"; msg.textContent = "Enter a code first."; return; }
if (state.promo === code) { msg.className = "field-error ok"; msg.textContent = "Already applied."; return; }
if (PROMOS[code]) {
state.promo = code;
msg.className = "field-error ok";
msg.textContent = "Code applied — " + PROMOS[code].label + "!";
btn.textContent = "Applied ✓";
btn.classList.add("applied");
recompute();
toast(PROMOS[code].label + " applied", "ok");
} else {
state.promo = null;
msg.className = "field-error";
msg.textContent = "That code isn't valid.";
btn.textContent = "Apply";
btn.classList.remove("applied");
recompute();
toast("Invalid promo code", "err");
}
}
$("#promoBtn").addEventListener("click", applyPromo);
$("#promoInput").addEventListener("keydown", function (e) {
if (e.key === "Enter") { e.preventDefault(); applyPromo(); }
var btn = $("#promoBtn");
if (state.promo) { state.promo = null; btn.textContent = "Apply"; btn.classList.remove("applied"); $("#promoMsg").textContent = ""; recompute(); }
});
/* ---------- Input formatting ---------- */
var cardInput = $('input[name="card"]');
cardInput.addEventListener("input", function () {
var v = this.value.replace(/\D/g, "").slice(0, 16);
this.value = v.replace(/(.{4})/g, "$1 ").trim();
});
var expInput = $('input[name="exp"]');
expInput.addEventListener("input", function () {
var v = this.value.replace(/\D/g, "").slice(0, 4);
if (v.length >= 3) v = v.slice(0, 2) + "/" + v.slice(2);
this.value = v;
});
$('input[name="cvc"]').addEventListener("input", function () {
this.value = this.value.replace(/\D/g, "").slice(0, 4);
});
/* ---------- Validation ---------- */
var validators = {
firstName: function (v) { return v.trim().length >= 1 || "Enter your first name."; },
lastName: function (v) { return v.trim().length >= 1 || "Enter your last name."; },
email: function (v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()) || "Enter a valid email."; },
phone: function (v) { return v.replace(/\D/g, "").length >= 7 || "Enter a valid mobile number."; },
card: function (v) { return v.replace(/\s/g, "").length === 16 || "Card number must be 16 digits."; },
exp: function (v) {
if (!/^\d{2}\/\d{2}$/.test(v)) return "Use MM/YY.";
var m = parseInt(v.slice(0, 2), 10);
return (m >= 1 && m <= 12) || "Invalid month.";
},
cvc: function (v) { return v.length >= 3 || "CVC must be 3-4 digits."; }
};
function setError(name, msg) {
var input = $('[name="' + name + '"]');
var field = input.closest(".field");
var err = $('.field-error[data-for="' + name + '"]');
if (msg) {
field.classList.add("invalid");
if (err) err.textContent = msg;
} else {
field.classList.remove("invalid");
if (err) err.textContent = "";
}
}
Object.keys(validators).forEach(function (name) {
var input = $('[name="' + name + '"]');
input.addEventListener("blur", function () {
var res = validators[name](input.value);
setError(name, res === true ? null : res);
});
input.addEventListener("input", function () {
if (input.closest(".field").classList.contains("invalid")) {
var res = validators[name](input.value);
if (res === true) setError(name, null);
}
});
});
function validateAll() {
var ok = true;
var firstBad = null;
Object.keys(validators).forEach(function (name) {
var input = $('[name="' + name + '"]');
var res = validators[name](input.value);
if (res !== true) { ok = false; setError(name, res); if (!firstBad) firstBad = input; }
else setError(name, null);
});
var terms = $('[name="terms"]');
if (!terms.checked) { ok = false; toast("Please accept the policy to continue", "err"); if (!firstBad) firstBad = terms; }
if (firstBad) firstBad.focus();
return ok;
}
/* ---------- Hold countdown ---------- */
var holdSeconds = 10 * 60;
var holdEl = $("#holdTime");
var holdWrap = $(".hold");
var holdTimer = setInterval(function () {
holdSeconds--;
if (holdSeconds < 0) {
clearInterval(holdTimer);
holdEl.textContent = "00:00";
toast("Your seat hold expired — reselect to continue", "err");
holdWrap.classList.remove("warn");
$(".hold-label").textContent = "Hold expired";
return;
}
var m = Math.floor(holdSeconds / 60);
var s = holdSeconds % 60;
holdEl.textContent = (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s;
if (holdSeconds <= 60) holdWrap.classList.add("warn");
}, 1000);
/* ---------- Event countdown ---------- */
var eventDate = new Date();
eventDate.setDate(eventDate.getDate() + 66);
function tickEvent() {
var diff = eventDate - new Date();
if (diff < 0) diff = 0;
var d = Math.floor(diff / 86400000);
var h = Math.floor((diff % 86400000) / 3600000);
var mi = Math.floor((diff % 3600000) / 60000);
var pad = function (n) { return (n < 10 ? "0" : "") + n; };
$("#cdDays").textContent = pad(d);
$("#cdHours").textContent = pad(h);
$("#cdMins").textContent = pad(mi);
}
tickEvent();
setInterval(tickEvent, 30000);
/* ---------- QR placeholder ---------- */
function buildQR(seed) {
var grid = $("#qrGrid");
grid.innerHTML = "";
var n = 121;
var s = seed || 7;
for (var i = 0; i < n; i++) {
var cell = document.createElement("span");
s = (s * 1103515245 + 12345) & 0x7fffffff;
var on = (s >> 16) % 100 < 52;
// finder-pattern corners always on
var r = Math.floor(i / 11), c = i % 11;
var corner = (r < 3 && c < 3) || (r < 3 && c > 7) || (r > 7 && c < 3);
if (!on && !corner) cell.className = "off";
grid.appendChild(cell);
}
}
/* ---------- Submit ---------- */
$("#checkoutForm").addEventListener("submit", function (e) {
e.preventDefault();
if (holdSeconds <= 0) { toast("Seat hold expired", "err"); return; }
if (!validateAll()) { toast("Please fix the highlighted fields", "err"); return; }
var totals = recompute();
if (totals.count === 0) { toast("Add at least one ticket", "err"); return; }
var btn = $("#payBtn");
btn.classList.add("loading");
btn.disabled = true;
$(".btn-pay-label").textContent = "Processing…";
setTimeout(function () {
clearInterval(holdTimer);
var ref = "#NP-" + String(Math.floor(100000 + Math.random() * 899999));
$("#orderRef").textContent = ref;
$("#tsCount").textContent = totals.count + (totals.count === 1 ? " ticket" : " tickets");
$("#tsPaid").textContent = money(totals.grand);
$("#tsEmail").textContent = $('[name="email"]').value.trim() || "your inbox";
buildQR(Date.now() % 1000 + 1);
var overlay = $("#overlay");
overlay.hidden = false;
$("#doneBtn").focus();
toast("Payment confirmed", "ok");
}, 1400);
});
$("#doneBtn").addEventListener("click", function () {
$("#overlay").hidden = true;
var btn = $("#payBtn");
btn.classList.remove("loading");
btn.disabled = false;
$(".btn-pay-label").textContent = "Place order";
toast("Tickets saved to your account", "ok");
});
// init
recompute();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Checkout — Neon Pulse Festival</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header class="topbar" role="banner">
<div class="wrap topbar-inner">
<a class="brand" href="#" aria-label="TixWave home">
<span class="brand-mark" aria-hidden="true">◤</span>
<span class="brand-name">TixWave</span>
</a>
<div class="hold" role="status" aria-live="polite">
<span class="hold-dot" aria-hidden="true"></span>
<span class="hold-label">Seats held for</span>
<span class="hold-time" id="holdTime">10:00</span>
</div>
</div>
</header>
<main class="wrap layout" id="main">
<section class="col-main" aria-labelledby="checkoutTitle">
<nav class="steps" aria-label="Checkout progress">
<ol>
<li class="done"><span class="num" aria-hidden="true">1</span> Tickets</li>
<li class="current" aria-current="step"><span class="num" aria-hidden="true">2</span> Details & pay</li>
<li><span class="num" aria-hidden="true">3</span> Confirmation</li>
</ol>
</nav>
<h1 id="checkoutTitle" class="page-title">Secure checkout</h1>
<form id="checkoutForm" novalidate>
<fieldset class="card">
<legend class="card-title">Attendee details</legend>
<div class="grid-2">
<label class="field">
<span class="field-label">First name</span>
<input type="text" name="firstName" autocomplete="given-name" required />
<span class="field-error" data-for="firstName"></span>
</label>
<label class="field">
<span class="field-label">Last name</span>
<input type="text" name="lastName" autocomplete="family-name" required />
<span class="field-error" data-for="lastName"></span>
</label>
</div>
<label class="field">
<span class="field-label">Email <span class="hint">— tickets sent here</span></span>
<input type="email" name="email" autocomplete="email" placeholder="[email protected]" required />
<span class="field-error" data-for="email"></span>
</label>
<label class="field">
<span class="field-label">Mobile</span>
<input type="tel" name="phone" autocomplete="tel" placeholder="+1 555 0100" required />
<span class="field-error" data-for="phone"></span>
</label>
</fieldset>
<fieldset class="card">
<legend class="card-title">Payment</legend>
<div class="paychips" role="group" aria-label="Accepted cards">
<span class="paychip">VISA</span>
<span class="paychip">MC</span>
<span class="paychip">AMEX</span>
<span class="paychip secure"><span class="lock" aria-hidden="true">🔒</span> Encrypted</span>
</div>
<label class="field">
<span class="field-label">Card number</span>
<input type="text" name="card" inputmode="numeric" placeholder="4242 4242 4242 4242" maxlength="19" required />
<span class="field-error" data-for="card"></span>
</label>
<div class="grid-2">
<label class="field">
<span class="field-label">Expiry</span>
<input type="text" name="exp" inputmode="numeric" placeholder="MM/YY" maxlength="5" required />
<span class="field-error" data-for="exp"></span>
</label>
<label class="field">
<span class="field-label">CVC</span>
<input type="text" name="cvc" inputmode="numeric" placeholder="123" maxlength="4" required />
<span class="field-error" data-for="cvc"></span>
</label>
</div>
<label class="check">
<input type="checkbox" name="terms" required />
<span>I agree to the fan code of conduct and refund policy.</span>
</label>
</fieldset>
<button type="submit" class="btn-pay" id="payBtn">
<span class="btn-pay-label">Place order</span>
<span class="btn-pay-amount" id="payBtnAmount">$0.00</span>
</button>
<p class="reassure">You won't be charged until you confirm. Free cancellation up to 48h before doors.</p>
</form>
</section>
<aside class="col-aside" aria-label="Order summary">
<div class="summary card" id="summary">
<div class="event">
<div class="event-hero" aria-hidden="true">
<span class="event-hero-tag">SAT · ALL AGES</span>
</div>
<div class="event-body">
<span class="badge badge-low" id="stockBadge">Only 14 left at this tier</span>
<h2 class="event-name">Neon Pulse Festival 2026</h2>
<p class="event-meta">
<span>📅 Sat, Aug 22 · 6:00 PM</span>
<span>📍 Harbor Lights Arena, Seattle</span>
</p>
<div class="countdown" id="countdown" aria-label="Time until event">
<span class="cd-cell"><b id="cdDays">00</b><i>days</i></span>
<span class="cd-cell"><b id="cdHours">00</b><i>hrs</i></span>
<span class="cd-cell"><b id="cdMins">00</b><i>min</i></span>
</div>
</div>
</div>
<div class="perforation" aria-hidden="true"></div>
<ul class="legend" aria-label="Seat tiers">
<li><span class="dot" style="--c:var(--accent)"></span> GA Pit</li>
<li><span class="dot" style="--c:var(--brand)"></span> Lower Bowl</li>
<li><span class="dot" style="--c:var(--warn)"></span> VIP Deck</li>
</ul>
<ul class="lines" id="orderLines">
<li class="line" data-price="129" data-qty="2">
<span class="line-dot" style="--c:var(--accent)" aria-hidden="true"></span>
<span class="line-info">
<span class="line-name">GA Pit · Standing</span>
<span class="line-sub">Sec PIT · General admission</span>
</span>
<span class="qty" role="group" aria-label="GA Pit quantity">
<button type="button" class="qbtn" data-act="dec" aria-label="Decrease GA Pit">−</button>
<span class="qnum" aria-live="polite">2</span>
<button type="button" class="qbtn" data-act="inc" aria-label="Increase GA Pit">+</button>
</span>
<span class="line-total">$258.00</span>
</li>
<li class="line" data-price="189" data-qty="1">
<span class="line-dot" style="--c:var(--warn)" aria-hidden="true"></span>
<span class="line-info">
<span class="line-name">VIP Deck · Seat F12</span>
<span class="line-sub">Sec 104 · Includes lounge access</span>
</span>
<span class="qty" role="group" aria-label="VIP Deck quantity">
<button type="button" class="qbtn" data-act="dec" aria-label="Decrease VIP Deck">−</button>
<span class="qnum" aria-live="polite">1</span>
<button type="button" class="qbtn" data-act="inc" aria-label="Increase VIP Deck">+</button>
</span>
<span class="line-total">$189.00</span>
</li>
</ul>
<div class="promo">
<label class="field promo-field">
<span class="field-label">Promo code</span>
<div class="promo-row">
<input type="text" id="promoInput" placeholder="Try PULSE15" autocomplete="off" />
<button type="button" class="btn-promo" id="promoBtn">Apply</button>
</div>
<span class="field-error" id="promoMsg"></span>
</label>
</div>
<div class="perforation" aria-hidden="true"></div>
<dl class="totals">
<div class="trow"><dt>Subtotal</dt><dd id="tSubtotal">$447.00</dd></div>
<div class="trow trow-discount" id="discountRow" hidden><dt>Discount <span id="discountTag"></span></dt><dd id="tDiscount">−$0.00</dd></div>
<div class="trow"><dt>Service fee <span class="hint">(8%)</span></dt><dd id="tFee">$35.76</dd></div>
<div class="trow"><dt>Facility charge</dt><dd id="tFacility">$6.00</dd></div>
<div class="trow trow-grand"><dt>Total</dt><dd id="tGrand">$488.76</dd></div>
</dl>
</div>
</aside>
</main>
<!-- Success overlay -->
<div class="overlay" id="overlay" hidden>
<div class="ticket-success" role="alertdialog" aria-modal="true" aria-labelledby="successTitle">
<div class="ts-top">
<span class="ts-check" aria-hidden="true">✓</span>
<h2 id="successTitle">You're in!</h2>
<p class="ts-sub">Order <b id="orderRef">#NP-000000</b> confirmed</p>
</div>
<div class="perforation perforation-dark" aria-hidden="true"></div>
<div class="ts-body">
<div class="ts-qr" aria-hidden="true">
<div class="qr-grid" id="qrGrid"></div>
</div>
<dl class="ts-meta">
<div><dt>Event</dt><dd>Neon Pulse Festival 2026</dd></div>
<div><dt>When</dt><dd>Sat, Aug 22 · 6:00 PM</dd></div>
<div><dt>Tickets</dt><dd id="tsCount">3 tickets</dd></div>
<div><dt>Paid</dt><dd id="tsPaid">$488.76</dd></div>
</dl>
<p class="ts-note">A copy was emailed to <b id="tsEmail">your inbox</b>. Add to wallet at the door.</p>
<button type="button" class="btn-done" id="doneBtn">Done</button>
</div>
</div>
</div>
<div class="toast-stack" id="toastStack" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Checkout
A high-contrast checkout for the fictional Neon Pulse Festival. A sticky order summary anchors the page with a photographic gradient hero, a low-stock badge, a live event countdown, and a color-coded seat-tier legend. Each order line has a quantity stepper that updates its subtotal, and the totals panel breaks the price into subtotal, optional discount, an 8% service fee, and a flat facility charge — all separated by dashed perforation lines that echo a real ticket stub.
The promo box accepts codes like PULSE15, EARLYBIRD, and FANCLUB, applying a
percentage or flat discount and rerunning the math instantly. The attendee and
payment fields validate inline on blur, auto-format the card number and expiry as
you type, and surface clear error messages. A ten-minute seat-hold timer counts
down in the header and turns red in the final minute.
Placing a valid order shows a brief processing state, then an animated success ticket with an order reference, a generated QR placeholder, and the amount paid. Toast notifications confirm each action. Everything runs on vanilla JavaScript with no frameworks or build step.
Illustrative UI only — fictional events, not a real ticketing service.