g1t/apps/sudo/app/routes/timezone.tsx

82 lines3,303 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Fast pages, required checks on the branch, self-hosted runners, honest incidents1import { redirect } from "react-router";
2
3import type { Route } from "./+types/timezone";
4import { Button, Field, PageHeader, Section, Select, When } from "~/components/ui";
5import { text } from "~/lib/forms";
6import { requireStaff, zoneContext } from "~/lib/staff";
7import { ZONE_COOKIE, validZone, zoneAbbr } from "~/lib/time";
8
9export const meta: Route.MetaFunction = () => [{ title: "Time zone · sudo" }, { name: "robots", content: "noindex, nofollow" }];
10
11/** Where to go back to: a page of sudo's own, never another site. */
12function backTo(raw: string | null): string {
13 return raw && /^\/(?!\/)[^\s\\]*$/.test(raw) ? raw : "/";
14}
15
16export async function loader({ request, context }: Route.LoaderArgs) {
17 requireStaff(context);
18 const { zone, chosen } = context.get(zoneContext);
19 const placed = (request as { cf?: { timezone?: unknown } }).cf?.timezone;
20 const zones = (Intl as { supportedValuesOf?: (key: string) => string[] }).supportedValuesOf?.("timeZone") ?? [];
21 return {
22 zone,
23 chosen,
24 placed: validZone(placed) ? placed : null,
25 zones: zones.includes("UTC") ? zones : ["UTC", ...zones],
26 back: backTo(new URL(request.url).searchParams.get("back")),
27 now: new Date().toISOString(),
28 };
29}
30
31export async function action({ request, context }: Route.ActionArgs) {
32 requireStaff(context);
33 const form = await request.formData();
34 const zone = text(form, "zone");
35 const back = backTo(text(form, "back"));
36 const cookie =
37 zone && validZone(zone)
38 ? `${ZONE_COOKIE}=${encodeURIComponent(zone)}; Path=/; Max-Age=31536000; Secure; HttpOnly; SameSite=Lax`
39 : `${ZONE_COOKIE}=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Lax`;
40 return redirect(back, { headers: { "set-cookie": cookie } });
41}
42
43export default function TimeZone({ loaderData }: Route.ComponentProps) {
44 const { zone, chosen, placed, zones, back, now } = loaderData;
45 const at = new Date(now);
46 return (
47 <main className="mx-auto max-w-3xl px-4 py-8 sm:py-10">
48 <PageHeader
49 title="Time zone"
50 description="Every time in sudo is shown in this zone, with its abbreviation. Hover a time to see it in UTC."
51 />
52 <form method="post" className="mt-6 space-y-6">
53 <input type="hidden" name="back" value={back} />
54 <Section title="Show times in">
55 <div className="space-y-4">
56 <Field
57 label="Zone"
58 hint={
59 placed
60 ? `Where Cloudflare places you: ${placed} (${zoneAbbr(at, placed)}). Choose it to follow you when you travel.`
61 : "Cloudflare did not say where you are; without a choice, times are in UTC."
62 }
63 >
64 <Select name="zone" defaultValue={chosen ? zone : ""}>
65 <option value="">Where Cloudflare places me{placed ? ` (${placed})` : " (UTC)"}</option>
66 {zones.map((z) => (
67 <option key={z} value={z}>
68 {z.replace(/_/g, " ")}
69 </option>
70 ))}
71 </Select>
72 </Field>
73 <p className="text-sm text-muted">
74 Now: <When at={now} time />
75 </p>
76 <Button type="submit">Save</Button>
77 </div>
78 </Section>
79 </form>
80 </main>
81 );
82}