import { useReducer } from "react"; import { useInterval } from "../hooks/effects"; /** * Re-renders the calling component every `intervalMs` milliseconds while * `active` is true, returning the current `Date.now()` value at each tick. * Returns the captured value when paused, so renders are stable. */ export function useTickingNow(active: boolean, intervalMs = 1000): number { const [now, tick] = useReducer(() => Date.now(), undefined, Date.now); useInterval(tick, intervalMs, active); return now; } /** * Whole seconds elapsed since an ISO 8601 timestamp, or `null` when the * timestamp is missing or unparseable. Never negative. Pass the value from * `useTickingNow` as `now` so the count advances on each tick. */ export function elapsedSecsSince(startedAt: string | null, now: number = Date.now()): number | null { if (!startedAt) return null; const startMs = Date.parse(startedAt); if (Number.isNaN(startMs)) return null; return Math.max(0, Math.floor((now - startMs) / 1000)); } function relativeTime(seconds: number, past: boolean): string { if (seconds < 60) return past ? "just now" : "in <1m"; const minutes = Math.floor(seconds / 60); if (minutes < 60) return past ? `${minutes}m ago` : `in ${minutes}m`; const hours = Math.floor(minutes / 60); if (hours < 24) return past ? `${hours}h ago` : `in ${hours}h`; const days = Math.floor(hours / 24); return past ? `${days}d ago` : `in ${days}d`; } /** * Format an ISO 8601 timestamp as a relative past time string (e.g. "2h ago", "3d ago"). */ export function timeAgo(iso: string): string { return relativeTime(Math.floor((Date.now() - new Date(iso).getTime()) / 1000), true); } /** * Format an ISO 8601 timestamp as a relative future time string (e.g. "in 2h", "in 3d"). */ export function timeUntil(iso: string): string { return relativeTime(Math.floor((new Date(iso).getTime() - Date.now()) / 1000), false); }