125 lines
3.9 KiB
TypeScript
125 lines
3.9 KiB
TypeScript
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<Record<string, BannerStats>> {
|
|
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<string, number> = {};
|
|
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<string>([
|
|
...Object.keys(imp1), ...Object.keys(imp7), ...Object.keys(impL),
|
|
...Object.keys(clk1), ...Object.keys(clk7), ...Object.keys(clkL),
|
|
]);
|
|
const out: Record<string, BannerStats> = {};
|
|
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 (
|
|
<main className="mx-auto max-w-6xl px-6 py-10 text-[#e5e7eb]">
|
|
<header className="mb-8 border-b border-[#222] pb-4 flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Banner creatives</h1>
|
|
<p className="text-sm text-[#9ca3af] mt-1">
|
|
{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.
|
|
</p>
|
|
</div>
|
|
<Link href="/admin" className="text-sm text-[#60a5fa] hover:underline">
|
|
← Admin home
|
|
</Link>
|
|
</header>
|
|
|
|
<BannerRows banners={banners} stats={stats} />
|
|
</main>
|
|
);
|
|
}
|