Skip to content

Commit da7bd42

Browse files
authored
Merge pull request #31 from atlp-rwanda/product-avail-unavail
Product should be enebled or disbled
2 parents 1186659 + 49c7710 commit da7bd42

File tree

9 files changed

+224
-13
lines changed

9 files changed

+224
-13
lines changed

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"@commitlint/config-conventional": "^19.2.2",
7171
"@testing-library/dom": "^10.2.0",
7272
"@types/jest": "^29.5.12",
73+
"@types/jwt-decode": "^3.1.0",
7374
"@types/node": "^20.14.8",
7475
"@types/react": "^18.2.66",
7576
"@types/react-dom": "^18.2.22",
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Imports required for testing
2+
import { AxiosError } from "axios";
3+
import { createAsyncThunk } from "@reduxjs/toolkit";
4+
5+
import axios from "../redux/api/api";
6+
import { createUser, verifyUser } from "../redux/reducers/registerSlice";
7+
8+
jest.mock("../redux/api/api");
9+
10+
describe("registerSlice Thunks", () => {
11+
const mockUser = {
12+
name: "John Doe",
13+
username: "johndoe",
14+
15+
password: "password123",
16+
};
17+
18+
const mockResponseData = { message: "User registered successfully" };
19+
const mockToken = "validToken123";
20+
21+
afterEach(() => {
22+
jest.resetAllMocks();
23+
});
24+
25+
describe("createUser thunk", () => {
26+
it("should handle fulfilled case", async () => {
27+
// @ts-ignore
28+
axios.post.mockResolvedValueOnce({ data: mockResponseData });
29+
30+
const thunk = createUser(mockUser);
31+
const dispatch = jest.fn();
32+
const getState = jest.fn();
33+
34+
await thunk(dispatch, getState, null);
35+
expect(dispatch).toHaveBeenCalled();
36+
});
37+
38+
it("should handle rejected case with response error data", async () => {
39+
const errorMessage = { error: "Registration failed" };
40+
// @ts-ignore
41+
axios.post.mockRejectedValueOnce({
42+
response: { data: errorMessage },
43+
message: "Request failed",
44+
});
45+
46+
const thunk = createUser(mockUser);
47+
const dispatch = jest.fn();
48+
const getState = jest.fn();
49+
50+
await thunk(dispatch, getState, null);
51+
expect(dispatch).toHaveBeenCalled();
52+
});
53+
54+
it("should handle rejected case without response error data", async () => {
55+
// @ts-ignore
56+
axios.post.mockRejectedValueOnce(new Error("Network Error"));
57+
58+
const thunk = createUser(mockUser);
59+
const dispatch = jest.fn();
60+
const getState = jest.fn();
61+
62+
await thunk(dispatch, getState, null);
63+
expect(dispatch).toHaveBeenCalled();
64+
});
65+
});
66+
67+
describe("verifyUser thunk", () => {
68+
it("should handle fulfilled case", async () => {
69+
// @ts-ignore
70+
axios.get.mockResolvedValueOnce({ data: true });
71+
72+
const thunk = verifyUser(mockToken);
73+
const dispatch = jest.fn();
74+
const getState = jest.fn();
75+
76+
await thunk(dispatch, getState, null);
77+
expect(dispatch).toHaveBeenCalled();
78+
});
79+
80+
it("should handle rejected case with response error data", async () => {
81+
const errorMessage = { error: "Verification failed" };
82+
// @ts-ignore
83+
axios.get.mockRejectedValueOnce({
84+
response: { data: errorMessage },
85+
message: "Request failed",
86+
});
87+
88+
const thunk = verifyUser(mockToken);
89+
const dispatch = jest.fn();
90+
const getState = jest.fn();
91+
92+
await thunk(dispatch, getState, null);
93+
expect(dispatch).toHaveBeenCalled();
94+
});
95+
96+
it("should handle rejected case without response error data", async () => {
97+
// @ts-ignore
98+
axios.get.mockRejectedValueOnce(new Error("Network Error"));
99+
100+
const thunk = verifyUser(mockToken);
101+
const dispatch = jest.fn();
102+
const getState = jest.fn();
103+
104+
await thunk(dispatch, getState, null);
105+
expect(dispatch).toHaveBeenCalled();
106+
});
107+
});
108+
});

