mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-01 07:37:55 -05:00
Move app to apps/web-react in pnpm workspace layout
This commit is contained in:
@@ -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;
|
||||
}
|
||||
22
apps/web-react/app/features/timezone/timezone-cookie.test.ts
Normal file
22
apps/web-react/app/features/timezone/timezone-cookie.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
57
apps/web-react/app/features/timezone/timezone-cookie.ts
Normal file
57
apps/web-react/app/features/timezone/timezone-cookie.ts
Normal 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;
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
Reference in New Issue
Block a user