98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
// 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 (
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
<path d="M5 12h14" />
|
|
<path d="M13 6l6 6-6 6" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export function BannerSlot({ slot, className }: { slot: BannerSlotName; className?: string }) {
|
|
const [banners, setBanners] = useState<PublicBanner[]>([]);
|
|
const navigate = useNavigate();
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
apiFetch<PublicBanner[]>(`/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<HTMLAnchorElement>) => {
|
|
// 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 (
|
|
<div className={`banner-slot ${slotClass} ${className ?? ''}`.trim()} aria-label="Reklama">
|
|
<a
|
|
className={`promo-banner ${withImage}`}
|
|
href={banner.linkUrl ?? undefined}
|
|
target={isInternal ? undefined : '_blank'}
|
|
rel={isInternal ? undefined : 'noopener noreferrer sponsored'}
|
|
onClick={handleClick}
|
|
style={style}
|
|
>
|
|
<span className="promo-banner-tag">Reklama</span>
|
|
<div className="promo-banner-body">
|
|
<strong className="promo-banner-title">{banner.title}</strong>
|
|
{banner.subtitle && <span className="promo-banner-sub">{banner.subtitle}</span>}
|
|
{banner.ctaLabel && (
|
|
<span className="promo-banner-cta">{banner.ctaLabel} <ArrowIcon /></span>
|
|
)}
|
|
</div>
|
|
</a>
|
|
</div>
|
|
);
|
|
}
|