Nonprofit — Campaign Manager
A warm, polished back-office campaign manager for a fictional charity, showing fundraising drives as cards or a sortable table with status badges, animated progress bars, goal-versus-raised totals, and date ranges. Staff filter by status, search by name, toggle the card and table views, and pause or activate drives in one click. A slide-in drawer creates or edits a campaign with goal, dates, status, a short story, and repeatable giving tiers, while impact numbers and a registered-charity badge reinforce transparency.
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 1px 3px rgba(42, 39, 34, 0.05);
--sh-md: 0 6px 18px rgba(42, 39, 34, 0.08), 0 2px 6px rgba(42, 39, 34, 0.05);
--sh-lg: 0 22px 60px rgba(42, 39, 34, 0.22);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3, .brand-name, .impact-card strong, .org-purse strong {
font-family: "Fraunces", Georgia, serif;
}
button { font-family: inherit; cursor: pointer; }
input, select, textarea { font-family: inherit; }
/* ---------- Layout ---------- */
.app { display: grid; grid-template-columns: 244px 1fr; min-height: 100vh; }
.sidebar {
background: linear-gradient(180deg, #fffdfa, #f5efe7);
border-right: 1px solid var(--line);
padding: 22px 18px;
display: flex; flex-direction: column; gap: 26px;
position: sticky; top: 0; height: 100vh;
}
.brand { display: flex; align-items: center; gap: 11px; }
.brand-mark {
width: 40px; height: 40px; border-radius: 12px; flex: none;
display: grid; place-items: center; color: #fff; font-weight: 700;
font-family: "Fraunces", serif; letter-spacing: .5px;
background: linear-gradient(145deg, var(--brand), var(--brand-d));
box-shadow: var(--sh-sm);
}
.brand-name { font-weight: 600; font-size: 1.05rem; line-height: 1.15; display: flex; flex-direction: column; }
.brand-name small { font-family: "Inter", sans-serif; font-size: .68rem; color: var(--muted); font-weight: 500; letter-spacing: .4px; text-transform: uppercase; }
.side-nav { display: flex; flex-direction: column; gap: 3px; }
.side-nav a {
display: flex; align-items: center; gap: 11px;
padding: 10px 12px; border-radius: var(--r-sm);
color: var(--ink-2); text-decoration: none; font-weight: 500; font-size: .92rem;
transition: background .15s, color .15s;
}
.side-nav a .ico { width: 18px; text-align: center; opacity: .8; }
.side-nav a:hover { background: rgba(31, 122, 109, 0.08); color: var(--brand-d); }
.side-nav a.active { background: var(--brand); color: #fff; box-shadow: var(--sh-sm); }
.side-nav a.active .ico { opacity: 1; }
.side-trust {
margin-top: auto; font-size: .76rem; color: var(--ink-2);
background: rgba(47, 158, 111, 0.08);
border: 1px solid rgba(47, 158, 111, 0.22);
padding: 11px 12px; border-radius: var(--r-sm); line-height: 1.45;
}
.side-trust small { display: block; color: var(--muted); margin-top: 3px; }
.badge-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 4px; }
.badge-dot.ok { background: var(--ok); box-shadow: 0 0 0 3px rgba(47, 158, 111, 0.18); }
.main { padding: 26px clamp(18px, 3vw, 40px) 56px; min-width: 0; }
/* ---------- Topbar ---------- */
.topbar { display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; flex-wrap: wrap; margin-bottom: 22px; }
.eyebrow { margin: 0; font-size: .72rem; font-weight: 600; letter-spacing: 1.4px; text-transform: uppercase; color: var(--brand); }
.topbar h1 { margin: 2px 0 0; font-size: clamp(1.6rem, 3vw, 2.05rem); font-weight: 600; letter-spacing: -.01em; }
.topbar-actions { display: flex; align-items: center; gap: 14px; }
.org-purse {
text-align: right; padding: 6px 14px; border-radius: var(--r-md);
background: var(--surface); border: 1px solid var(--line); box-shadow: var(--sh-sm);
}
.org-purse small { display: block; font-size: .68rem; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; }
.org-purse strong { font-size: 1.25rem; color: var(--brand-d); }
/* ---------- Buttons ---------- */
.btn {
border: 1px solid transparent; border-radius: 999px; padding: 10px 18px;
font-weight: 600; font-size: .9rem; transition: transform .12s, box-shadow .15s, background .15s;
display: inline-flex; align-items: center; gap: 6px;
}
.btn:active { transform: translateY(1px); }
.btn-accent { background: linear-gradient(145deg, var(--accent), var(--accent-d)); color: #fff; box-shadow: var(--sh-sm); }
.btn-accent:hover { box-shadow: 0 8px 20px rgba(232, 116, 59, 0.34); }
.btn-ghost { background: var(--surface); border-color: var(--line-2); color: var(--ink-2); }
.btn-ghost:hover { border-color: var(--brand); color: var(--brand-d); background: rgba(31, 122, 109, 0.05); }
.btn-sm { padding: 7px 13px; font-size: .82rem; }
.icon-btn {
border: 1px solid var(--line); background: var(--surface); border-radius: var(--r-sm);
width: 34px; height: 34px; display: grid; place-items: center; color: var(--ink-2); font-size: .95rem;
}
.icon-btn:hover { border-color: var(--danger); color: var(--danger); }
/* ---------- Impact ---------- */
.impact { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 22px; }
.impact-card {
background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-md);
padding: 14px 16px; display: flex; align-items: center; gap: 13px; box-shadow: var(--sh-sm);
}
.impact-ico {
width: 44px; height: 44px; flex: none; border-radius: 12px; display: grid; place-items: center; font-size: 1.3rem;
background: linear-gradient(145deg, rgba(31, 122, 109, 0.12), rgba(232, 116, 59, 0.12));
}
.impact-card strong { display: block; font-size: 1.4rem; line-height: 1.1; color: var(--ink); }
.impact-card small { color: var(--muted); font-size: .78rem; }
/* ---------- Toolbar ---------- */
.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; margin-bottom: 18px; }
.filters { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
border: 1px solid var(--line-2); background: var(--surface); color: var(--ink-2);
border-radius: 999px; padding: 7px 14px; font-weight: 600; font-size: .85rem;
display: inline-flex; align-items: center; gap: 7px; transition: all .15s;
}
.chip:hover { border-color: var(--brand); color: var(--brand-d); }
.chip.active { background: var(--brand); border-color: var(--brand); color: #fff; box-shadow: var(--sh-sm); }
.chip .count {
background: rgba(42, 39, 34, 0.08); color: inherit; border-radius: 999px;
min-width: 20px; padding: 0 6px; font-size: .72rem; text-align: center;
}
.chip.active .count { background: rgba(255, 255, 255, 0.25); }
.tools-right { display: flex; align-items: center; gap: 10px; }
.search {
display: flex; align-items: center; gap: 7px; background: var(--surface);
border: 1px solid var(--line-2); border-radius: 999px; padding: 6px 14px; color: var(--muted);
}
.search:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px rgba(31, 122, 109, 0.15); }
.search input { border: none; outline: none; background: transparent; font-size: .88rem; color: var(--ink); width: 160px; }
.view-toggle { display: flex; background: var(--surface); border: 1px solid var(--line-2); border-radius: var(--r-sm); overflow: hidden; }
.vt { border: none; background: transparent; padding: 7px 11px; color: var(--muted); font-size: 1rem; }
.vt.active { background: var(--brand); color: #fff; }
/* ---------- Cards ---------- */
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
.ccard {
background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
overflow: hidden; box-shadow: var(--sh-sm); display: flex; flex-direction: column;
transition: transform .16s, box-shadow .16s; animation: pop .3s ease both;
}
.ccard:hover { transform: translateY(-3px); box-shadow: var(--sh-md); }
@keyframes pop { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.ccard-photo { height: 92px; position: relative; display: flex; align-items: flex-end; padding: 10px 12px; }
.ccard-photo .cap { color: #fff; font-size: .74rem; font-weight: 600; text-shadow: 0 1px 6px rgba(0,0,0,.4); }
.ccard-body { padding: 14px 16px 16px; display: flex; flex-direction: column; gap: 11px; flex: 1; }
.ccard h3 { margin: 0; font-size: 1.12rem; font-weight: 600; letter-spacing: -.01em; }
.ccard .story { margin: -3px 0 0; font-size: .84rem; color: var(--ink-2); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.status {
font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .5px;
padding: 3px 9px; border-radius: 999px; display: inline-flex; align-items: center; gap: 5px;
}
.status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.status.active { color: var(--ok); background: rgba(47, 158, 111, 0.12); }
.status.draft { color: var(--muted); background: rgba(122, 115, 104, 0.12); }
.status.paused { color: var(--warn); background: rgba(217, 138, 43, 0.14); }
.status.ended { color: var(--ink-2); background: rgba(42, 39, 34, 0.08); }
.prog { display: flex; flex-direction: column; gap: 6px; }
.prog-bar { height: 9px; border-radius: 999px; background: rgba(42, 39, 34, 0.08); overflow: hidden; }
.prog-fill {
height: 100%; border-radius: 999px; width: 0;
background: linear-gradient(90deg, var(--brand), #34a08f);
transition: width .9s cubic-bezier(.2,.8,.2,1);
}
.ccard.over .prog-fill { background: linear-gradient(90deg, var(--accent), #f4a06f); }
.prog-meta { display: flex; justify-content: space-between; font-size: .8rem; }
.prog-meta .raised { font-weight: 700; color: var(--brand-d); }
.prog-meta .goal { color: var(--muted); }
.prog-pct { font-weight: 700; color: var(--ink); }
.ccard-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: auto; padding-top: 4px; }
.dates { font-size: .76rem; color: var(--muted); }
.card-actions { display: flex; gap: 6px; }
.mini {
border: 1px solid var(--line-2); background: var(--surface); border-radius: var(--r-sm);
padding: 6px 11px; font-size: .8rem; font-weight: 600; color: var(--ink-2); transition: all .14s;
}
.mini:hover { border-color: var(--brand); color: var(--brand-d); }
.mini.toggle:hover { border-color: var(--warn); color: var(--warn); }
/* ---------- Table ---------- */
.table-wrap { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); overflow: hidden; box-shadow: var(--sh-sm); }
.table { width: 100%; border-collapse: collapse; font-size: .88rem; }
.table th { text-align: left; padding: 13px 16px; font-size: .72rem; text-transform: uppercase; letter-spacing: .6px; color: var(--muted); background: #fbf8f3; border-bottom: 1px solid var(--line); }
.table td { padding: 13px 16px; border-bottom: 1px solid var(--line); vertical-align: middle; }
.table tr:last-child td { border-bottom: none; }
.table tbody tr:hover { background: rgba(31, 122, 109, 0.04); }
.table .num { text-align: right; }
.table .t-name { font-weight: 600; }
.table .t-prog { min-width: 130px; }
.table .t-prog .prog-bar { height: 7px; margin-bottom: 4px; }
.table .t-prog small { color: var(--muted); font-size: .72rem; }
.table .t-actions { white-space: nowrap; text-align: right; }
.empty { text-align: center; color: var(--muted); padding: 48px 0; font-size: .95rem; }
/* ---------- Drawer ---------- */
.drawer-scrim { position: fixed; inset: 0; background: rgba(42, 39, 34, 0.42); backdrop-filter: blur(2px); z-index: 40; animation: fade .2s ease; }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.drawer {
position: fixed; top: 0; right: 0; height: 100vh; width: min(440px, 94vw); z-index: 50;
background: var(--bg); box-shadow: var(--sh-lg); display: flex; flex-direction: column;
animation: slide .26s cubic-bezier(.2,.8,.2,1);
}
@keyframes slide { from { transform: translateX(100%); } to { transform: none; } }
.drawer-head { display: flex; align-items: center; justify-content: space-between; padding: 18px 22px; border-bottom: 1px solid var(--line); background: var(--surface); }
.drawer-head h2 { margin: 0; font-size: 1.3rem; font-weight: 600; }
.drawer-body { padding: 20px 22px; overflow-y: auto; display: flex; flex-direction: column; gap: 15px; }
.field { display: flex; flex-direction: column; gap: 5px; }
.field > span { font-size: .8rem; font-weight: 600; color: var(--ink-2); }
.field input, .field select, .field textarea {
border: 1px solid var(--line-2); border-radius: var(--r-sm); padding: 10px 12px;
font-size: .9rem; color: var(--ink); background: var(--surface); transition: border .15s, box-shadow .15s;
}
.field input:focus, .field select:focus, .field textarea:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(31, 122, 109, 0.15); }
.field textarea { resize: vertical; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.tiers { border: 1px solid var(--line); border-radius: var(--r-md); padding: 14px; margin: 0; }
.tiers legend { font-size: .82rem; font-weight: 700; color: var(--brand-d); padding: 0 6px; }
.tier-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; }
.tier-row { display: grid; grid-template-columns: 86px 1fr auto; gap: 8px; align-items: center; }
.tier-row input { border: 1px solid var(--line-2); border-radius: var(--r-sm); padding: 8px 10px; font-size: .85rem; }
.tier-row input:focus { outline: none; border-color: var(--brand); }
.tier-del { border: none; background: rgba(212, 80, 62, 0.1); color: var(--danger); border-radius: var(--r-sm); width: 32px; height: 32px; font-size: .9rem; }
.tier-del:hover { background: var(--danger); color: #fff; }
.drawer-foot { display: flex; justify-content: flex-end; gap: 10px; padding-top: 6px; margin-top: 4px; border-top: 1px solid var(--line); padding: 14px 0 4px; }
/* ---------- Toast ---------- */
.toast-wrap { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); z-index: 60; display: flex; flex-direction: column; gap: 8px; align-items: center; }
.toast {
background: var(--ink); color: #fff; padding: 11px 18px; border-radius: 999px;
font-size: .87rem; font-weight: 500; box-shadow: var(--sh-md); display: flex; align-items: center; gap: 8px;
animation: toastIn .25s ease both;
}
.toast::before { content: "✓"; color: #6fd6b0; font-weight: 700; }
.toast.warn::before { content: "⏸"; color: #f3c47a; }
@keyframes toastIn { from { opacity: 0; transform: translateY(12px) scale(.96); } to { opacity: 1; transform: none; } }
.toast.out { animation: toastOut .3s ease forwards; }
@keyframes toastOut { to { opacity: 0; transform: translateY(10px); } }
[hidden] { display: none !important; }
/* ---------- Responsive ---------- */
@media (max-width: 920px) {
.app { grid-template-columns: 1fr; }
.sidebar { position: static; height: auto; flex-direction: row; align-items: center; flex-wrap: wrap; gap: 14px; }
.side-nav { flex-direction: row; flex-wrap: wrap; }
.side-trust { margin: 0; }
.impact { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 520px) {
.main { padding: 18px 14px 44px; }
.topbar-actions { width: 100%; justify-content: space-between; }
.impact { grid-template-columns: 1fr 1fr; gap: 10px; }
.impact-card strong { font-size: 1.2rem; }
.toolbar { flex-direction: column; align-items: stretch; }
.tools-right { justify-content: space-between; }
.search input { width: 100%; }
.grid { grid-template-columns: 1fr; }
.field-row { grid-template-columns: 1fr; }
.table th:nth-child(4), .table td:nth-child(4),
.table th:nth-child(6), .table td:nth-child(6) { display: none; }
}(function () {
"use strict";
/* ---------- Seed data (fictional) ---------- */
var GRADS = [
"linear-gradient(135deg,#1f7a6d,#34a08f)",
"linear-gradient(135deg,#e8743b,#f4a06f)",
"linear-gradient(135deg,#155e54,#2f9e6f)",
"linear-gradient(135deg,#cc5d28,#e8743b)",
"linear-gradient(135deg,#3a6f8f,#5fa0bf)",
"linear-gradient(135deg,#8a5a9e,#b489c7)"
];
var campaigns = [
{
id: id(), name: "Clean Water Now: 12 Wells for Loma Verde",
story: "Drilling twelve solar-pumped wells to bring safe drinking water to 3,200 villagers.",
goal: 60000, raised: 47820, status: "active",
start: "2026-04-01", end: "2026-07-15", cap: "Loma Verde, dry season 2026", g: 0,
tiers: [{ amt: 25, label: "A week of clean water" }, { amt: 120, label: "A family's year" }, { amt: 500, label: "Sponsor a well plaque" }]
},
{
id: id(), name: "Warm Meals Winter Drive",
story: "Serving hot dinners and groceries to 900 families through the cold months.",
goal: 35000, raised: 35920, status: "active",
start: "2026-05-10", end: "2026-08-30", cap: "Community kitchen, Riverside", g: 1,
tiers: [{ amt: 15, label: "Five warm meals" }, { amt: 75, label: "A family box" }, { amt: 250, label: "Stock the pantry" }]
},
{
id: id(), name: "Bright Scholars Fund 2026",
story: "Full-year scholarships covering tuition, books, and transport for 40 first-generation students.",
goal: 90000, raised: 31200, status: "active",
start: "2026-03-15", end: "2026-09-01", cap: "Mentorship cohort, spring", g: 2,
tiers: [{ amt: 50, label: "Books for a term" }, { amt: 300, label: "A month of tuition" }, { amt: 2000, label: "Name a scholarship" }]
},
{
id: id(), name: "Mobile Health Clinic Expansion",
story: "Outfitting a second mobile clinic van to reach four remote highland districts.",
goal: 120000, raised: 18450, status: "paused",
start: "2026-06-01", end: "2026-12-15", cap: "Highland outreach route", g: 4,
tiers: [{ amt: 40, label: "A checkup" }, { amt: 200, label: "Vaccine kit" }, { amt: 1500, label: "A clinic day" }]
},
{
id: id(), name: "Reforest the Ridge",
story: "Planting 25,000 native saplings with local youth crews to restore eroded hillsides.",
goal: 28000, raised: 0, status: "draft",
start: "2026-08-01", end: "2026-11-30", cap: "Ridge restoration, autumn", g: 2,
tiers: [{ amt: 10, label: "Ten saplings" }, { amt: 60, label: "A grove" }]
},
{
id: id(), name: "Spring Gala: Lights of Hope",
story: "Our flagship dinner that funded shelters, tutoring, and emergency relief last spring.",
goal: 75000, raised: 81340, status: "ended",
start: "2026-01-20", end: "2026-04-12", cap: "Annual benefit gala", g: 5,
tiers: [{ amt: 150, label: "A seat" }, { amt: 1500, label: "A table of ten" }]
}
];
var state = { filter: "all", view: "cards", query: "", editingId: null };
/* ---------- Helpers ---------- */
function id() { return "c" + Math.random().toString(36).slice(2, 9); }
function money(n) { return "$" + Math.round(n).toLocaleString("en-US"); }
function pct(c) { return c.goal > 0 ? Math.round((c.raised / c.goal) * 100) : 0; }
function fmtDate(s) {
if (!s) return "";
var d = new Date(s + "T00:00:00");
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}
function fmtRange(c) { return fmtDate(c.start) + " – " + fmtDate(c.end); }
function esc(s) { return String(s).replace(/[&<>"']/g, function (m) { return ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[m]; }); }
/* ---------- Toast ---------- */
var toastWrap = document.getElementById("toastWrap");
function toast(msg, kind) {
var t = document.createElement("div");
t.className = "toast" + (kind ? " " + kind : "");
t.textContent = msg;
toastWrap.appendChild(t);
setTimeout(function () {
t.classList.add("out");
t.addEventListener("animationend", function () { t.remove(); });
}, 2600);
}
/* ---------- Rendering ---------- */
var cardGrid = document.getElementById("cardGrid");
var tableBody = document.getElementById("tableBody");
var tableWrap = document.getElementById("tableWrap");
var emptyState = document.getElementById("emptyState");
function filtered() {
var q = state.query.trim().toLowerCase();
return campaigns.filter(function (c) {
if (state.filter !== "all" && c.status !== state.filter) return false;
if (q && c.name.toLowerCase().indexOf(q) === -1 && c.story.toLowerCase().indexOf(q) === -1) return false;
return true;
});
}
function statusLabel(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
function cardHTML(c) {
var p = pct(c);
var over = c.raised >= c.goal && c.goal > 0;
return '<article class="ccard' + (over ? " over" : "") + '" data-id="' + c.id + '">' +
'<div class="ccard-photo" style="background:' + GRADS[c.g % GRADS.length] + '">' +
'<span class="cap">' + esc(c.cap) + '</span>' +
'</div>' +
'<div class="ccard-body">' +
'<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:8px">' +
'<h3>' + esc(c.name) + '</h3>' +
'<span class="status ' + c.status + '">' + statusLabel(c.status) + '</span>' +
'</div>' +
'<p class="story">' + esc(c.story) + '</p>' +
'<div class="prog">' +
'<div class="prog-bar"><div class="prog-fill" style="width:' + Math.min(p, 100) + '%"></div></div>' +
'<div class="prog-meta">' +
'<span><span class="raised">' + money(c.raised) + '</span> <span class="goal">/ ' + money(c.goal) + '</span></span>' +
'<span class="prog-pct">' + p + '%' + (over ? " 🎉" : "") + '</span>' +
'</div>' +
'</div>' +
'<div class="ccard-foot">' +
'<span class="dates">📅 ' + fmtRange(c) + '</span>' +
'<div class="card-actions">' +
'<button class="mini toggle" data-act="toggle">' + (c.status === "active" ? "Pause" : "Activate") + '</button>' +
'<button class="mini" data-act="edit">Edit</button>' +
'</div>' +
'</div>' +
'</div>' +
'</article>';
}
function rowHTML(c) {
var p = pct(c);
return '<tr data-id="' + c.id + '">' +
'<td class="t-name">' + esc(c.name) + '</td>' +
'<td><span class="status ' + c.status + '">' + statusLabel(c.status) + '</span></td>' +
'<td class="t-prog"><div class="prog-bar"><div class="prog-fill" style="width:' + Math.min(p, 100) + '%"></div></div><small>' + p + '% funded</small></td>' +
'<td class="num">' + money(c.goal) + '</td>' +
'<td class="num"><strong>' + money(c.raised) + '</strong></td>' +
'<td>' + fmtRange(c) + '</td>' +
'<td class="t-actions">' +
'<button class="mini toggle" data-act="toggle">' + (c.status === "active" ? "Pause" : "Activate") + '</button> ' +
'<button class="mini" data-act="edit">Edit</button>' +
'</td>' +
'</tr>';
}
function render() {
var list = filtered();
// counts (ignore search, only status buckets)
var byStatus = { all: campaigns.length, active: 0, draft: 0, paused: 0, ended: 0 };
campaigns.forEach(function (c) { byStatus[c.status]++; });
Object.keys(byStatus).forEach(function (k) {
var el = document.querySelector('[data-count="' + k + '"]');
if (el) el.textContent = byStatus[k];
});
if (state.view === "cards") {
cardGrid.hidden = false; tableWrap.hidden = true;
cardGrid.innerHTML = list.map(cardHTML).join("");
} else {
cardGrid.hidden = true; tableWrap.hidden = false;
tableBody.innerHTML = list.map(rowHTML).join("");
}
emptyState.hidden = list.length > 0;
// animate progress fills from 0
requestAnimationFrame(function () {
document.querySelectorAll(".prog-fill").forEach(function (f) {
var w = f.style.width; f.style.width = "0"; void f.offsetWidth; f.style.width = w;
});
});
updatePurse();
}
function updatePurse() {
var total = campaigns.reduce(function (s, c) {
return s + (c.status === "active" || c.status === "ended" ? c.raised : 0);
}, 0);
var el = document.getElementById("purseTotal");
countUp(el, total);
}
function countUp(el, target) {
var start = parseInt((el.textContent || "0").replace(/[^0-9]/g, ""), 10) || 0;
var t0 = performance.now(), dur = 600;
function step(now) {
var k = Math.min((now - t0) / dur, 1);
var v = Math.round(start + (target - start) * (1 - Math.pow(1 - k, 3)));
el.textContent = money(v);
if (k < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
/* ---------- Filters / search / view ---------- */
document.querySelectorAll(".chip").forEach(function (chip) {
chip.addEventListener("click", function () {
document.querySelectorAll(".chip").forEach(function (c) { c.classList.remove("active"); c.setAttribute("aria-selected", "false"); });
chip.classList.add("active"); chip.setAttribute("aria-selected", "true");
state.filter = chip.dataset.filter;
render();
});
});
document.getElementById("searchInput").addEventListener("input", function (e) {
state.query = e.target.value; render();
});
document.querySelectorAll(".vt").forEach(function (vt) {
vt.addEventListener("click", function () {
document.querySelectorAll(".vt").forEach(function (v) { v.classList.remove("active"); v.setAttribute("aria-pressed", "false"); });
vt.classList.add("active"); vt.setAttribute("aria-pressed", "true");
state.view = vt.dataset.view; render();
});
});
/* ---------- Card / row actions (delegated) ---------- */
function onAction(e) {
var btn = e.target.closest("[data-act]");
if (!btn) return;
var host = btn.closest("[data-id]");
if (!host) return;
var c = campaigns.find(function (x) { return x.id === host.dataset.id; });
if (!c) return;
if (btn.dataset.act === "toggle") toggleStatus(c);
else if (btn.dataset.act === "edit") openDrawer(c);
}
cardGrid.addEventListener("click", onAction);
tableBody.addEventListener("click", onAction);
function toggleStatus(c) {
if (c.status === "active") { c.status = "paused"; toast(c.name.split(":")[0].trim() + " paused", "warn"); }
else { c.status = "active"; toast(c.name.split(":")[0].trim() + " is now active"); }
render();
}
/* ---------- Drawer ---------- */
var scrim = document.getElementById("scrim");
var drawer = document.getElementById("drawer");
var form = document.getElementById("campaignForm");
var tierList = document.getElementById("tierList");
var lastFocus = null;
function tierRow(amt, label) {
var row = document.createElement("div");
row.className = "tier-row";
row.innerHTML =
'<input type="number" min="1" step="1" placeholder="$" value="' + (amt != null ? amt : "") + '" aria-label="Tier amount" />' +
'<input type="text" placeholder="What it funds" value="' + esc(label || "") + '" aria-label="Tier label" />' +
'<button type="button" class="tier-del" aria-label="Remove tier">✕</button>';
row.querySelector(".tier-del").addEventListener("click", function () { row.remove(); });
return row;
}
document.getElementById("addTier").addEventListener("click", function () {
tierList.appendChild(tierRow("", ""));
});
function openDrawer(c) {
lastFocus = document.activeElement;
state.editingId = c ? c.id : null;
document.getElementById("drawerTitle").textContent = c ? "Edit campaign" : "New campaign";
form.name.value = c ? c.name : "";
form.story.value = c ? c.story : "";
form.goal.value = c ? c.goal : "";
form.raised.value = c ? c.raised : "";
form.start.value = c ? c.start : "";
form.end.value = c ? c.end : "";
form.status.value = c ? c.status : "draft";
tierList.innerHTML = "";
var tiers = c && c.tiers ? c.tiers : [{ amt: 25, label: "" }, { amt: 100, label: "" }];
tiers.forEach(function (t) { tierList.appendChild(tierRow(t.amt, t.label)); });
scrim.hidden = false; drawer.hidden = false;
setTimeout(function () { form.name.focus(); }, 60);
document.addEventListener("keydown", onEsc);
}
function closeDrawer() {
scrim.hidden = true; drawer.hidden = true;
document.removeEventListener("keydown", onEsc);
if (lastFocus) lastFocus.focus();
}
function onEsc(e) { if (e.key === "Escape") closeDrawer(); }
document.getElementById("newBtn").addEventListener("click", function () { openDrawer(null); });
document.getElementById("closeDrawer").addEventListener("click", closeDrawer);
document.getElementById("cancelBtn").addEventListener("click", closeDrawer);
scrim.addEventListener("click", closeDrawer);
form.addEventListener("submit", function (e) {
e.preventDefault();
var tiers = [];
tierList.querySelectorAll(".tier-row").forEach(function (r) {
var ins = r.querySelectorAll("input");
var amt = parseFloat(ins[0].value);
if (!isNaN(amt) && amt > 0) tiers.push({ amt: amt, label: ins[1].value.trim() });
});
var data = {
name: form.name.value.trim(),
story: form.story.value.trim() || "A new campaign for our community.",
goal: Math.max(parseFloat(form.goal.value) || 0, 0),
raised: Math.max(parseFloat(form.raised.value) || 0, 0),
start: form.start.value, end: form.end.value,
status: form.status.value, tiers: tiers
};
if (state.editingId) {
var c = campaigns.find(function (x) { return x.id === state.editingId; });
Object.assign(c, data);
toast("Campaign updated");
} else {
data.id = id();
data.cap = "New campaign · " + new Date().getFullYear();
data.g = campaigns.length;
campaigns.unshift(data);
toast("Campaign created");
}
closeDrawer();
render();
});
/* ---------- Go ---------- */
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bright Futures Foundation — Campaign 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,[email protected],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">
<!-- Sidebar -->
<aside class="sidebar" aria-label="Primary">
<div class="brand">
<span class="brand-mark" aria-hidden="true">BF</span>
<span class="brand-name">Bright Futures<small>Foundation</small></span>
</div>
<nav class="side-nav">
<a href="#" class="active"><span class="ico" aria-hidden="true">◎</span> Campaigns</a>
<a href="#"><span class="ico" aria-hidden="true">♡</span> Donors</a>
<a href="#"><span class="ico" aria-hidden="true">▤</span> Reports</a>
<a href="#"><span class="ico" aria-hidden="true">✉</span> Appeals</a>
<a href="#"><span class="ico" aria-hidden="true">⚙</span> Settings</a>
</nav>
<div class="side-trust">
<span class="badge-dot ok"></span> Registered charity · 501(c)(3)
<small>Tax ID 47-8810023 · gifts tax-deductible</small>
</div>
</aside>
<!-- Main -->
<main class="main">
<header class="topbar">
<div>
<p class="eyebrow">Fundraising</p>
<h1>Campaign Manager</h1>
</div>
<div class="topbar-actions">
<div class="org-purse" title="Raised across active campaigns">
<small>Raised this year</small>
<strong id="purseTotal">$0</strong>
</div>
<button class="btn btn-accent" id="newBtn">+ New campaign</button>
</div>
</header>
<!-- Impact summary -->
<section class="impact" aria-label="Impact summary">
<article class="impact-card">
<span class="impact-ico" aria-hidden="true">💧</span>
<div><strong>12,480</strong><small>people given clean water</small></div>
</article>
<article class="impact-card">
<span class="impact-ico" aria-hidden="true">🍲</span>
<div><strong>318,902</strong><small>meals served this year</small></div>
</article>
<article class="impact-card">
<span class="impact-ico" aria-hidden="true">🎓</span>
<div><strong>1,205</strong><small>scholarships funded</small></div>
</article>
<article class="impact-card">
<span class="impact-ico" aria-hidden="true">📈</span>
<div><strong>89¢</strong><small>of each $1 to the field</small></div>
</article>
</section>
<!-- Toolbar -->
<section class="toolbar" aria-label="Campaign filters">
<div class="filters" role="tablist" aria-label="Status filter">
<button class="chip active" data-filter="all" role="tab" aria-selected="true">All <span class="count" data-count="all">0</span></button>
<button class="chip" data-filter="active" role="tab" aria-selected="false">Active <span class="count" data-count="active">0</span></button>
<button class="chip" data-filter="draft" role="tab" aria-selected="false">Draft <span class="count" data-count="draft">0</span></button>
<button class="chip" data-filter="paused" role="tab" aria-selected="false">Paused <span class="count" data-count="paused">0</span></button>
<button class="chip" data-filter="ended" role="tab" aria-selected="false">Ended <span class="count" data-count="ended">0</span></button>
</div>
<div class="tools-right">
<label class="search">
<span aria-hidden="true">⌕</span>
<input type="search" id="searchInput" placeholder="Search campaigns…" aria-label="Search campaigns" />
</label>
<div class="view-toggle" role="group" aria-label="View">
<button class="vt active" data-view="cards" aria-pressed="true" title="Card view">▦</button>
<button class="vt" data-view="table" aria-pressed="false" title="Table view">≣</button>
</div>
</div>
</section>
<!-- Card grid -->
<section id="cardGrid" class="grid" aria-label="Campaigns"></section>
<!-- Table -->
<section id="tableWrap" class="table-wrap" hidden aria-label="Campaigns table">
<table class="table">
<thead>
<tr>
<th>Campaign</th><th>Status</th><th>Progress</th>
<th class="num">Goal</th><th class="num">Raised</th>
<th>Dates</th><th></th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</section>
<p id="emptyState" class="empty" hidden>No campaigns match your filters.</p>
</main>
</div>
<!-- Drawer -->
<div class="drawer-scrim" id="scrim" hidden></div>
<aside class="drawer" id="drawer" role="dialog" aria-modal="true" aria-labelledby="drawerTitle" hidden>
<header class="drawer-head">
<h2 id="drawerTitle">New campaign</h2>
<button class="icon-btn" id="closeDrawer" aria-label="Close">✕</button>
</header>
<form id="campaignForm" class="drawer-body">
<label class="field">
<span>Campaign name</span>
<input name="name" required placeholder="e.g. Warm Coats for Winter" />
</label>
<label class="field">
<span>Short story</span>
<textarea name="story" rows="3" placeholder="One or two sentences donors will read first…"></textarea>
</label>
<div class="field-row">
<label class="field">
<span>Goal (USD)</span>
<input name="goal" type="number" min="100" step="100" required placeholder="25000" />
</label>
<label class="field">
<span>Already raised</span>
<input name="raised" type="number" min="0" step="1" placeholder="0" />
</label>
</div>
<div class="field-row">
<label class="field">
<span>Start date</span>
<input name="start" type="date" required />
</label>
<label class="field">
<span>End date</span>
<input name="end" type="date" required />
</label>
</div>
<label class="field">
<span>Status</span>
<select name="status">
<option value="draft">Draft</option>
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="ended">Ended</option>
</select>
</label>
<fieldset class="tiers">
<legend>Giving tiers</legend>
<div id="tierList" class="tier-list"></div>
<button type="button" class="btn btn-ghost btn-sm" id="addTier">+ Add tier</button>
</fieldset>
<footer class="drawer-foot">
<button type="button" class="btn btn-ghost" id="cancelBtn">Cancel</button>
<button type="submit" class="btn btn-accent" id="saveBtn">Save campaign</button>
</footer>
</form>
</aside>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Campaign Manager
A staff-facing dashboard for the fictional Bright Futures Foundation to run its fundraising drives. A warm sidebar carries the brand mark, navigation, and a registered-charity trust badge, while the header pairs a live “raised this year” purse with a prominent accent New campaign button. A row of impact cards — people given clean water, meals served, scholarships funded, and cents-on-the-dollar reaching the field — keeps transparency front and centre.
Campaigns render as a responsive grid of cards, each with a photo-placeholder gradient, status badge, two-line story, an animated progress bar showing raised against goal, the funded percentage, and the date range. A toolbar filters by status (with live counts), searches by name or story, and flips between the card grid and a compact table view. Every campaign can be paused or re-activated inline, firing a confirmation toast and re-flowing the counts.
Creating or editing a drive opens a slide-in drawer: name, short story, goal, amount already raised,
start and end dates, status, and a repeatable set of giving tiers you can add or remove. Saving
updates the grid, animates the progress fills, and recalculates the year-to-date purse. Everything is
self-contained vanilla HTML, CSS, and JavaScript — count-up totals, an accessible keyboard-closable
drawer, a toast() helper, and a layout that collapses gracefully down to about 360px.
Illustrative UI only — fictional organization, not a real charity or donation system.