- 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>
98 lines
3.1 KiB
TypeScript
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>
|
|
);
|
|
}
|