diff --git a/client/src/Hooks/useStatusPageForm.ts b/client/src/Hooks/useStatusPageForm.ts index 8bec86f9b..681a3da6c 100644 --- a/client/src/Hooks/useStatusPageForm.ts +++ b/client/src/Hooks/useStatusPageForm.ts @@ -6,6 +6,7 @@ import { DEFAULT_STATUS_PAGE_THEME_MODE, } from "@/Types/StatusPage"; import type { Monitor } from "@/Types/Monitor"; +import { resolveTimezone } from "@/Utils/TimeUtils"; interface UseStatusPageFormOptions { data?: StatusPage | null; @@ -30,7 +31,7 @@ export const useStatusPageForm = ({ const defaults: StatusPageFormData = { companyName: data?.companyName || "", url: data?.url || generateDefaultUrl(), - timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, + timezone: resolveTimezone(data?.timezone), type: data?.type || ["uptime"], color: data?.color || "#13715B", monitors: data?.monitors || [], diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx index 59ba4f21b..9c4001fae 100644 --- a/client/src/Pages/StatusPage/Status/index.tsx +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -126,6 +126,7 @@ const StatusPageView = () => { {themedRenderer} @@ -149,6 +150,7 @@ const StatusPageView = () => { {themedRenderer} diff --git a/client/src/Pages/StatusPage/Status/themes/StatusPageThemeProvider.tsx b/client/src/Pages/StatusPage/Status/themes/StatusPageThemeProvider.tsx index 2b10b0d9a..f79991c3b 100644 --- a/client/src/Pages/StatusPage/Status/themes/StatusPageThemeProvider.tsx +++ b/client/src/Pages/StatusPage/Status/themes/StatusPageThemeProvider.tsx @@ -13,6 +13,7 @@ import { type StatusPageTheme, type StatusPageThemeMode, } from "@/Types/StatusPage"; +import { resolveTimezone } from "@/Utils/TimeUtils"; import { themeTokens, type StatusPageThemeTokens } from "./tokens"; type ResolvedMode = "light" | "dark"; @@ -21,6 +22,7 @@ interface StatusPageThemeContextValue { theme: StatusPageTheme; mode: ResolvedMode; tokens: StatusPageThemeTokens; + timezone: string; } const StatusPageThemeContext = createContext(null); @@ -33,6 +35,7 @@ const resolveSystemMode = (): ResolvedMode => { interface Props { theme?: StatusPageTheme; themeMode?: StatusPageThemeMode; + timezone?: string; /** * When true, paint the document body with the theme background so it * covers beyond the provider's wrapper (useful on the public route @@ -54,12 +57,14 @@ interface Props { export const StatusPageThemeProvider = ({ theme, themeMode, + timezone, paintBody = false, transparent = false, children, }: Props) => { const resolvedTheme = resolveStatusPageTheme(theme); const resolvedThemeMode = resolveStatusPageThemeMode(themeMode); + const resolvedTimezone = resolveTimezone(timezone); const [systemMode, setSystemMode] = useState(resolveSystemMode); @@ -107,8 +112,13 @@ export const StatusPageThemeProvider = ({ const tokens = themeTokens[resolvedTheme][resolvedMode]; const value = useMemo( - () => ({ theme: resolvedTheme, mode: resolvedMode, tokens }), - [resolvedTheme, resolvedMode, tokens] + () => ({ + theme: resolvedTheme, + mode: resolvedMode, + tokens, + timezone: resolvedTimezone, + }), + [resolvedTheme, resolvedMode, tokens, resolvedTimezone] ); return ( @@ -136,6 +146,7 @@ export const useStatusPageTheme = (): StatusPageThemeContextValue => { theme: DEFAULT_STATUS_PAGE_THEME, mode: resolveSystemMode(), tokens: themeTokens[DEFAULT_STATUS_PAGE_THEME][resolveSystemMode()], + timezone: resolveTimezone(), }; } return ctx; diff --git a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx index 58ae86d44..fe56155c3 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx @@ -4,11 +4,10 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import type { SxProps, Theme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; -import { useSelector } from "react-redux"; -import type { RootState } from "@/store"; import type { CheckSnapshot } from "@/Types/Check"; import { formatDateWithTz } from "@/Utils/TimeUtils"; import { MAX_RECENT_CHECKS } from "@/Types/Monitor"; +import { useStatusPageTheme } from "../StatusPageThemeProvider"; const CELLS = MAX_RECENT_CHECKS; @@ -30,7 +29,7 @@ const classify = (check: CheckSnapshot): Exclude => { export const ThemedHeatmap = ({ checks, containerSx, cellSx }: Props) => { const { t } = useTranslation(); - const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + const { timezone } = useStatusPageTheme(); const source = checks.slice(-CELLS); const padded: (CheckSnapshot | null)[] = [ @@ -68,11 +67,7 @@ export const ThemedHeatmap = ({ checks, containerSx, cellSx }: Props) => { variant="caption" sx={{ opacity: 0.8 }} > - {formatDateWithTz( - check.createdAt, - "ddd, MMMM D, YYYY, HH:mm A", - uiTimezone - )} + {formatDateWithTz(check.createdAt, "ddd, MMMM D, YYYY, HH:mm A", timezone)} ); diff --git a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx index 8408a22ef..9d06a99ac 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx @@ -5,11 +5,10 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import type { SxProps, Theme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; -import { useSelector } from "react-redux"; -import type { RootState } from "@/store"; import type { CheckSnapshot } from "@/Types/Check"; import { formatDateWithTz } from "@/Utils/TimeUtils"; import { MAX_RECENT_CHECKS } from "@/Types/Monitor"; +import { useStatusPageTheme } from "../StatusPageThemeProvider"; const CELLS = MAX_RECENT_CHECKS; const MIN_HEIGHT_PCT = 6; @@ -34,7 +33,7 @@ export const ThemedHistogram = ({ statsGap = 1, }: Props) => { const { t } = useTranslation(); - const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + const { timezone } = useStatusPageTheme(); const { padded, max, avg, peak } = useMemo(() => { const source = checks.slice(-CELLS); @@ -83,7 +82,7 @@ export const ThemedHistogram = ({ {formatDateWithTz( check.createdAt, "ddd, MMMM D, YYYY, HH:mm A", - uiTimezone + timezone )} diff --git a/client/src/Utils/TimeUtils.ts b/client/src/Utils/TimeUtils.ts index cffdd5440..5b63de87e 100644 --- a/client/src/Utils/TimeUtils.ts +++ b/client/src/Utils/TimeUtils.ts @@ -15,12 +15,30 @@ export const MS_PER_HOUR = 60 * MS_PER_MINUTE; export const MS_PER_DAY = 24 * MS_PER_HOUR; export const MS_PER_WEEK = MS_PER_DAY * 7; -export const formatDateWithTz = (timestamp: string, format: string, timezone: string) => { +const resolveSystemTimezone = (): string => { + try { + return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; + } catch { + return "UTC"; + } +}; + +export const SYSTEM_TIMEZONE = resolveSystemTimezone(); + +export const resolveTimezone = (preferred?: string | null): string => + preferred?.trim() || SYSTEM_TIMEZONE; + +export const formatDateWithTz = ( + timestamp: string, + format: string, + timezone?: string +) => { if (!timestamp) { return "Unknown time"; } + const tz = resolveTimezone(timezone); try { - return dayjs(timestamp).tz(timezone).format(format); + return dayjs(timestamp).tz(tz).format(format); } catch { return dayjs(timestamp).utc().format(format); }