1import { headers } from "next/headers";
2import { visitorTimezoneHeader } from "../../src/VisitorTimezone.ts";
3
4function formatTime(
5 value: string | number,
6 locale: string,
7 timeZone: string,
8): { iso: string; label: string } {
9 const date = new Date(value);
10 if (Number.isNaN(date.getTime())) {
11 return { iso: "", label: "" };
12 }
13 return {
14 iso: date.toISOString(),
15 label: date.toLocaleString(locale, {
16 dateStyle: "medium",
17 timeStyle: "medium",
18 timeZone,
19 }),
20 };
21}
22
23async function visitorFormat(): Promise<{ locale: string; timeZone: string }> {
24 const h = await headers();
25 const locale = h.get("accept-language")?.split(",")[0]?.split(";")[0]?.trim();
26 return {
27 locale: locale || "en-US",
28 timeZone: h.get(visitorTimezoneHeader()) ?? "UTC",
29 };
30}
31
32/**
33 * Server timestamp. Visitor locale + Cloudflare timezone, unless `cached`
34 * (UTC) for prerendered routes that must not read request headers.
35 */
36export async function LocalTime({
37 value,
38 cached = false,
39 testId,
40}: {
41 value: string | number;
42 cached?: boolean;
43 testId?: string;
44}) {
45 const { locale, timeZone } = cached
46 ? { locale: "en-US", timeZone: "UTC" }
47 : await visitorFormat();
48 const { iso, label } = formatTime(value, locale, timeZone);
49
50 return (
51 <time data-testid={testId} dateTime={iso} title={iso}>
52 {label}
53 </time>
54 );
55}