diff --git a/client/src/App.tsx b/client/src/App.tsx index ef3625cad..0a6b364f8 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -8,7 +8,7 @@ import darkTheme from "./Utils/Theme/darkTheme"; import { CssBaseline, GlobalStyles } from "@mui/material"; import { logger } from "./Utils/Logger"; // Import the logger import { Routes } from "./Routes"; -import AppLayout from "@/Components/v1/Layouts/AppLayout"; +import AppLayout from "@/Components/v2/layout/AppLayout"; import type { RootState } from "@/Types/state"; function App() { diff --git a/client/src/Components/v1/Layouts/AppLayout/index.jsx b/client/src/Components/v1/Layouts/AppLayout/index.jsx deleted file mode 100644 index 0dbfb1e7a..000000000 --- a/client/src/Components/v1/Layouts/AppLayout/index.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import Box from "@mui/material/Box"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import BackgroundSVG from "../../../../assets/Images/background.svg"; -import { useSelector } from "react-redux"; - -const AppLayout = ({ children }) => { - const theme = useTheme(); - const ui = useSelector((state) => state.ui); - return ( - - {children} - - ); -}; - -AppLayout.propTypes = { - children: PropTypes.node, -}; - -export default AppLayout; diff --git a/client/src/Components/v2/design-elements/OfflineBanner.tsx b/client/src/Components/v2/design-elements/OfflineBanner.tsx new file mode 100644 index 000000000..fa5690c12 --- /dev/null +++ b/client/src/Components/v2/design-elements/OfflineBanner.tsx @@ -0,0 +1,76 @@ +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; +import { WifiOff } from "lucide-react"; + +interface OfflineBannerProps { + visible: boolean; + onRetry?: () => void; + isRetrying?: boolean; +} + +export const OfflineBanner = ({ visible, onRetry, isRetrying }: OfflineBannerProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + + if (!visible) { + return null; + } + + return ( + + + + + {t("components.offlineBanner.serverUnreachable")} + + {onRetry && ( + + )} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx index b7b1d484e..9a4b97a44 100644 --- a/client/src/Components/v2/design-elements/index.tsx +++ b/client/src/Components/v2/design-elements/index.tsx @@ -19,3 +19,4 @@ export * from "./Gauge"; export * from "./Tabs"; export * from "./SplitBox"; export * from "./TextLink"; +export * from "./OfflineBanner"; diff --git a/client/src/Components/v2/layout/AppLayout.tsx b/client/src/Components/v2/layout/AppLayout.tsx new file mode 100644 index 000000000..1310863f9 --- /dev/null +++ b/client/src/Components/v2/layout/AppLayout.tsx @@ -0,0 +1,39 @@ +import Box from "@mui/material/Box"; +import { ThemeProvider, useTheme } from "@mui/material/styles"; +import { useSelector } from "react-redux"; +import BackgroundSVG from "@/assets/Images/background.svg"; +import type { RootState } from "@/Types/state"; +import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme"; +import { OfflineBanner } from "@/Components/v2/design-elements"; + +interface AppLayoutProps { + children: React.ReactNode; +} + +const AppLayout = ({ children }: AppLayoutProps) => { + const theme = useTheme(); + const mode = useSelector((state: RootState) => state.ui.mode); + const v2theme = mode === "dark" ? darkTheme : lightTheme; + + return ( + + + + {children} + + + ); +}; + +export default AppLayout; diff --git a/client/src/Components/v2/layout/RootLayout.tsx b/client/src/Components/v2/layout/RootLayout.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/Components/v2/sidebar/index.tsx b/client/src/Components/v2/sidebar/index.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/Utils/ApiClient.ts b/client/src/Utils/ApiClient.ts index c39839558..4b2f4971f 100644 --- a/client/src/Utils/ApiClient.ts +++ b/client/src/Utils/ApiClient.ts @@ -15,6 +15,15 @@ type StoreType = { let storeInstance: StoreType | null = null; let interceptorsInitialized = false; +type ServerUnreachableCallback = (unreachable: boolean) => void; +let serverUnreachableCallback: ServerUnreachableCallback | null = null; + +export const setServerUnreachableCallback = ( + callback: ServerUnreachableCallback +): void => { + serverUnreachableCallback = callback; +}; + export const initApiClient = (store: StoreType): void => { storeInstance = store; @@ -41,6 +50,12 @@ export const initApiClient = (store: StoreType): void => { const onSuccess = (response: AxiosResponse) => response; const onError = (error: AxiosError) => { + // Handle network errors (server unreachable) + if (error.code === "ERR_NETWORK") { + serverUnreachableCallback?.(true); + return Promise.reject(error); + } + if (error.response?.status === 401) { if (window.location.pathname !== "/login") { window.location.href = "/login"; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index cbd9ea7b7..096fabd9d 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -325,6 +325,12 @@ "week": "Showing statistics for past 7 days.", "month": "Showing statistics for past 30 days." } + }, + "offlineBanner": { + "serverUnreachable": "Unable to reach server", + "retry": "Retry", + "retrying": "Retrying...", + "reconnected": "Connection restored" } }, "configure": "Configure",