diff --git a/client/src/Components/v2/design-elements/BasePage.tsx b/client/src/Components/v2/design-elements/BasePage.tsx
new file mode 100644
index 000000000..e3785936b
--- /dev/null
+++ b/client/src/Components/v2/design-elements/BasePage.tsx
@@ -0,0 +1,159 @@
+import Stack from "@mui/material/Stack";
+import {
+ ErrorFallback,
+ EmptyFallback,
+ EmptyMonitorFallback,
+} from "@/Components/v2/design-elements/Fallback";
+import { Breadcrumb } from "@/Components/v2/design-elements/Breadcrumb";
+import CircularProgress from "@mui/material/CircularProgress";
+
+import type { StackProps } from "@mui/material/Stack";
+import { useTheme } from "@mui/material/styles";
+import { useTranslation } from "react-i18next";
+interface BasePageProps extends StackProps {
+ loading?: boolean;
+ error?: boolean;
+ children: React.ReactNode;
+ breadcrumbOverride?: string[];
+}
+
+export const BasePage = ({
+ loading,
+ error,
+ children,
+ breadcrumbOverride,
+ ...props
+}: BasePageProps) => {
+ const theme = useTheme();
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ {children}
+
+ );
+};
+
+interface BasePageWithStatesProps extends StackProps {
+ loading: boolean;
+ error: any;
+ items: any[];
+ bullets: string[] | unknown;
+ page: string;
+ actionButtonText: string;
+ actionLink: string;
+ children: React.ReactNode;
+}
+
+export const BasePageWithStates = ({
+ loading,
+ error,
+ items,
+ page,
+ bullets,
+ actionButtonText,
+ actionLink,
+ children,
+ ...props
+}: BasePageWithStatesProps) => {
+ const showLoading = loading && (!items || items.length === 0);
+
+ if (isEmpty(items)) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+};
+
+interface MonitorBasePageWithStatesProps extends StackProps {
+ loading: boolean;
+ error: any;
+ items: any[];
+ page: string;
+ actionLink?: string;
+ children: React.ReactNode;
+}
+
+const isEmpty = (items: any[]) => {
+ if (!items) return true;
+ if (Array.isArray(items) && items.length === 0) return true;
+ return false;
+};
+
+export const MonitorBasePageWithStates = ({
+ loading,
+ error,
+ items,
+ page,
+ actionLink,
+ children,
+ ...props
+}: MonitorBasePageWithStatesProps) => {
+ const { t } = useTranslation();
+
+ const showLoading = loading && (!items || items.length === 0);
+
+ if (!loading && isEmpty(items)) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/Breadcrumb.tsx b/client/src/Components/v2/design-elements/Breadcrumb.tsx
new file mode 100644
index 000000000..4489f992d
--- /dev/null
+++ b/client/src/Components/v2/design-elements/Breadcrumb.tsx
@@ -0,0 +1,117 @@
+import MuiBreadcrumbs from "@mui/material/Breadcrumbs";
+import Typography from "@mui/material/Typography";
+import { Link, useLocation } from "react-router-dom";
+import { useTheme } from "@mui/material/styles";
+import { ChevronRight, Home } from "lucide-react";
+import { useTranslation } from "react-i18next";
+
+export const Breadcrumb = ({ breadcrumbOverride }: { breadcrumbOverride?: string[] }) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const location = useLocation();
+
+ // Don't show breadcrumb on dashboard
+ if (location.pathname === "/" || location.pathname === "/uptime") {
+ return null;
+ }
+
+ const pathnames = breadcrumbOverride || location.pathname.split("/").filter((x) => x);
+
+ // Don't show if only one level (e.g., /uptime)
+ if (pathnames.length === 0) {
+ return null;
+ }
+
+ return (
+
+ }
+ sx={{
+ fontSize: "14px",
+ marginBottom: theme.spacing(6),
+ "& .MuiBreadcrumbs-separator": {
+ color: theme.palette.text.secondary,
+ },
+ }}
+ >
+
+
+
+ Home
+
+
+ {pathnames.map((value, index) => {
+ const last = index === pathnames.length - 1;
+ const to = `/${pathnames.slice(0, index + 1).join("/")}`;
+
+ let displayName = "";
+
+ // Paths that are IDs should be detail pages
+ if (value.length === 24 || value.match(/^[a-f0-9-]{36}$/)) {
+ displayName = "Details";
+ } else {
+ displayName =
+ t(`common.breadcrumbs.${value}`) ||
+ value.charAt(0).toUpperCase() + value.slice(1);
+ }
+
+ return last ? (
+
+ {displayName}
+
+ ) : (
+
+
+ {displayName}
+
+
+ );
+ })}
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/BulletPointCheck.tsx b/client/src/Components/v2/design-elements/BulletPointCheck.tsx
new file mode 100644
index 000000000..2893ee4c2
--- /dev/null
+++ b/client/src/Components/v2/design-elements/BulletPointCheck.tsx
@@ -0,0 +1,46 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { Check } from "lucide-react";
+import { useTheme } from "@mui/material/styles";
+
+export const BulletPointCheck = ({
+ text,
+ variant = "info",
+}: {
+ text: string;
+ noHighlightText?: string;
+ variant?: "success" | "error" | "info";
+}) => {
+ const theme = useTheme();
+ const colors: Record = {
+ success: theme.palette.success.main,
+ error: theme.palette.error.main,
+ info: theme.palette.text.secondary,
+ };
+
+ return (
+
+
+
+ {text}
+
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/Fallback.tsx b/client/src/Components/v2/design-elements/Fallback.tsx
new file mode 100644
index 000000000..9dfeb5122
--- /dev/null
+++ b/client/src/Components/v2/design-elements/Fallback.tsx
@@ -0,0 +1,171 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { BulletPointCheck, SkeletonCard } from "@/Components/v2/design-elements";
+import { Button } from "@/Components/v2/inputs";
+
+import { useNavigate } from "react-router";
+import useMediaQuery from "@mui/material/useMediaQuery";
+import { useTheme } from "@mui/material/styles";
+import type { StackProps } from "@mui/material/Stack";
+
+interface BaseFallbackProps extends StackProps {
+ children: React.ReactNode;
+}
+
+export const BaseFallback = ({ children, ...props }: BaseFallbackProps) => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+
+ return (
+
+
+ {children}
+
+ );
+};
+
+export const ErrorFallback = ({
+ title,
+ subtitle,
+}: {
+ title: string;
+ subtitle: string;
+}) => {
+ const theme = useTheme();
+ return (
+
+
+ {title}
+
+ {subtitle}
+
+ );
+};
+
+export const EmptyFallback = ({
+ title,
+ bullets,
+ actionButtonText,
+ actionLink,
+}: {
+ title: string;
+ bullets: any;
+ 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,
+ actionLink,
+}: {
+ page: string;
+ title: string;
+ bullets: any;
+ 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/v2/design-elements/SkeletonCard.tsx b/client/src/Components/v2/design-elements/SkeletonCard.tsx
new file mode 100644
index 000000000..05f0c462a
--- /dev/null
+++ b/client/src/Components/v2/design-elements/SkeletonCard.tsx
@@ -0,0 +1,147 @@
+import { Box, Stack, useTheme } from "@mui/material";
+
+interface SkeletonCardProps {
+ width?: number | string | { xs?: number | string; md?: number | string };
+ showHalo?: boolean;
+}
+
+export const SkeletonCard = ({
+ width = { xs: 130, md: 216 },
+ showHalo = true,
+}: SkeletonCardProps) => {
+ const theme = useTheme();
+ const pulseAnimation = {
+ "@keyframes pulse": {
+ "0%, 100%": { opacity: 1 },
+ "50%": { opacity: 0.72 },
+ },
+ };
+
+ const floatAnimation = {
+ "@keyframes float": {
+ "0%, 100%": { transform: "rotate(5deg) translate(12px, -20px)" },
+ "50%": { transform: "rotate(5deg) translate(12px, -24px)" },
+ },
+ };
+
+ const cardStyle = {
+ width: width,
+ height: 57,
+ borderRadius: theme.shape.borderRadius,
+ background:
+ theme.palette.mode === "dark"
+ ? theme.palette.secondary.dark
+ : theme.palette.background.paper,
+ boxShadow:
+ theme.palette.mode === "dark"
+ ? "0 8px 30px rgba(0,0,0,.3), 0 0 0 1px rgba(255,255,255,.05) inset"
+ : "0 8px 30px rgba(0,0,0,.08), 0 0 0 1px rgba(0,0,0,.05) inset",
+ display: "flex",
+ gap: theme.spacing(5),
+ alignItems: "center",
+ padding: theme.spacing(5),
+ };
+
+ const blockStyle = {
+ width: 55,
+ height: 34,
+ borderRadius: theme.shape.borderRadius,
+ background: theme.palette.secondary.main,
+ border: `1px solid ${theme.palette.divider}`,
+ animation: "pulse 1.6s ease-in-out infinite",
+ ...pulseAnimation,
+ };
+
+ const lineStyle = {
+ height: 7,
+ borderRadius: theme.shape.borderRadius,
+ background: theme.palette.secondary.main,
+ border: `1px solid ${theme.palette.divider}`,
+ animation: "pulse 1.6s ease-in-out infinite",
+ ...pulseAnimation,
+ };
+
+ return (
+
+ {showHalo && (
+
+ )}
+
+ {/* Card stack */}
+
+ {/* Front card (now at bottom) */}
+
+
+
+ {/* Empty space to maintain card height */}
+
+
+
+ {/* Back card A */}
+
+
+
+
+
+
+
+
+
+ {/* Back card B */}
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx
new file mode 100644
index 000000000..9970ed980
--- /dev/null
+++ b/client/src/Components/v2/design-elements/index.tsx
@@ -0,0 +1,5 @@
+export * from "./Breadcrumb";
+export * from "./BasePage";
+export * from "./Fallback";
+export * from "./SkeletonCard";
+export * from "./BulletPointCheck";
diff --git a/client/src/Components/v2/inputs/Button.tsx b/client/src/Components/v2/inputs/Button.tsx
new file mode 100644
index 000000000..54f776075
--- /dev/null
+++ b/client/src/Components/v2/inputs/Button.tsx
@@ -0,0 +1,37 @@
+import Button from "@mui/material/Button";
+import type { ButtonProps } from "@mui/material/Button";
+import { useTheme } from "@mui/material/styles";
+
+export const ButtonInput = ({ sx, ...props }: ButtonProps) => {
+ const theme = useTheme();
+ const outlinedSx =
+ props.variant === "outlined"
+ ? {
+ color: theme.palette.text.primary,
+ borderColor: theme.palette.divider,
+ "&:hover": {
+ borderColor: theme.palette.text.secondary,
+ },
+ }
+ : {};
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx
new file mode 100644
index 000000000..9b582f3d7
--- /dev/null
+++ b/client/src/Components/v2/inputs/index.tsx
@@ -0,0 +1 @@
+export { ButtonInput as Button } from "./Button";
diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx
new file mode 100644
index 000000000..9883c1470
--- /dev/null
+++ b/client/src/Pages/Uptime/Monitors/index.tsx
@@ -0,0 +1,17 @@
+import { MonitorBasePageWithStates } from "@/Components/v2/design-elements";
+
+const UptimeMonitorsPage = () => {
+ return (
+
+ Test
+
+ );
+};
+
+export default UptimeMonitorsPage;
diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/old.jsx
similarity index 100%
rename from client/src/Pages/Uptime/Monitors/index.jsx
rename to client/src/Pages/Uptime/Monitors/old.jsx
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index 878b17b09..e37bae063 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -1,3 +1,7 @@
+// Temp v2 theme
+import { ThemeProvider } from "@mui/material";
+import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme";
+
import { useSelector } from "react-redux";
import { Navigate, Route, Routes as LibRoutes } from "react-router";
import HomeLayout from "@/Components/v1/Layouts/HomeLayout";
@@ -11,7 +15,7 @@ import AuthSetNewPassword from "../Pages/Auth/SetNewPassword.jsx";
import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed.jsx";
// Uptime
-import Uptime from "../Pages/Uptime/Monitors/index.jsx";
+import Uptime from "../Pages/Uptime/Monitors";
import UptimeDetails from "../Pages/Uptime/Details/index.jsx";
import UptimeCreate from "../Pages/Uptime/Create/index.jsx";
@@ -75,7 +79,13 @@ const Routes = () => {
/>
}
+ element={
+ <>
+
+
+
+ >
+ }
/>
+ createTheme({
+ breakpoints: {
+ values: {
+ xs: 300,
+ sm: 600,
+ md: 900,
+ lg: 1200,
+ xl: 1536,
+ },
+ },
+ spacing: 2,
+ palette: {
+ mode: mode,
+ ...palette,
+ },
+ typography: {
+ fontFamily: fontFamilyPrimary,
+ fontSize: typographyLevels.base,
+ h1: {
+ fontSize: typographyLevels.xl,
+ fontWeight: 500,
+ },
+ h2: {
+ fontSize: typographyLevels.l,
+ fontWeight: 400,
+ },
+ body1: {
+ fontSize: typographyLevels.m,
+ fontWeight: 400,
+ },
+ body2: {
+ fontSize: typographyLevels.s,
+ fontWeight: 400,
+ },
+ },
+
+ components: {
+ MuiButtonBase: {
+ defaultProps: {
+ disableRipple: true,
+ disableTouchRipple: true,
+ },
+ },
+ MuiButton: {
+ defaultProps: {
+ disableRipple: true,
+ disableTouchRipple: true,
+ disableFocusRipple: true,
+ },
+ },
+ MuiIconButton: {
+ defaultProps: {
+ disableRipple: true,
+ disableTouchRipple: true,
+ disableFocusRipple: true,
+ },
+ },
+ MuiListItemButton: {
+ defaultProps: {
+ disableRipple: true,
+ disableTouchRipple: true,
+ },
+ },
+ MuiTab: {
+ defaultProps: {
+ disableRipple: true,
+ disableTouchRipple: true,
+ disableFocusRipple: true,
+ },
+ },
+ MuiToggleButton: {
+ defaultProps: {
+ disableRipple: true,
+ disableTouchRipple: true,
+ disableFocusRipple: true,
+ },
+ },
+
+ MuiFormLabel: {
+ styleOverrides: {
+ root: ({ theme }) => ({
+ fontSize: typographyLevels.base,
+ "&.Mui-focused": {
+ color: theme.palette.secondary.contrastText,
+ },
+ }),
+ },
+ },
+ MuiInputLabel: {
+ styleOverrides: {
+ root: ({ theme }) => ({
+ top: `-${theme.spacing(4)}`,
+ "&.MuiInputLabel-shrink": {
+ top: 0,
+ },
+ }),
+ },
+ },
+
+ MuiOutlinedInput: {
+ styleOverrides: {
+ root: {
+ "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
+ borderColor: palette.primary.main,
+ },
+ },
+ },
+ },
+
+ MuiPaper: {
+ styleOverrides: {
+ root: ({ theme }) => {
+ return {
+ marginTop: 4,
+ padding: 0,
+ border: 1,
+ borderStyle: "solid",
+ borderColor: theme.palette.divider,
+ borderRadius: 4,
+ boxShadow: shadow,
+ backgroundColor: theme.palette.background.paper,
+ backgroundImage: "none",
+ };
+ },
+ },
+ },
+ },
+ shape: {
+ borderRadius: 2,
+ },
+ });
+
+export const lightTheme = createTheme(theme("light", lightPalette));
+export const darkTheme = createTheme(theme("dark", darkPalette));
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index d152a1809..1b3792e9e 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -125,6 +125,13 @@
"testLocale": "testLocale",
"add": "Add",
"monitors": "monitors",
+ "pages": {
+ "uptime": {
+ "fallback": {
+ "title": "An uptime monitor is used to:"
+ }
+ }
+ },
"distributedUptimeStatusCreateStatusPage": "status page",
"distributedUptimeStatusCreateStatusPageAccess": "Access",
"distributedUptimeStatusCreateStatusPageReady": "If your status page is ready, you can mark it as published.",