import Link from "next/link"; import { createAdminClient } from "@/lib/supabase/admin"; import { createClient } from "@/lib/supabase/server"; import { redirect } from "next/navigation"; import BannerRows from "./BannerRows"; export const dynamic = "force-dynamic"; export const revalidate = 0; 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; } async function requireAdmin() { const sb = await createClient(); const { data: { user } } = await sb.auth.getUser(); if (!user) return { ok: false as const }; const { data: profile } = await sb .from("user_profiles") .select("role,is_active") .eq("id", user.id) .maybeSingle(); if (!profile?.is_active) return { ok: false as const }; if (!["administrator", "admin"].includes((profile as any).role)) return { ok: false as const }; return { ok: true as const }; } async function loadStats(): Promise> { const admin = createAdminClient(); const now = new Date(); const d1Iso = new Date(now.getTime() - 24 * 3600 * 1000).toISOString(); const d7Iso = new Date(now.getTime() - 7 * 24 * 3600 * 1000).toISOString(); async function bucket(table: "ad_impressions" | "ad_clicks", since: string | null) { let q = admin.from(table).select("campaign_id"); if (since) q = q.gte("occurred_at", since); const { data } = await q.limit(100000); const counts: Record = {}; for (const r of (data || []) as any[]) { counts[r.campaign_id] = (counts[r.campaign_id] || 0) + 1; } return counts; } const [imp1, imp7, impL, clk1, clk7, clkL] = await Promise.all([ bucket("ad_impressions", d1Iso), bucket("ad_impressions", d7Iso), bucket("ad_impressions", null), bucket("ad_clicks", d1Iso), bucket("ad_clicks", d7Iso), bucket("ad_clicks", null), ]); const ids = new Set([ ...Object.keys(imp1), ...Object.keys(imp7), ...Object.keys(impL), ...Object.keys(clk1), ...Object.keys(clk7), ...Object.keys(clkL), ]); const out: Record = {}; for (const id of ids) { out[id] = { d1: { impressions: imp1[id] || 0, clicks: clk1[id] || 0 }, d7: { impressions: imp7[id] || 0, clicks: clk7[id] || 0 }, lifetime: { impressions: impL[id] || 0, clicks: clkL[id] || 0 }, }; } return out; } export default async function AdminBannersPage() { const guard = await requireAdmin(); if (!guard.ok) redirect("/admin"); const admin = createAdminClient(); const { data: rows } = await admin .from("banner_creatives") .select("*") .order("size", { ascending: true }) .order("slug", { ascending: true }); const banners = (rows || []) as BannerRow[]; const stats = await loadStats(); return (

Banner creatives

{banners.length} banners · live data from bb.banner_creatives. Stats roll forward in real time from bb.ad_impressions / bb.ad_clicks. Renderer cache TTL is 5 min — edits surface after the next refresh.

← Admin home
); }