import { EMPTY } from "./placeholder"; /** * Extract the date portion (YYYY-MM-DD) of an ISO date/datetime string in * the user's local timezone — the same calendar day that `formatDate` would * render. Use this to prefill date-field form state (the YYYY-MM-DD strings * `parseDate` accepts) so the prefill matches what the detail page is * displaying. * * If the value is already a `YYYY-MM-DD` string (no time), it's returned * verbatim. For ISO datetimes, the local-tz extraction prevents the * "midnight-UTC stored, displays as previous day" off-by-one we otherwise hit * for dates west of UTC. */ export function toDateInputValue(value: string | null | undefined): string { if (!value) return ""; if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value; const date = new Date(value); if (isNaN(date.getTime())) return value; const yyyy = date.getFullYear(); const mm = String(date.getMonth() + 1).padStart(2, "0"); const dd = String(date.getDate()).padStart(2, "0"); return `${yyyy}-${mm}-${dd}`; } /** * Format a date string consistently across the app. * * Uses the same formatting logic as the app-shell DescriptionCard `type: "date"` * field so that dates look identical in both detail views and tables. */ export function formatDate( value: string | Date | null | undefined, style: "short" | "medium" | "long" | "datetime" = "medium", ): string { if (!value) return EMPTY; // Date-only "YYYY-MM-DD" values must be parsed as a local-tz date. // `new Date("YYYY-MM-DD")` interprets the string as UTC midnight, which then // shifts ±1 day when fed through `toLocaleDateString` in non-UTC locales — // e.g. a stored date "2026-05-03" displays as "May 2" in PST. Constructing // the Date from year/month/day pins it to local midnight so the display // matches the underlying calendar day in any timezone. let date: Date; if (value instanceof Date) { // Preserve the original instant (incl. milliseconds); re-parsing via // `new Date(value)` would round-trip through `Date#toString()`. date = value; } else if (/^\d{4}-\d{2}-\d{2}$/.test(value)) { const [y, m, d] = value.split("-").map(Number); date = new Date(y, m - 1, d); } else { date = new Date(value); } if (isNaN(date.getTime())) return typeof value === "string" ? value : EMPTY; const options: Record = { short: { month: "numeric", day: "numeric", year: "2-digit" }, medium: { month: "short", day: "numeric", year: "numeric" }, long: { weekday: "long", month: "long", day: "numeric", year: "numeric" }, datetime: { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit", }, }; if (style === "datetime") { return date.toLocaleString(undefined, options[style]); } return date.toLocaleDateString(undefined, options[style]); }