diff --git a/apps/webapp/app/components/primitives/DateTime.tsx b/apps/webapp/app/components/primitives/DateTime.tsx index 11a7ab990e..7d75bc7351 100644 --- a/apps/webapp/app/components/primitives/DateTime.tsx +++ b/apps/webapp/app/components/primitives/DateTime.tsx @@ -98,7 +98,45 @@ export function formatDateTime( } export function formatDateTimeISO(date: Date, timeZone: string): string { - return new Date(date.toLocaleString("en-US", { timeZone })).toISOString(); + // Special handling for UTC + if (timeZone === "UTC") { + return date.toISOString(); + } + + // Get the date parts in the target timezone + const dateFormatter = new Intl.DateTimeFormat("en-US", { + timeZone, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }); + + // Get the timezone offset for this specific date + const timeZoneFormatter = new Intl.DateTimeFormat("en-US", { + timeZone, + timeZoneName: "longOffset", + }); + + const dateParts = Object.fromEntries( + dateFormatter.formatToParts(date).map(({ type, value }) => [type, value]) + ); + + const timeZoneParts = timeZoneFormatter.formatToParts(date); + const offset = + timeZoneParts.find((part) => part.type === "timeZoneName")?.value.replace("GMT", "") || + "+00:00"; + + // Format: YYYY-MM-DDThh:mm:ss.sss±hh:mm + return ( + `${dateParts.year}-${dateParts.month}-${dateParts.day}T` + + `${dateParts.hour}:${dateParts.minute}:${dateParts.second}.${String( + date.getMilliseconds() + ).padStart(3, "0")}${offset}` + ); } // New component that only shows date when it changes diff --git a/apps/webapp/test/components/DateTime.test.ts b/apps/webapp/test/components/DateTime.test.ts new file mode 100644 index 0000000000..103f416eb9 --- /dev/null +++ b/apps/webapp/test/components/DateTime.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from "vitest"; +import { formatDateTimeISO } from "~/components/primitives/DateTime"; + +describe("formatDateTimeISO", () => { + it("should format UTC dates with Z suffix", () => { + const date = new Date("2025-04-29T14:01:19.000Z"); + const result = formatDateTimeISO(date, "UTC"); + expect(result).toBe("2025-04-29T14:01:19.000Z"); + }); + + describe("British Time (Europe/London)", () => { + it("should format with +01:00 during BST (summer)", () => { + // BST - British Summer Time (last Sunday in March to last Sunday in October) + const summerDate = new Date("2025-07-15T14:01:19.000Z"); + const result = formatDateTimeISO(summerDate, "Europe/London"); + expect(result).toBe("2025-07-15T15:01:19.000+01:00"); + }); + + it("should format with +00:00 during GMT (winter)", () => { + // GMT - Greenwich Mean Time (winter) + const winterDate = new Date("2025-01-15T14:01:19.000Z"); + const result = formatDateTimeISO(winterDate, "Europe/London"); + expect(result).toBe("2025-01-15T14:01:19.000+00:00"); + }); + }); + + describe("US Pacific Time (America/Los_Angeles)", () => { + it("should format with -07:00 during PDT (summer)", () => { + // PDT - Pacific Daylight Time (second Sunday in March to first Sunday in November) + const summerDate = new Date("2025-07-15T14:01:19.000Z"); + const result = formatDateTimeISO(summerDate, "America/Los_Angeles"); + expect(result).toBe("2025-07-15T07:01:19.000-07:00"); + }); + + it("should format with -08:00 during PST (winter)", () => { + // PST - Pacific Standard Time (winter) + const winterDate = new Date("2025-01-15T14:01:19.000Z"); + const result = formatDateTimeISO(winterDate, "America/Los_Angeles"); + expect(result).toBe("2025-01-15T06:01:19.000-08:00"); + }); + }); + + it("should preserve milliseconds", () => { + const date = new Date("2025-04-29T14:01:19.123Z"); + const result = formatDateTimeISO(date, "UTC"); + expect(result).toBe("2025-04-29T14:01:19.123Z"); + }); + + it("should preserve milliseconds, not UTC", () => { + const date = new Date("2025-04-29T14:01:19.123Z"); + const result = formatDateTimeISO(date, "Europe/London"); + expect(result).toBe("2025-04-29T15:01:19.123+01:00"); + }); +});