diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d22ca467..b3aba5b8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,6 +32,7 @@ "color": "^4.2.3", "lib0": "^0.2.88", "moment": "^2.30.1", + "notistack": "^2.0.8", "randomcolor": "^0.6.2", "react": "^17.0.0 || ^18.0.0", "react-dom": "^17.0.0 || ^18.0.0", @@ -5312,6 +5313,42 @@ "resolved": "https://registry.npmjs.org/not/-/not-0.1.0.tgz", "integrity": "sha512-5PDmaAsVfnWUgTUbJ3ERwn7u79Z0dYxN9ErxCpVJJqe2RK0PJ3z+iFUxuqjwtlDDegXvtWoxD/3Fzxox7tFGWA==" }, + "node_modules/notistack": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-2.0.8.tgz", + "integrity": "sha512-/IY14wkFp5qjPgKNvAdfL5Jp6q90+MjgKTPh4c81r/lW70KeuX6b9pE/4f8L4FG31cNudbN9siiFS5ql1aSLRw==", + "dependencies": { + "clsx": "^1.1.0", + "hoist-non-react-statics": "^3.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/notistack" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "@mui/material": "^5.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/notistack/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 77a48e7a..9330e10e 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "color": "^4.2.3", "lib0": "^0.2.88", "moment": "^2.30.1", + "notistack": "^2.0.8", "randomcolor": "^0.6.2", "react": "^17.0.0 || ^18.0.0", "react-dom": "^17.0.0 || ^18.0.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index feb453f5..78aab66e 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,9 @@ import { useMemo } from "react"; import { selectConfig } from "./store/configSlice"; import axios from "axios"; import { routes } from "./routes"; +import { QueryCache, QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import AuthProvider from "./providers/AuthProvider"; +import { useErrorHandler } from "./hooks/useErrorHandler"; const router = createBrowserRouter(routes); @@ -32,14 +35,31 @@ function App() { }, }); }, [config.theme, prefersDarkMode]); + const handleError = useErrorHandler(); + const queryClient = useMemo(() => { + return new QueryClient({ + queryCache: new QueryCache({ + onError: handleError, + }), + defaultOptions: { + mutations: { + onError: handleError, + }, + }, + }); + }, [handleError]); return ( - - - - - - + + + + + + + + + + ); } diff --git a/frontend/src/components/common/CodePairError.tsx b/frontend/src/components/common/CodePairError.tsx new file mode 100644 index 00000000..773f12bd --- /dev/null +++ b/frontend/src/components/common/CodePairError.tsx @@ -0,0 +1,19 @@ +import { Stack, Typography } from "@mui/material"; +import { isRouteErrorResponse, useRouteError } from "react-router-dom"; + +function CodePairError() { + const error = useRouteError(); + + return ( + + + Something went wrong + {isRouteErrorResponse(error) && ( + Status Code: {error.status} + )} + + + ); +} + +export default CodePairError; diff --git a/frontend/src/hooks/useErrorHandler.ts b/frontend/src/hooks/useErrorHandler.ts new file mode 100644 index 00000000..eb6b553f --- /dev/null +++ b/frontend/src/hooks/useErrorHandler.ts @@ -0,0 +1,14 @@ +import { useSnackbar } from "notistack"; +import { useCallback } from "react"; + +export function useErrorHandler() { + const { enqueueSnackbar } = useSnackbar(); + const handleError = useCallback( + (error: Error) => { + enqueueSnackbar(error.message || "Something went wrong...", { variant: "error" }); + }, + [enqueueSnackbar] + ); + + return handleError; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index a793cbfb..489fad22 100755 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -6,21 +6,17 @@ import { store } from "./store/store"; import { Provider } from "react-redux"; import { PersistGate } from "redux-persist/integration/react"; import { persistStore } from "redux-persist"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import AuthProvider from "./providers/AuthProvider"; +import { SnackbarProvider } from "notistack"; const persistor = persistStore(store); -const queryClient = new QueryClient(); ReactDOM.createRoot(document.getElementById("root")!).render( - - - - - + + + diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 99acf738..9d7ce6ac 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -7,6 +7,18 @@ import WorkspaceLayout from "./components/layouts/WorkspaceLayout"; import GuestRoute from "./components/common/GuestRoute"; import PrivateRoute from "./components/common/PrivateRoute"; import WorkspaceIndex from "./pages/workspace/Index"; +import CodePairError from "./components/common/CodePairError"; + +interface CodePairRoute { + path: string; + accessType: AccessType; + element: JSX.Element; + errorElement?: JSX.Element; + children?: { + path: string; + element: JSX.Element; + }[]; +} const enum AccessType { PRIVATE, // Authroized user can access only @@ -14,7 +26,7 @@ const enum AccessType { GUEST, // Not authorized user can access only } -const codePairRoutes = [ +const codePairRoutes: Array = [ { path: "", accessType: AccessType.GUEST, @@ -63,6 +75,8 @@ const injectProtectedRoute = (routes: typeof codePairRoutes) => { route.element = {route.element}; } + route.errorElement = ; + return route; }); };