"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.
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.
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 BAMThe community board — say it to the senator, no account needed