src/components/dashboard/ConfirmModal.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ interface ConfirmDeleteModalProps {
66
message: string;
77
product: any;
88
loading: boolean;
9+
text: string;
910
}
1011

1112
const ConfirmModal: React.FC<ConfirmDeleteModalProps> = ({
@@ -14,6 +15,7 @@ const ConfirmModal: React.FC<ConfirmDeleteModalProps> = ({
1415
message,
1516
product,
1617
loading,
18+
text,
1719
}) => (
1820
<div className="fixed inset-0 z-40 flex items-center justify-center bg-black bg-opacity-50">
1921
<div className="bg-white py-8 px-6 rounded-lg duration-75 animate-fadeIn">
@@ -40,7 +42,7 @@ const ConfirmModal: React.FC<ConfirmDeleteModalProps> = ({
4042
className="bg-red-600 text-white px-4 py-2 rounded"
4143
onClick={onConfirm}
4244
>
43-
{loading ? "Loading..." : "Delete"}
45+
{loading ? "Loading..." : text}
4446
</button>
4547
</div>
4648
</div>

src/components/dashboard/products/ProductsTable.tsx

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Spinner from "../Spinner";
1313
import {
1414
deleteProduct,
1515
fetchProducts,
16+
isProductAvailable,
1617
} from "../../../redux/reducers/productsSlice";
1718
import { useAppDispatch } from "../../../redux/hooks";
1819
import ToggleSwitch from "../ToggleSwitch";
@@ -31,12 +32,16 @@ const ProductsTable: React.FC = () => {
3132

3233
useEffect(() => {
3334
const sorted = [...products].sort(
34-
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
35+
(a, b) =>
36+
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
3537
);
3638
setSortedProducts(sorted);
3739
}, [products]);
3840

3941
const [confirmDeleteModal, setConfirmModal] = useState(false);
42+
const [confirmTaggleModal, setConfirmTaggleModal] = useState(false);
43+
const [isAvailable, setIsAvailable] = useState(true);
44+
const [taggleLoading, setTaggleLoading] = useState(false);
4045

4146
const [currentPage, setCurrentPage] = useState(1);
4247
const [itemsPerPage, setItemsPerPage] = useState(5);
@@ -59,8 +64,34 @@ const ProductsTable: React.FC = () => {
5964
setCurrentPage(1);
6065
};
6166

62-
const handleToggle = (id: number) => {
63-
console.log("id", id);
67+
const handleCancelTaggle = () => {
68+
setConfirmTaggleModal(false);
69+
};
70+
71+
const handleTaggleClick = (product: any) => {
72+
setSelectedProduct(product);
73+
setConfirmTaggleModal(true);
74+
};
75+
76+
const handleToggle = async () => {
77+
if (selectedProduct !== null) {
78+
try {
79+
setTaggleLoading(true);
80+
const response = await dispatch(
81+
// @ts-ignore
82+
isProductAvailable(selectedProduct.id),
83+
).unwrap();
84+
setTaggleLoading(false);
85+
setIsAvailable(false);
86+
setConfirmTaggleModal(false);
87+
await dispatch(fetchProducts());
88+
toast.success(
89+
`Product was successfully ${isAvailable ? "Disabled" : "Enabled"}`,
90+
);
91+
} catch (err: any) {
92+
toast.error(err.message);
93+
}
94+
}
6495
};
6596

6697
const handleCancelDelete = () => {
@@ -166,7 +197,7 @@ const ProductsTable: React.FC = () => {
166197
<td className="py-3 px-4">
167198
<ToggleSwitch
168199
checked={item.isAvailable}
169-
onChange={() => handleToggle(item.id)}
200+
onChange={() => handleTaggleClick(item)}
170201
/>
171202
</td>
172203
<td className="px-4 py-2 whitespace-nowrap">
@@ -232,10 +263,21 @@ const ProductsTable: React.FC = () => {
232263
onConfirm={handleConfirmDelete}
233264
product={selectedProduct}
234265
loading={loadingDelete}
266+
text="Delete"
235267
onCancel={handleCancelDelete}
236268
message="Are you sure you want to delete this product?"
237269
/>
238270
)}
271+
{confirmTaggleModal && (
272+
<ConfirmModal
273+
onConfirm={handleToggle}
274+
product={selectedProduct}
275+
loading={taggleLoading}
276+
text="Confirm"
277+
onCancel={handleCancelTaggle}
278+
message={`Are you sure you want to ${isAvailable ? "disable" : "enable"} this product?`}
279+
/>
280+
)}
239281
</div>
240282
);
241283
};

src/pages/Wishes.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,7 @@ const BuyerWishesList: React.FC = () => {
7979
</p>
8080
);
8181
}
82-
if (!loggedInUserToken) {
83-
navigate("/login");
84-
}
8582

86-
if (loggedInUserToken && loggedInUser.roleId !== 1) {
87-
navigate("/");
88-
}
8983
return (
9084
<div className="w-full px-[2%] md:px-[4%]">
9185
<ToastContainer />

src/redux/reducers/productsSlice.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,26 @@ SearchParams
7979
},
8080
);
8181

82+
export const isProductAvailable = createAsyncThunk(
83+
"products/avail",
84+
async (id: number, { rejectWithValue }) => {
85+
try {
86+
const response = await api.patch(
87+
`/products/${id}/status`,
88+
{},
89+
{
90+
headers: {
91+
Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
92+
},
93+
},
94+
);
95+
return response.data;
96+
} catch (err) {
97+
return rejectWithValue(err);
98+
}
99+
},
100+
);
101+
82102
const initialState: ProductsState = {
83103
loading: false,
84104
data: [],
@@ -96,7 +116,9 @@ const productsSlice = createSlice({
96116
extraReducers: (builder) => {
97117
builder
98118
.addCase(fetchProducts.pending, (state) => {
99-
state.loading = true;
119+
if (state.data.length === 0) {
120+
state.loading = true;
121+
}
100122
})
101123
.addCase(fetchProducts.fulfilled, (state, action) => {
102124
state.loading = false;

src/redux/reducers/wishListSlice.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,14 @@ void,
4444
});
4545
return response.data.wishes;
4646
} catch (error: any) {
47-
return rejectWithValue(error.message || "Failed to fetch wishes");
47+
if (axios.isAxiosError(error)) {
48+
const axiosError = error as AxiosError;
49+
return rejectWithValue(
50+
// @ts-ignore
51+
axiosError.response?.data?.message ?? "Unknown error occurred",
52+
);
53+
}
54+
return rejectWithValue("Unknown error occurred");
4855
}
4956
});
5057

src/utils/isTokenExpired.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { decodeToken } from "react-jwt";
2+
3+
interface DecodedToken {
4+
exp: number;
5+
[key: string]: any;
6+
}
7+
8+
const isTokenExpired = (token: string | null) => {
9+
if (!token) {
10+
return true;
11+
}
12+
try {
13+
const decodedToken = decodeToken<DecodedToken>(token);
14+
const currentTime = Date.now() / 1000;
15+
if (decodedToken) {
16+
return decodedToken.exp < currentTime;
17+
}
18+
return false;
19+
} catch (error) {
20+
return true;
21+
}
22+
};
23+
24+
export default isTokenExpired;

0 commit comments

Comments
 (0)