"use client"; import { useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; interface BannerRow { id: string; slug: string; name: string; image_path: string; click_url: string | null; size: "300x250" | "300x600" | "728x90"; start_date: string | null; end_date: string | null; status: string; adsanity_source_id: number | null; notes: string | null; updated_at: string; } interface StatBucket { impressions: number; clicks: number; } interface BannerStats { d1: StatBucket; d7: StatBucket; lifetime: StatBucket; } function fmtDate(iso: string | null): string { if (!iso) return ""; return iso.slice(0, 10); } function pct(n: number, d: number): string { if (!d) return "—"; return ((n / d) * 100).toFixed(2) + "%"; } function fmtNum(n: number): string { return n.toLocaleString(); } function StatBlock({ label, b }: { label: string; b: StatBucket }) { return (
{label}
{fmtNum(b.impressions)} imp · {fmtNum(b.clicks)} clk · {pct(b.clicks, b.impressions)}
); } export default function BannerRows({ banners, stats, }: { banners: BannerRow[]; stats: Record; }) { const router = useRouter(); const [pending, startTransition] = useTransition(); const [edits, setEdits] = useState>>({}); const [err, setErr] = useState(null); const [saved, setSaved] = useState(null); function field(id: string, key: keyof BannerRow, value: any) { setEdits((e) => ({ ...e, [id]: { ...e[id], [key]: value } })); } async function save(id: string) { setErr(null); setSaved(null); const patch = edits[id]; if (!patch || Object.keys(patch).length === 0) return; const body: Record = { ...patch }; if (body.start_date === "") body.start_date = null; if (body.end_date === "") body.end_date = null; const res = await fetch(`/api/admin/banners/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!res.ok) { const j = await res.json().catch(() => ({})); setErr(j.error || `HTTP ${res.status}`); return; } setSaved(id); setEdits((e) => { const n = { ...e }; delete n[id]; return n; }); startTransition(() => router.refresh()); } const empty: BannerStats = { d1: { impressions: 0, clicks: 0 }, d7: { impressions: 0, clicks: 0 }, lifetime: { impressions: 0, clicks: 0 }, }; return (
{err &&
{err}
} {banners.map((b) => { const dirty = !!edits[b.id]; const current: BannerRow = { ...b, ...(edits[b.id] || {}) }; const s = stats[b.id] || empty; return (
{b.name}
{b.size}
Image path {b.image_path} {b.adsanity_source_id && ( AdSanity #{b.adsanity_source_id} )}
{saved === b.id && Saved} {dirty && saved !== b.id && Unsaved}
View detailed analytics →
); })}
); }