'use client'; import { useState, useRef, useCallback } from 'react'; import { Trophy, Users, Globe, Award, Search, Plus, ChevronLeft, ChevronRight, Sparkles, Flag, Filter, Flame, Clock, Target, Timer, } from 'lucide-react'; import { LeagueCard } from '@/ui/LeagueCardWrapper'; import { Button } from '@/ui/Button'; import { Card } from '@/ui/Card'; import { Input } from '@/ui/Input'; import { Heading } from '@/ui/Heading'; import { Container } from '@/ui/Container'; import { Box } from '@/ui/Box'; import { Stack } from '@/ui/Stack'; import { Text } from '@/ui/Text'; import { Grid } from '@/ui/Grid'; import { GridItem } from '@/ui/GridItem'; import { PageHero } from '@/ui/PageHero'; import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData'; import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel'; // ============================================================================ // 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: LeaguesViewData['leagues'][number]) => boolean; color?: string; } interface LeagueSliderProps { title: string; icon: React.ElementType; description: string; leagues: LeaguesViewData['leagues']; autoScroll?: boolean; iconColor?: string; scrollSpeedMultiplier?: number; scrollDirection?: 'left' | 'right'; } interface LeaguesTemplateProps { viewData: LeaguesViewData; } // ============================================================================ // 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 // ============================================================================ function LeagueSlider({ title, icon: Icon, description, leagues, 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 const initializeScroll = useCallback(() => { if (scrollDirection === 'left' && scrollRef.current) { const { scrollWidth, clientWidth } = scrollRef.current; scrollPositionRef.current = scrollWidth - clientWidth; scrollRef.current.scrollLeft = scrollPositionRef.current; } }, [scrollDirection]); // Smooth continuous auto-scroll using requestAnimationFrame with variable speed and direction const setupAutoScroll = useCallback(() => { // 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 const setupManualScroll = useCallback(() => { const scrollContainer = scrollRef.current; if (!scrollContainer) return; const handleScroll = () => { scrollPositionRef.current = scrollContainer.scrollLeft; checkScrollButtons(); }; scrollContainer.addEventListener('scroll', handleScroll); return () => scrollContainer.removeEventListener('scroll', handleScroll); }, [checkScrollButtons]); // Initialize effects useState(() => { initializeScroll(); }); // Setup auto-scroll effect useState(() => { setupAutoScroll(); }); // Setup manual scroll effect useState(() => { setupManualScroll(); }); 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) => { // Convert ViewData to ViewModel for LeagueCard const viewModel: LeagueSummaryViewModel = { id: league.id, name: league.name, description: league.description ?? '', logoUrl: league.logoUrl, ownerId: league.ownerId, createdAt: league.createdAt, maxDrivers: league.maxDrivers, usedDriverSlots: league.usedDriverSlots, maxTeams: league.maxTeams ?? 0, usedTeamSlots: league.usedTeamSlots ?? 0, structureSummary: league.structureSummary, timingSummary: league.timingSummary, category: league.category ?? undefined, scoring: league.scoring ? { ...league.scoring, primaryChampionshipType: league.scoring.primaryChampionshipType as 'driver' | 'team' | 'nations' | 'trophy', } : undefined, }; return ( ); })}
); } // ============================================================================ // MAIN TEMPLATE COMPONENT // ============================================================================ export function LeaguesClient({ viewData, }: LeaguesTemplateProps) { const [searchQuery, setSearchQuery] = useState(''); const [activeCategory, setActiveCategory] = useState('all'); const [showFilters, setShowFilters] = useState(false); // Filter by search query const searchFilteredLeagues = viewData.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) => { // First try to use the dedicated category field, fall back to scoring-based filtering acc[category.id] = searchFilteredLeagues.filter((league) => { // If league has a category field, use it directly if (league.category) { return league.category === category.id; } // Otherwise fall back to the existing scoring-based filter return category.filter(league); }); 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' }, ]; return ( {/* Hero Section */} { window.location.href = '/leagues/create'; }, icon: Plus, description: '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 */} {viewData.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) => { // Convert ViewData to ViewModel for LeagueCard const viewModel: LeagueSummaryViewModel = { id: league.id, name: league.name, description: league.description ?? '', logoUrl: league.logoUrl, ownerId: league.ownerId, createdAt: league.createdAt, maxDrivers: league.maxDrivers, usedDriverSlots: league.usedDriverSlots, maxTeams: league.maxTeams ?? 0, usedTeamSlots: league.usedTeamSlots ?? 0, structureSummary: league.structureSummary, timingSummary: league.timingSummary, category: league.category ?? undefined, scoring: league.scoring ? { ...league.scoring, primaryChampionshipType: league.scoring.primaryChampionshipType as 'driver' | 'team' | 'nations' | 'trophy', } : undefined, }; return ( ); })} ) : ( No leagues found{searchQuery ? ` matching "${searchQuery}"` : ' in this category'} )} )} ); }