Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | 542x 540x 542x 538x 498x 491x 484x 126x 542x 542x 538x 542x 401x 401x 401x 401x 401x 399x 397x 397x 397x 397x 397x 397x 395x 392x 137x 137x 137x 137x 137x 112x 106x | /**
* Shared date formatting utilities for the Portal.
*
* formatRelativeDate — converts a date to a human-readable relative label.
*
* Two granularity modes:
* "day" (default) — UTC-boundary day buckets (Today / Yesterday / Nd ago / …)
* "time" — minute/hour precision, then falls through to day buckets
*
* Invalid inputs (null, undefined, NaN, unparseable strings) return "" without throwing.
*/
export type DateInput = string | number | Date;
export type RelativeDateOpts = { granularity?: "day" | "time" };
function toDate(input: DateInput | null | undefined): Date | null {
if (input == null) return null;
const d = input instanceof Date ? input : new Date(input);
if (Number.isNaN(d.getTime())) return null;
return d;
}
/**
* Day-granular suffix computed from a pre-validated `diffDays` value.
* Used by both granularity branches for the 2d+ range.
*/
function daysSuffix(diffDays: number): string {
if (diffDays < 7) return `${diffDays}d ago`;
if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`;
if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo ago`;
return `${Math.floor(diffDays / 365)}y ago`;
}
export function formatRelativeDate(
input: DateInput | null | undefined,
opts?: RelativeDateOpts,
): string {
const date = toDate(input);
if (date === null) return "";
const granularity = opts?.granularity ?? "day";
if (granularity === "time") {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
// Future dates: clamp to "Just now"
Iif (diffMs < 0) return "Just now";
const diffMins = Math.floor(diffMs / (1000 * 60));
if (diffMins < 1) return "Just now";
if (diffMins < 60) return `${diffMins}m ago`;
const diffHours = Math.floor(diffMins / 60);
// For 1h+, use UTC-boundary day diff so "Yesterday" is consistent with
// granularity:"day" — both branches agree that Yesterday means UTC day diff === 1.
// This covers the 23h-crossing-midnight case: 23h elapsed but different UTC day
// correctly returns "Yesterday" rather than "23h ago".
const DAY_MS = 1000 * 60 * 60 * 24;
const utcDate = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
const utcNow = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
const utcDiffDays = Math.floor((utcNow - utcDate) / DAY_MS);
// Same UTC day → show hours (covers same-day multi-hour range)
if (utcDiffDays <= 0) return `${diffHours}h ago`;
if (utcDiffDays === 1) return "Yesterday";
return daysSuffix(utcDiffDays);
}
// --- day granularity (UTC boundary) ---
// Use UTC midnight values so the day boundary is consistent regardless of
// the local timezone of the runtime (matches the original BlogCard pattern).
const now = new Date();
const utcDate = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
const utcNow = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
const diffDays = Math.floor((utcNow - utcDate) / (1000 * 60 * 60 * 24));
// Future dates: clamp to "Today"
if (diffDays <= 0) return "Today";
if (diffDays === 1) return "Yesterday";
return daysSuffix(diffDays);
}
|