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 { Route, Clock, MapPin, Users, Zap, Globe, Calendar, Trophy, Skull, MoreVertical, Flag } from 'lucide-react'; import { useAuthor } from '@/hooks/useAuthor'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { genUserName } from '@/lib/genUserName'; import { ZapButton } from '@/components/ZapButton'; import { TrustBadge } from '@/components/TrustBadge'; import { ProBadge } from '@/components/ProBadge'; import { ReportDialog } from '@/components/ReportDialog'; import type { SkateTour, TourCategory } from '@/lib/types'; import { TOUR_CATEGORIES, TOUR_SKILL_LEVELS } from '@/lib/types'; import { TOUR_CATEGORY_KEYS } from '@/lib/labelKeys'; import { cn } from '@/lib/utils'; import { useTranslation } from '@/contexts/language'; type Translate = (key: string, params?: Record) => string; interface TourCardProps { tour: SkateTour; compact?: boolean; } function _getCategoryIcon(category: TourCategory) { switch (category) { case 'local': return ; case 'national': return ; case 'international': return ; case 'destruction': return ; default: return ; } } function formatDuration(duration: number | undefined, unit: string | undefined, t: Translate): string { if (!duration) return ''; switch (unit) { case 'hours': return t('placesUi.tourCard.durationHours', { count: duration }); case 'days': return t('placesUi.tourCard.durationDays', { count: duration }); case 'weeks': return duration >= 4 ? t('placesUi.tourCard.durationMonths', { count: Math.floor(duration / 4) }) : t('placesUi.tourCard.durationWeeks', { count: duration }); default: return `${duration}`; } } function formatDateRange(startDate?: number, endDate?: number, withTime = false): string { if (!startDate) return ''; const start = new Date(startDate * 1000); if (withTime) { // Local scheduled session — show the day and start time. return start.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', }); } const startStr = start.toLocaleDateString(dateLocaleCode(), { month: 'short', day: 'numeric' }); if (!endDate) return startStr; const end = new Date(endDate * 1000); const endStr = end.toLocaleDateString(dateLocaleCode(), { month: 'short', day: 'numeric', year: 'numeric' }); return `${startStr} - ${endStr}`; } /** * A short, human countdown for an upcoming (or live) tour based on its start * date. Returns null when there's no date or the tour is well in the past. */ function getCountdown( tour: SkateTour, t: Translate, ): { label: string; color: string; live?: boolean } | null { if (!tour.startDate) return null; const nowSecs = Date.now() / 1000; const start = tour.startDate; const end = tour.endDate; const accent = TOUR_CATEGORIES[tour.category].color; // Currently running (has an end date and we're inside the window). if (start <= nowSecs && end && nowSecs <= end) { return { label: t('placesUi.tourCard.happeningNow'), color: '#ff0040', live: true }; } // Already started but single-session / no end — treat same-day as live, else past. if (start <= nowSecs) { if (nowSecs - start < 12 * 60 * 60) { return { label: t('placesUi.tourCard.happeningNow'), color: '#ff0040', live: true }; } return null; } const diffSecs = start - nowSecs; const days = Math.floor(diffSecs / 86400); const hours = Math.floor(diffSecs / 3600); if (hours < 1) return { label: t('placesUi.tourCard.startingSoon'), color: accent, live: true }; if (hours < 24) return { label: t('placesUi.tourCard.inHours', { count: hours }), color: accent }; if (days === 1) return { label: t('placesUi.tourCard.tomorrow'), color: accent }; if (days < 7) return { label: t('placesUi.tourCard.inDays', { count: days }), color: accent }; if (days < 30) { const w = Math.round(days / 7); return { label: t('placesUi.tourCard.inWeeks', { count: w }), color: accent }; } const m = Math.round(days / 30); return { label: t('placesUi.tourCard.inMonths', { count: m }), color: accent }; } function getTourStatus(tour: SkateTour, t: Translate): { label: string; color: string } | null { if (tour.category !== 'destruction') return null; const now = Date.now() / 1000; if (tour.startDate && now < tour.startDate) { return { label: t('placesUi.tourCard.registrationOpen'), color: '#ccff00' }; } else if (tour.startDate && tour.endDate && now >= tour.startDate && now <= tour.endDate) { return { label: t('placesUi.tourCard.active'), color: '#ff0040' }; } else if (tour.endDate && now > tour.endDate) { return { label: t('placesUi.tourCard.completed'), color: '#888' }; } return null; } export function TourCard({ tour, compact = false }: TourCardProps) { const { t } = useTranslation(); const author = useAuthor(tour.pubkey); const { user } = useCurrentUser(); const metadata = author.data?.metadata; const displayName = metadata?.name ?? genUserName(tour.pubkey); const [showReportDialog, setShowReportDialog] = useState(false); const categoryInfo = TOUR_CATEGORIES[tour.category]; const skillInfo = tour.skillLevel ? TOUR_SKILL_LEVELS[tour.skillLevel as keyof typeof TOUR_SKILL_LEVELS] : null; const tourStatus = getTourStatus(tour, t); const countdown = getCountdown(tour, t); const isOwner = user?.pubkey === tour.pubkey; // Get location text based on category const locationText = tour.category === 'destruction' || tour.category === 'international' ? tour.countries?.join(', ') || tour.cities?.join(', ') || t('placesUi.tourCard.worldwide') : tour.location || tour.cities?.join(', '); if (compact) { return ( { e.currentTarget.style.borderColor = `${categoryInfo.color}99`; e.currentTarget.style.boxShadow = `0 10px 40px ${categoryInfo.color}33`; }} onMouseLeave={(e) => { e.currentTarget.style.borderColor = `${categoryInfo.color}33`; e.currentTarget.style.boxShadow = `0 0 0 0 ${categoryInfo.color}00`; }} >
{/* Category Badge */}
{categoryInfo.emoji}
{/* Content */}

{tour.title}

{countdown && ( {countdown.label} )} {tourStatus && ( {tourStatus.label} )}
{TOUR_CATEGORY_KEYS[tour.category] ? t(TOUR_CATEGORY_KEYS[tour.category]) : categoryInfo.label} {tour.duration && ( {formatDuration(tour.duration, tour.durationUnit, t)} )} {locationText && ( {locationText} )}
); } return ( { e.currentTarget.style.borderColor = `${categoryInfo.color}80`; e.currentTarget.style.boxShadow = `0 20px 50px ${categoryInfo.color}20`; }} onMouseLeave={(e) => { e.currentTarget.style.borderColor = `${categoryInfo.color}33`; e.currentTarget.style.boxShadow = 'none'; }} > {/* Image */}
{tour.image ? ( {tour.title} ) : (
{tour.category === 'destruction' ? ( ) : ( )}
)} {/* Category overlay */}
{categoryInfo.emoji} {TOUR_CATEGORY_KEYS[tour.category] ? t(TOUR_CATEGORY_KEYS[tour.category]) : categoryInfo.label}
{/* Status/Price overlay */}
{countdown && ( {countdown.label} )} {tourStatus && ( {tourStatus.label} )} {tour.prizePool && ( {t('placesUi.tourCard.prizePool', { amount: tour.prizePool.toLocaleString() })} )}
{/* Guide/Organizer info */}
{displayName.slice(0, 2).toUpperCase()}

