Ticketing — Organizer Dashboard
A bold, high-contrast organizer dashboard for the fictional Neon Pulse Festival. Four KPI cards track tickets sold, revenue, capacity and gate scans with trend chips and a low-stock badge. A timeframe toggle redraws an SVG sales chart across 7, 30 and 90 days, an interactive donut breaks sales down by ticket tier, a check-in progress ring shows pre-event readiness, and a recent-orders table drills into a ticket-stub drawer. 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;
--tier-1: #7c3aed;
--tier-2: #ff3d81;
--tier-3: #06b6d4;
--tier-4: #f59e0b;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-sm: 0 1px 2px rgba(14, 14, 22, 0.06), 0 2px 8px rgba(14, 14, 22, 0.05);
--sh-md: 0 8px 28px rgba(14, 14, 22, 0.1);
--sh-lg: 0 20px 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;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3 { margin: 0; line-height: 1.2; letter-spacing: -0.02em; }
a { color: inherit; }
.skip-link {
position: absolute;
left: -999px;
top: 0;
background: var(--ink);
color: #fff;
padding: 10px 16px;
border-radius: var(--r-sm);
z-index: 100;
}
.skip-link:focus { left: 12px; top: 12px; }
:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; border-radius: 6px; }
/* ---------- Shell ---------- */
.shell {
display: grid;
grid-template-columns: 264px 1fr;
min-height: 100vh;
}
/* ---------- Sidebar ---------- */
.sidebar {
background: linear-gradient(180deg, #15101f 0%, #0e0e16 100%);
color: #e9e8f2;
padding: 22px 18px;
display: flex;
flex-direction: column;
gap: 22px;
position: sticky;
top: 0;
align-self: start;
height: 100vh;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.2rem;
font-weight: 700;
padding: 4px 6px;
}
.brand-mark {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 10px;
background: linear-gradient(135deg, var(--brand), var(--accent));
color: #fff;
font-size: 1rem;
box-shadow: 0 4px 14px rgba(124, 58, 237, 0.5);
}
.brand-text { letter-spacing: -0.02em; }
.brand-text strong { color: var(--accent); font-weight: 800; }
.nav { display: flex; flex-direction: column; gap: 4px; }
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 10px;
text-decoration: none;
color: #b6b4c8;
font-weight: 500;
font-size: 0.92rem;
transition: background 0.15s, color 0.15s, transform 0.1s;
}
.nav-ico { width: 18px; text-align: center; opacity: 0.8; }
.nav-item:hover { background: rgba(255, 255, 255, 0.06); color: #fff; }
.nav-item.is-active {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.35), rgba(255, 61, 129, 0.22));
color: #fff;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
.event-pill {
margin-top: auto;
display: flex;
gap: 12px;
align-items: center;
padding: 12px;
border-radius: var(--r-md);
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.event-pill-art {
width: 44px;
height: 44px;
border-radius: 10px;
flex: none;
background:
radial-gradient(circle at 30% 20%, rgba(255, 255, 255, 0.45), transparent 45%),
linear-gradient(135deg, var(--accent), var(--brand) 70%, #06b6d4);
}
.event-pill-body { display: flex; flex-direction: column; min-width: 0; }
.event-pill-name { font-weight: 700; font-size: 0.9rem; }
.event-pill-meta { color: #9b99af; font-size: 0.78rem; }
/* ---------- Main ---------- */
.main { padding: 26px 32px 48px; min-width: 0; }
.topbar {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.topbar-title h1 { font-size: 1.7rem; font-weight: 800; }
.topbar-sub { margin: 4px 0 0; color: var(--muted); font-size: 0.92rem; }
.topbar-actions { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.seg {
display: inline-flex;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 4px;
box-shadow: var(--sh-sm);
}
.seg-btn {
border: 0;
background: transparent;
color: var(--ink-2);
font: inherit;
font-weight: 600;
font-size: 0.85rem;
padding: 7px 16px;
border-radius: 999px;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.seg-btn:hover { color: var(--ink); }
.seg-btn.is-active {
background: linear-gradient(135deg, var(--brand), var(--brand-d));
color: #fff;
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.4);
}
.btn-primary {
border: 0;
background: var(--ink);
color: #fff;
font: inherit;
font-weight: 600;
font-size: 0.88rem;
padding: 10px 18px;
border-radius: 999px;
cursor: pointer;
transition: transform 0.1s, box-shadow 0.15s, background 0.15s;
box-shadow: var(--sh-sm);
}
.btn-primary:hover { background: var(--brand-d); transform: translateY(-1px); box-shadow: var(--sh-md); }
.btn-primary:active { transform: translateY(0); }
/* ---------- KPIs ---------- */
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 18px;
}
.kpi {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 18px;
box-shadow: var(--sh-sm);
position: relative;
overflow: hidden;
transition: transform 0.12s, box-shadow 0.15s;
}
.kpi::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 4px;
background: linear-gradient(180deg, var(--brand), var(--accent));
}
.kpi:hover { transform: translateY(-2px); box-shadow: var(--sh-md); }
.kpi-top { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.kpi-label { color: var(--muted); font-size: 0.82rem; font-weight: 600; }
.kpi-val { font-size: 1.85rem; font-weight: 800; letter-spacing: -0.03em; margin: 8px 0 4px; }
.kpi-foot { color: var(--muted); font-size: 0.8rem; }
.trend { font-size: 0.76rem; font-weight: 700; padding: 2px 8px; border-radius: 999px; }
.trend.up { color: var(--ok); background: rgba(22, 163, 74, 0.1); }
.trend.down { color: var(--danger); background: rgba(220, 38, 38, 0.1); }
.badge {
font-size: 0.72rem;
font-weight: 700;
padding: 3px 9px;
border-radius: 999px;
letter-spacing: 0.01em;
}
.badge-warn { color: var(--warn); background: rgba(217, 119, 6, 0.13); }
.badge-mut { color: var(--muted); background: rgba(108, 108, 128, 0.12); }
.badge-ok { color: var(--ok); background: rgba(22, 163, 74, 0.12); }
.badge-danger { color: var(--danger); background: rgba(220, 38, 38, 0.12); }
.badge-brand { color: var(--brand); background: rgba(124, 58, 237, 0.12); }
.cap-bar {
height: 8px;
border-radius: 999px;
background: rgba(124, 58, 237, 0.12);
overflow: hidden;
margin-top: 12px;
}
.cap-fill {
display: block;
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--brand), var(--accent));
transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-areas:
"chart tier"
"orders checkin";
gap: 16px;
}
.chart-card { grid-area: chart; }
.tier-card { grid-area: tier; }
.checkin-card { grid-area: checkin; }
.orders-card { grid-area: orders; }
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 20px;
box-shadow: var(--sh-sm);
}
.card-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 10px;
margin-bottom: 16px;
}
.card-head h2 { font-size: 1.05rem; font-weight: 700; }
.card-sub { color: var(--muted); font-size: 0.82rem; }
.legend { display: flex; align-items: center; gap: 7px; }
.legend-dot { width: 10px; height: 10px; border-radius: 3px; background: var(--c, var(--brand)); }
.legend-txt { font-size: 0.8rem; color: var(--muted); font-weight: 600; }
.link-more { font-size: 0.82rem; font-weight: 600; color: var(--brand); text-decoration: none; }
.link-more:hover { text-decoration: underline; }
/* ---------- Chart ---------- */
.chart-wrap { position: relative; }
.chart { width: 100%; height: 240px; display: block; overflow: visible; }
.gridline { stroke: var(--line); stroke-width: 1; stroke-dasharray: 3 5; }
.area { fill: url(#areaGrad); opacity: 0.16; }
.line {
fill: none;
stroke: var(--brand);
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
}
.bar {
fill: rgba(124, 58, 237, 0.14);
rx: 4;
transition: fill 0.15s;
cursor: pointer;
}
.bar:hover, .bar.is-hot { fill: rgba(255, 61, 129, 0.35); }
.dot {
fill: #fff;
stroke: var(--brand);
stroke-width: 3;
cursor: pointer;
transition: r 0.12s;
}
.dot:hover { stroke: var(--accent); }
.chart-x {
display: flex;
justify-content: space-between;
margin-top: 6px;
font-size: 0.72rem;
color: var(--muted);
font-weight: 600;
}
.chart-x span { flex: 1; text-align: center; }
.chart-tip {
position: absolute;
transform: translate(-50%, -120%);
background: var(--ink);
color: #fff;
padding: 7px 11px;
border-radius: var(--r-sm);
font-size: 0.78rem;
font-weight: 600;
white-space: nowrap;
pointer-events: none;
box-shadow: var(--sh-md);
z-index: 5;
}
.chart-tip b { color: var(--accent); }
.chart-tip::after {
content: "";
position: absolute;
left: 50%;
bottom: -5px;
transform: translateX(-50%) rotate(45deg);
width: 9px;
height: 9px;
background: var(--ink);
}
/* ---------- Tier donut ---------- */
.donut-wrap { position: relative; width: 168px; height: 168px; margin: 4px auto 18px; }
.donut { width: 100%; height: 100%; transform: rotate(-90deg); }
.donut-track { fill: none; stroke: var(--bg); stroke-width: 14; }
.donut-seg {
fill: none;
stroke-width: 14;
stroke-linecap: butt;
transition: stroke-width 0.15s, opacity 0.15s;
cursor: pointer;
}
.donut-seg.is-dim { opacity: 0.32; }
.donut-seg.is-hot { stroke-width: 18; }
.donut-center {
position: absolute;
inset: 0;
display: grid;
place-content: center;
text-align: center;
}
.donut-pct { font-size: 1.5rem; font-weight: 800; letter-spacing: -0.03em; }
.donut-cap { font-size: 0.72rem; color: var(--muted); font-weight: 600; }
.tier-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.tier-row {
display: grid;
grid-template-columns: 12px 1fr auto;
align-items: center;
gap: 10px;
padding: 9px 10px;
border-radius: var(--r-sm);
cursor: pointer;
border: 1px solid transparent;
transition: background 0.13s, border-color 0.13s;
}
.tier-row:hover, .tier-row.is-active { background: var(--bg); border-color: var(--line); }
.tier-swatch { width: 12px; height: 12px; border-radius: 4px; background: var(--c); }
.tier-name { font-weight: 600; font-size: 0.9rem; }
.tier-meta { font-size: 0.76rem; color: var(--muted); }
.tier-figs { text-align: right; }
.tier-count { font-weight: 700; font-size: 0.9rem; }
.tier-rev { font-size: 0.76rem; color: var(--muted); }
/* ---------- Check-in ring ---------- */
.ring-wrap { position: relative; width: 150px; height: 150px; margin: 4px auto 16px; }
.ring { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring-track { fill: none; stroke: var(--bg); stroke-width: 12; }
.ring-fill {
fill: none;
stroke: url(#ringGrad);
stroke-width: 12;
stroke-linecap: round;
transition: stroke-dashoffset 0.9s cubic-bezier(0.22, 1, 0.36, 1);
}
.ring-center { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; }
.ring-pct { font-size: 1.6rem; font-weight: 800; letter-spacing: -0.03em; }
.ring-cap { font-size: 0.72rem; color: var(--muted); font-weight: 600; }
.mini-stats {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
text-align: center;
}
.mini-stats li {
background: var(--bg);
border-radius: var(--r-sm);
padding: 10px 6px;
}
.mini-num { display: block; font-weight: 800; font-size: 1.05rem; }
.mini-lab { font-size: 0.72rem; color: var(--muted); font-weight: 600; }
/* ---------- Orders table ---------- */
.table-scroll { overflow-x: auto; margin: 0 -6px; }
.orders { width: 100%; border-collapse: collapse; font-size: 0.88rem; min-width: 540px; }
.orders th {
text-align: left;
color: var(--muted);
font-weight: 600;
font-size: 0.74rem;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0 12px 10px;
border-bottom: 1px solid var(--line);
}
.orders td { padding: 12px; border-bottom: 1px solid var(--line); }
.orders tbody tr {
cursor: pointer;
transition: background 0.12s;
}
.orders tbody tr:hover { background: var(--bg); }
.orders tbody tr:last-child td { border-bottom: 0; }
.orders .num { text-align: right; }
.ord-id { font-weight: 700; color: var(--brand); }
.ord-buyer { display: flex; align-items: center; gap: 9px; }
.avatar {
width: 28px;
height: 28px;
border-radius: 50%;
display: grid;
place-items: center;
color: #fff;
font-weight: 700;
font-size: 0.72rem;
flex: none;
}
.tier-tag {
display: inline-flex;
align-items: center;
gap: 6px;
font-weight: 600;
font-size: 0.82rem;
}
.tier-tag::before {
content: "";
width: 8px;
height: 8px;
border-radius: 3px;
background: var(--c, var(--brand));
}
/* ---------- Drawer ---------- */
.drawer-overlay {
position: fixed;
inset: 0;
background: rgba(14, 14, 22, 0.45);
backdrop-filter: blur(2px);
z-index: 40;
animation: fade 0.2s ease;
}
.drawer {
position: fixed;
top: 0;
right: 0;
height: 100vh;
width: min(420px, 92vw);
background: var(--surface);
z-index: 50;
box-shadow: var(--sh-lg);
display: flex;
flex-direction: column;
animation: slideIn 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}
.drawer-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 22px 24px 18px;
border-bottom: 1px solid var(--line);
}
.drawer-eyebrow { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); font-weight: 700; }
.drawer-head h3 { font-size: 1.3rem; font-weight: 800; margin-top: 3px; }
.drawer-close {
border: 1px solid var(--line);
background: var(--surface);
width: 34px;
height: 34px;
border-radius: 10px;
cursor: pointer;
font-size: 0.9rem;
color: var(--ink-2);
transition: background 0.13s;
}
.drawer-close:hover { background: var(--bg); }
.drawer-body { padding: 22px 24px; overflow-y: auto; }
/* Ticket stub in drawer */
.stub {
border-radius: var(--r-md);
background: linear-gradient(135deg, var(--brand), var(--brand-d));
color: #fff;
padding: 20px;
position: relative;
overflow: hidden;
margin-bottom: 20px;
}
.stub::before, .stub::after {
content: "";
position: absolute;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--surface);
top: 50%;
transform: translateY(-50%);
}
.stub::before { left: -11px; }
.stub::after { right: -11px; }
.stub-perf {
position: absolute;
inset: 50% 0 auto 0;
border-top: 2px dashed rgba(255, 255, 255, 0.4);
}
.stub-top { display: flex; justify-content: space-between; align-items: flex-start; }
.stub-evt { font-weight: 800; font-size: 1.05rem; }
.stub-sub { font-size: 0.8rem; opacity: 0.85; }
.stub-qr {
width: 52px;
height: 52px;
border-radius: 8px;
background:
repeating-conic-gradient(#fff 0 25%, transparent 0 50%) 0 0 / 13px 13px,
#fff;
background-blend-mode: difference;
flex: none;
}
.stub-bottom { display: flex; justify-content: space-between; margin-top: 28px; font-size: 0.82rem; }
.stub-bottom b { display: block; font-size: 0.96rem; }
.dl { list-style: none; margin: 0; padding: 0; }
.dl li {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 11px 0;
border-bottom: 1px solid var(--line);
font-size: 0.9rem;
}
.dl li:last-child { border-bottom: 0; }
.dl dt { color: var(--muted); }
.dl dd { margin: 0; font-weight: 600; text-align: right; }
.dl .grand { font-weight: 800; font-size: 1.05rem; }
/* ---------- Toast ---------- */
.toast-host {
position: fixed;
bottom: 22px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 8px;
z-index: 80;
align-items: center;
}
.toast {
background: var(--ink);
color: #fff;
padding: 11px 18px;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 600;
box-shadow: var(--sh-md);
display: flex;
align-items: center;
gap: 9px;
animation: toastIn 0.25s ease;
}
.toast .dot-led { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
.toast.out { animation: toastOut 0.3s ease forwards; }
@keyframes fade { from { opacity: 0; } }
@keyframes slideIn { from { transform: translateX(100%); } }
@keyframes toastIn { from { opacity: 0; transform: translateY(12px); } }
@keyframes toastOut { to { opacity: 0; transform: translateY(12px); } }
/* ---------- Responsive ---------- */
@media (max-width: 1080px) {
.shell { grid-template-columns: 1fr; }
.sidebar {
flex-direction: row;
align-items: center;
height: auto;
position: static;
gap: 16px;
overflow-x: auto;
padding: 14px 16px;
}
.nav { flex-direction: row; }
.nav-item span:not(.nav-ico) { display: none; }
.event-pill { margin: 0; }
.kpis { grid-template-columns: repeat(2, 1fr); }
.grid {
grid-template-columns: 1fr;
grid-template-areas: "chart" "tier" "checkin" "orders";
}
}
@media (max-width: 520px) {
.main { padding: 18px 16px 40px; }
.topbar-title h1 { font-size: 1.4rem; }
.kpis { grid-template-columns: 1fr; }
.seg-btn { padding: 6px 12px; }
.btn-primary { padding: 9px 14px; }
.event-pill-body { display: none; }
.topbar-actions { width: 100%; justify-content: space-between; }
}(function () {
"use strict";
/* ---------------- Data ---------------- */
var TIERS = [
{ key: "ga", name: "General Admission", price: 79, sold: 1840, color: "var(--tier-1)" },
{ key: "vip", name: "VIP Lounge", price: 159, sold: 820, color: "var(--tier-2)" },
{ key: "pit", name: "Platinum Pit", price: 249, sold: 364, color: "var(--tier-3)" },
{ key: "early", name: "Early Bird", price: 59, sold: 160, color: "var(--tier-4)" }
];
// Daily ticket sales, newest last. 90 days of synthetic-but-stable data.
var DAILY = (function () {
var out = [], seed = 42;
function rnd() { seed = (seed * 9301 + 49297) % 233280; return seed / 233280; }
for (var i = 89; i >= 0; i--) {
// rising trend toward the event with weekend bumps and a couple of spikes
var base = 14 + (90 - i) * 0.55;
var weekend = (i % 7 === 1 || i % 7 === 2) ? 22 : 0;
var spike = (i === 60 || i === 18 || i === 5) ? 70 : 0;
var v = Math.round(base + weekend + spike + rnd() * 26);
out.push(Math.max(6, v));
}
return out;
})();
var ORDERS = [
{ id: "NP-8841", buyer: "Marisol Vega", tier: "vip", qty: 2, when: "12 min ago", email: "marisol.v@example.com", fee: 9.5, method: "Visa ·· 4012" },
{ id: "NP-8840", buyer: "Dre Okonkwo", tier: "ga", qty: 4, when: "31 min ago", email: "dre.ok@example.com", fee: 9.5, method: "Apple Pay" },
{ id: "NP-8839", buyer: "Lena Hartman", tier: "pit", qty: 1, when: "1 hr ago", email: "lena.h@example.com", fee: 9.5, method: "Mastercard ·· 7781" },
{ id: "NP-8838", buyer: "Yuki Tanaka", tier: "ga", qty: 2, when: "2 hr ago", email: "yuki.t@example.com", fee: 9.5, method: "Visa ·· 1190", refunded: true },
{ id: "NP-8837", buyer: "Omar Khalil", tier: "vip", qty: 3, when: "3 hr ago", email: "omar.k@example.com", fee: 9.5, method: "PayPal" },
{ id: "NP-8836", buyer: "Priya Nair", tier: "early", qty: 2, when: "4 hr ago", email: "priya.n@example.com", fee: 9.5, method: "Visa ·· 6620" },
{ id: "NP-8835", buyer: "Theo Brandt", tier: "ga", qty: 6, when: "5 hr ago", email: "theo.b@example.com", fee: 9.5, method: "Google Pay" },
{ id: "NP-8834", buyer: "Camila Rossi", tier: "pit", qty: 2, when: "6 hr ago", email: "camila.r@example.com", fee: 9.5, method: "Mastercard ·· 3308" }
];
var AVATAR_COLORS = ["#7c3aed", "#ff3d81", "#06b6d4", "#f59e0b", "#16a34a", "#6366f1"];
/* ---------------- Helpers ---------------- */
var $ = function (s, r) { return (r || document).querySelector(s); };
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
function money(n) { return "$" + n.toLocaleString("en-US"); }
function money2(n) { return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }
function initials(name) { return name.split(" ").map(function (w) { return w[0]; }).join("").slice(0, 2).toUpperCase(); }
function tierByKey(k) { for (var i = 0; i < TIERS.length; i++) if (TIERS[i].key === k) return TIERS[i]; return TIERS[0]; }
/* ---------------- Toast ---------------- */
var toastHost = $("#toastHost");
function toast(msg) {
var el = document.createElement("div");
el.className = "toast";
el.innerHTML = '<span class="dot-led"></span>' + msg;
toastHost.appendChild(el);
setTimeout(function () {
el.classList.add("out");
el.addEventListener("animationend", function () { el.remove(); });
}, 2600);
}
/* ---------------- Chart ---------------- */
var RANGE_DAYS = { "7d": 7, "30d": 30, "90d": 90 };
var DAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
var chart = $("#chart");
var W = 720, H = 260, PAD_L = 8, PAD_R = 8, PAD_T = 18, PAD_B = 22;
var tip = $("#chartTip");
var chartWrap = $(".chart-wrap");
// inject SVG gradients once
(function defs() {
var ns = "http://www.w3.org/2000/svg";
var defs = document.createElementNS(ns, "defs");
defs.innerHTML =
'<linearGradient id="areaGrad" x1="0" y1="0" x2="0" y2="1">' +
'<stop offset="0%" stop-color="#7c3aed"/><stop offset="100%" stop-color="#7c3aed" stop-opacity="0"/></linearGradient>' +
'<linearGradient id="ringGrad" x1="0" y1="0" x2="1" y2="1">' +
'<stop offset="0%" stop-color="#7c3aed"/><stop offset="100%" stop-color="#ff3d81"/></linearGradient>';
chart.insertBefore(defs, chart.firstChild);
})();
var currentRange = "7d";
function dataForRange(r) {
var n = RANGE_DAYS[r];
return DAILY.slice(DAILY.length - n);
}
function xLabelsForRange(r, data) {
var n = data.length;
if (r === "7d") {
// map last 7 days to weekday names ending today (assume today = Tue arbitrary anchor)
var anchor = 2; // Tue
return data.map(function (_, i) {
var d = (anchor - (n - 1 - i)) % 7;
if (d < 0) d += 7;
return DAY_LABELS[d];
});
}
// for 30/90 show ~5 sparse week markers
var labels = data.map(function () { return ""; });
var step = r === "30d" ? 6 : 18;
for (var i = n - 1; i >= 0; i -= step) {
var wk = Math.round((n - 1 - i) / 7) ;
labels[i] = "wk -" + Math.round((n - i) / 7);
}
labels[n - 1] = "now";
return labels;
}
function drawChart(r) {
var data = dataForRange(r);
var n = data.length;
var max = Math.max.apply(null, data) * 1.12;
var innerW = W - PAD_L - PAD_R;
var innerH = H - PAD_T - PAD_B;
var stepX = innerW / (n - 1 || 1);
function px(i) { return PAD_L + i * stepX; }
function py(v) { return PAD_T + innerH - (v / max) * innerH; }
// gridlines
var gl = $("#gridlines");
gl.innerHTML = "";
for (var g = 0; g <= 4; g++) {
var yy = PAD_T + (innerH / 4) * g;
var l = document.createElementNS("http://www.w3.org/2000/svg", "line");
l.setAttribute("class", "gridline");
l.setAttribute("x1", PAD_L); l.setAttribute("x2", W - PAD_R);
l.setAttribute("y1", yy); l.setAttribute("y2", yy);
gl.appendChild(l);
}
// line + area paths
var linePts = data.map(function (v, i) { return px(i) + "," + py(v); });
var lineD = "M" + linePts.join(" L");
var areaD = "M" + px(0) + "," + (H - PAD_B) + " L" + linePts.join(" L") + " L" + px(n - 1) + "," + (H - PAD_B) + " Z";
$("#line").setAttribute("d", lineD);
$("#area").setAttribute("d", areaD);
// bars (faint) — only when not too dense
var bars = $("#bars");
bars.innerHTML = "";
var showBars = n <= 31;
if (showBars) {
var bw = Math.min(stepX * 0.42, 22);
data.forEach(function (v, i) {
var rct = document.createElementNS("http://www.w3.org/2000/svg", "rect");
rct.setAttribute("class", "bar");
rct.setAttribute("x", px(i) - bw / 2);
rct.setAttribute("y", py(v));
rct.setAttribute("width", bw);
rct.setAttribute("height", (H - PAD_B) - py(v));
rct.setAttribute("rx", 4);
rct.dataset.i = i;
bars.appendChild(rct);
});
}
// dots — sparse for big ranges
var dots = $("#dots");
dots.innerHTML = "";
var dotEvery = n <= 14 ? 1 : (n <= 31 ? 3 : 9);
data.forEach(function (v, i) {
if (i % dotEvery !== 0 && i !== n - 1) return;
var c = document.createElementNS("http://www.w3.org/2000/svg", "circle");
c.setAttribute("class", "dot");
c.setAttribute("cx", px(i));
c.setAttribute("cy", py(v));
c.setAttribute("r", n <= 14 ? 5 : 4);
c.dataset.i = i;
dots.appendChild(c);
});
// x axis labels
var labels = xLabelsForRange(r, data);
$("#chartX").innerHTML = labels.map(function (t) { return "<span>" + t + "</span>"; }).join("");
// tooltip wiring
function showTip(i, target) {
var v = data[i];
var rect = chart.getBoundingClientRect();
var wrapRect = chartWrap.getBoundingClientRect();
var cx = (px(i) / W) * rect.width + (rect.left - wrapRect.left);
var cy = (py(v) / H) * rect.height + (rect.top - wrapRect.top);
tip.style.left = cx + "px";
tip.style.top = cy + "px";
tip.innerHTML = "<b>" + v + "</b> tickets";
tip.hidden = false;
$$(".bar").forEach(function (b) { b.classList.toggle("is-hot", b.dataset.i === String(i)); });
}
function hideTip() { tip.hidden = true; $$(".bar").forEach(function (b) { b.classList.remove("is-hot"); }); }
$$(".dot, .bar", chart).forEach(function (node) {
node.addEventListener("mouseenter", function () { showTip(+node.dataset.i, node); });
node.addEventListener("mouseleave", hideTip);
});
var totalSold = data.reduce(function (a, b) { return a + b; }, 0);
var subLabel = r === "7d" ? "Last 7 days" : (r === "30d" ? "Last 30 days" : "Last 90 days");
$("#chartSub").textContent = subLabel + " · " + totalSold.toLocaleString() + " tickets in range";
}
/* ---------------- Tier donut + list ---------------- */
var donutSegs = $("#donutSegs");
var R = 48, CIRC = 2 * Math.PI * R;
var activeTier = null;
function renderTiers() {
var totalSold = TIERS.reduce(function (a, t) { return a + t.sold; }, 0);
donutSegs.innerHTML = "";
var offset = 0;
TIERS.forEach(function (t) {
var frac = t.sold / totalSold;
var seg = document.createElementNS("http://www.w3.org/2000/svg", "circle");
seg.setAttribute("class", "donut-seg");
seg.setAttribute("cx", 60); seg.setAttribute("cy", 60); seg.setAttribute("r", R);
seg.setAttribute("stroke", t.color);
seg.setAttribute("stroke-dasharray", (frac * CIRC) + " " + CIRC);
seg.setAttribute("stroke-dashoffset", -offset * CIRC);
seg.dataset.key = t.key;
donutSegs.appendChild(seg);
offset += frac;
});
var list = $("#tierList");
list.innerHTML = "";
TIERS.forEach(function (t) {
var frac = Math.round((t.sold / totalSold) * 100);
var li = document.createElement("li");
li.className = "tier-row";
li.dataset.key = t.key;
li.setAttribute("tabindex", "0");
li.setAttribute("role", "button");
li.innerHTML =
'<span class="tier-swatch" style="--c:' + t.color + '"></span>' +
'<span><span class="tier-name">' + t.name + '</span><br>' +
'<span class="tier-meta">' + money(t.price) + ' · ' + frac + '% of sales</span></span>' +
'<span class="tier-figs"><span class="tier-count">' + t.sold.toLocaleString() + '</span><br>' +
'<span class="tier-rev">' + money(t.sold * t.price) + '</span></span>';
list.appendChild(li);
});
$("#tierTotal").textContent = totalSold.toLocaleString() + " sold";
$("#donutPct").textContent = "100%";
}
function selectTier(key) {
activeTier = (activeTier === key) ? null : key;
var totalSold = TIERS.reduce(function (a, t) { return a + t.sold; }, 0);
$$(".donut-seg").forEach(function (s) {
var on = !activeTier || s.dataset.key === activeTier;
s.classList.toggle("is-dim", !on);
s.classList.toggle("is-hot", activeTier && s.dataset.key === activeTier);
});
$$(".tier-row").forEach(function (r) {
r.classList.toggle("is-active", activeTier === r.dataset.key);
});
if (activeTier) {
var t = tierByKey(activeTier);
$("#donutPct").textContent = Math.round((t.sold / totalSold) * 100) + "%";
$(".donut-cap").textContent = t.name.split(" ")[0].toLowerCase() + " share";
toast(t.name + " — " + t.sold.toLocaleString() + " sold · " + money(t.sold * t.price));
} else {
$("#donutPct").textContent = "100%";
$(".donut-cap").textContent = "sold-through";
}
}
/* ---------------- Check-in ring ---------------- */
function setRing(pct) {
var rr = 50, c = 2 * Math.PI * rr;
var fill = $("#ringFill");
fill.setAttribute("stroke-dasharray", c);
fill.setAttribute("stroke-dashoffset", c * (1 - pct / 100));
$("#ringPct").textContent = pct + "%";
}
/* ---------------- Orders ---------------- */
function renderOrders() {
var body = $("#ordersBody");
body.innerHTML = "";
ORDERS.forEach(function (o, idx) {
var t = tierByKey(o.tier);
var total = o.qty * t.price + o.fee;
var tr = document.createElement("tr");
tr.dataset.id = o.id;
tr.setAttribute("tabindex", "0");
var statusBadge = o.refunded
? '<span class="badge badge-danger">Refunded</span>'
: '<span class="badge badge-ok">Paid</span>';
tr.innerHTML =
'<td><span class="ord-id">' + o.id + '</span><br><span class="tier-rev">' + o.when + '</span></td>' +
'<td><span class="ord-buyer"><span class="avatar" style="background:' + AVATAR_COLORS[idx % AVATAR_COLORS.length] + '">' + initials(o.buyer) + '</span>' + o.buyer + '</span></td>' +
'<td><span class="tier-tag" style="--c:' + t.color + '">' + t.name + '</span></td>' +
'<td class="num">' + o.qty + '</td>' +
'<td class="num"><strong>' + money2(total) + '</strong></td>' +
'<td>' + statusBadge + '</td>';
body.appendChild(tr);
});
}
/* ---------------- Drawer ---------------- */
var drawer = $("#drawer"), overlay = $("#drawerOverlay");
var lastFocus = null;
function openOrder(id) {
var o = null;
for (var i = 0; i < ORDERS.length; i++) if (ORDERS[i].id === id) { o = ORDERS[i]; break; }
if (!o) return;
var t = tierByKey(o.tier);
var sub = o.qty * t.price;
var total = sub + o.fee;
$("#drawerTitle").textContent = o.id;
$("#drawerBody").innerHTML =
'<div class="stub"><div class="stub-perf"></div>' +
'<div class="stub-top"><div><div class="stub-evt">Neon Pulse Festival</div>' +
'<div class="stub-sub">Aug 22, 2026 · Pier 48, San Francisco</div></div>' +
'<div class="stub-qr"></div></div>' +
'<div class="stub-bottom"><div>Tier<b>' + t.name + '</b></div>' +
'<div>Qty<b>' + o.qty + '</b></div>' +
'<div>Total<b>' + money2(total) + '</b></div></div>' +
'</div>' +
'<dl class="dl">' +
'<li><dt>Buyer</dt><dd>' + o.buyer + '</dd></li>' +
'<li><dt>Email</dt><dd>' + o.email + '</dd></li>' +
'<li><dt>Payment</dt><dd>' + o.method + '</dd></li>' +
'<li><dt>Placed</dt><dd>' + o.when + '</dd></li>' +
'<li><dt>Status</dt><dd>' + (o.refunded ? '<span class="badge badge-danger">Refunded</span>' : '<span class="badge badge-ok">Paid</span>') + '</dd></li>' +
'<li><dt>' + o.qty + ' × ' + t.name + '</dt><dd>' + money2(sub) + '</dd></li>' +
'<li><dt>Service fee</dt><dd>' + money2(o.fee) + '</dd></li>' +
'<li><dt class="grand">Total</dt><dd class="grand">' + money2(total) + '</dd></li>' +
'</dl>';
lastFocus = document.activeElement;
overlay.hidden = false;
drawer.hidden = false;
$("#drawerClose").focus();
}
function closeDrawer() {
drawer.hidden = true;
overlay.hidden = true;
if (lastFocus && lastFocus.focus) lastFocus.focus();
}
/* ---------------- Wiring ---------------- */
// timeframe toggle
$$(".seg-btn").forEach(function (b) {
b.addEventListener("click", function () {
if (b.classList.contains("is-active")) return;
$$(".seg-btn").forEach(function (x) { x.classList.remove("is-active"); x.setAttribute("aria-pressed", "false"); });
b.classList.add("is-active");
b.setAttribute("aria-pressed", "true");
currentRange = b.dataset.range;
drawChart(currentRange);
toast("Chart updated · " + b.textContent.trim());
});
});
// tier interactions (delegated)
$("#donutSegs").addEventListener("click", function (e) {
var seg = e.target.closest(".donut-seg");
if (seg) selectTier(seg.dataset.key);
});
$("#tierList").addEventListener("click", function (e) {
var row = e.target.closest(".tier-row");
if (row) selectTier(row.dataset.key);
});
$("#tierList").addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
var row = e.target.closest(".tier-row");
if (row) { e.preventDefault(); selectTier(row.dataset.key); }
}
});
// order drill
$("#ordersBody").addEventListener("click", function (e) {
var tr = e.target.closest("tr");
if (tr) openOrder(tr.dataset.id);
});
$("#ordersBody").addEventListener("keydown", function (e) {
if (e.key === "Enter") {
var tr = e.target.closest("tr");
if (tr) openOrder(tr.dataset.id);
}
});
$("#drawerClose").addEventListener("click", closeDrawer);
overlay.addEventListener("click", closeDrawer);
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !drawer.hidden) closeDrawer();
});
$("#exportBtn").addEventListener("click", function () {
toast("Report queued · CSV will email to organizers");
});
/* ---------------- Init ---------------- */
renderOrders();
renderTiers();
drawChart("7d");
setRing(0);
// animate check-in ring slightly to feel alive (pre-event = low)
setTimeout(function () { setRing(0); }, 100);
// re-flow chart on resize (debounced)
var rt;
window.addEventListener("resize", function () {
clearTimeout(rt);
rt = setTimeout(function () { drawChart(currentRange); }, 150);
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Neon Pulse Festival — Organizer Dashboard</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-link" href="#main">Skip to content</a>
<div class="shell">
<!-- Sidebar -->
<aside class="sidebar" aria-label="Primary">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◆</span>
<span class="brand-text">Pulse<strong>Pass</strong></span>
</div>
<nav class="nav" aria-label="Sections">
<a href="#" class="nav-item is-active" aria-current="page"><span class="nav-ico" aria-hidden="true">▣</span>Overview</a>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">◷</span>Sales</a>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">⛶</span>Check-in</a>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">▤</span>Orders</a>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">◈</span>Attendees</a>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">⚙</span>Settings</a>
</nav>
<div class="event-pill">
<div class="event-pill-art" aria-hidden="true"></div>
<div class="event-pill-body">
<span class="event-pill-name">Neon Pulse Festival</span>
<span class="event-pill-meta">Aug 22 · Pier 48, SF</span>
</div>
</div>
</aside>
<!-- Main -->
<main class="main" id="main">
<header class="topbar">
<div class="topbar-title">
<h1>Organizer Dashboard</h1>
<p class="topbar-sub">Neon Pulse Festival · live sales overview</p>
</div>
<div class="topbar-actions">
<div class="seg" role="group" aria-label="Timeframe">
<button class="seg-btn is-active" data-range="7d" aria-pressed="true">7D</button>
<button class="seg-btn" data-range="30d" aria-pressed="false">30D</button>
<button class="seg-btn" data-range="90d" aria-pressed="false">90D</button>
</div>
<button class="btn-primary" id="exportBtn">Export report</button>
</div>
</header>
<!-- KPI cards -->
<section class="kpis" aria-label="Key performance indicators">
<article class="kpi">
<div class="kpi-top">
<span class="kpi-label">Tickets sold</span>
<span class="trend up">▲ 8.4%</span>
</div>
<div class="kpi-val" data-kpi="sold">3,184</div>
<div class="kpi-foot">of 4,000 capacity</div>
</article>
<article class="kpi">
<div class="kpi-top">
<span class="kpi-label">Revenue</span>
<span class="trend up">▲ 11.2%</span>
</div>
<div class="kpi-val" data-kpi="revenue">$312,940</div>
<div class="kpi-foot">avg $98.28 / ticket</div>
</article>
<article class="kpi">
<div class="kpi-top">
<span class="kpi-label">Capacity</span>
<span class="badge badge-warn">Low stock</span>
</div>
<div class="kpi-val" data-kpi="capacity">79.6%</div>
<div class="cap-bar" role="progressbar" aria-valuenow="80" aria-valuemin="0" aria-valuemax="100" aria-label="Capacity filled">
<span class="cap-fill" style="width:79.6%"></span>
</div>
</article>
<article class="kpi">
<div class="kpi-top">
<span class="kpi-label">Scans (check-in)</span>
<span class="trend down">▼ 2.1%</span>
</div>
<div class="kpi-val" data-kpi="scans">0</div>
<div class="kpi-foot">gates open Aug 22, 4:00 PM</div>
</article>
</section>
<div class="grid">
<!-- Sales chart -->
<section class="card chart-card" aria-label="Sales over time">
<div class="card-head">
<div>
<h2>Sales over time</h2>
<p class="card-sub" id="chartSub">Last 7 days · tickets sold per day</p>
</div>
<div class="legend">
<span class="legend-dot" style="--c:var(--brand)"></span><span class="legend-txt">Tickets</span>
</div>
</div>
<div class="chart-wrap">
<svg class="chart" id="chart" viewBox="0 0 720 260" preserveAspectRatio="none" role="img" aria-label="Line and bar chart of daily ticket sales">
<g id="gridlines"></g>
<path id="area" class="area" d=""></path>
<path id="line" class="line" d=""></path>
<g id="bars"></g>
<g id="dots"></g>
</svg>
<div class="chart-x" id="chartX"></div>
<div class="chart-tip" id="chartTip" hidden></div>
</div>
</section>
<!-- Tier breakdown -->
<section class="card tier-card" aria-label="Sales by ticket tier">
<div class="card-head">
<h2>By ticket tier</h2>
<span class="card-sub" id="tierTotal">3,184 sold</span>
</div>
<div class="donut-wrap">
<svg class="donut" id="donut" viewBox="0 0 120 120" role="img" aria-label="Donut chart of tier split">
<circle class="donut-track" cx="60" cy="60" r="48"></circle>
<g id="donutSegs"></g>
</svg>
<div class="donut-center">
<span class="donut-pct" id="donutPct">100%</span>
<span class="donut-cap">sold-through</span>
</div>
</div>
<ul class="tier-list" id="tierList"></ul>
</section>
<!-- Check-in rate -->
<section class="card checkin-card" aria-label="Check-in rate">
<div class="card-head">
<h2>Check-in rate</h2>
<span class="badge badge-mut">Pre-event</span>
</div>
<div class="ring-wrap">
<svg class="ring" viewBox="0 0 120 120" role="img" aria-label="Check-in progress ring">
<circle class="ring-track" cx="60" cy="60" r="50"></circle>
<circle class="ring-fill" id="ringFill" cx="60" cy="60" r="50"></circle>
</svg>
<div class="ring-center">
<span class="ring-pct" id="ringPct">0%</span>
<span class="ring-cap">checked in</span>
</div>
</div>
<ul class="mini-stats">
<li><span class="mini-num">0</span><span class="mini-lab">Scanned</span></li>
<li><span class="mini-num">3,184</span><span class="mini-lab">Issued</span></li>
<li><span class="mini-num">6</span><span class="mini-lab">Gates</span></li>
</ul>
</section>
<!-- Recent orders -->
<section class="card orders-card" aria-label="Recent orders">
<div class="card-head">
<h2>Recent orders</h2>
<a href="#" class="link-more">View all →</a>
</div>
<div class="table-scroll">
<table class="orders">
<thead>
<tr>
<th scope="col">Order</th>
<th scope="col">Buyer</th>
<th scope="col">Tier</th>
<th scope="col" class="num">Qty</th>
<th scope="col" class="num">Total</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody id="ordersBody"></tbody>
</table>
</div>
</section>
</div>
</main>
</div>
<!-- Order drill drawer -->
<div class="drawer-overlay" id="drawerOverlay" hidden></div>
<aside class="drawer" id="drawer" role="dialog" aria-modal="true" aria-labelledby="drawerTitle" hidden>
<header class="drawer-head">
<div>
<span class="drawer-eyebrow">Order detail</span>
<h3 id="drawerTitle">—</h3>
</div>
<button class="drawer-close" id="drawerClose" aria-label="Close order detail">✕</button>
</header>
<div class="drawer-body" id="drawerBody"></div>
</aside>
<div class="toast-host" id="toastHost" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Organizer Dashboard
A dense, energetic control room for the fictional Neon Pulse Festival. A dark gradient sidebar carries the brand mark, section nav and a live event pill, while the main panel leads with four KPI cards — tickets sold against a 4,000 capacity, revenue with an average-per-ticket figure, a capacity meter with a low-stock badge, and pre-event gate scans. Each card pairs a big number with an up or down trend chip drawn in the event-ticketing palette.
The sales chart is hand-built in SVG: a gradient area, a rounded line, faint per-day bars and hover dots, all driven by the 7D / 30D / 90D segmented toggle in the top bar. Switching timeframe redraws the series, relabels the axis and updates the in-range total, with a tooltip that tracks the nearest point. Alongside it, a donut chart splits sales across four tiers — General Admission, VIP Lounge, Platinum Pit and Early Bird — colour-matched to the legend list; clicking a segment or row isolates that tier, recomputes its share and fires a confirmation toast.
A circular check-in ring reflects the pre-event scan rate next to gate and issuance mini-stats, and a recent-orders table lists buyers with coloured avatars, tier tags and paid or refunded badges. Selecting any order slides in a drawer with a perforated, QR-marked ticket stub and a full price breakdown (subtotal, service fee, grand total), dismissable by overlay, close button or Escape. The whole layout reflows from a two-column desktop grid to a stacked, icon-only mobile view at ~360px.
Illustrative UI only — fictional events, not a real ticketing service.