SMPT, SMS, Rejestracja + GUS
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user