banners: refresh 8 creatives + bb.banner_creatives + /admin/banners
Backend:
bb.banner_creatives (new) — image_path, click_url, size, start/end_date,
status, adsanity_source_id. Seeded with 8 banners from broadcas_temp
WP-export DB (post_type=ads): LiveU 728x90, Blackmagic DaVinci Resolve
20 (300x600), Studio Hero, Magewell Pro-Convert, LiveU PAYG, AJA
ColorBox, Zixi, Lectrosonics (all 300x250). Dates and click URLs come
from the AdSanity rows; current creatives downloaded from Wayback
snapshot 20260316123737.
src/lib/ads.ts:
- Reads bb.banner_creatives via anon supabase-js client, gated by
status='active' AND start<=NOW()<=end.
- In-process 5-min TTL cache with stale-while-revalidate. Module load
primes the cache; clients see fallback on first hit, live DB on
subsequent.
- Hardcoded FALLBACK list mirrors the seeded rows so the site keeps
rendering banners if Supabase is unreachable during deploy.
- Public helpers (rotateAll, pickAds, ADS_728X90, ADS_300X250,
FIXED_300X600) remain synchronous so existing client component
callsites work unchanged.
/admin/banners:
Admin-only list + inline editor for every banner_creatives row:
name, click URL, start/end date, status (active|scheduled|paused|
expired). PATCH via /api/admin/banners/[id] — service_role bypass for
the write, user_profiles.role admin/administrator for the gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
59
src/app/api/admin/banners/[id]/route.ts
Normal file
59
src/app/api/admin/banners/[id]/route.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { createClient } from "@/lib/supabase/server";
|
||||
import { createAdminClient } from "@/lib/supabase/admin";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
async function requireAdmin() {
|
||||
const sb = await createClient();
|
||||
const { data: { user } } = await sb.auth.getUser();
|
||||
if (!user) return { ok: false as const, status: 401, error: "Unauthorized" };
|
||||
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, status: 403, error: "Inactive" };
|
||||
if (!["administrator", "admin"].includes((profile as any).role)) {
|
||||
return { ok: false as const, status: 403, error: "Insufficient permissions" };
|
||||
}
|
||||
return { ok: true as const };
|
||||
}
|
||||
|
||||
const ALLOWED_FIELDS = new Set([
|
||||
"name",
|
||||
"click_url",
|
||||
"image_path",
|
||||
"size",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"status",
|
||||
"notes",
|
||||
]);
|
||||
|
||||
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const guard = await requireAdmin();
|
||||
if (!guard.ok) return NextResponse.json({ error: guard.error }, { status: guard.status });
|
||||
|
||||
const { id } = await params;
|
||||
const body = (await req.json()) as Record<string, any>;
|
||||
const patch: Record<string, any> = {};
|
||||
for (const [k, v] of Object.entries(body)) {
|
||||
if (ALLOWED_FIELDS.has(k)) patch[k] = v;
|
||||
}
|
||||
if (Object.keys(patch).length === 0) {
|
||||
return NextResponse.json({ error: "No allowed fields supplied" }, { status: 400 });
|
||||
}
|
||||
patch.updated_at = new Date().toISOString();
|
||||
|
||||
const admin = createAdminClient();
|
||||
const { data, error } = await admin
|
||||
.from("banner_creatives")
|
||||
.update(patch)
|
||||
.eq("id", id)
|
||||
.select("id, slug, status")
|
||||
.single();
|
||||
if (error || !data) return NextResponse.json({ error: error?.message || "Not found" }, { status: 500 });
|
||||
|
||||
return NextResponse.json({ ok: true, banner: data });
|
||||
}
|
||||
Reference in New Issue
Block a user