// Publiczny komponent slotu reklamowego. Pobiera aktywne banery dla danego miejsca i renderuje // pierwszy (najwyzszy priorytetem). Zlicza klikniecia. Gdy brak banera - nie renderuje nic. import { useEffect, useState, type MouseEvent } from 'react'; import { useNavigate } from 'react-router-dom'; import { apiFetch } from './auth'; export type BannerSlotName = 'HERO' | 'HOME_TOP' | 'HOME_MIDDLE' | 'SIDEBAR'; export type PublicBanner = { id: number; slot: BannerSlotName; title: string; subtitle: string | null; imageUrl: string | null; linkUrl: string | null; ctaLabel: string | null; }; function ArrowIcon() { return ( ); } export function BannerSlot({ slot, className }: { slot: BannerSlotName; className?: string }) { const [banners, setBanners] = useState([]); const navigate = useNavigate(); useEffect(() => { let cancelled = false; apiFetch(`/banners?slot=${slot}`) .then((data) => { if (!cancelled) { setBanners(Array.isArray(data) ? data : []); } }) .catch(() => { if (!cancelled) { setBanners([]); } }); return () => { cancelled = true; }; }, [slot]); const banner = banners[0]; if (!banner) { return null; } const isInternal = !!banner.linkUrl && banner.linkUrl.startsWith('/'); const handleClick = (event: MouseEvent) => { // Zliczenie klikniecia - bez blokowania nawigacji. apiFetch(`/banners/${banner.id}/click`, { method: 'POST' }).catch(() => {}); if (!banner.linkUrl) { event.preventDefault(); return; } if (isInternal) { event.preventDefault(); navigate(banner.linkUrl); } }; const slotClass = `banner-slot-${slot.toLowerCase()}`; const withImage = banner.imageUrl ? 'has-image' : 'no-image'; const style = banner.imageUrl ? { backgroundImage: `linear-gradient(90deg, rgba(9,20,38,0.78) 0%, rgba(9,20,38,0.45) 45%, rgba(9,20,38,0.15) 100%), url(${banner.imageUrl})` } : undefined; return (
Reklama
{banner.title} {banner.subtitle && {banner.subtitle}} {banner.ctaLabel && ( {banner.ctaLabel} )}
); }