From a6b13be1cb3b615859cc89a36c2030ca741b892a Mon Sep 17 00:00:00 2001 From: Nadeem Kamel Date: Fri, 10 Jul 2026 13:57:33 -0700 Subject: [PATCH] Fix locale-aware timestamp formatting --- frontend/package.json | 35 ++++++++++++++++++ .../src/__tests__/components/Navbar.test.tsx | 4 +-- frontend/src/__tests__/helpers.test.ts | 36 ++++++++++++++++++- frontend/src/app/layout.tsx | 1 + frontend/src/app/markets/[id]/page.tsx | 10 ++++-- frontend/src/utils/helpers.ts | 32 ++++++++++++++--- 6 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 frontend/package.json diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..0b4a502 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,35 @@ +{ + "name": "stellarpulse-frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "test": "vitest run", + "lint": "next lint" + }, + "dependencies": { + "@creit.tech/stellar-wallets-kit": "^1.2.0", + "@stellar/stellar-sdk": "^14.5.0", + "buffer": "^6.0.3", + "next": "^14.2.29", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-icons": "^5.4.0" + }, + "devDependencies": { + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@types/node": "^22.13.5", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "autoprefixer": "^10.4.20", + "jsdom": "^25.0.1", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3", + "vitest": "^2.1.8" + } +} diff --git a/frontend/src/__tests__/components/Navbar.test.tsx b/frontend/src/__tests__/components/Navbar.test.tsx index 222a0ec..69d7287 100644 --- a/frontend/src/__tests__/components/Navbar.test.tsx +++ b/frontend/src/__tests__/components/Navbar.test.tsx @@ -70,7 +70,7 @@ describe("Navbar", () => { it("renders the logo / brand name", () => { render(); - expect(screen.getByText(/StellarPulse/i)).toBeInTheDocument(); + expect(screen.getByText(/Stellar Pulse/i)).toBeInTheDocument(); }); it("renders all navigation links", () => { @@ -87,7 +87,7 @@ describe("Navbar", () => { it("logo links to home page", () => { render(); - const logo = screen.getByText(/StellarPulse/i); + const logo = screen.getByText(/Stellar Pulse/i); expect(logo.closest("a")).toHaveAttribute("href", "/"); }); diff --git a/frontend/src/__tests__/helpers.test.ts b/frontend/src/__tests__/helpers.test.ts index c039046..7c8e243 100644 --- a/frontend/src/__tests__/helpers.test.ts +++ b/frontend/src/__tests__/helpers.test.ts @@ -4,6 +4,8 @@ import { truncateAddress, isValidAmount, timeUntil, + formatDate, + formatTimestamp, calculatePayout, calculateOdds, bpsToPercent, @@ -128,7 +130,7 @@ describe("isValidAmount", () => { describe("timeUntil", () => { beforeEach(() => { vi.useFakeTimers(); - // Pin "now" to a known Unix time: 2026-02-26T00:00:00Z = 1771977600 + // Pin "now" to a known Unix time: 2026-02-26T00:00:00Z = 1772064000 vi.setSystemTime(new Date("2026-02-26T00:00:00Z")); }); @@ -171,6 +173,38 @@ describe("timeUntil", () => { }); }); +// ── timestamp formatting ───────────────────────────────────────────────────── + +describe("formatTimestamp", () => { + it("formats a Unix timestamp with date, time, and timezone", () => { + expect( + formatTimestamp(1772064000, { + locale: "en-US", + timeZone: "UTC", + timeZoneName: "short", + }) + ).toBe("Feb 26, 2026, 12:00 AM UTC"); + }); + + it("uses the requested timezone for display", () => { + expect( + formatTimestamp(1772064000, { + locale: "en-US", + timeZone: "America/Los_Angeles", + timeZoneName: "short", + }) + ).toBe("Feb 25, 2026, 04:00 PM PST"); + }); + + it("keeps formatDate on the same formatter", () => { + expect(formatDate(1772064000)).toBe(formatTimestamp(1772064000)); + }); + + it("handles invalid timestamps", () => { + expect(formatTimestamp(Number.NaN)).toBe("Invalid date"); + }); +}); + // ── calculatePayout ─────────────────────────────────────────────────────────── describe("calculatePayout", () => { diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 13a914a..06116fa 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -13,6 +13,7 @@ const inter = Inter({ const poppins = Poppins({ subsets: ["latin"], + weight: ["400", "500", "600", "700"], variable: "--font-heading", display: "swap", }); diff --git a/frontend/src/app/markets/[id]/page.tsx b/frontend/src/app/markets/[id]/page.tsx index 5ae3a63..917cd83 100644 --- a/frontend/src/app/markets/[id]/page.tsx +++ b/frontend/src/app/markets/[id]/page.tsx @@ -7,7 +7,13 @@ import { useWallet } from "@/hooks/useWallet"; import { useToken } from "@/hooks/useToken"; import { pollMarketEvents } from "@/services/events"; import { getXlmBalance } from "@/services/soroban"; -import { displayXLM, formatXLM, calculatePayout, truncateAddress } from "@/utils/helpers"; +import { + displayXLM, + formatXLM, + calculatePayout, + truncateAddress, + formatTimestamp, +} from "@/utils/helpers"; import { WIN_POINTS, LOSE_POINTS, @@ -319,7 +325,7 @@ export default function MarketDetailPage({ - {new Date(evt.timestamp * 1000).toLocaleTimeString()} + {formatTimestamp(evt.timestamp)} ))} diff --git a/frontend/src/utils/helpers.ts b/frontend/src/utils/helpers.ts index 4688130..e2ebb45 100644 --- a/frontend/src/utils/helpers.ts +++ b/frontend/src/utils/helpers.ts @@ -75,16 +75,40 @@ export function timeUntil(timestamp: number): string { } /** - * Format a Unix timestamp to a locale-aware date string. + * Format a Unix timestamp to a locale-aware date/time string. + * + * Uses the browser/user locale and timezone by default, while accepting an + * override for deterministic tests and places that need a specific zone. */ -export function formatDate(timestamp: number): string { - return new Date(timestamp * 1000).toLocaleDateString("en-US", { +export function formatTimestamp( + timestamp: number, + options: { + locale?: string | string[]; + timeZone?: string; + timeZoneName?: "short" | "long" | "shortOffset" | "longOffset" | "shortGeneric" | "longGeneric"; + } = {} +): string { + const date = new Date(timestamp * 1000); + if (!Number.isFinite(timestamp) || Number.isNaN(date.getTime())) { + return "Invalid date"; + } + + return new Intl.DateTimeFormat(options.locale, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", - }); + timeZone: options.timeZone, + timeZoneName: options.timeZoneName ?? "short", + }).format(date); +} + +/** + * Format a Unix timestamp to a locale-aware date/time string. + */ +export function formatDate(timestamp: number): string { + return formatTimestamp(timestamp); } /**