folx-tv/client/src/components/adsense.tsx

218 lines
6.7 KiB
TypeScript

import { useEffect, useMemo, useRef, useState } from "react";
import ArtistPatternBg from "./artist-pattern-bg";
type AdFormat = "auto" | "fluid" | "rectangle" | "horizontal" | "vertical" | "autorelaxed";
interface AdSenseProps {
slot: string;
format?: AdFormat;
responsive?: boolean;
className?: string;
style?: Record<string, string>;
layout?: string;
layoutKey?: string;
onAdStatus?: (filled: boolean) => void;
}
export default function AdSense({
slot,
format = "auto",
responsive = true,
className = "",
style,
layout,
layoutKey,
onAdStatus,
}: AdSenseProps) {
const adRef = useRef<HTMLModElement>(null);
const pushed = useRef(false);
useEffect(() => {
if (pushed.current) return;
try {
const adsbygoogle = (window as any).adsbygoogle || [];
adsbygoogle.push({});
pushed.current = true;
} catch (e) {
}
if (!onAdStatus) return;
const ins = adRef.current;
if (!ins) return;
// Google postavi data-ad-status na "filled", "unfilled" ali "unfill-optimized".
// Ce statusa ni, se zanesemo na dejansko visino. Odlocimo se enkrat.
let decided = false;
const decide = (filled: boolean) => {
if (decided) return;
decided = true;
obs.disconnect();
clearTimeout(timer);
onAdStatus(filled);
};
// Visina samega <ins> ni dokaz - nekatere enote imajo rezerviran prostor.
// Napolnjen oglas ima notri okvir (iframe) z dejansko visino.
const hasFrame = () => {
const f = ins.querySelector("iframe");
return !!f && (f as HTMLIFrameElement).clientHeight > 10;
};
const check = () => {
const st = ins.getAttribute("data-ad-status") || "";
if (st === "filled") return decide(true);
if (st.startsWith("unfill")) return decide(false);
if (hasFrame()) return decide(true);
};
const obs = new MutationObserver(check);
obs.observe(ins, { attributes: true, childList: true, subtree: true, attributeFilter: ["data-ad-status", "style"] });
const timer = setTimeout(() => decide(hasFrame()), 6000);
check();
return () => { obs.disconnect(); clearTimeout(timer); };
}, []);
return (
<div className={`ad-container ${className}`} data-testid={`ad-slot-${slot}`}>
<ins
ref={adRef}
className="adsbygoogle"
style={style || { display: "block" }}
data-ad-client="ca-pub-4465464714854276"
data-ad-slot={slot}
data-ad-format={format}
data-full-width-responsive={responsive ? "true" : undefined}
{...(layout ? { "data-ad-layout": layout } : {})}
{...(layoutKey ? { "data-ad-layout-key": layoutKey } : {})}
/>
</div>
);
}
let adSeedCounter = 1;
export function ArticleCardAd() {
const seed = useMemo(() => adSeedCounter++, []);
const [filled, setFilled] = useState<boolean | null>(null);
if (filled === false) return null;
return (
<ArtistPatternBg className="rounded-md border border-card-border h-full bg-card" seed={seed}>
<AdSense
slot="3854634730"
format="fluid"
layoutKey="-6r+cy-10+8a-3"
style={{ display: "block" }}
onAdStatus={setFilled}
/>
</ArtistPatternBg>
);
}
export function TopBannerAd() {
const [filled, setFilled] = useState<boolean | null>(null);
if (filled === false) return null;
const shown = filled === true;
return (
<div
className={shown
? "rounded-md overflow-hidden bg-card border border-card-border mb-4 flex flex-col items-center"
: "flex flex-col items-center"}
data-testid="ad-top-banner"
>
{shown && (
<div className="text-[9px] text-muted-foreground/40 text-center pt-1 uppercase tracking-widest w-full">Anzeige</div>
)}
<AdSense
slot="4154017639"
format="horizontal"
className="w-full"
style={{ display: "block", width: "100%", height: "90px", textAlign: "center", margin: "0 auto" }}
responsive={false}
onAdStatus={setFilled}
/>
</div>
);
}
export function InArticleAd() {
// Prava AdSense In-article enota (fluid + data-ad-layout="in-article").
// Kadar oglasa ni, se okvir in rezerviran prostor ne izriseta.
const [filled, setFilled] = useState<boolean | null>(null);
if (filled === false) return null;
const shown = filled === true;
return (
<div
className={shown ? "my-6 rounded-md overflow-hidden bg-card border border-card-border" : ""}
style={shown ? { minHeight: "260px" } : undefined}
>
{shown && (
<div className="text-[9px] text-muted-foreground/40 text-center pt-1 uppercase tracking-widest">Anzeige</div>
)}
<AdSense
slot="7672178246"
format="fluid"
layout="in-article"
style={{ display: "block", textAlign: "center" }}
onAdStatus={setFilled}
/>
</div>
);
}
export function MultiplexAd() {
const [filled, setFilled] = useState<boolean | null>(null);
if (filled === false) return null;
const shown = filled === true;
return (
<div className={shown ? "bg-card rounded-md border border-card-border overflow-hidden" : ""}>
<AdSense
slot="4593001335"
format="autorelaxed"
style={{ display: "block" }}
className={shown ? "w-full min-h-[250px]" : "w-full"}
onAdStatus={setFilled}
/>
</div>
);
}
export function PageSideAds({ contentHalfWidth = 640 }: { contentHalfWidth?: number }) {
const gap = 10;
const offset = contentHalfWidth + gap;
return (
<div className="relative">
<div className="hidden 2xl:flex fixed top-0 bottom-0 w-[160px] z-10 items-center justify-center pointer-events-none" style={{ left: `calc(50% - ${offset}px - 160px)` }} data-testid="ad-left-sidebar">
<div className="pointer-events-auto">
<AdSense
slot="2398082838"
format="vertical"
style={{ display: "block", width: "160px", height: "600px" }}
/>
</div>
</div>
<div className="hidden 2xl:flex fixed top-0 bottom-0 w-[160px] z-10 items-center justify-center pointer-events-none" style={{ right: `calc(50% - ${offset}px - 160px)` }} data-testid="ad-right-sidebar">
<div className="pointer-events-auto">
<AdSense
slot="2398082838"
format="vertical"
style={{ display: "block", width: "160px", height: "600px" }}
/>
</div>
</div>
</div>
);
}
export function SidebarAd() {
const [visible, setVisible] = useState(false);
return (
<div className={`sticky top-4 ${visible ? "" : "hidden"}`}>
<div className="bg-card rounded-md border border-card-border p-4 mt-4">
<AdSense
slot="2398082838"
format="auto"
onAdStatus={(filled) => setVisible(filled)}
/>
</div>
</div>
);
}