format
The formatters the components here already use — durations, dates, timestamps, initials.
Quick Preview
| Call | Result | Note |
|---|---|---|
| formatDuration(95) | 1:35 | Under an hour |
| formatDuration(3723) | 1:02:03 | Over an hour |
| formatDuration(NaN) | 0:00 | Metadata not loaded |
| formatDate(moment) | 08 August 2026 | Day first, month spelled |
| formatTimestamp(moment) | 14:05 | 24-hour |
| formatTimestamp('Yesterday') | Yesterday | Passed through |
| toDateTimeAttribute(moment) | 2026-08-08T14:05:00.000Z | For <time datetime> |
| getInitials('Daniel Ikoyo') | DI | First two words |
Usage
These were living inside the components that needed them. They are published because the same values get written outside a component as often as inside one — a duration in a table cell, initials on a server-rendered avatar, a date in an export — and a second implementation of "how we write a duration" is how a product ends up with two.
import { formatDuration, getInitials } from "@giddaa-housing/ui/utils/format";
formatDuration(3723); // "1:02:03"
getInitials("Daniel Ikoyo"); // "DI"Each is a pure function with no React and no styling, so a server-only module can import them. MediaPlayer, Message, EditorialCard and DatePicker now read from here rather than keeping their own copies.
Examples
What each one writes
| Call | Result | Note |
|---|---|---|
| formatDuration(95) | 1:35 | Under an hour |
| formatDuration(3723) | 1:02:03 | Over an hour |
| formatDuration(NaN) | 0:00 | Metadata not loaded |
| formatDate(moment) | 08 August 2026 | Day first, month spelled |
| formatTimestamp(moment) | 14:05 | 24-hour |
| formatTimestamp('Yesterday') | Yesterday | Passed through |
| toDateTimeAttribute(moment) | 2026-08-08T14:05:00.000Z | For <time datetime> |
| getInitials('Daniel Ikoyo') | DI | First two words |
Durations
The hours part is dropped below an hour, so a short clip does not read as if it might be long. Anything that is not a finite number is "0:00" — a media element reports NaN for its duration until metadata loads, and the alternative is NaN:NaN on screen for the first few frames.
formatDuration(95); // "1:35"
formatDuration(3723); // "1:02:03"
formatDuration(NaN); // "0:00"
formatDuration(-10); // "0:00"Timestamps
formatTimestamp passes a string through untouched, so a caller that already has the wording it wants can hand it over without this second-guessing it. Pair it with toDateTimeAttribute for a <time> element.
<time dateTime={toDateTimeAttribute(sentAt)}>{formatTimestamp(sentAt)}</time>formatTimestamp(new Date(2026, 7, 8, 14, 5)); // "14:05"
formatTimestamp("Yesterday"); // "Yesterday"Dates
Day before month, spelled out — "08 August 2026" — because "8/8/2026" is ambiguous between two conventions where a spelled month is not. Pass a locale for anything else.
formatDate(new Date(2026, 7, 8)); // "08 August 2026"
formatDate(new Date(2026, 7, 8), "en-US"); // "August 08, 2026"Props
| Prop/API | Type | Default | Description |
|---|---|---|---|
formatDuration | (seconds?: number) => string | - | Seconds as a clock. "0:00" for anything not finite. |
formatDate | (value: Date, locale?: string) => string | "en-GB" | A date written out in full. |
formatTimestamp | (value: Date | string, locale?: string) => string | - | A 24-hour time of day. A string passes through. |
toDateTimeAttribute | (value: Date | string) => string | - | The machine-readable form for a <time datetime>. |
getInitials | (name: string) => string | - | The first letter of the first two words. |
TimePicker's own parser is deliberately not here: it falls back to the current time when a value will not parse, which is right for seeding a picker's wheels and wrong for a general-purpose formatter.
Accessibility
- Pair
formatTimestampwithtoDateTimeAttributeon a<time>element so assistive tech and search engines get an unambiguous value alongside the readable one. getInitialsproduces a decorative monogram. Give the avatar an accessible name of the full name — initials alone are read out as two letters and identify nobody.- A duration read as
"1:02:03"is announced as digits by most screen readers. Where the exact length matters, put the spelled form in anaria-label.