Coworking — Events Calendar
A warm industrial events calendar page for a coworking space, listing workshops, talks, networking sessions and socials as rich event cards with date blocks, host details and live attendance counts. Members filter the programme by event type, flip between a stacked list and a colour-coded month grid, open any event in a detail dialog, and RSVP with instant toast feedback and a running going-to tally. Self-contained vanilla page, accessible controls, responsive down to small phones.
MCP
Code
:root {
--concrete: #efeae3;
--concrete-d: #e2dcd2;
--amber: #e8902b;
--amber-d: #cc7918;
--amber-50: #fdf1e2;
--char: #1c1b19;
--ink: #26241f;
--ink-2: #4a463e;
--muted: #7b766c;
--bg: #f6f3ee;
--surface: #ffffff;
--plant: #5f7a52;
--line: rgba(28, 27, 25, 0.1);
--line-2: rgba(28, 27, 25, 0.18);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--c-workshop: #e8902b;
--c-networking: #5f7a52;
--c-talk: #3f6ea8;
--c-social: #c8527f;
--shadow-sm: 0 1px 2px rgba(28, 27, 25, 0.06), 0 1px 3px rgba(28, 27, 25, 0.05);
--shadow-md: 0 6px 20px rgba(28, 27, 25, 0.08);
--shadow-lg: 0 18px 50px rgba(28, 27, 25, 0.18);
}
* { box-sizing: border-box; }
html { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
}
.page { max-width: 1120px; margin: 0 auto; padding: 0 20px 48px; }
/* ---------- Topbar ---------- */
.topbar {
display: flex;
align-items: center;
gap: 20px;
padding: 18px 0;
position: sticky;
top: 0;
z-index: 20;
background: linear-gradient(var(--bg) 78%, rgba(246, 243, 238, 0));
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
width: 38px; height: 38px; border-radius: 11px;
background: linear-gradient(135deg, var(--amber), var(--amber-d));
position: relative; flex: none;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
}
.brand-mark::after {
content: ""; position: absolute; inset: 11px 9px;
border-radius: 3px; border: 2px solid rgba(255, 255, 255, 0.85);
border-top: none;
}
.brand-text { display: flex; flex-direction: column; line-height: 1.25; }
.brand-text strong { font-size: 16px; font-weight: 800; letter-spacing: -0.01em; color: var(--char); }
.brand-text span { font-size: 11.5px; color: var(--muted); }
.topnav { display: flex; gap: 4px; margin-left: 8px; }
.topnav a {
text-decoration: none; color: var(--ink-2); font-size: 14px; font-weight: 500;
padding: 8px 12px; border-radius: var(--r-sm);
}
.topnav a:hover { background: var(--concrete); color: var(--char); }
.topnav a.active { background: var(--char); color: #fff; }
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 12px; }
.avatar {
width: 36px; height: 36px; border-radius: 50%; flex: none;
display: grid; place-items: center;
font-size: 12.5px; font-weight: 700; color: #fff;
background: linear-gradient(135deg, var(--plant), #4c6541);
box-shadow: var(--shadow-sm);
}
/* ---------- Buttons ---------- */
.btn {
font: inherit; font-weight: 600; font-size: 14px;
border: 1px solid transparent; border-radius: var(--r-sm);
padding: 9px 16px; cursor: pointer; transition: transform .08s ease, background .15s ease, box-shadow .15s ease;
}
.btn:active { transform: translateY(1px); }
.btn-primary { background: var(--amber); color: #2a1c08; box-shadow: var(--shadow-sm); }
.btn-primary:hover { background: var(--amber-d); color: #fff; }
.btn-primary.is-going { background: var(--plant); color: #fff; }
.btn-primary.is-going:hover { background: #4c6541; }
.btn-ghost { background: var(--surface); color: var(--ink); border-color: var(--line-2); }
.btn-ghost:hover { background: var(--concrete); }
/* ---------- Hero ---------- */
.hero {
display: flex; gap: 28px; align-items: flex-end; justify-content: space-between;
padding: 14px 0 26px; flex-wrap: wrap;
}
.eyebrow { margin: 0 0 8px; font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--amber-d); }
.hero h1 { margin: 0 0 8px; font-size: clamp(26px, 4vw, 38px); font-weight: 800; letter-spacing: -0.02em; color: var(--char); }
.hero-sub { margin: 0; color: var(--ink-2); max-width: 44ch; }
.hero-stats { display: flex; gap: 10px; }
.stat {
background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-md);
padding: 12px 16px; min-width: 92px; box-shadow: var(--shadow-sm);
}
.stat strong { display: block; font-size: 24px; font-weight: 800; color: var(--char); letter-spacing: -0.02em; }
.stat span { font-size: 11.5px; color: var(--muted); }
/* ---------- Toolbar ---------- */
.toolbar {
display: flex; align-items: center; justify-content: space-between; gap: 16px;
margin-bottom: 22px; flex-wrap: wrap;
}
.filters { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
font: inherit; font-size: 13.5px; font-weight: 600; color: var(--ink-2);
background: var(--surface); border: 1px solid var(--line-2); border-radius: 999px;
padding: 7px 14px; cursor: pointer; display: inline-flex; align-items: center; gap: 7px;
transition: background .15s, color .15s, border-color .15s;
}
.chip:hover { border-color: var(--char); color: var(--char); }
.chip.is-on { background: var(--char); color: #fff; border-color: var(--char); }
.dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
.dot-workshop { background: var(--c-workshop); }
.dot-networking { background: var(--c-networking); }
.dot-talk { background: var(--c-talk); }
.dot-social { background: var(--c-social); }
.view-toggle { display: inline-flex; background: var(--concrete); padding: 4px; border-radius: 999px; border: 1px solid var(--line); }
.toggle-btn {
font: inherit; font-size: 13.5px; font-weight: 600; color: var(--ink-2);
background: transparent; border: none; border-radius: 999px; padding: 6px 18px; cursor: pointer;
}
.toggle-btn.is-on { background: var(--surface); color: var(--char); box-shadow: var(--shadow-sm); }
/* ---------- List view ---------- */
.event-list { display: flex; flex-direction: column; gap: 14px; }
.event-card {
display: grid; grid-template-columns: 88px 1fr auto; gap: 18px; align-items: center;
background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
padding: 16px 18px; box-shadow: var(--shadow-sm);
transition: box-shadow .18s ease, transform .12s ease, border-color .18s;
text-align: left;
}
.event-card:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); border-color: var(--line-2); }
.ev-date {
display: grid; place-items: center; text-align: center;
background: var(--amber-50); border: 1px solid rgba(232, 144, 43, 0.25);
border-radius: var(--r-md); padding: 10px 6px; min-height: 72px;
}
.ev-date .dow { font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--amber-d); }
.ev-date .day { font-size: 26px; font-weight: 800; line-height: 1; color: var(--char); }
.ev-date .mon { font-size: 11px; color: var(--muted); }
.ev-main { min-width: 0; cursor: pointer; }
.ev-top { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; flex-wrap: wrap; }
.badge {
font-size: 11px; font-weight: 700; letter-spacing: .02em; text-transform: uppercase;
padding: 3px 9px; border-radius: 999px; color: #fff;
}
.badge[data-type="workshop"] { background: var(--c-workshop); }
.badge[data-type="networking"] { background: var(--c-networking); }
.badge[data-type="talk"] { background: var(--c-talk); }
.badge[data-type="social"] { background: var(--c-social); }
.ev-time { font-size: 12.5px; color: var(--muted); font-weight: 500; }
.ev-main h3 { margin: 0 0 4px; font-size: 17px; font-weight: 700; color: var(--char); letter-spacing: -0.01em; }
.ev-meta { font-size: 13px; color: var(--ink-2); display: flex; gap: 12px; flex-wrap: wrap; }
.ev-meta .pin::before { content: "◦ "; color: var(--muted); }
.ev-side { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
.attend { font-size: 12px; color: var(--muted); white-space: nowrap; }
.attend strong { color: var(--ink); }
.attend.near-full strong { color: var(--danger); }
.empty {
text-align: center; color: var(--muted); padding: 48px 0; font-size: 14.5px;
}
/* ---------- Calendar view ---------- */
.cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; flex-wrap: wrap; gap: 10px; }
.cal-head h2 { margin: 0; font-size: 19px; font-weight: 800; color: var(--char); }
.legend { display: flex; gap: 14px; flex-wrap: wrap; }
.legend span { font-size: 12px; color: var(--ink-2); display: inline-flex; align-items: center; gap: 6px; }
.cal-grid-head {
display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px; margin-bottom: 8px;
}
.cal-grid-head span { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); text-align: center; }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px; }
.cal-cell {
background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-md);
min-height: 92px; padding: 8px; display: flex; flex-direction: column; gap: 5px;
}
.cal-cell.is-empty { background: transparent; border: 1px dashed var(--line); }
.cal-cell.is-today { border-color: var(--amber); box-shadow: inset 0 0 0 1px var(--amber); }
.cal-num { font-size: 12.5px; font-weight: 700; color: var(--ink-2); }
.cal-cell.is-today .cal-num { color: var(--amber-d); }
.cal-ev {
font: inherit; text-align: left; border: none; cursor: pointer;
font-size: 11px; font-weight: 600; color: #fff; line-height: 1.25;
padding: 4px 7px; border-radius: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
transition: filter .12s;
}
.cal-ev:hover { filter: brightness(1.08); }
.cal-ev[data-type="workshop"] { background: var(--c-workshop); }
.cal-ev[data-type="networking"] { background: var(--c-networking); }
.cal-ev[data-type="talk"] { background: var(--c-talk); }
.cal-ev[data-type="social"] { background: var(--c-social); }
.cal-ev.is-hidden { display: none; }
/* ---------- Footer ---------- */
.foot {
margin-top: 36px; padding-top: 18px; border-top: 1px solid var(--line);
display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
font-size: 12.5px; color: var(--muted);
}
/* ---------- Modal ---------- */
.modal { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; padding: 20px; }
.modal[hidden] { display: none; }
.modal-backdrop { position: absolute; inset: 0; background: rgba(28, 27, 25, 0.5); backdrop-filter: blur(2px); }
.modal-card {
position: relative; width: min(520px, 100%); background: var(--surface);
border-radius: var(--r-lg); overflow: hidden; box-shadow: var(--shadow-lg);
animation: pop .18s ease;
}
@keyframes pop { from { opacity: 0; transform: translateY(12px) scale(.98); } to { opacity: 1; transform: none; } }
.modal-x {
position: absolute; top: 12px; right: 12px; z-index: 2;
width: 32px; height: 32px; border-radius: 50%; border: none; cursor: pointer;
background: rgba(255, 255, 255, 0.85); color: var(--char); font-size: 20px; line-height: 1;
}
.modal-x:hover { background: #fff; }
.modal-photo { height: 150px; background: linear-gradient(135deg, var(--amber), var(--amber-d)); }
.modal-body { padding: 18px 22px 22px; }
.modal-body h2 { margin: 8px 0 12px; font-size: 22px; font-weight: 800; color: var(--char); letter-spacing: -0.02em; }
.modal-meta { display: flex; gap: 14px; flex-wrap: wrap; font-size: 13px; color: var(--ink-2); margin-bottom: 12px; }
.modal-meta span::before { content: "◦ "; color: var(--muted); }
.modal-body p { margin: 0 0 16px; color: var(--ink-2); font-size: 14.5px; }
.modal-host { display: flex; align-items: center; gap: 11px; padding: 12px 0; border-top: 1px solid var(--line); }
.modal-host strong { display: block; font-size: 14px; color: var(--char); }
.modal-host span { font-size: 12.5px; color: var(--muted); }
.modal-foot { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding-top: 14px; border-top: 1px solid var(--line); }
/* ---------- Toast ---------- */
.toast-wrap { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); z-index: 80; display: flex; flex-direction: column; gap: 8px; align-items: center; }
.toast {
background: var(--char); color: #fff; font-size: 13.5px; font-weight: 500;
padding: 11px 18px; border-radius: 999px; box-shadow: var(--shadow-lg);
display: flex; align-items: center; gap: 9px; animation: rise .22s ease;
}
.toast .tdot { width: 8px; height: 8px; border-radius: 50%; background: var(--plant); flex: none; }
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.toast.out { opacity: 0; transform: translateY(8px); transition: opacity .25s, transform .25s; }
/* ---------- Responsive ---------- */
@media (max-width: 760px) {
.topnav { display: none; }
.hero-stats { order: 3; }
}
@media (max-width: 520px) {
.page { padding: 0 14px 40px; }
.topbar { gap: 12px; }
.topbar-right .btn-ghost { display: none; }
.hero-stats { gap: 8px; }
.stat { min-width: 0; flex: 1; padding: 10px 12px; }
.event-card { grid-template-columns: 64px 1fr; gap: 12px; padding: 14px; }
.ev-side { grid-column: 1 / -1; flex-direction: row; align-items: center; justify-content: space-between; }
.ev-date { min-height: 60px; }
.ev-date .day { font-size: 22px; }
.cal-grid, .cal-grid-head { gap: 4px; }
.cal-cell { min-height: 64px; padding: 5px; }
.cal-grid-head span { font-size: 9.5px; }
.cal-num { font-size: 11px; }
.cal-ev { font-size: 0; padding: 0; height: 7px; border-radius: 4px; }
.foot { flex-direction: column; gap: 6px; }
}(function () {
"use strict";
// June 2026 starts on a Monday — clean grid alignment.
var MONTH = "Jun";
var TODAY = 18;
var TYPE_LABEL = {
workshop: "Workshop",
networking: "Networking",
talk: "Talk",
social: "Social"
};
var events = [
{
id: "e1", day: 18, dow: "Thu", type: "workshop",
title: "Risograph Print Lab",
time: "18:30 – 20:30", room: "The Press Room",
host: "Dario Vence", going: false, cap: 14, count: 9,
desc: "Hands-on intro to two-colour riso printing. Pull a poster from your own artwork — paper, ink and aprons provided. Beginner friendly."
},
{
id: "e2", day: 19, dow: "Fri", type: "social",
title: "Friday Mill Mixer",
time: "17:00 – 19:00", room: "Loomhouse Café",
host: "Mara Quintero", going: false, cap: 60, count: 41,
desc: "Wind down the week with neighbours from every floor. Local cider, warm bread from the bakery downstairs, and a vinyl set from member DJ Halse."
},
{
id: "e3", day: 20, dow: "Sat", type: "workshop",
title: "Ceramics: Pinch Pot Basics",
time: "10:00 – 13:00", room: "Clay Studio B",
host: "Ines Okafor", going: false, cap: 10, count: 10,
desc: "Build a small vessel by hand and learn the slip-and-score technique. Clay fired and ready for collection the following week."
},
{
id: "e4", day: 23, dow: "Tue", type: "talk",
title: "Lunch Talk: Pricing Your Studio Work",
time: "12:30 – 13:30", room: "The Gallery",
host: "Tobias Rein", going: false, cap: 40, count: 22,
desc: "Freelance illustrator Tobias breaks down how he sets day rates and licensing fees. Bring your lunch — coffee is on the house."
},
{
id: "e5", day: 24, dow: "Wed", type: "networking",
title: "Makers & Founders Roundtable",
time: "09:00 – 10:30", room: "Boardroom · Mill 2",
host: "Priya Sundaram", going: false, cap: 16, count: 11,
desc: "Small-group roundtable for product makers and early founders. Share a current blocker, leave with three concrete intros."
},
{
id: "e6", day: 25, dow: "Thu", type: "talk",
title: "Type Design in the Wild",
time: "18:00 – 19:30", room: "The Gallery",
host: "Lena Brandt", going: false, cap: 50, count: 17,
desc: "A walk through how a custom typeface goes from sketch to shipped brand. Q&A and a small specimen giveaway to close."
},
{
id: "e7", day: 26, dow: "Fri", type: "social",
title: "Rooftop Garden Supper",
time: "19:00 – 22:00", room: "Mill Rooftop",
host: "Community Team", going: false, cap: 30, count: 28,
desc: "Long-table dinner cooked with herbs from the rooftop plots. Members +1 welcome. Vegetarian by default — flag dietary needs on RSVP."
},
{
id: "e8", day: 30, dow: "Tue", type: "networking",
title: "New Members Welcome Coffee",
time: "08:30 – 09:30", room: "Loomhouse Café",
host: "Mara Quintero", going: false, cap: 25, count: 6,
desc: "Joined this month? Come meet the team and other newcomers over flat whites. We’ll show you the quiet rooms and the good plug sockets."
}
];
var activeFilter = "all";
// ---------- helpers ----------
function el(id) { return document.getElementById(id); }
function toast(msg) {
var wrap = el("toastWrap");
var t = document.createElement("div");
t.className = "toast";
t.innerHTML = '<span class="tdot"></span>' + msg;
wrap.appendChild(t);
setTimeout(function () {
t.classList.add("out");
setTimeout(function () { t.remove(); }, 280);
}, 2400);
}
function decode(s) {
var d = document.createElement("textarea");
d.innerHTML = s;
return d.value;
}
function updateGoingCount() {
var n = events.filter(function (e) { return e.going; }).length;
el("goingCount").textContent = n;
}
function attendHtml(e) {
var nearFull = e.cap - e.count <= 3;
var spots = e.cap - e.count;
var label = e.count >= e.cap
? "Full · waitlist"
: "<strong>" + e.count + "</strong>/" + e.cap + " going";
return '<span class="attend ' + (nearFull ? "near-full" : "") + '">' + label +
(spots > 0 && nearFull ? " · " + spots + " left" : "") + "</span>";
}
// ---------- list rendering ----------
function renderList() {
var host = el("eventList");
host.innerHTML = "";
var shown = events.filter(function (e) {
return activeFilter === "all" || e.type === activeFilter;
});
el("listEmpty").hidden = shown.length > 0;
shown.forEach(function (e) {
var card = document.createElement("article");
card.className = "event-card";
card.dataset.type = e.type;
card.innerHTML =
'<div class="ev-date"><span class="dow">' + e.dow + '</span>' +
'<span class="day">' + e.day + '</span><span class="mon">' + MONTH + '</span></div>' +
'<div class="ev-main" role="button" tabindex="0">' +
'<div class="ev-top">' +
'<span class="badge" data-type="' + e.type + '">' + TYPE_LABEL[e.type] + '</span>' +
'<span class="ev-time">' + e.time + '</span>' +
'</div>' +
'<h3>' + e.title + '</h3>' +
'<div class="ev-meta"><span class="pin">' + e.room + '</span>' +
'<span>Hosted by ' + e.host + '</span></div>' +
'</div>' +
'<div class="ev-side">' + attendHtml(e) +
'<button class="btn btn-primary' + (e.going ? " is-going" : "") + '" type="button" data-rsvp="' + e.id + '">' +
(e.going ? "Going ✓" : "RSVP") + '</button>' +
'</div>';
var main = card.querySelector(".ev-main");
main.addEventListener("click", function () { openModal(e.id); });
main.addEventListener("keydown", function (ev) {
if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); openModal(e.id); }
});
host.appendChild(card);
});
}
// ---------- calendar rendering ----------
function renderCalendar() {
var grid = el("calGrid");
grid.innerHTML = "";
// June 2026: 30 days, day 1 = Monday (index 0)
var byDay = {};
events.forEach(function (e) { (byDay[e.day] = byDay[e.day] || []).push(e); });
for (var d = 1; d <= 30; d++) {
var cell = document.createElement("div");
cell.className = "cal-cell" + (d === TODAY ? " is-today" : "");
var html = '<span class="cal-num">' + d + "</span>";
(byDay[d] || []).forEach(function (e) {
var hidden = activeFilter !== "all" && e.type !== activeFilter;
html += '<button class="cal-ev' + (hidden ? " is-hidden" : "") +
'" data-type="' + e.type + '" data-open="' + e.id + '" title="' + decode(e.title) +
'">' + e.title + "</button>";
});
cell.innerHTML = html;
grid.appendChild(cell);
}
grid.querySelectorAll("[data-open]").forEach(function (b) {
b.addEventListener("click", function () { openModal(b.dataset.open); });
});
}
// ---------- modal ----------
function findEvent(id) {
for (var i = 0; i < events.length; i++) if (events[i].id === id) return events[i];
return null;
}
function openModal(id) {
var e = findEvent(id);
if (!e) return;
el("modalBadge").textContent = TYPE_LABEL[e.type];
el("modalBadge").setAttribute("data-type", e.type);
el("modalTitle").innerHTML = e.title;
el("modalMeta").innerHTML =
"<span>" + e.dow + " " + e.day + " " + MONTH + "</span><span>" + e.time + "</span>";
el("modalDesc").innerHTML = e.desc;
el("modalHost").textContent = e.host;
el("modalHostAv").textContent = e.host.split(" ").map(function (w) { return w[0]; }).join("").slice(0, 2).toUpperCase();
el("modalRoom").textContent = e.room;
el("modalAttend").outerHTML = attendHtml(e).replace('class="attend', 'id="modalAttend" class="attend');
el("modalPhoto").style.background =
"linear-gradient(135deg, var(--c-" + e.type + "), rgba(28,27,25,0.35))";
var rsvp = el("modalRsvp");
rsvp.className = "btn btn-primary" + (e.going ? " is-going" : "");
rsvp.textContent = e.going ? "Going ✓" : "RSVP";
rsvp.onclick = function () { toggleRsvp(e.id, true); };
el("modal").hidden = false;
rsvp.focus();
}
function closeModal() { el("modal").hidden = true; }
// ---------- RSVP ----------
function toggleRsvp(id, fromModal) {
var e = findEvent(id);
if (!e) return;
if (!e.going && e.count >= e.cap) {
toast("Event is full — you’ve joined the waitlist for " + decode(e.title));
e.going = true;
} else {
e.going = !e.going;
e.count += e.going ? 1 : -1;
toast(e.going
? "You’re going to " + decode(e.title) + " 🎉"
: "RSVP cancelled for " + decode(e.title));
}
updateGoingCount();
renderList();
if (fromModal) openModal(id);
}
// ---------- view toggle ----------
function setView(view) {
var isList = view === "list";
el("listView").hidden = !isList;
el("calendarView").hidden = isList;
document.querySelectorAll(".toggle-btn").forEach(function (b) {
var on = b.dataset.view === view;
b.classList.toggle("is-on", on);
b.setAttribute("aria-selected", on ? "true" : "false");
});
if (!isList) renderCalendar();
}
// ---------- wiring ----------
document.querySelectorAll(".chip").forEach(function (c) {
c.addEventListener("click", function () {
activeFilter = c.dataset.filter;
document.querySelectorAll(".chip").forEach(function (x) { x.classList.toggle("is-on", x === c); });
renderList();
if (!el("calendarView").hidden) renderCalendar();
});
});
document.querySelectorAll(".toggle-btn").forEach(function (b) {
b.addEventListener("click", function () { setView(b.dataset.view); });
});
document.addEventListener("click", function (ev) {
var rsvpBtn = ev.target.closest("[data-rsvp]");
if (rsvpBtn) { toggleRsvp(rsvpBtn.dataset.rsvp, false); return; }
if (ev.target.hasAttribute("data-close")) closeModal();
});
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape" && !el("modal").hidden) closeModal();
});
el("proposeBtn").addEventListener("click", function () {
toast("Thanks! The community team will review your event idea.");
});
// ---------- init ----------
updateGoingCount();
renderList();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Loomhouse — Events Calendar</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>
<div class="page">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true"></span>
<div class="brand-text">
<strong>Loomhouse</strong>
<span>Riverside Mill · Studio & Coworking</span>
</div>
</div>
<nav class="topnav" aria-label="Primary">
<a href="#" class="active">Events</a>
<a href="#">Spaces</a>
<a href="#">Members</a>
<a href="#">Café</a>
</nav>
<div class="topbar-right">
<button class="btn btn-ghost" type="button" id="proposeBtn">Propose an event</button>
<span class="avatar" title="Mara Quintero">MQ</span>
</div>
</header>
<main class="main">
<section class="hero">
<div class="hero-copy">
<p class="eyebrow">June 2026 · Community Programme</p>
<h1>What’s happening at the Mill</h1>
<p class="hero-sub">Workshops, talks and member mixers — open to every desk holder. RSVP to hold your spot.</p>
</div>
<div class="hero-stats" role="list">
<div class="stat" role="listitem"><strong>14</strong><span>events this month</span></div>
<div class="stat" role="listitem"><strong>6</strong><span>this week</span></div>
<div class="stat" role="listitem"><strong id="goingCount">0</strong><span>you’re going to</span></div>
</div>
</section>
<div class="toolbar">
<div class="filters" role="group" aria-label="Filter by event type">
<button class="chip is-on" type="button" data-filter="all">All</button>
<button class="chip" type="button" data-filter="workshop"><span class="dot dot-workshop"></span>Workshops</button>
<button class="chip" type="button" data-filter="networking"><span class="dot dot-networking"></span>Networking</button>
<button class="chip" type="button" data-filter="talk"><span class="dot dot-talk"></span>Talks</button>
<button class="chip" type="button" data-filter="social"><span class="dot dot-social"></span>Social</button>
</div>
<div class="view-toggle" role="tablist" aria-label="Calendar view">
<button class="toggle-btn is-on" type="button" role="tab" aria-selected="true" data-view="list">List</button>
<button class="toggle-btn" type="button" role="tab" aria-selected="false" data-view="calendar">Calendar</button>
</div>
</div>
<section class="view view-list" id="listView" aria-label="Upcoming events list">
<div class="event-list" id="eventList"><!-- events injected by script --></div>
<p class="empty" id="listEmpty" hidden>No events match this filter. Try another type.</p>
</section>
<section class="view view-calendar" id="calendarView" hidden aria-label="Month calendar">
<div class="cal-head">
<h2>June 2026</h2>
<div class="legend">
<span><span class="dot dot-workshop"></span>Workshop</span>
<span><span class="dot dot-networking"></span>Networking</span>
<span><span class="dot dot-talk"></span>Talk</span>
<span><span class="dot dot-social"></span>Social</span>
</div>
</div>
<div class="cal-grid-head" aria-hidden="true">
<span>Mon</span><span>Tue</span><span>Wed</span><span>Thu</span><span>Fri</span><span>Sat</span><span>Sun</span>
</div>
<div class="cal-grid" id="calGrid"><!-- days injected by script --></div>
</section>
</main>
<footer class="foot">
<span>Loomhouse Community Team · events@loomhouse.studio</span>
<span>Members get priority RSVP 48h before public release.</span>
</footer>
</div>
<!-- Event detail modal -->
<div class="modal" id="modal" hidden>
<div class="modal-backdrop" data-close></div>
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<button class="modal-x" type="button" data-close aria-label="Close">×</button>
<div class="modal-photo" id="modalPhoto"></div>
<div class="modal-body">
<span class="badge" id="modalBadge">Workshop</span>
<h2 id="modalTitle">Event title</h2>
<div class="modal-meta" id="modalMeta"></div>
<p id="modalDesc"></p>
<div class="modal-host">
<span class="avatar" id="modalHostAv">--</span>
<div><strong id="modalHost">Host</strong><span id="modalRoom"></span></div>
</div>
<div class="modal-foot">
<span class="attend" id="modalAttend"></span>
<button class="btn btn-primary" type="button" id="modalRsvp">RSVP</button>
</div>
</div>
</div>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Events Calendar
The community programme for the fictional Loomhouse coworking space at the Riverside Mill, presented as a single events page. The hero summarises the month at a glance — total events, how many fall this week, and a live count of the ones you have RSVP’d to. Below it, a pill filter bar narrows the programme to workshops, networking, talks or socials, each keyed to its own accent colour.
The body switches between two views. The list stacks rich event cards, each with an amber date block, a type badge, the room and host, and an attendance meter that turns red as a session nears capacity. The calendar view lays the same events onto a colour-coded June grid with today highlighted, and respects the active filter. Clicking any card or calendar chip opens a detail dialog with the full description and host, and an RSVP button. RSVPs work everywhere — list, modal and waitlist for full events — firing a small toast() and updating the attendance counts and your going-to tally in real time.
Everything is vanilla JavaScript with no dependencies. Controls are keyboard-usable, the dialog closes on Escape or backdrop click, and the warm-concrete-and-amber design system holds up from desktop down to a 360px phone.
Illustrative UI only — fictional coworking space, not a real booking system.