import { useState, useMemo, useRef, useEffect, useCallback } from 'react'; import { useSeoMeta } from '@unhead/react'; import { Link } from 'react-router-dom'; import { LazyMount } from '@/components/LazyMount'; import { EventCard, EventCardSkeleton } from '@/components/EventCard'; import { TourCard, TourCardSkeleton } from '@/components/TourCard'; import { HostingDashboard } from '@/components/HostingDashboard'; import { EventsCalendar } from '@/components/EventsCalendar'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { filterByTrustLevel, type TrustFilterLevel } from '@/lib/trustFilter'; import { type SortOption } from '@/components/SortDropdown'; import { useSkateEvents } from '@/hooks/useSkateEvents'; import { useBatchEventRsvps, eventCoord } from '@/hooks/useEventRsvps'; import { useTours } from '@/hooks/useTours'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useFollowing } from '@/hooks/useFollowing'; import { useBatchTrustScores } from '@/hooks/useBatchTrustScores'; import { useCommentCounts } from '@/hooks/useCommentCounts'; import { useTranslation } from '@/contexts/language'; import { EVENT_TYPE_LABELS, TOUR_CATEGORIES, TOUR_SKILL_LEVELS, type TourCategory, } from '@/lib/types'; import { Search, Plus, Calendar, X, Route, MapPin, SlidersHorizontal, Clock, Star, TrendingUp, Shield, Users, CheckCircle2, ChevronDown, Megaphone, LayoutGrid, CalendarDays, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { EmptyState as SharedEmptyState, type EmptyStateAction } from '@/components/EmptyState'; // ─── Accent colours ─────────────────────────────────────────────────────────── const ACCENT = { events: { active: 'bg-[#ccff00] text-black border-[#ccff00]', chip: 'bg-[#ccff00] text-black border-[#ccff00]', chipHover: 'hover:border-[#ccff00]/60 hover:text-[#ccff00]', border: 'border-[#ccff00]/40', icon: 'text-[#ccff00]', badge: 'bg-[#ccff00]/15 text-[#ccff00] border-[#ccff00]/30', stripe: 'bg-[#ccff00]', }, tours: { active: 'bg-cyan-500 text-white border-cyan-500', chip: 'bg-cyan-500 text-white border-cyan-500', chipHover: 'hover:border-cyan-500/60 hover:text-cyan-400', border: 'border-cyan-500/40', icon: 'text-cyan-400', badge: 'bg-cyan-500/15 text-cyan-400 border-cyan-500/30', stripe: 'bg-cyan-500', }, hosting: { active: 'bg-orange-500 text-white border-orange-500', chip: 'bg-orange-500 text-white border-orange-500', chipHover: 'hover:border-orange-500/60 hover:text-orange-400', border: 'border-orange-500/40', icon: 'text-orange-400', badge: 'bg-orange-500/15 text-orange-400 border-orange-500/30', stripe: 'bg-orange-500', }, } as const; type MainView = 'events' | 'tours' | 'hosting'; // ─── Date window filter (skaters plan by weekend) ───────────────────────────── type DateWindow = 'all' | 'today' | 'weekend' | 'week' | 'month'; const DATE_WINDOW_LABELS: Record, string> = { today: 'Today', weekend: 'Weekend', week: 'This Week', month: 'This Month', }; /** Compact labels for the 5-across mobile grid (avoid truncation). */ const DATE_WINDOW_LABELS_SHORT: Record = { all: 'Any', today: 'Today', weekend: 'Wknd', week: 'Week', month: 'Month', }; /** * Returns [startSec, endSec) for a date window, in local time. `weekend` covers * the upcoming Sat+Sun (or the current weekend if it's already Sat/Sun). */ function dateWindowRange(window: Exclude): [number, number] { const now = new Date(); const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const sec = (d: Date) => Math.floor(d.getTime() / 1000); const addDays = (d: Date, n: number) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n); switch (window) { case 'today': return [sec(startOfToday), sec(addDays(startOfToday, 1))]; case 'weekend': { // day: 0=Sun … 6=Sat. Find the coming Saturday (or today if Sat/Sun). const day = startOfToday.getDay(); let satOffset: number; if (day === 6) satOffset = 0; // Saturday else if (day === 0) satOffset = -1; // Sunday → weekend started yesterday else satOffset = 6 - day; // upcoming Saturday const sat = addDays(startOfToday, satOffset); const end = addDays(sat, 2); // Sat 00:00 → Mon 00:00 // Never start the window in the past. const start = sec(sat) < sec(startOfToday) ? sec(startOfToday) : sec(sat); return [start, sec(end)]; } case 'week': return [sec(startOfToday), sec(addDays(startOfToday, 7))]; case 'month': return [sec(startOfToday), sec(addDays(startOfToday, 30))]; } } // ─── FAB — create event / tour ──────────────────────────────────────────────── function CreateFAB({ mainView }: { mainView: MainView }) { const [open, setOpen] = useState(false); const ref = useRef(null); const { t } = useTranslation(); useEffect(() => { if (!open) return; const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [open]); const accent = ACCENT[mainView]; return (
{open && (
setOpen(false)} className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-bold uppercase tracking-wide bg-[#ccff00] text-black hover:bg-[#ccff00]/90 transition-all" > {t('events.createEvent')} setOpen(false)} className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-bold uppercase tracking-wide bg-cyan-500 text-white hover:bg-cyan-600 transition-all" > Create Tour
)}
); } // ─── Sticky toolbar ─────────────────────────────────────────────────────────── interface ToolbarProps { mainView: MainView; onViewChange: (v: MainView) => void; counts: { events: number; tours: number }; /** Show the "Hosting" tab (logged-in users only). */ showHosting: boolean; hostingCount: number; // Events searchQuery: string; onSearchChange: (q: string) => void; locationQuery: string; onLocationChange: (q: string) => void; activeFilterCount: number; onOpenFilters: () => void; // Active chips for events selectedTypes: string[]; onRemoveType: (t: string) => void; sortBy: SortOption; trustFilter: TrustFilterLevel; onClearAll: () => void; // Timing tab (events only) activeTab: 'upcoming' | 'past'; onTabChange: (t: 'upcoming' | 'past') => void; upcomingCount: number; pastCount: number; isLoading: boolean; totalCount: number; } function EventsToolbar({ mainView, onViewChange, counts, showHosting, hostingCount, searchQuery, onSearchChange, locationQuery, onLocationChange, activeFilterCount, onOpenFilters, selectedTypes, onRemoveType, sortBy, trustFilter, onClearAll, activeTab, onTabChange, upcomingCount, pastCount, isLoading, totalCount, }: ToolbarProps) { const accent = ACCENT[mainView]; const hasActiveChips = selectedTypes.length > 0 || trustFilter !== 'all' || sortBy !== 'recent'; return (
{/* Main view tabs: Events / Tours / Hosting */}
{(['events', 'tours', ...(showHosting ? ['hosting'] as const : [])] as MainView[]).map((view) => { const isActive = mainView === view; const a = ACCENT[view]; const Icon = view === 'events' ? Calendar : view === 'tours' ? Route : Megaphone; const label = view === 'events' ? 'Events' : view === 'tours' ? 'Tours' : 'Hosting'; const count = view === 'hosting' ? hostingCount : counts[view as 'events' | 'tours']; return ( ); })}
{/* Upcoming / Past sub-tabs (events only) */} {mainView === 'events' && (
{(['upcoming', 'past'] as const).map((tab) => { const isActive = activeTab === tab; const count = tab === 'upcoming' ? upcomingCount : pastCount; return ( ); })}
)} {/* Search + Location + Filter row (not shown in Hosting) */} {mainView !== 'hosting' && (
{/* Search */}
onSearchChange(e.target.value)} placeholder={mainView === 'events' ? 'Event name…' : 'Tour name…'} className="w-full h-9 pl-8 pr-3 rounded-xl bg-white/5 border border-white/10 text-sm text-white placeholder:text-gray-600 focus:outline-none focus:border-white/30 transition-all" /> {searchQuery && ( )}
{/* Location */}
onLocationChange(e.target.value)} placeholder="City…" className="w-full h-9 pl-8 pr-3 rounded-xl bg-white/5 border border-white/10 text-sm text-white placeholder:text-gray-600 focus:outline-none focus:border-white/30 transition-all" /> {locationQuery && ( )}
{/* Filter button */}
)} {/* Active filter chips — only shown when something is selected */} {mainView !== 'hosting' && hasActiveChips && (
{selectedTypes.map((type) => ( ))} {sortBy !== 'recent' && ( {sortBy === 'trust' && <> Top Rated} {sortBy === 'popular' && <> Popular} )} {trustFilter !== 'all' && ( {trustFilter === 'verified' ? 'Verified' : trustFilter === 'trusted' ? 'Trusted' : 'Following'} )}
)} {/* Results count — desktop only; on mobile it's just noise eating space */} {mainView !== 'hosting' && !isLoading && (

{totalCount === 0 ? 'No results' : `${totalCount} result${totalCount !== 1 ? 's' : ''}`}

)}
); } // ─── Filter Drawer — bottom sheet mobile, right panel desktop ───────────────── interface FilterDrawerProps { open: boolean; onClose: () => void; mainView: MainView; // Events selectedTypes: string[]; onToggleType: (t: string) => void; sortBy: SortOption; onSortChange: (s: SortOption) => void; trustFilter: TrustFilterLevel; onTrustChange: (t: TrustFilterLevel) => void; // Tours tourCategory: TourCategory | 'all'; onTourCategoryChange: (c: TourCategory | 'all') => void; tourSkillLevel: string; onSkillLevelChange: (s: string) => void; tourSortBy: SortOption; onTourSortChange: (s: SortOption) => void; tourTrustFilter: TrustFilterLevel; onTourTrustChange: (t: TrustFilterLevel) => void; onClearAll: () => void; activeCount: number; } function FilterDrawer({ open, onClose, mainView, selectedTypes, onToggleType, sortBy, onSortChange, trustFilter, onTrustChange, tourCategory, onTourCategoryChange, tourSkillLevel, onSkillLevelChange, tourSortBy, onTourSortChange, tourTrustFilter, onTourTrustChange, onClearAll, activeCount, }: FilterDrawerProps) { const accent = ACCENT[mainView]; const trustOptions: { value: TrustFilterLevel; label: string; icon: React.ComponentType<{ className?: string }> }[] = [ { value: 'all', label: 'All', icon: Users }, { value: 'verified', label: 'Verified', icon: CheckCircle2 }, { value: 'trusted', label: 'Trusted', icon: Shield }, { value: 'following', label: 'Following', icon: CheckCircle2 }, ]; const sortOptions: { value: SortOption; label: string; icon: React.ComponentType<{ className?: string }> }[] = [ { value: 'recent', label: 'Newest', icon: Clock }, { value: 'trust', label: 'Top Rated', icon: Star }, { value: 'popular', label: 'Popular', icon: TrendingUp }, ]; const activeSortBy = mainView === 'events' ? sortBy : tourSortBy; const activeTrustFilter = mainView === 'events' ? trustFilter : tourTrustFilter; const handleSortChange = mainView === 'events' ? onSortChange : onTourSortChange; const handleTrustChange = mainView === 'events' ? onTrustChange : onTourTrustChange; const drawerContent = ( <> {/* Header */}
Filters {activeCount > 0 && ( {activeCount} active )}
{activeCount > 0 && ( )}
{/* Scrollable body */}
{/* Sort */}

