initial commit

This commit is contained in:
Alex Holliday committed 2026-01-23 19:58:37 +00:00
1 parent f8bb01190e
commit 363d89e4f4
14 files changed
+901 -2

No files matched your search

@@ -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 (
<Stack
alignItems="center"
justifyContent="center"
sx={{ height: "100%" }}
>
<CircularProgress
color="primary"
size={28}
/>
</Stack>
);
}
if (error) {
return (
<ErrorFallback
title="Something went wrong..."
subtitle="Please try again later"
/>
);
}
return (
<Stack
spacing={theme.spacing(10)}
{...props}
>
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
{children}
</Stack>
);
};
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 (
<EmptyFallback
bullets={bullets}
title={page}
actionButtonText={actionButtonText}
actionLink={actionLink}
/>
);
}
return (
<BasePage
loading={showLoading}
error={error}
{...props}
>
{children}
</BasePage>
);
};
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 (
<EmptyMonitorFallback
page={page}
title={t(`pages.${page}.fallback.title`)}
bullets={t(`monitors.${page}.fallback.checks`, { returnObjects: true })}
actionButtonText={t(`monitors.${page}.fallback.actionButton`)}
actionLink={actionLink || ""}
/>
);
}
return (
<BasePage
loading={showLoading}
error={error}
{...props}
>
{children}
</BasePage>
);
};
@@ -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 (
<MuiBreadcrumbs
separator={
<ChevronRight
size={16}
strokeWidth={1.5}
/>
}
sx={{
fontSize: "14px",
marginBottom: theme.spacing(6),
"& .MuiBreadcrumbs-separator": {
color: theme.palette.text.secondary,
},
}}
>
<Link
to="/uptime"
style={{
display: "flex",
alignItems: "center",
gap: theme.spacing(2),
textDecoration: "none",
color: theme.palette.text.secondary,
}}
>
<Home
size={16}
strokeWidth={1.5}
/>
<Typography
sx={{
fontSize: "14px",
color: theme.palette.text.secondary,
"&:hover": {
color: theme.palette.primary.main,
},
}}
>
Home
</Typography>
</Link>
{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 ? (
<Typography
key={to}
sx={{
fontSize: "14px",
fontWeight: 600,
color: theme.palette.primary.main,
}}
>
{displayName}
</Typography>
) : (
<Link
key={to}
to={to}
style={{
textDecoration: "none",
}}
>
<Typography
sx={{
fontSize: "14px",
color: theme.palette.text.secondary,
"&:hover": {
color: theme.palette.primary.main,
},
}}
>
{displayName}
</Typography>
</Link>
);
})}
</MuiBreadcrumbs>
);
};
@@ -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<string, string | undefined> = {
success: theme.palette.success.main,
error: theme.palette.error.main,
info: theme.palette.text.secondary,
};
return (
<Stack
direction="row"
gap={theme.spacing(6)}
alignItems="center"
>
<Check
size={16}
strokeWidth={1.5}
style={{
flexShrink: 0,
}}
/>
<Typography
component="span"
color={variant === "info" ? theme.palette.text.secondary : colors[variant]}
fontWeight={450}
sx={{
opacity: 0.9,
}}
>
{text}
</Typography>
</Stack>
);
};
@@ -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 (
<Stack
alignItems={"center"}
margin={isSmall ? "inherit" : "auto"}
marginTop={isSmall ? "33%" : "10%"}
width={{
sm: "90%",
md: "70%",
lg: "50%",
xl: "40%",
}}
padding={{ xs: theme.spacing(8), md: theme.spacing(16) }}
bgcolor={theme.palette.background.paper}
border={1}
borderColor={theme.palette.divider}
borderRadius={theme.shape.borderRadius}
sx={{
borderStyle: "dashed",
}}
{...props}
>
<SkeletonCard showHalo={true} />
{children}
</Stack>
);
};
export const ErrorFallback = ({
title,
subtitle,
}: {
title: string;
subtitle: string;
}) => {
const theme = useTheme();
return (
<BaseFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.text.secondary}
>
{title}
</Typography>
<Typography>{subtitle}</Typography>
</BaseFallback>
);
};
export const EmptyFallback = ({
title,
bullets,
actionButtonText,
actionLink,
}: {
title: string;
bullets: any;
actionButtonText: string;
actionLink: string;
}) => {
const theme = useTheme();
const navigate = useNavigate();
return (
<BaseFallback>
<Stack
gap={theme.spacing(10)}
zIndex={1}
alignItems="center"
>
<Typography
component="h1"
color={theme.palette.primary.contrastText}
>
{title}
</Typography>
<Stack
sx={{
flexWrap: "wrap",
gap: theme.spacing(2),
maxWidth: "1100px",
width: "100%",
}}
>
{Array.isArray(bullets) &&
bullets?.map((bullet: string) => (
<BulletPointCheck
text={bullet}
key={`${bullet}-${Math.random()}`}
/>
))}
</Stack>
<Stack>
<Button
variant="contained"
color="primary"
onClick={() => navigate(actionLink)}
>
{actionButtonText}
</Button>
</Stack>
</Stack>
</BaseFallback>
);
};
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 (
<BaseFallback>
<Stack
gap={theme.spacing(10)}
zIndex={1}
alignItems="center"
>
<Typography
component="h1"
color={theme.palette.primary.contrastText}
>
{title}
</Typography>
<Stack
sx={{
flexWrap: "wrap",
gap: theme.spacing(2),
maxWidth: "1100px",
width: "100%",
}}
>
{Array.isArray(bullets) &&
bullets?.map((bullet: string, index: number) => (
<BulletPointCheck
text={bullet}
key={`${(page + "Monitors").trim().split(" ")[0]}-${index}`}
/>
))}
</Stack>
</Stack>
</BaseFallback>
);
};
@@ -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 (
<Box
position={"relative"}
mt={20}
mb={20}
>
{showHalo && (
<Box
sx={{
position: "absolute",
inset: 0,
display: "grid",
placeItems: "center",
pointerEvents: "none",
"&::before": {
content: '""',
width: "260px",
height: "260px",
borderRadius: "999px",
background:
theme.palette.mode === "dark" ? "rgba(255, 255, 255, 0.05)" : "#fff",
filter: "blur(60px)",
opacity: 0.9,
},
}}
aria-hidden="true"
/>
)}
{/* Card stack */}
<Box sx={{ position: "relative", zIndex: 1 }}>
{/* Front card (now at bottom) */}
<Box sx={cardStyle}>
<Box sx={blockStyle} />
<Stack sx={{ flex: 1, gap: "12px" }}>
{/* Empty space to maintain card height */}
</Stack>
</Box>
{/* Back card A */}
<Box
sx={{
...cardStyle,
transform: "rotate(-4deg) translate(-12px, -12px)",
filter: "blur(.2px)",
position: "absolute",
left: 0,
top: 0,
}}
aria-hidden="true"
>
<Box sx={blockStyle} />
<Stack sx={{ flex: 1, gap: "12px" }}>
<Box sx={{ ...lineStyle, width: "60%" }} />
<Box sx={{ ...lineStyle, width: "80%" }} />
<Box sx={{ ...lineStyle, width: "40%" }} />
</Stack>
</Box>
{/* Back card B */}
<Box
sx={{
...cardStyle,
transform: "rotate(5deg) translate(12px, -20px)",
filter: "blur(.3px)",
position: "absolute",
left: 0,
top: 0,
animation: "float 3s ease-in-out 5",
...floatAnimation,
}}
aria-hidden="true"
>
<Box sx={blockStyle} />
<Stack sx={{ flex: 1, gap: "12px" }}>
<Box sx={{ ...lineStyle, width: "60%" }} />
<Box sx={{ ...lineStyle, width: "80%" }} />
<Box sx={{ ...lineStyle, width: "40%" }} />
</Stack>
</Box>
</Box>
</Box>
);
};
@@ -0,0 +1,5 @@
export * from "./Breadcrumb";
export * from "./BasePage";
export * from "./Fallback";
export * from "./SkeletonCard";
export * from "./BulletPointCheck";
@@ -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 (
<Button
{...props}
sx={{
textTransform: "none",
height: 34,
fontWeight: 400,
borderRadius: 2,
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
boxShadow: "none",
"&:hover": {
boxShadow: "none",
},
...outlinedSx,
...sx,
}}
/>
);
};
@@ -0,0 +1 @@
export { ButtonInput as Button } from "./Button";
@@ -0,0 +1,17 @@
import { MonitorBasePageWithStates } from "@/Components/v2/design-elements";
const UptimeMonitorsPage = () => {
return (
<MonitorBasePageWithStates
loading={false}
error={false}
items={[]}
page="uptime"
actionLink="/uptime/create"
>
Test
</MonitorBasePageWithStates>
);
};
export default UptimeMonitorsPage;
+12 -2
View File
@@ -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 = () => {
/>
<Route
path="/uptime"
element={<Uptime />}
element={
<>
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
<Uptime />
</ThemeProvider>
</>
}
/>
<Route
+34
View File
@@ -0,0 +1,34 @@
const typographyBase = 13;
export const typographyLevels = {
base: typographyBase,
xs: `${(typographyBase - 4) / 16}rem`,
s: `${(typographyBase - 2) / 16}rem`,
m: `${typographyBase / 16}rem`,
l: `${(typographyBase + 2) / 16}rem`,
xl: `${(typographyBase + 10) / 16}rem`,
};
export const colors = {
gray200: "#EFEFEF",
gray700: "#313131",
blueBlueWave: "#1570EF",
};
export const lightPalette = {
primary: {
main: colors.blueBlueWave,
},
secondary: {
main: colors.gray200,
},
};
export const darkPalette = {
primary: {
main: colors.blueBlueWave,
},
secondary: {
main: colors.gray700,
},
};
+148
View File
@@ -0,0 +1,148 @@
import { createTheme } from "@mui/material";
import { lightPalette, darkPalette, typographyLevels } from "@/Utils/Theme/v2Palette";
import type { Theme } from "@mui/material/styles";
export type PaletteKey = {
[K in keyof Theme["palette"]]: Theme["palette"][K] extends { main: any } ? K : never;
}[keyof Theme["palette"]];
const fontFamilyPrimary = "system-ui, sans-serif";
const shadow =
"0px 4px 24px -4px rgba(16, 24, 40, 0.08), 0px 3px 3px -3px rgba(16, 24, 40, 0.03)";
export const theme = (mode: string, palette: any) =>
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));
+7
View File
@@ -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.",