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 | 190x 188x 190x 186x 146x 139x 132x 126x 190x 190x 186x 190x 49x 49x 49x 49x 49x 47x 45x 45x 45x 45x 45x 45x 43x 40x 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);
}
|