Gym — Public Schedule Page
A bold, public-facing weekly class schedule for a performance gym. Members and prospects pick a location, scan day tabs with a live today highlight, and filter classes by type or trainer. Every row shows time, class name, trainer, room and an intensity tag, plus live spot counts and an in-session marker. A Reserve button opens a free-trial sign-up modal, and a one-tap print layout produces a clean handout. Built with vanilla JavaScript only.
MCP
Kod
:root {
--bg: #0d0f12;
--surface: #15181d;
--surface-2: #1d2127;
--elevated: #23282f;
--ink: #f4f6f8;
--ink-2: #c2c8d0;
--muted: #8b929c;
--neon: #c6ff3a;
--neon-d: #a6e016;
--neon-50: rgba(198, 255, 58, 0.12);
--orange: #ff6a2b;
--orange-soft: rgba(255, 106, 43, 0.14);
--line: rgba(255, 255, 255, 0.08);
--line-2: rgba(255, 255, 255, 0.16);
--ok: #34d399;
--warn: #fbbf24;
--danger: #f87171;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow: 0 10px 30px -12px rgba(0, 0, 0, 0.7), 0 2px 8px -4px rgba(0, 0, 0, 0.5);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-image:
radial-gradient(1200px 500px at 80% -10%, rgba(198, 255, 58, 0.06), transparent 60%),
radial-gradient(900px 400px at -10% 10%, rgba(255, 106, 43, 0.05), transparent 55%);
}
.page {
max-width: 1080px;
margin: 0 auto;
padding: 22px clamp(16px, 4vw, 32px) 64px;
}
/* ===== Header ===== */
.site-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding-bottom: 20px;
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
display: grid;
place-items: center;
width: 44px;
height: 44px;
border-radius: var(--r-md);
background: var(--neon);
color: #0d0f12;
font-size: 22px;
box-shadow: 0 6px 18px -6px rgba(198, 255, 58, 0.6);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-name { font-weight: 900; letter-spacing: 0.06em; font-size: 20px; }
.brand-sub { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.12em; font-weight: 600; }
.loc-select { display: flex; flex-direction: column; gap: 4px; }
.loc-label {
font-size: 10px; text-transform: uppercase; letter-spacing: 0.14em;
color: var(--muted); font-weight: 700;
}
.loc-select select {
appearance: none;
background: var(--surface-2) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%238b929c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 5 7 9 11 5'/%3E%3C/svg%3E") no-repeat right 12px center;
color: var(--ink);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 10px 36px 10px 12px;
font: inherit;
font-weight: 600;
cursor: pointer;
}
select:focus-visible, .chip:focus-visible, .btn:focus-visible,
.day-tab:focus-visible, .reserve-btn:focus-visible, input:focus-visible,
.modal-close:focus-visible {
outline: 3px solid var(--neon);
outline-offset: 2px;
}
/* ===== Hero ===== */
.hero { padding: 30px 0 10px; }
.eyebrow {
margin: 0 0 10px;
font-size: 12px; font-weight: 800; letter-spacing: 0.18em;
text-transform: uppercase; color: var(--neon);
}
.hero h1 {
margin: 0 0 12px;
font-size: clamp(30px, 6vw, 50px);
font-weight: 900;
letter-spacing: -0.02em;
line-height: 1.05;
}
.hero h1 span { color: var(--orange); }
.hero-sub { margin: 0 0 22px; max-width: 58ch; color: var(--ink-2); font-size: 16px; }
.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; }
/* ===== Buttons ===== */
.btn {
font: inherit;
font-weight: 800;
letter-spacing: 0.02em;
border: 1px solid transparent;
border-radius: var(--r-md);
padding: 13px 22px;
cursor: pointer;
transition: transform 0.12s ease, background 0.15s ease, box-shadow 0.15s ease;
}
.btn:active { transform: translateY(1px) scale(0.99); }
.btn-neon {
background: var(--neon);
color: #0d0f12;
box-shadow: 0 8px 22px -10px rgba(198, 255, 58, 0.7);
}
.btn-neon:hover { background: var(--neon-d); box-shadow: 0 10px 26px -8px rgba(198, 255, 58, 0.8); }
.btn-ghost {
background: transparent;
color: var(--ink);
border-color: var(--line-2);
}
.btn-ghost:hover { background: var(--surface-2); border-color: var(--neon); color: var(--neon); }
.btn-block { width: 100%; }
/* ===== Filters ===== */
.filters {
display: flex;
gap: 28px;
flex-wrap: wrap;
align-items: flex-end;
margin: 26px 0 18px;
padding: 18px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
}
.filter-group { display: flex; flex-direction: column; gap: 10px; }
.filter-title {
font-size: 10px; text-transform: uppercase; letter-spacing: 0.14em;
color: var(--muted); font-weight: 800;
}
.chips { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
font: inherit;
font-weight: 700;
font-size: 13px;
color: var(--ink-2);
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 999px;
padding: 8px 16px;
cursor: pointer;
transition: all 0.14s ease;
}
.chip:hover { border-color: var(--line-2); color: var(--ink); }
.chip.is-active {
background: var(--neon);
color: #0d0f12;
border-color: var(--neon);
}
.trainer-filter { display: flex; flex-direction: column; gap: 10px; }
.trainer-filter select {
appearance: none;
background: var(--surface-2) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%238b929c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 5 7 9 11 5'/%3E%3C/svg%3E") no-repeat right 12px center;
color: var(--ink);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 10px 36px 10px 12px;
font: inherit;
font-weight: 600;
cursor: pointer;
min-width: 200px;
}
/* ===== Day tabs ===== */
.day-tabs {
display: flex;
gap: 6px;
overflow-x: auto;
padding-bottom: 6px;
margin-bottom: 18px;
scrollbar-width: thin;
}
.day-tab {
flex: 0 0 auto;
font: inherit;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
color: var(--ink-2);
padding: 10px 16px;
cursor: pointer;
text-align: center;
min-width: 78px;
transition: all 0.14s ease;
}
.day-tab .dt-day { display: block; font-weight: 800; font-size: 14px; letter-spacing: 0.04em; }
.day-tab .dt-date { display: block; font-size: 11px; color: var(--muted); font-weight: 600; }
.day-tab:hover { border-color: var(--line-2); color: var(--ink); }
.day-tab.is-active {
background: var(--elevated);
border-color: var(--neon);
color: var(--ink);
box-shadow: inset 0 -3px 0 var(--neon);
}
.day-tab.is-today { position: relative; }
.day-tab.is-today .dt-date::after {
content: "TODAY";
display: inline-block;
margin-left: 5px;
font-size: 8px;
font-weight: 900;
letter-spacing: 0.08em;
color: var(--orange);
vertical-align: middle;
}
/* ===== Schedule rows ===== */
.schedule { display: flex; flex-direction: column; gap: 12px; }
.class-row {
display: grid;
grid-template-columns: 92px 1fr auto;
align-items: center;
gap: 18px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 16px 18px;
transition: border-color 0.15s ease, transform 0.15s ease, background 0.15s ease;
animation: rise 0.32s ease both;
}
@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.class-row:hover {
border-color: var(--line-2);
transform: translateY(-2px);
background: var(--surface-2);
}
.class-row.is-now {
border-color: var(--neon);
background: linear-gradient(180deg, var(--neon-50), transparent 70%), var(--surface);
}
.cr-time { display: flex; flex-direction: column; }
.cr-time b { font-size: 18px; font-weight: 900; letter-spacing: -0.01em; }
.cr-time span { font-size: 12px; color: var(--muted); font-weight: 600; }
.cr-main { min-width: 0; }
.cr-name { font-size: 17px; font-weight: 800; margin: 0 0 5px; }
.cr-meta { display: flex; flex-wrap: wrap; gap: 8px 14px; font-size: 13px; color: var(--ink-2); }
.cr-meta .m-item { display: inline-flex; align-items: center; gap: 5px; }
.cr-meta .m-item svg { opacity: 0.7; }
.cr-trainer { font-weight: 600; }
.cr-side { display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
.badge {
font-size: 10px;
font-weight: 900;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 4px 9px;
border-radius: 999px;
border: 1px solid transparent;
white-space: nowrap;
}
.badge.low { background: rgba(52, 211, 153, 0.14); color: var(--ok); border-color: rgba(52, 211, 153, 0.3); }
.badge.mid { background: rgba(251, 191, 36, 0.14); color: var(--warn); border-color: rgba(251, 191, 36, 0.3); }
.badge.high { background: var(--orange-soft); color: var(--orange); border-color: rgba(255, 106, 43, 0.35); }
.reserve-btn {
font: inherit;
font-weight: 800;
font-size: 13px;
background: var(--neon);
color: #0d0f12;
border: 1px solid var(--neon);
border-radius: var(--r-md);
padding: 9px 18px;
cursor: pointer;
transition: all 0.14s ease;
}
.reserve-btn:hover { background: var(--neon-d); transform: translateY(-1px); }
.reserve-btn.full {
background: transparent;
color: var(--muted);
border-color: var(--line);
cursor: not-allowed;
}
.spots { font-size: 11px; color: var(--muted); font-weight: 600; }
.spots.few { color: var(--orange); }
.empty {
text-align: center;
color: var(--muted);
padding: 40px 16px;
background: var(--surface);
border: 1px dashed var(--line-2);
border-radius: var(--r-lg);
font-weight: 600;
}
/* ===== Footer ===== */
.site-foot {
margin-top: 40px;
padding-top: 22px;
border-top: 1px solid var(--line);
font-size: 13px;
color: var(--ink-2);
}
.site-foot .muted { color: var(--muted); font-size: 12px; }
/* ===== Modal ===== */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(5, 6, 8, 0.72);
backdrop-filter: blur(4px);
display: grid;
place-items: center;
padding: 20px;
z-index: 50;
animation: fade 0.18s ease both;
}
.modal-backdrop[hidden] { display: none; }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.modal {
position: relative;
width: 100%;
max-width: 420px;
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
padding: 28px;
box-shadow: var(--shadow);
animation: pop 0.22s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
@keyframes pop { from { opacity: 0; transform: translateY(14px) scale(0.97); } to { opacity: 1; transform: none; } }
.modal h2 { margin: 0 0 8px; font-size: 24px; font-weight: 900; }
.modal-class {
margin: 0 0 18px;
padding: 12px 14px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
font-size: 14px;
color: var(--ink-2);
}
.modal-class strong { color: var(--ink); }
.modal-close {
position: absolute;
top: 14px; right: 14px;
width: 34px; height: 34px;
border-radius: 50%;
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--ink-2);
cursor: pointer;
font-size: 14px;
}
.modal-close:hover { color: var(--ink); border-color: var(--line-2); }
.reserve-form { display: flex; flex-direction: column; gap: 14px; }
.field { display: flex; flex-direction: column; gap: 6px; }
.field span { font-size: 12px; font-weight: 700; letter-spacing: 0.04em; color: var(--ink-2); }
.field input {
font: inherit;
background: var(--surface-2);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 12px 14px;
color: var(--ink);
}
.field input::placeholder { color: var(--muted); }
.form-note { margin: 0; text-align: center; font-size: 12px; color: var(--muted); }
/* ===== Toast ===== */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: var(--elevated);
color: var(--ink);
border: 1px solid var(--neon);
border-radius: var(--r-md);
padding: 13px 20px;
font-weight: 700;
font-size: 14px;
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 60;
max-width: calc(100vw - 32px);
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ===== Responsive ===== */
@media (max-width: 520px) {
.hero h1 { font-size: 30px; }
.filters { flex-direction: column; align-items: stretch; gap: 18px; }
.trainer-filter select { min-width: 0; width: 100%; }
.class-row {
grid-template-columns: 1fr;
gap: 12px;
}
.cr-time { flex-direction: row; align-items: baseline; gap: 8px; }
.cr-side {
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
}
.hero-actions .btn { flex: 1; }
}
/* ===== Print ===== */
@media print {
body { background: #fff; color: #000; }
.site-head .loc-select, .hero-actions, .filters, .reserve-btn, .modal-backdrop, .toast { display: none !important; }
.hero-sub, .brand-mark { color: #333; }
.day-tabs { display: none; }
.class-row {
border: 1px solid #ccc;
background: #fff !important;
break-inside: avoid;
page-break-inside: avoid;
}
.class-row[hidden] { display: grid !important; }
.schedule { gap: 6px; }
.cr-name, .cr-time b { color: #000; }
.day-section-print { display: block !important; font-weight: 900; margin: 14px 0 6px; }
}
.day-section-print { display: none; }(function () {
"use strict";
/* ---------- Data ---------- */
var TYPE_LABEL = {
strength: "Strength",
hiit: "HIIT",
cycle: "Cycle",
yoga: "Yoga & Mobility",
boxing: "Boxing",
};
// Each class: day index (0 = Mon ... 6 = Sun), start time (minutes), duration,
// name, trainer, room, type, intensity, capacity, booked.
var CLASSES = {
downtown: [
{ d: 0, t: 360, dur: 60, name: "Sunrise Strength", trainer: "Mara Devlin", room: "Iron Floor", type: "strength", intensity: "mid", cap: 16, booked: 11 },
{ d: 0, t: 540, dur: 45, name: "Metcon Blitz", trainer: "Theo Vance", room: "Turf Zone", type: "hiit", intensity: "high", cap: 20, booked: 20 },
{ d: 0, t: 1080, dur: 50, name: "Power Cycle 45", trainer: "Lena Brooks", room: "Spin Studio", type: "cycle", intensity: "mid", cap: 24, booked: 17 },
{ d: 0, t: 1140, dur: 60, name: "Deep Mobility Flow", trainer: "Priya Nair", room: "Studio B", type: "yoga", intensity: "low", cap: 18, booked: 6 },
{ d: 1, t: 375, dur: 45, name: "Dawn HIIT 45", trainer: "Theo Vance", room: "Turf Zone", type: "hiit", intensity: "high", cap: 20, booked: 14 },
{ d: 1, t: 720, dur: 60, name: "Barbell Club", trainer: "Mara Devlin", room: "Iron Floor", type: "strength", intensity: "high", cap: 14, booked: 9 },
{ d: 1, t: 1110, dur: 45, name: "Boxing Fundamentals", trainer: "Caleb Ortiz", room: "Ring Room", type: "boxing", intensity: "mid", cap: 16, booked: 13 },
{ d: 2, t: 360, dur: 60, name: "Sunrise Strength", trainer: "Mara Devlin", room: "Iron Floor", type: "strength", intensity: "mid", cap: 16, booked: 8 },
{ d: 2, t: 1050, dur: 50, name: "Climb Cycle", trainer: "Lena Brooks", room: "Spin Studio", type: "cycle", intensity: "high", cap: 24, booked: 22 },
{ d: 2, t: 1140, dur: 60, name: "Restore Yoga", trainer: "Priya Nair", room: "Studio B", type: "yoga", intensity: "low", cap: 18, booked: 10 },
{ d: 3, t: 390, dur: 45, name: "Metcon Blitz", trainer: "Theo Vance", room: "Turf Zone", type: "hiit", intensity: "high", cap: 20, booked: 16 },
{ d: 3, t: 720, dur: 60, name: "Powerlift Lab", trainer: "Mara Devlin", room: "Iron Floor", type: "strength", intensity: "high", cap: 12, booked: 7 },
{ d: 3, t: 1080, dur: 45, name: "Sparring Skills", trainer: "Caleb Ortiz", room: "Ring Room", type: "boxing", intensity: "high", cap: 16, booked: 16 },
{ d: 4, t: 360, dur: 60, name: "Friday Forge", trainer: "Mara Devlin", room: "Iron Floor", type: "strength", intensity: "mid", cap: 16, booked: 12 },
{ d: 4, t: 540, dur: 45, name: "Power Cycle 45", trainer: "Lena Brooks", room: "Spin Studio", type: "cycle", intensity: "mid", cap: 24, booked: 19 },
{ d: 4, t: 1110, dur: 50, name: "Flow & Release", trainer: "Priya Nair", room: "Studio B", type: "yoga", intensity: "low", cap: 18, booked: 5 },
{ d: 5, t: 480, dur: 50, name: "Weekend Warrior HIIT", trainer: "Theo Vance", room: "Turf Zone", type: "hiit", intensity: "high", cap: 22, booked: 18 },
{ d: 5, t: 600, dur: 45, name: "Boxing Burn", trainer: "Caleb Ortiz", room: "Ring Room", type: "boxing", intensity: "high", cap: 16, booked: 11 },
{ d: 5, t: 660, dur: 60, name: "Mobility Reset", trainer: "Priya Nair", room: "Studio B", type: "yoga", intensity: "low", cap: 18, booked: 7 },
{ d: 6, t: 540, dur: 60, name: "Sunday Strength", trainer: "Mara Devlin", room: "Iron Floor", type: "strength", intensity: "mid", cap: 16, booked: 10 },
{ d: 6, t: 600, dur: 50, name: "Endurance Cycle", trainer: "Lena Brooks", room: "Spin Studio", type: "cycle", intensity: "high", cap: 24, booked: 14 },
],
riverside: [
{ d: 0, t: 375, dur: 45, name: "Harbor HIIT", trainer: "Jonas Pike", room: "Bay Floor", type: "hiit", intensity: "high", cap: 18, booked: 12 },
{ d: 0, t: 1080, dur: 60, name: "Strong Foundations", trainer: "Ines Calderon", room: "Lift Hall", type: "strength", intensity: "mid", cap: 16, booked: 9 },
{ d: 0, t: 1140, dur: 50, name: "Sunset Cycle", trainer: "Ravi Shah", room: "Spin Loft", type: "cycle", intensity: "mid", cap: 20, booked: 16 },
{ d: 1, t: 360, dur: 60, name: "Riverside Yoga", trainer: "Tess Moreau", room: "Glass Studio", type: "yoga", intensity: "low", cap: 20, booked: 8 },
{ d: 1, t: 720, dur: 45, name: "Lunch Lift", trainer: "Ines Calderon", room: "Lift Hall", type: "strength", intensity: "mid", cap: 16, booked: 13 },
{ d: 1, t: 1110, dur: 45, name: "Boxing Basics", trainer: "Marcus Bell", room: "Dock Ring", type: "boxing", intensity: "mid", cap: 14, booked: 10 },
{ d: 2, t: 390, dur: 45, name: "Harbor HIIT", trainer: "Jonas Pike", room: "Bay Floor", type: "hiit", intensity: "high", cap: 18, booked: 18 },
{ d: 2, t: 1050, dur: 50, name: "Power Climb", trainer: "Ravi Shah", room: "Spin Loft", type: "cycle", intensity: "high", cap: 20, booked: 15 },
{ d: 3, t: 360, dur: 60, name: "Strong Foundations", trainer: "Ines Calderon", room: "Lift Hall", type: "strength", intensity: "mid", cap: 16, booked: 11 },
{ d: 3, t: 1080, dur: 45, name: "Combat Conditioning", trainer: "Marcus Bell", room: "Dock Ring", type: "boxing", intensity: "high", cap: 14, booked: 14 },
{ d: 3, t: 1140, dur: 60, name: "Evening Flow", trainer: "Tess Moreau", room: "Glass Studio", type: "yoga", intensity: "low", cap: 20, booked: 6 },
{ d: 4, t: 375, dur: 45, name: "Friday Fire HIIT", trainer: "Jonas Pike", room: "Bay Floor", type: "hiit", intensity: "high", cap: 18, booked: 13 },
{ d: 4, t: 1080, dur: 60, name: "Heavy Friday", trainer: "Ines Calderon", room: "Lift Hall", type: "strength", intensity: "high", cap: 12, booked: 8 },
{ d: 5, t: 540, dur: 50, name: "Weekend Cycle", trainer: "Ravi Shah", room: "Spin Loft", type: "cycle", intensity: "mid", cap: 20, booked: 17 },
{ d: 5, t: 600, dur: 60, name: "Saturday Sweat", trainer: "Jonas Pike", room: "Bay Floor", type: "hiit", intensity: "high", cap: 18, booked: 9 },
{ d: 6, t: 600, dur: 60, name: "Slow Sunday Yoga", trainer: "Tess Moreau", room: "Glass Studio", type: "yoga", intensity: "low", cap: 20, booked: 12 },
{ d: 6, t: 660, dur: 45, name: "Boxing Burn", trainer: "Marcus Bell", room: "Dock Ring", type: "boxing", intensity: "high", cap: 14, booked: 7 },
],
};
var LOCATION_LABEL = {
downtown: { short: "Downtown", full: "Downtown — 12 Forge St" },
riverside: { short: "Riverside", full: "Riverside — 88 Harbor Ave" },
};
var DAY_NAMES = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
/* ---------- State ---------- */
var state = {
location: "downtown",
type: "all",
trainer: "all",
day: 0,
};
/* ---------- Helpers ---------- */
function $(sel, ctx) { return (ctx || document).querySelector(sel); }
function fmtTime(mins) {
var h = Math.floor(mins / 60);
var m = mins % 60;
var ampm = h >= 12 ? "PM" : "AM";
var hr = h % 12; if (hr === 0) hr = 12;
return hr + ":" + (m < 10 ? "0" + m : m) + " " + ampm;
}
function jsDayToIndex(js) { return js === 0 ? 6 : js - 1; } // JS Sun=0 -> our Sun=6
var toastTimer;
function toast(msg) {
var el = $("#toast");
el.textContent = msg;
el.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { el.classList.remove("show"); }, 2600);
}
var TODAY_IDX = jsDayToIndex(new Date().getDay());
/* ---------- Build day tabs ---------- */
function buildDayTabs() {
var tabs = $("#dayTabs");
tabs.innerHTML = "";
var base = new Date();
var todayJs = base.getDay();
// Find Monday of current week.
var monday = new Date(base);
monday.setDate(base.getDate() - ((todayJs + 6) % 7));
DAY_NAMES.forEach(function (name, i) {
var dayDate = new Date(monday);
dayDate.setDate(monday.getDate() + i);
var btn = document.createElement("button");
btn.className = "day-tab";
btn.type = "button";
btn.setAttribute("role", "tab");
btn.dataset.day = i;
if (i === state.day) btn.classList.add("is-active");
if (i === TODAY_IDX) btn.classList.add("is-today");
btn.setAttribute("aria-selected", i === state.day ? "true" : "false");
btn.innerHTML =
'<span class="dt-day">' + name + "</span>" +
'<span class="dt-date">' + dayDate.getDate() + "</span>";
btn.addEventListener("click", function () {
state.day = i;
syncTabs();
renderSchedule();
});
tabs.appendChild(btn);
});
}
function syncTabs() {
var tabs = $("#dayTabs").children;
for (var i = 0; i < tabs.length; i++) {
var active = Number(tabs[i].dataset.day) === state.day;
tabs[i].classList.toggle("is-active", active);
tabs[i].setAttribute("aria-selected", active ? "true" : "false");
}
}
/* ---------- Trainer dropdown ---------- */
function buildTrainerOptions() {
var sel = $("#trainerSelect");
var trainers = [];
CLASSES[state.location].forEach(function (c) {
if (trainers.indexOf(c.trainer) === -1) trainers.push(c.trainer);
});
trainers.sort();
sel.innerHTML = '<option value="all">All trainers</option>';
trainers.forEach(function (t) {
var o = document.createElement("option");
o.value = t;
o.textContent = t;
sel.appendChild(o);
});
// Reset trainer filter if no longer present.
if (state.trainer !== "all" && trainers.indexOf(state.trainer) === -1) {
state.trainer = "all";
}
sel.value = state.trainer;
}
/* ---------- Render schedule ---------- */
function intensityLabel(i) {
return i === "low" ? "Low" : i === "mid" ? "Moderate" : "High";
}
function renderSchedule() {
var container = $("#schedule");
container.innerHTML = "";
var list = CLASSES[state.location]
.filter(function (c) { return c.d === state.day; })
.filter(function (c) { return state.type === "all" || c.type === state.type; })
.filter(function (c) { return state.trainer === "all" || c.trainer === state.trainer; })
.sort(function (a, b) { return a.t - b.t; });
var nowMins = new Date().getHours() * 60 + new Date().getMinutes();
if (!list.length) {
$("#emptyState").hidden = false;
return;
}
$("#emptyState").hidden = true;
list.forEach(function (c, idx) {
var spotsLeft = c.cap - c.booked;
var full = spotsLeft <= 0;
var isNow = state.day === TODAY_IDX && nowMins >= c.t && nowMins < c.t + c.dur;
var row = document.createElement("article");
row.className = "class-row" + (isNow ? " is-now" : "");
row.style.animationDelay = (idx * 0.04) + "s";
var spotsClass = full ? "" : spotsLeft <= 3 ? "spots few" : "spots";
var spotsText = full ? "Class full" : spotsLeft + " spots left";
row.innerHTML =
'<div class="cr-time">' +
"<b>" + fmtTime(c.t) + "</b>" +
"<span>" + c.dur + " min</span>" +
"</div>" +
'<div class="cr-main">' +
'<h3 class="cr-name">' + c.name + (isNow ? ' <span class="badge high">In session</span>' : "") + "</h3>" +
'<div class="cr-meta">' +
'<span class="m-item cr-trainer">' + icon("user") + c.trainer + "</span>" +
'<span class="m-item">' + icon("pin") + c.room + "</span>" +
'<span class="m-item">' + icon("tag") + TYPE_LABEL[c.type] + "</span>" +
"</div>" +
"</div>" +
'<div class="cr-side">' +
'<span class="badge ' + c.intensity + '">' + intensityLabel(c.intensity) + " intensity</span>" +
(full
? '<button class="reserve-btn full" type="button" disabled>Waitlist</button>'
: '<button class="reserve-btn" type="button">Reserve</button>') +
'<span class="' + spotsClass + '">' + spotsText + "</span>" +
"</div>";
if (!full) {
row.querySelector(".reserve-btn").addEventListener("click", function () {
openModal(c);
});
} else {
row.querySelector(".reserve-btn").addEventListener("click", function () {
toast("Waitlist requested for " + c.name);
});
row.querySelector(".reserve-btn").disabled = false;
row.querySelector(".reserve-btn").style.cursor = "pointer";
}
container.appendChild(row);
});
}
function icon(kind) {
var paths = {
user: '<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/><path d="M2.5 14a5.5 5.5 0 0 1 11 0"/>',
pin: '<path d="M8 1.5c-2.5 0-4.5 2-4.5 4.5C3.5 9.5 8 14.5 8 14.5s4.5-5 4.5-8.5C12.5 3.5 10.5 1.5 8 1.5Z"/><circle cx="8" cy="6" r="1.6"/>',
tag: '<path d="M2 2h5l7 7-5 5-7-7V2Z"/><circle cx="5" cy="5" r="1"/>',
};
return '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">' + paths[kind] + "</svg>";
}
/* ---------- Modal ---------- */
var activeClass = null;
function openModal(c) {
activeClass = c;
$("#modalClassInfo").innerHTML =
"<strong>" + c.name + "</strong> · " + fmtTime(c.t) + " with " + c.trainer +
"<br>" + c.room + " · " + TYPE_LABEL[c.type];
$("#modal").hidden = false;
setTimeout(function () { $("#rName").focus(); }, 30);
document.addEventListener("keydown", onEsc);
}
function closeModal() {
$("#modal").hidden = true;
$("#reserveForm").reset();
document.removeEventListener("keydown", onEsc);
}
function onEsc(e) { if (e.key === "Escape") closeModal(); }
/* ---------- Location change ---------- */
function setLocation(loc) {
state.location = loc;
state.trainer = "all";
var lbl = LOCATION_LABEL[loc];
$("#heroLocation").textContent = lbl.short;
$("#footLocation").textContent = lbl.full;
buildTrainerOptions();
renderSchedule();
toast("Showing " + lbl.short + " schedule");
}
/* ---------- Wire up ---------- */
function init() {
buildDayTabs();
buildTrainerOptions();
renderSchedule();
$("#locationSelect").addEventListener("change", function (e) {
setLocation(e.target.value);
});
$("#typeChips").addEventListener("click", function (e) {
var chip = e.target.closest(".chip");
if (!chip) return;
state.type = chip.dataset.type;
var chips = $("#typeChips").children;
for (var i = 0; i < chips.length; i++) chips[i].classList.toggle("is-active", chips[i] === chip);
renderSchedule();
});
$("#trainerSelect").addEventListener("change", function (e) {
state.trainer = e.target.value;
renderSchedule();
});
$("#trialBtn").addEventListener("click", function () {
toast("Pick any class below and hit Reserve to start your free trial");
});
$("#printBtn").addEventListener("click", function () { window.print(); });
$("#modalClose").addEventListener("click", closeModal);
$("#modal").addEventListener("click", function (e) {
if (e.target === $("#modal")) closeModal();
});
$("#reserveForm").addEventListener("submit", function (e) {
e.preventDefault();
var name = $("#rName").value.trim();
var email = $("#rEmail").value.trim();
if (!name) { toast("Please enter your name"); $("#rName").focus(); return; }
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { toast("Enter a valid email"); $("#rEmail").focus(); return; }
closeModal();
toast("Reserved! " + (activeClass ? activeClass.name : "") + " — trial pass sent to " + email);
});
}
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>IRONPULSE — Weekly Class Schedule</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;900&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<!-- ===== Header ===== -->
<header class="site-head">
<div class="brand">
<span class="brand-mark" aria-hidden="true">⚡</span>
<div class="brand-text">
<span class="brand-name">IRONPULSE</span>
<span class="brand-sub">Strength · Conditioning · Recovery</span>
</div>
</div>
<label class="loc-select">
<span class="loc-label">Location</span>
<select id="locationSelect" aria-label="Choose location">
<option value="downtown">Downtown — 12 Forge St</option>
<option value="riverside">Riverside — 88 Harbor Ave</option>
</select>
</label>
</header>
<!-- ===== Hero ===== -->
<section class="hero">
<p class="eyebrow">Public Class Schedule</p>
<h1>Train this week at <span id="heroLocation">Downtown</span></h1>
<p class="hero-sub">Drop into any class with a free trial pass. Browse the full week, filter by what you love, and lock your spot in seconds.</p>
<div class="hero-actions">
<button class="btn btn-neon" id="trialBtn" type="button">Claim a free trial</button>
<button class="btn btn-ghost" id="printBtn" type="button">Print schedule</button>
</div>
</section>
<!-- ===== Filters ===== -->
<section class="filters" aria-label="Filter classes">
<div class="filter-group">
<span class="filter-title">Class type</span>
<div class="chips" id="typeChips" role="group" aria-label="Filter by class type">
<button class="chip is-active" data-type="all" type="button">All</button>
<button class="chip" data-type="strength" type="button">Strength</button>
<button class="chip" data-type="hiit" type="button">HIIT</button>
<button class="chip" data-type="cycle" type="button">Cycle</button>
<button class="chip" data-type="yoga" type="button">Yoga & Mobility</button>
<button class="chip" data-type="boxing" type="button">Boxing</button>
</div>
</div>
<div class="filter-group">
<label class="trainer-filter">
<span class="filter-title">Trainer</span>
<select id="trainerSelect" aria-label="Filter by trainer">
<option value="all">All trainers</option>
</select>
</label>
</div>
</section>
<!-- ===== Day tabs ===== -->
<nav class="day-tabs" id="dayTabs" role="tablist" aria-label="Days of the week"></nav>
<!-- ===== Schedule ===== -->
<main class="schedule" id="schedule" aria-live="polite"></main>
<p class="empty" id="emptyState" hidden>No classes match your filters. Try clearing the trainer or type filter.</p>
<footer class="site-foot">
<p>IRONPULSE Fitness · Open 5:00–23:00 daily · <span id="footLocation">Downtown — 12 Forge St</span></p>
<p class="muted">Schedule is fictional and for demo purposes. Times shown in local time.</p>
</footer>
</div>
<!-- ===== Reserve modal ===== -->
<div class="modal-backdrop" id="modal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<button class="modal-close" id="modalClose" type="button" aria-label="Close">✕</button>
<p class="eyebrow">Free trial reservation</p>
<h2 id="modalTitle">Reserve your spot</h2>
<p class="modal-class" id="modalClassInfo"></p>
<form id="reserveForm" class="reserve-form" novalidate>
<label class="field">
<span>Full name</span>
<input type="text" id="rName" name="name" placeholder="Alex Rivera" required />
</label>
<label class="field">
<span>Email</span>
<input type="email" id="rEmail" name="email" placeholder="[email protected]" required />
</label>
<button class="btn btn-neon btn-block" type="submit">Confirm reservation</button>
<p class="form-note">No card required. We'll email your trial pass.</p>
</form>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Public Schedule Page
A marketing-first weekly schedule for the fictional IRONPULSE gym, designed to convert curious visitors into trial members. A location selector at the top swaps the entire week between the Downtown and Riverside studios, updating the trainer roster and class list instantly. Seven day tabs run across the top with the current weekday auto-highlighted as TODAY, so the view always opens on the right day.
Each class is a high-contrast row showing the start time and duration, the class name, the assigned trainer, the room, the class type, and a color-coded intensity badge (low, moderate, high). Live spot counters warn when only a few places remain, full classes flip to a waitlist action, and any class currently running on today’s tab gets an “In session” marker and a neon-tinted row. Two filters — a chip row for class type and a trainer dropdown — narrow the day’s lineup in real time, with a friendly empty state when nothing matches.
Pressing Reserve on any open class opens a free-trial modal that validates name and email before confirming with a toast. Secondary actions include a hero “Claim a free trial” CTA and a Print button that triggers a dedicated print stylesheet for a clean, ink-friendly handout. Everything runs on a single vanilla JavaScript file with no dependencies, full keyboard support, visible focus rings, and a responsive layout that collapses gracefully down to 360px.