// 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 (
);
}