feat(client): PageHeader on every page; remove sidebar bottom menu

- New PageHeader component (variant D — grouped icon-pill toolbar):
  big title + muted description on the left, Support / Docs /
  Changelog as a 3-icon segmented pill on the right with tooltips.
  External links open in a new tab.
- BasePage / BasePageWithStates / MonitorBasePageWithStates accept
  a headerKey prop; when set, render PageHeader pulling
  pages.<headerKey>.header.title and .description from i18n.
  When unset, falls back to the existing breadcrumb (no behavior
  change for not-yet-migrated pages).
- All top-level pages (uptime, infrastructure, pageSpeed,
  notifications, maintenance, statusPages, incidents, logs, checks,
  account, settings) now pass their headerKey.
- Sidebar: removed the bottom menu (Support / Discussions / Docs /
  Changelog) since those links now live in the page header.
This commit is contained in:
gorkem-bwl committed 2026-04-25 23:27:22 -04:00
1 parent 031a6fdf3c
commit 262c4eee93
10 files changed
+212 -39

No files matched your search

@@ -10,6 +10,7 @@ import {
} from "@/Components/design-elements/Fallback";
import { EmptyState } from "@/Components/design-elements/EmptyState";
import { Breadcrumb } from "@/Components/design-elements/Breadcrumb";
import { PageHeader } from "@/Components/design-elements/PageHeader";
import CircularProgress from "@mui/material/CircularProgress";
import { HeaderAuthControls } from "@/Pages/Auth/components/HeaderAuthControls";
@@ -21,9 +22,16 @@ import { Typography } from "@mui/material";
export const PageSpeedKeyPriorityFallback = () => {
const theme = useTheme();
const { t } = useTranslation();
const isDark = theme.palette.mode === "dark";
const alertBg = isDark ? "rgba(245, 158, 11, 0.08)" : "#FFFAEB";
const alertBorder = isDark ? "rgba(245, 158, 11, 0.45)" : "#F59E0B";
const alertIcon = isDark ? "#FBBF24" : "#F59E0B";
return (
<EmptyState
title="PageSpeed monitor needs an API key"
fullscreen
title={t("pages.pageSpeed.fallback.title")}
description={t("pages.pageSpeed.fallback.description")}
alert={
<Stack
direction="row"
@@ -33,15 +41,15 @@ export const PageSpeedKeyPriorityFallback = () => {
width: "100%",
p: theme.spacing(LAYOUT.MD),
borderRadius: 1,
border: `1px solid ${theme.palette.warning.main}`,
backgroundColor: theme.palette.warning.light,
border: `1px solid ${alertBorder}`,
backgroundColor: alertBg,
textAlign: "left",
}}
>
<Box
component="span"
sx={{
color: theme.palette.warning.dark,
color: alertIcon,
fontSize: 18,
lineHeight: 1,
mt: "2px",
@@ -80,6 +88,9 @@ interface BasePageProps extends StackProps {
error?: boolean;
children: React.ReactNode;
breadcrumbOverride?: string[];
headerKey?: string;
backTo?: string;
backLabel?: string;
}
export const BasePage = ({
@@ -87,9 +98,13 @@ export const BasePage = ({
error,
children,
breadcrumbOverride,
headerKey,
backTo,
backLabel,
...props
}: BasePageProps) => {
const theme = useTheme();
const { t } = useTranslation();
if (loading) {
return (
@@ -120,7 +135,16 @@ export const BasePage = ({
spacing={theme.spacing(LAYOUT.LG)}
{...props}
>
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
{headerKey ? (
<PageHeader
title={t(`pages.${headerKey}.header.title`)}
description={t(`pages.${headerKey}.header.description`)}
backTo={backTo}
backLabel={backLabel}
/>
) : (
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
)}
{children}
</Stack>
);
@@ -130,11 +154,12 @@ interface BasePageWithStatesProps extends StackProps {
loading: boolean;
error: any;
totalCount: number;
bullets: string[] | unknown;
description?: string;
page: string;
actionButtonText: string;
actionLink: string;
children: React.ReactNode;
headerKey?: string;
}
export const BasePageWithStates = ({
@@ -142,10 +167,11 @@ export const BasePageWithStates = ({
error,
totalCount,
page,
bullets,
description,
actionButtonText,
actionLink,
children,
headerKey,
...props
}: BasePageWithStatesProps) => {
const showLoading = loading && totalCount === 0;
@@ -153,7 +179,7 @@ export const BasePageWithStates = ({
if (!loading && totalCount === 0) {
return (
<EmptyFallback
bullets={bullets}
description={description}
title={page}
actionButtonText={actionButtonText}
actionLink={actionLink}
@@ -165,6 +191,7 @@ export const BasePageWithStates = ({
<BasePage
loading={showLoading}
error={error}
headerKey={headerKey}
{...props}
>
{children}
@@ -180,6 +207,7 @@ interface MonitorBasePageWithStatesProps extends StackProps {
actionLink?: string;
children: React.ReactNode;
priorityFallback?: React.ReactNode;
headerKey?: string;
}
export const MonitorBasePageWithStates = ({
@@ -190,6 +218,7 @@ export const MonitorBasePageWithStates = ({
actionLink,
children,
priorityFallback,
headerKey,
...props
}: MonitorBasePageWithStatesProps) => {
const { t } = useTranslation();
@@ -197,15 +226,7 @@ export const MonitorBasePageWithStates = ({
const showLoading = loading && totalCount === 0;
if (priorityFallback) {
return (
<BasePage
loading={loading}
error={error}
{...props}
>
<Stack height={"100%"}>{priorityFallback}</Stack>
</BasePage>
);
return <>{priorityFallback}</>;
}
if (!loading && totalCount === 0) {
@@ -213,7 +234,7 @@ export const MonitorBasePageWithStates = ({
<EmptyMonitorFallback
page={page}
title={t(`pages.${page}.fallback.title`)}
bullets={t(`pages.${page}.fallback.checks`, { returnObjects: true })}
description={t(`pages.${page}.fallback.description`)}
actionButtonText={t(`pages.${page}.fallback.actionButton`)}
actionLink={actionLink || ""}
/>
@@ -224,6 +245,7 @@ export const MonitorBasePageWithStates = ({
<BasePage
loading={showLoading}
error={error}
headerKey={headerKey}
{...props}
>
{children}
@@ -0,0 +1,164 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import Tooltip from "@mui/material/Tooltip";
import { Link as RouterLink } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
import { ChevronLeft, HelpCircle, FileText, Code } from "lucide-react";
import type { ReactNode } from "react";
const EXTERNAL_LINKS = {
support: "https://discord.com/invite/NAb6H3UTjK",
docs: "https://checkmate.so/docs",
changelog: "https://github.com/bluewave-labs/checkmate/releases",
};
interface PageHeaderProps {
title: string;
description?: ReactNode;
backTo?: string;
backLabel?: string;
}
export const PageHeader = ({
title,
description,
backTo,
backLabel,
}: PageHeaderProps) => {
const theme = useTheme();
const { t } = useTranslation();
const linkItems: { key: keyof typeof EXTERNAL_LINKS; icon: ReactNode }[] = [
{
key: "support",
icon: (
<HelpCircle
size={16}
strokeWidth={1.6}
/>
),
},
{
key: "docs",
icon: (
<FileText
size={16}
strokeWidth={1.6}
/>
),
},
{
key: "changelog",
icon: (
<Code
size={16}
strokeWidth={1.6}
/>
),
},
];
return (
<Stack
direction="row"
alignItems="flex-start"
justifyContent="space-between"
gap={6}
sx={{ mb: theme.spacing(8) }}
>
<Stack gap={theme.spacing(1)}>
{backTo && backLabel && (
<Box
component={RouterLink}
to={backTo}
sx={{
display: "inline-flex",
alignItems: "center",
gap: 0.5,
textDecoration: "none",
color: theme.palette.text.secondary,
fontSize: 13,
mb: theme.spacing(1),
"&:hover": { color: theme.palette.text.primary },
}}
>
<ChevronLeft
size={14}
strokeWidth={1.8}
/>
{backLabel}
</Box>
)}
<Typography
sx={{
fontSize: 24,
fontWeight: 600,
color: theme.palette.text.primary,
lineHeight: 1.2,
letterSpacing: "-0.01em",
}}
>
{title}
</Typography>
{description && (
<Typography
sx={{
fontSize: 14,
color: theme.palette.text.secondary,
lineHeight: 1.55,
maxWidth: 640,
}}
>
{description}
</Typography>
)}
</Stack>
<Box
sx={{
display: "inline-flex",
alignItems: "center",
border: `1px solid ${theme.palette.divider}`,
borderRadius: 1,
overflow: "hidden",
backgroundColor: theme.palette.background.paper,
flexShrink: 0,
}}
>
{linkItems.map((item, idx) => (
<Tooltip
key={item.key}
title={t(`components.pageHeader.links.${item.key}`)}
>
<Box
component="a"
href={EXTERNAL_LINKS[item.key]}
target="_blank"
rel="noopener noreferrer"
sx={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: 36,
height: 32,
color: theme.palette.text.secondary,
borderRight:
idx < linkItems.length - 1
? `1px solid ${theme.palette.divider}`
: "none",
"&:hover": {
backgroundColor: theme.palette.action.hover,
color: theme.palette.text.primary,
},
}}
>
{item.icon}
</Box>
</Tooltip>
))}
</Box>
</Stack>
);
};
@@ -1,4 +1,5 @@
export * from "./Breadcrumb";
export * from "./PageHeader";
export * from "./BasePage";
export * from "./EmptyState";
export * from "./EmptyStateIllustration";
+1 -19
View File
@@ -6,7 +6,7 @@ import Divider from "@mui/material/Divider";
import { LAYOUT } from "@/Utils/Theme/constants";
import { useSidebar } from "@/Hooks/useSidebar.js";
import { Logo } from "@/Components/sidebar/Logo";
import { getMenu, getBottomMenu, getAccountMenu } from "@/Components/sidebar/Menu";
import { getMenu, getAccountMenu } from "@/Components/sidebar/Menu";
import { NavItem } from "@/Components/sidebar/NavItem";
import { StarPrompt } from "@/Components/sidebar/StarPrompt";
import { AuthFooter } from "@/Components/sidebar/Authfooter";
@@ -33,7 +33,6 @@ export const Sidebar = () => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const menu = getMenu(t);
const bottomMenu = getBottomMenu(t);
const accountMenu = getAccountMenu(t);
useEffect(() => {
@@ -103,23 +102,6 @@ export const Sidebar = () => {
})}
</List>
<StarPrompt />
<List
component="nav"
disablePadding
sx={{ px: theme.spacing(LAYOUT.SM) }}
>
{bottomMenu.map((item) => {
const selected = location.pathname.startsWith(`/${item.path}`);
return (
<NavItem
key={item.path}
item={item}
selected={selected}
onClick={() => handleNavClick(item.path)}
/>
);
})}
</List>
<Divider sx={{ borderColor: theme.palette.divider }} />
<AuthFooter
+1 -1
View File
@@ -25,7 +25,7 @@ const Account = ({ open = "profile" }: AccountProps) => {
}, [open]);
return (
<BasePage>
<BasePage headerKey="account">
<Tabs
value={activeTab}
onChange={(_, newValue: number) => setActiveTab(newValue)}
@@ -122,6 +122,7 @@ const InfrastructureMonitors = () => {
return (
<MonitorBasePageWithStates
headerKey="infrastructure"
loading={isLoading}
error={monitorsWithChecksError}
totalCount={effectiveTotalCount}
+1 -1
View File
@@ -10,7 +10,7 @@ const LogsPage = () => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<number>(1);
return (
<BasePage>
<BasePage headerKey="logs">
<Tabs
value={activeTab}
onChange={(_, newValue: number) => setActiveTab(newValue)}
@@ -77,6 +77,7 @@ const PageSpeedMonitorsPage = () => {
return (
<MonitorBasePageWithStates
headerKey="pageSpeed"
loading={isLoading}
error={monitorsError || settingsError}
totalCount={summary?.totalMonitors ?? 0}
+1
View File
@@ -277,6 +277,7 @@ export const SettingsPage = () => {
return (
<BasePage
headerKey="settings"
component="form"
onSubmit={form.handleSubmit(onSubmit, onError)}
>
@@ -140,6 +140,7 @@ const UptimeMonitorsPage = () => {
return (
<MonitorBasePageWithStates
headerKey="uptime"
loading={isLoading}
error={monitorsWithChecksError}
totalCount={effectiveTotalCount}