Coworking — Member Dashboard
A warm industrial member dashboard for a fictional coworking space, built with plain HTML, CSS and vanilla JavaScript. It surfaces today's bookings, an animated credits-and-hours meter, quick-book tiles with live free or occupied status, upcoming community events with RSVP, member shoutouts you can cheer, and an access card with check-in and a decorative door QR. Every control works: cancel a booking and the hours refund, top up, switch tabs, and watch the ring fill.
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;
--sh-sm: 0 1px 2px rgba(28, 27, 25, 0.06);
--sh-md: 0 6px 20px rgba(28, 27, 25, 0.08);
--sh-lg: 0 18px 44px rgba(28, 27, 25, 0.12);
}
* { box-sizing: border-box; }
html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
background-image:
radial-gradient(900px 500px at 90% -10%, rgba(232, 144, 43, 0.07), transparent 60%),
radial-gradient(700px 460px at -5% 110%, rgba(95, 122, 82, 0.07), transparent 60%);
}
h1, h2 { margin: 0; letter-spacing: -0.01em; }
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.app {
display: grid;
grid-template-columns: 268px 1fr;
min-height: 100vh;
max-width: 1280px;
margin: 0 auto;
}
/* ---------- Sidebar ---------- */
.sidebar {
background: var(--char);
color: var(--concrete);
padding: 26px 20px;
display: flex;
flex-direction: column;
gap: 22px;
position: sticky;
top: 0;
align-self: start;
height: 100vh;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
width: 40px; height: 40px; display: grid; place-items: center;
background: var(--amber); color: var(--char); border-radius: 12px;
font-size: 22px; font-weight: 800;
}
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-text strong { font-size: 18px; font-weight: 800; color: #fff; }
.brand-text span { font-size: 12px; color: #b6afa3; letter-spacing: 0.14em; text-transform: uppercase; }
.nav { display: flex; flex-direction: column; gap: 4px; }
.nav-item {
display: flex; align-items: center; gap: 12px;
padding: 11px 13px; border-radius: var(--r-sm);
color: #c9c2b6; text-decoration: none; font-size: 14px; font-weight: 500;
transition: background 0.16s, color 0.16s;
}
.nav-item .ni-ico { width: 18px; text-align: center; opacity: 0.85; }
.nav-item:hover { background: rgba(255, 255, 255, 0.06); color: #fff; }
.nav-item.is-active { background: var(--amber); color: var(--char); font-weight: 700; }
.nav-item.is-active .ni-ico { opacity: 1; }
.access-card {
margin-top: auto;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--r-md);
padding: 16px;
display: flex; flex-direction: column; gap: 10px;
}
.access-head { display: flex; align-items: center; gap: 9px; }
.access-dot {
width: 10px; height: 10px; border-radius: 50%;
background: var(--free); box-shadow: 0 0 0 4px rgba(47, 158, 111, 0.22);
transition: background 0.2s, box-shadow 0.2s;
}
.access-card.is-out .access-dot { background: var(--muted); box-shadow: 0 0 0 4px rgba(123, 118, 108, 0.18); }
.access-label { font-size: 14px; font-weight: 700; color: #fff; }
.access-sub { margin: 0; font-size: 12.5px; color: #aaa498; }
.access-btn {
border: 1px solid rgba(255, 255, 255, 0.18); background: transparent;
color: #fff; padding: 8px 12px; border-radius: var(--r-sm);
font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
transition: background 0.16s, border-color 0.16s;
}
.access-btn:hover { background: rgba(255, 255, 255, 0.1); }
.access-card.is-out .access-btn { background: var(--amber); border-color: var(--amber); color: var(--char); }
.qr { display: flex; align-items: center; gap: 10px; margin-top: 2px; }
.qr span { font-size: 11px; color: #8d8779; letter-spacing: 0.08em; text-transform: uppercase; }
.qr-grid {
width: 44px; height: 44px; display: grid;
grid-template-columns: repeat(7, 1fr); grid-template-rows: repeat(7, 1fr);
gap: 1px; background: #fff; padding: 3px; border-radius: 6px;
}
.qr-grid i { background: var(--char); border-radius: 0.5px; }
.qr-grid i.off { background: transparent; }
/* ---------- Main ---------- */
.main { padding: 26px 30px 40px; min-width: 0; }
.topbar {
display: flex; align-items: flex-end; justify-content: space-between;
gap: 16px; margin-bottom: 24px; flex-wrap: wrap;
}
.greeting h1 { font-size: 27px; font-weight: 800; color: var(--char); }
.greeting p { margin: 4px 0 0; color: var(--muted); font-size: 14px; }
.top-actions { display: flex; align-items: center; gap: 12px; }
.ghost-btn {
background: var(--amber); color: var(--char); border: none;
padding: 11px 18px; border-radius: var(--r-sm); font: inherit;
font-weight: 700; font-size: 14px; cursor: pointer;
box-shadow: var(--sh-sm); transition: transform 0.12s, background 0.16s, box-shadow 0.16s;
}
.ghost-btn:hover { background: var(--amber-d); box-shadow: var(--sh-md); }
.ghost-btn:active { transform: translateY(1px); }
.avatar {
width: 42px; height: 42px; border-radius: 50%;
background: linear-gradient(135deg, var(--plant), #45603a);
color: #fff; display: grid; place-items: center;
font-weight: 700; font-size: 14px; box-shadow: var(--sh-sm);
}
.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);
padding: 20px;
box-shadow: var(--sh-sm);
}
.card-head {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; margin-bottom: 16px;
}
.card-head h2 { font-size: 16px; font-weight: 700; color: var(--char); }
.pill {
font-size: 12px; font-weight: 600; color: var(--ink-2);
background: var(--concrete); padding: 4px 10px; border-radius: 999px;
border: 1px solid var(--line); white-space: nowrap;
}
/* ---------- Credits ---------- */
.credits { display: flex; flex-direction: column; align-items: center; text-align: center; }
.credits .card-head { width: 100%; }
.meter { position: relative; width: 168px; height: 168px; }
.ring { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring-bg { fill: none; stroke: var(--concrete-d); stroke-width: 13; }
.ring-fg {
fill: none; stroke: var(--amber); stroke-width: 13; stroke-linecap: round;
stroke-dasharray: 326.7; stroke-dashoffset: 326.7;
transition: stroke-dashoffset 1.1s cubic-bezier(0.22, 1, 0.36, 1);
}
.meter-center {
position: absolute; inset: 0; display: flex; flex-direction: column;
align-items: center; justify-content: center; line-height: 1.05;
}
.meter-center strong { font-size: 38px; font-weight: 800; color: var(--char); }
.meter-center span { font-size: 12.5px; color: var(--muted); }
.credit-legend { display: flex; gap: 18px; margin: 14px 0 4px; font-size: 13px; color: var(--ink-2); }
.credit-legend span { display: inline-flex; align-items: center; gap: 6px; }
.dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.dot-amber { background: var(--amber); }
.dot-line { background: var(--concrete-d); }
.link-btn {
background: none; border: none; color: var(--amber-d); font: inherit;
font-weight: 600; font-size: 13.5px; cursor: pointer; padding: 6px;
text-decoration: underline; text-underline-offset: 3px;
}
.link-btn:hover { color: var(--amber); }
/* ---------- Bookings ---------- */
.booking-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.booking {
display: grid; grid-template-columns: 56px 1fr auto;
align-items: center; gap: 14px;
padding: 12px; border: 1px solid var(--line); border-radius: var(--r-md);
background: var(--bg);
transition: border-color 0.18s, box-shadow 0.18s, opacity 0.3s, transform 0.3s;
}
.booking:hover { border-color: var(--line-2); box-shadow: var(--sh-sm); }
.booking.removing { opacity: 0; transform: translateX(14px); }
.bk-time {
display: flex; flex-direction: column; align-items: center;
border-right: 1px solid var(--line); padding-right: 12px;
}
.bk-time b { font-size: 15px; color: var(--char); font-weight: 700; }
.bk-time small { font-size: 11px; color: var(--muted); }
.bk-info b { display: block; font-size: 14.5px; color: var(--ink); }
.bk-info span { font-size: 12.5px; color: var(--muted); }
.bk-tag {
display: inline-block; margin-top: 3px; font-size: 11px; font-weight: 600;
padding: 2px 8px; border-radius: 999px;
}
.tag-desk { background: var(--amber-50); color: var(--amber-d); }
.tag-room { background: rgba(95, 122, 82, 0.14); color: var(--plant); }
.tag-booth { background: rgba(28, 27, 25, 0.07); color: var(--ink-2); }
.cancel-btn {
background: var(--surface); border: 1px solid var(--line-2);
color: var(--ink-2); padding: 7px 12px; border-radius: var(--r-sm);
font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.cancel-btn:hover { background: var(--danger); border-color: var(--danger); color: #fff; }
.empty { color: var(--muted); font-size: 13.5px; padding: 8px 4px; margin: 0; }
/* ---------- Quick book ---------- */
.quick-sub { margin: -6px 0 14px; color: var(--muted); font-size: 13.5px; }
.quick-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.quick-tile {
text-align: left; border: 1px solid var(--line); border-radius: var(--r-md);
background: var(--bg); padding: 13px 14px; cursor: pointer; font: inherit;
display: flex; flex-direction: column; gap: 7px;
transition: border-color 0.16s, box-shadow 0.16s, transform 0.12s;
}
.quick-tile:hover { border-color: var(--amber); box-shadow: var(--sh-sm); }
.quick-tile:active { transform: translateY(1px); }
.quick-tile:disabled { cursor: not-allowed; opacity: 0.55; }
.quick-tile:disabled:hover { border-color: var(--line); box-shadow: none; }
.qt-top { display: flex; align-items: center; justify-content: space-between; }
.qt-name { font-size: 14px; font-weight: 700; color: var(--ink); }
.qt-status { font-size: 11px; font-weight: 700; display: inline-flex; align-items: center; gap: 5px; }
.qt-status::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--free); }
.qt-status.busy { color: var(--occupied); }
.qt-status.busy::before { background: var(--occupied); }
.qt-status.free { color: var(--free); }
.qt-meta { font-size: 12px; color: var(--muted); }
.qt-cost { font-size: 12px; font-weight: 600; color: var(--amber-d); }
/* ---------- Feed / tabs ---------- */
.tabs-head { margin-bottom: 16px; }
.tabs { display: inline-flex; gap: 4px; background: var(--concrete); padding: 4px; border-radius: 999px; }
.tab {
border: none; background: transparent; color: var(--ink-2);
font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
padding: 7px 14px; border-radius: 999px; transition: background 0.16s, color 0.16s;
}
.tab:hover { color: var(--char); }
.tab.is-active { background: var(--surface); color: var(--char); box-shadow: var(--sh-sm); }
.tab-panel { display: none; }
.tab-panel.is-active { display: block; }
.event-list, .shout-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.event {
display: grid; grid-template-columns: 52px 1fr auto; gap: 13px; align-items: center;
padding: 11px; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--bg);
}
.ev-date {
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: var(--char); color: #fff; border-radius: var(--r-sm); padding: 6px 0;
}
.ev-date b { font-size: 17px; font-weight: 800; line-height: 1; }
.ev-date small { font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: #cdc6ba; }
.ev-info b { display: block; font-size: 14px; color: var(--ink); }
.ev-info span { font-size: 12.5px; color: var(--muted); }
.rsvp-btn {
border: 1px solid var(--plant); background: transparent; color: var(--plant);
padding: 7px 13px; border-radius: var(--r-sm); font: inherit; font-size: 12.5px;
font-weight: 700; cursor: pointer; transition: background 0.15s, color 0.15s; white-space: nowrap;
}
.rsvp-btn:hover { background: var(--plant); color: #fff; }
.rsvp-btn.is-going { background: var(--plant); color: #fff; }
.shout {
display: grid; grid-template-columns: 40px 1fr; gap: 12px; align-items: start;
padding: 11px; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--bg);
}
.sh-av {
width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
color: #fff; font-weight: 700; font-size: 13px;
}
.sh-body b { font-size: 13.5px; color: var(--ink); }
.sh-body p { margin: 3px 0 6px; font-size: 13px; color: var(--ink-2); }
.sh-foot { display: flex; align-items: center; gap: 14px; font-size: 12px; color: var(--muted); }
.cheer-btn {
background: none; border: none; font: inherit; font-size: 12px; font-weight: 600;
color: var(--muted); cursor: pointer; display: inline-flex; align-items: center; gap: 5px;
transition: color 0.15s;
}
.cheer-btn:hover, .cheer-btn.cheered { color: var(--amber-d); }
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%);
display: flex; flex-direction: column; gap: 8px; z-index: 50; align-items: center;
}
.toast {
background: var(--char); color: #fff; padding: 11px 18px; border-radius: 999px;
font-size: 13.5px; font-weight: 600; box-shadow: var(--sh-lg);
display: flex; align-items: center; gap: 9px;
animation: toast-in 0.25s cubic-bezier(0.22, 1, 0.36, 1);
}
.toast.out { animation: toast-out 0.3s forwards; }
.toast .t-ico { color: var(--amber); }
@keyframes toast-in { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
@keyframes toast-out { to { opacity: 0; transform: translateY(12px); } }
button:focus-visible, a:focus-visible {
outline: 3px solid var(--amber); outline-offset: 2px; border-radius: var(--r-sm);
}
/* ---------- Responsive ---------- */
@media (max-width: 900px) {
.app { grid-template-columns: 1fr; }
.sidebar {
position: static; height: auto; flex-direction: row; flex-wrap: wrap;
align-items: center; gap: 16px;
}
.nav { flex-direction: row; flex-wrap: wrap; flex: 1; }
.access-card { margin: 0; width: 100%; flex-direction: row; flex-wrap: wrap; align-items: center; }
.access-card .qr { margin-left: auto; }
}
@media (max-width: 740px) {
.grid { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.main { padding: 18px 16px 32px; }
.greeting h1 { font-size: 22px; }
.quick-grid { grid-template-columns: 1fr; }
.nav-item span.ni-ico { display: none; }
.booking { grid-template-columns: 48px 1fr; }
.booking .cancel-btn { grid-column: 2; justify-self: start; }
.topbar { align-items: flex-start; }
}(function () {
"use strict";
/* ---------- State ---------- */
var TOTAL_HOURS = 40;
var usedHours = 16;
var checkedIn = true;
var bookings = [
{ id: "b1", from: "09:30", to: "11:00", title: "Standing Desk 14", where: "North Loft · Floor 2", type: "desk" },
{ id: "b2", from: "13:00", to: "14:00", title: "Spindle Meeting Room", where: "Glass wing · seats 6", type: "room" },
{ id: "b3", from: "16:30", to: "17:00", title: "Focus Booth B", where: "Quiet corridor", type: "booth" }
];
var quickSpaces = [
{ name: "Hot Desk 02", area: "Open floor", cost: "1 hr", free: true },
{ name: "Phone Booth A", area: "Calls · 30 min", cost: "0.5 hr", free: true },
{ name: "Spindle Room", area: "Seats 6", cost: "2 hr", free: false },
{ name: "Maker Bench", area: "Workshop", cost: "1 hr", free: true }
];
var events = [
{ d: "19", m: "Jun", title: "Lunch & Learn: Type design", meta: "12:30 · The Commons", going: false },
{ d: "22", m: "Jun", title: "Members' rooftop social", meta: "18:00 · Sky Terrace", going: true },
{ d: "25", m: "Jun", title: "Founders coffee circle", meta: "08:30 · Café bar", going: false }
];
var shouts = [
{ who: "Priya Anand", init: "PA", hue: "var(--amber)", text: "Huge thanks to Dev for jump-starting my laptop in the lobby — lifesaver before a client call!", time: "2h ago", cheers: 14 },
{ who: "Theo Marsh", init: "TM", hue: "var(--plant)", text: "Maker Bench is freshly stocked with cutting mats and spare blades. Go build something.", time: "5h ago", cheers: 9 },
{ who: "Nina Okafor", init: "NO", hue: "#8a6f4a", text: "Found a navy water bottle in Spindle Room — it's at the front desk. Come claim it!", time: "Yesterday", cheers: 6 }
];
/* ---------- Helpers ---------- */
function $(sel, ctx) { return (ctx || document).querySelector(sel); }
var toastWrap = $("#toastWrap");
function toast(msg, icon) {
var t = document.createElement("div");
t.className = "toast";
t.innerHTML = '<span class="t-ico">' + (icon || "✓") + "</span>" + msg;
toastWrap.appendChild(t);
setTimeout(function () {
t.classList.add("out");
setTimeout(function () { t.remove(); }, 320);
}, 2600);
}
/* ---------- Credits ring ---------- */
var ring = $("#ringFg");
var creditsNum = $("#creditsNum");
var CIRC = 2 * Math.PI * 52; // ~326.7
function renderCredits(animate) {
var left = Math.max(0, TOTAL_HOURS - usedHours);
var frac = usedHours / TOTAL_HOURS;
ring.style.strokeDashoffset = String(CIRC * (1 - frac));
if (animate === false) {
creditsNum.textContent = left;
return;
}
// count-up animation on the "hours left" figure
var start = parseInt(creditsNum.textContent, 10) || 0;
var dur = 900, t0 = performance.now();
function step(now) {
var p = Math.min(1, (now - t0) / dur);
var eased = 1 - Math.pow(1 - p, 3);
creditsNum.textContent = Math.round(start + (left - start) * eased);
if (p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
/* ---------- Bookings ---------- */
var bookingList = $("#bookingList");
var bookingEmpty = $("#bookingEmpty");
var todayCount = $("#todayCount");
var TYPE_LABEL = { desk: "Desk", room: "Meeting room", booth: "Focus booth" };
function renderBookings() {
bookingList.innerHTML = "";
bookings.forEach(function (b) {
var li = document.createElement("li");
li.className = "booking";
li.dataset.id = b.id;
li.innerHTML =
'<div class="bk-time"><b>' + b.from + "</b><small>" + b.to + "</small></div>" +
'<div class="bk-info"><b>' + b.title + "</b><span>" + b.where + "</span>" +
'<span class="bk-tag tag-' + b.type + '">' + TYPE_LABEL[b.type] + "</span></div>" +
'<button class="cancel-btn" type="button">Cancel</button>';
li.querySelector(".cancel-btn").addEventListener("click", function () { cancelBooking(b.id); });
bookingList.appendChild(li);
});
var n = bookings.length;
todayCount.textContent = n + " booked";
bookingEmpty.hidden = n !== 0;
}
function cancelBooking(id) {
var li = bookingList.querySelector('[data-id="' + id + '"]');
if (li) li.classList.add("removing");
var idx = bookings.findIndex(function (b) { return b.id === id; });
var title = idx > -1 ? bookings[idx].title : "Booking";
setTimeout(function () {
if (idx > -1) {
// refund 1 credit hour on cancel
usedHours = Math.max(0, usedHours - 1);
bookings.splice(idx, 1);
}
renderBookings();
renderCredits(true);
}, 300);
toast(title + " cancelled · 1 hr refunded", "↺");
}
/* ---------- Quick book ---------- */
var quickGrid = $("#quickGrid");
var bookSeq = 4;
function renderQuick() {
quickGrid.innerHTML = "";
quickSpaces.forEach(function (s, i) {
var btn = document.createElement("button");
btn.className = "quick-tile";
btn.type = "button";
btn.disabled = !s.free;
btn.innerHTML =
'<div class="qt-top"><span class="qt-name">' + s.name + "</span>" +
'<span class="qt-status ' + (s.free ? "free" : "busy") + '">' + (s.free ? "Free now" : "Occupied") + "</span></div>" +
'<span class="qt-meta">' + s.area + "</span>" +
'<span class="qt-cost">' + s.cost + " · book to 4:30pm</span>";
if (s.free) {
btn.addEventListener("click", function () { quickBook(s, i); });
}
quickGrid.appendChild(btn);
});
}
function quickBook(space, i) {
bookSeq += 1;
var cost = space.cost.indexOf("0.5") === 0 ? 0.5 : (parseFloat(space.cost) || 1);
var typ = space.name.toLowerCase().indexOf("room") > -1 ? "room"
: space.name.toLowerCase().indexOf("booth") > -1 ? "booth" : "desk";
bookings.push({
id: "q" + bookSeq, from: "Now", to: "+1h",
title: space.name, where: space.area, type: typ
});
usedHours = Math.min(TOTAL_HOURS, usedHours + cost);
// mark occupied for the session
space.free = false;
renderBookings();
renderQuick();
renderCredits(true);
toast(space.name + " booked · added to today", "✓");
}
/* ---------- Tabs ---------- */
var tabs = document.querySelectorAll(".tab");
tabs.forEach(function (tab) {
tab.addEventListener("click", function () {
tabs.forEach(function (t) {
t.classList.remove("is-active");
t.setAttribute("aria-selected", "false");
});
tab.classList.add("is-active");
tab.setAttribute("aria-selected", "true");
var which = tab.dataset.panel;
["events", "shouts"].forEach(function (p) {
var panel = $("#panel-" + p);
var on = p === which;
panel.classList.toggle("is-active", on);
panel.hidden = !on;
});
});
});
/* ---------- Events ---------- */
var eventList = $("#eventList");
function renderEvents() {
eventList.innerHTML = "";
events.forEach(function (e, i) {
var li = document.createElement("li");
li.className = "event";
li.innerHTML =
'<div class="ev-date"><b>' + e.d + "</b><small>" + e.m + "</small></div>" +
'<div class="ev-info"><b>' + e.title + "</b><span>" + e.meta + "</span></div>" +
'<button class="rsvp-btn' + (e.going ? " is-going" : "") + '" type="button">' +
(e.going ? "Going" : "RSVP") + "</button>";
var btn = li.querySelector(".rsvp-btn");
btn.addEventListener("click", function () {
e.going = !e.going;
btn.classList.toggle("is-going", e.going);
btn.textContent = e.going ? "Going" : "RSVP";
toast(e.going ? "You're going to " + e.title : "RSVP removed", e.going ? "✓" : "✕");
});
eventList.appendChild(li);
});
}
/* ---------- Shoutouts ---------- */
var shoutList = $("#shoutList");
function renderShouts() {
shoutList.innerHTML = "";
shouts.forEach(function (s) {
var li = document.createElement("li");
li.className = "shout";
li.innerHTML =
'<div class="sh-av" style="background:' + s.hue + '">' + s.init + "</div>" +
'<div class="sh-body"><b>' + s.who + "</b><p>" + s.text + "</p>" +
'<div class="sh-foot"><span>' + s.time + "</span>" +
'<button class="cheer-btn" type="button">▲ <span class="cheer-n">' + s.cheers + "</span></button></div></div>";
var btn = li.querySelector(".cheer-btn");
var nSpan = li.querySelector(".cheer-n");
var cheered = false;
btn.addEventListener("click", function () {
cheered = !cheered;
s.cheers += cheered ? 1 : -1;
nSpan.textContent = s.cheers;
btn.classList.toggle("cheered", cheered);
});
shoutList.appendChild(li);
});
}
/* ---------- Access / check-in ---------- */
var accessCard = $("#accessCard");
var accessLabel = $("#accessLabel");
var accessSub = $("#accessSub");
var accessToggle = $("#accessToggle");
function renderAccess() {
accessCard.classList.toggle("is-out", !checkedIn);
accessLabel.textContent = checkedIn ? "Checked in" : "Checked out";
accessSub.textContent = checkedIn ? "North Loft · Floor 2" : "Tap to scan at the door";
accessToggle.textContent = checkedIn ? "Check out" : "Check in";
}
accessToggle.addEventListener("click", function () {
checkedIn = !checkedIn;
renderAccess();
toast(checkedIn ? "Welcome back — checked in" : "Checked out. See you soon!", checkedIn ? "◍" : "◌");
});
/* ---------- QR (decorative) ---------- */
function renderQR() {
var grid = $("#qrGrid");
// deterministic pseudo-pattern so it looks like a real code
var seed = 7;
for (var i = 0; i < 49; i++) {
var cell = document.createElement("i");
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
var on = (seed >> 7) % 100 > 48;
// force finder-pattern-ish corners
var r = Math.floor(i / 7), c = i % 7;
var corner = (r < 2 && c < 2) || (r < 2 && c > 4) || (r > 4 && c < 2);
if (corner) on = (r === 0 || r === 6 || c === 0 || c === 6 || (r === 1 && c === 1));
if (!on) cell.className = "off";
grid.appendChild(cell);
}
}
/* ---------- Top up ---------- */
$("#topUpBtn").addEventListener("click", function () {
usedHours = Math.max(0, usedHours - 8);
renderCredits(true);
toast("+8 hrs added to your balance", "+");
});
/* ---------- Quick book top button → first free space ---------- */
$("#quickBookBtn").addEventListener("click", function () {
var idx = quickSpaces.findIndex(function (s) { return s.free; });
if (idx === -1) { toast("No instant spaces free right now", "✕"); return; }
quickBook(quickSpaces[idx], idx);
});
/* ---------- Init ---------- */
renderQR();
renderAccess();
renderBookings();
renderQuick();
renderEvents();
renderShouts();
renderCredits(false);
// trigger ring + count animation shortly after paint
requestAnimationFrame(function () {
setTimeout(function () { renderCredits(true); }, 220);
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Looms Coworking — Member Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<!-- Sidebar -->
<aside class="sidebar" aria-label="Member navigation">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◍</span>
<div class="brand-text">
<strong>Looms</strong>
<span>Coworking</span>
</div>
</div>
<nav class="nav">
<a class="nav-item is-active" href="#" aria-current="page"><span class="ni-ico">▦</span> Dashboard</a>
<a class="nav-item" href="#"><span class="ni-ico">▤</span> Book a space</a>
<a class="nav-item" href="#"><span class="ni-ico">◷</span> My bookings</a>
<a class="nav-item" href="#"><span class="ni-ico">◇</span> Community</a>
<a class="nav-item" href="#"><span class="ni-ico">⚙</span> Settings</a>
</nav>
<div class="access-card" id="accessCard">
<div class="access-head">
<span class="access-dot" id="accessDot" aria-hidden="true"></span>
<span class="access-label" id="accessLabel">Checked in</span>
</div>
<p class="access-sub" id="accessSub">North Loft · Floor 2</p>
<button class="access-btn" id="accessToggle" type="button">Check out</button>
<div class="qr" aria-hidden="true">
<div class="qr-grid" id="qrGrid"></div>
<span>Door pass</span>
</div>
</div>
</aside>
<!-- Main -->
<main class="main">
<header class="topbar">
<div class="greeting">
<h1>Good afternoon, Marlowe</h1>
<p id="greetSub">Tuesday, June 18 · 3 things on your plate today</p>
</div>
<div class="top-actions">
<button class="ghost-btn" id="quickBookBtn" type="button">+ Quick book</button>
<div class="avatar" title="Marlowe Quinn">MQ</div>
</div>
</header>
<section class="grid">
<!-- Credits meter -->
<article class="card credits" aria-labelledby="creditsTitle">
<div class="card-head">
<h2 id="creditsTitle">Booking credits</h2>
<span class="pill">Resets Jul 1</span>
</div>
<div class="meter">
<svg class="ring" viewBox="0 0 120 120" role="img" aria-label="Credits remaining">
<circle class="ring-bg" cx="60" cy="60" r="52" />
<circle class="ring-fg" id="ringFg" cx="60" cy="60" r="52" />
</svg>
<div class="meter-center">
<strong id="creditsNum">0</strong>
<span>of 40 hrs</span>
</div>
</div>
<div class="credit-legend">
<span><i class="dot dot-amber"></i> 16h used</span>
<span><i class="dot dot-line"></i> 24h left</span>
</div>
<button class="link-btn" id="topUpBtn" type="button">Top up hours</button>
</article>
<!-- Today's bookings -->
<article class="card today" aria-labelledby="todayTitle">
<div class="card-head">
<h2 id="todayTitle">Today's bookings</h2>
<span class="pill" id="todayCount">3 booked</span>
</div>
<ul class="booking-list" id="bookingList"></ul>
<p class="empty" id="bookingEmpty" hidden>No bookings left today. Enjoy the quiet.</p>
</article>
<!-- Quick book -->
<article class="card quick" aria-labelledby="quickTitle">
<div class="card-head">
<h2 id="quickTitle">Quick book</h2>
</div>
<p class="quick-sub">Grab an open space for the next hour.</p>
<div class="quick-grid" id="quickGrid"></div>
</article>
<!-- Tabs: events / shoutouts -->
<article class="card feed" aria-labelledby="feedTitle">
<div class="card-head tabs-head">
<h2 id="feedTitle" class="sr-only">Community feed</h2>
<div class="tabs" role="tablist" aria-label="Community feed">
<button class="tab is-active" role="tab" aria-selected="true" id="tab-events" data-panel="events">Upcoming events</button>
<button class="tab" role="tab" aria-selected="false" id="tab-shouts" data-panel="shouts">Shoutouts</button>
</div>
</div>
<div class="tab-panel is-active" id="panel-events" role="tabpanel" aria-labelledby="tab-events">
<ul class="event-list" id="eventList"></ul>
</div>
<div class="tab-panel" id="panel-shouts" role="tabpanel" aria-labelledby="tab-shouts" hidden>
<ul class="shout-list" id="shoutList"></ul>
</div>
</article>
</section>
</main>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Member Dashboard
A self-contained member home screen for Looms Coworking, styled in the warm-industrial palette — concrete neutrals, an amber accent, and a plant-green secondary. A matte-black sidebar holds navigation plus a live access card with a check-in toggle and a procedurally drawn door QR. The main panel lays out four cards: an animated credits ring, today’s bookings, quick-book tiles, and a tabbed community feed.
The interactions are real. Cancelling a booking slides it out and refunds one credit hour into the animated ring; the Quick book tiles show each space as free or occupied and, when booked, append a live entry to today’s list while marking the space occupied and deducting credits. The top-bar Quick book button grabs the first open space, Top up hours restores balance, and the ring count-up eases on load and after every change.
The community card tabs between upcoming events — each with a toggleable RSVP — and member shoutouts you can cheer up or down. Toasts confirm every action, all buttons are keyboard-focusable with visible focus rings, and the grid collapses cleanly to a single column down to 360px.
Illustrative UI only — fictional coworking space, not a real booking system.