Nonprofit — Events Calendar
A warm, hopeful events and fundraisers page for a fictional charity. Browse galas, runs, food drives and workshops as photo cards showing date, time, location, a live spots-left counter and an RSVP button, then toggle to a month calendar where each day carries colored dots per event type. Chip filters narrow by category, RSVPs and waitlist confirm with a toast, animated impact counters and a campaign progress thermometer round out the design — all self-contained vanilla JS.
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.1);
--sh-lg: 0 18px 44px rgba(42, 39, 34, 0.14);
/* event type accents */
--c-gala: #8b5cf6;
--c-run: #2f9e6f;
--c-drive: #e8743b;
--c-workshop: #2d8fd6;
}
* { 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 {
font-family: "Fraunces", Georgia, serif;
font-weight: 600;
line-height: 1.15;
margin: 0;
letter-spacing: -0.01em;
}
a { color: inherit; }
.wrap { width: min(1120px, 92vw); margin-inline: auto; }
.skip {
position: absolute;
left: -999px;
top: 8px;
background: var(--ink);
color: #fff;
padding: 10px 16px;
border-radius: var(--r-sm);
z-index: 50;
}
.skip:focus { left: 12px; }
/* ---------- buttons ---------- */
.btn {
font: inherit;
font-weight: 600;
border: 0;
cursor: pointer;
border-radius: 999px;
padding: 11px 20px;
transition: transform .12s ease, box-shadow .12s ease, background .15s ease;
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
.btn--donate {
background: var(--accent);
color: #fff;
box-shadow: 0 6px 16px rgba(232, 116, 59, 0.32);
}
.btn--donate:hover { background: var(--accent-d); box-shadow: 0 8px 20px rgba(204, 93, 40, 0.4); }
.btn--block { width: 100%; margin-top: 14px; }
/* ---------- topbar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 30;
background: rgba(250, 246, 240, 0.86);
backdrop-filter: saturate(1.1) blur(8px);
border-bottom: 1px solid var(--line);
}
.topbar__row {
display: flex;
align-items: center;
gap: 18px;
height: 66px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 9px;
text-decoration: none;
font-family: "Fraunces", serif;
font-weight: 700;
font-size: 1.18rem;
color: var(--ink);
}
.brand__mark {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 9px;
background: linear-gradient(135deg, var(--brand), var(--brand-d));
color: #fff;
font-size: .95rem;
}
.topnav {
display: flex;
gap: 22px;
margin-left: auto;
margin-right: 6px;
}
.topnav a {
text-decoration: none;
color: var(--ink-2);
font-weight: 500;
font-size: .95rem;
padding: 6px 2px;
border-bottom: 2px solid transparent;
}
.topnav a:hover { color: var(--brand-d); border-color: var(--brand); }
/* ---------- hero ---------- */
.hero {
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 40px;
align-items: center;
padding: 54px 0 36px;
}
.eyebrow {
display: inline-block;
font-weight: 600;
font-size: .82rem;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--brand-d);
background: rgba(31, 122, 109, 0.1);
padding: 5px 12px;
border-radius: 999px;
margin-bottom: 16px;
}
.hero h1 { font-size: clamp(2.3rem, 5vw, 3.5rem); }
.hero__lede {
color: var(--ink-2);
font-size: 1.08rem;
max-width: 46ch;
margin: 16px 0 24px;
}
.hero__stats {
display: flex;
gap: 26px;
flex-wrap: wrap;
margin-bottom: 22px;
}
.stat { display: flex; flex-direction: column; }
.stat__num {
font-family: "Fraunces", serif;
font-weight: 700;
font-size: 1.9rem;
color: var(--brand-d);
line-height: 1;
}
.stat__label { font-size: .82rem; color: var(--muted); max-width: 14ch; }
.trust { display: flex; gap: 8px; flex-wrap: wrap; }
.badge {
font-size: .76rem;
font-weight: 600;
color: var(--ink-2);
background: var(--surface);
border: 1px solid var(--line-2);
padding: 6px 11px;
border-radius: 999px;
}
.hero__art { margin: 0; }
.photo {
position: relative;
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--sh-md);
background: linear-gradient(135deg, #f5c9a0, #e8743b 45%, #1f7a6d);
}
.photo--hero { aspect-ratio: 4 / 5; }
.photo::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(120% 80% at 70% 10%, rgba(255,255,255,.28), transparent 55%),
linear-gradient(180deg, transparent 45%, rgba(42,39,34,.55));
}
.photo__cap {
position: absolute;
left: 16px;
bottom: 14px;
right: 16px;
z-index: 2;
color: #fff;
font-size: .9rem;
font-weight: 500;
text-shadow: 0 1px 8px rgba(0,0,0,.4);
}
/* ---------- controls ---------- */
.controls {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding: 8px 0 18px;
}
.filters { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
display: inline-flex;
align-items: center;
gap: 7px;
font: inherit;
font-weight: 600;
font-size: .9rem;
color: var(--ink-2);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 8px 15px;
cursor: pointer;
transition: all .14s ease;
}
.chip:hover { border-color: var(--brand); color: var(--brand-d); }
.chip:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
.chip.is-active {
background: var(--brand);
border-color: var(--brand);
color: #fff;
}
.chip.is-active .dot { box-shadow: 0 0 0 2px rgba(255,255,255,.7); }
.dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.dot--gala { background: var(--c-gala); }
.dot--run { background: var(--c-run); }
.dot--drive { background: var(--c-drive); }
.dot--workshop { background: var(--c-workshop); }
.viewtoggle {
display: inline-flex;
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 4px;
}
.seg {
font: inherit;
font-weight: 600;
font-size: .9rem;
color: var(--ink-2);
border: 0;
background: transparent;
border-radius: 999px;
padding: 7px 18px;
cursor: pointer;
}
.seg:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
.seg.is-active { background: var(--ink); color: #fff; }
/* ---------- events list ---------- */
.panel { padding-bottom: 48px; }
.events {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
gap: 20px;
}
.card {
display: flex;
flex-direction: column;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--sh-sm);
transition: transform .15s ease, box-shadow .15s ease;
}
.card:hover { transform: translateY(-3px); box-shadow: var(--sh-lg); }
.card__photo {
position: relative;
aspect-ratio: 16 / 9;
}
.card__type {
position: absolute;
top: 12px;
left: 12px;
z-index: 2;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: .76rem;
font-weight: 700;
color: #fff;
background: rgba(42,39,34,.55);
backdrop-filter: blur(4px);
padding: 5px 11px;
border-radius: 999px;
text-transform: capitalize;
}
.card__date {
position: absolute;
top: 12px;
right: 12px;
z-index: 2;
text-align: center;
background: var(--surface);
border-radius: var(--r-sm);
padding: 6px 10px;
box-shadow: var(--sh-sm);
line-height: 1;
}
.card__date b { display: block; font-family: "Fraunces", serif; font-size: 1.3rem; color: var(--accent-d); }
.card__date span { font-size: .68rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.card__body { padding: 16px 18px 18px; display: flex; flex-direction: column; gap: 8px; flex: 1; }
.card__title { font-size: 1.28rem; }
.card__meta { display: flex; flex-direction: column; gap: 4px; font-size: .88rem; color: var(--ink-2); }
.card__meta span { display: inline-flex; align-items: center; gap: 7px; }
.card__meta i { font-style: normal; width: 16px; text-align: center; opacity: .7; }
.card__desc { color: var(--muted); font-size: .9rem; margin: 2px 0 4px; }
.card__foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-top: auto;
padding-top: 8px;
}
.seats { font-size: .82rem; color: var(--ink-2); font-weight: 500; }
.seats.is-low { color: var(--warn); }
.seats.is-full { color: var(--danger); font-weight: 600; }
.rsvp {
font: inherit;
font-weight: 700;
font-size: .9rem;
color: #fff;
background: var(--brand);
border: 0;
border-radius: 999px;
padding: 9px 18px;
cursor: pointer;
transition: background .15s ease, transform .1s ease;
}
.rsvp:hover { background: var(--brand-d); }
.rsvp:active { transform: translateY(1px); }
.rsvp:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.rsvp.is-going { background: var(--ok); }
.rsvp:disabled { background: var(--line-2); color: var(--ink-2); cursor: not-allowed; }
.empty {
text-align: center;
color: var(--muted);
padding: 48px 0;
font-size: 1rem;
}
/* card photo gradients by type */
.pg--gala { background: linear-gradient(135deg, #8b5cf6, #4c2f8b); }
.pg--run { background: linear-gradient(135deg, #4fd49b, #155e54); }
.pg--drive { background: linear-gradient(135deg, #f5b07a, #cc5d28); }
.pg--workshop { background: linear-gradient(135deg, #7ec5ee, #1b5d8a); }
.card__photo::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(0,0,0,.05), rgba(0,0,0,.18));
}
/* ---------- calendar ---------- */
.cal__head {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
margin-bottom: 14px;
}
.cal__title { font-size: 1.5rem; min-width: 220px; text-align: center; }
.navbtn {
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid var(--line-2);
background: var(--surface);
font-size: 1.3rem;
color: var(--ink-2);
cursor: pointer;
transition: all .14s ease;
}
.navbtn:hover { background: var(--brand); color: #fff; border-color: var(--brand); }
.navbtn:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
.cal__dow {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
margin-bottom: 8px;
}
.cal__dow span {
text-align: center;
font-size: .76rem;
font-weight: 700;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--muted);
}
.cal__grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
}
.day {
position: relative;
aspect-ratio: 1 / 1;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--surface);
padding: 8px;
text-align: left;
font: inherit;
cursor: default;
color: var(--ink-2);
display: flex;
flex-direction: column;
}
.day--empty { background: transparent; border-color: transparent; }
.day--has { cursor: pointer; transition: transform .12s ease, box-shadow .12s ease; }
.day--has:hover { transform: translateY(-2px); box-shadow: var(--sh-md); border-color: var(--brand); }
.day--has:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
.day__n { font-weight: 600; font-size: .92rem; }
.day--today .day__n {
background: var(--accent);
color: #fff;
width: 26px;
height: 26px;
border-radius: 50%;
display: grid;
place-items: center;
}
.day__dots { display: flex; gap: 4px; flex-wrap: wrap; margin-top: auto; }
.day__count {
position: absolute;
bottom: 6px;
right: 8px;
font-size: .68rem;
font-weight: 700;
color: var(--muted);
}
.cal__hint { text-align: center; color: var(--muted); font-size: .85rem; margin-top: 16px; }
/* ---------- impact / thermometer ---------- */
.impact {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 24px;
padding: 8px 0 56px;
}
.thermo, .donors {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 26px;
box-shadow: var(--sh-sm);
}
.thermo h2 { font-size: 1.5rem; }
.thermo__sub { color: var(--ink-2); margin: 8px 0 20px; }
.thermo__bar {
position: relative;
height: 22px;
border-radius: 999px;
background: rgba(31,122,109,.12);
overflow: hidden;
}
.thermo__fill {
position: absolute;
inset: 0 auto 0 0;
width: 0;
border-radius: 999px;
background: linear-gradient(90deg, var(--brand), var(--ok));
transition: width 1.4s cubic-bezier(.2,.7,.2,1);
}
.thermo__mark {
position: absolute;
top: -4px;
bottom: -4px;
width: 2px;
background: var(--accent-d);
}
.thermo__row {
display: flex;
justify-content: space-between;
margin-top: 12px;
font-weight: 600;
}
.thermo__raised { color: var(--brand-d); }
.thermo__goal { color: var(--muted); }
.donors h3 { font-size: 1.2rem; margin-bottom: 12px; }
.donors ul { list-style: none; margin: 0; padding: 0; }
.donors li {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 9px 0;
border-bottom: 1px solid var(--line);
font-size: .92rem;
}
.donors li span { color: var(--ink-2); }
.donors li b { color: var(--accent-d); }
/* ---------- footer ---------- */
.foot {
border-top: 1px solid var(--line);
background: var(--surface);
}
.foot__row {
padding: 24px 0;
display: flex;
justify-content: space-between;
gap: 14px;
flex-wrap: wrap;
color: var(--muted);
font-size: .85rem;
}
.foot__fine { font-style: italic; }
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 140%);
background: var(--ink);
color: #fff;
padding: 13px 20px;
border-radius: var(--r-md);
box-shadow: var(--sh-lg);
font-weight: 500;
font-size: .94rem;
z-index: 60;
max-width: 90vw;
transition: transform .35s cubic-bezier(.2,.9,.3,1.2);
}
.toast.show { transform: translate(-50%, 0); }
.toast.toast--ok { background: var(--brand-d); }
.toast.toast--warn { background: var(--accent-d); }
/* ---------- responsive ---------- */
@media (max-width: 880px) {
.hero { grid-template-columns: 1fr; gap: 28px; }
.hero__art { order: -1; }
.photo--hero { aspect-ratio: 16 / 10; }
.impact { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.topnav { display: none; }
}
@media (max-width: 520px) {
.topbar__row { height: 58px; }
.hero { padding: 32px 0 24px; }
.hero__stats { gap: 18px; }
.stat__num { font-size: 1.55rem; }
.controls { flex-direction: column; align-items: stretch; }
.viewtoggle { align-self: stretch; }
.seg { flex: 1; }
.events { grid-template-columns: 1fr; }
.cal__grid { gap: 5px; }
.day { padding: 5px; border-radius: 10px; }
.day__n { font-size: .8rem; }
.day__count { display: none; }
.cal__title { font-size: 1.25rem; min-width: 150px; }
.foot__row { flex-direction: column; }
}
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; }
}(function () {
"use strict";
// ---------- data (fictional) ----------
var TYPE_LABEL = { gala: "Gala", run: "Run", drive: "Drive", workshop: "Workshop" };
var MONTHS = ["January","February","March","April","May","June","July",
"August","September","October","November","December"];
// All events live in June 2026 so the calendar lands on a populated month.
var EVENTS = [
{ id: "e1", type: "gala", title: "Riverlight Benefit Gala", date: "2026-06-06",
time: "7:00 PM", loc: "The Old Mill Ballroom", desc: "Black-tie dinner & live auction to fund winter shelter beds.",
capacity: 220, taken: 198 },
{ id: "e2", type: "run", title: "Hope Riverside 10K", date: "2026-06-13",
time: "8:00 AM", loc: "Mill Race Park, North Gate", desc: "Family-friendly 10K & 2K — every entry feeds a child for a week.",
capacity: 600, taken: 412 },
{ id: "e3", type: "drive", title: "Summer Pantry Food Drive", date: "2026-06-17",
time: "10:00 AM", loc: "Riverline Community Hub", desc: "Drop off staples or volunteer to sort 8,000 meal kits.",
capacity: 80, taken: 80 },
{ id: "e4", type: "workshop", title: "Budgeting & Benefits Workshop", date: "2026-06-20",
time: "2:00 PM", loc: "Larkfield Library, Room B", desc: "Free guided session helping neighbors navigate aid programs.",
capacity: 40, taken: 33 },
{ id: "e5", type: "drive", title: "Back-to-School Backpack Drive", date: "2026-06-24",
time: "9:00 AM", loc: "Greenway Coffee Co.", desc: "Sponsor or assemble backpacks for 500 students.",
capacity: 120, taken: 71 },
{ id: "e6", type: "gala", title: "Donor Appreciation Evening", date: "2026-06-27",
time: "6:30 PM", loc: "Riverline Gardens Terrace", desc: "An intimate thank-you for our monthly giving circle.",
capacity: 90, taken: 64 },
{ id: "e7", type: "workshop", title: "Volunteer Leadership Lab", date: "2026-06-28",
time: "11:00 AM", loc: "Riverline Community Hub", desc: "Hands-on training for crew leads ahead of the summer push.",
capacity: 50, taken: 22 }
];
// ---------- elements ----------
var grid = document.getElementById("eventGrid");
var emptyState = document.getElementById("emptyState");
var listView = document.getElementById("listView");
var calendarView = document.getElementById("calendarView");
var calGrid = document.getElementById("calGrid");
var calTitle = document.getElementById("calTitle");
var toastEl = document.getElementById("toast");
var activeFilter = "all";
var rsvped = {}; // id -> true
var viewDate = new Date(2026, 5, 1); // June 2026
// ---------- toast helper ----------
var toastTimer;
function toast(msg, variant) {
toastEl.textContent = msg;
toastEl.className = "toast show" + (variant ? " toast--" + variant : "");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.className = "toast";
}, 2600);
}
// ---------- helpers ----------
function dayPart(iso) { return parseInt(iso.slice(8, 10), 10); }
function shortMonth(iso) { return MONTHS[parseInt(iso.slice(5, 7), 10) - 1].slice(0, 3); }
function seatState(ev) {
var left = ev.capacity - ev.taken;
if (left <= 0) return { left: 0, cls: "is-full", label: "Sold out · join waitlist" };
if (left <= ev.capacity * 0.12) return { left: left, cls: "is-low", label: left + " spots left" };
return { left: left, cls: "", label: left + " spots left" };
}
// ---------- render list ----------
function renderList() {
var rows = EVENTS.filter(function (ev) {
return activeFilter === "all" || ev.type === activeFilter;
});
grid.innerHTML = "";
emptyState.hidden = rows.length > 0;
rows.forEach(function (ev) {
var s = seatState(ev);
var going = !!rsvped[ev.id];
var li = document.createElement("li");
li.className = "card";
li.id = "card-" + ev.id;
var btnLabel = going ? "✓ You're going" : (s.left <= 0 ? "Join waitlist" : "RSVP");
var btnClass = "rsvp" + (going ? " is-going" : "");
li.innerHTML =
'<div class="card__photo pg--' + ev.type + '">' +
'<span class="card__type"><span class="dot dot--' + ev.type + '"></span>' + TYPE_LABEL[ev.type] + '</span>' +
'<span class="card__date"><b>' + dayPart(ev.date) + '</b><span>' + shortMonth(ev.date) + '</span></span>' +
'</div>' +
'<div class="card__body">' +
'<h3 class="card__title">' + ev.title + '</h3>' +
'<div class="card__meta">' +
'<span><i>🕑</i>' + ev.time + '</span>' +
'<span><i>📍</i>' + ev.loc + '</span>' +
'</div>' +
'<p class="card__desc">' + ev.desc + '</p>' +
'<div class="card__foot">' +
'<span class="seats ' + s.cls + '" data-seats="' + ev.id + '">' + (going ? "You're confirmed" : s.label) + '</span>' +
'<button class="' + btnClass + '" type="button" data-rsvp="' + ev.id + '">' + btnLabel + '</button>' +
'</div>' +
'</div>';
grid.appendChild(li);
});
}
// ---------- RSVP ----------
function handleRsvp(id) {
var ev = EVENTS.find(function (e) { return e.id === id; });
if (!ev) return;
var s = seatState(ev);
if (rsvped[id]) {
rsvped[id] = false;
ev.taken = Math.max(0, ev.taken - 1);
toast("RSVP cancelled for " + ev.title, "warn");
} else if (s.left <= 0) {
rsvped[id] = true;
toast("Added to the waitlist for " + ev.title + " — we'll be in touch!", "ok");
} else {
rsvped[id] = true;
ev.taken += 1;
toast("You're going to " + ev.title + "! See you on the " + dayPart(ev.date) + "th.", "ok");
}
renderList();
}
grid.addEventListener("click", function (e) {
var btn = e.target.closest("[data-rsvp]");
if (btn) handleRsvp(btn.getAttribute("data-rsvp"));
});
// ---------- filters ----------
document.querySelector(".filters").addEventListener("click", function (e) {
var chip = e.target.closest(".chip");
if (!chip) return;
activeFilter = chip.getAttribute("data-filter");
document.querySelectorAll(".chip").forEach(function (c) { c.classList.remove("is-active"); });
chip.classList.add("is-active");
renderList();
if (!calendarView.hidden) renderCalendar();
});
// ---------- view toggle ----------
document.querySelector(".viewtoggle").addEventListener("click", function (e) {
var seg = e.target.closest(".seg");
if (!seg) return;
var view = seg.getAttribute("data-view");
document.querySelectorAll(".seg").forEach(function (s) {
var on = s === seg;
s.classList.toggle("is-active", on);
s.setAttribute("aria-pressed", on ? "true" : "false");
});
var calendar = view === "calendar";
listView.hidden = calendar;
calendarView.hidden = !calendar;
if (calendar) renderCalendar();
});
// ---------- calendar ----------
function eventsForFilter() {
return EVENTS.filter(function (ev) {
return activeFilter === "all" || ev.type === activeFilter;
});
}
function renderCalendar() {
var year = viewDate.getFullYear();
var month = viewDate.getMonth();
calTitle.textContent = MONTHS[month] + " " + year;
var firstDow = new Date(year, month, 1).getDay();
var daysInMonth = new Date(year, month + 1, 0).getDate();
var today = new Date(2026, 5, 17); // demo "today" = Jun 17 2026
// map day -> events
var byDay = {};
eventsForFilter().forEach(function (ev) {
var d = new Date(ev.date + "T00:00:00");
if (d.getFullYear() === year && d.getMonth() === month) {
var k = d.getDate();
(byDay[k] = byDay[k] || []).push(ev);
}
});
calGrid.innerHTML = "";
for (var i = 0; i < firstDow; i++) {
var blank = document.createElement("div");
blank.className = "day day--empty";
calGrid.appendChild(blank);
}
for (var d = 1; d <= daysInMonth; d++) {
var evs = byDay[d] || [];
var has = evs.length > 0;
var cell = document.createElement(has ? "button" : "div");
cell.className = "day" + (has ? " day--has" : "");
if (has) cell.type = "button";
var isToday = year === today.getFullYear() && month === today.getMonth() && d === today.getDate();
if (isToday) cell.classList.add("day--today");
var dots = "";
var seen = {};
evs.forEach(function (ev) {
if (seen[ev.type]) return;
seen[ev.type] = true;
dots += '<span class="dot dot--' + ev.type + '"></span>';
});
cell.innerHTML =
'<span class="day__n">' + d + '</span>' +
(has ? '<span class="day__dots">' + dots + '</span>' : '') +
(has ? '<span class="day__count">' + evs.length + (evs.length > 1 ? " events" : " event") + '</span>' : '');
if (has) {
cell.setAttribute("aria-label",
MONTHS[month] + " " + d + ", " + evs.length + " event" + (evs.length > 1 ? "s" : ""));
(function (list, dayNum) {
cell.addEventListener("click", function () {
jumpToDayInList(list, dayNum);
});
})(evs, d);
}
calGrid.appendChild(cell);
}
}
function jumpToDayInList(evs, dayNum) {
var names = evs.map(function (e) { return e.title; }).join(" · ");
toast(evs.length + " event" + (evs.length > 1 ? "s" : "") + " on the " + dayNum + "th: " + names);
// switch to list and scroll to the first event of that day
document.querySelector('.seg[data-view="list"]').click();
var first = evs[0];
setTimeout(function () {
var card = document.getElementById("card-" + first.id);
if (card) {
card.scrollIntoView({ behavior: "smooth", block: "center" });
card.style.outline = "3px solid var(--accent)";
card.style.outlineOffset = "2px";
setTimeout(function () { card.style.outline = ""; }, 1800);
}
}, 60);
}
document.getElementById("prevMonth").addEventListener("click", function () {
viewDate.setMonth(viewDate.getMonth() - 1);
renderCalendar();
});
document.getElementById("nextMonth").addEventListener("click", function () {
viewDate.setMonth(viewDate.getMonth() + 1);
renderCalendar();
});
// ---------- donate buttons ----------
document.querySelectorAll("[data-donate]").forEach(function (b) {
b.addEventListener("click", function () {
toast("Thank you! In a live site this opens secure checkout. 💛", "ok");
});
});
// ---------- animated counters ----------
function animateCounters() {
document.querySelectorAll("[data-count]").forEach(function (el) {
var target = parseInt(el.getAttribute("data-count"), 10);
var start = performance.now();
var dur = 1300;
function step(now) {
var p = Math.min(1, (now - start) / dur);
var eased = 1 - Math.pow(1 - p, 3);
el.textContent = Math.round(target * eased).toLocaleString();
if (p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
});
}
// ---------- thermometer fill ----------
function fillThermo() {
var fill = document.getElementById("thermoFill");
if (fill) requestAnimationFrame(function () { fill.style.width = (93400 / 120000 * 100) + "%"; });
}
// ---------- init ----------
renderList();
animateCounters();
fillThermo();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Riverline Hope — Events & Fundraisers</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>
<a class="skip" href="#main">Skip to events</a>
<header class="topbar">
<div class="wrap topbar__row">
<a class="brand" href="#" aria-label="Riverline Hope home">
<span class="brand__mark" aria-hidden="true">✦</span>
<span class="brand__text">Riverline Hope</span>
</a>
<nav class="topnav" aria-label="Primary">
<a href="#main">Events</a>
<a href="#impact">Impact</a>
<a href="#">Our work</a>
<a href="#">About</a>
</nav>
<button class="btn btn--donate" type="button" data-donate>Donate</button>
</div>
</header>
<main id="main">
<section class="hero wrap" aria-labelledby="hero-title">
<div class="hero__copy">
<span class="eyebrow">Get involved · 2026 Season</span>
<h1 id="hero-title">Events & Fundraisers</h1>
<p class="hero__lede">
Join a gala, lace up for a run, or drop by a community drive. Every ticket and
RSVP helps Riverline Hope deliver meals, books, and shelter across the river district.
</p>
<div class="hero__stats" role="list">
<div class="stat" role="listitem">
<span class="stat__num" data-count="48200">0</span>
<span class="stat__label">meals served this year</span>
</div>
<div class="stat" role="listitem">
<span class="stat__num" data-count="1340">0</span>
<span class="stat__label">volunteers mobilized</span>
</div>
<div class="stat" role="listitem">
<span class="stat__num" data-count="26">0</span>
<span class="stat__label">events this season</span>
</div>
</div>
<div class="trust" aria-label="Trust and transparency">
<span class="badge">★ Registered Charity #88-4471</span>
<span class="badge">Donations tax-deductible</span>
<span class="badge">92¢ of every $1 to programs</span>
</div>
</div>
<figure class="hero__art" aria-hidden="true">
<div class="photo photo--hero">
<span class="photo__cap">Volunteers packing the spring food drive</span>
</div>
</figure>
</section>
<section class="controls wrap" aria-label="Event controls">
<div class="filters" role="group" aria-label="Filter events by type">
<button class="chip is-active" type="button" data-filter="all">All</button>
<button class="chip" type="button" data-filter="gala"><span class="dot dot--gala"></span>Galas</button>
<button class="chip" type="button" data-filter="run"><span class="dot dot--run"></span>Runs</button>
<button class="chip" type="button" data-filter="drive"><span class="dot dot--drive"></span>Drives</button>
<button class="chip" type="button" data-filter="workshop"><span class="dot dot--workshop"></span>Workshops</button>
</div>
<div class="viewtoggle" role="group" aria-label="Switch view">
<button class="seg is-active" type="button" data-view="list" aria-pressed="true">List</button>
<button class="seg" type="button" data-view="calendar" aria-pressed="false">Calendar</button>
</div>
</section>
<!-- LIST VIEW -->
<section class="panel wrap" id="listView" aria-label="Events list">
<ul class="events" id="eventGrid"><!-- injected by JS --></ul>
<p class="empty" id="emptyState" hidden>No events match that filter yet — check back soon.</p>
</section>
<!-- CALENDAR VIEW -->
<section class="panel wrap" id="calendarView" hidden aria-label="Events calendar">
<div class="cal__head">
<button class="navbtn" type="button" id="prevMonth" aria-label="Previous month">‹</button>
<h2 class="cal__title" id="calTitle">June 2026</h2>
<button class="navbtn" type="button" id="nextMonth" aria-label="Next month">›</button>
</div>
<div class="cal__dow" aria-hidden="true">
<span>Sun</span><span>Mon</span><span>Tue</span><span>Wed</span><span>Thu</span><span>Fri</span><span>Sat</span>
</div>
<div class="cal__grid" id="calGrid"><!-- injected by JS --></div>
<p class="cal__hint">Days with a colored dot have a scheduled event. Click a day to RSVP from the list.</p>
</section>
<section class="impact wrap" id="impact" aria-labelledby="impact-title">
<div class="thermo">
<h2 id="impact-title">Spring Campaign goal</h2>
<p class="thermo__sub">Help us reach <strong>$120,000</strong> to fund 30,000 summer meals.</p>
<div class="thermo__bar" role="progressbar" aria-valuemin="0" aria-valuemax="120000" aria-valuenow="93400" aria-label="Campaign progress">
<span class="thermo__fill" id="thermoFill"></span>
<span class="thermo__mark" style="left:78%"></span>
</div>
<div class="thermo__row">
<span class="thermo__raised">$93,400 raised</span>
<span class="thermo__goal">of $120,000</span>
</div>
</div>
<aside class="donors" aria-label="Recent supporters">
<h3>Recent supporters</h3>
<ul>
<li><span>Marisol & Dev Okafor</span><b>$2,500</b></li>
<li><span>The Larkfield Foundation</span><b>$10,000</b></li>
<li><span>Anonymous</span><b>$500</b></li>
<li><span>Greenway Coffee Co.</span><b>$1,200</b></li>
</ul>
<button class="btn btn--donate btn--block" type="button" data-donate>Donate to the campaign</button>
</aside>
</section>
</main>
<footer class="foot">
<div class="wrap foot__row">
<p>Riverline Hope · 14 Mill Race Lane, Riverline · Registered Charity #88-4471</p>
<p class="foot__fine">Illustrative demo — fictional organization. No real donations are processed.</p>
</div>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Events Calendar
A get-involved page for the fictional Riverline Hope charity, built in the warm, human nonprofit palette — mission teal-green, a donate-orange accent, a humane Fraunces serif over Inter. The hero pairs a prominent Donate CTA with transparency cues: animated impact counters (meals served, volunteers mobilized) and trust badges for registered-charity status and tax-deductibility. Events render as photo cards showing the event type, date chip, time, location, a short description and a live spots-left counter that shifts to amber when seats run low and red when an event sells out.
Chip filters narrow the season to galas, runs, drives or workshops, and a List / Calendar toggle swaps the card grid for a full month grid. In calendar view each day marks its scheduled events with colored category dots and a count; clicking a populated day fires a toast and jumps back to that event in the list, highlighting the card. Month navigation steps forward and back through the season.
RSVP buttons confirm attendance, decrement remaining capacity, can be cancelled, and offer a waitlist when an event is full — each action announced through a small toast helper. A Spring Campaign thermometer animates to its raised total beside a recent-supporters donor list, all keyboard-usable with visible focus rings, AA-contrast text and a layout that collapses cleanly to a single column down to 360px.
Illustrative UI only — fictional organization, not a real charity or donation system.