Ticketing — Event Discovery
A bold, high-contrast event discovery page for a fictional ticketing service. Search artists and venues, filter by city, category and date chips, and browse a responsive grid of event cards with photographic gradient heroes, date flags, price-from labels and low-stock or sold-out badges. A featured carousel, trending sidebar with interest scores, and a color-coded tier legend round it out, while sorting by soonest, popularity or price and one-tap save keep the whole thing genuinely interactive.
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 14px rgba(14, 14, 22, 0.06);
--sh-2: 0 12px 34px rgba(14, 14, 22, 0.16);
--sh-brand: 0 10px 28px rgba(124, 58, 237, 0.32);
--c-music: #7c3aed;
--c-comedy: #f59e0b;
--c-sports: #0ea5e9;
--c-arts: #ff3d81;
--c-festival: #16a34a;
}
* { 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;
text-rendering: optimizeLegibility;
}
h1, h2, h3 { margin: 0; line-height: 1.15; letter-spacing: -0.02em; }
a { color: inherit; }
img { max-width: 100%; display: block; }
.wrap { width: min(1180px, 100% - 40px); margin-inline: auto; }
.skip-link {
position: absolute; left: 12px; top: -48px;
background: var(--ink); color: #fff; padding: 8px 14px;
border-radius: var(--r-sm); z-index: 30; transition: top .15s;
}
.skip-link:focus { top: 12px; }
:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; border-radius: 6px; }
/* ---------- buttons ---------- */
.btn {
font: inherit; font-weight: 600; cursor: pointer;
border: 1px solid transparent; border-radius: 999px;
padding: 10px 18px; display: inline-flex; align-items: center; gap: 8px;
transition: transform .12s, box-shadow .15s, background .15s, border-color .15s;
}
.btn:active { transform: translateY(1px) scale(.99); }
.btn-primary { background: var(--brand); color: #fff; box-shadow: var(--sh-brand); }
.btn-primary:hover { background: var(--brand-d); }
.btn-ghost { background: transparent; border-color: var(--line); color: var(--ink); }
.btn-ghost:hover { background: rgba(124, 58, 237, .07); border-color: rgba(124,58,237,.35); }
.pill {
background: var(--accent); color: #fff; font-size: 12px; font-weight: 700;
min-width: 20px; height: 20px; padding: 0 6px; border-radius: 999px;
display: inline-flex; align-items: center; justify-content: center;
}
/* ---------- topbar ---------- */
.topbar {
position: sticky; top: 0; z-index: 20;
background: rgba(255, 255, 255, .86); backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.topbar-inner { display: flex; align-items: center; gap: 22px; height: 64px; }
.brand { display: flex; align-items: center; gap: 8px; text-decoration: none; font-weight: 800; }
.brand-mark {
color: #fff; background: linear-gradient(135deg, var(--brand), var(--accent));
width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center;
font-size: 16px; box-shadow: var(--sh-brand);
}
.brand-name { font-size: 20px; letter-spacing: -.03em; }
.brand-tag { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .12em; color: var(--muted); }
.topnav { display: flex; gap: 20px; margin-left: 8px; }
.topnav a { text-decoration: none; color: var(--ink-2); font-weight: 600; font-size: 15px; padding: 4px 0; border-bottom: 2px solid transparent; }
.topnav a:hover { color: var(--ink); }
.topnav a.is-active { color: var(--brand); border-color: var(--brand); }
.topbar .btn-ghost { margin-left: auto; }
/* ---------- hero ---------- */
.hero {
padding: 46px 0 30px;
background:
radial-gradient(1100px 320px at 12% -10%, rgba(124,58,237,.22), transparent 60%),
radial-gradient(900px 300px at 92% 0%, rgba(255,61,129,.18), transparent 60%),
var(--bg);
}
.eyebrow { margin: 0 0 6px; text-transform: uppercase; letter-spacing: .14em; font-size: 12px; font-weight: 700; color: var(--brand-d); }
.hero h1 { font-size: clamp(30px, 5vw, 50px); font-weight: 800; }
.hero-sub { margin: 12px 0 22px; color: var(--ink-2); max-width: 56ch; font-size: 17px; }
.searchbar {
display: grid; grid-template-columns: 2fr 1.1fr 1.1fr auto;
gap: 10px; background: var(--surface); padding: 10px;
border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--sh-2);
}
.field {
display: flex; align-items: center; gap: 8px;
background: var(--bg); border: 1px solid transparent; border-radius: var(--r-md);
padding: 0 12px; min-height: 48px;
}
.field:focus-within { border-color: rgba(124,58,237,.45); background: #fff; }
.field .ic { color: var(--muted); font-size: 16px; flex: none; }
.field input, .field select {
border: 0; background: transparent; font: inherit; color: var(--ink);
width: 100%; padding: 12px 0; outline: none; appearance: none;
}
.field select { cursor: pointer; }
.searchbar .btn-primary { min-height: 48px; justify-content: center; }
.datechips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.chip {
font: inherit; font-weight: 600; font-size: 14px; cursor: pointer;
background: var(--surface); border: 1px solid var(--line); color: var(--ink-2);
padding: 8px 15px; border-radius: 999px; transition: all .14s;
}
.chip:hover { border-color: rgba(124,58,237,.4); color: var(--ink); }
.chip.is-active { background: var(--ink); border-color: var(--ink); color: #fff; }
/* ---------- featured carousel ---------- */
.featured { padding: 30px 0 8px; }
.featured-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.featured-head h2 { font-size: 22px; }
.carousel-ctrl { display: flex; gap: 8px; }
.cbtn {
width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--line);
background: var(--surface); font-size: 22px; line-height: 1; cursor: pointer; color: var(--ink);
transition: all .14s;
}
.cbtn:hover { background: var(--ink); color: #fff; border-color: var(--ink); }
.carousel {
display: flex; gap: 16px; overflow-x: auto; scroll-snap-type: x mandatory;
padding: 4px 0 16px; width: min(1180px, 100% - 40px); margin-inline: auto;
scrollbar-width: thin;
}
.carousel::-webkit-scrollbar { height: 8px; }
.carousel::-webkit-scrollbar-thumb { background: rgba(14,14,22,.18); border-radius: 99px; }
.feat-card {
flex: 0 0 min(420px, 82%); scroll-snap-align: start;
position: relative; border-radius: var(--r-lg); overflow: hidden;
min-height: 230px; color: #fff; box-shadow: var(--sh-2);
display: flex; flex-direction: column; justify-content: flex-end;
padding: 20px; text-decoration: none;
transition: transform .18s;
}
.feat-card:hover { transform: translateY(-3px); }
.feat-card::before {
content: ""; position: absolute; inset: 0; z-index: 0;
background: var(--ph, linear-gradient(135deg, #4c1d95, #be185d));
}
.feat-card::after {
content: ""; position: absolute; inset: 0; z-index: 1;
background: linear-gradient(180deg, rgba(0,0,0,0) 30%, rgba(0,0,0,.78) 100%);
}
.feat-card > * { position: relative; z-index: 2; }
.feat-badge {
align-self: flex-start; background: rgba(255,255,255,.18); backdrop-filter: blur(4px);
border: 1px solid rgba(255,255,255,.35); padding: 4px 11px; border-radius: 999px;
font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
margin-bottom: auto;
}
.feat-card h3 { font-size: 24px; font-weight: 800; margin-bottom: 4px; }
.feat-meta { font-size: 14px; opacity: .92; display: flex; gap: 12px; flex-wrap: wrap; }
.feat-price { margin-top: 10px; font-weight: 700; font-size: 15px; }
/* ---------- layout ---------- */
.layout {
display: grid; grid-template-columns: 1fr 300px; gap: 26px;
padding: 26px 0 50px; align-items: start;
}
.results-bar { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-bottom: 16px; flex-wrap: wrap; }
.results-bar h2 { font-size: 22px; }
.results-tools { display: flex; align-items: center; gap: 16px; }
.count { color: var(--muted); font-size: 14px; font-weight: 600; }
.sort { display: flex; align-items: center; gap: 8px; font-size: 14px; color: var(--ink-2); font-weight: 600; }
.sort select {
font: inherit; font-weight: 600; padding: 8px 12px; border-radius: var(--r-sm);
border: 1px solid var(--line); background: var(--surface); cursor: pointer; color: var(--ink);
}
.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 18px; }
.card {
background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
overflow: hidden; box-shadow: var(--sh-1); display: flex; flex-direction: column;
transition: transform .16s, box-shadow .16s, border-color .16s;
}
.card:hover { transform: translateY(-4px); box-shadow: var(--sh-2); border-color: rgba(124,58,237,.3); }
.card-media {
position: relative; aspect-ratio: 16 / 9;
background: var(--ph, linear-gradient(135deg, #4c1d95, #be185d));
display: flex; align-items: flex-start; justify-content: space-between; padding: 12px;
}
.cat-tag {
font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
color: #fff; padding: 4px 10px; border-radius: 999px;
background: rgba(0,0,0,.42); backdrop-filter: blur(3px);
display: inline-flex; align-items: center; gap: 6px;
}
.cat-tag .dot { width: 8px; height: 8px; }
.stock-badge {
font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .05em;
padding: 4px 9px; border-radius: 999px; color: #fff;
}
.stock-low { background: var(--warn); }
.stock-out { background: var(--danger); }
.stock-hot { background: var(--accent); }
.date-flag {
position: absolute; left: 12px; bottom: 12px;
background: #fff; color: var(--ink); border-radius: var(--r-sm);
text-align: center; padding: 5px 9px; box-shadow: var(--sh-1); line-height: 1;
}
.date-flag .m { font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; color: var(--danger); }
.date-flag .d { font-size: 19px; font-weight: 800; }
.card-body { padding: 14px 16px 16px; display: flex; flex-direction: column; flex: 1; }
.card-title { font-size: 17px; font-weight: 700; letter-spacing: -.01em; }
.card-venue { color: var(--muted); font-size: 13.5px; margin: 4px 0 2px; display: flex; align-items: center; gap: 6px; }
.card-when { color: var(--ink-2); font-size: 13.5px; font-weight: 600; }
.card-foot {
margin-top: auto; padding-top: 12px; display: flex; align-items: center; justify-content: space-between;
border-top: 1px dashed var(--line);
}
.price { font-weight: 800; font-size: 16px; }
.price small { font-weight: 600; color: var(--muted); font-size: 12px; }
.save {
border: 1px solid var(--line); background: var(--surface); cursor: pointer;
width: 38px; height: 38px; border-radius: 50%; font-size: 17px; color: var(--muted);
display: grid; place-items: center; transition: all .14s;
}
.save:hover { border-color: var(--accent); color: var(--accent); }
.save.is-saved { background: var(--accent); border-color: var(--accent); color: #fff; }
.empty {
grid-column: 1 / -1; text-align: center; color: var(--muted);
padding: 40px 20px; background: var(--surface); border: 1px dashed var(--line);
border-radius: var(--r-lg); font-weight: 500;
}
/* ---------- sidebar ---------- */
.side { display: flex; flex-direction: column; gap: 18px; position: sticky; top: 80px; }
.side-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 18px; box-shadow: var(--sh-1); }
.side-card h3 { font-size: 16px; margin-bottom: 12px; }
.trend { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; counter-reset: t; }
.trend li {
counter-increment: t; display: flex; gap: 11px; align-items: center;
padding: 9px 8px; border-radius: var(--r-sm); cursor: pointer; transition: background .14s;
}
.trend li:hover { background: var(--bg); }
.trend li::before {
content: counter(t); font-weight: 800; font-size: 14px; color: var(--brand);
width: 22px; text-align: center; flex: none;
}
.trend-info { display: flex; flex-direction: column; min-width: 0; }
.trend-name { font-weight: 700; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.trend-meta { font-size: 12px; color: var(--muted); }
.legend ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.legend li { display: flex; align-items: center; gap: 9px; font-size: 14px; font-weight: 600; color: var(--ink-2); }
.dot { width: 11px; height: 11px; border-radius: 50%; flex: none; }
.dot-music { background: var(--c-music); }
.dot-comedy { background: var(--c-comedy); }
.dot-sports { background: var(--c-sports); }
.dot-arts { background: var(--c-arts); }
.dot-festival { background: var(--c-festival); }
/* ---------- footer ---------- */
.foot { border-top: 1px solid var(--line); background: var(--surface); padding: 26px 0; }
.foot p { margin: 0; color: var(--muted); font-size: 14px; }
/* ---------- toast ---------- */
.toast {
position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 24px);
background: var(--ink); color: #fff; padding: 12px 20px; border-radius: 999px;
font-weight: 600; font-size: 14px; box-shadow: var(--sh-2);
opacity: 0; pointer-events: none; transition: opacity .22s, transform .22s; z-index: 40;
max-width: calc(100% - 32px);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- responsive ---------- */
@media (max-width: 920px) {
.layout { grid-template-columns: 1fr; }
.side { position: static; flex-direction: row; flex-wrap: wrap; }
.side-card { flex: 1 1 240px; }
.searchbar { grid-template-columns: 1fr 1fr; }
.field-search { grid-column: 1 / -1; }
}
@media (max-width: 520px) {
.topnav { display: none; }
.topbar .btn-ghost { margin-left: auto; }
.hero { padding: 30px 0 22px; }
.searchbar { grid-template-columns: 1fr; padding: 8px; }
.grid { grid-template-columns: 1fr; }
.results-tools { width: 100%; justify-content: space-between; }
.feat-card { flex-basis: 88%; }
.side { flex-direction: column; }
.brand-tag { display: none; }
}(() => {
"use strict";
/* ---------- fictional data ---------- */
const GRADIENTS = {
Music: "linear-gradient(135deg,#4c1d95,#7c3aed 55%,#ff3d81)",
Comedy: "linear-gradient(135deg,#7c2d12,#f59e0b)",
Sports: "linear-gradient(135deg,#0c4a6e,#0ea5e9)",
Arts: "linear-gradient(135deg,#831843,#ff3d81)",
Festival: "linear-gradient(135deg,#14532d,#16a34a 60%,#84cc16)"
};
const CAT_DOT = {
Music: "dot-music", Comedy: "dot-comedy", Sports: "dot-sports",
Arts: "dot-arts", Festival: "dot-festival"
};
// day offsets from "today" so date chips behave deterministically
const EVENTS = [
{ id: "e1", title: "Neon Cartography — Live", cat: "Music", venue: "Aurora Hall", city: "Harbor District", off: 0, time: "8:00 PM", price: 42, pop: 98, stock: "hot", featured: true },
{ id: "e2", title: "Midnight Roast Showcase", cat: "Comedy", venue: "The Velvet Cellar", city: "Old Town", off: 1, time: "9:30 PM", price: 24, pop: 71, stock: "low" },
{ id: "e3", title: "Riverside Derby: Foxes vs Tide", cat: "Sports", venue: "Granite Arena", city: "Riverside", off: 2, time: "7:15 PM", price: 35, pop: 88, stock: "ok", featured: true },
{ id: "e4", title: "Glass Garden — Light Exhibit", cat: "Arts", venue: "Pier 9 Gallery", city: "Harbor District", off: 5, time: "All day", price: 18, pop: 54, stock: "ok" },
{ id: "e5", title: "Lowtide Folk Festival", cat: "Festival", venue: "Brookline Commons", city: "Brookline", off: 6, time: "12:00 PM", price: 65, pop: 93, stock: "low", featured: true },
{ id: "e6", title: "Static Bloom + Paper Tigers", cat: "Music", venue: "Echo Room", city: "Old Town", off: 3, time: "8:30 PM", price: 29, pop: 76, stock: "ok" },
{ id: "e7", title: "Improv After Dark", cat: "Comedy", venue: "Loft 21", city: "Brookline", off: 4, time: "10:00 PM", price: 16, pop: 48, stock: "out" },
{ id: "e8", title: "Harbor Half Marathon", cat: "Sports", venue: "Seawall Track", city: "Harbor District", off: 12, time: "6:00 AM", price: 40, pop: 62, stock: "ok" },
{ id: "e9", title: "Strings & Steel Quartet", cat: "Arts", venue: "Old Town Chapel", city: "Old Town", off: 8, time: "7:00 PM", price: 27, pop: 58, stock: "low" },
{ id: "e10", title: "Velvet Pulse — Tour Finale", cat: "Music", venue: "Aurora Hall", city: "Harbor District", off: 18, time: "9:00 PM", price: 58, pop: 95, stock: "hot", featured: true },
{ id: "e11", title: "Riverside Night Market", cat: "Festival", venue: "Quay Plaza", city: "Riverside", off: 9, time: "5:00 PM", price: 0, pop: 67, stock: "ok" },
{ id: "e12", title: "Standup Marathon: 12 Comics", cat: "Comedy", venue: "The Velvet Cellar", city: "Old Town", off: 22, time: "8:00 PM", price: 22, pop: 51, stock: "ok" }
];
const MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
const WEEKDAYS = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
const TODAY = new Date();
const dateForOff = (off) => {
const d = new Date(TODAY);
d.setDate(d.getDate() + off);
return d;
};
/* ---------- state ---------- */
const saved = new Set();
const filters = { q: "", city: "", cat: "", date: "any" };
let sort = "date";
/* ---------- helpers ---------- */
const $ = (s, r = document) => r.querySelector(s);
const fmtPrice = (p) => (p === 0 ? "Free" : "$" + p);
let toastT;
function toast(msg) {
const el = $("#toast");
el.textContent = msg;
el.classList.add("show");
clearTimeout(toastT);
toastT = setTimeout(() => el.classList.remove("show"), 2200);
}
function matchesDate(ev) {
const off = ev.off;
switch (filters.date) {
case "today": return off === 0;
case "week": return off >= 0 && off <= 7;
case "month": return off >= 0 && off <= 31;
case "weekend": {
const wd = dateForOff(off).getDay();
return off <= 9 && (wd === 5 || wd === 6 || wd === 0);
}
default: return true;
}
}
function visibleEvents() {
const q = filters.q.trim().toLowerCase();
let list = EVENTS.filter((ev) => {
if (filters.city && ev.city !== filters.city) return false;
if (filters.cat && ev.cat !== filters.cat) return false;
if (!matchesDate(ev)) return false;
if (q) {
const hay = (ev.title + " " + ev.venue + " " + ev.cat + " " + ev.city).toLowerCase();
if (!hay.includes(q)) return false;
}
return true;
});
list.sort((a, b) => {
if (sort === "popularity") return b.pop - a.pop;
if (sort === "price") return a.price - b.price;
return a.off - b.off;
});
return list;
}
/* ---------- renderers ---------- */
function stockBadge(ev) {
if (ev.stock === "out") return '<span class="stock-badge stock-out">Sold out</span>';
if (ev.stock === "low") return '<span class="stock-badge stock-low">Few left</span>';
if (ev.stock === "hot") return '<span class="stock-badge stock-hot">Selling fast</span>';
return "";
}
function eventCard(ev) {
const d = dateForOff(ev.off);
const card = document.createElement("article");
card.className = "card";
card.style.setProperty("--ph", GRADIENTS[ev.cat]);
const isSaved = saved.has(ev.id);
card.innerHTML = `
<div class="card-media">
<span class="cat-tag"><span class="dot ${CAT_DOT[ev.cat]}"></span>${ev.cat}</span>
${stockBadge(ev)}
<span class="date-flag"><span class="m">${MONTHS[d.getMonth()]}</span><span class="d">${d.getDate()}</span></span>
</div>
<div class="card-body">
<h3 class="card-title">${ev.title}</h3>
<p class="card-venue">◎ ${ev.venue} · ${ev.city}</p>
<p class="card-when">${WEEKDAYS[d.getDay()]} · ${ev.time}</p>
<div class="card-foot">
<span class="price">${fmtPrice(ev.price)} <small>${ev.price ? "from" : ""}</small></span>
<button class="save ${isSaved ? "is-saved" : ""}" type="button"
aria-pressed="${isSaved}" aria-label="${isSaved ? "Remove from saved" : "Save event"}">${isSaved ? "♥" : "♡"}</button>
</div>
</div>`;
card.querySelector(".save").addEventListener("click", () => toggleSave(ev, card));
return card;
}
function renderResults() {
const list = visibleEvents();
const grid = $("#results");
grid.innerHTML = "";
list.forEach((ev) => grid.appendChild(eventCard(ev)));
$("#empty").hidden = list.length !== 0;
$("#resultCount").textContent =
list.length + (list.length === 1 ? " event" : " events");
const parts = [];
if (filters.cat) parts.push(filters.cat);
if (filters.city) parts.push("in " + filters.city);
$("#resultsTitle").textContent = parts.length ? parts.join(" ") : "All events";
}
function renderFeatured() {
const car = $("#carousel");
car.innerHTML = "";
EVENTS.filter((e) => e.featured).forEach((ev) => {
const d = dateForOff(ev.off);
const a = document.createElement("a");
a.className = "feat-card";
a.href = "#results";
a.style.setProperty("--ph", GRADIENTS[ev.cat]);
a.innerHTML = `
<span class="feat-badge">${ev.cat} · Featured</span>
<h3>${ev.title}</h3>
<p class="feat-meta"><span>📍 ${ev.venue}</span><span>${MONTHS[d.getMonth()]} ${d.getDate()} · ${ev.time}</span></p>
<p class="feat-price">${fmtPrice(ev.price)}${ev.price ? " and up" : ""}</p>`;
a.addEventListener("click", () => toast("Opening " + ev.title));
car.appendChild(a);
});
}
function renderTrending() {
const ol = $("#trending");
ol.innerHTML = "";
[...EVENTS].sort((a, b) => b.pop - a.pop).slice(0, 6).forEach((ev) => {
const li = document.createElement("li");
li.innerHTML = `
<span class="trend-info">
<span class="trend-name">${ev.title}</span>
<span class="trend-meta">${ev.cat} · ${ev.pop}% interest</span>
</span>`;
li.addEventListener("click", () => {
$("#q").value = ev.title;
filters.q = ev.title;
renderResults();
toast("Filtered to “" + ev.title + "”");
});
ol.appendChild(li);
});
}
/* ---------- saving ---------- */
function toggleSave(ev, card) {
const btn = card.querySelector(".save");
if (saved.has(ev.id)) {
saved.delete(ev.id);
btn.classList.remove("is-saved");
btn.textContent = "♡";
btn.setAttribute("aria-pressed", "false");
btn.setAttribute("aria-label", "Save event");
toast("Removed from saved");
} else {
saved.add(ev.id);
btn.classList.add("is-saved");
btn.textContent = "♥";
btn.setAttribute("aria-pressed", "true");
btn.setAttribute("aria-label", "Remove from saved");
toast("Saved " + ev.title);
}
$("#savedCount").textContent = saved.size;
}
/* ---------- wiring ---------- */
$("#searchForm").addEventListener("submit", (e) => {
e.preventDefault();
filters.q = $("#q").value;
filters.city = $("#location").value;
filters.cat = $("#category").value;
renderResults();
toast(visibleEvents().length + " events found");
});
$("#q").addEventListener("input", (e) => { filters.q = e.target.value; renderResults(); });
$("#location").addEventListener("change", (e) => { filters.city = e.target.value; renderResults(); });
$("#category").addEventListener("change", (e) => { filters.cat = e.target.value; renderResults(); });
$("#sort").addEventListener("change", (e) => { sort = e.target.value; renderResults(); });
$("#dateChips").addEventListener("click", (e) => {
const chip = e.target.closest(".chip");
if (!chip) return;
$("#dateChips").querySelectorAll(".chip").forEach((c) => c.classList.remove("is-active"));
chip.classList.add("is-active");
filters.date = chip.dataset.date;
renderResults();
});
$("#savedBtn").addEventListener("click", () => {
toast(saved.size ? saved.size + " event(s) saved" : "No saved events yet");
});
const scrollCar = (dir) => $("#carousel").scrollBy({ left: dir * 440, behavior: "smooth" });
$("#carNext").addEventListener("click", () => scrollCar(1));
$("#carPrev").addEventListener("click", () => scrollCar(-1));
/* ---------- init ---------- */
renderFeatured();
renderTrending();
renderResults();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pulse — Discover Live Events</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="#results">Skip to events</a>
<header class="topbar">
<div class="wrap topbar-inner">
<a class="brand" href="#" aria-label="Pulse home">
<span class="brand-mark" aria-hidden="true">◈</span>
<span class="brand-name">Pulse</span>
<span class="brand-tag">tickets</span>
</a>
<nav class="topnav" aria-label="Primary">
<a href="#" class="is-active">Discover</a>
<a href="#">Sell tickets</a>
<a href="#">Help</a>
</nav>
<button class="btn btn-ghost" type="button" id="savedBtn">
<span aria-hidden="true">♥</span> Saved <span class="pill" id="savedCount">0</span>
</button>
</div>
</header>
<main>
<section class="hero" aria-label="Search events">
<div class="wrap">
<p class="eyebrow">Live shows · this season</p>
<h1>Find your next night out.</h1>
<p class="hero-sub">Concerts, comedy, sports and culture across the city — handpicked and bookable in seconds.</p>
<form class="searchbar" id="searchForm" role="search" autocomplete="off">
<div class="field field-search">
<span class="ic" aria-hidden="true">⌕</span>
<input type="search" id="q" placeholder="Search artists, teams, venues…" aria-label="Search events" />
</div>
<div class="field">
<span class="ic" aria-hidden="true">◎</span>
<select id="location" aria-label="Location">
<option value="">All cities</option>
<option>Brookline</option>
<option>Harbor District</option>
<option>Old Town</option>
<option>Riverside</option>
</select>
</div>
<div class="field">
<span class="ic" aria-hidden="true">▦</span>
<select id="category" aria-label="Category">
<option value="">All categories</option>
<option>Music</option>
<option>Comedy</option>
<option>Sports</option>
<option>Arts</option>
<option>Festival</option>
</select>
</div>
<button class="btn btn-primary" type="submit">Search</button>
</form>
<div class="datechips" id="dateChips" role="group" aria-label="Filter by date">
<button type="button" class="chip is-active" data-date="any">Any date</button>
<button type="button" class="chip" data-date="today">Today</button>
<button type="button" class="chip" data-date="weekend">This weekend</button>
<button type="button" class="chip" data-date="week">This week</button>
<button type="button" class="chip" data-date="month">This month</button>
</div>
</div>
</section>
<section class="featured" aria-label="Featured events">
<div class="wrap featured-head">
<h2>Featured this week</h2>
<div class="carousel-ctrl">
<button type="button" class="cbtn" id="carPrev" aria-label="Previous featured">‹</button>
<button type="button" class="cbtn" id="carNext" aria-label="Next featured">›</button>
</div>
</div>
<div class="carousel" id="carousel" tabindex="0" aria-label="Featured events carousel"></div>
</section>
<div class="wrap layout">
<section class="results-col" aria-label="Event results">
<div class="results-bar">
<h2 id="resultsTitle">All events</h2>
<div class="results-tools">
<span class="count" id="resultCount" aria-live="polite"></span>
<label class="sort">
<span>Sort</span>
<select id="sort" aria-label="Sort events">
<option value="date">Soonest</option>
<option value="popularity">Most popular</option>
<option value="price">Lowest price</option>
</select>
</label>
</div>
</div>
<div class="grid" id="results"></div>
<p class="empty" id="empty" hidden>No events match those filters. Try clearing the date or category.</p>
</section>
<aside class="side" aria-label="Trending">
<div class="side-card">
<h3>Trending now</h3>
<ol class="trend" id="trending"></ol>
</div>
<div class="side-card legend">
<h3>Price tiers</h3>
<ul>
<li><span class="dot dot-music"></span> Music</li>
<li><span class="dot dot-comedy"></span> Comedy</li>
<li><span class="dot dot-sports"></span> Sports</li>
<li><span class="dot dot-arts"></span> Arts</li>
<li><span class="dot dot-festival"></span> Festival</li>
</ul>
</div>
</aside>
</div>
</main>
<footer class="foot">
<div class="wrap">
<p>Pulse Tickets — illustrative demo. Fictional events, venues and prices.</p>
</div>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Event Discovery
A self-contained event discovery surface for a fictional ticketing brand, Pulse. A glassy sticky header sits above a gradient hero with a four-part search bar — keyword, city, category and a primary search action — plus a row of date chips (Any date, Today, This weekend, This week, This month) that instantly narrow the listings. Below, a horizontally scrollable featured carousel highlights the week’s biggest shows with photographic gradient blocks, venue, date and price-from labels.
The main column renders a responsive grid of event cards, each with a category tag, a calendar-style date flag, and stock badges that surface “Selling fast”, “Few left” or “Sold out” states. Typing in the search box filters live, the city and category dropdowns refine results, and a sort control reorders by soonest, most popular or lowest price. Every card has a heart toggle that updates a saved counter in the header and fires a small toast. A sidebar lists trending events by interest score (click one to filter to it) alongside a color-coded price-tier legend.
Everything is vanilla HTML, CSS and JavaScript — no frameworks, no build step, no network calls. The layout collapses gracefully from a two-column desktop view down to a single stacked column at ~360px, and interactive elements stay keyboard-usable with visible focus rings and ARIA state.
Illustrative UI only — fictional events, not a real ticketing service.