import Link from "next/link"; import { notFound, redirect } from "next/navigation"; import { createAdminClient } from "@/lib/supabase/admin"; import { createClient } from "@/lib/supabase/server"; import AnalyticsCharts from "./AnalyticsCharts"; 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: string; status: string; } interface Row { campaign_id: string; occurred_at: string; page_path: string | null; referrer: string | null; is_bot: boolean; viewport?: boolean; } 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 }; } function dayBuckets(rows: Row[], days = 30): { date: string; count: number }[] { const today = new Date(); today.setUTCHours(0, 0, 0, 0); const out: { date: string; count: number }[] = []; const map = new Map(); for (const r of rows) { const d = r.occurred_at.slice(0, 10); map.set(d, (map.get(d) || 0) + 1); } for (let i = days - 1; i >= 0; i--) { const d = new Date(today.getTime() - i * 86400000); const key = d.toISOString().slice(0, 10); out.push({ date: key, count: map.get(key) || 0 }); } return out; } function topGroup(rows: Row[], key: keyof Row, limit = 8): { label: string; count: number }[] { const m = new Map(); for (const r of rows) { const v = (r[key] || "(none)") as string; m.set(v, (m.get(v) || 0) + 1); } return [...m.entries()] .sort((a, b) => b[1] - a[1]) .slice(0, limit) .map(([label, count]) => ({ label, count })); } function referrerDomain(r: string | null): string { if (!r) return "(direct)"; try { const u = new URL(r); return u.hostname || "(direct)"; } catch { return "(invalid)"; } } export default async function BannerAnalyticsPage({ params, }: { params: Promise<{ id: string }>; }) { const guard = await requireAdmin(); if (!guard.ok) redirect("/admin"); const { id } = await params; const admin = createAdminClient(); const { data: banner } = await admin .from("banner_creatives") .select("id, slug, name, image_path, click_url, size, status") .eq("id", id) .maybeSingle(); if (!banner) notFound(); const b = banner as BannerRow; const sinceIso = new Date(Date.now() - 30 * 86400000).toISOString(); const [imp, clk] = await Promise.all([ admin .from("ad_impressions") .select("campaign_id, occurred_at, page_path, referrer, is_bot, viewport") .eq("campaign_id", id) .gte("occurred_at", sinceIso) .order("occurred_at", { ascending: false }) .limit(50000), admin .from("ad_clicks") .select("campaign_id, occurred_at, page_url, referrer, is_bot") .eq("campaign_id", id) .gte("occurred_at", sinceIso) .order("occurred_at", { ascending: false }) .limit(50000), ]); const impRows = (imp.data || []) as Row[]; const clkRows = (clk.data || []).map((r: any) => ({ campaign_id: r.campaign_id, occurred_at: r.occurred_at, page_path: r.page_url || null, referrer: r.referrer, is_bot: r.is_bot, })) as Row[]; const impHuman = impRows.filter((r) => !r.is_bot); const clkHuman = clkRows.filter((r) => !r.is_bot); const impSeries = dayBuckets(impHuman, 30); const clkSeries = dayBuckets(clkHuman, 30); const impSeriesBot = dayBuckets(impRows, 30); const clkSeriesBot = dayBuckets(clkRows, 30); const topPages = topGroup(impHuman, "page_path"); const topReferrers = topGroup( impHuman.map((r) => ({ ...r, referrer: referrerDomain(r.referrer) })), "referrer", ); return (
← Back to banners
{b.name}

{b.name}

{b.size} · {b.status} ·{" "} {b.click_url && ( {b.click_url.slice(0, 80)}{b.click_url.length > 80 ? "…" : ""} )}

); }