- New `event_starts_at timestamptz` column on `scoring_events`; backfilled
from `event_date` (midnight UTC) via migration for all existing rows
- Admin create/edit forms consolidated from separate Date + Time inputs into
a single `datetime-local` field; server derives `eventDate` from the UTC
date portion of `eventStartsAt` so calendar-window queries continue to work
- Edit form pre-fill computed client-side via useEffect to avoid server-timezone
mismatch for non-UTC admins
- Sort fix: replaced `eventDate ?? earliestGameTime.split("T")[0]` with
`toEventSortKey` helper that prefers `earliestGameTime ?? eventDate` so
bracket events with only per-game timestamps sort in the correct calendar position
- DB sort queries updated to use `eventStartsAt` as a tiebreaker after `eventDate`
- F1/IndyCar/golf events now populate `earliestGameTime` from `eventStartsAt`,
giving time display in `UpcomingCalendarPanel`
- Bulk import gains optional `HH:MM` (UTC) third column
- New unit tests for `toEventSortKey` and `eventStartsAt` model behavior
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
58 lines
2.4 KiB
TypeScript
58 lines
2.4 KiB
TypeScript
/**
|
|
* Converts a datetime-local input value (local time, no timezone info)
|
|
* to a UTC ISO string for server submission.
|
|
*
|
|
* `datetime-local` inputs use the format "YYYY-MM-DDTHH:MM" and do NOT accept
|
|
* ISO strings with a timezone designator (e.g. "2026-03-11T17:00:00.000Z").
|
|
* Setting an ISO UTC string directly on a datetime-local input causes the browser
|
|
* to silently clear the field value. Use this function with a separate hidden input
|
|
* to hold the converted UTC value before form submission.
|
|
*
|
|
* Returns null when value is empty, nullish, or not a valid date.
|
|
*
|
|
* NOTE: The returned ISO string (e.g. "2026-03-11T17:00:00.000Z") is intentionally
|
|
* incompatible with datetime-local inputs, which only accept "YYYY-MM-DDTHH:MM".
|
|
* Setting a Z-suffixed string on a datetime-local input causes browsers to silently
|
|
* clear the field. Always write the result to a separate hidden input, not back to
|
|
* the datetime-local input itself.
|
|
*/
|
|
import { format, parseISO } from "date-fns";
|
|
|
|
/**
|
|
* Format a YYYY-MM-DD date string for display (e.g. "Apr 9").
|
|
* Returns null when the value is missing or unparseable — callers decide how to handle that case.
|
|
*/
|
|
export function formatEventDate(dateStr: string | null | undefined): string | null {
|
|
if (!dateStr) return null;
|
|
try {
|
|
return format(parseISO(dateStr), "MMM d");
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Produces a stable sort key for upcoming calendar events.
|
|
*
|
|
* Bracket events carry a full ISO game timestamp in `earliestGameTime`; all-compete
|
|
* events (F1, IndyCar, golf) use the ISO timestamp in `earliestGameTime` when a start
|
|
* time has been set, or fall back to the date-only `eventDate` string. Events with
|
|
* neither sort to the far future so they appear last.
|
|
*
|
|
* The mixed comparison of ISO timestamps and "YYYY-MM-DD" strings works correctly via
|
|
* lexicographic order because both share the same date prefix; a date-only string always
|
|
* sorts before a same-day timestamp (shorter string < longer string with "T" suffix).
|
|
*/
|
|
export function toEventSortKey(e: {
|
|
eventDate: string | null;
|
|
earliestGameTime: string | null;
|
|
}): string {
|
|
return e.earliestGameTime ?? e.eventDate ?? "9999-12-31";
|
|
}
|
|
|
|
export function localDateTimeToUtcIso(value: string | null | undefined): string | null {
|
|
if (!value) return null;
|
|
const date = new Date(value);
|
|
if (isNaN(date.getTime())) return null;
|
|
return date.toISOString();
|
|
}
|