'use client'; import { useState, useEffect, useRef, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { Trophy, Users, Globe, Award, Search, Plus, ChevronLeft, ChevronRight, Sparkles, Flag, Filter, Gamepad2, Flame, Clock, Zap, Target, Star, TrendingUp, Calendar, Timer, Car, } from 'lucide-react'; import LeagueCard from '@/components/leagues/LeagueCard'; import Button from '@/components/ui/Button'; import Card from '@/components/ui/Card'; import Input from '@/components/ui/Input'; import Heading from '@/components/ui/Heading'; import type { LeagueSummaryDTO } from '@gridpilot/racing/application/dto/LeagueSummaryDTO'; import { getGetAllLeaguesWithCapacityAndScoringQuery } from '@/lib/di-container'; // ============================================================================ // TYPES // ============================================================================ type CategoryId = | 'all' | 'driver' | 'team' | 'nations' | 'trophy' | 'new' | 'popular' | 'iracing' | 'acc' | 'f1' | 'endurance' | 'sprint' | 'openSlots'; interface Category { id: CategoryId; label: string; icon: React.ElementType; description: string; filter: (league: LeagueSummaryDTO) => boolean; color?: string; } // ============================================================================ // DEMO LEAGUES DATA // ============================================================================ const DEMO_LEAGUES: LeagueSummaryDTO[] = [ // Driver Championships { id: 'demo-1', name: 'iRacing GT3 Pro Series', description: 'Elite GT3 competition for serious sim racers. Weekly races on iconic tracks with professional stewarding and live commentary.', createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), // 2 days ago ownerId: 'owner-1', maxDrivers: 32, usedDriverSlots: 28, structureSummary: 'Solo • 32 drivers', scoringPatternSummary: 'Sprint + Main • Best 8 of 10', timingSummary: '20 min Quali • 45 min Race', scoring: { gameId: 'iracing', gameName: 'iRacing', primaryChampionshipType: 'driver', scoringPresetId: 'sprint-main-driver', scoringPresetName: 'Sprint + Main (Driver)', dropPolicySummary: 'Best 8 of 10', scoringPatternSummary: 'Sprint + Main • Best 8 of 10', }, }, { id: 'demo-2', name: 'iRacing IMSA Championship', description: 'Race across continents in the most prestigious GT championship. Professional-grade competition with real-world rules.', createdAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), ownerId: 'owner-2', maxDrivers: 40, usedDriverSlots: 35, structureSummary: 'Solo • 40 drivers', scoringPatternSummary: 'Feature Race • Best 6 of 8', timingSummary: '30 min Quali • 60 min Race', scoring: { gameId: 'iracing', gameName: 'iRacing', primaryChampionshipType: 'driver', scoringPresetId: 'feature-driver', scoringPresetName: 'Feature Race (Driver)', dropPolicySummary: 'Best 6 of 8', scoringPatternSummary: 'Feature Race • Best 6 of 8', }, }, { id: 'demo-3', name: 'iRacing Formula Championship', description: 'The ultimate open-wheel experience. Full calendar, realistic regulations, and championship-level competition.', createdAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), // Yesterday ownerId: 'owner-3', maxDrivers: 20, usedDriverSlots: 20, structureSummary: 'Solo • 20 drivers', scoringPatternSummary: 'Sprint + Feature • All rounds count', timingSummary: '18 min Quali • 50% Race', scoring: { gameId: 'iracing', gameName: 'iRacing', primaryChampionshipType: 'driver', scoringPresetId: 'sprint-feature-driver', scoringPresetName: 'Sprint + Feature (Driver)', dropPolicySummary: 'All rounds count', scoringPatternSummary: 'Sprint + Feature • All rounds count', }, }, // Team Championships { id: 'demo-4', name: 'Le Mans Virtual Series', description: 'Endurance racing at its finest. Multi-class prototype and GT competition with team strategy at the core.', createdAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), ownerId: 'owner-4', maxDrivers: 48, usedDriverSlots: 42, maxTeams: 16, usedTeamSlots: 14, structureSummary: 'Teams • 16 × 3 drivers', scoringPatternSummary: 'Endurance • Best 4 of 6', timingSummary: '30 min Quali • 6h Race', scoring: { gameId: 'iracing', gameName: 'iRacing', primaryChampionshipType: 'team', scoringPresetId: 'endurance-team', scoringPresetName: 'Endurance (Team)', dropPolicySummary: 'Best 4 of 6', scoringPatternSummary: 'Endurance • Best 4 of 6', }, }, { id: 'demo-5', name: 'iRacing British GT Teams', description: 'British GT-style team championship. Pro-Am format with driver ratings and team strategy.', createdAt: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), ownerId: 'owner-5', maxDrivers: 40, usedDriverSlots: 32, maxTeams: 20, usedTeamSlots: 16, structureSummary: 'Teams • 20 × 2 drivers', scoringPatternSummary: 'Sprint + Main • Best 8 of 10', timingSummary: '15 min Quali • 60 min Race', scoring: { gameId: 'iracing', gameName: 'iRacing', primaryChampionshipType: 'team', scoringPresetId: 'sprint-main-team', scoringPresetName: 'Sprint + Main (Team)', dropPolicySummary: 'Best 8 of 10', scoringPatternSummary: 'Sprint + Main • Best 8 of 10', }, }, // Nations Cup { id: 'demo-6', name: 'FIA Nations Cup iRacing', description: 'Represent your nation in this prestigious international competition. Pride, glory, and national anthems.', createdAt: new Date(Date.now() - 4 * 24 * 60 * 60 * 1000), ownerId: 'owner-6', maxDrivers: 50, usedDriverSlots: 45, structureSummary: 'Nations • 50 drivers', scoringPatternSummary: 'Feature Race • All rounds count', timingSummary: '20 min Quali • 40 min Race', scoring: { gameId: 'iracing', gameName: 'iRacing', primaryChampionshipType: 'nations', scoringPresetId: 'feature-nations', scoringPresetName: 'Feature Race (Nations)', dropPolicySummary: 'All rounds count', scoringPatternSummary: 'Feature Race • All rounds count', }, }, { id: 'demo-7', name: 'European Nations GT Cup', description: 'The best European nations battle it out in GT3 machinery. Honor your flag.', createdAt: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000), ownerId: 'owner-7', maxDrivers: 30, usedDriverSlots: 24, structureSummary: 'Nations • 30 drivers', scoringPatternSummary: 'Sprint + Main • Best 6 of 8', timingSummary: '15 min Quali • 45 min Race', scoring: { gameId: 'iracing', gameName: 'iRacing', primaryChampionshipType: 'nations', scoringPresetId: 'sprint-main-nations', scoringPresetName: 'Sprint + Main (Nations)', dropPolicySummary: 'Best 6 of 8', scoringPatternSummary: 'Sprint + Main • Best 6 of 8', }, }, // Trophy Series { id: 'demo-8', name: 'Rookie Trophy Challenge', description: 'Perfect for newcomers! Learn the ropes of competitive racing in a supportive environment.', createdAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), // Yesterday ownerId: 'owner-8', maxDrivers: 24, usedDriverSlots: 18, structureSummary: 'Solo • 24 drivers', scoringPatternSummary: 'Feature Race • Best 8 of 10', timingSummary: '10 min Quali • 20 min Race', scoring: { gameId: 'iracing', gameName: 'iRacing', primaryChampionshipType: 'trophy', scoringPresetId: 'feature-trophy', scoringPresetName: 'Feature Race (Trophy)', dropPolicySummary: 'Best 8 of 10', scoringPatternSummary: 'Feature Race • Best 8 of 10', }, }, { id: 'demo-9', name: 'Porsche Cup Masters', description: 'One-make series featuring the iconic Porsche 911 GT3 Cup. Pure driving skill determines the winner.', createdAt: new Date(Date.now() - 8 * 24 * 60 * 60 * 1000), ownerId: 'owner-9', maxDrivers: 28, usedDriverSlots: 26, structureSummary: 'Solo • 28 drivers', scoringPatternSummary: 'Sprint + Main • Best 10 of 12', timingSummary: '15 min Quali • 30 min Race', scoring: { gameId: 'iracing', gameName: 'iRacing', primaryChampionshipType: 'trophy', scoringPresetId: 'sprint-main-trophy', scoringPresetName: 'Sprint + Main (Trophy)', dropPolicySummary: 'Best 10 of 12', scoringPatternSummary: 'Sprint + Main • Best 10 of 12', }, }, // More variety - Recently Added { id: 'demo-10', name: 'GT World Challenge Sprint', description: 'Fast-paced sprint racing in GT3 machinery. Short, intense races that reward consistency.', createdAt: new Date(Date.now() - 12 * 60 * 60 * 1000), // 12 hours ago ownerId: 'owner-10', maxDrivers: 36, usedDriverSlots: 12, structureSummary: 'Solo • 36 drivers', scoringPatternSummary: 'Sprint Format • Best 8 of 10', timingSummary: '10 min Quali • 25 min Race', scoring: { gameId: 'iracing', gameName: 'iRacing', primaryChampionshipType: 'driver', scoringPresetId: 'sprint-driver', scoringPresetName: 'Sprint (Driver)', dropPolicySummary: 'Best 8 of 10', scoringPatternSummary: 'Sprint Format • Best 8 of 10', }, }, { id: 'demo-11', name: 'Nürburgring 24h League', description: 'The ultimate test of endurance. Teams battle through day and night at the legendary Nordschleife.', createdAt: new Date(Date.now() - 6 * 60 * 60 * 1000), // 6 hours ago ownerId: 'owner-11', maxDrivers: 60, usedDriverSlots: 8, maxTeams: 20, usedTeamSlots: 4, structureSummary: 'Teams • 20 × 3 drivers', scoringPatternSummary: 'Endurance • All races count', timingSummary: '45 min Quali • 24h Race', scoring: { gameId: 'iracing', gameName: 'iRacing', primaryChampionshipType: 'team', scoringPresetId: 'endurance-team', scoringPresetName: 'Endurance (Team)', dropPolicySummary: 'All races count', scoringPatternSummary: 'Endurance • All races count', }, }, { id: 'demo-12', name: 'iRacing Constructors Battle', description: 'Team-based championship. Coordinate with your teammate to maximize constructor points.', createdAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago ownerId: 'owner-12', maxDrivers: 20, usedDriverSlots: 6, maxTeams: 10, usedTeamSlots: 3, structureSummary: 'Teams • 10 × 2 drivers', scoringPatternSummary: 'Full Season • All rounds count', timingSummary: '18 min Quali • 60 min Race', scoring: { gameId: 'iracing', gameName: 'iRacing', primaryChampionshipType: 'team', scoringPresetId: 'full-season-team', scoringPresetName: 'Full Season (Team)', dropPolicySummary: 'All rounds count', scoringPatternSummary: 'Full Season • All rounds count', }, }, // Additional popular leagues { id: 'demo-13', name: 'VRS GT Endurance Series', description: 'Multi-class endurance racing with LMP2 and GT3. Strategic pit stops and driver changes required.', createdAt: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000), ownerId: 'owner-13', maxDrivers: 54, usedDriverSlots: 51, maxTeams: 18, usedTeamSlots: 17, structureSummary: 'Teams • 18 × 3 drivers', scoringPatternSummary: 'Endurance • Best 5 of 6', timingSummary: '30 min Quali • 4h Race', scoring: { gameId: 'iracing', gameName: 'iRacing', primaryChampionshipType: 'team', scoringPresetId: 'endurance-team', scoringPresetName: 'Endurance (Team)', dropPolicySummary: 'Best 5 of 6', scoringPatternSummary: 'Endurance • Best 5 of 6', }, }, { id: 'demo-14', name: 'Ferrari Challenge Series', description: 'One-make Ferrari 488 Challenge championship. Italian passion meets precision racing.', createdAt: new Date(Date.now() - 20 * 24 * 60 * 60 * 1000), ownerId: 'owner-14', maxDrivers: 24, usedDriverSlots: 22, structureSummary: 'Solo • 24 drivers', scoringPatternSummary: 'Sprint + Main • Best 10 of 12', timingSummary: '15 min Quali • 35 min Race', scoring: { gameId: 'iracing', gameName: 'iRacing', primaryChampionshipType: 'trophy', scoringPresetId: 'sprint-main-trophy', scoringPresetName: 'Sprint + Main (Trophy)', dropPolicySummary: 'Best 10 of 12', scoringPatternSummary: 'Sprint + Main • Best 10 of 12', }, }, { id: 'demo-15', name: 'Oceania Nations Cup', description: 'Australia and New Zealand battle for Pacific supremacy in this regional nations championship.', createdAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), ownerId: 'owner-15', maxDrivers: 20, usedDriverSlots: 15, structureSummary: 'Nations • 20 drivers', scoringPatternSummary: 'Feature Race • Best 6 of 8', timingSummary: '15 min Quali • 45 min Race', scoring: { gameId: 'iracing', gameName: 'iRacing', primaryChampionshipType: 'nations', scoringPresetId: 'feature-nations', scoringPresetName: 'Feature Race (Nations)', dropPolicySummary: 'Best 6 of 8', scoringPatternSummary: 'Feature Race • Best 6 of 8', }, }, { id: 'demo-16', name: 'iRacing Sprint Series', description: 'Quick 20-minute races for drivers with limited time. Maximum action, minimum commitment.', createdAt: new Date(Date.now() - 18 * 60 * 60 * 1000), // 18 hours ago ownerId: 'owner-16', maxDrivers: 28, usedDriverSlots: 14, structureSummary: 'Solo • 28 drivers', scoringPatternSummary: 'Sprint Only • All races count', timingSummary: '8 min Quali • 20 min Race', scoring: { gameId: 'iracing', gameName: 'iRacing', primaryChampionshipType: 'driver', scoringPresetId: 'sprint-driver', scoringPresetName: 'Sprint (Driver)', dropPolicySummary: 'All races count', scoringPatternSummary: 'Sprint Only • All races count', }, }, ]; // ============================================================================ // CATEGORIES // ============================================================================ const CATEGORIES: Category[] = [ { id: 'all', label: 'All', icon: Globe, description: 'Browse all available leagues', filter: () => true, }, { id: 'popular', label: 'Popular', icon: Flame, description: 'Most active leagues right now', filter: (league) => { const fillRate = (league.usedDriverSlots ?? 0) / (league.maxDrivers ?? 1); return fillRate > 0.7; }, color: 'text-orange-400', }, { id: 'new', label: 'New', icon: Sparkles, description: 'Fresh leagues looking for members', filter: (league) => { const oneWeekAgo = new Date(); oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); return new Date(league.createdAt) > oneWeekAgo; }, color: 'text-performance-green', }, { id: 'openSlots', label: 'Open Slots', icon: Target, description: 'Leagues with available spots', filter: (league) => { // Check for team slots if it's a team league if (league.maxTeams && league.maxTeams > 0) { const usedTeams = league.usedTeamSlots ?? 0; return usedTeams < league.maxTeams; } // Otherwise check driver slots const used = league.usedDriverSlots ?? 0; const max = league.maxDrivers ?? 0; return max > 0 && used < max; }, color: 'text-neon-aqua', }, { id: 'driver', label: 'Driver', icon: Trophy, description: 'Compete as an individual', filter: (league) => league.scoring?.primaryChampionshipType === 'driver', }, { id: 'team', label: 'Team', icon: Users, description: 'Race together as a team', filter: (league) => league.scoring?.primaryChampionshipType === 'team', }, { id: 'nations', label: 'Nations', icon: Flag, description: 'Represent your country', filter: (league) => league.scoring?.primaryChampionshipType === 'nations', }, { id: 'trophy', label: 'Trophy', icon: Award, description: 'Special championship events', filter: (league) => league.scoring?.primaryChampionshipType === 'trophy', }, { id: 'endurance', label: 'Endurance', icon: Timer, description: 'Long-distance racing', filter: (league) => league.scoring?.scoringPresetId?.includes('endurance') ?? league.timingSummary?.includes('h Race') ?? false, }, { id: 'sprint', label: 'Sprint', icon: Clock, description: 'Quick, intense races', filter: (league) => (league.scoring?.scoringPresetId?.includes('sprint') ?? false) && !(league.scoring?.scoringPresetId?.includes('endurance') ?? false), }, ]; // ============================================================================ // LEAGUE SLIDER COMPONENT // ============================================================================ interface LeagueSliderProps { title: string; icon: React.ElementType; description: string; leagues: LeagueSummaryDTO[]; onLeagueClick: (id: string) => void; autoScroll?: boolean; iconColor?: string; scrollSpeedMultiplier?: number; scrollDirection?: 'left' | 'right'; } function LeagueSlider({ title, icon: Icon, description, leagues, onLeagueClick, autoScroll = true, iconColor = 'text-primary-blue', scrollSpeedMultiplier = 1, scrollDirection = 'right', }: LeagueSliderProps) { const scrollRef = useRef(null); const [canScrollLeft, setCanScrollLeft] = useState(false); const [canScrollRight, setCanScrollRight] = useState(true); const [isHovering, setIsHovering] = useState(false); const animationRef = useRef(null); const scrollPositionRef = useRef(0); const checkScrollButtons = useCallback(() => { if (scrollRef.current) { const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current; setCanScrollLeft(scrollLeft > 0); setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 10); } }, []); const scroll = useCallback((direction: 'left' | 'right') => { if (scrollRef.current) { const cardWidth = 340; const scrollAmount = direction === 'left' ? -cardWidth : cardWidth; // Update the ref so auto-scroll continues from new position scrollPositionRef.current = scrollRef.current.scrollLeft + scrollAmount; scrollRef.current.scrollBy({ left: scrollAmount, behavior: 'smooth' }); } }, []); // Initialize scroll position for left-scrolling sliders useEffect(() => { if (scrollDirection === 'left' && scrollRef.current) { const { scrollWidth, clientWidth } = scrollRef.current; scrollPositionRef.current = scrollWidth - clientWidth; scrollRef.current.scrollLeft = scrollPositionRef.current; } }, [scrollDirection, leagues.length]); // Smooth continuous auto-scroll using requestAnimationFrame with variable speed and direction useEffect(() => { // Allow scroll even with just 2 leagues (minimum threshold = 1) if (!autoScroll || leagues.length <= 1) return; const scrollContainer = scrollRef.current; if (!scrollContainer) return; let lastTimestamp = 0; // Base speed with multiplier for variation between sliders const baseSpeed = 0.025; const scrollSpeed = baseSpeed * scrollSpeedMultiplier; const directionMultiplier = scrollDirection === 'left' ? -1 : 1; const animate = (timestamp: number) => { if (!isHovering && scrollContainer) { const delta = lastTimestamp ? timestamp - lastTimestamp : 0; lastTimestamp = timestamp; scrollPositionRef.current += scrollSpeed * delta * directionMultiplier; const { scrollWidth, clientWidth } = scrollContainer; const maxScroll = scrollWidth - clientWidth; // Handle wrap-around for both directions if (scrollDirection === 'right' && scrollPositionRef.current >= maxScroll) { scrollPositionRef.current = 0; } else if (scrollDirection === 'left' && scrollPositionRef.current <= 0) { scrollPositionRef.current = maxScroll; } scrollContainer.scrollLeft = scrollPositionRef.current; } else { lastTimestamp = timestamp; } animationRef.current = requestAnimationFrame(animate); }; animationRef.current = requestAnimationFrame(animate); return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } }; }, [autoScroll, leagues.length, isHovering, scrollSpeedMultiplier, scrollDirection]); // Sync scroll position when user manually scrolls useEffect(() => { const scrollContainer = scrollRef.current; if (!scrollContainer) return; const handleScroll = () => { scrollPositionRef.current = scrollContainer.scrollLeft; checkScrollButtons(); }; scrollContainer.addEventListener('scroll', handleScroll); return () => scrollContainer.removeEventListener('scroll', handleScroll); }, [checkScrollButtons]); if (leagues.length === 0) return null; return (
{/* Section header */}

{title}

{description}

{leagues.length}
{/* Navigation arrows */}
{/* Scrollable container with fade edges */}
{/* Left fade gradient */}
{/* Right fade gradient */}
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} className="flex gap-4 overflow-x-auto pb-4 px-4" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none', }} > {leagues.map((league) => (
onLeagueClick(league.id)} />
))}
); } // ============================================================================ // MAIN PAGE COMPONENT // ============================================================================ export default function LeaguesPage() { const router = useRouter(); const [realLeagues, setRealLeagues] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [activeCategory, setActiveCategory] = useState('all'); const [showFilters, setShowFilters] = useState(false); useEffect(() => { loadLeagues(); }, []); const loadLeagues = async () => { try { const query = getGetAllLeaguesWithCapacityAndScoringQuery(); const allLeagues = await query.execute(); setRealLeagues(allLeagues); } catch (error) { console.error('Failed to load leagues:', error); } finally { setLoading(false); } }; // Combine real leagues with demo leagues const leagues = [...realLeagues, ...DEMO_LEAGUES]; const handleLeagueClick = (leagueId: string) => { // Don't navigate for demo leagues if (leagueId.startsWith('demo-')) { return; } router.push(`/leagues/${leagueId}`); }; // Filter by search query const searchFilteredLeagues = leagues.filter((league) => { if (!searchQuery) return true; const query = searchQuery.toLowerCase(); return ( league.name.toLowerCase().includes(query) || (league.description ?? '').toLowerCase().includes(query) || (league.scoring?.gameName ?? '').toLowerCase().includes(query) ); }); // Get leagues for active category const activeCategoryData = CATEGORIES.find((c) => c.id === activeCategory); const categoryFilteredLeagues = activeCategoryData ? searchFilteredLeagues.filter(activeCategoryData.filter) : searchFilteredLeagues; // Group leagues by category for slider view const leaguesByCategory = CATEGORIES.reduce( (acc, category) => { acc[category.id] = searchFilteredLeagues.filter(category.filter); return acc; }, {} as Record, ); // Featured categories to show as sliders with different scroll speeds and alternating directions const featuredCategoriesWithSpeed: { id: CategoryId; speed: number; direction: 'left' | 'right' }[] = [ { id: 'popular', speed: 1.0, direction: 'right' }, { id: 'new', speed: 1.3, direction: 'left' }, { id: 'driver', speed: 0.8, direction: 'right' }, { id: 'team', speed: 1.1, direction: 'left' }, { id: 'nations', speed: 0.9, direction: 'right' }, { id: 'endurance', speed: 0.7, direction: 'left' }, { id: 'sprint', speed: 1.2, direction: 'right' }, ]; if (loading) { return (

