Coworking — Meeting Room Booking
A warm industrial coworking meeting-room booking screen. Browse room cards showing capacity, AV gear, hourly price and live availability, switch between days, then drag across a 30-minute timeline to pick a slot with built-in conflict checking. Choose a duration, add attendees against the room seat limit, watch a running price and credits meter update, and confirm to lock the slot. Pure HTML, CSS and vanilla JavaScript with toast feedback.
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;
--occupied: #d4503e;
--free: #2f9e6f;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow: 0 1px 2px rgba(28, 27, 25, 0.05), 0 8px 24px rgba(28, 27, 25, 0.06);
--shadow-lg: 0 12px 40px rgba(28, 27, 25, 0.14);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
}
body {
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;
}
.visually-hidden {
position: absolute !important;
width: 1px; height: 1px;
overflow: hidden; clip: rect(0 0 0 0);
white-space: nowrap;
}
.app {
max-width: 1180px;
margin: 0 auto;
padding: 22px 22px 48px;
}
/* Topbar */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 18px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand__mark {
width: 42px; height: 42px;
display: grid; place-items: center;
background: var(--char);
color: var(--amber);
border-radius: var(--r-md);
font-size: 22px;
}
.brand__txt { display: flex; flex-direction: column; line-height: 1.25; }
.brand__txt strong { font-size: 15px; font-weight: 800; letter-spacing: -0.01em; }
.brand__txt span { font-size: 12px; color: var(--muted); }
.topbar__right { display: flex; align-items: center; gap: 14px; }
.credits {
display: flex; align-items: center; gap: 8px;
background: var(--amber-50);
border: 1px solid rgba(232, 144, 43, 0.3);
color: var(--amber-d);
padding: 7px 12px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
}
.credits__ico { font-size: 14px; }
.credits__val b { color: var(--char); font-weight: 800; }
.avatar {
width: 40px; height: 40px;
border-radius: 50%;
display: grid; place-items: center;
background: var(--plant);
color: #fff;
font-weight: 700;
font-size: 14px;
border: 2px solid #fff;
box-shadow: 0 0 0 1px var(--line);
}
/* Layout */
.layout {
display: grid;
grid-template-columns: 1fr 400px;
gap: 22px;
margin-top: 22px;
}
.section-head h1 {
margin: 0;
font-size: 24px;
font-weight: 800;
letter-spacing: -0.02em;
color: var(--char);
}
.section-head p { margin: 4px 0 0; color: var(--muted); font-size: 14px; }
/* Room cards */
.room-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 14px;
margin-top: 18px;
}
.room {
position: relative;
text-align: left;
background: var(--surface);
border: 1.5px solid var(--line);
border-radius: var(--r-md);
padding: 0;
cursor: pointer;
overflow: hidden;
transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
font-family: inherit;
color: inherit;
}
.room:hover { transform: translateY(-3px); box-shadow: var(--shadow-lg); }
.room:focus-visible { outline: 3px solid rgba(232, 144, 43, 0.5); outline-offset: 2px; }
.room.is-sel {
border-color: var(--amber);
box-shadow: 0 0 0 3px rgba(232, 144, 43, 0.18), var(--shadow);
}
.room__photo {
height: 88px;
position: relative;
}
.room__tag {
position: absolute;
top: 8px; left: 8px;
background: rgba(28, 27, 25, 0.72);
color: #fff;
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: 999px;
backdrop-filter: blur(2px);
}
.room__sel-mark {
position: absolute;
top: 8px; right: 8px;
width: 22px; height: 22px;
border-radius: 50%;
background: var(--amber);
color: #fff;
display: none;
place-items: center;
font-size: 13px;
font-weight: 800;
}
.room.is-sel .room__sel-mark { display: grid; }
.room__body { padding: 12px 13px 13px; }
.room__name { margin: 0; font-size: 15px; font-weight: 700; color: var(--char); letter-spacing: -0.01em; }
.room__meta {
display: flex; flex-wrap: wrap; gap: 6px;
margin: 9px 0 0;
}
.chip {
font-size: 11px;
font-weight: 600;
color: var(--ink-2);
background: var(--concrete);
border: 1px solid var(--line);
padding: 3px 8px;
border-radius: 999px;
}
.room__foot {
display: flex; align-items: baseline; justify-content: space-between;
margin-top: 12px;
}
.room__price b { font-size: 17px; font-weight: 800; color: var(--char); }
.room__price span { font-size: 12px; color: var(--muted); }
.room__avail { font-size: 11px; font-weight: 600; }
.room__avail.busy { color: var(--danger); }
.room__avail.open { color: var(--ok); }
/* Booking panel */
.booking {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
padding: 18px 18px 20px;
align-self: start;
position: sticky;
top: 18px;
}
.day-head {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 12px;
}
.day-head h2 { margin: 0; font-size: 18px; font-weight: 800; color: var(--char); letter-spacing: -0.01em; }
.room-meta-line { margin: 3px 0 0; font-size: 12px; color: var(--muted); }
.day-pills { display: flex; gap: 6px; }
.day-pill {
border: 1px solid var(--line-2);
background: var(--surface);
border-radius: var(--r-sm);
padding: 5px 9px;
font-size: 12px;
font-weight: 600;
color: var(--ink-2);
cursor: pointer;
font-family: inherit;
line-height: 1.1;
text-align: center;
transition: all 0.14s ease;
}
.day-pill small { display: block; font-size: 10px; color: var(--muted); font-weight: 500; }
.day-pill.is-on { background: var(--char); border-color: var(--char); color: #fff; }
.day-pill.is-on small { color: rgba(255, 255, 255, 0.7); }
/* Timeline */
.timeline-wrap { margin-top: 16px; }
.legend {
display: flex; gap: 14px;
font-size: 11px; color: var(--muted);
margin-bottom: 8px;
}
.legend .dot { width: 9px; height: 9px; border-radius: 3px; display: inline-block; margin-right: 4px; vertical-align: middle; }
.dot--free { background: rgba(47, 158, 111, 0.4); border: 1px solid var(--ok); }
.dot--booked { background: var(--occupied); }
.dot--sel { background: var(--amber); }
.timeline {
display: grid;
grid-template-columns: repeat(20, 1fr);
gap: 3px;
border: 1px solid var(--line);
background: var(--concrete);
padding: 8px;
border-radius: var(--r-md);
user-select: none;
touch-action: none;
}
.timeline:focus-visible { outline: 3px solid rgba(232, 144, 43, 0.5); outline-offset: 2px; }
.slot {
position: relative;
height: 46px;
border-radius: 6px;
background: rgba(47, 158, 111, 0.14);
border: 1px solid rgba(47, 158, 111, 0.3);
cursor: pointer;
transition: background 0.1s ease, transform 0.08s ease;
}
.slot::after {
content: attr(data-time);
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%) rotate(-90deg);
font-size: 9px;
font-weight: 600;
color: var(--ink-2);
white-space: nowrap;
pointer-events: none;
opacity: 0.55;
}
.slot:hover { background: rgba(47, 158, 111, 0.26); }
.slot.booked {
background: var(--occupied);
border-color: var(--amber-d);
cursor: not-allowed;
background-image: repeating-linear-gradient(45deg, rgba(0,0,0,0.08) 0 4px, transparent 4px 8px);
}
.slot.booked::after { color: #fff; opacity: 0.8; }
.slot.sel {
background: var(--amber);
border-color: var(--amber-d);
transform: scale(1.04);
z-index: 1;
}
.slot.sel::after { color: #fff; opacity: 0.95; }
/* Form */
.form { margin-top: 18px; }
.field { margin-bottom: 16px; }
.field > label {
display: block;
font-size: 12px;
font-weight: 700;
color: var(--ink-2);
margin-bottom: 7px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.dur { display: flex; gap: 7px; }
.dur__btn {
flex: 1;
border: 1px solid var(--line-2);
background: var(--surface);
border-radius: var(--r-sm);
padding: 9px 0;
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
cursor: pointer;
font-family: inherit;
transition: all 0.14s ease;
}
.dur__btn:hover { border-color: var(--amber); color: var(--amber-d); }
.dur__btn.is-on { background: var(--amber-50); border-color: var(--amber); color: var(--amber-d); }
.att__list { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.att__list:empty { display: none; }
.att-pill {
display: inline-flex; align-items: center; gap: 6px;
background: var(--concrete);
border: 1px solid var(--line);
border-radius: 999px;
padding: 4px 6px 4px 4px;
font-size: 12px;
font-weight: 600;
color: var(--ink);
}
.att-pill__av {
width: 20px; height: 20px;
border-radius: 50%;
display: grid; place-items: center;
background: var(--plant);
color: #fff;
font-size: 10px;
font-weight: 700;
}
.att-pill__x {
border: none; background: none; cursor: pointer;
color: var(--muted); font-size: 15px; line-height: 1;
padding: 0 2px;
}
.att-pill__x:hover { color: var(--danger); }
.att__add { display: flex; gap: 7px; }
.att__add input {
flex: 1;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 9px 11px;
font-size: 13px;
font-family: inherit;
background: var(--surface);
color: var(--ink);
}
.att__add input:focus { outline: none; border-color: var(--amber); box-shadow: 0 0 0 3px rgba(232,144,43,0.15); }
.att__count { display: block; margin-top: 7px; font-size: 11px; color: var(--muted); }
.att__count.over { color: var(--danger); font-weight: 600; }
.summary {
background: var(--concrete);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 13px 14px;
margin-bottom: 14px;
}
.summary__row {
display: flex; justify-content: space-between; align-items: center;
font-size: 13px;
padding: 4px 0;
}
.summary__row span { color: var(--muted); }
.summary__row b { color: var(--ink); font-weight: 600; text-align: right; }
.summary__row--total {
border-top: 1px dashed var(--line-2);
margin-top: 5px; padding-top: 9px;
}
.summary__row--total span { color: var(--ink); font-weight: 700; }
.summary__row--total b { font-size: 18px; font-weight: 800; color: var(--char); }
.btn {
font-family: inherit;
border: none;
cursor: pointer;
font-weight: 700;
border-radius: var(--r-md);
transition: all 0.14s ease;
}
.btn--amber {
width: 100%;
background: var(--amber);
color: #fff;
padding: 13px;
font-size: 14px;
box-shadow: 0 4px 14px rgba(232, 144, 43, 0.3);
}
.btn--amber:hover:not(:disabled) { background: var(--amber-d); transform: translateY(-1px); }
.btn--amber:active:not(:disabled) { transform: translateY(0); }
.btn--amber:disabled {
background: var(--concrete-d);
color: var(--muted);
box-shadow: none;
cursor: not-allowed;
}
.btn--ghost {
background: var(--surface);
border: 1px solid var(--line-2);
color: var(--ink-2);
padding: 9px 13px;
font-size: 13px;
font-weight: 600;
}
.btn--ghost:hover { border-color: var(--amber); color: var(--amber-d); }
/* Toast */
.toast {
position: fixed;
left: 50%; bottom: 26px;
transform: translateX(-50%) translateY(24px);
background: var(--char);
color: #fff;
padding: 13px 20px;
border-radius: var(--r-md);
font-size: 14px;
font-weight: 600;
box-shadow: var(--shadow-lg);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
z-index: 50;
max-width: 90vw;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast--ok { border-left: 4px solid var(--ok); }
.toast--warn { border-left: 4px solid var(--warn); }
/* Responsive */
@media (max-width: 920px) {
.layout { grid-template-columns: 1fr; }
.booking { position: static; }
}
@media (max-width: 520px) {
.app { padding: 14px 13px 40px; }
.topbar { flex-wrap: wrap; gap: 10px; }
.section-head h1 { font-size: 21px; }
.room-grid { grid-template-columns: 1fr 1fr; gap: 11px; }
.day-head { flex-direction: column; }
.timeline { grid-template-columns: repeat(20, 1fr); overflow-x: auto; }
.slot { height: 40px; }
.slot::after { font-size: 8px; }
}
@media (max-width: 380px) {
.room-grid { grid-template-columns: 1fr; }
}(function () {
"use strict";
// ---- Data ------------------------------------------------------------
// Timeline runs 08:00 -> 18:00, in 30-min slots (20 slots).
var START_HOUR = 8;
var SLOTS = 20; // 10 hours * 2
var GRADS = {
atrium: "linear-gradient(135deg,#e8902b,#cc7918)",
foundry: "linear-gradient(135deg,#5f7a52,#41553a)",
press: "linear-gradient(135deg,#4a463e,#1c1b19)",
clay: "linear-gradient(135deg,#d98a2b,#b06a1f)",
loft: "linear-gradient(135deg,#7b766c,#4a463e)"
};
var ROOMS = [
{ id: "atrium", name: "The Atrium", seats: 8, price: 18, tag: "Bright · top floor",
meta: ["Seats 8", "4K display", "Whiteboard"], booked: { 0: [4,5,6,7, 14,15], 1: [0,1,2], 2: [8,9,10,11] } },
{ id: "foundry", name: "Foundry Room", seats: 4, price: 12, tag: "Quiet · plant wall",
meta: ["Seats 4", "TV + cam", "Soundproof"], booked: { 0: [0,1, 10,11,12], 1: [6,7,8,9,10,11], 2: [2,3] } },
{ id: "press", name: "The Press", seats: 14, price: 32, tag: "Boardroom",
meta: ["Seats 14", "Dual screen", "Catering"], booked: { 0: [2,3,4,5,6,7,8], 1: [14,15,16,17], 2: [] } },
{ id: "clay", name: "Clay Studio", seats: 6, price: 15, tag: "Workshop · L-shape",
meta: ["Seats 6", "Projector", "Movable desks"], booked: { 0: [16,17,18,19], 1: [0,1,2,3,4], 2: [12,13,14,15] } },
{ id: "loft", name: "Loft Nook", seats: 2, price: 8, tag: "Focus pod",
meta: ["Seats 2", "Mic + cam", "Standing"], booked: { 0: [], 1: [10,11], 2: [4,5,6,7,8,9] } }
];
var DAYS = (function () {
var base = new Date();
var out = [];
var names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
for (var i = 0; i < 3; i++) {
var d = new Date(base.getTime() + i * 86400000);
out.push({ key: i, label: i === 0 ? "Today" : names[d.getDay()], num: d.getDate() });
}
return out;
})();
// ---- State -----------------------------------------------------------
var state = {
roomId: "atrium",
day: 0,
durH: 0.5, // duration in hours
selStart: null, // slot index
attendees: ["Mara Velez", "Theo Park"]
};
// ---- Helpers ---------------------------------------------------------
function $(sel) { return document.querySelector(sel); }
function room() { return ROOMS.filter(function (r) { return r.id === state.roomId; })[0]; }
function durSlots() { return Math.round(state.durH * 2); }
function slotTime(i) {
var h = START_HOUR + Math.floor(i / 2);
var m = i % 2 === 0 ? "00" : "30";
return (h < 10 ? "0" + h : h) + ":" + m;
}
function rangeLabel(start, len) {
var endIdx = start + len;
return slotTime(start) + "–" + slotTime(endIdx);
}
function initials(name) {
var p = name.trim().split(/\s+/);
return ((p[0] || "")[0] + (p[1] ? p[1][0] : "")).toUpperCase();
}
function bookedSet() {
var r = room();
var arr = (r.booked && r.booked[state.day]) || [];
var s = {};
arr.forEach(function (i) { s[i] = true; });
return s;
}
var toastEl = $("#toast");
var toastT;
function toast(msg, kind) {
toastEl.textContent = msg;
toastEl.className = "toast show" + (kind ? " toast--" + kind : "");
clearTimeout(toastT);
toastT = setTimeout(function () { toastEl.className = "toast"; }, 2600);
}
// ---- Render: rooms ---------------------------------------------------
function freeCount(r, day) {
var arr = (r.booked && r.booked[day]) || [];
return SLOTS - arr.length;
}
function renderRooms() {
var grid = $("#roomGrid");
grid.innerHTML = "";
ROOMS.forEach(function (r) {
var free = freeCount(r, state.day);
var busy = free < SLOTS * 0.4;
var btn = document.createElement("button");
btn.type = "button";
btn.className = "room" + (r.id === state.roomId ? " is-sel" : "");
btn.setAttribute("role", "radio");
btn.setAttribute("aria-checked", r.id === state.roomId ? "true" : "false");
btn.innerHTML =
'<div class="room__photo" style="background:' + GRADS[r.id] + '">' +
'<span class="room__tag">' + r.tag + '</span>' +
'<span class="room__sel-mark" aria-hidden="true">✓</span>' +
'</div>' +
'<div class="room__body">' +
'<p class="room__name">' + r.name + '</p>' +
'<div class="room__meta">' + r.meta.map(function (m) { return '<span class="chip">' + m + '</span>'; }).join("") + '</div>' +
'<div class="room__foot">' +
'<span class="room__price"><b>$' + r.price + '</b> <span>/ hr</span></span>' +
'<span class="room__avail ' + (busy ? "busy" : "open") + '">' + (busy ? "Limited" : "Open") + '</span>' +
'</div>' +
'</div>';
btn.addEventListener("click", function () { selectRoom(r.id); });
grid.appendChild(btn);
});
}
function selectRoom(id) {
state.roomId = id;
state.selStart = null;
var r = room();
$("#roomTitle").textContent = r.name;
$("#roomMeta").textContent = r.meta.join(" · ");
renderRooms();
renderTimeline();
renderAttendees();
updateSummary();
}
// ---- Render: days ----------------------------------------------------
function renderDays() {
var wrap = $("#dayPills");
wrap.innerHTML = "";
DAYS.forEach(function (d) {
var b = document.createElement("button");
b.type = "button";
b.className = "day-pill" + (d.key === state.day ? " is-on" : "");
b.setAttribute("role", "tab");
b.setAttribute("aria-selected", d.key === state.day ? "true" : "false");
b.innerHTML = d.label + "<small>" + d.num + "</small>";
b.addEventListener("click", function () {
state.day = d.key;
state.selStart = null;
renderDays();
renderRooms();
renderTimeline();
updateSummary();
});
wrap.appendChild(b);
});
}
// ---- Render: timeline ------------------------------------------------
function conflict(start, len) {
if (start == null) return true;
if (start + len > SLOTS) return true;
var bk = bookedSet();
for (var i = start; i < start + len; i++) {
if (bk[i]) return true;
}
return false;
}
function renderTimeline() {
var tl = $("#timeline");
tl.innerHTML = "";
var bk = bookedSet();
var len = durSlots();
for (var i = 0; i < SLOTS; i++) {
var s = document.createElement("div");
s.className = "slot" + (bk[i] ? " booked" : "");
s.setAttribute("data-i", i);
s.setAttribute("data-time", slotTime(i));
s.setAttribute("role", "gridcell");
if (bk[i]) s.setAttribute("aria-label", slotTime(i) + " booked");
else s.setAttribute("aria-label", slotTime(i) + " available");
tl.appendChild(s);
}
paintSelection();
bindTimeline();
}
function paintSelection() {
var slots = $("#timeline").querySelectorAll(".slot");
var len = durSlots();
slots.forEach(function (el) { el.classList.remove("sel"); });
if (state.selStart == null) return;
if (conflict(state.selStart, len)) return;
for (var i = state.selStart; i < state.selStart + len; i++) {
if (slots[i]) slots[i].classList.add("sel");
}
}
function trySelect(start) {
var len = durSlots();
var bk = bookedSet();
if (bk[start]) { toast("That slot is already booked.", "warn"); return; }
if (start + len > SLOTS) {
// shift back so the range fits within the day
start = SLOTS - len;
}
if (conflict(start, len)) {
toast("Not enough free time there — pick another slot.", "warn");
return;
}
state.selStart = start;
paintSelection();
updateSummary();
}
var dragging = false;
function bindTimeline() {
var tl = $("#timeline");
var slots = tl.querySelectorAll(".slot");
slots.forEach(function (el) {
el.addEventListener("mousedown", function () {
dragging = true;
trySelect(parseInt(el.getAttribute("data-i"), 10));
});
el.addEventListener("mouseenter", function () {
if (dragging) trySelect(parseInt(el.getAttribute("data-i"), 10));
});
// touch
el.addEventListener("touchstart", function (e) {
e.preventDefault();
trySelect(parseInt(el.getAttribute("data-i"), 10));
}, { passive: false });
});
tl.addEventListener("touchmove", function (e) {
var t = e.touches[0];
var target = document.elementFromPoint(t.clientX, t.clientY);
if (target && target.classList.contains("slot")) {
trySelect(parseInt(target.getAttribute("data-i"), 10));
}
}, { passive: true });
}
document.addEventListener("mouseup", function () { dragging = false; });
// keyboard on timeline
$("#timeline").addEventListener("keydown", function (e) {
var len = durSlots();
if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
e.preventDefault();
var cur = state.selStart == null ? 0 : state.selStart;
var next = e.key === "ArrowRight" ? cur + 1 : cur - 1;
next = Math.max(0, Math.min(SLOTS - len, next));
// walk to next non-conflicting start
var guard = 0;
while (conflict(next, len) && guard < SLOTS) {
next += e.key === "ArrowRight" ? 1 : -1;
next = Math.max(0, Math.min(SLOTS - len, next));
guard++;
}
if (!conflict(next, len)) { state.selStart = next; paintSelection(); updateSummary(); }
}
});
// ---- Duration --------------------------------------------------------
function bindDuration() {
var btns = document.querySelectorAll(".dur__btn");
btns.forEach(function (b) {
b.addEventListener("click", function () {
btns.forEach(function (x) { x.classList.remove("is-on"); });
b.classList.add("is-on");
state.durH = parseFloat(b.getAttribute("data-h"));
$("#durSel").value = b.getAttribute("data-h");
// re-validate current selection
if (state.selStart != null && conflict(state.selStart, durSlots())) {
state.selStart = null;
toast("Selection no longer fits — choose a new slot.", "warn");
}
paintSelection();
updateSummary();
});
});
}
// ---- Attendees -------------------------------------------------------
function renderAttendees() {
var list = $("#attList");
list.innerHTML = "";
state.attendees.forEach(function (name, idx) {
var pill = document.createElement("span");
pill.className = "att-pill";
pill.innerHTML =
'<span class="att-pill__av">' + initials(name) + '</span>' +
'<span>' + name + '</span>' +
'<button type="button" class="att-pill__x" aria-label="Remove ' + name + '">×</button>';
pill.querySelector(".att-pill__x").addEventListener("click", function () {
state.attendees.splice(idx, 1);
renderAttendees();
updateSummary();
});
list.appendChild(pill);
});
var r = room();
var c = $("#attCount");
c.textContent = state.attendees.length + " attendee" + (state.attendees.length === 1 ? "" : "s") + " · room seats " + r.seats;
c.classList.toggle("over", state.attendees.length > r.seats);
}
function addAttendee() {
var input = $("#attInput");
var v = input.value.trim();
if (!v) return;
if (state.attendees.length >= room().seats) {
toast("Room only seats " + room().seats + ".", "warn");
return;
}
state.attendees.push(v);
input.value = "";
renderAttendees();
updateSummary();
}
function bindAttendees() {
$("#attAdd").addEventListener("click", addAttendee);
$("#attInput").addEventListener("keydown", function (e) {
if (e.key === "Enter") { e.preventDefault(); addAttendee(); }
});
}
// ---- Summary + confirm ----------------------------------------------
function durLabel() {
return state.durH < 1 ? (state.durH * 60) + "m"
: (state.durH % 1 === 0 ? state.durH + "h" : state.durH + "h");
}
function updateSummary() {
var r = room();
$("#sumRoom").textContent = r.name;
var len = durSlots();
var ok = state.selStart != null && !conflict(state.selStart, len);
$("#sumWhen").textContent = ok
? DAYS[state.day].label + " · " + rangeLabel(state.selStart, len)
: "Select a time";
$("#sumDur").textContent = durLabel();
var price = ok ? Math.round(r.price * state.durH) : 0;
$("#sumPrice").textContent = "$" + price;
var btn = $("#confirmBtn");
if (!ok) {
btn.disabled = true;
btn.textContent = "Select a time to continue";
} else if (state.attendees.length > r.seats) {
btn.disabled = true;
btn.textContent = "Too many attendees for this room";
} else {
btn.disabled = false;
btn.textContent = "Confirm booking · $" + price;
}
}
function confirm() {
var len = durSlots();
if (state.selStart == null || conflict(state.selStart, len)) {
toast("Pick an available time first.", "warn");
return;
}
var r = room();
// mark slots booked locally so they show as taken
var arr = (r.booked[state.day] = r.booked[state.day] || []);
for (var i = state.selStart; i < state.selStart + len; i++) arr.push(i);
// deduct credits
var left = parseFloat($("#creditsLeft").textContent) - state.durH;
if (left < 0) left = 0;
$("#creditsLeft").textContent = (left % 1 === 0 ? left : left.toFixed(1));
toast(r.name + " booked · " + DAYS[state.day].label + " " + rangeLabel(state.selStart, len), "ok");
state.selStart = null;
renderRooms();
renderTimeline();
updateSummary();
}
// ---- Init ------------------------------------------------------------
function init() {
renderDays();
renderRooms();
var r = room();
$("#roomTitle").textContent = r.name;
$("#roomMeta").textContent = r.meta.join(" · ");
renderTimeline();
renderAttendees();
bindDuration();
bindAttendees();
updateSummary();
$("#confirmBtn").addEventListener("click", confirm);
}
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>Coworking — Meeting Room Booking</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="app">
<header class="topbar">
<div class="brand">
<span class="brand__mark" aria-hidden="true">◳</span>
<div class="brand__txt">
<strong>Foundry & Co.</strong>
<span>Loft Coworking · Studio Floor</span>
</div>
</div>
<div class="topbar__right">
<div class="credits" title="Your monthly meeting-room credits">
<span class="credits__ico" aria-hidden="true">⏱</span>
<span class="credits__val"><b id="creditsLeft">14</b> / 20 hrs</span>
</div>
<div class="avatar" title="Mara Velez — Resident member" aria-label="Member: Mara Velez">MV</div>
</div>
</header>
<main class="layout">
<section class="rooms" aria-label="Meeting rooms">
<div class="section-head">
<h1>Book a meeting room</h1>
<p>Pick a room, drag across the timeline to choose your slot, then confirm.</p>
</div>
<div class="room-grid" id="roomGrid" role="radiogroup" aria-label="Choose a room"></div>
</section>
<section class="booking" aria-label="Booking panel">
<div class="day-head">
<div>
<h2 id="roomTitle">The Atrium</h2>
<p id="roomMeta" class="room-meta-line">Up to 8 · 4K display · Whiteboard</p>
</div>
<div class="day-pills" id="dayPills" role="tablist" aria-label="Choose day"></div>
</div>
<div class="timeline-wrap">
<div class="legend">
<span><i class="dot dot--free"></i> Available</span>
<span><i class="dot dot--booked"></i> Booked</span>
<span><i class="dot dot--sel"></i> Your selection</span>
</div>
<div class="timeline" id="timeline" role="grid" aria-label="Daily availability timeline. Drag to select a time range." tabindex="0"></div>
</div>
<div class="form">
<div class="field">
<label for="durSel">Duration</label>
<div class="dur" id="durPicker" role="group" aria-label="Duration">
<button type="button" class="dur__btn is-on" data-h="0.5">30m</button>
<button type="button" class="dur__btn" data-h="1">1h</button>
<button type="button" class="dur__btn" data-h="1.5">1.5h</button>
<button type="button" class="dur__btn" data-h="2">2h</button>
</div>
<select id="durSel" class="visually-hidden" aria-hidden="true" tabindex="-1">
<option value="0.5">30m</option><option value="1">1h</option>
<option value="1.5">1.5h</option><option value="2">2h</option>
</select>
</div>
<div class="field">
<label for="attInput">Attendees</label>
<div class="att">
<div class="att__list" id="attList"></div>
<div class="att__add">
<input id="attInput" type="text" placeholder="Add teammate & press Enter" autocomplete="off" />
<button type="button" id="attAdd" class="btn btn--ghost">Add</button>
</div>
<small id="attCount" class="att__count">2 attendees · room seats 8</small>
</div>
</div>
<div class="summary" id="summary" aria-live="polite">
<div class="summary__row"><span>Room</span><b id="sumRoom">The Atrium</b></div>
<div class="summary__row"><span>When</span><b id="sumWhen">Select a time</b></div>
<div class="summary__row"><span>Duration</span><b id="sumDur">30m</b></div>
<div class="summary__row summary__row--total"><span>Total</span><b id="sumPrice">$0</b></div>
</div>
<button type="button" id="confirmBtn" class="btn btn--amber" disabled>Select a time to continue</button>
</div>
</section>
</main>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Meeting Room Booking
A meeting-room reservation screen for a fictional loft coworking space, built in the warm-industrial Foundry & Co. style — concrete tones, an amber accent and a plant-green member badge. A grid of room cards leads the layout, each with a gradient photo block, capacity and AV chips, an hourly rate and a live Open / Limited availability flag that recalculates per day. Selecting a card highlights it and re-points the booking panel beside it.
The booking panel pairs a three-day pill switcher with a draggable 08:00–18:00 timeline split into 30-minute slots. Available slots glow green, booked slots show a hatched red fill, and pressing and dragging paints your selection in amber — sized by the active duration picker (30m to 2h). Selections are conflict-checked in real time, so you can never overlap an existing booking, and arrow keys offer a keyboard-friendly path to the next free range.
A running summary tracks room, time, duration and total price, while the attendees field adds removable member pills and warns when you exceed the room’s seat count. Confirming a booking deducts from the monthly credits meter, marks those slots as taken, and fires a toast — all state handled in plain vanilla JavaScript with no dependencies.
Illustrative UI only — fictional coworking space, not a real booking system.