Coworking — Community Feed
A warm industrial community feed for a coworking space, mixing announcements, event posts, member intros and a marketplace into one filterable stream. Members can RSVP to events, like posts, search the feed, browse a live member directory sidebar with check-in status, and compose new posts by type from an inline composer. Built as a self-contained vanilla page with toast feedback, accessible controls and a fully responsive layout 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;
--occupied: #d4503e;
--free: #2f9e6f;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(28, 27, 25, 0.06), 0 6px 18px rgba(28, 27, 25, 0.05);
--sh-2: 0 8px 30px rgba(28, 27, 25, 0.1);
}
* { box-sizing: border-box; }
html { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1200px 480px at 12% -8%, var(--amber-50), transparent 60%),
var(--bg);
}
.skip-link {
position: absolute;
left: -999px;
top: 0;
background: var(--char);
color: #fff;
padding: 8px 14px;
border-radius: 0 0 var(--r-sm) 0;
z-index: 50;
}
.skip-link:focus { left: 0; }
/* Topbar */
.topbar {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
gap: 16px;
padding: 12px clamp(14px, 4vw, 36px);
background: rgba(246, 243, 238, 0.86);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
width: 40px; height: 40px;
display: grid; place-items: center;
background: var(--char); color: var(--amber);
border-radius: var(--r-md);
font-size: 22px; font-weight: 800;
box-shadow: var(--sh-1);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.2; }
.brand-text strong { font-size: 16px; letter-spacing: -0.01em; }
.brand-text span { font-size: 12px; color: var(--muted); }
.topbar-actions { display: flex; align-items: center; gap: 10px; margin-left: auto; }
.search {
display: flex; align-items: center; gap: 8px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 8px 14px;
width: clamp(160px, 26vw, 320px);
transition: border-color .15s, box-shadow .15s;
}
.search:focus-within { border-color: var(--amber); box-shadow: 0 0 0 3px var(--amber-50); }
.search-ico { color: var(--muted); font-size: 16px; }
.search input { border: 0; outline: 0; background: transparent; font: inherit; width: 100%; color: var(--ink); }
.me .avatar { box-shadow: 0 0 0 2px var(--surface), 0 0 0 3px var(--line-2); }
/* Buttons */
.btn {
font: inherit; font-weight: 600;
border: 1px solid transparent;
border-radius: var(--r-sm);
padding: 9px 16px;
cursor: pointer;
transition: transform .08s, background .15s, box-shadow .15s, border-color .15s;
white-space: nowrap;
}
.btn:active { transform: translateY(1px); }
.btn-amber { background: var(--amber); color: #2a1c08; box-shadow: var(--sh-1); }
.btn-amber:hover { background: var(--amber-d); }
.btn-ghost { background: var(--surface); color: var(--ink); border-color: var(--line-2); }
.btn-ghost:hover { background: var(--concrete); }
.btn:focus-visible, .filter:focus-visible, .type-chip:focus-visible,
.like-btn:focus-visible, .link-btn:focus-visible, .icon-btn:focus-visible {
outline: 3px solid var(--amber); outline-offset: 2px;
}
.icon-btn {
border: 0; background: transparent; color: var(--muted);
font-size: 16px; cursor: pointer; border-radius: var(--r-sm);
width: 30px; height: 30px;
}
.icon-btn:hover { background: var(--concrete); color: var(--ink); }
/* Avatars */
.avatar {
width: 40px; height: 40px;
flex: 0 0 40px;
display: grid; place-items: center;
border-radius: 50%;
font-size: 13px; font-weight: 700; color: #fff;
background: linear-gradient(135deg, var(--ink-2), var(--char));
}
.av-plant { background: linear-gradient(135deg, #7a9468, var(--plant)); }
.av-amber { background: linear-gradient(135deg, #f0a84e, var(--amber-d)); color: #2a1c08; }
.av-river { background: linear-gradient(135deg, #6c93a8, #3f6377); }
.av-clay { background: linear-gradient(135deg, #c77a5a, #a3553a); }
/* Layout shell */
.shell {
max-width: 1140px;
margin: 0 auto;
padding: 24px clamp(14px, 4vw, 36px) 64px;
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
gap: 24px;
align-items: start;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-1);
}
.card-title {
margin: 0;
font-size: 14px; font-weight: 700;
letter-spacing: .02em; text-transform: uppercase; color: var(--ink-2);
}
/* Inline composer */
.composer-inline {
padding: 16px;
margin-bottom: 16px;
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
background: var(--surface);
box-shadow: var(--sh-2);
}
.composer-head { display: flex; align-items: center; gap: 12px; }
.composer-head strong { flex: 1; font-size: 15px; }
.composer-types { display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0; }
.type-chip {
font: inherit; font-size: 13px; font-weight: 500;
border: 1px solid var(--line-2); background: var(--bg);
color: var(--ink-2); border-radius: 999px; padding: 6px 12px; cursor: pointer;
transition: all .12s;
}
.type-chip:hover { border-color: var(--amber); }
.type-chip.is-on { background: var(--char); color: var(--concrete); border-color: var(--char); }
.composer-inline textarea {
width: 100%; resize: vertical; min-height: 70px;
font: inherit; color: var(--ink);
border: 1px solid var(--line-2); border-radius: var(--r-md);
padding: 12px; background: var(--bg);
}
.composer-inline textarea:focus { outline: 0; border-color: var(--amber); box-shadow: 0 0 0 3px var(--amber-50); }
.composer-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; }
.char-count { font-size: 12px; color: var(--muted); }
.char-count.over { color: var(--danger); font-weight: 600; }
/* Filters */
.filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 18px; }
.filter {
font: inherit; font-size: 13px; font-weight: 600;
border: 1px solid var(--line-2); background: var(--surface);
color: var(--ink-2); border-radius: 999px; padding: 8px 14px; cursor: pointer;
transition: all .12s;
}
.filter:hover { border-color: var(--amber); color: var(--ink); }
.filter.is-on { background: var(--amber); color: #2a1c08; border-color: var(--amber); box-shadow: var(--sh-1); }
/* Feed */
.feed { display: flex; flex-direction: column; gap: 16px; }
.post { padding: 18px; transition: box-shadow .15s, transform .1s; outline: none; }
.post:hover { box-shadow: var(--sh-2); }
.post:focus-visible { box-shadow: 0 0 0 3px var(--amber-50), var(--sh-2); }
.post-head { display: flex; align-items: center; gap: 12px; }
.post-meta { display: flex; flex-direction: column; line-height: 1.25; flex: 1; min-width: 0; }
.post-author { font-size: 15px; }
.post-sub { font-size: 12.5px; color: var(--muted); }
.badge {
font-size: 11px; font-weight: 700; letter-spacing: .03em; text-transform: uppercase;
padding: 4px 9px; border-radius: 999px; white-space: nowrap;
}
.badge.announcement { background: var(--amber-50); color: var(--amber-d); }
.badge.event { background: #e9f1ec; color: var(--plant); }
.badge.intro { background: #eef0f4; color: #4f6072; }
.badge.marketplace { background: #f6ece6; color: var(--clay, #a3553a); }
.post-body { margin: 12px 0 4px; color: var(--ink); font-size: 14.5px; }
.post-body strong { color: var(--char); }
.event-box {
margin-top: 12px; padding: 12px 14px;
background: #e9f1ec; border: 1px solid rgba(95, 122, 82, .25);
border-radius: var(--r-md);
display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
}
.event-when { font-size: 13px; font-weight: 600; color: var(--plant); }
.event-rsvp { display: flex; align-items: center; gap: 10px; }
.rsvp-count { font-size: 12.5px; color: var(--ink-2); }
.rsvp-btn.is-going { background: var(--plant); color: #fff; }
.rsvp-btn.is-going:hover { background: #4f6845; }
.market-box {
margin-top: 12px; padding: 10px 14px;
background: var(--bg); border: 1px dashed var(--line-2);
border-radius: var(--r-md);
display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.price { font-size: 18px; font-weight: 800; color: var(--char); }
.post-foot {
display: flex; align-items: center; gap: 6px;
margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line);
}
.like-btn, .link-btn {
font: inherit; font-size: 13px; font-weight: 600;
border: 0; background: transparent; color: var(--ink-2);
cursor: pointer; padding: 7px 12px; border-radius: var(--r-sm);
display: inline-flex; align-items: center; gap: 6px;
transition: background .12s, color .12s;
}
.like-btn:hover, .link-btn:hover { background: var(--concrete); }
.share-btn { margin-left: auto; }
.like-btn .heart { font-size: 16px; line-height: 1; transition: transform .15s; }
.like-btn[aria-pressed="true"] { color: var(--danger); }
.like-btn[aria-pressed="true"] .heart { transform: scale(1.15); }
.empty { text-align: center; color: var(--muted); padding: 40px 0; font-size: 14px; }
/* Sidebar */
.side-col { display: flex; flex-direction: column; gap: 16px; position: sticky; top: 80px; }
.stat-card, .dir-card, .cta-card { padding: 16px; }
.stat-list { list-style: none; margin: 12px 0 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.stat-list li { display: flex; align-items: center; gap: 10px; font-size: 13.5px; color: var(--ink-2); }
.dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 9px; }
.dot-free { background: var(--free); }
.dot-warn { background: var(--warn); }
.dot-plant { background: var(--plant); }
.count {
font-size: 12px; font-weight: 700; color: var(--amber-d);
background: var(--amber-50); border-radius: 999px; padding: 2px 8px; margin-left: 6px;
}
.dir-search { display: block; margin: 12px 0 10px; }
.dir-search input {
width: 100%; font: inherit; border: 1px solid var(--line-2);
border-radius: var(--r-sm); padding: 8px 12px; background: var(--bg); color: var(--ink);
}
.dir-search input:focus { outline: 0; border-color: var(--amber); box-shadow: 0 0 0 3px var(--amber-50); }
.dir-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.dir-item {
display: flex; align-items: center; gap: 11px; padding: 7px 6px;
border-radius: var(--r-md); cursor: pointer; transition: background .12s;
}
.dir-item:hover { background: var(--concrete); }
.dir-item .avatar { width: 34px; height: 34px; flex-basis: 34px; font-size: 12px; }
.dir-info { display: flex; flex-direction: column; line-height: 1.2; min-width: 0; }
.dir-name { font-size: 13.5px; font-weight: 600; }
.dir-role { font-size: 11.5px; color: var(--muted); }
.dir-status { margin-left: auto; width: 8px; height: 8px; border-radius: 50%; flex: 0 0 8px; }
.dir-status.in { background: var(--free); }
.dir-status.out { background: var(--line-2); }
.cta-card p { margin: 10px 0 14px; font-size: 13px; color: var(--ink-2); }
.cta-card .btn { width: 100%; }
/* Toast */
.toast {
position: fixed; left: 50%; bottom: 24px; transform: translate(-50%, 24px);
background: var(--char); color: var(--concrete);
padding: 12px 18px; border-radius: 999px; font-size: 13.5px; font-weight: 500;
box-shadow: var(--sh-2); opacity: 0; pointer-events: none;
transition: opacity .2s, transform .25s; z-index: 60; max-width: 90vw;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* Responsive */
@media (max-width: 880px) {
.shell { grid-template-columns: 1fr; }
.side-col { position: static; order: -1; }
}
@media (max-width: 520px) {
.topbar { flex-wrap: wrap; gap: 10px; }
.search { width: 100%; order: 3; }
.brand-text span { display: none; }
.shell { padding-top: 16px; }
.post-foot { flex-wrap: wrap; }
.share-btn { margin-left: 0; }
}(function () {
"use strict";
var BADGE_LABEL = {
announcement: "Announcement",
event: "Event",
intro: "Member intro",
marketplace: "Marketplace"
};
var AVATAR_CLASSES = ["av-plant", "av-amber", "av-river", "av-clay"];
function initials(name) {
return name.split(/\s+/).map(function (w) { return w[0]; }).join("").slice(0, 2).toUpperCase();
}
var posts = [
{
id: "p1", type: "announcement", author: "Loomhouse Hosts", avatar: "av-amber",
sub: "Community team · 2h ago",
body: "The Loom-floor printers are back online and the new fibre line is live — speeds doubled. Thanks for your patience while we re-wired the mezzanine.",
likes: 34, liked: false, comments: 7
},
{
id: "p2", type: "event", author: "Marisol Vega", avatar: "av-plant",
sub: "Ceramics studio · 4h ago",
body: "Hosting a slow-coffee + sketch morning this Friday in the roof garden. Bring a notebook, leave the laptop. All skill levels welcome ✏️",
event: { when: "Fri · Jun 20 · 9:00–10:30am · Roof Garden", going: false, count: 12 },
likes: 18, liked: true, comments: 4
},
{
id: "p3", type: "intro", author: "Dev Okoro", avatar: "av-river",
sub: "New member · 6h ago",
body: "Hey Loomhouse! 👋 I'm Dev, building a small climate-data startup. Desk 14 on the Loom floor. Always up to trade product feedback for a flat white.",
likes: 27, liked: false, comments: 11
},
{
id: "p4", type: "marketplace", author: "Priya Anand", avatar: "av-clay",
sub: "You · 8h ago",
body: "Selling a barely-used standing desk converter — height-adjustable, fits a 27\" monitor. Upgraded to a full sit-stand, so this needs a new home.",
market: { price: "$85" },
likes: 9, liked: false, comments: 3
},
{
id: "p5", type: "event", author: "Tobias Frei", avatar: "av-amber",
sub: "Founders circle · yesterday",
body: "Monthly Founders Lunch is open for sign-ups. Lightning intros, one shared problem each, and the Mill kitchen handles the food. Cap is 20 seats.",
event: { when: "Wed · Jun 25 · 12:30–2:00pm · Kiln Room", going: false, count: 16 },
likes: 22, liked: false, comments: 6
},
{
id: "p6", type: "announcement", author: "Loomhouse Hosts", avatar: "av-amber",
sub: "Community team · yesterday",
body: "Reminder: phone booths are for calls, not focus naps 😴. We added two new acoustic pods by the river-side windows — first come, first served.",
likes: 41, liked: true, comments: 9
},
{
id: "p7", type: "intro", author: "Lena Brandt", avatar: "av-plant",
sub: "New member · 2 days ago",
body: "Hi all — Lena here, freelance motion designer. I gave a talk on After Effects rigging last year and I'm happy to do an informal session if there's interest!",
likes: 31, liked: false, comments: 8
}
];
var members = [
{ name: "Marisol Vega", role: "Ceramics studio", avatar: "av-plant", in: true },
{ name: "Dev Okoro", role: "Climate-data founder", avatar: "av-river", in: true },
{ name: "Tobias Frei", role: "Founders circle host", avatar: "av-amber", in: true },
{ name: "Lena Brandt", role: "Motion designer", avatar: "av-plant", in: false },
{ name: "Amara Sow", role: "Brand strategist", avatar: "av-clay", in: true },
{ name: "Jonas Reidt", role: "iOS engineer", avatar: "av-river", in: false },
{ name: "Priya Anand", role: "Product designer", avatar: "av-clay", in: true },
{ name: "Noah Källström", role: "Illustrator", avatar: "av-amber", in: true }
];
var feedEl = document.getElementById("feed");
var emptyEl = document.getElementById("emptyState");
var tpl = document.getElementById("postTpl");
var activeFilter = "all";
var query = "";
/* ---- toast ---- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2400);
}
/* ---- render feed ---- */
function matches(post) {
if (activeFilter !== "all" && post.type !== activeFilter) return false;
if (!query) return true;
var hay = (post.author + " " + post.body).toLowerCase();
return hay.indexOf(query) !== -1;
}
function buildPost(post) {
var node = tpl.content.firstElementChild.cloneNode(true);
node.dataset.id = post.id;
var av = node.querySelector(".avatar");
av.classList.add(post.avatar);
av.textContent = initials(post.author);
node.querySelector(".post-author").textContent = post.author;
node.querySelector(".post-sub").textContent = post.sub;
var badge = node.querySelector(".badge");
badge.textContent = BADGE_LABEL[post.type];
badge.classList.add(post.type);
node.querySelector(".post-body").textContent = post.body;
if (post.event) {
var box = node.querySelector(".event-box");
box.hidden = false;
box.querySelector(".event-when").textContent = post.event.when;
var rsvp = box.querySelector(".rsvp-btn");
var rc = box.querySelector(".rsvp-count");
function paintRsvp() {
rsvp.textContent = post.event.going ? "✓ Going" : "RSVP";
rsvp.classList.toggle("is-going", post.event.going);
rsvp.setAttribute("aria-pressed", String(post.event.going));
rc.textContent = post.event.count + " going";
}
paintRsvp();
rsvp.addEventListener("click", function () {
post.event.going = !post.event.going;
post.event.count += post.event.going ? 1 : -1;
paintRsvp();
toast(post.event.going ? "You're going — see you there!" : "RSVP removed");
});
}
if (post.market) {
var mbox = node.querySelector(".market-box");
mbox.hidden = false;
mbox.querySelector(".price").textContent = post.market.price;
mbox.querySelector("button").addEventListener("click", function () {
toast("Message sent to " + post.author.split(" ")[0]);
});
}
var like = node.querySelector(".like-btn");
var likeCount = node.querySelector(".like-count");
var heart = node.querySelector(".heart");
function paintLike() {
like.setAttribute("aria-pressed", String(post.liked));
heart.textContent = post.liked ? "♥" : "♡";
likeCount.textContent = post.likes;
}
paintLike();
like.addEventListener("click", function () {
post.liked = !post.liked;
post.likes += post.liked ? 1 : -1;
paintLike();
});
node.querySelector(".comment-count").textContent = post.comments;
node.querySelector(".comment-btn").addEventListener("click", function () {
toast("Comments are illustrative in this demo");
});
node.querySelector(".share-btn").addEventListener("click", function () {
toast("Link copied to clipboard");
});
return node;
}
function render() {
var list = posts.filter(matches);
feedEl.innerHTML = "";
list.forEach(function (p) { feedEl.appendChild(buildPost(p)); });
emptyEl.hidden = list.length !== 0;
}
/* ---- filters ---- */
var filterBtns = Array.prototype.slice.call(document.querySelectorAll(".filter"));
filterBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
filterBtns.forEach(function (b) { b.classList.remove("is-on"); });
btn.classList.add("is-on");
activeFilter = btn.dataset.filter;
render();
});
});
document.getElementById("search").addEventListener("input", function (e) {
query = e.target.value.trim().toLowerCase();
render();
});
/* ---- directory ---- */
var dirList = document.getElementById("dirList");
var dirCount = document.getElementById("dirCount");
function renderDir(filterStr) {
var f = (filterStr || "").trim().toLowerCase();
dirList.innerHTML = "";
var shown = 0;
members.forEach(function (m) {
if (f && (m.name + " " + m.role).toLowerCase().indexOf(f) === -1) return;
shown++;
var li = document.createElement("li");
li.className = "dir-item";
li.tabIndex = 0;
li.innerHTML =
'<span class="avatar ' + m.avatar + '">' + initials(m.name) + "</span>" +
'<span class="dir-info"><span class="dir-name">' + m.name + "</span>" +
'<span class="dir-role">' + m.role + "</span></span>" +
'<span class="dir-status ' + (m.in ? "in" : "out") + '" title="' +
(m.in ? "Checked in" : "Away") + '"></span>';
function open() { toast((m.in ? "" : "Away · ") + m.name + " — " + m.role); }
li.addEventListener("click", open);
li.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); open(); }
});
dirList.appendChild(li);
});
dirCount.textContent = shown;
}
document.getElementById("dirSearch").addEventListener("input", function (e) {
renderDir(e.target.value);
});
/* ---- composer ---- */
var composer = document.getElementById("composerInline");
var composeBody = document.getElementById("composeBody");
var charCount = document.getElementById("charCount");
var composeType = "announcement";
var MAX = 280;
document.getElementById("composeOpen").addEventListener("click", function () {
composer.hidden = false;
composeBody.focus();
composer.scrollIntoView({ behavior: "smooth", block: "nearest" });
});
document.getElementById("composeClose").addEventListener("click", function () {
composer.hidden = true;
});
var typeChips = Array.prototype.slice.call(composer.querySelectorAll(".type-chip"));
typeChips.forEach(function (chip) {
chip.addEventListener("click", function () {
typeChips.forEach(function (c) { c.classList.remove("is-on"); });
chip.classList.add("is-on");
composeType = chip.dataset.type;
});
});
composeBody.addEventListener("input", function () {
var len = composeBody.value.length;
charCount.textContent = len + " / " + MAX;
charCount.classList.toggle("over", len > MAX);
});
document.getElementById("composePost").addEventListener("click", function () {
var text = composeBody.value.trim();
if (!text) { toast("Write something first ✏️"); composeBody.focus(); return; }
if (text.length > MAX) { toast("Post is too long"); return; }
var newPost = {
id: "p" + Date.now(),
type: composeType,
author: "Priya Anand",
avatar: "av-clay",
sub: "You · just now",
body: text,
likes: 0, liked: false, comments: 0
};
if (composeType === "event") {
newPost.event = { when: "Date TBD · The Mill", going: false, count: 0 };
}
if (composeType === "marketplace") {
newPost.market = { price: "Make offer" };
}
posts.unshift(newPost);
composeBody.value = "";
charCount.textContent = "0 / " + MAX;
charCount.classList.remove("over");
composer.hidden = true;
if (activeFilter !== "all" && activeFilter !== composeType) {
filterBtns.forEach(function (b) {
b.classList.toggle("is-on", b.dataset.filter === "all");
});
activeFilter = "all";
}
query = "";
document.getElementById("search").value = "";
render();
feedEl.firstElementChild.scrollIntoView({ behavior: "smooth", block: "nearest" });
toast("Posted to the community feed");
});
document.getElementById("houseBtn").addEventListener("click", function () {
toast("House guide opens in the member handbook");
});
/* ---- init ---- */
render();
renderDir("");
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Loomhouse — Community Feed</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#feed">Skip to feed</a>
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◵</span>
<div class="brand-text">
<strong>Loomhouse</strong>
<span>Community · Riverside Mill</span>
</div>
</div>
<div class="topbar-actions">
<label class="search" aria-label="Search the feed">
<span class="search-ico" aria-hidden="true">⌕</span>
<input id="search" type="search" placeholder="Search posts, members, events…" autocomplete="off" />
</label>
<button id="composeOpen" class="btn btn-amber" type="button">+ New post</button>
<div class="me" title="Signed in as Priya Anand">
<span class="avatar av-plant" aria-hidden="true">PA</span>
</div>
</div>
</header>
<main class="shell">
<section class="feed-col" aria-label="Community feed">
<div class="composer-inline" id="composerInline" hidden>
<div class="composer-head">
<span class="avatar av-plant" aria-hidden="true">PA</span>
<strong>Share with the Loomhouse community</strong>
<button class="icon-btn" id="composeClose" type="button" aria-label="Close composer">✕</button>
</div>
<div class="composer-types" role="group" aria-label="Post type">
<button class="type-chip is-on" data-type="announcement" type="button">📣 Announcement</button>
<button class="type-chip" data-type="event" type="button">📅 Event</button>
<button class="type-chip" data-type="intro" type="button">👋 Intro</button>
<button class="type-chip" data-type="marketplace" type="button">🛒 Marketplace</button>
</div>
<textarea id="composeBody" rows="3" placeholder="What's happening at the Mill?"></textarea>
<div class="composer-foot">
<span class="char-count" id="charCount">0 / 280</span>
<button class="btn btn-amber" id="composePost" type="button">Post</button>
</div>
</div>
<nav class="filters" aria-label="Filter feed">
<button class="filter is-on" data-filter="all" type="button">All</button>
<button class="filter" data-filter="announcement" type="button">📣 Announcements</button>
<button class="filter" data-filter="event" type="button">📅 Events</button>
<button class="filter" data-filter="intro" type="button">👋 Intros</button>
<button class="filter" data-filter="marketplace" type="button">🛒 Marketplace</button>
</nav>
<div class="feed" id="feed"></div>
<p class="empty" id="emptyState" hidden>No posts match this filter yet.</p>
</section>
<aside class="side-col" aria-label="Member directory and space info">
<div class="card stat-card">
<h2 class="card-title">Today at the Mill</h2>
<ul class="stat-list">
<li><span class="dot dot-free"></span> 41 members checked in</li>
<li><span class="dot dot-warn"></span> 6 desks free on Loom floor</li>
<li><span class="dot dot-plant"></span> Roof garden open till 8pm</li>
</ul>
</div>
<div class="card dir-card">
<h2 class="card-title">Member directory <span class="count" id="dirCount">8</span></h2>
<label class="dir-search" aria-label="Filter members">
<input id="dirSearch" type="search" placeholder="Find a member…" autocomplete="off" />
</label>
<ul class="dir-list" id="dirList"></ul>
</div>
<div class="card cta-card">
<h2 class="card-title">House rules</h2>
<p>Be kind, keep calls in the booths, water the plants. The feed is moderated by community hosts.</p>
<button class="btn btn-ghost" id="houseBtn" type="button">View full guide</button>
</div>
</aside>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<template id="postTpl">
<article class="post card" tabindex="0">
<header class="post-head">
<span class="avatar"></span>
<div class="post-meta">
<strong class="post-author"></strong>
<span class="post-sub"></span>
</div>
<span class="badge"></span>
</header>
<div class="post-body"></div>
<div class="event-box" hidden>
<div class="event-when"></div>
<div class="event-rsvp">
<button class="btn btn-amber rsvp-btn" type="button"></button>
<span class="rsvp-count"></span>
</div>
</div>
<div class="market-box" hidden>
<span class="price"></span>
<button class="btn btn-ghost" type="button">Message seller</button>
</div>
<footer class="post-foot">
<button class="like-btn" type="button" aria-pressed="false">
<span class="heart" aria-hidden="true">♡</span> <span class="like-count">0</span>
</button>
<button class="link-btn comment-btn" type="button">💬 <span class="comment-count">0</span></button>
<button class="link-btn share-btn" type="button">↗ Share</button>
</footer>
</article>
</template>
<script src="script.js"></script>
</body>
</html>Community Feed
A single-screen community feed for the fictional Loomhouse coworking space at the Riverside Mill. The main column streams four kinds of posts — announcements from the host team, member-run events, new-member intros, and a peer marketplace — each tagged with a colour-coded badge and an author avatar. A pill filter bar and a live search box narrow the stream instantly, while event posts carry an inline RSVP toggle and a running attendee count.
The right rail keeps the community context close: a “Today at the Mill” occupancy snapshot with free/away status dots, a searchable member directory showing who is checked in, and a house-rules card. Hitting New post slides open an inline composer where you pick a post type, watch a live character counter, and publish straight to the top of the feed.
Everything runs on vanilla JavaScript with no dependencies. Likes, RSVPs, directory lookups, search, and posting all give immediate feedback through a small toast() helper, and the warm-concrete-and-amber design system holds up from a wide desktop down to a 360px phone.
Illustrative UI only — fictional coworking space, not a real booking system.