"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { CATEGORIES, sectionName } from "../lib/categories.js"; import { clipText, dek } from "../lib/text.js"; import { PHOTOS, commonsUrl } from "../lib/photos.js"; // BidaAngMamamayan — a PUBLIC tribute news desk for Senator BAM Aquino. // No login, no accounts, no "I like this" — the site's whole identity is the // subject. Views: front, each desk, and the community board. The reader is a // side drawer over the current view. const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; function fmtDate(d) { return `${DAY_NAMES[d.getDay()]}, ${MONTHS[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`; } function ago(iso) { if (!iso) return ""; const h = (Date.now() - new Date(iso).getTime()) / 3600e3; if (!Number.isFinite(h) || h < 0) return ""; if (h < 1) return `${Math.max(1, Math.round(h * 60))}m`; if (h < 24) return `${Math.round(h)}h`; return `${Math.round(h / 24)}d`; } function imgSrc(u) { if (!u) return null; return `/api/img?u=${encodeURIComponent(u)}`; } // The masthead wordmark: BIDA ANG MAMAMAYAN, with the B-A-M letters carried in // gold (bold) as the tribute's highlight. function Brand() { return (
BidaAngMamamayan
); } // The live "Who he is" box. Text comes from /api/bio (the crawl tick caches // it from the Wikipedia article, so it's current; the box links straight to // the article). Until the first bio lands, a short accurate default shows. function WhoBox({ bio }) { const text = bio && bio.text; const updated = bio && bio.updatedAt ? new Date(bio.updatedAt).toLocaleDateString("en-PH", { month: "long", day: "numeric", year: "numeric" }) : null; return (
Who he is
{text ? ( <> {text}
Full article on Wikipedia ↗
) : ( Paolo Benigno "Bam" Aguirre Aquino IV — senator of the Philippines, and the first gamer to reach the chamber. )}
SOURCE: WIKIPEDIA{updated ? ` · UPDATED ${updated.toUpperCase()}` : ""}
); } export default function Home() { const [view, setView] = useState("front"); // front | | board const [edition, setEdition] = useState(null); const [desks, setDesks] = useState([]); const [stats, setStats] = useState(null); const [boardPosts, setBoardPosts] = useState(null); const [boardCount, setBoardCount] = useState(0); const [bio, setBio] = useState(null); // live "Who he is" (cached from Wikipedia) const [reading, setReading] = useState(null); // story in the drawer const [toast, setToast] = useState(""); const toastTimer = useRef(null); const [loadErr, setLoadErr] = useState(""); const say = useCallback((msg) => { setToast(msg); clearTimeout(toastTimer.current); toastTimer.current = setTimeout(() => setToast(""), 3200); }, []); async function loadFeed() { try { const r = await fetch("/api/feed", { cache: "no-store", signal: AbortSignal.timeout(20000) }); const j = await r.json(); if (j.ok) { setEdition(j.feed || null); setDesks(j.desks || []); setStats(j.stats || null); setLoadErr(""); } else setLoadErr("The desk is still warming up — check back in a minute."); } catch { setLoadErr("Could not reach the desk. The site may be mid-deploy — try again shortly."); } } async function loadBoard() { try { const r = await fetch("/api/board", { cache: "no-store", signal: AbortSignal.timeout(15000) }); const j = await r.json(); if (j.ok) { setBoardPosts(j.posts || []); setBoardCount(j.count || 0); } } catch { /* board is optional chrome on the front view */ } } async function loadBio() { try { const r = await fetch("/api/bio", { cache: "no-store", signal: AbortSignal.timeout(10000) }); const j = await r.json(); if (j.ok && j.text) setBio(j); } catch { /* bio box shows the static fallback if this fails */ } } useEffect(() => { loadFeed(); loadBoard(); loadBio(); return () => clearTimeout(toastTimer.current); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Refresh the edition every 5 min so a long-open tab keeps ticking. useEffect(() => { const t = setInterval(loadFeed, 5 * 60 * 1000); return () => clearInterval(t); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const items = useMemo(() => (edition ? edition.items || [] : []), [edition]); const namedItems = useMemo(() => items.filter((i) => i.named).sort((a, b) => b.score - a.score), [items]); const lead = useMemo(() => { if (!items.length) return null; const n = namedItems[0]; if (n) return n; return items[0]; }, [items, namedItems]); const deskByKey = useMemo(() => new Map(desks.map((d) => [d.key, d])), [desks]); // The view's story list: the whole edition on front; a desk's stories on a // desk view. The desk grid itself feeds from the pool (deskHeadlines), which // is a superset of the ranked edition — use whichever has more. const viewItems = useMemo(() => { if (view === "front") return items; if (view === "board") return []; const d = deskByKey.get(view); const fromPool = d ? d.stories || [] : []; const fromEdition = items.filter((i) => i.category === view); const map = new Map(); for (const s of [...fromEdition, ...fromPool]) map.set(s.url, s); return [...map.values()].sort((a, b) => b.score - a.score || new Date(b.published || 0) - new Date(a.published || 0)); }, [view, items, deskByKey]); function openStory(s) { if (!s) return; setReading({ story: s, state: "loading", article: null, realUrl: s.url }); } function closeReading() { setReading(null); } // ── ticker: named-BAM headlines scroll, pause on hover ───────────────── const tickerItems = useMemo(() => namedItems.slice(0, 12), [namedItems]); // ── board posting ────────────────────────────────────────────────────── const [nick, setNick] = useState(""); const [msg, setMsg] = useState(""); const [posting, setPosting] = useState(false); const [postErr, setPostErr] = useState(""); const [freshId, setFreshId] = useState(null); async function doPost(e) { e.preventDefault(); setPostErr(""); setPosting(true); try { const r = await fetch("/api/board", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ nick, message: msg }), signal: AbortSignal.timeout(15000), }); const j = await r.json(); if (j.ok && j.post) { setBoardPosts((p) => (p ? [j.post, ...p] : [j.post])); setBoardCount((c) => c + 1); setMsg(""); setFreshId(j.post.id); say("Posted. Thank you — the board is full of you."); } else { setPostErr(j.error || "Could not post right now."); } } catch { setPostErr("Network hiccup — try again."); } finally { setPosting(false); } } const deskLabel = view === "front" ? "Front Desk" : view === "board" ? "The Board" : sectionName(view); return ( <>
Bida ang Mamamayan
The supporter tribute desk for Senator Bam Aquino — the gamer of the Philippine Senate, champion of college students, scholarships, and the people's power bill. Every story that names him, crawled live from the wire.
DISCLAIMER — BidaAngMamamayan is built and run by supporters of Senator Bam Aquino. It is not affiliated with, endorsed by, or operated by the senator or his office. News items belong to their original publishers; this site aggregates them for tribute purposes only.
{stats && (
LIVE DESK
STORIES {edition ? edition.total : "—"}
LAST 24H {stats.today ?? "—"}
NAMED BAM {edition ? edition.named : "—"}
BOARD {boardCount}
EDITION {fmtDate(new Date())}
)} {edition && (
EDITION STRENGTH — {edition.total} WIRE STORIES LOADED
)} {tickerItems.length > 1 && (
BAM WIRE
{[...tickerItems, ...tickerItems].map((s, i) => ( {ago(s.published) || s.source} openStory(s)}>{s.title} ))}
)}
{loadErr && !edition && (
{loadErr}
)} {view === "front" && ( <> {/* ── IN PICTURES — credited public photos, its own full-width band ── */}
In Pictures Public photos of the senator — every image credited to its source
{PHOTOS.map((p, i) => (
window.open(commonsUrl(p.commons), "_blank", "noopener")}>
{p.caption}
📷 {p.source}
{p.credit}
))}
Click a photo to view it on Wikimedia Commons. Photos are public-domain, sourced from the senator's official Senate / social pages. This section is separate from the news wire.
{!edition && ( <>
)} {lead && (
openStory(lead)}>
{!lead.image && BAM}
{lead.named && ★ NAMED BAM} {sectionName(lead.category)} {lead.source}

{lead.title}

{lead.summary &&
{dek(lead.summary, 220)}
}
{ago(lead.published)} · {lead.desk || sectionName(lead.category)} · READ IN THE DESK →
)}
On the wire {viewItems.length} stories
{viewItems.slice(0, 14).map((s) => ( openStory(s)} /> ))} {viewItems.length === 0 &&
The wire is quiet — the next crawl tick is on the way.
}
In this edition {desks.length} desks
{desks.map((d) => (
{ setView(d.key); window.scrollTo({ top: 0 }); }}>
{d.label}{d.count}
{clipText(d.stories[0].title, 90)}
{ago(d.stories[0].published)} · {d.stories[0].source}
))} {desks.length === 0 &&
Desks staff as the wire fills up.
}
{/* ── THE BOARD — its own prominent full-width band, not a news desk ── */}
Thank you, Senator BAM The community board — say it to the senator, no account needed
setNick(e.target.value)} maxLength={24} placeholder="e.g. PampangaProud, BamGang13" required />