SMPT, SMS, Rejestracja + GUS

This commit is contained in:
2026-08-09 21:12:16 +02:00
parent a0c9d7947b
commit c464c5f708
78 changed files with 5841 additions and 258 deletions
+97
View File
@@ -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>
);
}