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 | 282x 280x 282x 278x 227x 208x 179x 169x 282x 282x 278x 282x 90x 90x 90x 84x 84x 81x 71x 71x 71x 71x 71x 71x 63x 60x 188x 188x 188x 188x 188x 177x 167x | /**
* 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"
if (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);
}
|