Skip to content

Commit

Permalink
Merge pull request #25 from atlp-rwanda/ft-Intercept-Axios-#187790848
Browse files Browse the repository at this point in the history
#187790848 Set up Axios interceptors for Network errors, 401 Errors and redirections
  • Loading branch information
teerenzo authored Jul 17, 2024
2 parents 701fbe7 + c069817 commit 837a593
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 48 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ module.exports = {
"no-return-assign": "off",
"react-refresh/only-export-components":"off",
"jsx-a11y/label-has-associated-control": "off",
"import/no-cycle": "off",
"react/function-component-definition": [
"warn",
{
Expand Down
64 changes: 64 additions & 0 deletions src/__test__/productSlice.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { configureStore } from "@reduxjs/toolkit";

import productsReducer, {
fetchProducts,
deleteProduct,
handleSearchProduct,
} from "../redux/reducers/productsSlice";
import api from "../redux/api/api";

jest.mock("../redux/api/api");

describe("products slice", () => {
let store;

beforeEach(() => {
store = configureStore({
reducer: {
products: productsReducer,
},
});
});

it("should handle fetchProducts", async () => {
const mockProducts = [
{ id: 1, name: "Product 1", price: 100 },
{ id: 2, name: "Product 2", price: 200 },
];

(api.get as jest.Mock).mockResolvedValueOnce({
data: { products: mockProducts },
});

await store.dispatch(fetchProducts());

expect(store.getState().products.data).toEqual(mockProducts);
expect(store.getState().products.loading).toBe(false);
});

it("should handle deleteProduct", async () => {
const productId = 1;

(api.delete as jest.Mock).mockResolvedValueOnce({});

await store.dispatch(deleteProduct(productId));

expect(store.getState().products.data).not.toContainEqual({
id: productId,
});
});

it("should handle handleSearchProduct", async () => {
const mockProducts = [
{ id: 1, name: "Product 1", price: 100 },
{ id: 2, name: "Product 2", price: 200 },
];

(api.get as jest.Mock).mockResolvedValueOnce({ data: mockProducts });

await store.dispatch(handleSearchProduct({ name: "Product" }));

expect(store.getState().products.data).toEqual(mockProducts);
expect(store.getState().products.loading).toBe(false);
});
});
12 changes: 12 additions & 0 deletions src/__test__/updatePasswordApiSlice.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ import updatePasswordApiSlice, {
} from "../redux/api/updatePasswordApiSlice";

jest.mock("axios");
jest.mock("../redux/api/api", () => ({
api: {
interceptors: {
response: {
use: jest.fn(),
},
request: {
use: jest.fn(),
},
},
},
}));

describe("updatePasswordApiSlice", () => {
let store;
Expand Down
9 changes: 7 additions & 2 deletions src/components/cards/ProductCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { IconButton, Rating, Typography } from "@mui/material";
import { FaEye } from "react-icons/fa";
import { CiHeart } from "react-icons/ci";
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";
import { ToastContainer, toast } from "react-toastify";
import { AxiosError } from "axios";
import { useSelector } from "react-redux";
Expand All @@ -28,6 +28,7 @@ const ProductCard: React.FC<IProductCardProps> = ({ product }) => {
const [isLoading, setIsLoading] = useState(false);
const [reviews, setReviews] = useState<Review[]>([]);
const dispatch = useAppDispatch();
const navigate = useNavigate();

const formatPrice = (price: number) => {
if (price < 1000) {
Expand Down Expand Up @@ -84,6 +85,10 @@ const ProductCard: React.FC<IProductCardProps> = ({ product }) => {
const handleAddToCart = async () => {
if (!localStorage.getItem("accessToken")) {
toast.info("Please Log in to add to cart.");
setTimeout(() => {
navigate("/login");
}, 4000);

return;
}
setIsLoading(true);
Expand All @@ -94,7 +99,7 @@ const ProductCard: React.FC<IProductCardProps> = ({ product }) => {
dispatch(cartManage());
} catch (err) {
const error = err as AxiosError;
toast.error(`Failed to add product to cart: ${error.message}`);
// toast.error(`Failed to add product to cart: ${error.message}`);
} finally {
setIsLoading(false);
}
Expand Down
43 changes: 43 additions & 0 deletions src/components/common/errors/networkErrorPopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useState, useEffect } from "react";
import { set } from "react-hook-form";

import api from "../../../redux/api/action";
// import { useSelector, useDispatch } from "react-redux";

// import { clearNetworkError } from "../../../redux/reducers/networkErrorSlice";

const Popup = () => {
const [showPopup, setShowPopup] = useState(false);
useEffect(() => {
const response = api
.get("/")
.then(() => {
setShowPopup(false);
})
.catch(() => {
setShowPopup(true);
});
}, []);

if (!showPopup) return null;

return (
<div className="fixed inset-0 z-40 flex items-center justify-center bg-[#D0D0D0] bg-opacity-50">
<div className="relative bg-white rounded-lg p-10 w-[90%] md:w-[65%] lg:w-[55%] xl:w-[50%] duration-75 animate-fadeIn">
<p>
📵
<b>Network Error </b>
<br />
Please check your internet connection.
</p>
<button
onClick={() => setShowPopup(false)}
className="mt-10 bg-transparent text-primary border border-[#DB4444] px-4 py-2 rounded"
>
Close
</button>
</div>
</div>
);
};
export default Popup;
42 changes: 30 additions & 12 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from "react";
import React, { useState, useEffect } from "react";
import axios from "axios";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import "./index.css";
Expand All @@ -7,17 +8,34 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

import store from "./redux/store";
import App from "./App";
import Popup from "./components/common/errors/networkErrorPopup";

const client = new QueryClient({});
const Main = () => {
const [showPopup, setShowPopup] = useState(false);
useEffect(() => {
const response = axios
.get(process.env.BASE_URL as string)
.then(() => {
setShowPopup(false);
})
.catch(() => {
setShowPopup(true);
});
}, []);

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QueryClientProvider client={client}>
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>
</QueryClientProvider>
</React.StrictMode>,
);
return (
<React.StrictMode>
<QueryClientProvider client={client}>
<Provider store={store}>
<Router>
{showPopup && <Popup />}
<App />
</Router>
</Provider>
</QueryClientProvider>
</React.StrictMode>
);
};

ReactDOM.createRoot(document.getElementById("root")!).render(<Main />);
9 changes: 9 additions & 0 deletions src/redux/api/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import axios from "axios";

const api = axios.create({
baseURL: process.env.VITE_BASE_URL,
headers: {
"Content-Type": "application/json",
},
});
export default api;
43 changes: 37 additions & 6 deletions src/redux/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,41 @@
import axios from "axios";
import { set } from "react-hook-form";
import { useState, useEffect } from "react";
import { toast } from "react-toastify";

const api = axios.create({
baseURL: process.env.VITE_BASE_URL,
headers: {
"Content-Type": "application/json",
import store from "../store";

import api from "./action";

let navigateFunction = null;

export const setNavigateFunction = (navigate) => {
navigateFunction = navigate;
};
const redirectToLogin = (navigate) => {
setTimeout(() => {
navigate("/login");
}, 2000);
};

api.interceptors.response.use(
(response) => response,
(error) => {
const excludeRoute = "/";

if (
error.response
&& error.response.status === 401
&& window.location.pathname !== excludeRoute
) {
if (navigateFunction) {
toast("Login is Required for this action \n Redirecting to Login \n ");
setTimeout(() => {
redirectToLogin(navigateFunction);
}, 2000);
}
}
return Promise.reject(error);
},
});
);

export default api;
88 changes: 60 additions & 28 deletions src/routes/AppRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Route, Routes } from "react-router-dom";
import { Route, Routes, useNavigate } from "react-router-dom";
import { useEffect } from "react";

import RootLayout from "../components/layouts/RootLayout";
import Homepage from "../pages/Homepage";
Expand All @@ -20,33 +21,64 @@ import Settings from "../dashboard/admin/Settings";
import Analytics from "../dashboard/admin/Analytics";
import Dashboard from "../dashboard/admin/Dashboard";
import CartManagement from "../pages/CartManagement";
import { setNavigateFunction } from "../redux/api/api";

const AppRoutes = () => (
<Routes>
<Route path="/" element={<RootLayout />}>
<Route index element={<Homepage />} />
<Route path="products" element={<ProductPage />} />
<Route path="products/:id" element={<ProductDetails />} />
<Route path="/carts" element={<CartManagement />} />
</Route>
<Route path="/password-reset-link" element={<GetLinkPage />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route path="/register" element={<RegisterUser />} />
<Route path="/login" element={<Login />} />
<Route path="2fa-verify" element={<OtpVerificationForm />} />
<Route path="/dashboard" element={<SellerDashboard />} />
<Route path="/dashboard/addproduct" element={<AddProduct />} />
<Route path="/update-password" element={<UpdatePasswordPage />} />
<Route path="/profile" element={<UsersProfile />} />
<Route path="/profile/update" element={<UpdateUserProfile />} />
<Route path="/dashboard/products" element={<Products />} />
<Route path="/dashboard/products/:id" element={<AddProduct />} />
<Route path="/admin/dashboard" element={<Dashboard />} />
<Route path="/admin/users" element={<UserManagement />} />
<Route path="/admin/settings" element={<Settings />} />
<Route path="/admin/analytics" element={<Analytics />} />
<Route path="/admin/Products" element={<Products />} />
</Routes>
);
const AppRoutes = () => {
const navigate = useNavigate();
useEffect(() => {
setNavigateFunction(navigate);
}, [navigate]);

const AlreadyLogged = ({ children }) => {
const navigate = useNavigate();
const token = localStorage.getItem("accessToken");
const decodedToken = token ? JSON.parse(atob(token!.split(".")[1])) : {};
const tokenIsValid = decodedToken.id && decodedToken.roleId;
const isSeller = decodedToken.roleId === 2;

useEffect(() => {
if (tokenIsValid) {
isSeller ? navigate("/dashboard") : navigate("/");
}
}, [tokenIsValid, navigate]);

return tokenIsValid ? null : children;
};

return (
<Routes>
<Route path="/" element={<RootLayout />}>
<Route index element={<Homepage />} />
<Route path="products" element={<ProductPage />} />
<Route path="products/:id" element={<ProductDetails />} />
<Route path="/carts" element={<CartManagement />} />
</Route>
<Route path="/password-reset-link" element={<GetLinkPage />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route path="/register" element={<RegisterUser />} />
<Route
path="/login"
element={(
<AlreadyLogged>
<Login />
</AlreadyLogged>
)}
/>
<Route path="2fa-verify" element={<OtpVerificationForm />} />
<Route path="/dashboard" element={<SellerDashboard />} />
<Route path="/dashboard/addproduct" element={<AddProduct />} />
<Route path="/update-password" element={<UpdatePasswordPage />} />
<Route path="/profile" element={<UsersProfile />} />
<Route path="/profile/update" element={<UpdateUserProfile />} />
<Route path="/dashboard/products" element={<Products />} />
<Route path="/dashboard/products/:id" element={<AddProduct />} />
<Route path="/admin/dashboard" element={<Dashboard />} />
<Route path="/admin/users" element={<UserManagement />} />
<Route path="/admin/settings" element={<Settings />} />
<Route path="/admin/analytics" element={<Analytics />} />
<Route path="/admin/Products" element={<Products />} />
</Routes>
);
};

export default AppRoutes;

0 comments on commit 837a593

Please sign in to comment.