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; layout?: string; layoutKey?: string; eager?: boolean; onAdStatus?: (filled: boolean) => void; } export default function AdSense({ slot, format = "auto", responsive = true, className = "", style, layout, layoutKey, // Privzeto zahtevamo takoj. Odlozena zahteva dvigne vidnost, a znizza stevilo // prikazov pri enotah globlje na strani - in prav te (in-article, multiplex) // nosijo prihodek. Lazy pot ostaja na voljo prek eager={false}, a je nikjer // ne uporabljamo, dokler ucinka ne moremo izmeriti v AdSense. eager = true, onAdStatus, }: AdSenseProps) { const adRef = useRef(null); const pushed = useRef(false); useEffect(() => { if (pushed.current) return; const ins = adRef.current; let io: IntersectionObserver | null = null; let obs: MutationObserver | null = null; let timer: ReturnType | null = null; let decided = false; // Google postavi data-ad-status na "filled", "unfilled" ali "unfill-optimized". // Ce statusa ni, se zanesemo na dejansko visino. Odlocimo se enkrat. // AdSense odzivni enoti vnaprej doloci visino po sirini zaslona. Ce je // postrezena kreativa nizja, ostane pod njo prazen rezerviran pas znotraj // okvirja. Visino zato poravnamo na dejansko visino oglasnega okvirja. // Nic se ne odreze - visina je natanko taksna kot oglas. let fitTimers: ReturnType[] = []; const fitToCreative = () => { // Enota s trdno visino v style (fiksne Display enote) se ne prilagaja // kreativi, sicer nam Google 300x400 razpotegne na 300x600. if (style && typeof (style as any).height === "string") return; const f = ins?.querySelector("iframe") as HTMLIFrameElement | null; if (!ins || !f) return; const h = f.getBoundingClientRect().height; if (h > 10 && Math.abs(ins.getBoundingClientRect().height - h) > 2) { ins.style.height = `${Math.ceil(h)}px`; } }; const scheduleFit = () => { fitTimers.forEach(clearTimeout); fitTimers = [0, 300, 1000, 2500].map((d) => setTimeout(fitToCreative, d)); }; const decide = (filled: boolean) => { if (decided) return; decided = true; obs?.disconnect(); if (timer) clearTimeout(timer); if (filled) scheduleFit(); onAdStatus?.(filled); }; // Visina samega 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); }; // Oglas zahtevamo sele, ko se enota priblizuje vidnemu polju (200 px prej). // Enote globoko pod pregibom so se prej zahtevale takoj ob nalaganju strani in // vecine uporabnik ni nikoli videl - nizka vidnost pomeni nizjo ceno. // Odlocitev o praznosti steje sele od zahteve naprej, ne od mounta. const push = () => { if (pushed.current) return; try { const adsbygoogle = (window as any).adsbygoogle || []; adsbygoogle.push({}); pushed.current = true; } catch (e) { } if (!ins) return; // Opazujemo tudi pri enotah brez onAdStatus - potrebujemo jih za poravnavo // visine na kreativo. obs = new MutationObserver(() => { check(); fitToCreative(); }); obs.observe(ins, { attributes: true, childList: true, subtree: true, attributeFilter: ["data-ad-status", "style"] }); timer = setTimeout(() => decide(hasFrame()), 3000); window.addEventListener("resize", scheduleFit); check(); scheduleFit(); }; if (eager || !ins || typeof IntersectionObserver === "undefined") { push(); } else { io = new IntersectionObserver((entries) => { if (entries.some((e) => e.isIntersecting)) { io?.disconnect(); io = null; push(); } }, { rootMargin: "200px 0px" }); io.observe(ins); } return () => { io?.disconnect(); obs?.disconnect(); if (timer) clearTimeout(timer); fitTimers.forEach(clearTimeout); window.removeEventListener("resize", scheduleFit); }; }, []); return (
); } let adSeedCounter = 1; // Vodoravni pas med vrsticami mreze. Oglas v celici je vrstico raztegnil na // svojo visino (546 px proti 399 px kartice) in pod izvlecki je ostalo prazno. // Pas cez vso sirino na visino kartic ne vpliva in ima boljso zapolnjenost. export function GridBannerAd() { const [filled, setFilled] = useState(null); if (filled === false) return null; return ( // Pas cez vso sirino je bil visok 248 do 280 px in je bralca prekinil bolj, // kot je prinesel. Zozimo ga na 728 px: pri tej sirini Google servira // leaderboard 728x90, na telefonu pa 320x100. Rezanja ne uporabljamo, // ker je obrezan oglas krsitev pravil.
Anzeige
{/* FOLX 2026 - Leaderboard */}
); } export function ArticleCardAd() { const [filled, setFilled] = useState(null); if (filled === false) return null; // 31. 8. 2026: oglas mora biti enak kartici, torej cela celica in ne nizji // blok z luknjo pod njim. Okvir je zato h-full in prevzame visino vrstice, // oglas pa je v njem centriran. Enota 9916590834 je fiksna 300x400: fiksne // mere Googlova skripta ne prevaja, zato se ne razteguje kot responsive. return (
{/* FOLX 2026 - Grid, fiksna Display enota 300x600. Fiksna mera pomeni, da Googlova skripta visine ne prevaja; stara 9916590834 je bila Responsiv in se je razteznila na 548 px. */}
); } export function TopBannerAd() { const [filled, setFilled] = useState(null); if (filled === false) return null; const shown = filled === true; return (
{shown && (
Anzeige
)} {/* format="auto" + full-width-responsive: Google dobi celo sirino in izbere kreativo, ki jo zapolni. Pri "horizontal" s trdo visino je vracal 320x50 in okrog je ostal prazen rob. */} {/* FOLX 2026 - Leaderboard */}
); } // Navaden odzivni display v toku besedila. Za razliko od fluid in-article enote // dobi Google celotno sirino in lahko postrezhe visoko kreativo (336x280, 300x600), // torej en cel oglas, mimo katerega bralec odskrola - namesto vec drobnih. export function InContentDisplayAd() { const [filled, setFilled] = useState(null); if (filled === false) return null; const shown = filled === true; return (
{shown && (
Anzeige
)} {/* Sirina 728 px: pri njej Google servira leaderboard 728x90, na telefonu 320x100. Pri polni sirini clanka je servirala velik nativni oglas visok 250 px in bralcu je zakrila zacetek besedila. */}
); } 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(null); if (filled === false) return null; const shown = filled === true; return (
{shown && (
Anzeige
)} {/* FOLX 2026 - In-Article */}
); } export function MultiplexAd() { const [filled, setFilled] = useState(null); if (filled === false) return null; const shown = filled === true; return (
); } // Oba stolpca ostaneta (nic se ne odvzame), a se prilagodita sirini zaslona: // 160x600 od 1536 px, 300x600 od 1700 px naprej, kjer je prostora dovolj. // Sirino dolocz CSS, AdSense jo prebere ob zahtevi. Dodan onAdStatus, da prazna // enota ne pusti luknje - prej je ni imela. function SideRail({ side, offset }: { side: "left" | "right"; offset: number }) { const [filled, setFilled] = useState(null); if (filled === false) return null; return (
{/* Trdna mera 160x600 (skyscraper). Prej je bil pas na sirokih zaslonih 300 px in z responsive vertical si je Google visino racunal sam, zato se je raztegoval cez pol zaslona. */} {/* FOLX 2026 - Skyscraper, fiksna Display enota 160x600. */}
); } export function PageSideAds({ contentHalfWidth = 640 }: { contentHalfWidth?: number }) { // 31. 8. 2026: pasova sta bila prilepljena na rob zaslona, ker je bila // spremenljivka --rail-w nad 1700 px se vedno 300, enota pa je zdaj fiksna // 160 px - odmik se je racunal s 140 px prevelike sirine. Zdaj je povsod // 160, razmik do vsebine pa majhen, da sta pasova tik ob clanku. const gap = 8; const offset = contentHalfWidth + gap; return (
); } export function SidebarAd() { const [visible, setVisible] = useState(false); return (
setVisible(filled)} />
); }