// ── Formatting, with no dependencies and no React ───────────────────────── // // Every function here is pure and takes what the API answered, so the suite next // door can ask all of it without a DOM. That is deliberate: the client half's // real failures are timing and resolution (see `test/build.test.js`), which a // DOM-less runner cannot see — so the way to have any test coverage at all on // this side is to keep the parts that CAN be tested free of React. // // `Intl` does the work. It is in every browser core supports, it knows the // viewer's locale and their clock, and it is one fewer thing in a chunk an // operator ships. const RELATIVE = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }) const UNITS = [ ['year', 31536000], ['month', 2592000], ['week', 604800], ['day', 86400], ['hour', 3600], ['minute', 60], ['second', 1], ] /** * "3 minutes ago", from an ISO string or an epoch-millisecond number. * * Both shapes arrive from this module's own API: `updatedAt` is an ISO string * the model produced, and an event's `t` is the millisecond stamp the plugin put * on the frame. Accepting both here is what stops every caller remembering which * is which. */ export function ago(value, now = Date.now()) { const at = toMillis(value) if (at === null) return 'never' const seconds = Math.round((at - now) / 1000) const magnitude = Math.abs(seconds) // Under a minute, "in 0 seconds" is what `numeric: 'auto'` produces and it is // not what anybody means. Say the thing. if (magnitude < 45) return 'just now' const [unit, size] = UNITS.find(([, s]) => magnitude >= s) || ['second', 1] return RELATIVE.format(Math.round(seconds / size), unit) } /** * The stamp on a feed row. * * **Today's rows get a time; everything older gets a date as well.** The feed can * be filtered to a past wipe, and a row from six weeks ago rendered as `02:03 PM` * reads as this afternoon — which the page walk found the moment it looked at the * previous wipe: three events from August, all apparently a few minutes old. * * `now` is a parameter so the boundary is testable rather than a property of the * machine the test runs on. */ export function clock(value, now = Date.now()) { const at = toMillis(value) if (at === null) return '' const when = new Date(at) const time = when.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }) const today = new Date(now) const sameDay = when.getFullYear() === today.getFullYear() && when.getMonth() === today.getMonth() && when.getDate() === today.getDate() if (sameDay) return time return `${when.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} ${time}` } /** A date, for a wipe: the thing people actually compare wipes by. */ export function day(value) { const at = toMillis(value) if (at === null) return 'unknown' return new Date(at).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) } /** * A session or a playtime, as `4h 12m`. * * Seconds are dropped above a minute and kept below it, because a two-hour * session reported to the second is noise and a forty-second one reported as * "0m" is wrong. */ export function duration(seconds) { const total = Number(seconds) if (!Number.isFinite(total) || total <= 0) return '—' if (total < 60) return `${Math.round(total)}s` const hours = Math.floor(total / 3600) const minutes = Math.round((total % 3600) / 60) if (hours === 0) return `${minutes}m` return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m` } /** Thousands separators, in the viewer's locale. */ export function count(value) { const n = Number(value) return Number.isFinite(n) ? n.toLocaleString() : '0' } /** * A prefab short name as something readable — `patrolhelicopter` stays itself, * `rifle.ak` becomes `rifle ak`. * * Deliberately a light touch rather than a lookup table. A table mapping every * Rust prefab to a pretty name is a second copy of the game's item list that * goes stale every wipe, and the short name is what a Rust player reads on their * own server console anyway. */ export function prefab(name) { if (!name) return '' return String(name).replace(/[_.]+/g, ' ').trim() } /** A steam id, shortened for a table cell, without pretending it is a name. */ export function shortId(steamId) { const id = String(steamId || '') return id.length > 10 ? `…${id.slice(-6)}` : id } function toMillis(value) { if (value === null || value === undefined || value === '') return null if (typeof value === 'number') return Number.isFinite(value) ? value : null const parsed = Date.parse(value) return Number.isNaN(parsed) ? null : parsed } export default { ago, clock, day, duration, count, prefab, shortId }