{displayName}

{metadata?.nip05 && (
{metadata.nip05}
)}

{tour.title}

{/* Tour schedule — shown whenever the tour has a date set */} {tour.startDate && (
{formatDateRange(tour.startDate, tour.endDate, tour.category === 'local')}
)} {/* Tour meta info */}
{tour.duration && ( {formatDuration(tour.duration, tour.durationUnit, t)} )} {tour.maxGroupSize && ( {t('placesUi.tourCard.maxGroup', { count: tour.maxGroupSize })} )} {tour.maxTeams && ( {t('placesUi.tourCard.teamsMax', { count: tour.maxTeams })} )} {skillInfo && ( {skillInfo.emoji} {skillInfo.label} )}
{locationText && (

{locationText}

)} {tour.description && (

{tour.description}

)} {/* Price or Entry Fee */}
{tour.category === 'destruction' && tour.entryFee ? ( {t('placesUi.tourCard.entry', { amount: tour.entryFee.toLocaleString() })} ) : tour.price ? ( {t('placesUi.tourCard.price', { amount: tour.price.toLocaleString() })} ) : ( {t('placesUi.tourCard.contactForPrice')} )}
e.preventDefault()}> {/* More options menu */} {!isOwner && ( { e.preventDefault(); e.stopPropagation(); setShowReportDialog(true); }} className="text-red-500 focus:text-red-500" > {t('placesUi.eventCard.report')} )}
{/* Report Dialog */} ); } export function TourCardSkeleton({ compact = false }: { compact?: boolean }) { if (compact) { return (
); } return (
); }