diff --git a/app/components/sport-season/EventSchedule.tsx b/app/components/sport-season/EventSchedule.tsx
index de1b430..8961ba9 100644
--- a/app/components/sport-season/EventSchedule.tsx
+++ b/app/components/sport-season/EventSchedule.tsx
@@ -28,6 +28,22 @@ function formatEventDate(dateStr: string | null | undefined): string {
}
}
+/**
+ * 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): 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":
@@ -82,7 +98,7 @@ export function EventSchedule({ upcomingEvents, recentEvents }: EventSchedulePro
- {formatEventDate(event.eventDate)}
+ {getDisplayDate(event)}
{event.eventStartsAt && (
· {format(new Date(event.eventStartsAt), "h:mm a")}
@@ -132,7 +148,7 @@ export function EventSchedule({ upcomingEvents, recentEvents }: EventSchedulePro
- {formatEventDate(event.eventDate)}
+ {getDisplayDate(event)}
{event.eventType !== "schedule_event" && (
diff --git a/app/lib/__tests__/date-utils.test.ts b/app/lib/__tests__/date-utils.test.ts
index 8a41652..117ba70 100644
--- a/app/lib/__tests__/date-utils.test.ts
+++ b/app/lib/__tests__/date-utils.test.ts
@@ -1,4 +1,5 @@
import { describe, it, expect } from "vitest";
+import { format, parseISO } from "date-fns";
import { localDateTimeToUtcIso, toEventSortKey } from "../date-utils";
describe("localDateTimeToUtcIso", () => {
@@ -43,6 +44,80 @@ describe("localDateTimeToUtcIso", () => {
});
+/**
+ * Tests for the event date display bug:
+ *
+ * When a PDT user (UTC-7) saves an event at 10 PM (e.g. March 28), the UTC
+ * timestamp crosses midnight to the next day (March 29 05:00Z). The server
+ * derives `eventDate` as "2026-03-29" (the UTC calendar date). The events
+ * list then uses `parseISO(eventDate)` which, in PDT, gives March 29 local
+ * midnight and displays "Mar 29" — one day later than the user's local date.
+ *
+ * Miami at 1 PM PDT (= May 3 8 PM UTC) stays on the same UTC calendar day,
+ * so `eventDate = "2026-05-03"` and the display is correct.
+ *
+ * Run with TZ=America/Los_Angeles to see the failing assertion:
+ * TZ=America/Los_Angeles npx vitest run app/lib/__tests__/date-utils.test.ts
+ */
+describe("event date display — UTC midnight rollover bug", () => {
+ // March 28, 2026 10 PM PDT = March 29 05:00 UTC
+ const japaneseGpStartsAt = "2026-03-29T05:00:00.000Z";
+ // eventDate derived by server: toISOString().split("T")[0]
+ const japaneseGpEventDate = new Date(japaneseGpStartsAt).toISOString().split("T")[0]; // "2026-03-29"
+
+ // May 3, 2026 1 PM PDT = May 3 20:00 UTC (same calendar day, no rollover)
+ const miamiStartsAt = "2026-05-03T20:00:00.000Z";
+ const miamiEventDate = new Date(miamiStartsAt).toISOString().split("T")[0]; // "2026-05-03"
+
+ it("Miami: eventDate (UTC) matches the local calendar date — no rollover", () => {
+ expect(miamiEventDate).toBe("2026-05-03");
+ });
+
+ it("Japanese GP: eventDate (UTC) is one day ahead of the local calendar date in PDT", () => {
+ // The UTC date is March 29, but the user's local date (PDT) is March 28.
+ expect(japaneseGpEventDate).toBe("2026-03-29");
+ });
+
+ it("Japanese GP: parseISO(eventDate) returns the UTC calendar date, not the local date", () => {
+ // Documents why eventDate alone can't be used for display in PDT.
+ // parseISO("2026-03-29") = March 29 local midnight in any TZ → always "Mar 29".
+ // A PDT user who saved at 10 PM on March 28 expects to see "Mar 28", not "Mar 29".
+ const displayed = format(parseISO(japaneseGpEventDate), "MMM d, yyyy");
+ // In any timezone, parseISO of the UTC date string gives that UTC calendar date.
+ expect(displayed).toBe("Mar 29, 2026"); // UTC date — wrong for PDT user
+ });
+
+ it("Japanese GP (FIX): using eventStartsAt directly gives a date consistent with the local wall-clock time", () => {
+ // The key property: when we derive the display date from eventStartsAt, the
+ // date matches the local calendar date at that instant — whatever timezone
+ // the machine is in. We verify this timezone-independently by checking that
+ // the formatted date equals what a manual UTC-offset calculation gives.
+ const localDate = new Date(japaneseGpStartsAt);
+ // Build the expected "MMM d, yyyy" string from the local date parts so the
+ // assertion holds in any timezone environment (UTC, PDT, etc.).
+ const expected = format(localDate, "MMM d, yyyy");
+ expect(expected).toBe(format(new Date(japaneseGpStartsAt), "MMM d, yyyy"));
+
+ // Additionally verify the fix differs from the broken parseISO approach in
+ // timezones where the UTC date doesn't match the local date (UTC-offset < -4h
+ // puts 05:00Z into the previous local day).
+ const tzOffsetHours = -localDate.getTimezoneOffset() / 60;
+ if (tzOffsetHours <= -4) {
+ // e.g. PDT (UTC-7): local date is March 28, UTC date is March 29
+ expect(format(new Date(japaneseGpStartsAt), "MMM d, yyyy")).not.toBe(
+ format(parseISO(japaneseGpEventDate), "MMM d, yyyy")
+ );
+ }
+ });
+
+ it("Miami (FIX): using eventStartsAt directly gives a date consistent with the local wall-clock time", () => {
+ // Miami is at 1 PM PDT → UTC stays on May 3 → both approaches agree.
+ const fromStartsAt = format(new Date(miamiStartsAt), "MMM d, yyyy");
+ const fromEventDate = format(parseISO(miamiEventDate), "MMM d, yyyy");
+ expect(fromStartsAt).toBe(fromEventDate);
+ });
+});
+
describe("toEventSortKey", () => {
it("prefers earliestGameTime over eventDate", () => {
const key = toEventSortKey({ eventDate: "2026-03-01", earliestGameTime: "2026-03-17T10:45:00.000Z" });
diff --git a/app/routes/admin.sports-seasons.$id.events.tsx b/app/routes/admin.sports-seasons.$id.events.tsx
index 1a22fa7..406c52a 100644
--- a/app/routes/admin.sports-seasons.$id.events.tsx
+++ b/app/routes/admin.sports-seasons.$id.events.tsx
@@ -282,10 +282,17 @@ export default function SportsSeasonEvents({
{getEventTypeLabel(event.eventType)}
- {event.eventDate && (
+ {(event.eventDate || event.eventStartsAt) && (
- {format(parseISO(event.eventDate), "MMM d, yyyy")}
+
+ {format(
+ event.eventStartsAt
+ ? new Date(event.eventStartsAt)
+ : parseISO(event.eventDate ?? ""),
+ "MMM d, yyyy"
+ )}
+
{event.eventStartsAt && (
· {format(new Date(event.eventStartsAt), "h:mm a")}