| 1 | import { redirect } from "react-router"; |
| 2 | |
| 3 | import type { Route } from "./+types/timezone"; |
| 4 | import { Button, Field, PageHeader, Section, Select, When } from "~/components/ui"; |
| 5 | import { text } from "~/lib/forms"; |
| 6 | import { requireStaff, zoneContext } from "~/lib/staff"; |
| 7 | import { ZONE_COOKIE, validZone, zoneAbbr } from "~/lib/time"; |
| 8 | |
| 9 | export 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. */ |
| 12 | function backTo(raw: string | null): string { |
| 13 | return raw && /^\/(?!\/)[^\s\\]*$/.test(raw) ? raw : "/"; |
| 14 | } |
| 15 | |
| 16 | export 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 | |
| 31 | export 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 | |
| 43 | export 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 | } |