"use client";
// Times in the visitor's own time zone and language. The server doesn't know either, so it renders times in UTC and
// the browser replaces them once the page is running.
import { useEffect, useState, useSyncExternalStore } from "react";
const noSubscription = () => () => {};
/** False on the server and while hydrating, true after. */
function useInBrowser(): boolean {
return useSyncExternalStore(
noSubscription,
() => true,
() => false,
);
}
const units: [Intl.RelativeTimeFormatUnit, number][] = [
["year", 365 * 24 * 3600],
["month", 30 * 24 * 3600],
["week", 7 * 24 * 3600],
["day", 24 * 3600],
["hour", 3600],
["minute", 60],
];
const relative = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
function ago(iso: string, now: number): string {
const seconds = (new Date(iso).getTime() - now) / 1000;
for (const [unit, size] of units) {
if (Math.abs(seconds) >= size) return relative.format(Math.round(seconds / size), unit);
}
return "just now";
}
/** "5 minutes ago", kept up to date. */
export function TimeAgo({ iso }: { iso: string }) {
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
const timer = setInterval(() => setNow(Date.now()), 30_000);
return () => clearInterval(timer);
}, []);
const local = useInBrowser();
return (
);
}
function format(iso: string, options: Intl.DateTimeFormatOptions, local: boolean): string {
return new Date(iso).toLocaleString(local ? undefined : "en", local ? options : { ...options, timeZone: "UTC" });
}
/** A date (and time). */
export function DateTime({ iso, dateOnly = false }: { iso: string; dateOnly?: boolean }) {
const local = useInBrowser();
const options: Intl.DateTimeFormatOptions = dateOnly
? { dateStyle: "medium" }
: { dateStyle: "medium", timeStyle: "short" };
return ;
}
/** A time of day ("3:04 PM"), with the date too when it isn't today. */
export function Since({ iso }: { iso: string }) {
const local = useInBrowser();
const today = local && new Date(iso).toDateString() === new Date().toDateString();
const options: Intl.DateTimeFormatOptions = today
? { hour: "numeric", minute: "2-digit" }
: { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" };
return (
);
}