Nonprofit — Volunteer Manager
A warm, human nonprofit admin screen for managing a volunteer roster and shift roster in one view. It pairs a searchable, filterable volunteers table — name, role, logged hours and status — with a Saturday shift grid showing live fill thermometers and open slots. Coordinators can approve pending applicants, log hours, select a volunteer and assign them to an open shift, while impact stats roll up total hours, active helpers and estimated meals served.
MCP
Code
:root {
--brand: #1f7a6d;
--brand-d: #155e54;
--accent: #e8743b;
--accent-d: #cc5d28;
--ink: #2a2722;
--ink-2: #524d44;
--muted: #7a7368;
--bg: #faf6f0;
--surface: #ffffff;
--line: rgba(42, 39, 34, 0.1);
--line-2: rgba(42, 39, 34, 0.18);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--sh-sm: 0 1px 2px rgba(42, 39, 34, 0.06), 0 2px 8px rgba(42, 39, 34, 0.05);
--sh-md: 0 6px 22px rgba(42, 39, 34, 0.09);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3 { font-family: "Fraunces", Georgia, serif; line-height: 1.2; margin: 0; }
.app { max-width: 1180px; margin: 0 auto; padding: 0 20px 64px; }
/* Topbar */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding: 20px 0 22px;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand__mark {
display: grid;
place-items: center;
width: 44px;
height: 44px;
border-radius: 12px;
background: linear-gradient(145deg, var(--brand), var(--brand-d));
color: #fff;
font-size: 22px;
box-shadow: var(--sh-sm);
}
.brand__name { margin: 0; font-weight: 700; font-size: 1.05rem; letter-spacing: -0.01em; }
.brand__sub { margin: 0; color: var(--muted); font-size: 0.82rem; }
.topbar__right { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.badge {
font-size: 0.74rem;
font-weight: 600;
padding: 6px 11px;
border-radius: 999px;
white-space: nowrap;
}
.badge--trust { color: var(--brand-d); background: rgba(31, 122, 109, 0.1); border: 1px solid rgba(31, 122, 109, 0.22); }
/* Buttons */
.btn {
font: inherit;
font-weight: 600;
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink);
padding: 9px 15px;
border-radius: var(--r-sm);
cursor: pointer;
transition: transform .08s ease, box-shadow .15s ease, background .15s ease, border-color .15s ease;
}
.btn:hover { box-shadow: var(--sh-sm); }
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 3px solid rgba(31, 122, 109, 0.4); outline-offset: 2px; }
.btn--accent {
background: linear-gradient(145deg, var(--accent), var(--accent-d));
border-color: var(--accent-d);
color: #fff;
box-shadow: 0 4px 14px rgba(232, 116, 59, 0.32);
}
.btn--accent:hover { box-shadow: 0 6px 18px rgba(232, 116, 59, 0.42); }
.btn--sm { padding: 5px 10px; font-size: 0.78rem; border-radius: 7px; }
.btn--ghost { background: transparent; border-color: var(--line-2); }
.btn--ok { background: var(--ok); border-color: var(--ok); color: #fff; }
.btn--ok:hover { box-shadow: 0 4px 12px rgba(47, 158, 111, 0.35); }
.btn[disabled] { opacity: .45; cursor: not-allowed; box-shadow: none; }
/* Stats */
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 22px;
}
.stat {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px 18px;
box-shadow: var(--sh-sm);
}
.stat--impact {
background: linear-gradient(150deg, rgba(31, 122, 109, 0.08), rgba(232, 116, 59, 0.08));
border-color: rgba(31, 122, 109, 0.2);
}
.stat__num {
margin: 0;
font-family: "Fraunces", serif;
font-size: 1.85rem;
font-weight: 700;
color: var(--brand-d);
letter-spacing: -0.01em;
}
.stat--impact .stat__num { color: var(--accent-d); }
.stat__label { margin: 2px 0 0; color: var(--muted); font-size: 0.8rem; }
/* Grid layout */
.grid { display: grid; grid-template-columns: 1.55fr 1fr; gap: 18px; align-items: start; }
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 20px;
box-shadow: var(--sh-md);
}
.panel__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 14px;
}
.panel__title { font-size: 1.18rem; font-weight: 600; }
.muted-pill {
font-size: 0.76rem;
color: var(--ink-2);
background: var(--bg);
border: 1px solid var(--line);
padding: 5px 10px;
border-radius: 999px;
}
.hint { margin: 0 0 14px; font-size: 0.82rem; color: var(--muted); }
.hint strong { color: var(--brand-d); }
/* Filters */
.filters { display: flex; gap: 6px; flex-wrap: wrap; }
.chip {
font: inherit;
font-size: 0.8rem;
font-weight: 600;
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink-2);
padding: 5px 12px;
border-radius: 999px;
cursor: pointer;
transition: all .15s ease;
}
.chip:hover { border-color: var(--brand); color: var(--brand-d); }
.chip.is-active { background: var(--brand); border-color: var(--brand); color: #fff; }
.chip:focus-visible { outline: 3px solid rgba(31, 122, 109, 0.4); outline-offset: 2px; }
/* Search */
.search { margin-bottom: 12px; }
.search input {
font: inherit;
width: 100%;
padding: 10px 14px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--bg);
color: var(--ink);
}
.search input:focus-visible {
outline: none;
border-color: var(--brand);
box-shadow: 0 0 0 3px rgba(31, 122, 109, 0.18);
}
/* Table */
.table-wrap { overflow-x: auto; }
.vtable { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
.vtable th {
text-align: left;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
font-weight: 700;
padding: 8px 10px;
border-bottom: 2px solid var(--line);
}
.vtable th.num { text-align: right; }
.vtable td { padding: 11px 10px; border-bottom: 1px solid var(--line); vertical-align: middle; }
.vtable tr { transition: background .12s ease; }
.vtable tbody tr:hover { background: rgba(31, 122, 109, 0.04); }
.vtable tbody tr.is-selected { background: rgba(232, 116, 59, 0.09); box-shadow: inset 3px 0 0 var(--accent); }
.person { display: flex; align-items: center; gap: 10px; cursor: pointer; }
.avatar {
display: grid;
place-items: center;
width: 34px;
height: 34px;
flex: 0 0 auto;
border-radius: 50%;
color: #fff;
font-weight: 700;
font-size: 0.8rem;
}
.person__name { font-weight: 600; }
.person__email { font-size: 0.74rem; color: var(--muted); }
.hours { text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ink); }
.status {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.76rem;
font-weight: 600;
padding: 4px 10px;
border-radius: 999px;
}
.status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.status--active { color: var(--ok); background: rgba(47, 158, 111, 0.12); }
.status--pending { color: var(--warn); background: rgba(217, 138, 43, 0.14); }
.status--inactive { color: var(--muted); background: rgba(122, 115, 104, 0.12); }
.row-actions { display: flex; gap: 6px; justify-content: flex-end; }
.actions-col { text-align: right; }
.empty { text-align: center; color: var(--muted); padding: 28px 0; font-size: 0.9rem; }
/* Shifts */
.shifts { display: flex; flex-direction: column; gap: 12px; }
.shift {
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 13px 14px;
background: var(--bg);
}
.shift__top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.shift__name { font-weight: 700; font-size: 0.92rem; }
.shift__time { font-size: 0.76rem; color: var(--muted); }
.shift__fill { font-size: 0.74rem; color: var(--ink-2); margin: 6px 0 8px; }
.bar { height: 7px; border-radius: 999px; background: rgba(42, 39, 34, 0.08); overflow: hidden; margin-bottom: 10px; }
.bar__fill { height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--brand), var(--accent)); transition: width .5s cubic-bezier(.2,.8,.2,1); }
.shift__slots { display: flex; flex-wrap: wrap; gap: 6px; }
.slot {
font-size: 0.74rem;
font-weight: 600;
padding: 4px 9px;
border-radius: 999px;
border: 1px dashed var(--line-2);
color: var(--muted);
background: var(--surface);
}
.slot--filled { border-style: solid; border-color: var(--brand); color: var(--brand-d); background: rgba(31, 122, 109, 0.08); }
.shift__assign { margin-top: 10px; width: 100%; }
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translateX(-50%) translateY(24px);
background: var(--ink);
color: #fff;
padding: 11px 18px;
border-radius: var(--r-sm);
font-size: 0.86rem;
font-weight: 500;
box-shadow: var(--sh-md);
opacity: 0;
pointer-events: none;
transition: opacity .25s ease, transform .25s ease;
z-index: 50;
max-width: calc(100vw - 32px);
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast.toast--ok { background: var(--brand-d); }
@media (max-width: 920px) {
.grid { grid-template-columns: 1fr; }
.stats { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 520px) {
.app { padding: 0 14px 48px; }
.stats { grid-template-columns: 1fr 1fr; gap: 10px; }
.stat__num { font-size: 1.5rem; }
.person__email { display: none; }
.panel { padding: 16px; border-radius: var(--r-md); }
.vtable { font-size: 0.82rem; }
.topbar__right { width: 100%; justify-content: space-between; }
}(function () {
"use strict";
// --- Fictional data ----------------------------------------------------
var AVATAR_COLORS = ["#1f7a6d", "#e8743b", "#2f9e6f", "#d98a2b", "#155e54", "#cc5d28"];
var volunteers = [
{ id: 1, name: "Maya Okafor", email: "maya.o@hearthlight.org", role: "Kitchen Lead", hours: 142, status: "active" },
{ id: 2, name: "Diego Marín", email: "diego.m@hearthlight.org", role: "Food Runner", hours: 88, status: "active" },
{ id: 3, name: "Priya Nair", email: "priya.n@hearthlight.org", role: "Greeter", hours: 0, status: "pending" },
{ id: 4, name: "Tom Brennan", email: "tom.b@hearthlight.org", role: "Driver", hours: 64, status: "active" },
{ id: 5, name: "Aisha Rahman", email: "aisha.r@hearthlight.org", role: "Coordinator", hours: 211, status: "active" },
{ id: 6, name: "Leo Fontaine", email: "leo.f@hearthlight.org", role: "Dishwashing", hours: 12, status: "inactive" },
{ id: 7, name: "Grace Wu", email: "grace.w@hearthlight.org", role: "Food Runner", hours: 0, status: "pending" },
{ id: 8, name: "Samuel Idris", email: "samuel.i@hearthlight.org", role: "Prep Cook", hours: 97, status: "active" }
];
var shifts = [
{ id: "morning", name: "Morning prep", time: "7:00 – 10:00", capacity: 3, filled: ["Aisha Rahman", "Samuel Idris"] },
{ id: "lunch", name: "Lunch service", time: "11:00 – 14:00", capacity: 4, filled: ["Maya Okafor", "Diego Marín"] },
{ id: "cleanup", name: "Cleanup crew", time: "14:00 – 16:00", capacity: 3, filled: ["Tom Brennan"] }
];
var MEALS_PER_HOUR = 4;
var filter = "all";
var query = "";
var selectedId = null;
var nextId = 9;
// --- Helpers -----------------------------------------------------------
var $ = function (sel) { return document.querySelector(sel); };
function initials(name) {
return name.split(/\s+/).map(function (p) { return p[0]; }).join("").slice(0, 2).toUpperCase();
}
function colorFor(id) { return AVATAR_COLORS[id % AVATAR_COLORS.length]; }
function statusLabel(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
var toastEl = $("#toast");
var toastTimer;
function toast(msg, ok) {
toastEl.textContent = msg;
toastEl.classList.toggle("toast--ok", !!ok);
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2600);
}
// --- Renderers ---------------------------------------------------------
function renderStats() {
var hours = volunteers.reduce(function (a, v) { return a + v.hours; }, 0);
var active = volunteers.filter(function (v) { return v.status === "active"; }).length;
var pending = volunteers.filter(function (v) { return v.status === "pending"; }).length;
animate($("#statHours"), hours);
animate($("#statActive"), active);
animate($("#statPending"), pending);
animate($("#statMeals"), hours * MEALS_PER_HOUR);
}
function animate(el, to) {
var from = parseInt(el.textContent.replace(/\D/g, ""), 10) || 0;
if (from === to) { el.textContent = to.toLocaleString(); return; }
var start = performance.now(), dur = 500;
(function step(now) {
var t = Math.min(1, (now - start) / dur);
var eased = 1 - Math.pow(1 - t, 3);
el.textContent = Math.round(from + (to - from) * eased).toLocaleString();
if (t < 1) requestAnimationFrame(step);
})(start);
}
function visibleVolunteers() {
return volunteers.filter(function (v) {
if (filter !== "all" && v.status !== filter) return false;
if (query) {
var hay = (v.name + " " + v.role).toLowerCase();
if (hay.indexOf(query) === -1) return false;
}
return true;
});
}
function renderTable() {
var body = $("#vbody");
var rows = visibleVolunteers();
body.innerHTML = "";
$("#emptyRow").hidden = rows.length > 0;
rows.forEach(function (v) {
var tr = document.createElement("tr");
if (v.id === selectedId) tr.classList.add("is-selected");
tr.dataset.id = v.id;
var actions = v.status === "pending"
? '<button class="btn btn--sm btn--ok" data-act="approve">Approve</button>'
+ '<button class="btn btn--sm btn--ghost" data-act="decline">Decline</button>'
: '<button class="btn btn--sm btn--ghost" data-act="log">+ Log hrs</button>'
+ '<button class="btn btn--sm btn--ghost" data-act="select">'
+ (v.id === selectedId ? "Selected" : "Select") + '</button>';
tr.innerHTML =
'<td><div class="person" data-act="select">'
+ '<span class="avatar" style="background:' + colorFor(v.id) + '">' + initials(v.name) + '</span>'
+ '<span><span class="person__name">' + v.name + '</span><br>'
+ '<span class="person__email">' + v.email + '</span></span></div></td>'
+ '<td>' + v.role + '</td>'
+ '<td class="hours">' + v.hours + '</td>'
+ '<td><span class="status status--' + v.status + '">' + statusLabel(v.status) + '</span></td>'
+ '<td class="actions-col"><div class="row-actions">' + actions + '</div></td>';
body.appendChild(tr);
});
}
function renderShifts() {
var wrap = $("#shifts");
wrap.innerHTML = "";
shifts.forEach(function (sh) {
var pct = Math.round((sh.filled.length / sh.capacity) * 100);
var open = sh.capacity - sh.filled.length;
var card = document.createElement("div");
card.className = "shift";
card.dataset.id = sh.id;
var slots = sh.filled.map(function (n) {
return '<span class="slot slot--filled">' + n.split(" ")[0] + "</span>";
});
for (var i = 0; i < open; i++) slots.push('<span class="slot">open</span>');
var canAssign = selectedId !== null && open > 0;
var assignLabel = open === 0 ? "Shift full"
: (selectedId === null ? "Select a volunteer first" : "Assign selected →");
card.innerHTML =
'<div class="shift__top"><span class="shift__name">' + sh.name + '</span>'
+ '<span class="shift__time">' + sh.time + '</span></div>'
+ '<p class="shift__fill">' + sh.filled.length + ' / ' + sh.capacity + ' filled · '
+ open + ' open</p>'
+ '<div class="bar"><span class="bar__fill" style="width:' + pct + '%"></span></div>'
+ '<div class="shift__slots">' + slots.join("") + '</div>'
+ '<button class="btn btn--sm shift__assign" data-assign="' + sh.id + '"'
+ (canAssign ? "" : " disabled") + '>' + assignLabel + '</button>';
wrap.appendChild(card);
});
}
function renderAll() {
renderStats();
renderTable();
renderShifts();
}
// --- Actions -----------------------------------------------------------
function findById(id) {
for (var i = 0; i < volunteers.length; i++) if (volunteers[i].id === id) return volunteers[i];
return null;
}
function approve(v) { v.status = "active"; toast(v.name + " approved — welcome aboard!", true); renderAll(); }
function decline(v) {
volunteers = volunteers.filter(function (x) { return x.id !== v.id; });
if (selectedId === v.id) selectedId = null;
toast(v.name + "'s application was declined.");
renderAll();
}
function logHours(v) {
v.hours += 3;
toast("+3 hours logged for " + v.name + ".", true);
renderAll();
}
function select(v) {
if (v.status === "pending") { toast("Approve " + v.name + " before assigning shifts."); return; }
selectedId = (selectedId === v.id) ? null : v.id;
renderTable();
renderShifts();
}
function assign(shiftId) {
var v = findById(selectedId);
if (!v) return;
var sh = shifts.filter(function (s) { return s.id === shiftId; })[0];
if (!sh || sh.filled.length >= sh.capacity) return;
if (sh.filled.indexOf(v.name) !== -1) { toast(v.name + " is already on this shift."); return; }
sh.filled.push(v.name);
if (v.status === "inactive") v.status = "active";
toast(v.name + " assigned to " + sh.name + ".", true);
selectedId = null;
renderAll();
}
function addVolunteer() {
var name = (window.prompt("New volunteer name:", "") || "").trim();
if (!name) return;
var role = (window.prompt("Role (e.g. Food Runner):", "Greeter") || "Greeter").trim();
var first = name.toLowerCase().split(/\s+/)[0];
volunteers.unshift({
id: nextId++, name: name, email: first + "@hearthlight.org",
role: role, hours: 0, status: "pending"
});
filter = "all";
syncChips();
toast(name + " added — pending approval.", true);
renderAll();
}
// --- Events ------------------------------------------------------------
function syncChips() {
document.querySelectorAll(".chip").forEach(function (c) {
c.classList.toggle("is-active", c.dataset.filter === filter);
});
}
document.querySelector(".filters").addEventListener("click", function (e) {
var chip = e.target.closest(".chip");
if (!chip) return;
filter = chip.dataset.filter;
syncChips();
renderTable();
});
$("#search").addEventListener("input", function (e) {
query = e.target.value.trim().toLowerCase();
renderTable();
});
$("#vbody").addEventListener("click", function (e) {
var btn = e.target.closest("[data-act]");
var tr = e.target.closest("tr");
if (!tr) return;
var v = findById(parseInt(tr.dataset.id, 10));
if (!v) return;
var act = btn ? btn.dataset.act : null;
if (act === "approve") approve(v);
else if (act === "decline") decline(v);
else if (act === "log") logHours(v);
else if (act === "select") select(v);
});
$("#shifts").addEventListener("click", function (e) {
var btn = e.target.closest("[data-assign]");
if (!btn || btn.disabled) return;
assign(btn.dataset.assign);
});
$("#addBtn").addEventListener("click", addVolunteer);
renderAll();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hearthlight Outreach — Volunteer Manager</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=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app" role="application" aria-label="Volunteer manager dashboard">
<header class="topbar">
<div class="brand">
<span class="brand__mark" aria-hidden="true">♥</span>
<div>
<p class="brand__name">Hearthlight Outreach</p>
<p class="brand__sub">Volunteer Manager</p>
</div>
</div>
<div class="topbar__right">
<span class="badge badge--trust" title="Registered 501(c)(3) nonprofit">✓ Registered Charity</span>
<button class="btn btn--accent" id="addBtn" type="button">+ Add Volunteer</button>
</div>
</header>
<main class="layout">
<!-- Impact / hours rollup -->
<section class="stats" aria-label="Volunteer impact summary">
<article class="stat">
<p class="stat__num" id="statHours">0</p>
<p class="stat__label">Hours logged this season</p>
</article>
<article class="stat">
<p class="stat__num" id="statActive">0</p>
<p class="stat__label">Active volunteers</p>
</article>
<article class="stat">
<p class="stat__num" id="statPending">0</p>
<p class="stat__label">Awaiting approval</p>
</article>
<article class="stat stat--impact">
<p class="stat__num" id="statMeals">0</p>
<p class="stat__label">Meals served (≈4 / hour)</p>
</article>
</section>
<div class="grid">
<!-- Volunteers table -->
<section class="panel" aria-labelledby="rosterHead">
<div class="panel__head">
<h2 class="panel__title" id="rosterHead">Volunteer roster</h2>
<div class="filters" role="group" aria-label="Filter by status">
<button class="chip is-active" data-filter="all" type="button">All</button>
<button class="chip" data-filter="active" type="button">Active</button>
<button class="chip" data-filter="pending" type="button">Pending</button>
<button class="chip" data-filter="inactive" type="button">Inactive</button>
</div>
</div>
<div class="search">
<input type="search" id="search" placeholder="Search by name or role…" aria-label="Search volunteers" />
</div>
<div class="table-wrap">
<table class="vtable">
<thead>
<tr>
<th scope="col">Volunteer</th>
<th scope="col">Role</th>
<th scope="col" class="num">Hours</th>
<th scope="col">Status</th>
<th scope="col" class="actions-col">Actions</th>
</tr>
</thead>
<tbody id="vbody"><!-- rows injected --></tbody>
</table>
<p class="empty" id="emptyRow" hidden>No volunteers match your filter.</p>
</div>
</section>
<!-- Shift roster -->
<aside class="panel" aria-labelledby="shiftHead">
<div class="panel__head">
<h2 class="panel__title" id="shiftHead">Saturday shift roster</h2>
<span class="muted-pill">Apr 27 · Riverside Kitchen</span>
</div>
<p class="hint">Select a volunteer in the roster, then click <strong>Assign</strong> on an open slot.</p>
<div class="shifts" id="shifts"><!-- shift cards injected --></div>
</aside>
</div>
</main>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Volunteer Manager
A coordinator-facing dashboard for the fictional Hearthlight Outreach soup kitchen. The header carries the mission brand, a registered-charity trust badge and a prominent accent CTA to add a volunteer. A row of impact stats rolls up logged hours, active and pending volunteers, and an estimated meals-served figure — each animating with a count-up whenever the data changes, reinforcing transparency.
The left panel is a searchable, filterable volunteer roster. Status chips switch between all, active, pending and inactive helpers, and the search box matches on name or role. Pending applicants get Approve and Decline buttons; active volunteers can log hours or be selected. The right panel is the Saturday shift roster — three time blocks with fill thermometers, filled and open slot pills, and an Assign button per shift.
The interaction loop is select-then-assign: pick a volunteer in the table, then click Assign on an open slot to place them on a shift, which updates the thermometer, the slot pills and the rollup stats together. Approving an applicant moves them into the active count, logging hours bumps both their total and the meals estimate, and every action surfaces a small toast for feedback. Everything is keyboard-usable, responsive down to ~360px, and built with vanilla JS — no frameworks.
Illustrative UI only — fictional organization, not a real charity or donation system.