Ticketing — Event Setup
A bold event setup wizard for organizers. A four-step flow covers event details, a live ticket-tier builder with per-tier color, price and quantity, a seating-mode switch between general admission and reserved sections, and publish toggles. Every edit drives a live ticket-style preview with color-coded tiers, sold-out flags and running capacity and gross-potential totals. Inline validation blocks publishing until the name, venue and a future date are set. Vanilla JS only.
MCP
Code
:root {
--brand: #7c3aed;
--brand-d: #6d28d9;
--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: #ff3d81;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(14, 14, 22, 0.06), 0 4px 12px rgba(14, 14, 22, 0.05);
--sh-2: 0 10px 30px rgba(14, 14, 22, 0.14);
--sh-pop: 0 18px 50px rgba(124, 58, 237, 0.28);
--hero: #7c3aed;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
background:
radial-gradient(1100px 500px at 100% -10%, rgba(124, 58, 237, 0.1), transparent 60%),
radial-gradient(900px 460px at -5% 0%, rgba(255, 61, 129, 0.08), transparent 55%),
var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.skip {
position: absolute;
left: -999px;
top: 8px;
background: var(--ink);
color: #fff;
padding: 8px 14px;
border-radius: var(--r-sm);
z-index: 50;
}
.skip:focus { left: 12px; }
button { font-family: inherit; cursor: pointer; }
:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
/* ---- Topbar ---- */
.topbar {
position: sticky;
top: 0;
z-index: 30;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 14px clamp(16px, 4vw, 40px);
background: rgba(255, 255, 255, 0.82);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark {
width: 34px; height: 34px;
display: grid; place-items: center;
border-radius: 10px;
background: linear-gradient(140deg, var(--brand), var(--accent));
color: #fff; font-size: 19px;
box-shadow: var(--sh-pop);
}
.brand-name { font-weight: 800; letter-spacing: -0.02em; font-size: 18px; }
.brand-sub { color: var(--muted); font-weight: 600; }
.topbar-actions { display: flex; align-items: center; gap: 10px; }
.status-pill {
font-size: 12px; font-weight: 700;
padding: 5px 12px; border-radius: 999px;
background: rgba(14, 14, 22, 0.07); color: var(--ink-2);
text-transform: uppercase; letter-spacing: 0.04em;
}
.status-pill[data-state="live"] { background: rgba(22, 163, 74, 0.14); color: var(--ok); }
.btn {
border: 1px solid transparent;
border-radius: var(--r-sm);
padding: 10px 16px;
font-weight: 700;
font-size: 14px;
transition: transform 0.08s ease, box-shadow 0.15s ease, background 0.15s ease;
}
.btn:active { transform: translateY(1px); }
.btn-primary {
background: linear-gradient(135deg, var(--brand), var(--brand-d));
color: #fff;
box-shadow: var(--sh-pop);
}
.btn-primary:hover { box-shadow: 0 22px 56px rgba(124, 58, 237, 0.4); }
.btn-ghost { background: var(--surface); border-color: var(--line); color: var(--ink); }
.btn-ghost:hover { background: #faf9fe; }
.btn-dashed {
width: 100%;
background: transparent;
border: 2px dashed var(--line);
color: var(--brand-d);
padding: 13px;
font-weight: 700;
}
.btn-dashed:hover { border-color: var(--brand); background: rgba(124, 58, 237, 0.04); }
/* ---- Shell ---- */
.shell {
max-width: 1180px;
margin: 0 auto;
padding: clamp(20px, 4vw, 36px) clamp(16px, 4vw, 40px) 80px;
}
.steps {
display: flex;
flex-wrap: wrap;
gap: 10px 22px;
list-style: none;
margin: 0 0 26px;
padding: 0;
font-weight: 700;
color: var(--muted);
font-size: 14px;
}
.step { display: flex; align-items: center; gap: 9px; }
.step-dot {
width: 26px; height: 26px;
display: grid; place-items: center;
border-radius: 999px;
background: rgba(14, 14, 22, 0.08);
color: var(--ink-2);
font-size: 13px;
}
.step.is-active { color: var(--ink); }
.step.is-active .step-dot {
background: linear-gradient(135deg, var(--brand), var(--brand-d));
color: #fff;
}
.layout {
display: grid;
grid-template-columns: 1fr 360px;
gap: 26px;
align-items: start;
}
/* ---- Panels ---- */
.editor { display: flex; flex-direction: column; gap: 22px; }
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: clamp(18px, 3vw, 26px);
box-shadow: var(--sh-1);
}
.panel-head { margin-bottom: 18px; }
.panel-head h2 { margin: 0; font-size: 19px; letter-spacing: -0.02em; }
.panel-head p { margin: 4px 0 0; color: var(--muted); font-size: 14px; }
/* ---- Fields ---- */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.span-2 { grid-column: 1 / -1; }
.field { display: flex; flex-direction: column; gap: 7px; border: 0; margin: 0; padding: 0; }
.field-label { font-weight: 600; font-size: 13.5px; color: var(--ink-2); }
.field-label em { color: var(--accent); font-style: normal; }
.field input,
.field textarea {
width: 100%;
font: inherit;
color: var(--ink);
padding: 11px 13px;
border: 1px solid var(--line);
border-radius: var(--r-sm);
background: #fbfaff;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.field textarea { resize: vertical; min-height: 64px; }
.field input:focus,
.field textarea:focus {
outline: none;
border-color: var(--brand);
box-shadow: 0 0 0 4px rgba(124, 58, 237, 0.14);
background: #fff;
}
.field.has-error input { border-color: var(--danger); box-shadow: 0 0 0 4px rgba(220, 38, 38, 0.12); }
.field-err { color: var(--danger); font-size: 12.5px; font-weight: 600; min-height: 0; }
.field.has-error .field-err { min-height: 16px; }
.hero-picker { gap: 10px; }
.swatches { display: flex; gap: 10px; }
.swatch {
width: 34px; height: 34px;
border-radius: 9px;
border: 2px solid #fff;
background: var(--s);
box-shadow: 0 0 0 1px var(--line);
transition: transform 0.1s ease, box-shadow 0.12s ease;
}
.swatch:hover { transform: translateY(-2px); }
.swatch.is-on { box-shadow: 0 0 0 3px var(--ink); transform: translateY(-2px); }
/* ---- Tiers ---- */
.tiers { display: flex; flex-direction: column; gap: 12px; margin-bottom: 14px; }
.tier {
display: grid;
grid-template-columns: 8px 1fr auto;
gap: 14px;
align-items: center;
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px;
background: #fcfbff;
}
.tier-bar { width: 8px; height: 100%; min-height: 56px; border-radius: 999px; background: var(--tc); }
.tier-fields { display: grid; grid-template-columns: 1.4fr 0.8fr 0.8fr; gap: 10px; }
.tier-fields label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; font-weight: 600; color: var(--muted); }
.tier-fields input {
font: inherit; font-weight: 600; color: var(--ink);
padding: 9px 11px;
border: 1px solid var(--line);
border-radius: var(--r-sm);
background: #fff;
}
.tier-fields input:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.13); }
.tier-fields .with-prefix { position: relative; }
.tier-fields .with-prefix input { padding-left: 24px; }
.tier-fields .prefix { position: absolute; left: 11px; bottom: 9px; color: var(--muted); font-weight: 600; pointer-events: none; }
.tier-remove {
width: 34px; height: 34px;
border: 1px solid var(--line);
border-radius: var(--r-sm);
background: #fff; color: var(--danger);
font-size: 17px; line-height: 1;
}
.tier-remove:hover { background: rgba(220, 38, 38, 0.08); border-color: rgba(220, 38, 38, 0.35); }
.tier-remove:disabled { opacity: 0.35; cursor: not-allowed; }
.tier-totals {
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
margin-top: 16px; padding-top: 14px;
border-top: 1px dashed var(--line);
color: var(--muted); font-size: 14px;
}
.tier-totals strong { color: var(--ink); font-size: 15px; }
.tier-totals .dot { color: var(--line); }
/* ---- Seating ---- */
.seat-modes { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.seat-mode {
text-align: left;
display: grid; gap: 6px;
padding: 16px;
border: 2px solid var(--line);
border-radius: var(--r-md);
background: #fcfbff;
transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.08s ease;
}
.seat-mode:hover { border-color: rgba(124, 58, 237, 0.4); }
.seat-mode.is-on { border-color: var(--brand); box-shadow: 0 0 0 4px rgba(124, 58, 237, 0.12); background: #fff; }
.seat-mode-ico { font-size: 22px; }
.seat-mode-t { font-weight: 700; }
.seat-mode-d { font-size: 13px; color: var(--muted); }
.sections-wrap { margin-top: 22px; }
.mini-h { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 0 0 12px; }
.sections { display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; }
.section-row {
display: grid;
grid-template-columns: 1fr 110px 34px;
gap: 10px;
align-items: end;
}
.section-row label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; font-weight: 600; color: var(--muted); }
.section-row input {
font: inherit; font-weight: 600; padding: 9px 11px;
border: 1px solid var(--line); border-radius: var(--r-sm); background: #fff;
}
.section-row input:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.13); }
/* ---- Publish ---- */
.pub-list { display: flex; flex-direction: column; gap: 6px; }
.toggle {
display: flex; align-items: flex-start; gap: 14px;
padding: 14px; border-radius: var(--r-md);
cursor: pointer; transition: background 0.12s ease;
}
.toggle:hover { background: #fbfaff; }
.toggle input { position: absolute; opacity: 0; width: 0; height: 0; }
.toggle-track {
flex: none;
width: 46px; height: 27px;
border-radius: 999px;
background: rgba(14, 14, 22, 0.2);
position: relative;
transition: background 0.18s ease;
margin-top: 2px;
}
.toggle-track::after {
content: "";
position: absolute; top: 3px; left: 3px;
width: 21px; height: 21px;
border-radius: 50%;
background: #fff;
box-shadow: var(--sh-1);
transition: transform 0.18s ease;
}
.toggle input:checked + .toggle-track { background: linear-gradient(135deg, var(--brand), var(--brand-d)); }
.toggle input:checked + .toggle-track::after { transform: translateX(19px); }
.toggle input:focus-visible + .toggle-track { outline: 3px solid var(--brand); outline-offset: 2px; }
.toggle-txt { display: flex; flex-direction: column; gap: 2px; }
.toggle-txt strong { font-size: 14.5px; }
.toggle-txt em { font-style: normal; color: var(--muted); font-size: 13px; }
/* ---- Preview ---- */
.preview-sticky { position: sticky; top: 88px; display: flex; flex-direction: column; gap: 14px; }
.preview-tag {
align-self: flex-start;
font-size: 11px; font-weight: 800;
text-transform: uppercase; letter-spacing: 0.08em;
color: var(--brand-d);
background: rgba(124, 58, 237, 0.1);
padding: 5px 11px; border-radius: 999px;
}
.ticket {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--sh-2);
position: relative;
}
.ticket-hero {
padding: 22px 20px 24px;
color: #fff;
background:
radial-gradient(120% 120% at 80% -10%, rgba(255, 255, 255, 0.28), transparent 50%),
linear-gradient(150deg, var(--hero), color-mix(in srgb, var(--hero) 55%, #0e0e16));
}
.ticket-badge {
display: inline-block;
font-size: 11px; font-weight: 800;
text-transform: uppercase; letter-spacing: 0.06em;
background: rgba(0, 0, 0, 0.28);
padding: 4px 10px; border-radius: 999px;
margin-bottom: 12px;
}
.ticket-badge[data-live="1"] { background: rgba(22, 163, 74, 0.9); }
.ticket-hero h3 { margin: 0; font-size: 21px; line-height: 1.15; letter-spacing: -0.02em; word-break: break-word; }
.ticket-meta { margin: 8px 0 0; font-size: 13px; opacity: 0.92; }
.dotty { margin: 0 6px; opacity: 0.6; }
.perf {
height: 18px;
background:
radial-gradient(circle at 9px 50%, transparent 8px, var(--surface) 9px) left / 18px 100% repeat-x;
border-top: 2px dashed var(--line);
position: relative;
}
.perf::before, .perf::after {
content: "";
position: absolute; top: -10px;
width: 20px; height: 20px;
border-radius: 50%;
background: var(--bg);
border: 1px solid var(--line);
}
.perf::before { left: -11px; }
.perf::after { right: -11px; }
.ticket-body { padding: 16px 20px 20px; }
.ticket-desc { margin: 0 0 12px; font-size: 13px; color: var(--muted); }
.ticket-desc:empty { display: none; }
.ticket-rows { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }
.pv-row {
display: grid;
grid-template-columns: 10px 1fr auto;
gap: 10px; align-items: center;
font-size: 13.5px;
}
.pv-swatch { width: 10px; height: 10px; border-radius: 3px; background: var(--tc); }
.pv-name { font-weight: 600; }
.pv-name small { color: var(--muted); font-weight: 500; }
.pv-price { font-weight: 800; }
.pv-empty { color: var(--muted); font-size: 13px; font-style: italic; }
.ticket-foot {
display: flex; align-items: center; justify-content: space-between;
padding-top: 14px; border-top: 1px dashed var(--line);
}
.seatmode-chip {
font-size: 12px; font-weight: 700;
background: rgba(14, 14, 22, 0.06);
padding: 6px 11px; border-radius: 999px;
color: var(--ink-2);
}
.qr {
display: grid;
grid-template-columns: repeat(3, 9px);
grid-auto-rows: 9px; gap: 2px;
}
.qr span { background: var(--ink); border-radius: 1px; }
.qr span:nth-child(2), .qr span:nth-child(4), .qr span:nth-child(6), .qr span:nth-child(8) { background: var(--brand); }
.preview-summary {
display: grid; grid-template-columns: repeat(3, 1fr);
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
box-shadow: var(--sh-1);
}
.preview-summary div { padding: 12px; text-align: center; border-right: 1px solid var(--line); }
.preview-summary div:last-child { border-right: 0; }
.preview-summary span { display: block; font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.preview-summary strong { font-size: 17px; }
/* ---- Toast ---- */
.toast-wrap {
position: fixed; left: 50%; bottom: 24px;
transform: translateX(-50%);
display: flex; flex-direction: column; gap: 8px;
z-index: 60; pointer-events: none;
width: min(92vw, 380px);
}
.toast {
display: flex; align-items: center; gap: 10px;
background: var(--ink); color: #fff;
padding: 12px 16px; border-radius: var(--r-md);
font-weight: 600; font-size: 14px;
box-shadow: var(--sh-2);
animation: toast-in 0.25s ease, toast-out 0.3s ease 2.7s forwards;
}
.toast::before { content: "✓"; color: var(--ok); font-weight: 800; }
.toast[data-kind="err"]::before { content: "!"; color: var(--accent); }
.toast[data-kind="err"] { background: #2a1620; }
@keyframes toast-in { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@keyframes toast-out { to { opacity: 0; transform: translateY(8px); } }
@media (max-width: 920px) {
.layout { grid-template-columns: 1fr; }
.preview-sticky { position: static; }
}
@media (max-width: 520px) {
.topbar { flex-wrap: wrap; }
.topbar-actions { width: 100%; justify-content: flex-end; }
.grid-2 { grid-template-columns: 1fr; }
.span-2 { grid-column: auto; }
.tier { grid-template-columns: 6px 1fr; }
.tier-remove { grid-column: 2; justify-self: end; }
.tier-fields { grid-template-columns: 1fr 1fr; }
.tier-fields label:first-child { grid-column: 1 / -1; }
.seat-modes { grid-template-columns: 1fr; }
.steps { font-size: 13px; gap: 8px 14px; }
.btn { padding: 9px 13px; }
}(function () {
"use strict";
var TIER_COLORS = ["#7c3aed", "#ff3d81", "#0ea5e9", "#f97316", "#16a34a", "#d97706"];
var state = {
tiers: [
{ id: 1, name: "General Admission", price: 49, qty: 1200, color: "#7c3aed" },
{ id: 2, name: "VIP Lounge", price: 129, qty: 300, color: "#ff3d81" },
{ id: 3, name: "Platinum Pit", price: 249, qty: 60, color: "#0ea5e9" },
],
sections: [
{ id: 1, name: "Floor A", cap: 400 },
{ id: 2, name: "Mezzanine", cap: 220 },
],
seatMode: "ga",
hero: "#7c3aed",
nextTier: 4,
nextSection: 3,
published: false,
};
var $ = function (s, r) { return (r || document).querySelector(s); };
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
/* ---------- toast ---------- */
function toast(msg, kind) {
var wrap = $("#toastWrap");
var t = document.createElement("div");
t.className = "toast";
if (kind === "err") t.setAttribute("data-kind", "err");
t.textContent = msg;
wrap.appendChild(t);
setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, 3100);
}
function money(n) {
return "$" + Math.round(n).toLocaleString("en-US");
}
function fmtDate(val) {
if (!val) return "Date TBA";
var d = new Date(val + "T00:00:00");
if (isNaN(d)) return "Date TBA";
return d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", year: "numeric" });
}
/* ---------- tiers ---------- */
function renderTiers() {
var list = $("#tierList");
list.innerHTML = "";
state.tiers.forEach(function (tier) {
var row = document.createElement("div");
row.className = "tier";
row.style.setProperty("--tc", tier.color);
row.innerHTML =
'<span class="tier-bar"></span>' +
'<div class="tier-fields">' +
'<label>Tier name<input type="text" data-f="name" value="' + escAttr(tier.name) + '" placeholder="Tier name" /></label>' +
'<label class="with-prefix">Price<span class="prefix">$</span><input type="number" min="0" step="1" data-f="price" value="' + tier.price + '" /></label>' +
'<label>Quantity<input type="number" min="0" step="1" data-f="qty" value="' + tier.qty + '" /></label>' +
'</div>' +
'<button class="tier-remove" type="button" aria-label="Remove tier"' + (state.tiers.length <= 1 ? " disabled" : "") + '>×</button>';
row.querySelectorAll("input").forEach(function (inp) {
inp.addEventListener("input", function () {
var f = inp.getAttribute("data-f");
if (f === "name") tier.name = inp.value;
else tier[f] = Math.max(0, parseInt(inp.value, 10) || 0);
updateTotals();
renderPreview();
});
});
var rm = row.querySelector(".tier-remove");
rm.addEventListener("click", function () {
if (state.tiers.length <= 1) return;
state.tiers = state.tiers.filter(function (t) { return t.id !== tier.id; });
markDraft();
renderTiers(); updateTotals(); renderPreview();
toast("Removed “" + (tier.name || "tier") + "”");
});
list.appendChild(row);
});
}
function addTier() {
var color = TIER_COLORS[state.tiers.length % TIER_COLORS.length];
state.tiers.push({ id: state.nextTier++, name: "New tier", price: 35, qty: 100, color: color });
markDraft();
renderTiers(); updateTotals(); renderPreview();
toast("Tier added");
var inputs = $$("#tierList .tier:last-child input[data-f='name']");
if (inputs[0]) inputs[0].select();
}
function updateTotals() {
var cap = 0, gross = 0;
state.tiers.forEach(function (t) { cap += t.qty; gross += t.qty * t.price; });
$("#totalCap").textContent = cap.toLocaleString("en-US");
$("#grossPot").textContent = money(gross);
$("#pvCap").textContent = cap.toLocaleString("en-US");
$("#pvCount").textContent = String(state.tiers.length);
$("#pvGross").textContent = money(gross);
}
/* ---------- seating ---------- */
function renderSections() {
var list = $("#sectionList");
list.innerHTML = "";
state.sections.forEach(function (sec) {
var row = document.createElement("div");
row.className = "section-row";
row.innerHTML =
'<label>Section name<input type="text" data-f="name" value="' + escAttr(sec.name) + '" /></label>' +
'<label>Capacity<input type="number" min="0" data-f="cap" value="' + sec.cap + '" /></label>' +
'<button class="tier-remove" type="button" aria-label="Remove section">×</button>';
row.querySelectorAll("input").forEach(function (inp) {
inp.addEventListener("input", function () {
var f = inp.getAttribute("data-f");
if (f === "name") sec.name = inp.value;
else sec.cap = Math.max(0, parseInt(inp.value, 10) || 0);
});
});
row.querySelector(".tier-remove").addEventListener("click", function () {
state.sections = state.sections.filter(function (s) { return s.id !== sec.id; });
renderSections();
toast("Section removed");
});
list.appendChild(row);
});
}
function setSeatMode(mode) {
state.seatMode = mode;
$$(".seat-mode").forEach(function (b) {
var on = b.getAttribute("data-mode") === mode;
b.classList.toggle("is-on", on);
b.setAttribute("aria-checked", on ? "true" : "false");
});
$("#sectionsWrap").hidden = mode !== "reserved";
if (mode === "reserved" && state.sections.length === 0) addSection();
markDraft();
renderPreview();
}
function addSection() {
state.sections.push({ id: state.nextSection++, name: "Section " + state.nextSection, cap: 150 });
renderSections();
}
/* ---------- preview ---------- */
function renderPreview() {
var name = $("#evName").value.trim() || "Untitled event";
var venue = $("#evVenue").value.trim() || "Venue TBA";
var date = $("#evDate").value;
var desc = $("#evDesc").value.trim();
document.documentElement.style.setProperty("--hero", state.hero);
$("#pvName").textContent = name;
$("#pvVenue").textContent = venue;
$("#pvDate").textContent = fmtDate(date);
$("#pvDesc").textContent = desc;
var seatLabel = state.seatMode === "reserved" ? "Reserved seating" : "General admission";
$("#pvSeat").textContent = seatLabel;
var rows = $("#pvTiers");
rows.innerHTML = "";
if (!state.tiers.length) {
rows.innerHTML = '<p class="pv-empty">No tiers yet — add one to see pricing.</p>';
} else {
state.tiers.forEach(function (t) {
var el = document.createElement("div");
el.className = "pv-row";
el.style.setProperty("--tc", t.color);
var soldOut = t.qty === 0;
el.innerHTML =
'<span class="pv-swatch"></span>' +
'<span class="pv-name">' + escHtml(t.name || "Tier") +
(soldOut ? ' <small style="color:var(--danger)">Sold out</small>'
: ' <small>' + t.qty.toLocaleString("en-US") + ' left</small>') + '</span>' +
'<span class="pv-price">' + money(t.price) + '</span>';
rows.appendChild(el);
});
}
var badge = $("#pvStatus");
badge.textContent = state.published ? "Live" : "Draft";
badge.setAttribute("data-live", state.published ? "1" : "0");
}
/* ---------- status ---------- */
function markDraft() {
if (!state.published) return;
state.published = false;
setStatus("draft");
renderPreview();
}
function setStatus(s) {
var pill = $("#statusPill");
pill.setAttribute("data-state", s);
pill.textContent = s === "live" ? "Live" : "Draft";
}
/* ---------- validation ---------- */
function clearErr(id) {
var f = document.querySelector('[data-err="' + id + '"]');
if (f) { f.textContent = ""; f.closest(".field").classList.remove("has-error"); }
}
function setErr(id, msg) {
var f = document.querySelector('[data-err="' + id + '"]');
if (f) { f.textContent = msg; f.closest(".field").classList.add("has-error"); }
}
function validate() {
var ok = true;
var checks = [
["evName", $("#evName").value.trim(), "Event name is required."],
["evVenue", $("#evVenue").value.trim(), "Venue is required."],
["evDate", $("#evDate").value, "Pick an event date."],
];
checks.forEach(function (c) {
clearErr(c[0]);
if (!c[1]) { setErr(c[0], c[2]); ok = false; }
});
if ($("#evDate").value) {
var d = new Date($("#evDate").value + "T00:00:00");
var today = new Date(); today.setHours(0, 0, 0, 0);
if (d < today) { setErr("evDate", "Date can't be in the past."); ok = false; }
}
var totalCap = state.tiers.reduce(function (s, t) { return s + t.qty; }, 0);
var named = state.tiers.every(function (t) { return t.name.trim().length > 0; });
if (!named) { toast("Every tier needs a name.", "err"); ok = false; }
if (totalCap <= 0) { toast("Add capacity to at least one tier.", "err"); ok = false; }
return ok;
}
/* ---------- escaping ---------- */
function escHtml(s) { return String(s).replace(/[&<>]/g, function (c) { return { "&": "&", "<": "<", ">": ">" }[c]; }); }
function escAttr(s) { return escHtml(s).replace(/"/g, """); }
/* ---------- wiring ---------- */
function init() {
renderTiers();
renderSections();
updateTotals();
renderPreview();
$("#addTierBtn").addEventListener("click", addTier);
$("#addSectionBtn").addEventListener("click", function () { addSection(); toast("Section added"); });
$$(".seat-mode").forEach(function (b) {
b.addEventListener("click", function () { setSeatMode(b.getAttribute("data-mode")); });
});
["evName", "evVenue", "evDate", "evTime", "evDesc"].forEach(function (id) {
var el = $("#" + id);
el.addEventListener("input", function () { clearErr(id); markDraft(); renderPreview(); });
});
$$("#heroSwatches .swatch").forEach(function (sw) {
sw.addEventListener("click", function () {
state.hero = sw.getAttribute("data-c");
$$("#heroSwatches .swatch").forEach(function (s) {
var on = s === sw;
s.classList.toggle("is-on", on);
s.setAttribute("aria-checked", on ? "true" : "false");
});
markDraft();
renderPreview();
});
});
$("#saveDraftBtn").addEventListener("click", function () {
toast("Draft saved");
});
$("#publishBtn").addEventListener("click", function () {
if (!validate()) {
toast("Fix the highlighted fields to publish.", "err");
var firstErr = document.querySelector(".has-error input");
if (firstErr) firstErr.focus();
return;
}
state.published = true;
setStatus("live");
renderPreview();
toast("“" + ($("#evName").value.trim()) + "” is live!");
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ticketing — Event Setup</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>
<a class="skip" href="#main">Skip to content</a>
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◐</span>
<span class="brand-name">PulseTix <span class="brand-sub">Studio</span></span>
</div>
<nav class="topbar-actions" aria-label="Setup actions">
<span class="status-pill" id="statusPill" data-state="draft">Draft</span>
<button class="btn btn-ghost" id="saveDraftBtn" type="button">Save draft</button>
<button class="btn btn-primary" id="publishBtn" type="button">Publish event</button>
</nav>
</header>
<main id="main" class="shell">
<!-- Stepper -->
<ol class="steps" aria-label="Setup progress">
<li class="step is-active" data-step="1"><span class="step-dot">1</span> Details</li>
<li class="step" data-step="2"><span class="step-dot">2</span> Ticket tiers</li>
<li class="step" data-step="3"><span class="step-dot">3</span> Seating</li>
<li class="step" data-step="4"><span class="step-dot">4</span> Publish</li>
</ol>
<div class="layout">
<!-- Editor column -->
<div class="editor">
<!-- STEP 1 — Details -->
<section class="panel" id="panel-1" aria-labelledby="h-details">
<div class="panel-head">
<h2 id="h-details">Event details</h2>
<p>The headline info fans see first.</p>
</div>
<div class="grid-2">
<label class="field span-2">
<span class="field-label">Event name <em>*</em></span>
<input id="evName" type="text" maxlength="80" placeholder="e.g. Neon Pulse Festival 2026" value="Neon Pulse Festival 2026" />
<span class="field-err" data-err="evName"></span>
</label>
<label class="field">
<span class="field-label">Date <em>*</em></span>
<input id="evDate" type="date" value="2026-08-22" />
<span class="field-err" data-err="evDate"></span>
</label>
<label class="field">
<span class="field-label">Doors open</span>
<input id="evTime" type="time" value="18:30" />
</label>
<label class="field span-2">
<span class="field-label">Venue <em>*</em></span>
<input id="evVenue" type="text" placeholder="e.g. Aurora Riverside Park" value="Aurora Riverside Park" />
<span class="field-err" data-err="evVenue"></span>
</label>
<label class="field span-2">
<span class="field-label">Short description</span>
<textarea id="evDesc" rows="3" maxlength="180" placeholder="One punchy line for the listing.">A three-stage open-air night of synthwave, house and live electronica by the river.</textarea>
</label>
<fieldset class="field span-2 hero-picker">
<legend class="field-label">Hero color</legend>
<div class="swatches" id="heroSwatches" role="radiogroup" aria-label="Hero color">
<button type="button" class="swatch is-on" data-c="#7c3aed" style="--s:#7c3aed" role="radio" aria-checked="true" aria-label="Violet"></button>
<button type="button" class="swatch" data-c="#ff3d81" style="--s:#ff3d81" role="radio" aria-checked="false" aria-label="Pink"></button>
<button type="button" class="swatch" data-c="#0ea5e9" style="--s:#0ea5e9" role="radio" aria-checked="false" aria-label="Sky"></button>
<button type="button" class="swatch" data-c="#f97316" style="--s:#f97316" role="radio" aria-checked="false" aria-label="Orange"></button>
<button type="button" class="swatch" data-c="#16a34a" style="--s:#16a34a" role="radio" aria-checked="false" aria-label="Green"></button>
</div>
</fieldset>
</div>
</section>
<!-- STEP 2 — Tiers -->
<section class="panel" id="panel-2" aria-labelledby="h-tiers">
<div class="panel-head">
<h2 id="h-tiers">Ticket tiers</h2>
<p>Define what's on sale. Each tier maps to a color in the seat legend.</p>
</div>
<div class="tiers" id="tierList" aria-live="polite"></div>
<button class="btn btn-dashed" id="addTierBtn" type="button">+ Add ticket tier</button>
<div class="tier-totals">
<span>Total capacity</span>
<strong id="totalCap">0</strong>
<span class="dot" aria-hidden="true">•</span>
<span>Gross potential</span>
<strong id="grossPot">$0</strong>
</div>
</section>
<!-- STEP 3 — Seating -->
<section class="panel" id="panel-3" aria-labelledby="h-seat">
<div class="panel-head">
<h2 id="h-seat">Seating mode</h2>
<p>Choose how attendees occupy the space.</p>
</div>
<div class="seat-modes" role="radiogroup" aria-label="Seating mode">
<button type="button" class="seat-mode is-on" data-mode="ga" role="radio" aria-checked="true">
<span class="seat-mode-ico" aria-hidden="true">⛶</span>
<span class="seat-mode-t">General admission</span>
<span class="seat-mode-d">Open floor, first come first served. Fastest to set up.</span>
</button>
<button type="button" class="seat-mode" data-mode="reserved" role="radio" aria-checked="false">
<span class="seat-mode-ico" aria-hidden="true">▦</span>
<span class="seat-mode-t">Reserved seating</span>
<span class="seat-mode-d">Assigned seats by section. Define rows & capacity below.</span>
</button>
</div>
<div class="sections-wrap" id="sectionsWrap" hidden>
<h3 class="mini-h">Sections</h3>
<div class="sections" id="sectionList"></div>
<button class="btn btn-dashed" id="addSectionBtn" type="button">+ Add section</button>
</div>
</section>
<!-- STEP 4 — Publish -->
<section class="panel" id="panel-4" aria-labelledby="h-pub">
<div class="panel-head">
<h2 id="h-pub">Publish settings</h2>
<p>Final controls before going live.</p>
</div>
<div class="pub-list">
<label class="toggle">
<input type="checkbox" id="optPublic" checked />
<span class="toggle-track" aria-hidden="true"></span>
<span class="toggle-txt"><strong>Public listing</strong><em>Show on the discovery page and search.</em></span>
</label>
<label class="toggle">
<input type="checkbox" id="optWaitlist" />
<span class="toggle-track" aria-hidden="true"></span>
<span class="toggle-txt"><strong>Waitlist when sold out</strong><em>Collect emails after capacity is reached.</em></span>
</label>
<label class="toggle">
<input type="checkbox" id="optTransfer" checked />
<span class="toggle-track" aria-hidden="true"></span>
<span class="toggle-txt"><strong>Allow ticket transfers</strong><em>Buyers can reassign tickets to a friend.</em></span>
</label>
</div>
<label class="field" style="margin-top:18px">
<span class="field-label">On-sale date</span>
<input id="onSale" type="date" value="2026-07-01" />
</label>
</section>
</div>
<!-- Preview column -->
<aside class="preview" aria-label="Live preview">
<div class="preview-sticky">
<span class="preview-tag">Live preview</span>
<article class="ticket" id="previewTicket">
<div class="ticket-hero" id="pvHero">
<span class="ticket-badge" id="pvStatus">Draft</span>
<h3 id="pvName">Neon Pulse Festival 2026</h3>
<p class="ticket-meta">
<span id="pvDate">Sat, Aug 22 2026</span>
<span class="dotty" aria-hidden="true">•</span>
<span id="pvVenue">Aurora Riverside Park</span>
</p>
</div>
<div class="perf" aria-hidden="true"></div>
<div class="ticket-body">
<p class="ticket-desc" id="pvDesc"></p>
<div class="ticket-rows" id="pvTiers"></div>
<div class="ticket-foot">
<span class="seatmode-chip" id="pvSeat">General admission</span>
<span class="qr" aria-hidden="true">
<span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span>
<span></span>
</span>
</div>
</div>
</article>
<div class="preview-summary">
<div><span>Capacity</span><strong id="pvCap">0</strong></div>
<div><span>Tiers</span><strong id="pvCount">0</strong></div>
<div><span>Potential</span><strong id="pvGross">$0</strong></div>
</div>
</div>
</aside>
</div>
</main>
<div class="toast-wrap" id="toastWrap" aria-live="assertive"></div>
<script src="script.js"></script>
</body>
</html>Event Setup
An organizer-facing setup wizard for the fictional PulseTix Studio. A four-step rail — Details, Ticket tiers, Seating, Publish — frames a single scrolling editor. The details panel captures event name, date, doors time, venue, a short description and a hero color picker; the ticket-tier builder lets you add and remove color-coded tiers, each with an editable name, price and quantity that feed live capacity and gross-potential totals.
The seating panel toggles between general admission and reserved seating, revealing an editable list of sections with capacities when reserved is chosen. Publish settings expose toggles for public listing, sold-out waitlists and ticket transfers, plus an on-sale date. Throughout, a sticky preview renders a perforated ticket card with QR motif, color-coded tier rows, sold-out badges, the chosen hero color and a draft/live status badge that updates as you type.
Publishing runs inline validation: required name and venue, a future date, named tiers and non-zero capacity. Failed fields highlight in red and a toast points you to them; on success the status pill and preview badge flip to Live. A small toast() helper confirms tier and section edits, and the layout reflows from a two-column desktop view down to a single column at ~360px.
Illustrative UI only — fictional events, not a real ticketing service.