"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 (
);
}
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.
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.