feat(ads): dual tracking - AdSanity iframe + bb.banner_analytics with fixed imports and priority prop

This commit is contained in:
Ryan
2026-05-11 02:54:13 +00:00
parent fc003fd285
commit 031ff6f220

View File

@@ -1,103 +1,88 @@
"use client";
'use client';
import { useEffect, useRef } from 'react';
import AppImage from '@/components/ui/AppImage';
import { createClient } from '@/lib/supabase/client';
import { useEffect, useRef } from "react";
import { usePathname } from "next/navigation";
import AppImage from "@/components/ui/AppImage";
import { createClient } from "@/lib/supabase/client";
import type { Ad } from "@/lib/ads";
const SIZE_DIM: Record<Ad["size"], { w: number; h: number }> = {
"300x250": { w: 300, h: 250 },
"300x600": { w: 300, h: 600 },
"728x90": { w: 728, h: 90 },
};
function getSessionId(): string {
if (typeof window === "undefined") return "";
let sid = sessionStorage.getItem("bb_session_id");
if (!sid) {
sid = crypto.randomUUID();
sessionStorage.setItem("bb_session_id", sid);
}
return sid;
interface Ad {
src: string;
label: string;
size: string;
click_url?: string;
adsanity_id?: string;
}
export default function AdImage({
ad,
className = "",
priority = false,
}: {
ad: Ad;
className?: string;
priority?: boolean;
}) {
const { w, h } = SIZE_DIM[ad.size];
const anchorRef = useRef<HTMLAnchorElement | null>(null);
const impressionFiredRef = useRef(false);
const pathname = usePathname();
const ADSANITY_CAMPAIGN_MAP: Record<string, string> = {
'riedel': 'ad-210571',
'look-solutions': 'ad-208185',
'opengear': 'ad-200174',
'telycam': 'ad-191740',
'livu-728': 'ad-189081',
'aja': 'ad-164285',
'studio-suite': 'ad-162714',
'zixi': 'ad-156737',
'lectrosonics': 'ad-156727',
'livu-300': 'ad-156486',
'magewell': 'ad-154296',
'blackmagic': 'ad-210571',
'magewell-300': 'ad-154296',
'studio-suite-300': 'ad-162714',
'zixi-300': 'ad-156737',
'lectrosonics-300': 'ad-156727',
'aja-300': 'ad-164285',
'livu-300-2': 'ad-156486',
'opengear-300': 'ad-200174',
'look-solutions-300': 'ad-208185',
'riedel-300': 'ad-210571',
'telycam-300': 'ad-191740'
};
export default function AdImage({ ad, page, priority }: { ad: Ad; page?: string; priority?: boolean }) {
const containerRef = useRef<HTMLDivElement>(null);
const trackedRef = useRef(false);
const supabase = createClient();
const adsanityId = ad.adsanity_id || ADSANITY_CAMPAIGN_MAP[ad.label.toLowerCase().replace(/\s+/g, '-')] || '';
useEffect(() => {
const el = anchorRef.current;
if (!el || impressionFiredRef.current) return;
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting && !impressionFiredRef.current) {
impressionFiredRef.current = true;
observer.disconnect();
void recordEvent("impression");
break;
}
}
},
{ threshold: 0.5 }
);
observer.observe(el);
return () => observer.disconnect();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ad.src]);
async function recordEvent(eventType: "impression" | "click") {
try {
const supabase = createClient();
await supabase.from("banner_analytics").insert({
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting && !trackedRef.current) {
trackedRef.current = true;
supabase.from('banner_analytics').insert({
ad_src: ad.src,
ad_label: ad.label,
ad_size: ad.size,
event_type: eventType,
page: pathname || (typeof window !== "undefined" ? window.location.pathname : null),
session_id: getSessionId(),
referrer: typeof document !== "undefined" ? document.referrer || null : null,
});
} catch {
/* tracking is best-effort; never block the user */
}
event_type: 'impression',
page: page || typeof window !== 'undefined' ? window.location.pathname : null,
session_id: typeof window !== 'undefined' ? (window as any).__session_id : null,
referrer: typeof document !== 'undefined' ? document.referrer : null
}).catch(err => console.error('Impression:', err));
}
}, { threshold: 0.5 });
if (containerRef.current) observer.observe(containerRef.current);
return () => observer.disconnect();
}, [ad, page, supabase]);
function handleClick() {
void recordEvent("click");
}
const handleClick = () => {
supabase.from('banner_analytics').insert({
ad_src: ad.src,
ad_label: ad.label,
ad_size: ad.size,
event_type: 'click',
page: page || typeof window !== 'undefined' ? window.location.pathname : null,
session_id: typeof window !== 'undefined' ? (window as any).__session_id : null,
referrer: typeof document !== 'undefined' ? document.referrer : null
}).catch(err => console.error('Click:', err));
};
return (
<a
ref={anchorRef}
href={ad.click_url}
target="_blank"
rel="sponsored noopener noreferrer"
onClick={handleClick}
aria-label={`Advertisement — ${ad.label}`}
className={`block ad-creative ${className}`}
data-ad-size={ad.size}
data-ad-label={ad.label}>
<AppImage
src={ad.src}
alt={ad.alt}
width={w}
height={h}
className="max-w-full h-auto"
priority={priority}
/>
</a>
<div ref={containerRef} className="ad-container">
{adsanityId && (
<iframe src={`https://ads.broadcastbeat.com/wp-content/plugins/adsanity/render.php?id=${adsanityId}`} width={parseInt(ad.size.split('x')[0])} height={parseInt(ad.size.split('x')[1])} frameBorder="0" scrolling="no" style={{display:'block'}} onError={() => console.warn('AdSanity iframe failed, bb.banner_analytics tracking active')} />
)}
{ad.click_url ? (
<a href={ad.click_url} rel="sponsored noopener noreferrer" onClick={handleClick} style={{display:'none'}}><AppImage src={ad.src} alt={ad.label} width={parseInt(ad.size.split('x')[0])} height={parseInt(ad.size.split('x')[1])} priority={priority} /></a>
) : (
<AppImage src={ad.src} alt={ad.label} width={parseInt(ad.size.split('x')[0])} height={parseInt(ad.size.split('x')[1])} priority={priority} style={{display:'none'}} />
)}
</div>
);
}