/* ═══════════════════════════════════════════════════════════════ FRONT PAGE ROUTE — read-only, mounted under /api GET /front-page the home page's configuration, resolved Everything the admin's Front page editor holds, shaped for the page: hidden sections dropped, stats counted, paths carrying their actions, and the countdown's event looked up. The retreats carousel and the timeline rail are not in here. They fetch /events and /history themselves, as they do on their own pages, so the rules for which events and entries are public live in one place each. This route only says whether those bands appear and under what heading. ── Stats ── A stat's source picks a query from STAT_QUERIES. Each counts exactly what the matching public page shows: published rows, and for awards only public citations to published people. A count that disagreed with the page it summarises would be worse than none. 'manual' and 'years_since' read the row's own value. ── Countdown ── The pinned event if it is still published and not over; otherwise the next published, non-cancelled event that hasn't ended. "Hasn't ended" is COALESCE(ends_on, starts_on) >= today, so a running series with a start date in the past still counts. The client works out the next meeting of a series from `series`. ═══════════════════════════════════════════════════════════════ */ import { Hono } from "hono"; import { asBool, shapeSeries } from "../shape.js"; const home = new Hono(); const CACHE = "public, max-age=60, stale-while-revalidate=300"; const json = (c, body) => c.json(body, 200, { "Cache-Control": CACHE }); const PAGE_ID = "home"; const STAT_QUERIES = { regions: `SELECT COUNT(*) AS n FROM organizations WHERE kind = 'region' AND is_published = 1`, chapters: `SELECT COUNT(*) AS n FROM organizations WHERE kind = 'chapter' AND is_published = 1`, partners: `SELECT COUNT(*) AS n FROM organizations WHERE kind = 'partner' AND is_published = 1`, events_held: `SELECT COUNT(*) AS n FROM v_events WHERE is_published = 1 AND effective_status = 'past'`, retreats_held: `SELECT COUNT(*) AS n FROM v_events WHERE is_published = 1 AND effective_status = 'past' AND event_type = 'retreat'`, people: `SELECT COUNT(*) AS n FROM people WHERE is_published = 1`, awards_given: `SELECT COUNT(*) AS n FROM person_awards pa JOIN people p ON p.id = pa.person_id AND p.is_published = 1 JOIN awards a ON a.id = pa.award_id AND a.is_published = 1 WHERE pa.is_public = 1`, }; /* The number as a string, or null when there's nothing to print — a manual stat nobody filled in, or a year that isn't one. */ function statValue(db, row) { if (row.source === "manual") return row.value || null; if (row.source === "years_since") { const year = Number.parseInt(row.value ?? "", 10); if (!Number.isInteger(year)) return null; return String(Math.max(0, new Date().getFullYear() - year)); } const sql = STAT_QUERIES[row.source]; return sql ? String(db.prepare(sql).get().n) : null; } function shapeCountdown(row) { if (!row) return null; return { id: row.id, title: row.title, theme: row.theme, starts_on: row.starts_on, ends_on: row.ends_on, date_label: row.date_label, location_label: row.location_label, is_online: asBool(row.is_online), color: row.effective_color, event_logo: row.event_logo, series: shapeSeries(row), }; } home.get("/front-page", (c) => { const db = c.get("db"); const page = db.prepare(`SELECT * FROM front_page WHERE id = ?`).get(PAGE_ID); // Migration 017 creates the row and the engine refuses to delete // it, so this is a database that hasn't been migrated. Say so. if (!page) return c.json({ error: "The front page hasn't been set up." }, 500); const byOrder = (table) => db.prepare(`SELECT * FROM ${table} WHERE page_id = ? ORDER BY sort_order`).all(PAGE_ID); const sections = byOrder("front_page_sections") .filter((row) => !asBool(row.is_hidden)) .map((row) => ({ section: row.section, title: row.title, blurb: row.blurb })); const slides = byOrder("front_page_slides").map((row) => ({ media: row.media, alt: row.alt, caption: row.caption, link_url: row.link_url, })); const stats = byOrder("front_page_stats") .map((row) => ({ label: row.label, value: statValue(db, row), suffix: row.suffix, note: row.note, })) .filter((stat) => stat.value !== null); const actions = db.prepare( `SELECT label, description, url FROM front_page_path_actions WHERE path_id = ? ORDER BY sort_order`, ); const paths = byOrder("front_page_paths") .map((row) => ({ label: row.label, icon: row.icon, blurb: row.blurb, actions: actions.all(row.id), })) // A path with nothing to do is a dead tab. .filter((path) => path.actions.length > 0); const notOver = `is_published = 1 AND effective_status != 'cancelled' AND COALESCE(ends_on, starts_on) >= date('now')`; const pinned = page.countdown_event_id ? db .prepare(`SELECT * FROM v_events WHERE id = ? AND ${notOver}`) .get(page.countdown_event_id) : null; const next = pinned ?? db .prepare( `SELECT * FROM v_events WHERE ${notOver} ORDER BY starts_on, title LIMIT 1`, ) .get(); return json(c, { front_page: { hero: { mode: page.hero_mode, eyebrow: page.eyebrow, headline: page.headline, subhead: page.subhead, primary: page.primary_label && page.primary_url ? { label: page.primary_label, url: page.primary_url } : null, secondary: page.secondary_label && page.secondary_url ? { label: page.secondary_label, url: page.secondary_url } : null, slide_seconds: page.slide_seconds, slides, livestream: page.livestream_url ? { url: page.livestream_url, title: page.livestream_title } : null, }, sections, stats, paths, countdown: shapeCountdown(next), }, }); }); export default home;