Files
SatsFaucet/frontend/src/components/SponsorCard.tsx
Michilis dc7007f708 Add sponsors system with time slider, LNbits invoices, and UX improvements
- Sponsors table, LNbits createInvoice, webhook handler
- Sponsor routes: create, homepage, list, my-ads, click, extend, check-payment
- Admin routes for sponsor management
- Frontend: SponsorForm, SponsorTimeSlider, SponsorCard, SponsorsSection
- Sponsors page, My Ads page, homepage sponsor block
- Header login dropdown with My Ads, Create Sponsor
- Transactions integration for sponsor payments
- View/click tracking
- OG meta fetch for sponsor images
- Sponsor modal spacing, invoice polling fallback
- Remove Lightning address and Category fields from sponsor form

Made-with: Cursor
2026-03-16 00:01:19 +00:00

85 lines
2.6 KiB
TypeScript

import { useEffect, useRef } from "react";
import { getSponsorClickUrl, patchSponsorView, type SponsorHomepageItem } from "../api";
interface SponsorCardProps {
sponsor: SponsorHomepageItem;
}
function getDaysLeft(expiresAt: number | null): number {
if (!expiresAt) return 0;
const now = Math.floor(Date.now() / 1000);
return Math.max(0, Math.ceil((expiresAt - now) / 86400));
}
function extractDomain(url: string): string {
try {
const u = new URL(url);
return u.hostname.replace(/^www\./, "");
} catch {
return "Sponsor";
}
}
export function SponsorCard({ sponsor }: SponsorCardProps) {
const cardRef = useRef<HTMLDivElement>(null);
const viewedRef = useRef(false);
useEffect(() => {
const el = cardRef.current;
if (!el || viewedRef.current) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0]?.isIntersecting && !viewedRef.current) {
viewedRef.current = true;
patchSponsorView(sponsor.id).catch(() => {});
}
},
{ threshold: 0.5 }
);
observer.observe(el);
return () => observer.disconnect();
}, [sponsor.id]);
const daysLeft = getDaysLeft(sponsor.expires_at);
const clickUrl = getSponsorClickUrl(sponsor.id);
return (
<article ref={cardRef} className="sponsor-card">
<a
href={clickUrl}
target="_blank"
rel="noopener noreferrer"
className="sponsor-card-link"
aria-label={`Visit ${sponsor.title}`}
>
<div className="sponsor-card-image-wrap">
{sponsor.image_url ? (
<img
src={sponsor.image_url}
alt=""
className="sponsor-card-image"
loading="lazy"
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
(e.target as HTMLImageElement).nextElementSibling?.classList.remove("hidden");
}}
/>
) : null}
<div className={`sponsor-card-fallback ${sponsor.image_url ? "hidden" : ""}`}>
<span className="sponsor-card-fallback-icon">🔗</span>
<span className="sponsor-card-fallback-domain">{extractDomain(sponsor.link_url)}</span>
</div>
</div>
<div className="sponsor-card-body">
<h3 className="sponsor-card-title">{sponsor.title}</h3>
<p className="sponsor-card-desc">{sponsor.description}</p>
<span className="sponsor-card-cta">Visit sponsor</span>
</div>
</a>
<div className="sponsor-card-meta">
<span className="sponsor-card-days">{daysLeft} days left</span>
</div>
</article>
);
}