From ed3645c3d0ae63706b805769f7cc8c8a26402d32 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Tue, 28 Apr 2026 16:38:35 -0400 Subject: [PATCH 1/2] refactor(status-page): drop legacy single-design renderer and CSS overrides The 4 themed renderers (refined, modern, bold, editorial) cover every StatusPageTheme value and the model defaults to "refined", so the legacy fallback path in Status/index.tsx was unreachable in practice. Removing it lets us delete: - theme-overrides.css (the only remaining CSS file in StatusPage) - StatusBar / MonitorsList / InfrastructureMetrics (only the legacy fallback consumed them) - The .sp-* className hooks that wired CSS rules into the components Pure sx via the per-theme styles.ts recipes is now the single source of truth for every status page surface. Net diff is -801 / +42. Status/index.tsx now resolves a renderer from the theme map (falling back to the default theme) and keeps the public-route and admin-preview branches it already had. --- .../Components/HeaderStatusPageControls.tsx | 1 - .../Components/InfrastructureMetrics.tsx | 246 ------------------ .../Status/Components/MonitorsList.tsx | 174 ------------- .../Status/Components/StatusBar.tsx | 173 ------------ client/src/Pages/StatusPage/Status/index.tsx | 134 +++------- .../Status/themes/StatusPageThemeProvider.tsx | 1 - .../Status/themes/theme-overrides.css | 156 ----------- 7 files changed, 42 insertions(+), 843 deletions(-) delete mode 100644 client/src/Pages/StatusPage/Status/Components/InfrastructureMetrics.tsx delete mode 100644 client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx delete mode 100644 client/src/Pages/StatusPage/Status/Components/StatusBar.tsx delete mode 100644 client/src/Pages/StatusPage/Status/themes/theme-overrides.css diff --git a/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx b/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx index 1371fa722..2cc2395bb 100644 --- a/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx +++ b/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx @@ -37,7 +37,6 @@ export const HeaderStatusPageControls = ({ > { - const theme = useTheme(); - return ( - - - {label} - - {value} - - ); -}; - -interface MetricItemProps { - label: string; - progress: number; - details?: MetricDetail[]; -} - -const MetricItem = ({ label, progress, details }: MetricItemProps) => { - const theme = useTheme(); - const isSmall = useMediaQuery(theme.breakpoints.down("md")); - return ( - - - - {label} - - {details && details.length > 0 && ( - - {details.map((detail) => ( - - ))} - - )} - - ); -}; - -type LatestCheck = NonNullable[number]; - -const buildCpuMetric = ( - check: LatestCheck, - t: (key: string) => string -): MetricConfig | null => { - if (!check.cpu || typeof check.cpu.usage_percent !== "number") { - return null; - } - const usagePercent = (check.cpu.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER; - return { - key: "cpu", - label: t("pages.statusPages.monitorsList.infrastructure.cpu"), - hasData: true, - progress: usagePercent, - details: [ - { - label: t("pages.statusPages.monitorsList.infrastructure.usage"), - value: `${usagePercent.toFixed(2)}%`, - }, - ], - }; -}; - -const buildMemoryMetric = ( - check: LatestCheck, - t: (key: string) => string -): MetricConfig | null => { - if ( - !check.memory || - typeof check.memory.usage_percent !== "number" || - typeof check.memory.used_bytes !== "number" || - typeof check.memory.total_bytes !== "number" - ) { - return null; - } - return { - key: "memory", - label: t("pages.statusPages.monitorsList.infrastructure.memory"), - hasData: true, - progress: (check.memory.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER, - details: [ - { - label: t("pages.statusPages.monitorsList.infrastructure.used"), - value: prettyBytes(check.memory.used_bytes ?? 0), - }, - { - label: t("pages.statusPages.monitorsList.infrastructure.total"), - value: prettyBytes(check.memory.total_bytes ?? 0), - }, - ], - }; -}; - -const buildDiskMetric = ( - check: LatestCheck, - t: (key: string) => string -): MetricConfig | null => { - if (!check.disk || check.disk.length === 0) { - return null; - } - const disks = check.disk; - const avgUsagePercent = - (disks.reduce((acc, disk) => acc + (disk?.usage_percent ?? 0), 0) / disks.length) * - PERCENTAGE_MULTIPLIER; - const totalUsedBytes = disks.reduce((acc, disk) => acc + (disk?.used_bytes ?? 0), 0); - const totalTotalBytes = disks.reduce((acc, disk) => acc + (disk?.total_bytes ?? 0), 0); - - return { - key: "disk", - label: t("pages.statusPages.monitorsList.infrastructure.disk"), - hasData: true, - progress: avgUsagePercent, - details: [ - { - label: t("pages.statusPages.monitorsList.infrastructure.used"), - value: prettyBytes(totalUsedBytes), - }, - { - label: t("pages.statusPages.monitorsList.infrastructure.total"), - value: prettyBytes(totalTotalBytes), - }, - ], - }; -}; - -export const InfrastructureMetrics = ({ monitor }: { monitor: StatusPageMonitor }) => { - const theme = useTheme(); - const { t } = useTranslation(); - - const latestCheck = monitor.recentChecks?.[0] ?? monitor.checks?.[0]; - - if (!latestCheck) { - return ( - - {t("pages.statusPages.monitorsList.noData")} - - ); - } - - const metrics: MetricConfig[] = [ - buildCpuMetric(latestCheck, t), - buildMemoryMetric(latestCheck, t), - buildDiskMetric(latestCheck, t), - ].filter((m): m is MetricConfig => m !== null); - - if (metrics.length === 0) { - return ( - - {t("pages.statusPages.monitorsList.noData")} - - ); - } - - return ( - - {metrics.map(({ key, label, progress, details }) => ( - - ))} - - ); -}; diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx deleted file mode 100644 index 01e7d1975..000000000 --- a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import Stack from "@mui/material/Stack"; -import { useTranslation } from "react-i18next"; -import Box from "@mui/material/Box"; -import Typography from "@mui/material/Typography"; -import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/common"; -import { StatusLabel, BaseBox } from "@/Components/design-elements"; -import { SwitchComponent } from "@/Components/inputs"; -import { InfrastructureMetrics } from "@/Pages/StatusPage/Status/Components/InfrastructureMetrics"; - -import { useTheme, type Theme } from "@mui/material/styles"; -import { useSelector } from "react-redux"; -import { useState } from "react"; -import type { Monitor } from "@/Types/Monitor"; -import type { StatusPage } from "@/Types/StatusPage"; -import { getMonitorTypeLabel } from "@/Types/StatusPage"; -import type { RootState } from "@/Types/state"; -import { LAYOUT, SPACING } from "@/Utils/Theme/constants"; - -interface StatusPageMonitor extends Monitor { - checks?: Monitor["recentChecks"]; -} - -interface MonitorsListProps { - statusPage: StatusPage; - monitors: StatusPageMonitor[]; -} - -const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => { - const bg = - monitorType === "hardware" ? theme.palette.info.light : theme.palette.success.light; - return { - backgroundColor: bg, - color: theme.palette.background.paper, - padding: `${theme.spacing(SPACING.SM)} ${theme.spacing(SPACING.LG)}`, - borderRadius: theme.shape.borderRadius, - }; -}; - -const MonitorHeader = ({ - monitor, - showURL, -}: { - monitor: StatusPageMonitor; - showURL: boolean; -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - - return ( - - - - - {monitor.name} - - - {getMonitorTypeLabel(monitor.type, t)} - - - {showURL && monitor.url && ( - - {monitor.url} - - )} - - - - ); -}; - -const MonitorContent = ({ - monitor, - statusPage, - chartType, -}: { - monitor: StatusPageMonitor; - statusPage: StatusPage; - chartType: string; -}) => { - const theme = useTheme(); - - if (monitor.type === "hardware") { - if (statusPage.showInfrastructure === false) return null; - return ; - } - - if (statusPage.showCharts === false) return null; - - const checks = monitor.checks?.slice().reverse() ?? []; - return ( - - {chartType === "histogram" ? ( - - ) : ( - - )} - - ); -}; - -export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { - const theme = useTheme(); - const { t } = useTranslation(); - const showURL = useSelector((state: RootState) => state.ui?.showURL); - const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram"); - - return ( - - {statusPage.showCharts && ( - - {t("pages.statusPages.monitorsList.chartTypeHeatmap")} - setChartType(e.target.checked ? "histogram" : "heatmap")} - /> - - {t("pages.statusPages.monitorsList.chartTypeHistogram")} - - - )} - - {monitors.map((monitor) => ( - - - - - ))} - - ); -}; diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx b/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx deleted file mode 100644 index 49f70d952..000000000 --- a/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { - AlertTriangle, - CircleCheck, - CircleX, - Loader, - PauseCircle, - ShieldAlert, - Wrench, -} from "lucide-react"; -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; - -import { useTranslation } from "react-i18next"; -import { useTheme } from "@mui/material"; -import type { Monitor, MonitorStatus } from "@/Types/Monitor"; - -type SemanticTone = "up" | "down" | "warn" | "info"; - -interface StatusDisplay { - icon: JSX.Element; - msg?: string; - tone: SemanticTone; -} - -const getMonitorStatus = (monitors: Monitor[], t: Function): StatusDisplay => { - if (monitors.length === 0) { - return { - msg: t("pages.statusPages.statusBar.noMonitors"), - tone: "warn", - icon: , - }; - } - - const allOf = (...statuses: MonitorStatus[]) => - monitors.every((m) => statuses.includes(m.status)); - const someOf = (...statuses: MonitorStatus[]) => - monitors.some((m) => statuses.includes(m.status)); - const noneOf = (...statuses: MonitorStatus[]) => - monitors.every((m) => !statuses.includes(m.status)); - - if (allOf("up")) { - return { - msg: t("pages.statusPages.statusBar.allUp"), - tone: "up", - icon: , - }; - } - if (allOf("breached")) { - return { - msg: t("pages.statusPages.statusBar.allBreached"), - tone: "down", - icon: , - }; - } - if (allOf("maintenance")) { - return { - msg: t("pages.statusPages.statusBar.allMaintenance"), - tone: "warn", - icon: , - }; - } - if (allOf("down")) { - return { - msg: t("pages.statusPages.statusBar.allDown"), - tone: "down", - icon: , - }; - } - if (allOf("paused")) { - return { - msg: t("pages.statusPages.statusBar.allPaused"), - tone: "warn", - icon: , - }; - } - if (allOf("initializing")) { - return { - msg: t("pages.statusPages.statusBar.allInitializing"), - tone: "warn", - icon: , - }; - } - - // Breached takes highest priority in mixed states - if (someOf("breached") && someOf("down")) { - return { - msg: t("pages.statusPages.statusBar.breachedAndDown"), - tone: "down", - icon: , - }; - } - if (someOf("breached")) { - return { - msg: t("pages.statusPages.statusBar.breached"), - tone: "down", - icon: , - }; - } - if (someOf("maintenance") && someOf("down")) { - return { - msg: t("pages.statusPages.statusBar.maintenanceAndDown"), - tone: "down", - icon: , - }; - } - if (someOf("maintenance") && noneOf("down")) { - return { - msg: t("pages.statusPages.statusBar.maintenance"), - tone: "warn", - icon: , - }; - } - if (someOf("down")) { - return { - msg: t("pages.statusPages.statusBar.degraded"), - tone: "warn", - icon: , - }; - } - if (someOf("paused")) { - return { - msg: t("pages.statusPages.statusBar.partiallyPaused"), - tone: "warn", - icon: , - }; - } - if (someOf("initializing")) { - return { - msg: t("pages.statusPages.statusBar.initializing"), - tone: "info", - icon: , - }; - } - - return { - msg: t("pages.statusPages.statusBar.unknown"), - tone: "warn", - icon: , - }; -}; - -const toneToCssVar: Record = { - up: "var(--sp-up)", - down: "var(--sp-down)", - warn: "var(--sp-warn)", - info: "var(--sp-up)", -}; - -interface StatusBarProps { - monitors: Monitor[]; -} - -export const StatusBar = ({ monitors }: StatusBarProps) => { - const theme = useTheme(); - const { t } = useTranslation(); - const monitorsStatus = getMonitorStatus(monitors, t); - - return ( - - {monitorsStatus.icon} - {monitorsStatus.msg} - - ); -}; diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx index a4864d748..992b2523f 100644 --- a/client/src/Pages/StatusPage/Status/index.tsx +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -1,18 +1,20 @@ import { BasePage, BaseFallback } from "@/Components/design-elements"; -import { StatusBar } from "@/Pages/StatusPage/Status/Components/StatusBar"; -import { MonitorsList } from "@/Pages/StatusPage/Status/Components/MonitorsList"; import Typography from "@mui/material/Typography"; import { Link } from "react-router-dom"; import Stack from "@mui/material/Stack"; -import Box from "@mui/material/Box"; -import { useMediaQuery, useTheme } from "@mui/material"; +import { useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useIsAdmin } from "@/Hooks/useIsAdmin"; import { useLocation, useParams } from "react-router-dom"; import { useGet } from "@/Hooks/UseApi"; import type { Monitor } from "@/Types/Monitor"; -import type { StatusPage, StatusPageResponse, StatusPageTheme } from "@/Types/StatusPage"; +import { + DEFAULT_STATUS_PAGE_THEME, + type StatusPage, + type StatusPageResponse, + type StatusPageTheme, +} from "@/Types/StatusPage"; import { HeaderStatusPageControls } from "./Components/HeaderStatusPageControls"; import { StatusPageThemeProvider } from "./themes/StatusPageThemeProvider"; import { RefinedStatusPage } from "./themes/refined/RefinedStatusPage"; @@ -26,8 +28,9 @@ type ThemedRendererProps = { monitors: (Monitor & { checks?: Monitor["recentChecks"] })[]; }; -const THEMED_RENDERERS: Partial< - Record> +const THEMED_RENDERERS: Record< + StatusPageTheme, + React.ComponentType > = { refined: RefinedStatusPage, modern: ModernStatusPage, @@ -41,7 +44,6 @@ const StatusPageView = () => { const { url } = useParams(); const isAdmin = useIsAdmin(); const location = useLocation(); - const isSmall = useMediaQuery(theme.breakpoints.down("md")); const isPublic = location.pathname.startsWith("/status/public"); const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null; @@ -86,103 +88,51 @@ const StatusPageView = () => { ); } - let sx: React.CSSProperties = {}; - if (isPublic) { - sx.paddingTop = theme.spacing(20); - sx.paddingLeft = isSmall ? "5vw" : "20vw"; - sx.paddingRight = isSmall ? "5vw" : "20vw"; - } - - const logoSrc = statusPage.logo?.data - ? `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}` - : null; - - const ThemedRenderer = statusPage.theme - ? THEMED_RENDERERS[statusPage.theme] - : undefined; - const themedRenderer = ThemedRenderer ? ( + const ThemedRenderer = + THEMED_RENDERERS[statusPage.theme ?? DEFAULT_STATUS_PAGE_THEME] ?? + THEMED_RENDERERS[DEFAULT_STATUS_PAGE_THEME]; + const themedRenderer = ( - ) : null; + ); - if (themedRenderer) { - // Public route: render directly on the viewport, themed background covers everything. - if (isPublic) { - return ( - - {themedRenderer} - - ); - } - - // Admin preview: wrap in a mock browser frame inside the admin shell. - const publicUrl = `${window.location.origin}/status/public/${statusPage.url}`; + // Public route: render directly on the viewport, themed background covers everything. + if (isPublic) { return ( - - - - {themedRenderer} - - + {themedRenderer} + ); } + // Admin preview: wrap in a mock browser frame inside the admin shell. + const publicUrl = `${window.location.origin}/status/public/${statusPage.url}`; return ( - - + - - {logoSrc && ( - - )} - - - - + {themedRenderer} + + ); }; diff --git a/client/src/Pages/StatusPage/Status/themes/StatusPageThemeProvider.tsx b/client/src/Pages/StatusPage/Status/themes/StatusPageThemeProvider.tsx index 294198330..ffff8df2e 100644 --- a/client/src/Pages/StatusPage/Status/themes/StatusPageThemeProvider.tsx +++ b/client/src/Pages/StatusPage/Status/themes/StatusPageThemeProvider.tsx @@ -13,7 +13,6 @@ import { type StatusPageThemeMode, } from "@/Types/StatusPage"; import { themeTokens, toCssVars, type StatusPageThemeTokens } from "./tokens"; -import "./theme-overrides.css"; type ResolvedMode = "light" | "dark"; diff --git a/client/src/Pages/StatusPage/Status/themes/theme-overrides.css b/client/src/Pages/StatusPage/Status/themes/theme-overrides.css deleted file mode 100644 index b66257d18..000000000 --- a/client/src/Pages/StatusPage/Status/themes/theme-overrides.css +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Scoped styling for the public status page themes. - * Every selector is prefixed with [data-status-theme="..."] so these styles - * only apply inside the StatusPageThemeProvider subtree. - * - * Colour tokens come from CSS custom properties set by the provider - * (see tokens.ts → toCssVars). This file adds structural and surface rules - * that a plain token swap can't express. - */ - -/* --- shared: push tokens onto MUI surfaces inside the themed tree --- */ -[data-status-theme] { - color: var(--sp-text); -} - -[data-status-theme] .sp-card, -[data-status-theme] .MuiBox-root.sp-card { - background-color: var(--sp-surface) !important; - border-color: var(--sp-border) !important; - border-radius: var(--sp-radius) !important; - color: var(--sp-text); -} - -[data-status-theme] .sp-heading { - color: var(--sp-text); -} - -[data-status-theme] .sp-badge { - background-color: var(--sp-up-soft) !important; - color: var(--sp-up) !important; - border-radius: 999px; - padding: 2px 10px; - font-weight: 600; - letter-spacing: 0.04em; -} - -/* Status bar / hero */ -[data-status-theme] .sp-hero { - border-radius: var(--sp-radius); -} - -/* --- refined: soft shadow, small accent on card --- */ -[data-status-theme="refined"] .sp-card { - box-shadow: - 0 1px 2px rgba(10, 16, 32, 0.06), - 0 6px 18px rgba(10, 16, 32, 0.08) !important; -} -[data-status-theme="refined"] .sp-hero-title { - font-weight: 600; - letter-spacing: -0.01em; -} - -/* --- modern: stronger shadow, larger radius, pulse dot + staggered fade --- */ -[data-status-theme="modern"] .sp-card { - box-shadow: - 0 2px 4px rgba(16, 24, 40, 0.04), - 0 12px 32px rgba(16, 24, 40, 0.06) !important; - border-radius: 16px !important; - opacity: 0; - transform: translateY(8px); - animation: sp-card-fadein 0.5s forwards; -} -[data-status-theme="modern"] .sp-card:nth-of-type(1) { - animation-delay: 0.05s; -} -[data-status-theme="modern"] .sp-card:nth-of-type(2) { - animation-delay: 0.1s; -} -[data-status-theme="modern"] .sp-card:nth-of-type(3) { - animation-delay: 0.15s; -} -[data-status-theme="modern"] .sp-card:nth-of-type(4) { - animation-delay: 0.2s; -} -[data-status-theme="modern"] .sp-card:nth-of-type(5) { - animation-delay: 0.25s; -} -[data-status-theme="modern"] .sp-card:nth-of-type(n + 6) { - animation-delay: 0.3s; -} -@keyframes sp-card-fadein { - to { - opacity: 1; - transform: translateY(0); - } -} -[data-status-theme="modern"] .sp-hero-title { - font-size: 22px; - font-weight: 700; - letter-spacing: -0.02em; -} - -/* --- bold: large type, conic-gradient hero accent when no logo --- */ -[data-status-theme="bold"] .sp-hero { - background: - linear-gradient(135deg, var(--sp-up-soft), transparent 60%), var(--sp-surface) !important; - border: 1px solid var(--sp-border) !important; -} -[data-status-theme="bold"] .sp-hero-title { - font-size: 32px; - font-weight: 800; - letter-spacing: -0.02em; - line-height: 1.1; - color: var(--sp-text) !important; -} -[data-status-theme="bold"] .sp-card { - border-radius: 18px !important; - border: 1px solid var(--sp-border) !important; -} - -/* --- editorial: serif headings, hairline dividers instead of cards --- */ -[data-status-theme="editorial"] .sp-heading { - font-family: Georgia, "Iowan Old Style", "Palatino Linotype", serif; - letter-spacing: -0.01em; -} -[data-status-theme="editorial"] .sp-card { - background-color: transparent !important; - border: 0 !important; - border-bottom: 1px solid var(--sp-border) !important; - border-radius: 0 !important; - box-shadow: none !important; - padding-left: 0 !important; - padding-right: 0 !important; -} -[data-status-theme="editorial"] .sp-card:last-of-type { - border-bottom: 0 !important; -} -[data-status-theme="editorial"] .sp-hero { - background: transparent !important; - color: var(--sp-text) !important; - border-bottom: 2px solid var(--sp-text) !important; - border-radius: 0 !important; - box-shadow: none !important; -} -[data-status-theme="editorial"] .sp-hero-title { - font-family: Georgia, "Iowan Old Style", "Palatino Linotype", serif; - font-weight: 700; -} -[data-status-theme="editorial"] .sp-badge { - background-color: transparent !important; - color: var(--sp-text-muted) !important; - border: 1px solid var(--sp-border) !important; - border-radius: 0 !important; - text-transform: uppercase; - font-size: 10px; - letter-spacing: 0.12em; -} - -/* Reduce motion */ -@media (prefers-reduced-motion: reduce) { - [data-status-theme] .sp-card { - animation: none; - opacity: 1; - transform: none; - } -} From bab65770ce3c074ae9341e5483cdb7d6ade14337 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Tue, 28 Apr 2026 16:52:16 -0400 Subject: [PATCH 2/2] refactor(status-page): simplify renderer lookup, extract route prefix, memoize wrapper style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four small follow-ups from a code review pass: 1. Drop the redundant `?? THEMED_RENDERERS[DEFAULT_STATUS_PAGE_THEME]` fallback in Status/index.tsx — the map is now a total Record, so the first lookup cannot return undefined. 2. Extract the literal "/status/public" into PUBLIC_STATUS_PAGE_PREFIX in Types/StatusPage.ts and reuse from the three runtime call sites (Status/index.tsx, HeaderStatusPageControls.tsx, StatusPagesTable.tsx). The Routes/index.tsx route definition keeps its string literal — the route table is otherwise all literals and the prefix only matters when it's interpolated. 3. Delete two narrating comments above the public/admin branches in Status/index.tsx — the JSX is self-describing. 4. Move the inline `style` object construction in StatusPageThemeProvider into the existing useMemo so the wrapper div receives a stable style reference. The previous spread defeated the surrounding memoization on every render. --- .../Components/HeaderStatusPageControls.tsx | 4 ++-- client/src/Pages/StatusPage/Status/index.tsx | 11 ++++------ .../Status/themes/StatusPageThemeProvider.tsx | 22 ++++++++++--------- .../Components/StatusPagesTable.tsx | 4 ++-- client/src/Types/StatusPage.ts | 2 ++ 5 files changed, 22 insertions(+), 21 deletions(-) diff --git a/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx b/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx index 2cc2395bb..ba3a5940d 100644 --- a/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx +++ b/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx @@ -8,7 +8,7 @@ import { Settings, ExternalLink } from "lucide-react"; import { useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import type { StatusPage } from "@/Types/StatusPage"; +import { PUBLIC_STATUS_PAGE_PREFIX, type StatusPage } from "@/Types/StatusPage"; interface HeaderStatusPageControlsProps { isAdmin: boolean; @@ -50,7 +50,7 @@ export const HeaderStatusPageControls = ({ { window.open( - `/status/public/${statusPage.url}`, + `${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`, "_blank", "noopener,noreferrer" ); diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx index 992b2523f..4757de5a3 100644 --- a/client/src/Pages/StatusPage/Status/index.tsx +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -11,6 +11,7 @@ import { useGet } from "@/Hooks/UseApi"; import type { Monitor } from "@/Types/Monitor"; import { DEFAULT_STATUS_PAGE_THEME, + PUBLIC_STATUS_PAGE_PREFIX, type StatusPage, type StatusPageResponse, type StatusPageTheme, @@ -44,7 +45,7 @@ const StatusPageView = () => { const { url } = useParams(); const isAdmin = useIsAdmin(); const location = useLocation(); - const isPublic = location.pathname.startsWith("/status/public"); + const isPublic = location.pathname.startsWith(PUBLIC_STATUS_PAGE_PREFIX); const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null; @@ -88,9 +89,7 @@ const StatusPageView = () => { ); } - const ThemedRenderer = - THEMED_RENDERERS[statusPage.theme ?? DEFAULT_STATUS_PAGE_THEME] ?? - THEMED_RENDERERS[DEFAULT_STATUS_PAGE_THEME]; + const ThemedRenderer = THEMED_RENDERERS[statusPage.theme ?? DEFAULT_STATUS_PAGE_THEME]; const themedRenderer = ( { /> ); - // Public route: render directly on the viewport, themed background covers everything. if (isPublic) { return ( { ); } - // Admin preview: wrap in a mock browser frame inside the admin shell. - const publicUrl = `${window.location.origin}/status/public/${statusPage.url}`; + const publicUrl = `${window.location.origin}${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`; return ( toCssVars(tokens) as React.CSSProperties, [tokens]); + const style = useMemo( + () => + transparent + ? (toCssVars(tokens) as React.CSSProperties) + : { + ...(toCssVars(tokens) as React.CSSProperties), + background: tokens.bg, + minHeight: "100vh", + }, + [tokens, transparent] + ); const value = useMemo( () => ({ theme: resolvedTheme, mode: resolvedMode, tokens }), @@ -118,15 +128,7 @@ export const StatusPageThemeProvider = ({ data-status-theme={resolvedTheme} data-status-mode={resolvedMode} data-status-transparent={transparent ? "" : undefined} - style={ - transparent - ? (style as React.CSSProperties) - : { - ...style, - background: tokens.bg, - minHeight: "100vh", - } - } + style={style} > {children} diff --git a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx index 8f72931d6..34daf203c 100644 --- a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx +++ b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx @@ -9,7 +9,7 @@ import { useTranslation } from "react-i18next"; import { useTheme } from "@mui/material/styles"; import { useNavigate } from "react-router-dom"; -import type { StatusPage } from "@/Types/StatusPage"; +import { PUBLIC_STATUS_PAGE_PREFIX, type StatusPage } from "@/Types/StatusPage"; interface StatusPagesTableProps { data: StatusPage[]; @@ -50,7 +50,7 @@ export const StatusPagesTable = ({ const handleUrlClick = (e: React.MouseEvent, row: StatusPage) => { if (row.isPublished) { e.stopPropagation(); - const url = `/status/public/${row.url}`; + const url = `${PUBLIC_STATUS_PAGE_PREFIX}/${row.url}`; window.open(url, "_blank", "noopener,noreferrer"); } }; diff --git a/client/src/Types/StatusPage.ts b/client/src/Types/StatusPage.ts index 6388ccfd9..5b8c4acfa 100644 --- a/client/src/Types/StatusPage.ts +++ b/client/src/Types/StatusPage.ts @@ -29,6 +29,8 @@ export const STATUS_PAGE_THEME_MODES = ["auto", "light", "dark"] as const; export type StatusPageThemeMode = (typeof STATUS_PAGE_THEME_MODES)[number]; export const DEFAULT_STATUS_PAGE_THEME_MODE: StatusPageThemeMode = "auto"; +export const PUBLIC_STATUS_PAGE_PREFIX = "/status/public"; + export interface StatusPage { id: string; userId: string;