SkateSpots/src/components/EventSponsors.tsx
shakespeare.diy 87c81a63c9 Event detail hero redesign + live countdown + sponsors
- Immersive EventDetailPage hero: full-bleed cover with legibility gradient,
  bold uppercase title, type pills, host/music-act chips, and a 3-up meta bar
  (date block · time · where). Past events render dimmed.
- EventCountdown: live status pill — "IN 3 DAYS" / "IN 2H 14M" /
  "HAPPENING NOW" (pulsing) / "ENDED", refreshing every 30s.
- Actions (edit/calendar/share) moved into a clean glass overlay on the cover.
- Event sponsors: EventSponsor type + parse/serialize (mirrors TourSponsor),
  EventSponsors "Presented by" strip (title sponsor hero row + logo grid),
  sponsor editor on CreateEventPage, and sponsor preservation on edit.
- Document event sponsor tag in NIP.md.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 12:04:07 +07:00

98 lines
3.1 KiB
TypeScript

import { ExternalLink, Sparkles } from 'lucide-react';
import { sanitizeUrl } from '@/lib/sanitizeUrl';
import { cn } from '@/lib/utils';
import { EVENT_SPONSOR_TIERS, type EventSponsor } from '@/lib/types';
function SponsorLogo({ sponsor, large }: { sponsor: EventSponsor; large?: boolean }) {
const safeUrl = sponsor.url ? sanitizeUrl(sponsor.url) : null;
const safeLogo = sponsor.logo ? sanitizeUrl(sponsor.logo) : null;
const inner = (
<div
className={cn(
'group flex flex-col items-center justify-center gap-2 rounded-xl border border-white/10 bg-white/[0.03] p-3 transition-all hover:border-white/25 hover:bg-white/[0.06]',
large ? 'min-h-[96px]' : 'min-h-[72px]',
)}
>
{safeLogo ? (
<img
src={safeLogo}
alt={sponsor.name}
className={cn('object-contain', large ? 'max-h-14' : 'max-h-9')}
loading="lazy"
/>
) : (
<span className={cn('font-black uppercase tracking-wide text-white', large ? 'text-lg' : 'text-sm')}>
{sponsor.name}
</span>
)}
{safeLogo && (
<span className="max-w-full truncate text-[10px] font-bold uppercase tracking-wide text-gray-400">
{sponsor.name}
{safeUrl && (
<ExternalLink className="ml-1 inline h-2.5 w-2.5 opacity-0 transition-opacity group-hover:opacity-100" />
)}
</span>
)}
</div>
);
if (safeUrl) {
return (
<a href={safeUrl} target="_blank" rel="noopener noreferrer nofollow" className="block">
{inner}
</a>
);
}
return inner;
}
/**
* Sponsor strip for an event. Title sponsor gets a hero row; the rest render in
* a responsive grid. Sponsors give events a "this is a real production" feel and
* open a monetization surface for hosts + brands.
*/
export function EventSponsors({
sponsors,
className,
}: {
sponsors: EventSponsor[];
className?: string;
}) {
if (!sponsors || sponsors.length === 0) return null;
const title = sponsors.filter((s) => s.tier === 'title');
const rest = sponsors.filter((s) => s.tier !== 'title');
return (
<div className={cn('space-y-4', className)}>
<div className="flex items-center gap-2">
<Sparkles className="h-4 w-4 text-[#ccff00]" />
<h3 className="text-xs font-black uppercase tracking-widest text-gray-400">
Presented by
</h3>
</div>
{title.length > 0 && (
<div className="grid grid-cols-1 gap-3">
{title.map((s, i) => (
<div key={`${s.name}-${i}`} className="space-y-1.5">
<span className="text-[10px] font-black uppercase tracking-widest text-[#ccff00]">
{EVENT_SPONSOR_TIERS.title.label}
</span>
<SponsorLogo sponsor={s} large />
</div>
))}
</div>
)}
{rest.length > 0 && (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
{rest.map((s, i) => (
<SponsorLogo key={`${s.name}-${i}`} sponsor={s} />
))}
</div>
)}
</div>
);
}