Skip to content

Commit 837a593

Browse files
authored
Merge pull request #25 from atlp-rwanda/ft-Intercept-Axios-#187790848
#187790848 Set up Axios interceptors for Network errors, 401 Errors and redirections
2 parents 701fbe7 + c069817 commit 837a593

File tree

9 files changed

+263
-48
lines changed

9 files changed

+263
-48
lines changed

.eslintrc.cjs

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ module.exports = {
5050
"no-return-assign": "off",
5151
"react-refresh/only-export-components":"off",
5252
"jsx-a11y/label-has-associated-control": "off",
53+
"import/no-cycle": "off",
5354
"react/function-component-definition": [
5455
"warn",
5556
{

src/__test__/productSlice.test.tsx

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { configureStore } from "@reduxjs/toolkit";
2+
3+
import productsReducer, {
4+
fetchProducts,
5+
deleteProduct,
6+
handleSearchProduct,
7+
} from "../redux/reducers/productsSlice";
8+
import api from "../redux/api/api";
9+
10+
jest.mock("../redux/api/api");
11+
12+
describe("products slice", () => {
13+
let store;
14+
15+
beforeEach(() => {
16+
store = configureStore({
17+
reducer: {
18+
products: productsReducer,
19+
},
20+
});
21+
});
22+
23+
it("should handle fetchProducts", async () => {
24+
const mockProducts = [
25+
{ id: 1, name: "Product 1", price: 100 },
26+
{ id: 2, name: "Product 2", price: 200 },
27+
];
28+
29+
(api.get as jest.Mock).mockResolvedValueOnce({
30+
data: { products: mockProducts },
31+
});
32+
33+
await store.dispatch(fetchProducts());
34+
35+
expect(store.getState().products.data).toEqual(mockProducts);
36+
expect(store.getState().products.loading).toBe(false);
37+
});
38+
39+
it("should handle deleteProduct", async () => {
40+
const productId = 1;
41+
42+
(api.delete as jest.Mock).mockResolvedValueOnce({});
43+
44+
await store.dispatch(deleteProduct(productId));
45+
46+
expect(store.getState().products.data).not.toContainEqual({
47+
id: productId,
48+
});
49+
});
50+
51+
it("should handle handleSearchProduct", async () => {
52+
const mockProducts = [
53+
{ id: 1, name: "Product 1", price: 100 },
54+
{ id: 2, name: "Product 2", price: 200 },
55+
];
56+
57+
(api.get as jest.Mock).mockResolvedValueOnce({ data: mockProducts });
58+
59+
await store.dispatch(handleSearchProduct({ name: "Product" }));
60+
61+
expect(store.getState().products.data).toEqual(mockProducts);
62+
expect(store.getState().products.loading).toBe(false);
63+
});
64+
});

src/__test__/updatePasswordApiSlice.test.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ import updatePasswordApiSlice, {
66
} from "../redux/api/updatePasswordApiSlice";
77

88
jest.mock("axios");
9+
jest.mock("../redux/api/api", () => ({
10+
api: {
11+
interceptors: {
12+
response: {
13+
use: jest.fn(),
14+
},
15+
request: {
16+
use: jest.fn(),
17+
},
18+
},
19+
},
20+
}));
921

1022
describe("updatePasswordApiSlice", () => {
1123
let store;

src/components/cards/ProductCard.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { IconButton, Rating, Typography } from "@mui/material";
22
import { FaEye } from "react-icons/fa";
33
import { CiHeart } from "react-icons/ci";
44
import React, { useEffect, useState } from "react";
5-
import { Link } from "react-router-dom";
5+
import { Link, useNavigate } from "react-router-dom";
66
import { ToastContainer, toast } from "react-toastify";
77
import { AxiosError } from "axios";
88
import { useSelector } from "react-redux";
@@ -28,6 +28,7 @@ const ProductCard: React.FC<IProductCardProps> = ({ product }) => {
2828
const [isLoading, setIsLoading] = useState(false);
2929
const [reviews, setReviews] = useState<Review[]>([]);
3030
const dispatch = useAppDispatch();
31+
const navigate = useNavigate();
3132

3233
const formatPrice = (price: number) => {
3334
if (price < 1000) {
@@ -84,6 +85,10 @@ const ProductCard: React.FC<IProductCardProps> = ({ product }) => {
8485
const handleAddToCart = async () => {
8586
if (!localStorage.getItem("accessToken")) {
8687
toast.info("Please Log in to add to cart.");
88+
setTimeout(() => {
89+
navigate("/login");
90+
}, 4000);
91+
8792
return;
8893
}
8994
setIsLoading(true);
@@ -94,7 +99,7 @@ const ProductCard: React.FC<IProductCardProps> = ({ product }) => {
9499
dispatch(cartManage());
95100
} catch (err) {
96101
const error = err as AxiosError;
97-
toast.error(`Failed to add product to cart: ${error.message}`);
102+
// toast.error(`Failed to add product to cart: ${error.message}`);
98103
} finally {
99104
setIsLoading(false);
100105
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React, { useState, useEffect } from "react";
2+
import { set } from "react-hook-form";
3+
4+
import api from "../../../redux/api/action";
5+
// import { useSelector, useDispatch } from "react-redux";
6+
7+
// import { clearNetworkError } from "../../../redux/reducers/networkErrorSlice";
8+
9+
const Popup = () => {
10+
const [showPopup, setShowPopup] = useState(false);
11+
useEffect(() => {
12+
const response = api
13+
.get("/")
14+
.then(() => {
15+
setShowPopup(false);
16+
})
17+
.catch(() => {
18+
setShowPopup(true);
19+
});
20+
}, []);
21+
22+
if (!showPopup) return null;
23+
24+
return (
25+
<div className="fixed inset-0 z-40 flex items-center justify-center bg-[#D0D0D0] bg-opacity-50">
26+
<div className="relative bg-white rounded-lg p-10 w-[90%] md:w-[65%] lg:w-[55%] xl:w-[50%] duration-75 animate-fadeIn">
27+
<p>
28+
📵
29+
<b>Network Error </b>
30+
<br />
31+
Please check your internet connection.
32+
</p>
33+
<button
34+
onClick={() => setShowPopup(false)}
35+
className="mt-10 bg-transparent text-primary border border-[#DB4444] px-4 py-2 rounded"
36+
>
37+
Close
38+
</button>
39+
</div>
40+
</div>
41+
);
42+
};
43+
export default Popup;

src/main.tsx

+30-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import React from "react";
1+
import React, { useState, useEffect } from "react";
2+
import axios from "axios";
23
import ReactDOM from "react-dom/client";
34
import { Provider } from "react-redux";
45
import "./index.css";
@@ -7,17 +8,34 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
78

89
import store from "./redux/store";
910
import App from "./App";
11+
import Popup from "./components/common/errors/networkErrorPopup";
1012

1113
const client = new QueryClient({});
14+
const Main = () => {
15+
const [showPopup, setShowPopup] = useState(false);
16+
useEffect(() => {
17+
const response = axios
18+
.get(process.env.BASE_URL as string)
19+
.then(() => {
20+
setShowPopup(false);
21+
})
22+
.catch(() => {
23+
setShowPopup(true);
24+
});
25+
}, []);
1226

13-
ReactDOM.createRoot(document.getElementById("root")!).render(
14-
<React.StrictMode>
15-
<QueryClientProvider client={client}>
16-
<Provider store={store}>
17-
<Router>
18-
<App />
19-
</Router>
20-
</Provider>
21-
</QueryClientProvider>
22-
</React.StrictMode>,
23-
);
27+
return (
28+
<React.StrictMode>
29+
<QueryClientProvider client={client}>
30+
<Provider store={store}>
31+
<Router>
32+
{showPopup && <Popup />}
33+
<App />
34+
</Router>
35+
</Provider>
36+
</QueryClientProvider>
37+
</React.StrictMode>
38+
);
39+
};
40+
41+
ReactDOM.createRoot(document.getElementById("root")!).render(<Main />);

src/redux/api/action.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import axios from "axios";
2+
3+
const api = axios.create({
4+
baseURL: process.env.VITE_BASE_URL,
5+
headers: {
6+
"Content-Type": "application/json",
7+
},
8+
});
9+
export default api;

src/redux/api/api.ts

+37-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,41 @@
1-
import axios from "axios";
1+
import { set } from "react-hook-form";
2+
import { useState, useEffect } from "react";
3+
import { toast } from "react-toastify";
24

3-
const api = axios.create({
4-
baseURL: process.env.VITE_BASE_URL,
5-
headers: {
6-
"Content-Type": "application/json",
5+
import store from "../store";
6+
7+
import api from "./action";
8+
9+
let navigateFunction = null;
10+
11+
export const setNavigateFunction = (navigate) => {
12+
navigateFunction = navigate;
13+
};
14+
const redirectToLogin = (navigate) => {
15+
setTimeout(() => {
16+
navigate("/login");
17+
}, 2000);
18+
};
19+
20+
api.interceptors.response.use(
21+
(response) => response,
22+
(error) => {
23+
const excludeRoute = "/";
24+
25+
if (
26+
error.response
27+
&& error.response.status === 401
28+
&& window.location.pathname !== excludeRoute
29+
) {
30+
if (navigateFunction) {
31+
toast("Login is Required for this action \n Redirecting to Login \n ");
32+
setTimeout(() => {
33+
redirectToLogin(navigateFunction);
34+
}, 2000);
35+
}
36+
}
37+
return Promise.reject(error);
738
},
8-
});
39+
);
940

1041
export default api;

src/routes/AppRoutes.tsx

+60-28
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Route, Routes } from "react-router-dom";
1+
import { Route, Routes, useNavigate } from "react-router-dom";
2+
import { useEffect } from "react";
23

34
import RootLayout from "../components/layouts/RootLayout";
45
import Homepage from "../pages/Homepage";
@@ -20,33 +21,64 @@ import Settings from "../dashboard/admin/Settings";
2021
import Analytics from "../dashboard/admin/Analytics";
2122
import Dashboard from "../dashboard/admin/Dashboard";
2223
import CartManagement from "../pages/CartManagement";
24+
import { setNavigateFunction } from "../redux/api/api";
2325

24-
const AppRoutes = () => (
25-
<Routes>
26-
<Route path="/" element={<RootLayout />}>
27-
<Route index element={<Homepage />} />
28-
<Route path="products" element={<ProductPage />} />
29-
<Route path="products/:id" element={<ProductDetails />} />
30-
<Route path="/carts" element={<CartManagement />} />
31-
</Route>
32-
<Route path="/password-reset-link" element={<GetLinkPage />} />
33-
<Route path="/reset-password" element={<ResetPassword />} />
34-
<Route path="/register" element={<RegisterUser />} />
35-
<Route path="/login" element={<Login />} />
36-
<Route path="2fa-verify" element={<OtpVerificationForm />} />
37-
<Route path="/dashboard" element={<SellerDashboard />} />
38-
<Route path="/dashboard/addproduct" element={<AddProduct />} />
39-
<Route path="/update-password" element={<UpdatePasswordPage />} />
40-
<Route path="/profile" element={<UsersProfile />} />
41-
<Route path="/profile/update" element={<UpdateUserProfile />} />
42-
<Route path="/dashboard/products" element={<Products />} />
43-
<Route path="/dashboard/products/:id" element={<AddProduct />} />
44-
<Route path="/admin/dashboard" element={<Dashboard />} />
45-
<Route path="/admin/users" element={<UserManagement />} />
46-
<Route path="/admin/settings" element={<Settings />} />
47-
<Route path="/admin/analytics" element={<Analytics />} />
48-
<Route path="/admin/Products" element={<Products />} />
49-
</Routes>
50-
);
26+
const AppRoutes = () => {
27+
const navigate = useNavigate();
28+
useEffect(() => {
29+
setNavigateFunction(navigate);
30+
}, [navigate]);
31+
32+
const AlreadyLogged = ({ children }) => {
33+
const navigate = useNavigate();
34+
const token = localStorage.getItem("accessToken");
35+
const decodedToken = token ? JSON.parse(atob(token!.split(".")[1])) : {};
36+
const tokenIsValid = decodedToken.id && decodedToken.roleId;
37+
const isSeller = decodedToken.roleId === 2;
38+
39+
useEffect(() => {
40+
if (tokenIsValid) {
41+
isSeller ? navigate("/dashboard") : navigate("/");
42+
}
43+
}, [tokenIsValid, navigate]);
44+
45+
return tokenIsValid ? null : children;
46+
};
47+
48+
return (
49+
<Routes>
50+
<Route path="/" element={<RootLayout />}>
51+
<Route index element={<Homepage />} />
52+
<Route path="products" element={<ProductPage />} />
53+
<Route path="products/:id" element={<ProductDetails />} />
54+
<Route path="/carts" element={<CartManagement />} />
55+
</Route>
56+
<Route path="/password-reset-link" element={<GetLinkPage />} />
57+
<Route path="/reset-password" element={<ResetPassword />} />
58+
<Route path="/register" element={<RegisterUser />} />
59+
<Route
60+
path="/login"
61+
element={(
62+
<AlreadyLogged>
63+
<Login />
64+
</AlreadyLogged>
65+
)}
66+
/>
67+
<Route path="2fa-verify" element={<OtpVerificationForm />} />
68+
<Route path="/dashboard" element={<SellerDashboard />} />
69+
<Route path="/dashboard/addproduct" element={<AddProduct />} />
70+
<Route path="/update-password" element={<UpdatePasswordPage />} />
71+
<Route path="/profile" element={<UsersProfile />} />
72+
<Route path="/profile/update" element={<UpdateUserProfile />} />
73+
<Route path="/dashboard/products" element={<Products />} />
74+
<Route path="/dashboard/products/:id" element={<AddProduct />} />
75+
<Route path="/admin/dashboard" element={<Dashboard />} />
76+
<Route path="/admin/users" element={<UserManagement />} />
77+
<Route path="/admin/settings" element={<Settings />} />
78+
<Route path="/admin/analytics" element={<Analytics />} />
79+
<Route path="/admin/Products" element={<Products />} />
80+
</Routes>
81+
);
82+
};
5183

5284
export default AppRoutes;

0 commit comments

Comments
 (0)