Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/utils/__tests__/time.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, it, expect } from 'vitest';
import { formatRelativeTimeLabel } from '../time.utils';

function makeDate(secondsAgo: number, from: Date): Date {
return new Date(from.getTime() - secondsAgo * 1000);
}

describe('formatRelativeTimeLabel', () => {
const now = new Date('2026-06-27T12:00:00.000Z');

it('returns "just now" for 0 seconds ago', () => {
expect(formatRelativeTimeLabel(now, now)).toBe('just now');
});

it('returns "just now" for 59 seconds ago', () => {
expect(formatRelativeTimeLabel(makeDate(59, now), now)).toBe('just now');
});

it('returns "1 minutes ago" for exactly 60 seconds ago', () => {
expect(formatRelativeTimeLabel(makeDate(60, now), now)).toBe('1 minutes ago');
});

it('returns "45 minutes ago" for 45 minutes ago', () => {
expect(formatRelativeTimeLabel(makeDate(45 * 60, now), now)).toBe('45 minutes ago');
});

it('returns "59 minutes ago" for 59 minutes 59 seconds ago', () => {
expect(formatRelativeTimeLabel(makeDate(59 * 60 + 59, now), now)).toBe('59 minutes ago');
});

it('returns "1 hours ago" for exactly 1 hour ago', () => {
expect(formatRelativeTimeLabel(makeDate(3600, now), now)).toBe('1 hours ago');
});

it('returns "23 hours ago" for 23 hours ago', () => {
expect(formatRelativeTimeLabel(makeDate(23 * 3600, now), now)).toBe('23 hours ago');
});

it('returns "1 days ago" for exactly 24 hours ago', () => {
expect(formatRelativeTimeLabel(makeDate(24 * 3600, now), now)).toBe('1 days ago');
});

it('returns "29 days ago" for 29 days ago', () => {
expect(formatRelativeTimeLabel(makeDate(29 * 24 * 3600, now), now)).toBe('29 days ago');
});

it('returns a formatted date for exactly 30 days ago', () => {
const date = makeDate(30 * 24 * 3600, now);
const result = formatRelativeTimeLabel(date, now);
expect(result).toMatch(/\d{1,2} \w+ \d{4}/);
});

it('returns a formatted date for dates older than 30 days', () => {
const date = new Date('2026-01-12T00:00:00.000Z');
const result = formatRelativeTimeLabel(date, now);
expect(result).toMatch(/12 Jan 2026/);
});

it('defaults now to the current time when omitted', () => {
const veryRecentDate = new Date(Date.now() - 5000);
expect(formatRelativeTimeLabel(veryRecentDate)).toBe('just now');
});
});
18 changes: 18 additions & 0 deletions src/utils/time.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,24 @@ export function formatAbsoluteDateTime(
}).format(date);
}

export function formatRelativeTimeLabel(date: Date, now: Date = new Date()): string {
const diffMs = now.getTime() - date.getTime();
const diffSec = Math.floor(diffMs / 1000);

if (diffSec < 60) return 'just now';

const diffMin = Math.floor(diffSec / 60);
if (diffMin < 60) return `${diffMin} minutes ago`;

const diffHr = Math.floor(diffMin / 60);
if (diffHr < 24) return `${diffHr} hours ago`;

const diffDay = Math.floor(diffHr / 24);
if (diffDay < 30) return `${diffDay} days ago`;

return date.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
}

export function formatRelativeTime(
input: string | number | Date | null | undefined,
options: RelativeTimeOptions = {}
Expand Down
Loading