refactor(web): consolidate duplicated relative-time helpers

Eight copies of the compact "5m ago / 3h ago" relative-time formatter
existed across components and pages, differing only in rounding,
"just now" capitalization, absolute-date cutoff, and missing-value
fallback. Extend lib/date.ts formatRelativeTime with options for those
axes (backwards compatible for existing callers) and convert the
duplicates to thin wrappers preserving each call site's exact output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Quick
2026-08-03 09:12:53 -04:00
committed by Quick104
co-authored by Claude Fable 5
parent 2dedf3ff26
commit 9c585e75a2
9 changed files with 126 additions and 88 deletions
+2 -13
View File
@@ -23,7 +23,7 @@ import {
import { SETTING_KEYS } from "@/lib/settingsContract";
import { cn } from "@/lib/utils";
import type { AdminDeviceSetting } from "@/hooks/queries/admin/users";
import { formatDate } from "@/lib/datetime";
import { formatRelativeTime } from "@/lib/date";
export const UNKNOWN_PROFILE_ID = "unknown-profile";
@@ -90,18 +90,7 @@ export function platformLabel(raw: string | undefined): string {
}
export function formatRelative(iso: string | undefined): string {
if (!iso) return "—";
const then = new Date(iso).getTime();
if (Number.isNaN(then)) return "—";
const diff = Date.now() - then;
const m = Math.round(diff / 60000);
if (m < 1) return "just now";
if (m < 60) return `${m}m ago`;
const h = Math.round(m / 60);
if (h < 24) return `${h}h ago`;
const d = Math.round(h / 24);
if (d < 30) return `${d}d ago`;
return formatDate(iso);
return formatRelativeTime(iso, { absoluteAfterDays: 30 }) ?? "—";
}
export function shortenId(id: string, visible = 8): string {
@@ -26,7 +26,7 @@ import {
providerLabel,
staggerRowClass,
} from "./subtitleAdminStyles";
import { formatDate } from "@/lib/datetime";
import { formatRelativeTime } from "@/lib/date";
interface AdminSubtitlesTableProps {
subtitles: AdminDownloadedSubtitle[];
@@ -37,17 +37,7 @@ interface AdminSubtitlesTableProps {
}
function formatRelative(value: string): string {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
const deltaMs = Date.now() - date.getTime();
const minutes = Math.floor(deltaMs / 60000);
if (minutes < 1) return "just now";
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
if (days < 30) return `${days}d ago`;
return formatDate(date);
return formatRelativeTime(value, { rounding: "floor", absoluteAfterDays: 30 }) ?? value;
}
export default function AdminSubtitlesTable({
+73
View File
@@ -0,0 +1,73 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { formatRelativeTime } from "./date";
import { setDateTimeFormatPreferences } from "./datetime";
const NOW = new Date(2026, 5, 5, 12, 0, 0);
function ago(ms: number): string {
return new Date(NOW.getTime() - ms).toISOString();
}
const MINUTE = 60_000;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(NOW);
});
afterEach(() => {
vi.useRealTimers();
setDateTimeFormatPreferences({ dateFormat: "auto", timeFormat: "auto" });
});
describe("formatRelativeTime", () => {
it("returns null for missing or unparseable values", () => {
expect(formatRelativeTime(null)).toBeNull();
expect(formatRelativeTime(undefined)).toBeNull();
expect(formatRelativeTime("")).toBeNull();
expect(formatRelativeTime("not-a-date")).toBeNull();
});
it("formats the compact tiers", () => {
expect(formatRelativeTime(ago(10_000))).toBe("just now");
expect(formatRelativeTime(ago(5 * MINUTE))).toBe("5m ago");
expect(formatRelativeTime(ago(3 * HOUR))).toBe("3h ago");
expect(formatRelativeTime(ago(2 * DAY))).toBe("2d ago");
});
it("treats future timestamps as just now", () => {
expect(formatRelativeTime(ago(-5 * MINUTE))).toBe("just now");
});
it("rounds by default and floors when asked", () => {
// 1h31m: round → 2h, floor → 1h.
const value = ago(HOUR + 31 * MINUTE);
expect(formatRelativeTime(value)).toBe("2h ago");
expect(formatRelativeTime(value, { rounding: "floor" })).toBe("1h ago");
});
it("supports a custom just-now label", () => {
expect(formatRelativeTime(ago(10_000), { justNowLabel: "Just now" })).toBe("Just now");
});
it("switches to an absolute date after the configured number of days", () => {
setDateTimeFormatPreferences({ dateFormat: "YYYY-MM-DD", timeFormat: "auto" });
expect(formatRelativeTime(ago(29 * DAY), { absoluteAfterDays: 30 })).toBe("29d ago");
expect(formatRelativeTime(ago(40 * DAY), { absoluteAfterDays: 30 })).toBe("2026-04-26");
// absoluteAfterDays: 1 skips the day tier entirely (floor keeps 25h at 1d).
expect(formatRelativeTime(ago(25 * HOUR), { rounding: "floor", absoluteAfterDays: 1 })).toBe(
"2026-06-04",
);
});
it("uses a caller-provided absolute renderer", () => {
expect(
formatRelativeTime(ago(40 * DAY), {
absoluteAfterDays: 30,
absolute: (date) => `on ${date.getFullYear()}`,
}),
).toBe("on 2026");
});
});
+28 -7
View File
@@ -1,10 +1,26 @@
import { formatDate } from "@/lib/datetime";
export interface RelativeTimeOptions {
/** Unit arithmetic: "round" (default) or "floor". */
rounding?: "round" | "floor";
/** Label for deltas under one minute (default "just now"). */
justNowLabel?: string;
/** Switch to an absolute date once the delta reaches this many days. */
absoluteAfterDays?: number;
/** Absolute-date renderer (default: preference-aware numeric formatDate). */
absolute?: (date: Date) => string;
}
/**
* Compact relative-time label ("just now", "5m ago", "3h ago", "2d ago").
* Returns null for missing or unparseable values.
* Compact relative-time label ("just now", "5m ago", "3h ago", "2d ago"),
* optionally switching to an absolute date after `absoluteAfterDays`.
* Returns null for missing or unparseable values so call sites can pick
* their own fallback ("—", "Never", the raw value, ...).
*/
export function formatRelativeTime(value: string | null): string | null {
export function formatRelativeTime(
value: string | null | undefined,
options?: RelativeTimeOptions,
): string | null {
if (!value) {
return null;
}
@@ -12,18 +28,23 @@ export function formatRelativeTime(value: string | null): string | null {
if (Number.isNaN(date.getTime())) {
return null;
}
const diffMinutes = Math.round((Date.now() - date.getTime()) / 60_000);
const toUnit = options?.rounding === "floor" ? Math.floor : Math.round;
const diffMinutes = toUnit((Date.now() - date.getTime()) / 60_000);
if (diffMinutes < 1) {
return "just now";
return options?.justNowLabel ?? "just now";
}
if (diffMinutes < 60) {
return `${diffMinutes}m ago`;
}
const diffHours = Math.round(diffMinutes / 60);
const diffHours = toUnit(diffMinutes / 60);
if (diffHours < 24) {
return `${diffHours}h ago`;
}
return `${Math.round(diffHours / 24)}d ago`;
const diffDays = toUnit(diffHours / 24);
if (diffDays < (options?.absoluteAfterDays ?? Number.POSITIVE_INFINITY)) {
return `${diffDays}d ago`;
}
return (options?.absolute ?? formatDate)(date);
}
export function formatBirthDate(dateStr: string): string {
+3 -12
View File
@@ -79,10 +79,8 @@ import type {
UpdateHistoryImportSourceRequest,
} from "@/api/types";
import { cn } from "@/lib/utils";
import {
formatDate as formatPreferredDate,
formatDateTime as formatPreferredDateTime,
} from "@/lib/datetime";
import { formatRelativeTime } from "@/lib/date";
import { formatDateTime as formatPreferredDateTime } from "@/lib/datetime";
// ---------------------------------------------------------------------------
// Shared helpers
@@ -135,14 +133,7 @@ function StatusBadge({ status }: { status: HistoryImportRun["status"] }) {
}
function timeAgo(dateStr: string | undefined) {
if (!dateStr) return "Never";
const d = new Date(dateStr);
const now = Date.now();
const diff = now - d.getTime();
if (diff < 60_000) return "just now";
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
return formatPreferredDate(d);
return formatRelativeTime(dateStr, { rounding: "floor", absoluteAfterDays: 1 }) ?? "Never";
}
function formatDate(dateStr: string | undefined) {
+4 -13
View File
@@ -15,6 +15,7 @@ import {
import { usePageActivity } from "@/hooks/usePageActivity";
import { cn } from "@/lib/utils";
import type { TaskCategory, TaskInfo, TriggerConfig } from "@/api/types";
import { formatRelativeTime } from "@/lib/date";
import { formatDateTime as formatPreferredDateTime } from "@/lib/datetime";
const CATEGORY_ORDER: TaskCategory[] = ["library", "metadata", "system"];
@@ -51,18 +52,6 @@ function useTaskClock() {
return now;
}
function formatRelativeTime(dateStr: string, now: number): string {
const diff = Math.max(0, now - new Date(dateStr).getTime());
const seconds = Math.floor(diff / 1000);
if (seconds < 60) return "just now";
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
function formatDuration(ms: number): string {
if (ms <= 0) return "<1ms";
if (ms < 1000) return `${ms}ms`;
@@ -250,7 +239,9 @@ function TaskRow({
{!describeSchedule(task.triggers) && <span>No schedule</span>}
{task.last_execution && (
<span className="ml-2">
· Last run: {formatRelativeTime(task.last_execution.completed_at, now)}
· Last run:{" "}
{formatRelativeTime(task.last_execution.completed_at, { rounding: "floor" }) ??
"—"}
{typeof task.last_execution.duration_ms === "number"
? ` · Duration: ${formatDuration(task.last_execution.duration_ms)}`
: ""}
+4 -10
View File
@@ -97,6 +97,7 @@ import {
needsConnectionStep,
needsDeliveryChoice,
} from "./sourceDescriptor";
import { formatRelativeTime as formatRelativeTimeBase } from "@/lib/date";
// ---------------------------------------------------------------------------
// Helpers
@@ -125,16 +126,9 @@ function resolveSourceName(
}
function formatRelativeTime(isoString: string | null): string {
if (!isoString) return "Never";
const date = new Date(isoString);
const diffMs = Date.now() - date.getTime();
const diffMin = Math.floor(diffMs / 60_000);
if (diffMin < 1) return "Just now";
if (diffMin < 60) return `${diffMin}m ago`;
const diffHr = Math.floor(diffMin / 60);
if (diffHr < 24) return `${diffHr}h ago`;
const diffDays = Math.floor(diffHr / 24);
return `${diffDays}d ago`;
return (
formatRelativeTimeBase(isoString, { rounding: "floor", justNowLabel: "Just now" }) ?? "Never"
);
}
// ---------------------------------------------------------------------------
@@ -38,7 +38,7 @@ import {
} from "./HistoryImportSettings.utils";
import { cn } from "@/lib/utils";
import { AlertTriangle, CheckCircle2, CircleSlash2, Clock, Loader2, XCircle } from "lucide-react";
import { formatDate } from "@/lib/datetime";
import { formatRelativeTime as formatRelativeTimeBase } from "@/lib/date";
const STATUS_CONFIG = {
queued: {
@@ -987,16 +987,13 @@ function HistoryRunCard({
──────────────────────────────────────────────────────────── */
function formatRelativeTime(dateStr: string): string {
const diffMs = Date.now() - new Date(dateStr).getTime();
const minutes = Math.floor(diffMs / 60_000);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (minutes < 1) return "Just now";
if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ago`;
if (days < 7) return `${days}d ago`;
return formatDate(dateStr);
return (
formatRelativeTimeBase(dateStr, {
rounding: "floor",
justNowLabel: "Just now",
absoluteAfterDays: 7,
}) ?? ""
);
}
function formatDuration(startStr: string, endStr: string): string {
@@ -32,18 +32,10 @@ import {
type WatchProviderSyncRun,
} from "@/hooks/queries/watchProviders";
import { Input } from "@/components/ui/input";
import { formatRelativeTime as formatRelativeTimeBase } from "@/lib/date";
function formatRelativeTime(value?: string) {
if (!value) return "Never";
const timestamp = new Date(value).getTime();
if (Number.isNaN(timestamp)) return "Never";
const seconds = Math.max(0, Math.floor((Date.now() - timestamp) / 1000));
if (seconds < 60) return "Just now";
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
return `${Math.floor(hours / 24)}d ago`;
return formatRelativeTimeBase(value, { rounding: "floor", justNowLabel: "Just now" }) ?? "Never";
}
function formatRetryAfter(seconds?: number) {