Sort By

{sortOptions.map(({ value, label, icon: Icon }) => ( ))}
{/* Trust — inline pills */}

Trust Level

{trustOptions.map(({ value, label, icon: Icon }) => ( ))}
{/* Events: event type chips */} {mainView === 'events' && (

Event Type

{Object.entries(EVENT_TYPE_LABELS).map(([type, label]) => { const isActive = selectedTypes.includes(type); return ( ); })}
)} {/* Tours: category chips */} {mainView === 'tours' && ( <>

Tour Category

{Object.entries(TOUR_CATEGORIES).map(([key, info]) => { const isActive = tourCategory === key; return ( ); })}

Skill Level

{Object.entries(TOUR_SKILL_LEVELS).filter(([k]) => k !== 'all').map(([key, info]) => { const isActive = tourSkillLevel === key; return ( ); })}
)}
{/* Apply */}
); return ( <> {/* Mobile: bottom sheet */}
!v && onClose()}> Filters {drawerContent}
{/* Desktop: right side panel */}
!v && onClose()}> Filters {drawerContent}
); } // ─── Skeleton rows ──────────────────────────────────────────────────────────── function SkeletonGrid({ count = 6 }: { count?: number }) { return (
{Array.from({ length: count }).map((_, i) => ( ))}
); } function TourSkeletonGrid({ count = 6 }: { count?: number }) { return (
{Array.from({ length: count }).map((_, i) => ( ))}
); } function CalendarSkeleton() { return (
{Array.from({ length: 42 }).map((_, i) => ( ))}
{Array.from({ length: 3 }).map((_, i) => ( ))}
); } // ─── Empty states ───────────────────────────────────────────────────────────── function EmptyState({ icon: Icon, title, body, cta, to, ctaClass, onClear, hasFilters, }: { icon: React.ComponentType<{ className?: string }>; title: string; body: string; cta?: string; to?: string; ctaClass?: string; onClear?: () => void; hasFilters: boolean; }) { const actions: EmptyStateAction[] = hasFilters && onClear ? [{ label: 'Clear Filters', onClick: onClear, icon: X, variant: 'secondary' }] : cta && to ? [{ label: cta, to, icon: Plus, className: ctaClass }] : []; return ( ); } // ─── Page ───────────────────────────────────────────────────────────────────── export default function EventsPage() { const { t } = useTranslation(); useSeoMeta({ title: 'Skate Events & Tours - SkateSpots', description: 'Discover skateboarding events, competitions, tours, and sessions near you.', }); const { user } = useCurrentUser(); const { data: followingList } = useFollowing(user?.pubkey); const followCount = followingList?.length ?? 0; const smartDefault: TrustFilterLevel = (user && followCount >= 5) ? 'trusted' : 'all'; const { data: events, isLoading: eventsLoading } = useSkateEvents(100); const { data: tours, isLoading: toursLoading } = useTours(100); // ── View state ────────────────────────────────────────────────────────────── const [mainView, setMainView] = useState('events'); // Grid (list) vs month-calendar layout. Applies to events & tours only. const [layout, setLayout] = useState<'list' | 'calendar'>('list'); // ── Event filters ─────────────────────────────────────────────────────────── const [searchQuery, setSearchQuery] = useState(''); const [locationQuery, setLocationQuery] = useState(''); const [selectedTypes, setSelectedTypes] = useState([]); const [activeTab, setActiveTab] = useState<'upcoming' | 'past'>('upcoming'); const [trustFilter, setTrustFilter] = useState(smartDefault); const [sortBy, setSortBy] = useState('recent'); const [dateWindow, setDateWindow] = useState('all'); // ── Tour filters ───────────────────────────────────────────────────────────── const [tourSearchQuery, setTourSearchQuery] = useState(''); const [tourLocationQuery, setTourLocationQuery] = useState(''); const [tourCategory, setTourCategory] = useState('all'); const [tourSkillLevel, setTourSkillLevel] = useState('all'); const [tourTrustFilter, setTourTrustFilter] = useState(smartDefault); const [tourSortBy, setTourSortBy] = useState('recent'); const [filtersOpen, setFiltersOpen] = useState(false); const now = Math.floor(Date.now() / 1000); // ── Trust scores ───────────────────────────────────────────────────────────── const allPubkeys = useMemo(() => { const ep = events?.map((e) => e.pubkey) || []; const tp = tours?.map((t) => t.pubkey) || []; return [...new Set([...ep, ...tp])]; }, [events, tours]); const { data: trustScores } = useBatchTrustScores(allPubkeys); // `trustScores` is a plain Map (see useBatchTrustScores). const getTrustScore = useCallback((pubkey: string): number | undefined => trustScores?.get(pubkey), [trustScores]); // ── Comment counts ──────────────────────────────────────────────────────────── const allEventIds = useMemo(() => { const eIds = events?.map((e) => e.event.id) || []; const tIds = tours?.map((t) => t.event.id) || []; return [...eIds, ...tIds]; }, [events, tours]); const { data: commentCounts } = useCommentCounts(allEventIds); const getCommentCount = useCallback((eventId: string) => commentCounts?.get(eventId) ?? 0, [commentCounts]); // ── Batch RSVP counts (one query for the whole grid, not one per card) ───────── const eventCoords = useMemo(() => (events ?? []).map((e) => eventCoord(e)), [events]); const batchRsvps = useBatchEventRsvps(eventCoords); // Going counts keyed by SkateEvent.id, for the calendar agenda. const goingById = useMemo(() => { const map = new Map(); for (const e of events ?? []) { map.set(e.id, batchRsvps.get(eventCoord(e))?.goingCount ?? 0); } return map; }, [events, batchRsvps]); // ── Counts for tab badges ───────────────────────────────────────────────────── const rawCounts = useMemo(() => ({ events: events?.length ?? 0, tours: tours?.length ?? 0, }), [events, tours]); // Events the logged-in user is hosting (for the Hosting tab badge). const hostingCount = useMemo( () => (user && events ? events.filter((e) => e.pubkey === user.pubkey).length : 0), [user, events], ); // ── Filter events ───────────────────────────────────────────────────────────── const filteredEvents = useMemo(() => { if (!events) return { upcoming: [], past: [] }; let filtered = events.filter((event) => { if (searchQuery) { const q = searchQuery.toLowerCase(); if ( !event.title.toLowerCase().includes(q) && !event.description?.toLowerCase().includes(q) ) return false; } if (locationQuery) { const lq = locationQuery.toLowerCase(); if (!event.location?.toLowerCase().includes(lq)) return false; } if (selectedTypes.length > 0) { if (!event.types.some((type) => selectedTypes.includes(type))) return false; } return true; }); if (trustScores && trustFilter !== 'all') { if (trustFilter === 'trusted') { filtered.sort((a, b) => { const tA = (getTrustScore(a.pubkey) ?? -1) >= 60 ? 1 : 0; const tB = (getTrustScore(b.pubkey) ?? -1) >= 60 ? 1 : 0; return tB - tA; }); } else { filtered = filterByTrustLevel(filtered, trustFilter, getTrustScore); } } let upcoming = filtered.filter((e) => e.start >= now); const past = filtered.filter((e) => e.start < now); // Date window applies to upcoming events only. if (dateWindow !== 'all') { const [ws, we] = dateWindowRange(dateWindow); upcoming = upcoming.filter((e) => e.start >= ws && e.start < we); } const sortFn = (a: typeof events[0], b: typeof events[0]) => { if (sortBy === 'trust') { const diff = (getTrustScore(b.pubkey) || 0) - (getTrustScore(a.pubkey) || 0); if (diff !== 0) return diff; return a.start - b.start; } if (sortBy === 'popular') { const diff = getCommentCount(b.event.id) - getCommentCount(a.event.id); if (diff !== 0) return diff; return a.start - b.start; } return 0; }; upcoming.sort((a, b) => sortBy === 'recent' ? a.start - b.start : sortFn(a, b)); past.sort((a, b) => sortBy === 'recent' ? b.start - a.start : sortFn(a, b)); return { upcoming, past }; }, [events, searchQuery, locationQuery, selectedTypes, trustFilter, getTrustScore, trustScores, sortBy, dateWindow, now, getCommentCount]); // ── Filter tours ────────────────────────────────────────────────────────────── const filteredTours = useMemo(() => { if (!tours) return []; let filtered = tours.filter((tour) => { if (tourSearchQuery) { const q = tourSearchQuery.toLowerCase(); if ( !tour.title.toLowerCase().includes(q) && !tour.description?.toLowerCase().includes(q) && !tour.cities?.some((c) => c.toLowerCase().includes(q)) && !tour.countries?.some((c) => c.toLowerCase().includes(q)) ) return false; } if (tourLocationQuery) { const lq = tourLocationQuery.toLowerCase(); if (!tour.location?.toLowerCase().includes(lq)) return false; } if (tourCategory !== 'all' && tour.category !== tourCategory) return false; if (tourSkillLevel !== 'all' && tour.skillLevel !== tourSkillLevel) return false; return true; }); if (trustScores && tourTrustFilter !== 'all') { if (tourTrustFilter === 'trusted') { filtered.sort((a, b) => { const tA = (getTrustScore(a.pubkey) ?? -1) >= 60 ? 1 : 0; const tB = (getTrustScore(b.pubkey) ?? -1) >= 60 ? 1 : 0; return tB - tA; }); } else { filtered = filterByTrustLevel(filtered, tourTrustFilter, getTrustScore); } } filtered.sort((a, b) => { if (tourSortBy === 'trust') { const diff = (getTrustScore(b.pubkey) || 0) - (getTrustScore(a.pubkey) || 0); if (diff !== 0) return diff; } if (tourSortBy === 'popular') { const diff = getCommentCount(b.event.id) - getCommentCount(a.event.id); if (diff !== 0) return diff; } return b.createdAt - a.createdAt; }); return filtered; }, [tours, tourSearchQuery, tourLocationQuery, tourCategory, tourSkillLevel, tourTrustFilter, getTrustScore, trustScores, tourSortBy, getCommentCount]); // ── Active filter counts ────────────────────────────────────────────────────── const activeEventFilterCount = selectedTypes.length + (trustFilter !== 'all' ? 1 : 0) + (sortBy !== 'recent' ? 1 : 0) + (dateWindow !== 'all' ? 1 : 0); const activeTourFilterCount = (tourCategory !== 'all' ? 1 : 0) + (tourSkillLevel !== 'all' ? 1 : 0) + (tourTrustFilter !== 'all' ? 1 : 0) + (tourSortBy !== 'recent' ? 1 : 0); const activeFilterCount = mainView === 'events' ? activeEventFilterCount : activeTourFilterCount; const handleClearEventFilters = () => { setSearchQuery(''); setLocationQuery(''); setSelectedTypes([]); setTrustFilter('all'); setSortBy('recent'); setDateWindow('all'); }; const handleClearTourFilters = () => { setTourSearchQuery(''); setTourLocationQuery(''); setTourCategory('all'); setTourSkillLevel('all'); setTourTrustFilter('all'); setTourSortBy('recent'); }; const handleClearAll = () => { if (mainView === 'events') handleClearEventFilters(); else handleClearTourFilters(); }; const handleViewChange = (view: MainView) => { setMainView(view); setFiltersOpen(false); }; const isLoading = mainView === 'events' ? eventsLoading : toursLoading; const totalCount = mainView === 'events' ? (activeTab === 'upcoming' ? filteredEvents.upcoming.length : filteredEvents.past.length) : filteredTours.length; const currentSearchQuery = mainView === 'events' ? searchQuery : tourSearchQuery; const currentLocationQuery = mainView === 'events' ? locationQuery : tourLocationQuery; const handleSearchChange = mainView === 'events' ? setSearchQuery : setTourSearchQuery; const handleLocationChange = mainView === 'events' ? setLocationQuery : setTourLocationQuery; const hasEventFilters = !!(searchQuery || locationQuery || selectedTypes.length > 0 || trustFilter !== 'all' || sortBy !== 'recent' || dateWindow !== 'all'); const hasTourFilters = !!(tourSearchQuery || tourLocationQuery || tourCategory !== 'all' || tourSkillLevel !== 'all' || tourTrustFilter !== 'all' || tourSortBy !== 'recent'); return ( <>
{/* ── Page header ── */}

