import { dateLocaleCode } from '@/lib/dateFnsI18n'; import { useState } from 'react'; import { Link } from 'react-router-dom'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Skeleton } from '@/components/ui/skeleton'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Calendar, Clock, MapPin, MoreVertical, Flag, Users, Sparkles } from 'lucide-react'; import { useAuthor } from '@/hooks/useAuthor'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { genUserName } from '@/lib/genUserName'; import { useEventRsvps, eventCoord } from '@/hooks/useEventRsvps'; import { ZapButton } from '@/components/ZapButton'; import { EventCountdown } from '@/components/EventCountdown'; import { TrustBadge } from '@/components/TrustBadge'; import { OfficialBadge } from '@/components/OfficialBadge'; import { ProBadge } from '@/components/ProBadge'; import { ReportDialog } from '@/components/ReportDialog'; import { MuteMenuItem } from '@/components/MuteMenuItem'; import { useTranslation } from '@/contexts/language'; import type { SkateEvent } from '@/lib/types'; import { EVENT_TYPE_LABELS } from '@/lib/types'; import { EVENT_TYPE_KEYS } from '@/lib/labelKeys'; function eventTypeLabel(t: (key: string) => string, type: string): string { return EVENT_TYPE_KEYS[type] ? t(EVENT_TYPE_KEYS[type]) : (EVENT_TYPE_LABELS[type] || type); } interface EventCardProps { event: SkateEvent; compact?: boolean; /** * Pre-computed going count from a batch query (see useBatchEventRsvps). When * provided, the card skips its own per-card RSVP query — this avoids an N+1 * query storm when rendering a grid of cards. */ goingCount?: number; } function formatDate(timestamp: number): string { const date = new Date(timestamp * 1000); return date.toLocaleDateString(dateLocaleCode(), { weekday: 'short', month: 'short', day: 'numeric', }); } function formatTime(timestamp: number): string { const date = new Date(timestamp * 1000); return date.toLocaleTimeString(dateLocaleCode(), { hour: 'numeric', minute: '2-digit', }); } export function EventCard({ event, compact = false, goingCount: goingCountProp }: EventCardProps) { const { t } = useTranslation(); const author = useAuthor(event.pubkey); const { user } = useCurrentUser(); const metadata = author.data?.metadata; const displayName = metadata?.name ?? genUserName(event.pubkey); const [showReportDialog, setShowReportDialog] = useState(false); const isOwner = user?.pubkey === event.pubkey; // Skip the per-card RSVP query when a batch count was supplied (grid view). const { goingCount: goingCountSelf } = useEventRsvps( goingCountProp === undefined ? eventCoord(event) : undefined, ); const goingCount = goingCountProp ?? goingCountSelf; // Sponsors ride along on the parsed event (no extra query). Title sponsor // first, so paid placements get real visibility in the discovery feed. const sponsors = event.sponsors ?? []; const leadSponsor = sponsors.find((s) => s.tier === 'title') ?? sponsors[0]; if (compact) { return ( {/* Date Badge */} {new Date(event.start * 1000).toLocaleDateString(dateLocaleCode(), { month: 'short' })} {new Date(event.start * 1000).getDate()} {/* Content */} {event.title} {formatTime(event.start)} {event.location && ( {event.location} )} {goingCount > 0 && ( {goingCount} )} ); } return ( {/* Image */} {event.image ? ( ) : ( )} {/* Bottom legibility gradient for overlays */} {/* Date overlay */} {new Date(event.start * 1000).toLocaleDateString(dateLocaleCode(), { month: 'short' })} {new Date(event.start * 1000).getDate()} {/* Live status pill — the at-a-glance signal in the grid */} {/* Type badges + Pro */} {event.types.slice(0, 2).map((type) => ( {eventTypeLabel(t, type)} ))} {/* Sponsor hint — gives paid placements visibility in the feed */} {leadSponsor && ( {t('placesUi.sponsors.presentedBy')} {leadSponsor.name} {sponsors.length > 1 && ( +{sponsors.length - 1} )} )} {/* Name Display at Top */} {displayName.slice(0, 2).toUpperCase()} {displayName} {metadata?.nip05 && ( {metadata.nip05} )} {event.title} {formatDate(event.start)} {formatTime(event.start)} {event.location && ( {event.location} )} {event.description && ( {event.description} )} {goingCount > 0 ? ( {goingCount} {t('placesUi.attendees.goingCount')} ) : ( )} e.preventDefault()}> {/* More options menu */} e.stopPropagation()} > {!isOwner && ( { e.preventDefault(); e.stopPropagation(); setShowReportDialog(true); }} className="text-red-500 focus:text-red-500" > {t('placesUi.eventCard.report')} )} {/* Report Dialog */} ); } export function EventCardSkeleton({ compact = false }: { compact?: boolean }) { if (compact) { return ( ); } return ( ); }
{event.location}
{event.description}