"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
); } 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 [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 */ } } useEffect(() => { loadFeed(); loadBoard(); 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.
}
Who he is
Francis "Bam" Tan Aquino IV — Senator for Pampanga. The first gamer to reach the Philippine Senate: a runner-up in the 2025 PalaPinas esports championship, and a legislator who has made the fight for college students, scholarships, and fair electricity his signature. This desk follows the wire for every story that bears his name — from verified Philippine and international news outlets only.
{/* ── 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 />