diff --git a/csv-table-hub-main/src/__tests__/local-time.test.ts b/csv-table-hub-main/src/__tests__/local-time.test.ts new file mode 100644 index 0000000..e529201 --- /dev/null +++ b/csv-table-hub-main/src/__tests__/local-time.test.ts @@ -0,0 +1,30 @@ +// src/__tests__/local-time.test.ts — upload times in the viewer's time zone (#119). +import { describe, it, expect } from "vitest"; +import { formatLocalTime, formatUtc } from "@/lib/utils"; + +describe("formatLocalTime", () => { + it("shows the #119 upload in Sydney time, not UTC", () => { + // Uploaded 27 Oct 2026 19:33 AEDT, stored as 08:33 UTC. + expect(formatLocalTime("2026-10-27T08:33:00Z", "Australia/Sydney")).toBe("27 Oct 2026, 19:33"); + }); + + it("crosses midnight correctly", () => { + expect(formatLocalTime("2026-10-26T15:16:00Z", "Australia/Sydney")).toBe("27 Oct 2026, 02:16"); + }); + + it("uses each viewer's own zone", () => { + expect(formatLocalTime("2026-10-27T08:33:00Z", "Europe/London")).toBe("27 Oct 2026, 08:33"); + }); + + it("accepts epoch milliseconds (import history)", () => { + expect(formatLocalTime(Date.UTC(2026, 9, 27, 8, 33), "Australia/Sydney")).toBe( + "27 Oct 2026, 19:33", + ); + }); +}); + +describe("formatUtc (tooltip)", () => { + it("keeps the exact UTC time for audit", () => { + expect(formatUtc("2026-10-27T08:33:05.123Z")).toBe("2026-10-27 08:33:05 UTC"); + }); +}); diff --git a/csv-table-hub-main/src/lib/utils.ts b/csv-table-hub-main/src/lib/utils.ts index a5ef193..cffef86 100644 --- a/csv-table-hub-main/src/lib/utils.ts +++ b/csv-table-hub-main/src/lib/utils.ts @@ -4,3 +4,26 @@ import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +/** "27 Oct 2026, 19:33" in the viewer's time zone (#119). Pass timeZone for tests. */ +export function formatLocalTime(iso: string | number, timeZone?: string): string { + const p = Object.fromEntries( + new Intl.DateTimeFormat("en-GB", { + day: "2-digit", + month: "short", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + hourCycle: "h23", + timeZone, + }) + .formatToParts(new Date(iso)) + .map((x) => [x.type, x.value]), + ); + return `${p.day} ${p.month} ${p.year}, ${p.hour}:${p.minute}`; +} + +/** "2026-10-27 08:33:05 UTC", kept as the tooltip for audit (#119). */ +export function formatUtc(iso: string | number): string { + return `${new Date(iso).toISOString().replace("T", " ").slice(0, 19)} UTC`; +} diff --git a/csv-table-hub-main/src/routes/_authenticated/index.tsx b/csv-table-hub-main/src/routes/_authenticated/index.tsx index b5bad0f..cfc995a 100644 --- a/csv-table-hub-main/src/routes/_authenticated/index.tsx +++ b/csv-table-hub-main/src/routes/_authenticated/index.tsx @@ -34,6 +34,7 @@ import { } from "@/lib/mapping-templates"; import { downloadErrorReport, buildErrorRows, type ExportFormat } from "@/lib/export"; import type { Job, JobStatus } from "@/lib/job-types"; +import { formatLocalTime, formatUtc } from "@/lib/utils"; import { classifyUploadResult, classifyThrownError, buildCancelPatch, buildCancelToastMessage, decideOverwriteAction, failedRowsNote, @@ -690,7 +691,7 @@ function FilesList({files}:{files:CsvFileSummary[]}){ <>