diff --git a/frontend/src/utils/helpers.ts b/frontend/src/utils/helpers.ts index 3f28ea7..6a26100 100644 --- a/frontend/src/utils/helpers.ts +++ b/frontend/src/utils/helpers.ts @@ -13,305 +13,109 @@ export function formatXLM(stroops: bigint): string { const fractional = abs % STROOPS_PER_XLM; const fracStr = fractional.toString().padStart(7, "0").replace(/0+$/, ""); const sign = isNegative ? "-" : ""; - - return fracStr.length === 0 - ? `${sign}${whole} XLM` - : `${sign}${whole}.${fracStr} XLM`; + return fracStr.length === 0 ? sign+whole+" XLM" : sign+whole+"."+fracStr+" XLM"; } -/** Format a number that is already expressed in XLM. */ export function displayXLM(xlm: number): string { if (xlm === 0) return "0 XLM"; - const formatted = xlm.toFixed(2).replace(/\.?0+$/, ""); - return `${formatted} XLM`; + const formatted = xlm.toFixed(2).replace(/.?0+$/, ""); + return formatted+" XLM"; } -/** Truncate a Stellar address for display. */ export function truncateAddress(addr: string): string { if (!addr || addr.length <= 10) return addr; - return `${addr.slice(0, 4)}...${addr.slice(-4)}`; + return addr.slice(0, 4)+"..."+addr.slice(-4); } -/** Validate a bet amount against the minimum and the user's balance. */ export function isValidAmount(amount: string, balance: number): boolean { const parsed = parseFloat(amount); if (Number.isNaN(parsed) || parsed < 1) return false; return parsed <= balance; } -/** Return a human-readable duration until a Unix-seconds timestamp. */ export function timeUntil(timestamp: number): string { const now = Math.floor(Date.now() / 1000); const diff = timestamp - now; - if (diff <= 0) return "Ended"; - const days = Math.floor(diff / 86_400); const hours = Math.floor((diff % 86_400) / 3_600); const minutes = Math.floor((diff % 3_600) / 60); - - if (days > 0) return `${days}d ${hours}h ${minutes}m`; - if (hours > 0) return `${hours}h ${minutes}m`; - if (minutes > 0) return `${minutes}m`; - return `${diff}s`; + if (days > 0) return days+"d "+hours+"h "+minutes+"m"; + if (hours > 0) return hours+"h "+minutes+"m"; + if (minutes > 0) return minutes+"m"; + return diff+"s"; } -/** - * Normalize a Unix timestamp that may be seconds or milliseconds to ms. - * Values below ~1e12 are almost certainly seconds; above are ms. - */ export function toTimestampMs(timestamp: number): number { - if (!Number.isFinite(timestamp)) return Date.now(); - return timestamp < 1e12 ? timestamp * 1000 : timestamp; + if (!Number.isFinite(timestamp) || timestamp <= 0) return Number.NaN; + const ts = timestamp < MILLISECOND_TIMESTAMP_THRESHOLD ? timestamp * 1_000 : timestamp; + return ts <= MAX_DATE_TIMESTAMP_MS ? ts : Number.NaN; } const DATE_TIME_OPTIONS: Intl.DateTimeFormatOptions = { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - timeZoneName: "short", + 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", + hour: "2-digit", minute: "2-digit", timeZoneName: "short" }; -/** - * 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" - */ -/** Normalize a positive Unix timestamp supplied in seconds or milliseconds. */ -export function toTimestampMs(timestamp: number): number { - if (!Number.isFinite(timestamp) || timestamp <= 0) return Number.NaN; - const timestampMs = - timestamp < MILLISECOND_TIMESTAMP_THRESHOLD - ? timestamp * 1_000 - : timestamp; - return timestampMs <= MAX_DATE_TIMESTAMP_MS ? timestampMs : Number.NaN; -} - -/** Format a timestamp in the viewer's timezone, including its timezone label. */ -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(ms)); -} - -/** - * 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(ms)); +/** Format a timestamp in the viewer's locale timezone — fixes issue #80 */ +export function formatDate(timestamp: number, locale?: Intl.LocalesArgument, options: Intl.DateTimeFormatOptions = {}): string { + const ts = toTimestampMs(timestamp); + if (!Number.isFinite(ts)) return INVALID_TIMESTAMP; + return new Intl.DateTimeFormat(locale ?? navigator.language, { ...DATE_TIME_OPTIONS, ...options }).format(new Date(ts)); } -/** - * Format an event timestamp (milliseconds) to a locale-aware date+time string. - * 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, DATE_TIME_OPTIONS); +/** Format only the local time portion — fixes issue #80 */ +export function formatTime(timestamp: number, locale?: Intl.LocalesArgument, options: Intl.DateTimeFormatOptions = {}): string { + const ts = toTimestampMs(timestamp); + if (!Number.isFinite(ts)) return INVALID_TIMESTAMP; + return new Intl.DateTimeFormat(locale ?? navigator.language, { ...TIME_OPTIONS, ...options }).format(new Date(ts)); } -/** - * Return a human-readable relative time string from a Unix timestamp (seconds). - * Uses the viewer's locale via Intl.RelativeTimeFormat. - * - * Examples: "2 hours ago", "3 days ago", "just now" - */ -export function timeAgo(timestamp: number): string { - 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); - - if (diffSeconds < 5) return "just now"; - - const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }); - - const thresholds: [number, Intl.RelativeTimeFormatUnit][] = [ - [60, "second"], - [3_600, "minute"], - [86_400, "hour"], - [604_800, "day"], - [2_592_000, "week"], - [31_536_000, "month"], - ]; - - for (const [limit, unit] of thresholds) { - if (diffSeconds < limit) { - 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); - } - } - - return rtf.format(-Math.floor(diffSeconds / 31_536_000), "year"); -} - -/** - * Calculate a winner's payout from a prediction market. - * payout = (userNetBet / winningSideTotal) × totalPool - const timestampMs = toTimestampMs(timestamp); - if (!Number.isFinite(timestampMs)) return INVALID_TIMESTAMP; - - return new Intl.DateTimeFormat(locale, { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - timeZoneName: "short", - ...options, - }).format(new Date(timestampMs)); -} - -/** Format only the local time portion of a timestamp, with its timezone label. */ -export function formatTime( - timestamp: number, - locale?: Intl.LocalesArgument, - options: Intl.DateTimeFormatOptions = {} -): string { - const timestampMs = toTimestampMs(timestamp); - if (!Number.isFinite(timestampMs)) return INVALID_TIMESTAMP; - - return new Intl.DateTimeFormat(locale, { - hour: "2-digit", - minute: "2-digit", - timeZoneName: "short", - ...options, - }).format(new Date(timestampMs)); -} - -/** Format a timestamp relative to now while accepting seconds or milliseconds. */ -export function timeAgo( - timestamp: number, - locale?: Intl.LocalesArgument -): string { - const timestampMs = toTimestampMs(timestamp); - if (!Number.isFinite(timestampMs)) return INVALID_TIMESTAMP; - - const diffSeconds = (timestampMs - Date.now()) / 1_000; - const absoluteSeconds = Math.abs(diffSeconds); - if (absoluteSeconds < 5) return "just now"; - +/** Format relative time — fixes issue #80 */ +export function timeAgo(timestamp: number, locale?: Intl.LocalesArgument): string { + const ts = toTimestampMs(timestamp); + if (!Number.isFinite(ts)) return INVALID_TIMESTAMP; + const diffSeconds = (ts - Date.now()) / 1_000; + const absSeconds = Math.abs(diffSeconds); + if (absSeconds < 5) return "just now"; const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [ - ["year", 31_536_000], - ["month", 2_592_000], - ["week", 604_800], - ["day", 86_400], - ["hour", 3_600], - ["minute", 60], - ["second", 1], + ["year", 31_536_000], ["month", 2_592_000], ["week", 604_800], + ["day", 86_400], ["hour", 3_600], ["minute", 60], ["second", 1] ]; - const [unit, unitSeconds] = - units.find(([, seconds]) => absoluteSeconds >= seconds) ?? units[6]; - - const value = - Math.sign(diffSeconds) * Math.round(absoluteSeconds / unitSeconds); - return new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format( - value, - unit - ); + const [unit, unitSec] = units.find(([, s]) => absSeconds >= s) ?? units[6]; + const value = Math.sign(diffSeconds) * Math.round(absSeconds / unitSec); + return new Intl.RelativeTimeFormat(locale ?? navigator.language, { numeric: "auto" }).format(value, unit); } -/** 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. */ export function formatEventTime(timestampMs: number): string { if (!Number.isFinite(timestampMs) || timestampMs <= 0) return INVALID_TIMESTAMP; - return new Date(timestampMs).toLocaleString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - timeZoneName: "short", - }); + return new Date(timestampMs).toLocaleString(navigator.language, DATE_TIME_OPTIONS); } -/** Calculate a winner's payout from a prediction market. - * - * payout = (userNetBet / winningSideTotal) × totalPool - * - * All values in XLM (not stroops). - */ -export function calculatePayout( - userNetBet: number, - winningSideTotal: number, - totalPool: number -): number { +export function calculatePayout(userNetBet: number, winningSideTotal: number, totalPool: number): number { if (winningSideTotal <= 0) return 0; return (userNetBet / winningSideTotal) * totalPool; } -/** - * Calculate YES/NO odds percentages from net totals. - * Returns { yesPercent, noPercent } — each 0-100. -/** Calculate YES/NO odds percentages from net totals. - * Returns { yesPercent, noPercent } – each 0-100. - */ -export function calculateOdds( - totalYes: number, - totalNo: number -): { yesPercent: number; noPercent: number } { +export function calculateOdds(totalYes: number, totalNo: number): { yesPercent: number; noPercent: number } { const total = totalYes + totalNo; if (total <= 0) return { yesPercent: 50, noPercent: 50 }; const yesPercent = Math.round((totalYes / total) * 100); return { yesPercent, noPercent: 100 - yesPercent }; } -/** - * Build a Stellar Expert explorer URL for transactions, accounts, or contracts. - */ -/** Convert basis points to a percentage string. */ -export function bpsToPercent(bps: number): string { - return `${bps / 100}%`; -} +export function bpsToPercent(bps: number): string { return (bps / 100)+"%"; } -/** Build a Stellar Expert explorer URL. */ -export function explorerUrl( - type: "tx" | "account" | "contract", - id: string, - network: "public" | "testnet" = "public" -): string { - const base = - network === "testnet" - ? "https://stellar.expert/explorer/testnet" - : "https://stellar.expert/explorer/public"; - return `${base}/${type}/${id}`; - const base = `https://stellar.expert/explorer/${network}`; +export function explorerUrl(type: "tx" | "account" | "contract", id: string, network: "public" | "testnet" = "public"): string { + const base = network === "testnet" ? "https://stellar.expert/explorer/testnet" : "https://stellar.expert/explorer/public"; switch (type) { - case "tx": - return `${base}/tx/${id}`; - case "account": - return `${base}/account/${id}`; - case "contract": - return `${base}/contract/${id}`; - default: - return base; + case "tx": return base+"/tx/"+id; + case "account": return base+"/account/"+id; + case "contract": return base+"/contract/"+id; + default: return base; } }