Loading leagues...

); } return (
{/* Hero Section */}
{/* Background decoration */}
Find Your Grid

From casual sprints to epic endurance battles — discover the perfect league for your racing style.

{/* Stats */}
{leagues.length} active leagues
{leaguesByCategory.new.length} new this week
{leaguesByCategory.openSlots.length} with open slots
{/* CTA */}

Set up your own racing series

{/* Search and Filter Bar */}
{/* Search */}
setSearchQuery(e.target.value)} className="pl-11" />
{/* Filter toggle (mobile) */}
{/* Category Tabs */}
{CATEGORIES.map((category) => { const Icon = category.icon; const count = leaguesByCategory[category.id].length; const isActive = activeCategory === category.id; return ( ); })}
{/* Content */} {leagues.length === 0 ? ( /* Empty State */
No leagues yet

Be the first to create a racing series. Start your own league and invite drivers to compete for glory.

) : activeCategory === 'all' && !searchQuery ? ( /* Slider View - Show featured categories with sliders at different speeds and directions */
{featuredCategoriesWithSpeed .map(({ id, speed, direction }) => { const category = CATEGORIES.find((c) => c.id === id)!; return { category, speed, direction }; }) .filter(({ category }) => leaguesByCategory[category.id].length > 0) .map(({ category, speed, direction }) => ( ))}
) : ( /* Grid View - Filtered by category or search */
{categoryFilteredLeagues.length > 0 ? ( <>

Showing {categoryFilteredLeagues.length}{' '} {categoryFilteredLeagues.length === 1 ? 'league' : 'leagues'} {searchQuery && ( {' '} for "{searchQuery}" )}

{categoryFilteredLeagues.map((league) => ( handleLeagueClick(league.id)} /> ))}
) : (

No leagues found{searchQuery ? ` matching "${searchQuery}"` : ' in this category'}

)}
)}
); }