SKATE{' '} {mainView === 'events' ? 'EVENTS' : mainView === 'tours' ? 'TOURS' : 'HOSTING'}

{/* Subtitle is noise on a small screen — hide it on mobile to lift the cards above the fold. */}

{mainView === 'events' ? t('events.findEvents') : mainView === 'tours' ? 'Local guides, road trips & world destruction competitions' : 'Your events — RSVPs, updates & quick edits'}

{user ? ( ) : ( 🔒 {t('events.loginToCreate')} )}
{/* ── Sticky toolbar ── */} setFiltersOpen(true)} selectedTypes={selectedTypes} onRemoveType={(type) => setSelectedTypes((prev) => prev.filter((t) => t !== type))} sortBy={sortBy} trustFilter={trustFilter} onClearAll={handleClearAll} activeTab={activeTab} onTabChange={setActiveTab} upcomingCount={filteredEvents.upcoming.length} pastCount={filteredEvents.past.length} isLoading={isLoading} totalCount={totalCount} /> {/* ── Quick date-window chips (events · upcoming · list) ── */} {mainView === 'events' && activeTab === 'upcoming' && layout === 'list' && (
{(['all', 'today', 'weekend', 'week', 'month'] as DateWindow[]).map((w) => { const active = dateWindow === w; const fullLabel = w === 'all' ? 'Anytime' : DATE_WINDOW_LABELS[w]; return ( ); })}
)} {/* ── Layout toggle (list ⇄ calendar) — events & tours only ── */} {mainView !== 'hosting' && (
{([ { key: 'list' as const, label: 'List', Icon: LayoutGrid }, { key: 'calendar' as const, label: 'Calendar', Icon: CalendarDays }, ]).map(({ key, label, Icon }) => { const active = layout === key; const activeCls = mainView === 'events' ? 'bg-[#ccff00] text-black' : 'bg-cyan-500 text-white'; return ( ); })}
)} {/* ── Results ── */}
{/* Hosting dashboard */} {mainView === 'hosting' && } {/* Calendar layout — events */} {mainView === 'events' && layout === 'calendar' && ( eventsLoading ? ( ) : ( 0 ? filteredEvents.past.reduce((m, e) => Math.max(m, e.start), 0) : undefined } /> ) )} {/* Calendar layout — tours */} {mainView === 'tours' && layout === 'calendar' && ( toursLoading ? ( ) : ( ) )} {/* Events (list) */} {mainView === 'events' && layout === 'list' && ( eventsLoading ? ( ) : activeTab === 'upcoming' ? ( filteredEvents.upcoming.length > 0 ? (
{filteredEvents.upcoming.map((event) => ( ))}
) : ( ) ) : ( filteredEvents.past.length > 0 ? (
{filteredEvents.past.map((event) => ( ))}
) : ( ) ) )} {/* Tours (list) */} {mainView === 'tours' && layout === 'list' && ( toursLoading ? ( ) : filteredTours.length > 0 ? (
{filteredTours.map((tour) => ( ))}
) : ( ) )}
{/* ── Filter drawer ── */} setFiltersOpen(false)} mainView={mainView} selectedTypes={selectedTypes} onToggleType={(type) => setSelectedTypes((prev) => prev.includes(type) ? prev.filter((t) => t !== type) : [...prev, type] ) } sortBy={sortBy} onSortChange={setSortBy} trustFilter={trustFilter} onTrustChange={setTrustFilter} tourCategory={tourCategory} onTourCategoryChange={setTourCategory} tourSkillLevel={tourSkillLevel} onSkillLevelChange={setTourSkillLevel} tourSortBy={tourSortBy} onTourSortChange={setTourSortBy} tourTrustFilter={tourTrustFilter} onTourTrustChange={setTourTrustFilter} onClearAll={handleClearAll} activeCount={activeFilterCount} /> ); }