Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions csv-table-hub-main/src/__tests__/local-time.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
23 changes: 23 additions & 0 deletions csv-table-hub-main/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
}
3 changes: 2 additions & 1 deletion csv-table-hub-main/src/routes/_authenticated/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -690,7 +691,7 @@ function FilesList({files}:{files:CsvFileSummary[]}){
<>
<div className="overflow-x-auto"><Table>
<TableHeader><TableRow className="[&>th]:h-8 [&>th]:py-1"><TableHead className="w-12 text-right">#</TableHead><TableHead>File</TableHead><TableHead>Table</TableHead><TableHead className="w-16 text-right">Cols</TableHead><TableHead className="w-16 text-right">Rows</TableHead><TableHead className="w-40">Uploaded</TableHead><TableHead className="w-24"/></TableRow></TableHeader>
<TableBody>{visible.map((f,i)=><TableRow key={f.id} className="[&>td]:py-1.5"><TableCell className="text-right tabular-nums text-xs text-muted-foreground">{startIndex+i+1}</TableCell><TableCell className="max-w-[22rem] truncate font-medium" title={f.file_name}>{f.file_name}</TableCell><TableCell><code className="rounded bg-muted px-1.5 py-0.5 text-xs">{f.table_name}</code></TableCell><TableCell className="text-right tabular-nums">{f.column_names.length}</TableCell><TableCell className="text-right tabular-nums">{f.row_count}{failedRowsNote(f)&&<span className="block text-xs text-destructive">{failedRowsNote(f)}</span>}</TableCell><TableCell className="whitespace-nowrap text-xs text-muted-foreground" suppressHydrationWarning><time dateTime={f.created_at} title={new Date(f.created_at).toISOString()}>{new Date(f.created_at).toISOString().replace("T"," ").slice(0,16)}</time></TableCell><TableCell className="text-right"><Button size="sm" variant="ghost" className="h-7 px-2" onClick={()=>setPreview(f)}><Eye className="mr-1 h-3.5 w-3.5"/>Preview</Button></TableCell></TableRow>)}</TableBody>
<TableBody>{visible.map((f,i)=><TableRow key={f.id} className="[&>td]:py-1.5"><TableCell className="text-right tabular-nums text-xs text-muted-foreground">{startIndex+i+1}</TableCell><TableCell className="max-w-[22rem] truncate font-medium" title={f.file_name}>{f.file_name}</TableCell><TableCell><code className="rounded bg-muted px-1.5 py-0.5 text-xs">{f.table_name}</code></TableCell><TableCell className="text-right tabular-nums">{f.column_names.length}</TableCell><TableCell className="text-right tabular-nums">{f.row_count}{failedRowsNote(f)&&<span className="block text-xs text-destructive">{failedRowsNote(f)}</span>}</TableCell><TableCell className="whitespace-nowrap text-xs text-muted-foreground" suppressHydrationWarning><time dateTime={f.created_at} title={formatUtc(f.created_at)}>{formatLocalTime(f.created_at)}</time></TableCell><TableCell className="text-right"><Button size="sm" variant="ghost" className="h-7 px-2" onClick={()=>setPreview(f)}><Eye className="mr-1 h-3.5 w-3.5"/>Preview</Button></TableCell></TableRow>)}</TableBody>
</Table></div>

<div className="mt-4 flex flex-wrap items-center justify-between gap-3 border-t pt-3">
Expand Down
3 changes: 2 additions & 1 deletion csv-table-hub-main/src/routes/audit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { useCallback, useMemo, useState } from "react";
import { AUDIT_LOG } from "@/lib/audit-log";
import { formatLocalTime, formatUtc } from "@/lib/utils";
import { useLocalStorageState } from "@/hooks/use-local-storage";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
Expand Down Expand Up @@ -171,7 +172,7 @@ function AuditPage() {
<TableCell className="text-right tabular-nums text-xs">
{job.totalRows!=null ? <div className="flex flex-col items-end gap-0.5"><span>{job.totalRows} total</span>{job.insertedRows!=null&&<span className="text-emerald-600 dark:text-emerald-400">{job.insertedRows} ok</span>}{(job.failedRows??0)>0&&<span className="text-destructive">{job.failedRows} failed</span>}</div> : <span className="text-muted-foreground">—</span>}
</TableCell>
<TableCell className="text-xs text-muted-foreground" suppressHydrationWarning>{new Date(job.createdAt).toISOString().replace("T"," ").slice(0,19)} UTC</TableCell>
<TableCell className="text-xs text-muted-foreground" suppressHydrationWarning><time dateTime={new Date(job.createdAt).toISOString()} title={formatUtc(job.createdAt)}>{formatLocalTime(job.createdAt)}</time></TableCell>
<TableCell className="max-w-[160px] text-xs text-muted-foreground">{mappingSummary(job.headerMapping)||<span className="text-muted-foreground/40">—</span>}</TableCell>
<TableCell>
<div className="flex items-center justify-end gap-1">
Expand Down
Loading