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