Move app to apps/web-react in pnpm workspace layout

This commit is contained in:
Kalle
2026-08-16 09:20:33 +03:00
parent 7e365ccfcf
commit bbc8ea57af
2913 changed files with 227 additions and 250 deletions

View File

@@ -0,0 +1,16 @@
import { AsyncLocalStorage } from "node:async_hooks";
interface ViewerTimezoneContext {
timezone: string | null;
}
export const viewerTimezoneAsyncLocalStorage =
new AsyncLocalStorage<ViewerTimezoneContext>();
/**
* The viewer's IANA timezone for the current request. `null` when the browser has
* not reported it yet, which is the case for a device's very first document request.
*/
export function getViewerTimezone(): string | null {
return viewerTimezoneAsyncLocalStorage.getStore()?.timezone ?? null;
}

View File

@@ -0,0 +1,22 @@
import { describe, expect, test } from "vitest";
import { viewerTimezoneFromCookieHeader } from "./timezone-cookie";
describe("viewerTimezoneFromCookieHeader", () => {
test.each([
["timezone=Europe/Helsinki", "Europe/Helsinki"],
["theme=dark; timezone=Asia/Tokyo; i18n=en", "Asia/Tokyo"],
["timezone=UTC", "UTC"],
])("%s -> %s", (header, expected) => {
expect(viewerTimezoneFromCookieHeader(header)).toBe(expected);
});
test.each([
["$why: no cookie header at all", null],
["$why: cookie not written yet", "theme=dark"],
["$why: not a timezone", "timezone=Not/AZone"],
["$why: empty value", "timezone="],
["$why: only a prefix of the name matches", "mytimezone=Asia/Tokyo"],
])("%s", (_why, header) => {
expect(viewerTimezoneFromCookieHeader(header)).toBeNull();
});
});

View File

@@ -0,0 +1,57 @@
import { z } from "zod";
import { logger } from "~/utils/logger";
const COOKIE_NAME = "timezone";
const TEN_YEARS_IN_MS = 315_360_000_000;
const ianaTimezone = z
.string()
.max(100)
.refine((value) => {
try {
new Intl.DateTimeFormat("en-US", { timeZone: value });
return true;
} catch {
return false;
}
});
/**
* Stores the browser's IANA timezone in a cookie so that the server can read it
* while rendering. Neither a request header nor a client hint carries the
* timezone, so it has to be written client-side before the server can know it.
*/
export function writeViewerTimezoneCookie() {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (!timezone) return;
void cookieStore
.set({
name: COOKIE_NAME,
value: timezone,
path: "/",
expires: Date.now() + TEN_YEARS_IN_MS,
sameSite: "lax",
})
.catch((error) =>
logger.error("Failed to store the timezone cookie", error),
);
}
/**
* The viewer's IANA timezone as written by {@link writeViewerTimezoneCookie}.
* `null` when the cookie is missing or holds something that is not a timezone.
*/
export function viewerTimezoneFromCookieHeader(
header: string | null,
): string | null {
const value = header
?.split(";")
.map((cookie) => cookie.trim())
.find((cookie) => cookie.startsWith(`${COOKIE_NAME}=`))
?.slice(COOKIE_NAME.length + 1);
const parsed = ianaTimezone.safeParse(value);
return parsed.success ? parsed.data : null;
}

View File

@@ -0,0 +1,16 @@
import type { MiddlewareFunction } from "react-router";
import { viewerTimezoneAsyncLocalStorage } from "./timezone-context.server";
import { viewerTimezoneFromCookieHeader } from "./timezone-cookie";
/**
* Resolves the viewer's timezone from their cookie and makes it available to
* loaders via `getViewerTimezone` for the duration of the request.
*/
export const timezoneMiddleware: MiddlewareFunction<Response> = (
{ request },
next,
) =>
viewerTimezoneAsyncLocalStorage.run(
{ timezone: viewerTimezoneFromCookieHeader(request.headers.get("Cookie")) },
() => next(),
);