2026-06-17 06:09:29 +00:00
|
|
|
import { format } from "date-fns";
|
|
|
|
|
import { useHasMounted } from "~/hooks/useHasMounted";
|
2026-06-16 14:11:51 +00:00
|
|
|
import { Link } from "react-router";
|
2026-03-07 21:59:29 -08:00
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
|
|
|
|
import { Badge } from "~/components/ui/badge";
|
|
|
|
|
import { Calendar, CheckCircle2, Clock } from "lucide-react";
|
|
|
|
|
|
|
|
|
|
interface ScheduleEvent {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
eventDate: string | null;
|
2026-03-15 10:22:42 -07:00
|
|
|
eventStartsAt?: Date | string | null;
|
2026-03-07 21:59:29 -08:00
|
|
|
eventType: string;
|
|
|
|
|
isComplete: boolean;
|
|
|
|
|
completedAt?: Date | string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface EventScheduleProps {
|
|
|
|
|
upcomingEvents: ScheduleEvent[];
|
|
|
|
|
recentEvents: ScheduleEvent[];
|
2026-06-16 14:11:51 +00:00
|
|
|
/** When provided, event names link to the event detail page */
|
|
|
|
|
leagueId?: string;
|
|
|
|
|
sportsSeasonId?: string;
|
2026-03-07 21:59:29 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatEventDate(dateStr: string | null | undefined): string {
|
|
|
|
|
if (!dateStr) return "Date TBD";
|
|
|
|
|
try {
|
2026-06-17 06:09:29 +00:00
|
|
|
const [year, month, day] = dateStr.split("-").map(Number);
|
|
|
|
|
return format(new Date(year, month - 1, day), "MMM d, yyyy");
|
2026-03-07 21:59:29 -08:00
|
|
|
} catch {
|
|
|
|
|
return dateStr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 11:18:16 -07:00
|
|
|
|
2026-03-07 21:59:29 -08:00
|
|
|
function getEventTypeLabel(eventType: string): string {
|
|
|
|
|
switch (eventType) {
|
|
|
|
|
case "playoff_game":
|
|
|
|
|
return "Bracket";
|
|
|
|
|
case "major_tournament":
|
|
|
|
|
return "Major";
|
|
|
|
|
case "final_standings":
|
|
|
|
|
return "Final Standings";
|
|
|
|
|
case "schedule_event":
|
|
|
|
|
return "Non-Scoring";
|
|
|
|
|
default:
|
|
|
|
|
return eventType;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 14:11:51 +00:00
|
|
|
export function EventSchedule({ upcomingEvents, recentEvents, leagueId, sportsSeasonId }: EventScheduleProps) {
|
2026-06-17 06:09:29 +00:00
|
|
|
const isMounted = useHasMounted();
|
|
|
|
|
|
|
|
|
|
function getDisplayDate(event: Pick<ScheduleEvent, "eventDate" | "eventStartsAt">): string {
|
|
|
|
|
if (isMounted && event.eventStartsAt) {
|
|
|
|
|
const d = new Date(event.eventStartsAt as string);
|
|
|
|
|
if (!isNaN(d.getTime())) return format(d, "MMM d, yyyy");
|
|
|
|
|
}
|
|
|
|
|
return formatEventDate(event.eventDate);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 21:59:29 -08:00
|
|
|
const hasUpcoming = upcomingEvents.length > 0;
|
|
|
|
|
const hasRecent = recentEvents.length > 0;
|
|
|
|
|
|
|
|
|
|
if (!hasUpcoming && !hasRecent) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
|
|
|
{/* Upcoming Events */}
|
|
|
|
|
{hasUpcoming && (
|
|
|
|
|
<Card className="border-electric/20">
|
|
|
|
|
<CardHeader className="pb-3">
|
|
|
|
|
<CardTitle className="text-sm font-medium flex items-center gap-2 text-electric">
|
|
|
|
|
<Clock className="h-4 w-4" />
|
|
|
|
|
Upcoming
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="pt-0">
|
|
|
|
|
<ul className="space-y-3">
|
2026-06-16 14:11:51 +00:00
|
|
|
{upcomingEvents.map((event, index) => {
|
|
|
|
|
const eventDetailUrl = leagueId && sportsSeasonId
|
|
|
|
|
? `/leagues/${leagueId}/sports-seasons/${sportsSeasonId}/events/${event.id}`
|
|
|
|
|
: null;
|
|
|
|
|
return (
|
2026-03-07 21:59:29 -08:00
|
|
|
<li
|
|
|
|
|
key={event.id}
|
|
|
|
|
className={`flex items-start justify-between gap-2 ${
|
|
|
|
|
index === 0 ? "" : "border-t pt-3"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<p
|
|
|
|
|
className={`text-sm font-medium truncate ${
|
|
|
|
|
index === 0 ? "text-foreground" : "text-muted-foreground"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{index === 0 && (
|
|
|
|
|
<span className="inline-block w-1.5 h-1.5 rounded-full bg-electric mr-2 mb-0.5" />
|
|
|
|
|
)}
|
2026-06-16 14:11:51 +00:00
|
|
|
{eventDetailUrl ? (
|
|
|
|
|
<Link to={eventDetailUrl} className="hover:underline">
|
|
|
|
|
{event.name}
|
|
|
|
|
</Link>
|
|
|
|
|
) : (
|
|
|
|
|
event.name
|
|
|
|
|
)}
|
2026-03-07 21:59:29 -08:00
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground flex items-center gap-1 mt-0.5">
|
|
|
|
|
<Calendar className="h-3 w-3" />
|
2026-06-17 06:09:29 +00:00
|
|
|
<span>{getDisplayDate(event)}</span>
|
|
|
|
|
{isMounted && event.eventStartsAt && (
|
|
|
|
|
<span>· {format(new Date(event.eventStartsAt), "h:mm a")}</span>
|
2026-03-15 10:22:42 -07:00
|
|
|
)}
|
2026-03-07 21:59:29 -08:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
{event.eventType === "schedule_event" ? (
|
2026-03-15 12:28:39 -07:00
|
|
|
!event.isComplete && (() => {
|
|
|
|
|
const isPast = event.eventStartsAt
|
|
|
|
|
? new Date(event.eventStartsAt) < new Date()
|
2026-03-21 09:44:05 -07:00
|
|
|
: event.eventDate !== null && event.eventDate < new Date().toISOString().split("T")[0];
|
2026-03-15 12:28:39 -07:00
|
|
|
return isPast && (
|
|
|
|
|
<Badge variant="outline" className="text-xs shrink-0 border-amber-500/30 text-amber-400">
|
|
|
|
|
Results Pending
|
|
|
|
|
</Badge>
|
|
|
|
|
);
|
|
|
|
|
})()
|
2026-03-07 21:59:29 -08:00
|
|
|
) : (
|
|
|
|
|
<Badge variant="outline" className="text-xs shrink-0">
|
|
|
|
|
{getEventTypeLabel(event.eventType)}
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
</li>
|
2026-06-16 14:11:51 +00:00
|
|
|
);
|
|
|
|
|
})}
|
2026-03-07 21:59:29 -08:00
|
|
|
</ul>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Recent Results */}
|
|
|
|
|
{hasRecent && (
|
|
|
|
|
<Card className="border-emerald-500/20">
|
|
|
|
|
<CardHeader className="pb-3">
|
|
|
|
|
<CardTitle className="text-sm font-medium flex items-center gap-2 text-emerald-400">
|
|
|
|
|
<CheckCircle2 className="h-4 w-4" />
|
|
|
|
|
Recent Results
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="pt-0">
|
|
|
|
|
<ul className="space-y-3">
|
2026-06-16 14:11:51 +00:00
|
|
|
{recentEvents.map((event, index) => {
|
|
|
|
|
const eventDetailUrl = leagueId && sportsSeasonId
|
|
|
|
|
? `/leagues/${leagueId}/sports-seasons/${sportsSeasonId}/events/${event.id}`
|
|
|
|
|
: null;
|
|
|
|
|
return (
|
2026-03-07 21:59:29 -08:00
|
|
|
<li
|
|
|
|
|
key={event.id}
|
|
|
|
|
className={`flex items-start justify-between gap-2 ${
|
|
|
|
|
index === 0 ? "" : "border-t pt-3"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<p className="text-sm font-medium text-muted-foreground truncate">
|
2026-06-16 14:11:51 +00:00
|
|
|
{eventDetailUrl ? (
|
|
|
|
|
<Link to={eventDetailUrl} className="hover:underline">
|
|
|
|
|
{event.name}
|
|
|
|
|
</Link>
|
|
|
|
|
) : (
|
|
|
|
|
event.name
|
|
|
|
|
)}
|
2026-03-07 21:59:29 -08:00
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground flex items-center gap-1 mt-0.5">
|
|
|
|
|
<Calendar className="h-3 w-3" />
|
2026-06-17 06:09:29 +00:00
|
|
|
<span>{getDisplayDate(event)}</span>
|
2026-03-07 21:59:29 -08:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
{event.eventType !== "schedule_event" && (
|
|
|
|
|
<Badge
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="text-xs shrink-0 border-emerald-500/30 text-emerald-400"
|
|
|
|
|
>
|
|
|
|
|
Done
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
</li>
|
2026-06-16 14:11:51 +00:00
|
|
|
);
|
|
|
|
|
})}
|
2026-03-07 21:59:29 -08:00
|
|
|
</ul>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|