From ff208710d7dfba46a1cd88c73f99bb80812c5314 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Sat, 25 Apr 2026 21:19:52 -0400 Subject: [PATCH] feat(client): unified EmptyState component for empty/error placeholders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the cards-with-halo SkeletonCard fallback (which produced a washed-out, hard-to-read placeholder) with a single canonical empty state used everywhere in the app. What's new: - EmptyStateIllustration.tsx — custom inline SVG: two stacked monitor cards (front blue, back gray, both with hairline strokes and a soft fade toward the bottom), set against a faint grid of solid + dashed background lines. Uses only lightened palette tokens (#7DA8E8 blue, #B6C2CF gray, #CBD5E1 grid). - EmptyState.tsx — the layout: illustration + title + description + optional CTA + optional secondary link + optional inline alert slot. Supports a compact horizontal variant for in-panel use. Migrations: - Fallback.tsx: BaseFallback / EmptyFallback / EmptyMonitorFallback / ErrorFallback now delegate to EmptyState. Public APIs unchanged so every existing consumer (BasePageWithStates, MonitorBasePageWithStates, Pages/Uptime, Pages/Notifications, Pages/Infrastructure, etc.) keeps working without edits. - BasePage.tsx: PageSpeedKeyPriorityFallback rebuilt on top of EmptyState with the warning slot. Fixes the previous reports of "white cloudy area" + half-faded text — message and link now render in consistent dark text with green Settings link. No prop changes for any page-level consumer. The illustration is a single SVG with no external assets. --- .../Components/design-elements/BasePage.tsx | 77 ++++--- .../Components/design-elements/EmptyState.tsx | 155 +++++++++++++ .../EmptyStateIllustration.tsx | 214 ++++++++++++++++++ .../Components/design-elements/Fallback.tsx | 147 +++--------- .../src/Components/design-elements/index.tsx | 2 + 5 files changed, 451 insertions(+), 144 deletions(-) create mode 100644 client/src/Components/design-elements/EmptyState.tsx create mode 100644 client/src/Components/design-elements/EmptyStateIllustration.tsx diff --git a/client/src/Components/design-elements/BasePage.tsx b/client/src/Components/design-elements/BasePage.tsx index 691e770dc..38274f2d4 100644 --- a/client/src/Components/design-elements/BasePage.tsx +++ b/client/src/Components/design-elements/BasePage.tsx @@ -1,15 +1,14 @@ import Logo from "@/assets/icons/checkmate-icon.svg?react"; import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; -import Alert from "@mui/material/Alert"; import Link from "@mui/material/Link"; import { LAYOUT, SPACING } from "@/Utils/Theme/constants"; import { ErrorFallback, EmptyFallback, EmptyMonitorFallback, - BaseFallback, } from "@/Components/design-elements/Fallback"; +import { EmptyState } from "@/Components/design-elements/EmptyState"; import { Breadcrumb } from "@/Components/design-elements/Breadcrumb"; import CircularProgress from "@mui/material/CircularProgress"; import { HeaderAuthControls } from "@/Pages/Auth/components/HeaderAuthControls"; @@ -21,32 +20,58 @@ import { Link as RouterLink } from "react-router-dom"; import { Typography } from "@mui/material"; export const PageSpeedKeyPriorityFallback = () => { + const theme = useTheme(); return ( - - - - - ), + + - - - + > + ⚠ + + + + ), + }} + /> + + + } + /> ); }; diff --git a/client/src/Components/design-elements/EmptyState.tsx b/client/src/Components/design-elements/EmptyState.tsx new file mode 100644 index 000000000..d6795e1e9 --- /dev/null +++ b/client/src/Components/design-elements/EmptyState.tsx @@ -0,0 +1,155 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import { Link as RouterLink } from "react-router-dom"; +import { Button } from "@/Components/inputs"; +import { LAYOUT } from "@/Utils/Theme/constants"; +import { EmptyStateIllustration } from "./EmptyStateIllustration"; + +import type { ReactNode } from "react"; + +interface EmptyStateProps { + title: string; + description?: ReactNode; + actionText?: string; + actionTo?: string; + onAction?: () => void; + secondaryText?: string; + secondaryHref?: string; + compact?: boolean; + alert?: ReactNode; + children?: ReactNode; +} + +export const EmptyState = ({ + title, + description, + actionText, + actionTo, + onAction, + secondaryText, + secondaryHref, + compact = false, + alert, + children, +}: EmptyStateProps) => { + const theme = useTheme(); + + if (compact) { + return ( + + + + + {title} + + {description && ( + + {description} + + )} + + + ); + } + + return ( + + + + {title} + + {description && ( + + {description} + + )} + {alert && ( + + {alert} + + )} + {(actionText || secondaryText) && ( + + {actionText && (actionTo || onAction) && ( + + )} + {secondaryText && secondaryHref && ( + + {secondaryText} → + + )} + + )} + {children} + + ); +}; diff --git a/client/src/Components/design-elements/EmptyStateIllustration.tsx b/client/src/Components/design-elements/EmptyStateIllustration.tsx new file mode 100644 index 000000000..e8f74ed88 --- /dev/null +++ b/client/src/Components/design-elements/EmptyStateIllustration.tsx @@ -0,0 +1,214 @@ +interface EmptyStateIllustrationProps { + width?: number | string; + height?: number | string; +} + +export const EmptyStateIllustration = ({ + width = 160, + height = 120, +}: EmptyStateIllustrationProps) => { + return ( + + ); +}; diff --git a/client/src/Components/design-elements/Fallback.tsx b/client/src/Components/design-elements/Fallback.tsx index 003bda647..aa30c75ad 100644 --- a/client/src/Components/design-elements/Fallback.tsx +++ b/client/src/Components/design-elements/Fallback.tsx @@ -1,12 +1,8 @@ import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import { BulletPointCheck, SkeletonCard } from "@/Components/design-elements"; -import { Button } from "@/Components/inputs"; -import { SPACING, LAYOUT } from "@/Utils/Theme/constants"; - -import { useNavigate } from "react-router"; -import useMediaQuery from "@mui/material/useMediaQuery"; import { useTheme } from "@mui/material/styles"; +import { LAYOUT } from "@/Utils/Theme/constants"; +import { EmptyState } from "./EmptyState"; + import type { StackProps } from "@mui/material/Stack"; interface BaseFallbackProps extends StackProps { @@ -15,30 +11,13 @@ interface BaseFallbackProps extends StackProps { export const BaseFallback = ({ children, ...props }: BaseFallbackProps) => { const theme = useTheme(); - const isSmall = useMediaQuery(theme.breakpoints.down("md")); - return ( - {children} ); @@ -51,21 +30,19 @@ export const ErrorFallback = ({ title: string; subtitle: string; }) => { - const theme = useTheme(); return ( - - - {title} - - {subtitle} - + ); }; +const bulletsToDescription = (bullets: unknown): string | undefined => { + if (!Array.isArray(bullets) || bullets.length === 0) return undefined; + return bullets.join(" • "); +}; + export const EmptyFallback = ({ title, bullets, @@ -73,52 +50,21 @@ export const EmptyFallback = ({ actionLink, }: { title: string; - bullets: any; + bullets: unknown; actionButtonText: string; actionLink: string; }) => { - const theme = useTheme(); - const navigate = useNavigate(); return ( - - - {title} - - {Array.isArray(bullets) && - bullets?.map((bullet: string) => ( - - ))} - - - - - - + ); }; export const EmptyMonitorFallback = ({ - page, title, bullets, actionButtonText, @@ -126,51 +72,16 @@ export const EmptyMonitorFallback = ({ }: { page: string; title: string; - bullets: any; + bullets: unknown; actionButtonText: string; actionLink: string; }) => { - const theme = useTheme(); - const navigate = useNavigate(); return ( - - - - {title} - - - {Array.isArray(bullets) && - bullets?.map((bullet: string, index: number) => ( - - ))} - - - - - - + ); }; diff --git a/client/src/Components/design-elements/index.tsx b/client/src/Components/design-elements/index.tsx index ed6a885f6..12fad3f62 100644 --- a/client/src/Components/design-elements/index.tsx +++ b/client/src/Components/design-elements/index.tsx @@ -1,5 +1,7 @@ export * from "./Breadcrumb"; export * from "./BasePage"; +export * from "./EmptyState"; +export * from "./EmptyStateIllustration"; export * from "./Fallback"; export * from "./SkeletonCard"; export * from "./BulletPointCheck";