brackt/app/lib/date-utils.ts

87 lines
3.5 KiB
TypeScript
Raw Permalink Normal View History

Fix datetime-local to UTC conversion for game scheduling (#138) * fix: use hidden input for UTC date value in add-game form The onSubmit handler was setting the datetime-local input's value to an ISO UTC string (e.g. "2026-03-11T17:00:00.000Z"). datetime-local inputs only accept the format "YYYY-MM-DDTHH:MM" — values with a 'Z' timezone suffix are invalid and the browser silently clears the field to "". This caused the form to submit an empty scheduledAt, which the server treated as null, so games were always saved without a date. Fix by keeping the datetime-local input for user interaction (renamed scheduledAtLocal, not submitted) and writing the converted UTC ISO string into a separate hidden input named scheduledAt in the onSubmit handler. Also adds app/lib/date-utils.ts with a localDateTimeToUtcIso helper and matching tests to document and verify the datetime-local ↔ ISO conversion behaviour. https://claude.ai/code/session_0148fgZiXpyvGX8ZX3BFRGCs * refactor: use localDateTimeToUtcIso in component and clean up tests - Import and call localDateTimeToUtcIso in the add-game onSubmit handler instead of duplicating the conversion inline; also applies the null/ invalid-date safety from the utility to the component - Remove the redundant "incompatible with datetime-local" test case (its intent is better expressed as a code comment than a test assertion) - Move the datetime-local incompatibility explanation into a NOTE comment on the utility function's JSDoc https://claude.ai/code/session_0148fgZiXpyvGX8ZX3BFRGCs --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 16:22:29 -07:00
/**
* 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 } from "date-fns";
2026-03-12 10:12:38 -07:00
const pad = (n: number) => String(n).padStart(2, "0");
2026-03-12 10:12:38 -07:00
/**
* 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 {
const [year, month, day] = dateStr.split("-").map(Number);
return format(new Date(year, month - 1, day), "MMM d");
2026-03-12 10:12:38 -07:00
} 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 (auto racing, 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";
}
Fix datetime-local to UTC conversion for game scheduling (#138) * fix: use hidden input for UTC date value in add-game form The onSubmit handler was setting the datetime-local input's value to an ISO UTC string (e.g. "2026-03-11T17:00:00.000Z"). datetime-local inputs only accept the format "YYYY-MM-DDTHH:MM" — values with a 'Z' timezone suffix are invalid and the browser silently clears the field to "". This caused the form to submit an empty scheduledAt, which the server treated as null, so games were always saved without a date. Fix by keeping the datetime-local input for user interaction (renamed scheduledAtLocal, not submitted) and writing the converted UTC ISO string into a separate hidden input named scheduledAt in the onSubmit handler. Also adds app/lib/date-utils.ts with a localDateTimeToUtcIso helper and matching tests to document and verify the datetime-local ↔ ISO conversion behaviour. https://claude.ai/code/session_0148fgZiXpyvGX8ZX3BFRGCs * refactor: use localDateTimeToUtcIso in component and clean up tests - Import and call localDateTimeToUtcIso in the add-game onSubmit handler instead of duplicating the conversion inline; also applies the null/ invalid-date safety from the utility to the component - Remove the redundant "incompatible with datetime-local" test case (its intent is better expressed as a code comment than a test assertion) - Move the datetime-local incompatibility explanation into a NOTE comment on the utility function's JSDoc https://claude.ai/code/session_0148fgZiXpyvGX8ZX3BFRGCs --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 16:22:29 -07:00
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();
}
/**
* Converts a stored UTC value (ISO string or Date) to the "YYYY-MM-DDTHH:MM"
* string a `datetime-local` input expects, expressed in the **browser's local**
* timezone. This is the inverse of `localDateTimeToUtcIso`.
*
* Built from local date getters (not `toISOString`, which is UTC) so the input
* shows the viewer's wall-clock time. Because it depends on the runtime
* timezone, call it on the client (e.g. inside `useEffect`) to avoid SSR
* hydration mismatches.
*
* Returns "" for nullish or unparseable input so it can be assigned directly to
* an input value.
*/
export function utcIsoToLocalDateTime(value: string | Date | null | undefined): string {
if (!value) return "";
const date = value instanceof Date ? value : new Date(value);
if (isNaN(date.getTime())) return "";
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const hours = pad(date.getHours());
const minutes = pad(date.getMinutes());
return `${year}-${month}-${day}T${hours}:${minutes}`;
}