refactor: extract shared formatting utilities (duration, date, age)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-05-29 16:05:35 +01:00
co-authored by Sisyphus
parent 2b56c5640d
commit e8c03d6c30
+88
View File
@@ -0,0 +1,88 @@
/**
* Shared formatting utilities for consistent display across the app.
*/
/**
* Convert minutes to a human-readable duration string.
*
* Examples:
* 45 → "45m"
* 60 → "1h"
* 90 → "1h 30m"
* 120 → "2h"
* 150 → "2h 30m"
*/
export function formatDuration(minutes: number): string {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (hours === 0) return `${mins}m`;
if (mins === 0) return `${hours}h`;
return `${hours}h ${mins}m`;
}
/**
* Format a date/time to "Weekday, Month Day at HH:MM AM/PM".
*
* Examples:
* "2026-05-29T10:00:00Z" → "Friday, May 29 at 10:00 AM"
* "2026-06-05T14:30:00Z" → "Friday, Jun 5 at 2:30 PM"
*/
export function formatDateTime(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date;
const dateStr = d.toLocaleDateString('en-US', {
weekday: 'long',
month: 'short',
day: 'numeric'
});
const timeStr = d.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
});
return `${dateStr} at ${timeStr}`;
}
/**
* Format a date value to "Weekday, Month Day" only (no time).
*
* Example: "2026-05-29" → "Friday, May 29"
*/
export function formatDate(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleDateString('en-US', {
weekday: 'long',
month: 'short',
day: 'numeric'
});
}
/**
* Format a time value to "HH:MM AM/PM".
*
* Example: new Date("2026-05-29T14:30:00Z") → "2:30 PM"
*/
export function formatTime(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
});
}
/**
* Calculate age from a date-of-birth string.
* Returns the number of full years, or null if DOB is invalid.
*/
export function calculateAge(dateOfBirth: string | undefined | null): number | null {
if (!dateOfBirth) return null;
const dob = new Date(dateOfBirth);
if (isNaN(dob.getTime())) return null;
const today = new Date();
let age = today.getFullYear() - dob.getFullYear();
const monthDiff = today.getMonth() - dob.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dob.getDate())) {
age--;
}
return age;
}