import { format } from "date-fns"; import { ChevronRight, Clock } from "lucide-react"; import { useMemo } from "react"; import { Link } from "react-router"; import { Badge } from "~/components/ui/badge"; import { LeagueAvatar } from "~/components/league/LeagueAvatar"; import { GradientIcon } from "~/components/ui/GradientIcon"; import { formatEventDate } from "~/lib/date-utils"; import { BRACKT_GRADIENT } from "~/lib/brand"; import type { CalendarPanelEvent } from "./UpcomingCalendarPanel"; interface Props { events: CalendarPanelEvent[]; title?: string; limit?: number; viewAllUrl?: string; emptyMessage?: string; /** Whether to show the league avatar next to participants. Default true. */ showLeagueAvatar?: boolean; } export interface LeagueParticipants { leagueId: string; leagueName: string; participants: Array<{ id: string; name: string }>; } export interface GroupedEvent { id: string; name: string; matchLabel?: string | null; eventDate: string | null; earliestGameTime: string | null; eventType: string; sportName: string; sportSeasonName: string; sportsSeasonPageUrl?: string; leagues: LeagueParticipants[]; totalParticipants: number; isAllCompete: boolean; } function groupEvents(events: CalendarPanelEvent[]): GroupedEvent[] { const map = new Map(); for (const event of events) { const existing = map.get(event.id); const isAllCompete = event.eventType !== "playoff_game"; const leagueEntry: LeagueParticipants = { leagueId: event.leagueId ?? "unknown", leagueName: event.leagueName ?? "League", participants: event.relevantParticipants, }; if (existing) { existing.leagues.push(leagueEntry); existing.totalParticipants += event.relevantParticipants.length; } else { map.set(event.id, { id: event.id, name: event.name, matchLabel: event.matchLabel, eventDate: event.eventDate, earliestGameTime: event.earliestGameTime, eventType: event.eventType, sportName: event.sportName, sportSeasonName: event.sportSeasonName, sportsSeasonPageUrl: event.sportsSeasonPageUrl, leagues: [leagueEntry], totalParticipants: event.relevantParticipants.length, isAllCompete, }); } } return Array.from(map.values()); } function TimelineDot({ isFirst }: { isFirst: boolean }) { if (isFirst) { return (
); } return (
); } function ParticipantsByLeague({ leagues, isAllCompete, showLeagueAvatar = true, }: { leagues: LeagueParticipants[]; isAllCompete: boolean; showLeagueAvatar?: boolean; }) { return (
{leagues.map((league) => { const content = isAllCompete && league.participants.length > 1 ? ( {league.participants.length} of your picks ) : ( {league.participants.slice(0, 3).map((p) => ( {p.name} ))} {league.participants.length > 3 && ( +{league.participants.length - 3} more )} ); if (!showLeagueAvatar) { return
{content}
; } return (
{content}
); })}
); } export function EventRow({ event, isFirst, isLast, showLeagueAvatar = true, }: { event: GroupedEvent; isFirst: boolean; isLast: boolean; showLeagueAvatar?: boolean; }) { const gameDate = event.earliestGameTime ? new Date(event.earliestGameTime) : null; const dateStr = gameDate ? format(gameDate, "MMM d") : formatEventDate(event.eventDate); const timeStr = gameDate ? gameDate.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }) : null; const displayName = event.matchLabel ? `${event.name} — ${event.matchLabel}` : event.name; const content = (
{/* Timeline spine */}
{!isLast &&
}
{/* Content */}
{/* Left: date + name */}
{dateStr ?? "TBD"} {timeStr && ( <> | {timeStr} )}
{displayName}
{event.sportName}
{/* Divider — hidden on mobile where layout is stacked */}
{/* Right: participants per league */}
); if (event.sportsSeasonPageUrl) { return ( {content} ); } return
{content}
; } export function UpcomingEventsCard({ events, title = "My Upcoming Events", limit, viewAllUrl, emptyMessage, showLeagueAvatar = true, }: Props) { const localFilteredEvents = useMemo(() => { const todayStr = new Intl.DateTimeFormat("en-CA").format(new Date()); return events.filter( (e) => (e.earliestGameTime ?? e.eventDate ?? "9999-12-31") >= todayStr ); }, [events]); const grouped = groupEvents(localFilteredEvents); const displayedEvents = limit !== undefined ? grouped.slice(0, limit) : grouped; const hiddenCount = limit !== undefined ? Math.max(0, grouped.length - limit) : 0; return (
{/* Header */}
{title}
{viewAllUrl && ( View All )}
{/* Timeline */} {grouped.length === 0 ? (

{emptyMessage ?? "No upcoming events in the next 30 days."}

) : (
{displayedEvents.map((event, index) => ( ))} {viewAllUrl && hiddenCount > 0 && ( View all {grouped.length} upcoming events )}
)}
); }