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
This commit is contained in:
Claude 2026-03-11 23:19:25 +00:00
parent 1ef37fa8db
commit 4288f14a79
No known key found for this signature in database
3 changed files with 9 additions and 15 deletions

View file

@ -41,15 +41,4 @@ describe("localDateTimeToUtcIso", () => {
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
}); });
it("the returned ISO string is incompatible with datetime-local input format", () => {
// This documents WHY we need a hidden input instead of setting the
// datetime-local input's value directly. datetime-local only accepts
// "YYYY-MM-DDTHH:MM" — a 'Z' suffix makes the value invalid and the
// browser silently clears the field.
const result = localDateTimeToUtcIso("2026-03-11T10:00");
expect(result).not.toBeNull();
expect(result).toContain("Z");
// The value does NOT match the datetime-local format (no Z allowed)
expect(result).not.toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/);
});
}); });

View file

@ -9,6 +9,12 @@
* to hold the converted UTC value before form submission. * to hold the converted UTC value before form submission.
* *
* Returns null when value is empty, nullish, or not a valid date. * 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.
*/ */
export function localDateTimeToUtcIso(value: string | null | undefined): string | null { export function localDateTimeToUtcIso(value: string | null | undefined): string | null {
if (!value) return null; if (!value) return null;

View file

@ -1,5 +1,6 @@
import { Form, Link } from "react-router"; import { Form, Link } from "react-router";
import { Fragment, useState, useEffect, useMemo } from "react"; import { Fragment, useState, useEffect, useMemo } from "react";
import { localDateTimeToUtcIso } from "~/lib/date-utils";
import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.bracket"; import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.bracket";
import { loader, action } from "./admin.sports-seasons.$id.events.$eventId.bracket.server"; import { loader, action } from "./admin.sports-seasons.$id.events.$eventId.bracket.server";
@ -732,10 +733,8 @@ export default function EventBracket({
const scheduledAtLocal = form.elements.namedItem("scheduledAtLocal") as HTMLInputElement; const scheduledAtLocal = form.elements.namedItem("scheduledAtLocal") as HTMLInputElement;
// Write the UTC ISO string into the hidden input that IS submitted // Write the UTC ISO string into the hidden input that IS submitted
const scheduledAtHidden = form.elements.namedItem("scheduledAt") as HTMLInputElement; const scheduledAtHidden = form.elements.namedItem("scheduledAt") as HTMLInputElement;
if (scheduledAtLocal?.value && scheduledAtHidden) { if (scheduledAtHidden) {
// datetime-local inputs reject ISO strings with a 'Z' suffix and silently scheduledAtHidden.value = localDateTimeToUtcIso(scheduledAtLocal?.value) ?? "";
// clear their value, so we use a separate hidden input for the UTC value.
scheduledAtHidden.value = new Date(scheduledAtLocal.value).toISOString();
} }
}}> }}>
<input type="hidden" name="intent" value="add-game" /> <input type="hidden" name="intent" value="add-game" />