Nonprofit — Donations Dashboard
A warm, human nonprofit admin dashboard for tracking fundraising. It surfaces four headline KPIs (total raised, recurring revenue, average gift, new donors), a redrawable donations-over-time area chart with hover tooltips, a recurring-versus-one-time donut split with live impact numbers, ranked top campaigns with drill-in filtering, a goal thermometer with leadership donor recognition, and a searchable recent-donations table that exports to CSV — all self-contained vanilla HTML, CSS, and JavaScript.
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 22px rgba(42, 39, 34, 0.08);
--sh-lg: 0 18px 48px rgba(42, 39, 34, 0.12);
}
* { 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 {
font-family: "Fraunces", Georgia, serif;
font-weight: 600;
letter-spacing: -0.01em;
margin: 0;
}
/* ---------- Layout ---------- */
.app {
display: grid;
grid-template-columns: 248px 1fr;
min-height: 100vh;
}
.sidebar {
background: var(--surface);
border-right: 1px solid var(--line);
padding: 22px 18px;
display: flex;
flex-direction: column;
gap: 22px;
position: sticky;
top: 0;
height: 100vh;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
width: 42px; height: 42px;
display: grid; place-items: center;
border-radius: 12px;
background: linear-gradient(135deg, var(--brand), var(--brand-d));
color: #fff;
font-family: "Fraunces", serif;
font-weight: 700;
font-size: 15px;
box-shadow: var(--sh-sm);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-text strong { font-family: "Fraunces", serif; font-size: 17px; }
.brand-text span { font-size: 12px; color: var(--muted); letter-spacing: 0.04em; text-transform: uppercase; }
.nav { display: flex; flex-direction: column; gap: 3px; }
.nav-item {
display: flex; align-items: center; gap: 11px;
padding: 9px 12px;
border-radius: var(--r-sm);
color: var(--ink-2);
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: background 0.16s, color 0.16s;
}
.ni-dot {
width: 7px; height: 7px; border-radius: 50%;
background: var(--line-2);
transition: background 0.16s, transform 0.16s;
}
.nav-item:hover { background: rgba(31, 122, 109, 0.07); color: var(--ink); }
.nav-item.is-active { background: rgba(31, 122, 109, 0.12); color: var(--brand-d); font-weight: 600; }
.nav-item.is-active .ni-dot { background: var(--accent); transform: scale(1.25); }
.trust-card {
margin-top: auto;
background: rgba(31, 122, 109, 0.06);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px;
}
.trust-badge {
display: inline-block;
font-size: 12px; font-weight: 600;
color: var(--brand-d);
background: rgba(47, 158, 111, 0.14);
padding: 3px 9px;
border-radius: 999px;
margin-bottom: 8px;
}
.trust-card p { margin: 0; font-size: 11.5px; color: var(--muted); line-height: 1.5; }
/* ---------- Main ---------- */
.main { padding: 26px 30px 48px; max-width: 1220px; }
.topbar {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18px;
flex-wrap: wrap;
margin-bottom: 22px;
}
.eyebrow {
margin: 0 0 2px;
font-size: 12px; font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--accent-d);
}
.topbar h1 { font-size: clamp(24px, 3vw, 32px); }
.topbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.seg {
display: inline-flex;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 3px;
box-shadow: var(--sh-sm);
}
.seg-btn {
border: 0; background: transparent;
font: inherit; font-size: 13px; font-weight: 600;
color: var(--ink-2);
padding: 6px 14px;
border-radius: 999px;
cursor: pointer;
transition: background 0.16s, color 0.16s;
}
.seg-btn:hover { color: var(--ink); }
.seg-btn.is-active { background: var(--brand); color: #fff; box-shadow: var(--sh-sm); }
.btn {
font: inherit; font-weight: 600; font-size: 13.5px;
border: 1px solid transparent;
border-radius: 999px;
padding: 9px 18px;
cursor: pointer;
transition: transform 0.12s, box-shadow 0.16s, background 0.16s, border-color 0.16s;
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 3px solid rgba(31, 122, 109, 0.4); outline-offset: 2px; }
.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); }
.btn-donate {
background: linear-gradient(135deg, var(--accent), var(--accent-d));
color: #fff;
box-shadow: 0 6px 16px rgba(232, 116, 59, 0.32);
}
.btn-donate:hover { box-shadow: 0 10px 24px rgba(232, 116, 59, 0.42); }
.btn-sm { padding: 6px 12px; font-size: 12.5px; }
/* ---------- KPIs ---------- */
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 18px;
}
.kpi {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px 18px;
box-shadow: var(--sh-sm);
transition: transform 0.16s, box-shadow 0.16s;
}
.kpi:hover { transform: translateY(-2px); box-shadow: var(--sh-md); }
.kpi-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.kpi-label { font-size: 12.5px; color: var(--muted); font-weight: 500; }
.pill {
font-size: 11.5px; font-weight: 700;
padding: 2px 8px; border-radius: 999px;
}
.pill-up { color: var(--ok); background: rgba(47, 158, 111, 0.13); }
.pill-down { color: var(--danger); background: rgba(212, 80, 62, 0.13); }
.kpi-value {
font-family: "Fraunces", serif;
font-weight: 700;
font-size: 28px;
margin: 6px 0 1px;
letter-spacing: -0.02em;
}
.kpi-sub { font-size: 12px; color: var(--muted); }
/* ---------- Cards / grid ---------- */
.grid-2 {
display: grid;
grid-template-columns: 1.55fr 1fr;
gap: 16px;
margin-bottom: 18px;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 20px 22px;
box-shadow: var(--sh-sm);
}
.card-head {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 12px; margin-bottom: 16px; flex-wrap: wrap;
}
.card-head h2 { font-size: 18px; }
.card-meta { margin: 2px 0 0; font-size: 12.5px; color: var(--muted); }
.legend { display: flex; gap: 14px; }
.lg { display: inline-flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--ink-2); }
.lg-sw { width: 11px; height: 11px; border-radius: 4px; display: inline-block; }
.lg-brand { background: var(--brand); }
.lg-accent { background: var(--accent); }
/* ---------- Chart ---------- */
.chart { position: relative; }
.chart svg { width: 100%; height: 260px; display: block; }
.gridlines line { stroke: var(--line); stroke-width: 1; stroke-dasharray: 3 5; }
#line { transition: d 0.4s ease; }
.dot {
fill: var(--surface);
stroke: var(--brand);
stroke-width: 2.5;
cursor: pointer;
transition: r 0.12s;
}
.dot:hover, .dot.is-hot { r: 6; fill: var(--brand); }
.chart-tip {
position: absolute;
transform: translate(-50%, -118%);
background: var(--ink);
color: #fff;
padding: 7px 11px;
border-radius: var(--r-sm);
font-size: 12.5px;
font-weight: 600;
white-space: nowrap;
pointer-events: none;
box-shadow: var(--sh-md);
z-index: 4;
}
.chart-tip b { color: #ffd9c4; }
.chart-tip::after {
content: ""; position: absolute; left: 50%; bottom: -5px;
transform: translateX(-50%);
border: 5px solid transparent; border-top-color: var(--ink);
}
.x-axis {
display: flex; justify-content: space-between;
margin-top: 8px; font-size: 11px; color: var(--muted);
}
/* ---------- Donut / split ---------- */
.donut { position: relative; width: 168px; margin: 4px auto 14px; }
.donut svg { width: 100%; display: block; }
#donutArc { transition: stroke-dasharray 0.5s ease; }
.donut-center {
position: absolute; inset: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
}
.donut-center strong { font-family: "Fraunces", serif; font-size: 26px; }
.donut-center span { font-size: 11.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
.split-legend { list-style: none; margin: 0 0 16px; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.split-legend li { display: flex; align-items: center; gap: 9px; font-size: 13.5px; }
.split-legend li b { margin-left: auto; font-family: "Fraunces", serif; }
.impact-strip {
display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
border-top: 1px solid var(--line); padding-top: 14px;
}
.impact-strip div { text-align: center; }
.impact-strip strong { display: block; font-family: "Fraunces", serif; font-size: 20px; color: var(--brand-d); }
.impact-strip span { font-size: 11.5px; color: var(--muted); }
/* ---------- Campaigns ---------- */
.campaigns { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.campaign {
display: grid;
grid-template-columns: 1fr auto;
gap: 4px 12px;
align-items: center;
padding: 11px 12px;
border-radius: var(--r-md);
border: 1px solid transparent;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.campaign:hover { background: rgba(31, 122, 109, 0.05); border-color: var(--line); }
.campaign.is-active { background: rgba(232, 116, 59, 0.08); border-color: rgba(232, 116, 59, 0.35); }
.campaign-name { font-weight: 600; font-size: 14px; }
.campaign-amt { font-family: "Fraunces", serif; font-weight: 600; font-size: 15px; text-align: right; }
.campaign-bar {
grid-column: 1 / -1;
height: 6px; border-radius: 999px;
background: var(--line); overflow: hidden;
}
.campaign-bar i { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--brand), var(--brand-d)); transition: width 0.5s ease; }
.campaign-meta { grid-column: 1 / -1; font-size: 11.5px; color: var(--muted); }
/* ---------- Thermometer ---------- */
.therm-track { height: 18px; border-radius: 999px; background: var(--line); overflow: hidden; }
.therm-fill {
height: 100%; border-radius: 999px;
background: linear-gradient(90deg, var(--accent), var(--accent-d));
width: 0; transition: width 0.7s cubic-bezier(0.2, 0.7, 0.2, 1);
box-shadow: inset 0 -2px 4px rgba(0, 0, 0, 0.12);
}
.therm-stats { display: flex; justify-content: space-between; margin-top: 10px; }
.therm-stats div span { display: block; font-size: 12px; color: var(--muted); }
.therm-stats strong { font-family: "Fraunces", serif; font-size: 20px; }
.ta-right { text-align: right; }
.donors-recog { margin-top: 18px; border-top: 1px solid var(--line); padding-top: 14px; }
.recog-title { margin: 0 0 8px; font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
.donors-recog ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
.donors-recog li { display: flex; align-items: center; gap: 10px; font-size: 13.5px; }
.donors-recog li .av {
width: 28px; height: 28px; border-radius: 50%;
display: grid; place-items: center;
font-size: 11px; font-weight: 700; color: #fff;
background: linear-gradient(135deg, var(--brand), var(--accent));
}
.donors-recog li b { margin-left: auto; font-family: "Fraunces", serif; }
/* ---------- Table ---------- */
.table-card { padding-bottom: 14px; }
.table-tools { display: flex; align-items: center; gap: 10px; }
.search {
font: inherit; font-size: 13.5px;
padding: 8px 13px;
border: 1px solid var(--line-2);
border-radius: 999px;
background: var(--bg);
color: var(--ink);
width: 230px; max-width: 52vw;
}
.search:focus-visible { outline: 3px solid rgba(31, 122, 109, 0.35); outline-offset: 1px; border-color: var(--brand); }
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
thead th {
text-align: left; font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.05em;
color: var(--muted); font-weight: 600;
padding: 8px 12px; border-bottom: 1px solid var(--line-2);
white-space: nowrap;
}
thead th.num, td.num { text-align: right; }
tbody td { padding: 12px; border-bottom: 1px solid var(--line); }
tbody tr { transition: background 0.13s; }
tbody tr:hover { background: rgba(31, 122, 109, 0.04); }
tbody tr:last-child td { border-bottom: 0; }
.donor-cell { display: flex; align-items: center; gap: 10px; }
.donor-cell .av {
width: 30px; height: 30px; border-radius: 50%;
display: grid; place-items: center;
font-size: 11px; font-weight: 700; color: #fff;
flex: 0 0 auto;
}
.donor-name { font-weight: 600; }
td.num { font-family: "Fraunces", serif; font-weight: 600; }
.type-tag {
font-size: 11.5px; font-weight: 600;
padding: 2px 9px; border-radius: 999px;
}
.type-recurring { color: var(--brand-d); background: rgba(31, 122, 109, 0.12); }
.type-onetime { color: var(--accent-d); background: rgba(232, 116, 59, 0.13); }
.status {
font-size: 11.5px; font-weight: 600;
display: inline-flex; align-items: center; gap: 6px;
}
.status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; }
.status-completed { color: var(--ok); }
.status-completed::before { background: var(--ok); }
.status-pending { color: var(--warn); }
.status-pending::before { background: var(--warn); }
.empty { text-align: center; color: var(--muted); padding: 26px 0 14px; font-size: 14px; }
/* ---------- Toast ---------- */
.toast {
position: fixed; left: 50%; bottom: 26px;
transform: translate(-50%, 24px);
background: var(--ink); color: #fff;
padding: 12px 20px; border-radius: 999px;
font-size: 14px; font-weight: 500;
box-shadow: var(--sh-lg);
opacity: 0; pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 50;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 980px) {
.app { grid-template-columns: 1fr; }
.sidebar {
position: static; height: auto; flex-direction: row; align-items: center;
flex-wrap: wrap; gap: 14px;
}
.nav { flex-direction: row; flex-wrap: wrap; }
.trust-card { margin-top: 0; flex: 1 1 220px; }
.kpis { grid-template-columns: repeat(2, 1fr); }
.grid-2 { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.main { padding: 18px 14px 40px; }
.topbar { align-items: flex-start; }
.topbar-actions { width: 100%; }
.kpis { grid-template-columns: 1fr 1fr; gap: 12px; }
.kpi-value { font-size: 24px; }
.card { padding: 16px; border-radius: var(--r-md); }
.nav-item span:not(.ni-dot) { font-size: 13px; }
.search { width: 100%; max-width: none; }
.table-tools { flex: 1 1 100%; flex-wrap: wrap; }
}(function () {
"use strict";
/* ---------- Helpers ---------- */
var $ = function (s, r) { return (r || document).querySelector(s); };
var fmt = function (n) {
return "$" + Math.round(n).toLocaleString("en-US");
};
var fmtK = function (n) {
if (n >= 1000) return "$" + (n / 1000).toFixed(n >= 10000 ? 0 : 1) + "k";
return "$" + Math.round(n);
};
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2600);
}
/* ---------- Seeded deterministic data ---------- */
function rng(seed) {
var s = seed % 2147483647;
if (s <= 0) s += 2147483646;
return function () { s = (s * 16807) % 2147483647; return (s - 1) / 2147483646; };
}
// Build a daily series of donation totals for up to 365 days.
var rand = rng(7351);
var DAYS = 365;
var series = [];
for (var i = 0; i < DAYS; i++) {
var base = 2400 + i * 6; // gentle upward trend
var wave = Math.sin(i / 7) * 700; // weekly rhythm
var spike = rand() < 0.05 ? 4200 * rand() : 0; // occasional appeal spikes
series.push(Math.max(600, base + wave + spike + (rand() - 0.5) * 1300));
}
var RANGES = {
"30": { label: "Last 30 days", ticks: 6 },
"90": { label: "Last 90 days", ticks: 6 },
"365": { label: "Last 12 months", ticks: 6 }
};
/* ---------- Campaigns ---------- */
var campaigns = [
{ id: "spring", name: "Spring Appeal", share: 0.31, gifts: 412 },
{ id: "meals", name: "Meals on Wheels", share: 0.24, gifts: 689 },
{ id: "water", name: "Clean Water Initiative", share: 0.19, gifts: 254 },
{ id: "winter", name: "Winter Warmth Drive", share: 0.15, gifts: 503 },
{ id: "scholar", name: "Scholarship Fund", share: 0.11, gifts: 138 }
];
/* ---------- Donations table ---------- */
var firstNames = ["Maya", "Daniel", "Priya", "Liam", "Sofia", "Omar", "Grace", "Noah", "Amara", "Ethan", "Lena", "Marcus", "Yuki", "Rosa", "Theo", "Nadia", "Caleb", "Imani", "Felix", "Aisha"];
var lastNames = ["Okafor", "Reyes", "Patel", "Hansen", "Bennett", "Khan", "Lindgren", "Castro", "Walsh", "Mori", "Adeyemi", "Costa", "Forsberg", "Nguyen", "Bauer", "Silva", "Brandt", "Osei", "Romano", "Haddad"];
var statuses = ["completed", "completed", "completed", "completed", "pending"];
var avatarColors = ["#1f7a6d", "#e8743b", "#2f9e6f", "#d98a2b", "#155e54", "#cc5d28"];
var donations = [];
var dr = rng(2024);
for (var d = 0; d < 38; d++) {
var fn = firstNames[Math.floor(dr() * firstNames.length)];
var ln = lastNames[Math.floor(dr() * lastNames.length)];
var camp = campaigns[Math.floor(dr() * campaigns.length)];
var recurring = dr() < 0.42;
var amt = recurring
? [10, 15, 25, 35, 50, 75][Math.floor(dr() * 6)]
: [20, 40, 50, 75, 100, 150, 250, 500, 1000][Math.floor(dr() * 9)];
var daysAgo = Math.floor(dr() * 29);
var dt = new Date(2026, 5, 16);
dt.setDate(dt.getDate() - daysAgo);
donations.push({
name: fn + " " + ln,
initials: fn[0] + ln[0],
color: avatarColors[Math.floor(dr() * avatarColors.length)],
campaign: camp.name,
campaignId: camp.id,
type: recurring ? "recurring" : "onetime",
amount: amt,
date: dt,
status: statuses[Math.floor(dr() * statuses.length)]
});
}
donations.sort(function (a, b) { return b.date - a.date; });
var leadershipGifts = [
{ name: "The Hollis Family Trust", initials: "HF", amt: 25000 },
{ name: "Verdant Tech Foundation", initials: "VT", amt: 18500 },
{ name: "Anonymous donor", initials: "A", amt: 12000 }
];
/* ---------- State ---------- */
var state = { range: "30", campaignFilter: null, search: "" };
/* ---------- KPI computation ---------- */
function rangeSlice(days) {
return series.slice(series.length - days);
}
function sum(arr) { return arr.reduce(function (a, b) { return a + b; }, 0); }
function computeKpis(days) {
var cur = rangeSlice(days);
var prev = series.slice(series.length - days * 2, series.length - days);
var totalRaised = sum(cur);
var prevRaised = sum(prev) || totalRaised;
var recurringRev = totalRaised * 0.38;
var prevRecurring = prevRaised * 0.355;
var giftCount = Math.round(days * 23 + (days * 0.4));
var prevGiftCount = Math.round(days * 21);
var avg = totalRaised / giftCount;
var prevAvg = prevRaised / prevGiftCount;
var newDonors = Math.round(days * 4.6);
var prevNew = Math.round(days * 4.1);
return {
raised: { v: totalRaised, t: pct(totalRaised, prevRaised) },
recurring: { v: recurringRev, t: pct(recurringRev, prevRecurring) },
avg: { v: avg, t: pct(avg, prevAvg) },
donors: { v: newDonors, t: pct(newDonors, prevNew) }
};
}
function pct(cur, prev) {
if (!prev) return 0;
return ((cur - prev) / prev) * 100;
}
function setTrend(key, value) {
var el = document.querySelector('[data-trend="' + key + '"]');
var up = value >= 0;
el.textContent = (up ? "▲ " : "▼ ") + Math.abs(value).toFixed(1) + "%";
el.classList.toggle("pill-up", up);
el.classList.toggle("pill-down", !up);
}
function renderKpis() {
var k = computeKpis(parseInt(state.range, 10));
animateValue('[data-kpi="raised"]', k.raised.v, true);
animateValue('[data-kpi="recurring"]', k.recurring.v, true);
animateValue('[data-kpi="avg"]', k.avg.v, true);
animateValue('[data-kpi="donors"]', k.donors.v, false);
setTrend("raised", k.raised.t);
setTrend("recurring", k.recurring.t);
setTrend("avg", k.avg.t);
setTrend("donors", k.donors.t);
// Split donut
var rec = k.raised.v * 0.38;
var one = k.raised.v - rec;
var recPct = Math.round((rec / k.raised.v) * 100);
$("#recAmt").textContent = fmt(rec);
$("#oneAmt").textContent = fmt(one);
$("#donutPct").textContent = recPct + "%";
var circ = 2 * Math.PI * 48;
$("#donutArc").style.strokeDasharray = (circ * recPct / 100) + " " + circ;
// Impact numbers
animateValue("#impactMeals", Math.round(k.raised.v / 2.4), false);
animateValue("#impactKits", Math.round(k.raised.v / 65), false);
}
function animateValue(sel, target, money) {
var el = typeof sel === "string" ? document.querySelector(sel) : sel;
if (!el) return;
var start = parseFloat((el.getAttribute("data-raw") || "0")) || 0;
var dur = 550, t0 = performance.now();
function step(now) {
var p = Math.min(1, (now - t0) / dur);
var eased = 1 - Math.pow(1 - p, 3);
var val = start + (target - start) * eased;
el.textContent = money ? fmt(val) : Math.round(val).toLocaleString("en-US");
if (p < 1) requestAnimationFrame(step);
else el.setAttribute("data-raw", target);
}
requestAnimationFrame(step);
}
/* ---------- Chart ---------- */
var W = 720, H = 260, PADX = 8, PADY = 18;
function bucketData(days) {
var data = rangeSlice(days);
// Downsample to ~30 points for readability on long ranges
var maxPts = 30;
if (data.length <= maxPts) return aggregate(data, days);
var groupSize = Math.ceil(data.length / maxPts);
var out = [];
for (var i = 0; i < data.length; i += groupSize) {
var chunk = data.slice(i, i + groupSize);
out.push(sum(chunk));
}
return labelize(out, days);
}
function aggregate(data) { return labelize(data.slice(), null); }
function labelize(values, days) {
return values.map(function (v, idx) {
return { v: v, idx: idx, count: values.length };
});
}
function buildPath(points) {
var max = Math.max.apply(null, points.map(function (p) { return p.v; }));
var min = Math.min.apply(null, points.map(function (p) { return p.v; }));
var range = (max - min) || 1;
var n = points.length;
var coords = points.map(function (p, i) {
var x = PADX + (i / (n - 1)) * (W - PADX * 2);
var y = PADY + (1 - (p.v - min * 0.85) / (max - min * 0.85 || 1)) * (H - PADY * 2);
return { x: x, y: y, v: p.v };
});
var line = coords.map(function (c, i) { return (i ? "L" : "M") + c.x.toFixed(1) + " " + c.y.toFixed(1); }).join(" ");
var area = line + " L" + coords[coords.length - 1].x.toFixed(1) + " " + (H - PADY) +
" L" + coords[0].x.toFixed(1) + " " + (H - PADY) + " Z";
return { line: line, area: area, coords: coords, max: max };
}
function renderChart() {
var days = parseInt(state.range, 10);
var pts = bucketData(days);
var built = buildPath(pts);
$("#line").setAttribute("d", built.line);
$("#area").setAttribute("d", built.area);
// Gridlines
var gl = $("#gridlines");
gl.innerHTML = "";
for (var g = 1; g <= 4; g++) {
var y = PADY + (g / 5) * (H - PADY * 2);
var ln = document.createElementNS("http://www.w3.org/2000/svg", "line");
ln.setAttribute("x1", PADX); ln.setAttribute("x2", W - PADX);
ln.setAttribute("y1", y); ln.setAttribute("y2", y);
gl.appendChild(ln);
}
// Dots
var dots = $("#dots");
dots.innerHTML = "";
built.coords.forEach(function (c, i) {
var circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.setAttribute("class", "dot");
circle.setAttribute("cx", c.x);
circle.setAttribute("cy", c.y);
circle.setAttribute("r", "3.5");
circle.setAttribute("tabindex", "0");
circle.setAttribute("data-i", i);
circle.addEventListener("mouseenter", function () { showTip(c, days); });
circle.addEventListener("focus", function () { showTip(c, days); });
circle.addEventListener("mouseleave", hideTip);
circle.addEventListener("blur", hideTip);
dots.appendChild(circle);
});
// X-axis labels
var xa = $("#xaxis");
xa.innerHTML = "";
var labels = axisLabels(days);
labels.forEach(function (l) {
var span = document.createElement("span");
span.textContent = l;
xa.appendChild(span);
});
$("[data-chart-meta]").textContent = RANGES[state.range].label;
}
function axisLabels(days) {
var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
var end = new Date(2026, 5, 16);
var out = [];
if (days <= 30) {
for (var i = 4; i >= 0; i--) {
var dt = new Date(end); dt.setDate(dt.getDate() - i * 7);
out.push(months[dt.getMonth()] + " " + dt.getDate());
}
} else if (days <= 90) {
for (var j = 5; j >= 0; j--) {
var d2 = new Date(end); d2.setDate(d2.getDate() - j * 15);
out.push(months[d2.getMonth()] + " " + d2.getDate());
}
} else {
for (var m = 5; m >= 0; m--) {
var d3 = new Date(end); d3.setMonth(d3.getMonth() - m * 2);
out.push(months[d3.getMonth()]);
}
}
return out;
}
var tip = $("#tip");
function showTip(c, days) {
var label = days <= 30 ? "day" : days <= 90 ? "3-day total" : "period";
tip.hidden = false;
tip.innerHTML = "<b>" + fmtK(c.v) + "</b> · " + label;
var rect = $("#chart").getBoundingClientRect();
tip.style.left = (c.x / W * rect.width) + "px";
tip.style.top = (c.y / H * 260) + "px";
var hot = document.querySelector(".dot.is-hot");
if (hot) hot.classList.remove("is-hot");
}
function hideTip() { tip.hidden = true; }
/* ---------- Campaigns render ---------- */
function renderCampaigns() {
var totalRaised = computeKpis(parseInt(state.range, 10)).raised.v;
var max = Math.max.apply(null, campaigns.map(function (c) { return c.share; }));
var list = $("#campaigns");
list.innerHTML = "";
campaigns.forEach(function (c) {
var amt = totalRaised * c.share;
var li = document.createElement("li");
li.className = "campaign" + (state.campaignFilter === c.id ? " is-active" : "");
li.setAttribute("role", "button");
li.setAttribute("tabindex", "0");
li.innerHTML =
'<span class="campaign-name">' + c.name + '</span>' +
'<span class="campaign-amt">' + fmt(amt) + '</span>' +
'<span class="campaign-bar"><i style="width:' + (c.share / max * 100) + '%"></i></span>' +
'<span class="campaign-meta">' + c.gifts + ' gifts · ' + Math.round(c.share * 100) + '% of total</span>';
function activate() { drillCampaign(c.id); }
li.addEventListener("click", activate);
li.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); activate(); }
});
list.appendChild(li);
});
}
function drillCampaign(id) {
state.campaignFilter = state.campaignFilter === id ? null : id;
var c = campaigns.filter(function (x) { return x.id === id; })[0];
if (state.campaignFilter) {
toast("Drilling into " + c.name);
$("#tableTitle").textContent = c.name + " donations";
$("#tableMeta").textContent = "Filtered campaign";
$("#clearDrill").hidden = false;
} else {
$("#tableTitle").textContent = "Recent donations";
$("#tableMeta").textContent = "All campaigns";
$("#clearDrill").hidden = true;
}
renderCampaigns();
renderTable();
}
/* ---------- Thermometer + recognition ---------- */
function renderTherm() {
var goal = 250000;
var raised = 183420;
$("#thermFill").style.width = (raised / goal * 100) + "%";
animateValue("#thermRaised", raised, true);
var rl = $("#recogList");
rl.innerHTML = "";
leadershipGifts.forEach(function (g) {
var li = document.createElement("li");
li.innerHTML = '<span class="av">' + g.initials + '</span><span>' + g.name + '</span><b>' + fmt(g.amt) + '</b>';
rl.appendChild(li);
});
}
/* ---------- Table ---------- */
function filteredDonations() {
var q = state.search.trim().toLowerCase();
return donations.filter(function (d) {
if (state.campaignFilter && d.campaignId !== state.campaignFilter) return false;
if (q && d.name.toLowerCase().indexOf(q) === -1 && d.campaign.toLowerCase().indexOf(q) === -1) return false;
return true;
});
}
function renderTable() {
var tbody = $("#tbody");
var rows = filteredDonations();
tbody.innerHTML = "";
$("#emptyRow").hidden = rows.length !== 0;
rows.forEach(function (d) {
var tr = document.createElement("tr");
var dateStr = d.date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
tr.innerHTML =
'<td><div class="donor-cell"><span class="av" style="background:' + d.color + '">' + d.initials + '</span>' +
'<span class="donor-name">' + d.name + '</span></div></td>' +
'<td>' + d.campaign + '</td>' +
'<td><span class="type-tag ' + (d.type === "recurring" ? "type-recurring" : "type-onetime") + '">' +
(d.type === "recurring" ? "Monthly" : "One-time") + '</span></td>' +
'<td class="num">' + fmt(d.amount) + '</td>' +
'<td>' + dateStr + '</td>' +
'<td><span class="status status-' + d.status + '">' + d.status.charAt(0).toUpperCase() + d.status.slice(1) + '</span></td>';
tbody.appendChild(tr);
});
}
/* ---------- Export ---------- */
function exportCsv() {
var rows = filteredDonations();
var header = ["Donor", "Campaign", "Type", "Amount", "Date", "Status"];
var lines = [header.join(",")];
rows.forEach(function (d) {
lines.push([
'"' + d.name + '"',
'"' + d.campaign + '"',
d.type,
d.amount,
d.date.toISOString().slice(0, 10),
d.status
].join(","));
});
var blob = new Blob([lines.join("\n")], { type: "text/csv" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = "brightwell-donations.csv";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast("Exported " + rows.length + " donations to CSV");
}
/* ---------- Events ---------- */
var segBtns = document.querySelectorAll(".seg-btn");
segBtns.forEach(function (b) {
b.addEventListener("click", function () {
if (b.classList.contains("is-active")) return;
segBtns.forEach(function (x) { x.classList.remove("is-active"); x.setAttribute("aria-pressed", "false"); });
b.classList.add("is-active");
b.setAttribute("aria-pressed", "true");
state.range = b.getAttribute("data-range");
renderKpis();
renderChart();
renderCampaigns();
toast("Showing " + RANGES[state.range].label.toLowerCase());
});
});
$("#exportBtn").addEventListener("click", exportCsv);
$("#donateBtn").addEventListener("click", function () {
toast("Thank you! Redirecting to the secure donation form…");
});
$("#clearDrill").addEventListener("click", function () {
if (state.campaignFilter) drillCampaign(state.campaignFilter);
});
var searchEl = $("#search");
var searchTimer;
searchEl.addEventListener("input", function () {
clearTimeout(searchTimer);
searchTimer = setTimeout(function () {
state.search = searchEl.value;
renderTable();
}, 130);
});
window.addEventListener("resize", function () {
clearTimeout(window.__rcz);
window.__rcz = setTimeout(renderChart, 150);
});
/* ---------- Init ---------- */
renderKpis();
renderChart();
renderCampaigns();
renderTherm();
renderTable();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Brightwell Foundation — Donations 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=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 navigation">
<div class="brand">
<div class="brand-mark" aria-hidden="true">BW</div>
<div class="brand-text">
<strong>Brightwell</strong>
<span>Foundation</span>
</div>
</div>
<nav class="nav">
<a href="#" class="nav-item is-active"><span class="ni-dot" aria-hidden="true"></span>Donations</a>
<a href="#" class="nav-item"><span class="ni-dot" aria-hidden="true"></span>Campaigns</a>
<a href="#" class="nav-item"><span class="ni-dot" aria-hidden="true"></span>Donors</a>
<a href="#" class="nav-item"><span class="ni-dot" aria-hidden="true"></span>Programs</a>
<a href="#" class="nav-item"><span class="ni-dot" aria-hidden="true"></span>Reports</a>
</nav>
<div class="trust-card">
<span class="trust-badge">✓ Registered Charity</span>
<p>EIN 47‑1029384 · Gifts are tax‑deductible to the fullest extent allowed by law.</p>
</div>
</aside>
<!-- Main -->
<main class="main">
<header class="topbar">
<div>
<p class="eyebrow">Fundraising overview</p>
<h1>Donations Dashboard</h1>
</div>
<div class="topbar-actions">
<div class="seg" role="group" aria-label="Timeframe">
<button class="seg-btn is-active" data-range="30" aria-pressed="true">30D</button>
<button class="seg-btn" data-range="90" aria-pressed="false">90D</button>
<button class="seg-btn" data-range="365" aria-pressed="false">1Y</button>
</div>
<button class="btn btn-ghost" id="exportBtn" type="button">
<span aria-hidden="true">⤓</span> Export CSV
</button>
<button class="btn btn-donate" id="donateBtn" type="button">Donate</button>
</div>
</header>
<!-- KPIs -->
<section class="kpis" aria-label="Key metrics">
<article class="kpi">
<div class="kpi-top"><span class="kpi-label">Total raised</span><span class="pill pill-up" data-trend="raised">▲ 0%</span></div>
<div class="kpi-value" data-kpi="raised">$0</div>
<div class="kpi-sub" data-kpisub="raised">vs. previous period</div>
</article>
<article class="kpi">
<div class="kpi-top"><span class="kpi-label">Recurring revenue</span><span class="pill pill-up" data-trend="recurring">▲ 0%</span></div>
<div class="kpi-value" data-kpi="recurring">$0</div>
<div class="kpi-sub" data-kpisub="recurring">monthly run‑rate</div>
</article>
<article class="kpi">
<div class="kpi-top"><span class="kpi-label">Average gift</span><span class="pill pill-up" data-trend="avg">▲ 0%</span></div>
<div class="kpi-value" data-kpi="avg">$0</div>
<div class="kpi-sub" data-kpisub="avg">per donation</div>
</article>
<article class="kpi">
<div class="kpi-top"><span class="kpi-label">New donors</span><span class="pill pill-up" data-trend="donors">▲ 0%</span></div>
<div class="kpi-value" data-kpi="donors">0</div>
<div class="kpi-sub" data-kpisub="donors">first‑time givers</div>
</article>
</section>
<!-- Chart + split -->
<section class="grid-2">
<article class="card chart-card">
<div class="card-head">
<div>
<h2>Donations over time</h2>
<p class="card-meta"><span data-chart-meta>Last 30 days</span></p>
</div>
<div class="legend">
<span class="lg"><i class="lg-sw lg-brand" aria-hidden="true"></i>Daily total</span>
</div>
</div>
<div class="chart" id="chart">
<svg viewBox="0 0 720 260" preserveAspectRatio="none" role="img" aria-label="Donations over time area chart">
<defs>
<linearGradient id="fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--brand)" stop-opacity="0.28" />
<stop offset="100%" stop-color="var(--brand)" stop-opacity="0" />
</linearGradient>
</defs>
<g class="gridlines" id="gridlines"></g>
<path id="area" fill="url(#fill)" stroke="none"></path>
<path id="line" fill="none" stroke="var(--brand)" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"></path>
<g id="dots"></g>
</svg>
<div class="chart-tip" id="tip" hidden></div>
<div class="x-axis" id="xaxis"></div>
</div>
</article>
<article class="card split-card">
<div class="card-head"><h2>Recurring vs one‑time</h2></div>
<div class="donut" role="img" aria-label="Recurring versus one-time donation split">
<svg viewBox="0 0 120 120">
<circle cx="60" cy="60" r="48" fill="none" stroke="var(--line)" stroke-width="16" />
<circle id="donutArc" cx="60" cy="60" r="48" fill="none" stroke="var(--brand)" stroke-width="16" stroke-linecap="round" transform="rotate(-90 60 60)" />
</svg>
<div class="donut-center">
<strong id="donutPct">0%</strong>
<span>recurring</span>
</div>
</div>
<ul class="split-legend">
<li><i class="lg-sw lg-brand"></i><span>Recurring</span><b id="recAmt">$0</b></li>
<li><i class="lg-sw lg-accent"></i><span>One‑time</span><b id="oneAmt">$0</b></li>
</ul>
<div class="impact-strip">
<div><strong id="impactMeals">0</strong><span>meals funded</span></div>
<div><strong id="impactKits">0</strong><span>relief kits</span></div>
</div>
</article>
</section>
<!-- Campaigns + thermometer -->
<section class="grid-2">
<article class="card">
<div class="card-head">
<h2>Top campaigns</h2>
<p class="card-meta">Click a campaign to drill in</p>
</div>
<ul class="campaigns" id="campaigns"></ul>
</article>
<article class="card therm-card">
<div class="card-head"><h2>Spring Appeal goal</h2></div>
<div class="therm">
<div class="therm-track"><div class="therm-fill" id="thermFill"></div></div>
<div class="therm-stats">
<div><strong id="thermRaised">$0</strong><span>raised</span></div>
<div class="ta-right"><strong>$250,000</strong><span>goal</span></div>
</div>
</div>
<div class="donors-recog">
<p class="recog-title">Recent leadership gifts</p>
<ul id="recogList"></ul>
</div>
</article>
</section>
<!-- Recent donations -->
<section class="card table-card">
<div class="card-head">
<div>
<h2 id="tableTitle">Recent donations</h2>
<p class="card-meta" id="tableMeta">All campaigns</p>
</div>
<div class="table-tools">
<input type="search" id="search" class="search" placeholder="Search donor or campaign…" aria-label="Search donations" />
<button class="btn btn-ghost btn-sm" id="clearDrill" type="button" hidden>Clear filter ✕</button>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Donor</th><th>Campaign</th><th>Type</th><th class="num">Amount</th><th>Date</th><th>Status</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
<p class="empty" id="emptyRow" hidden>No donations match your search.</p>
</section>
</main>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Donations Dashboard
A fundraising overview for the fictional Brightwell Foundation, built in the warm, hopeful nonprofit style — a mission teal-green and donate-orange palette, a humane Fraunces serif for headings, and soft sand surfaces. The top row tracks four headline KPIs (total raised, recurring revenue, average gift, new donors) with period-over-period trend pills that count up smoothly whenever the data changes. Below sit a donations-over-time area chart with dotted gridlines and hover tooltips, and a recurring-versus-one-time donut whose center percentage and impact numbers (meals funded, relief kits) update in lockstep.
The dashboard is genuinely interactive. The 30D / 90D / 1Y timeframe toggle recomputes every KPI and redraws the chart by downsampling a seeded daily series, so longer ranges stay legible. Top campaigns are ranked with progress bars and gift counts; clicking one drills the recent-donations table down to that campaign and highlights the card, with a one-tap clear. The table supports debounced search across donor and campaign names, and an Export CSV button downloads exactly the rows currently in view. A goal thermometer and leadership-gift recognition list round out the transparency story, and a small toast helper confirms each action.
Everything is keyboard-usable, AA-contrast, and responsive down to roughly 360px, where the sidebar collapses to a horizontal strip and the grids stack. No frameworks, no build step — just three files.
Illustrative UI only — fictional organization, not a real charity or donation system.