* Fix event date display off-by-one for late-night events in UTC-negative timezones Events saved at 10 PM PDT (UTC-7) cross UTC midnight, so eventDate is stored as the next UTC calendar day (e.g. March 28 10 PM PDT → eventDate "2026-03-29"). Displaying that date string via parseISO() gives March 29 local midnight, so the events list showed "Mar 29" when the user expected "Mar 28". Fix: when eventStartsAt is available, derive the display date from it directly (format(new Date(eventStartsAt), ...)) rather than from the stored eventDate string. This correctly converts the UTC timestamp to the user's local calendar date. Date-only events (no eventStartsAt) are unchanged and continue to use parseISO(eventDate). Also tighten the getDisplayDate() guard: replace a misleading try/catch (new Date() never throws) with an explicit isNaN check, and replace a non-null assertion (eventDate!) with null-coalescing in the admin events list. Tests cover both UTC and PDT environments and are timezone-independent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix same-day events incorrectly showing Upcoming instead of Results Pending String date comparison (eventDate < today) is strictly less-than, so events on the current day always showed Upcoming regardless of time. When eventStartsAt is available, use timestamp comparison (new Date(eventStartsAt) < new Date()) for accurate past/future determination. Fixed in three locations: admin events list (getStatusBadge call site), admin event detail isPast check, and EventSchedule upcoming badge. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
175 lines
6.1 KiB
TypeScript
175 lines
6.1 KiB
TypeScript
import { format, parseISO } from "date-fns";
|
|
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;
|
|
eventStartsAt?: Date | string | null;
|
|
eventType: string;
|
|
isComplete: boolean;
|
|
completedAt?: Date | string | null;
|
|
}
|
|
|
|
interface EventScheduleProps {
|
|
upcomingEvents: ScheduleEvent[];
|
|
recentEvents: ScheduleEvent[];
|
|
}
|
|
|
|
function formatEventDate(dateStr: string | null | undefined): string {
|
|
if (!dateStr) return "Date TBD";
|
|
try {
|
|
// eventDate is stored as a date string "YYYY-MM-DD"
|
|
return format(parseISO(dateStr), "MMM d, yyyy");
|
|
} catch {
|
|
return dateStr;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the display date for an event.
|
|
* Prefers eventStartsAt (a full UTC timestamp → correct local date) over
|
|
* eventDate (the UTC calendar date, which is off by one day for late-night
|
|
* events in UTC-negative timezones like PDT).
|
|
*/
|
|
function getDisplayDate(event: Pick<ScheduleEvent, "eventDate" | "eventStartsAt">): string {
|
|
if (event.eventStartsAt) {
|
|
const d = new Date(event.eventStartsAt);
|
|
if (!isNaN(d.getTime())) {
|
|
return format(d, "MMM d, yyyy");
|
|
}
|
|
}
|
|
return formatEventDate(event.eventDate);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
export function EventSchedule({ upcomingEvents, recentEvents }: EventScheduleProps) {
|
|
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">
|
|
{upcomingEvents.map((event, index) => (
|
|
<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" />
|
|
)}
|
|
{event.name}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground flex items-center gap-1 mt-0.5">
|
|
<Calendar className="h-3 w-3" />
|
|
<span suppressHydrationWarning>{getDisplayDate(event)}</span>
|
|
{event.eventStartsAt && (
|
|
<span suppressHydrationWarning>
|
|
· {format(new Date(event.eventStartsAt), "h:mm a")}
|
|
</span>
|
|
)}
|
|
</p>
|
|
</div>
|
|
{event.eventType === "schedule_event" ? (
|
|
!event.isComplete && (() => {
|
|
const isPast = event.eventStartsAt
|
|
? new Date(event.eventStartsAt) < new Date()
|
|
: event.eventDate != null && event.eventDate < new Date().toISOString().split("T")[0];
|
|
return isPast && (
|
|
<Badge variant="outline" className="text-xs shrink-0 border-amber-500/30 text-amber-400">
|
|
Results Pending
|
|
</Badge>
|
|
);
|
|
})()
|
|
) : (
|
|
<Badge variant="outline" className="text-xs shrink-0">
|
|
{getEventTypeLabel(event.eventType)}
|
|
</Badge>
|
|
)}
|
|
</li>
|
|
))}
|
|
</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">
|
|
{recentEvents.map((event, index) => (
|
|
<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">
|
|
{event.name}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground flex items-center gap-1 mt-0.5">
|
|
<Calendar className="h-3 w-3" />
|
|
<span suppressHydrationWarning>{getDisplayDate(event)}</span>
|
|
</p>
|
|
</div>
|
|
{event.eventType !== "schedule_event" && (
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs shrink-0 border-emerald-500/30 text-emerald-400"
|
|
>
|
|
Done
|
|
</Badge>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|