Openstatus www.openstatus.dev

chore: convert timeone to gmt (#550)

authored by

Maximilian Kaske and committed by
GitHub
b36fe1c3 340d94f0

+75 -37
+1
apps/web/package.json
··· 49 49 "cobe": "0.6.3", 50 50 "contentlayer": "0.3.4", 51 51 "date-fns": "2.30.0", 52 + "date-fns-tz": "2.0.0", 52 53 "lucide-react": "0.279.0", 53 54 "luxon": "3.3.0", 54 55 "micro": "10.0.1",
+2 -4
apps/web/src/app/api/og/route.tsx
··· 1 - import { headers } from "next/headers"; 2 1 import { ImageResponse } from "next/server"; 3 2 4 3 import { DESCRIPTION, TITLE } from "@/app/shared-metadata"; 5 4 import { getMonitorListData } from "@/lib/tb"; 5 + import { convertTimezoneToGMT } from "@/lib/timezone"; 6 6 import { 7 7 addBlackListInfo, 8 8 getStatus, ··· 18 18 }; 19 19 20 20 const LIMIT = 40; 21 - const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 22 21 23 22 const interRegular = fetch( 24 23 new URL("../../../public/fonts/Inter-Regular.ttf", import.meta.url), ··· 47 46 ? searchParams.get("monitorId") 48 47 : undefined; 49 48 50 - const headersList = headers(); 51 - const timezone = headersList.get("x-vercel-ip-timezone") || currentTimezone; 49 + const timezone = convertTimezoneToGMT(); 52 50 53 51 // currently, we only show the tracker for a single(!) monitor 54 52 const data =
+17 -18
apps/web/src/app/play/page.tsx
··· 1 - import { headers } from "next/headers"; 1 + import { notFound } from "next/navigation"; 2 2 import * as z from "zod"; 3 3 4 4 import { Label } from "@openstatus/ui"; 5 5 6 6 import { Tracker } from "@/components/tracker"; 7 7 import { getHomeMonitorListData } from "@/lib/tb"; 8 + import { convertTimezoneToGMT, getRequestHeaderTimezone } from "@/lib/timezone"; 8 9 import { TimezoneCombobox } from "./_components/timezone-combobox"; 9 10 10 11 const supportedTimezones = Intl.supportedValuesOf("timeZone"); 11 - const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 12 12 13 13 /** 14 14 * allowed URL search params ··· 22 22 }: { 23 23 searchParams: { [key: string]: string | string[] | undefined }; 24 24 }) { 25 - const headersList = headers(); 26 - const timezone = headersList.get("x-vercel-ip-timezone") || currentTimezone; 27 - 28 25 const search = searchParamsSchema.safeParse(searchParams); 26 + const requestTimezone = getRequestHeaderTimezone(); 29 27 30 - function getDefaultValue() { 31 - if ( 32 - search.success && 33 - search.data.timezone && 34 - supportedTimezones.includes(search.data.timezone) 35 - ) { 36 - return search.data.timezone; 37 - } 38 - return timezone; 28 + if (!search.success) { 29 + return notFound(); 30 + } 31 + 32 + if ( 33 + search.data.timezone && 34 + !supportedTimezones.includes(search.data.timezone) 35 + ) { 36 + return notFound(); 39 37 } 40 38 41 - const defaultValue = getDefaultValue(); 39 + const gmt = convertTimezoneToGMT(search.data.timezone); 42 40 43 - const data = await getHomeMonitorListData({ timezone: defaultValue }); 41 + const data = await getHomeMonitorListData({ timezone: gmt }); 44 42 45 43 return ( 46 44 <div className="relative grid gap-4"> ··· 58 56 name="Ping" 59 57 url="https://www.openstatus.dev/api/ping" 60 58 context="play" 61 - timezone={defaultValue} 62 59 /> 63 60 )} 64 61 </div> 65 62 <div className="mt-6 flex justify-start"> 66 63 <div className="grid items-center gap-1"> 67 64 <Label className="text-muted-foreground text-xs">Timezone</Label> 68 - <TimezoneCombobox defaultValue={defaultValue} /> 65 + <TimezoneCombobox 66 + defaultValue={search.data.timezone || requestTimezone || undefined} 67 + /> 69 68 </div> 70 69 </div> 71 70 </div>
+3 -7
apps/web/src/components/marketing/status-page/tracker-example.tsx
··· 1 1 import { Suspense } from "react"; 2 - import { headers } from "next/headers"; 3 2 import Link from "next/link"; 4 3 5 4 import { Button } from "@openstatus/ui"; 6 5 7 6 import { Tracker } from "@/components/tracker"; 8 7 import { getHomeMonitorListData } from "@/lib/tb"; 9 - 10 - const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 8 + import { convertTimezoneToGMT } from "@/lib/timezone"; 11 9 12 10 export async function TrackerExample() { 13 11 return ( ··· 36 34 } 37 35 38 36 async function ExampleTracker() { 39 - const headersList = headers(); 40 - const timezone = headersList.get("x-vercel-ip-timezone") || currentTimezone; 41 - const data = await getHomeMonitorListData({ timezone }); 37 + const gmt = convertTimezoneToGMT(); 38 + const data = await getHomeMonitorListData({ timezone: gmt }); 42 39 if (!data) return null; 43 40 return ( 44 41 <Tracker ··· 47 44 name="Ping" 48 45 context="play" 49 46 url="https://www.openstatus.dev/api/ping" 50 - timezone={timezone} 51 47 /> 52 48 ); 53 49 }
+3 -7
apps/web/src/components/status-page/monitor.tsx
··· 1 - import { headers } from "next/headers"; 2 1 import type { z } from "zod"; 3 2 4 3 import type { ··· 7 6 } from "@openstatus/db/src/schema"; 8 7 9 8 import { getMonitorListData } from "@/lib/tb"; 9 + import { convertTimezoneToGMT } from "@/lib/timezone"; 10 10 import { Tracker } from "../tracker"; 11 11 12 - const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 13 - 14 12 export const Monitor = async ({ 15 13 monitor, 16 14 statusReports, ··· 18 16 monitor: z.infer<typeof selectPublicMonitorSchema>; 19 17 statusReports: z.infer<typeof selectPublicStatusReportSchemaWithRelation>[]; 20 18 }) => { 21 - const headersList = headers(); 22 - const timezone = headersList.get("x-vercel-ip-timezone") || currentTimezone; 23 - 19 + const gmt = convertTimezoneToGMT(); 24 20 const data = await getMonitorListData({ 25 21 monitorId: String(monitor.id), 26 - timezone, 22 + timezone: gmt, 27 23 }); 28 24 29 25 // TODO: we could handle the `statusReports` here instead of passing it down to the tracker
-1
apps/web/src/components/tracker.tsx
··· 57 57 name: string; 58 58 description?: string; 59 59 context?: "play" | "status-page"; // TODO: we might need to extract those two different use cases - for now it's ok I'd say. 60 - timezone?: string; 61 60 reports?: (StatusReport & { statusReportUpdates: StatusReportUpdate[] })[]; 62 61 } 63 62
+38
apps/web/src/lib/timezone.ts
··· 1 + import { headers } from "next/headers"; 2 + import { getTimezoneOffset } from "date-fns-tz"; 3 + 4 + export function getRequestHeaderTimezone() { 5 + const headersList = headers(); 6 + 7 + /** 8 + * Vercel includes ip timezone to request header 9 + */ 10 + const requestTimezone = headersList.get("x-vercel-ip-timezone"); 11 + 12 + return requestTimezone; 13 + } 14 + 15 + export function convertTimezoneToGMT(timezone?: string) { 16 + const requestTimezone = getRequestHeaderTimezone(); 17 + 18 + /** 19 + * Server region timezone as fallback 20 + */ 21 + const clientTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 22 + 23 + const msOffset = getTimezoneOffset( 24 + timezone || requestTimezone || clientTimezone, 25 + ); 26 + 27 + if (isNaN(msOffset)) return "Etc/UTC"; 28 + 29 + const hrOffset = msOffset / (1000 * 60 * 60); 30 + const offset = hrOffset >= 0 ? `-${hrOffset}` : `+${Math.abs(hrOffset)}`; 31 + 32 + return `Etc/GMT${offset}` as const; 33 + } 34 + 35 + /** 36 + * All supported browser / node timezones 37 + */ 38 + export const supportedTimezones = Intl.supportedValuesOf("timeZone");
+11
pnpm-lock.yaml
··· 221 221 date-fns: 222 222 specifier: 2.30.0 223 223 version: 2.30.0 224 + date-fns-tz: 225 + specifier: 2.0.0 226 + version: 2.0.0(date-fns@2.30.0) 224 227 lucide-react: 225 228 specifier: 0.279.0 226 229 version: 0.279.0(react@18.2.0) ··· 6557 6560 abab: 2.0.6 6558 6561 whatwg-mimetype: 3.0.0 6559 6562 whatwg-url: 11.0.0 6563 + dev: false 6564 + 6565 + /date-fns-tz@2.0.0(date-fns@2.30.0): 6566 + resolution: {integrity: sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ==} 6567 + peerDependencies: 6568 + date-fns: '>=2.0.0' 6569 + dependencies: 6570 + date-fns: 2.30.0 6560 6571 dev: false 6561 6572 6562 6573 /date-fns@2.30.0: