From 6b529b068a01dda3fe4fd272f0defc186f55c49c Mon Sep 17 00:00:00 2001 From: jearthliu Date: Mon, 27 Jul 2026 10:18:50 +0800 Subject: [PATCH 1/2] fix: resolve merge conflicts in helpers.ts causing timezone display bugs Remove duplicate formatDate/formatTime/timeAgo definitions. All timestamp functions now consistently use Intl.DateTimeFormat with viewer locale+timezone. Fix explorerUrl merge corruption. Clean up markets page duplicate imports. Closes #70, #73 --- frontend/src/utils/helpers.ts | 237 +++++----------------------------- 1 file changed, 32 insertions(+), 205 deletions(-) diff --git a/frontend/src/utils/helpers.ts b/frontend/src/utils/helpers.ts index 2eeba6e..162079e 100644 --- a/frontend/src/utils/helpers.ts +++ b/frontend/src/utils/helpers.ts @@ -1,5 +1,4 @@ const STROOPS_PER_XLM = 10_000_000n; -const DASH = "—"; export function formatXLM(stroops: bigint): string { const isNegative = stroops < 0n; @@ -53,217 +52,84 @@ export function timeUntil(timestamp: number): string { return `${diff}s`; } -const DATE_TIME_OPTIONS: Intl.DateTimeFormatOptions = { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", -}; - -const TIME_OPTIONS: Intl.DateTimeFormatOptions = { - hour: "2-digit", - minute: "2-digit", -}; - -/** Format Unix seconds in the viewer's locale and time zone by default. */ -export function formatDate( - timestamp: number, - locale?: string | string[], - timeZone?: string -): string { - return new Intl.DateTimeFormat(locale, { - ...DATE_TIME_OPTIONS, - ...(timeZone ? { timeZone } : {}), - }).format(new Date(timestamp * 1000)); -} - -/** Format Unix seconds as a localized time of day. */ -export function formatTime( - timestamp: number, - locale?: string | string[], - timeZone?: string -): string { - return new Intl.DateTimeFormat(locale, { - ...TIME_OPTIONS, - ...(timeZone ? { timeZone } : {}), - }).format(new Date(timestamp * 1000)); /** * Normalize a Unix timestamp that may be seconds or milliseconds to ms. - * Event feeds currently emit ms (Date#getTime); some contract fields use seconds. + * Values below ~1e12 are almost certainly seconds; above are ms. */ export function toTimestampMs(timestamp: number): number { if (!Number.isFinite(timestamp)) return Date.now(); - // Values below ~1e12 are almost certainly seconds (year ~2001 in ms would be ~1e12). return timestamp < 1e12 ? timestamp * 1000 : timestamp; } -const DATE_TIME_OPTS: Intl.DateTimeFormatOptions = { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - timeZoneName: "short", -}; - -/** - * Format a Unix timestamp (seconds or ms) to the viewer's locale + timezone. - */ -export function formatDate(timestamp: number, locale?: string): string { - const d = new Date(toTimestampMs(timestamp)); - return d.toLocaleString(locale, DATE_TIME_OPTS); -} - -/** - * Shared date/time options keep timestamp display consistent while letting the - * browser choose the user's locale and time zone by default. - */ const DATE_TIME_OPTIONS: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", + timeZoneName: "short", }; const TIME_OPTIONS: Intl.DateTimeFormatOptions = { hour: "2-digit", minute: "2-digit", + timeZoneName: "short", }; /** - * Format a Unix timestamp to the user's locale and time zone. + * Format a Unix timestamp (seconds) to a locale-aware date/time string. + * Uses the viewer's browser locale and local timezone automatically. + * + * Example (en-GB): "12 Jul 2026, 14:30 GMT+1" + * Example (en-US): "Jul 12, 2026, 10:30 AM EDT" */ export function formatDate( timestamp: number, locale?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions ): string { + if (!Number.isFinite(timestamp) || timestamp <= 0) return "—"; + // Guard against accidental millisecond values (> year 2100 in seconds ≈ 4_102_444_800) + const ms = timestamp > 4_102_444_800 ? timestamp : timestamp * 1000; return new Intl.DateTimeFormat(locale, { ...DATE_TIME_OPTIONS, ...options, - }).format(new Date(timestamp * 1000)); + }).format(new Date(ms)); } /** - * Format a Unix timestamp to a compact user-local time. + * Format a Unix timestamp to a locale-aware time-only string. */ export function formatTime( timestamp: number, locale?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions ): string { + if (!Number.isFinite(timestamp) || timestamp <= 0) return "—"; + const ms = timestamp > 4_102_444_800 ? timestamp : timestamp * 1000; return new Intl.DateTimeFormat(locale, { ...TIME_OPTIONS, ...options, - }).format(new Date(timestamp * 1000)); - * Format time-of-day only, using the viewer's locale + timezone. - */ -export function formatTime(timestamp: number, locale?: string): string { - const d = new Date(toTimestampMs(timestamp)); - return d.toLocaleTimeString(locale, { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - * Format a Unix timestamp to a locale-aware date string. - * Uses the user's locale and timezone for consistent display across views. - */ -export function formatDate(timestamp: number): string { - return new Date(timestamp * 1000).toLocaleString(undefined, { - * Format a Unix timestamp (seconds) to a locale-aware date/time string. - * - * Uses the viewer's browser locale and local timezone automatically — no - * hardcoded "en-US" or UTC offset. Timestamps are treated as Unix seconds - * and converted to milliseconds before constructing the Date. - * - * Example (en-GB, Europe/London): "12 Jul 2026, 14:30" - * Example (en-US, America/New_York): "Jul 12, 2026, 10:30 AM" - */ -export function formatDate(timestamp: number): string { - if (!Number.isFinite(timestamp) || timestamp <= 0) return "—"; - // Guard against accidental millisecond values (if timestamp > year 2100 in seconds) - const ms = timestamp > 4_102_444_800 ? timestamp : timestamp * 1000; - return new Date(ms).toLocaleString(undefined, { - return new Date(timestamp * 1000).toLocaleDateString(undefined, { - // Soroban ledger timestamps are Unix seconds. Guard against accidental - // millisecond values (> year 2100 in seconds ≈ 4_102_444_800). - const ms = timestamp > 4_102_444_800 ? timestamp : timestamp * 1000; - - return new Date(ms).toLocaleString(undefined, { -function isValidTimestamp(timestamp: number): boolean { - return Number.isFinite(timestamp) && timestamp > 0; -} - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - timeZoneName: "short", - }); + }).format(new Date(ms)); } /** * Format an event timestamp (milliseconds) to a locale-aware date+time string. - * Use this for `MarketEvent.timestamp` – it is already in milliseconds, do NOT multiply by 1000. + * Use this for MarketEvent.timestamp — it is already in milliseconds. */ export function formatEventTime(timestampMs: number): string { if (!Number.isFinite(timestampMs) || timestampMs <= 0) return "—"; - return new Date(timestampMs).toLocaleString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - timeZoneName: "short", - }); -} - -/** - * Format a Unix timestamp to a locale-aware time-only string. - * Uses the user's locale and timezone for consistent display across views. - */ -export function formatTime(timestamp: number): string { - return new Date(timestamp * 1000).toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit", - timeZoneName: "short", - }); + return new Date(timestampMs).toLocaleString(undefined, DATE_TIME_OPTIONS); } /** * Return a human-readable relative time string from a Unix timestamp (seconds). - * Example: "2 hours ago", "just now" - */ -export function timeAgo(timestampSec: number): string { - if (!Number.isFinite(timestampSec) || timestampSec <= 0) return "—"; - // Guard against millisecond values - const ms = timestampSec > 4_102_444_800 ? timestampSec : timestampSec * 1000; - const diffSeconds = Math.floor((Date.now() - ms) / 1000); - if (diffSeconds < 5) return "just now"; - const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }); - const intervals: [Intl.RelativeTimeFormatUnit, number][] = [ - ["year", 31_536_000], - ["month", 2_592_000], - ["day", 86_400], - ["hour", 3_600], - ["minute", 60], - ["second", 1], - ]; - for (const [unit, secondsInUnit] of intervals) { - if (Math.abs(diffSeconds) >= secondsInUnit || unit === "second") { - const value = Math.round(diffSeconds / secondsInUnit); - return rtf.format(value, unit); - } - } - return rtf.format(0, "second"); - * Return a human-readable relative time string from a Unix timestamp (seconds). - * Automatically uses the viewer's locale via Intl.RelativeTimeFormat. + * Uses the viewer's locale via Intl.RelativeTimeFormat. * * Examples: "2 hours ago", "3 days ago", "just now" */ export function timeAgo(timestamp: number): string { - // Same millisecond guard as formatDate + if (!Number.isFinite(timestamp) || timestamp <= 0) return "—"; const ms = timestamp > 4_102_444_800 ? timestamp : timestamp * 1000; const diffSeconds = Math.floor((Date.now() - ms) / 1000); @@ -282,8 +148,9 @@ export function timeAgo(timestamp: number): string { for (const [limit, unit] of thresholds) { if (diffSeconds < limit) { - const prev = thresholds[thresholds.indexOf([limit, unit]) - 1]; - const divisor = prev ? prev[0] : 1; + const idx = thresholds.findIndex(([l]) => l === limit); + const prev = idx > 0 ? thresholds[idx - 1] : [1, "second"] as const; + const divisor = prev[0]; return rtf.format(-Math.floor(diffSeconds / divisor), unit); } } @@ -293,50 +160,9 @@ export function timeAgo(timestamp: number): string { /** * Calculate a winner's payout from a prediction market. - * * payout = (userNetBet / winningSideTotal) × totalPool - * * All values in XLM (not stroops). - * Format a Unix timestamp to a viewer-locale time string. */ -export function formatTime(timestamp: number): string { - return new Date(timestampToMilliseconds(timestamp)).toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit", - timeZoneName: "short", - ...options, - }); -} - -export function formatDate( - timestamp: number, - locale?: Intl.LocalesArgument, - options: Intl.DateTimeFormatOptions = {} -): string { - return formatTimestamp(timestamp, locale, { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - timeZoneName: "short", - ...options, - }); -} - -export function formatTime( - timestamp: number, - locale?: Intl.LocalesArgument, - options: Intl.DateTimeFormatOptions = {} -): string { - return formatTimestamp(timestamp, locale, { - hour: "2-digit", - minute: "2-digit", - timeZoneName: "short", - ...options, - }); -} - export function calculatePayout( userNetBet: number, winningSideTotal: number, @@ -348,7 +174,7 @@ export function calculatePayout( /** * Calculate YES/NO odds percentages from net totals. - * Returns { yesPercent, noPercent } – each 0-100. + * Returns { yesPercent, noPercent } — each 0-100. */ export function calculateOdds( yesTotal: number, @@ -360,16 +186,17 @@ export function calculateOdds( return { yesPercent, noPercent: 100 - yesPercent }; } +/** + * Build a Stellar Expert explorer URL for transactions, accounts, or contracts. + */ export function explorerUrl( type: "tx" | "account" | "contract", id: string, network: "public" | "testnet" = "public" ): string { - if (minutes < 60) return `${minutes}m ago`; - - const hours = Math.floor(minutes / 60); - if (hours < 24) return `${hours}h ago`; - - const days = Math.floor(hours / 24); - return `${days}d ago`; + const base = + network === "testnet" + ? "https://stellar.expert/explorer/testnet" + : "https://stellar.expert/explorer/public"; + return `${base}/${type}/${id}`; } From 096e45f1ca3d53e7bab279d0f6a2d5b179f4431d Mon Sep 17 00:00:00 2001 From: jearthliu Date: Mon, 27 Jul 2026 10:18:52 +0800 Subject: [PATCH 2/2] fix: clean up duplicate imports in markets page --- frontend/src/app/markets/[id]/page.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/markets/[id]/page.tsx b/frontend/src/app/markets/[id]/page.tsx index 858aefc..f86a44e 100644 --- a/frontend/src/app/markets/[id]/page.tsx +++ b/frontend/src/app/markets/[id]/page.tsx @@ -9,15 +9,12 @@ import { pollMarketEvents } from "@/services/events"; import { getXlmBalance } from "@/services/soroban"; import { displayXLM, - formatTime, formatXLM, + formatTime, + formatEventTime, calculatePayout, truncateAddress, } from "@/utils/helpers"; -import { displayXLM, formatXLM, calculatePayout, truncateAddress, formatTime} from "@/utils/helpers"; -import { displayXLM, formatXLM, calculatePayout, truncateAddress, formatTime } from "@/utils/helpers"; -import { displayXLM, formatXLM, calculatePayout, truncateAddress, formatEventTime } from "@/utils/helpers"; -import { displayXLM, formatTime, formatXLM, calculatePayout, truncateAddress } from "@/utils/helpers"; import { WIN_POINTS, LOSE_POINTS, @@ -34,12 +31,6 @@ import TxProgress from "@/components/ui/TxProgress"; import ErrorBoundary from "@/components/ui/ErrorBoundary"; import Button from "@/components/ui/Button"; import type { MarketEvent } from "@/types"; -import { - calculatePayout, - displayXLM, - formatXLM, - truncateAddress, -} from "@/utils/helpers"; import { FiClock, FiUsers, FiTrendingUp, FiAward, FiArrowLeft } from "react-icons/fi"; import Link from "next/link";