Skip to content

Commit

Permalink
Merge pull request #31 from atlp-rwanda/product-avail-unavail
Browse files Browse the repository at this point in the history
Product should be enebled or disbled
  • Loading branch information
teerenzo authored Jul 29, 2024
2 parents 1186659 + 49c7710 commit da7bd42
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 13 deletions.
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"@commitlint/config-conventional": "^19.2.2",
"@testing-library/dom": "^10.2.0",
"@types/jest": "^29.5.12",
"@types/jwt-decode": "^3.1.0",
"@types/node": "^20.14.8",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
Expand Down
108 changes: 108 additions & 0 deletions src/__test__/registerSlice.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Imports required for testing
import { AxiosError } from "axios";
import { createAsyncThunk } from "@reduxjs/toolkit";

import axios from "../redux/api/api";
import { createUser, verifyUser } from "../redux/reducers/registerSlice";

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

describe("registerSlice Thunks", () => {
const mockUser = {
name: "John Doe",
username: "johndoe",
email: "[email protected]",
password: "password123",
};

const mockResponseData = { message: "User registered successfully" };
const mockToken = "validToken123";

afterEach(() => {
jest.resetAllMocks();
});

describe("createUser thunk", () => {
it("should handle fulfilled case", async () => {
// @ts-ignore
axios.post.mockResolvedValueOnce({ data: mockResponseData });

const thunk = createUser(mockUser);
const dispatch = jest.fn();
const getState = jest.fn();

await thunk(dispatch, getState, null);
expect(dispatch).toHaveBeenCalled();
});

it("should handle rejected case with response error data", async () => {
const errorMessage = { error: "Registration failed" };
// @ts-ignore
axios.post.mockRejectedValueOnce({
response: { data: errorMessage },
message: "Request failed",
});

const thunk = createUser(mockUser);
const dispatch = jest.fn();
const getState = jest.fn();

await thunk(dispatch, getState, null);
expect(dispatch).toHaveBeenCalled();
});

it("should handle rejected case without response error data", async () => {
// @ts-ignore
axios.post.mockRejectedValueOnce(new Error("Network Error"));

const thunk = createUser(mockUser);
const dispatch = jest.fn();
const getState = jest.fn();

await thunk(dispatch, getState, null);
expect(dispatch).toHaveBeenCalled();
});
});

describe("verifyUser thunk", () => {
it("should handle fulfilled case", async () => {
// @ts-ignore
axios.get.mockResolvedValueOnce({ data: true });

const thunk = verifyUser(mockToken);
const dispatch = jest.fn();
const getState = jest.fn();

await thunk(dispatch, getState, null);
expect(dispatch).toHaveBeenCalled();
});

it("should handle rejected case with response error data", async () => {
const errorMessage = { error: "Verification failed" };
// @ts-ignore
axios.get.mockRejectedValueOnce({
response: { data: errorMessage },
message: "Request failed",
});

const thunk = verifyUser(mockToken);
const dispatch = jest.fn();
const getState = jest.fn();

await thunk(dispatch, getState, null);
expect(dispatch).toHaveBeenCalled();
});

it("should handle rejected case without response error data", async () => {
// @ts-ignore
axios.get.mockRejectedValueOnce(new Error("Network Error"));

const thunk = verifyUser(mockToken);
const dispatch = jest.fn();
const getState = jest.fn();

await thunk(dispatch, getState, null);
expect(dispatch).toHaveBeenCalled();
});
});
});
4 changes: 3 additions & 1 deletion src/components/dashboard/ConfirmModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ interface ConfirmDeleteModalProps {
message: string;
product: any;
loading: boolean;
text: string;
}

const ConfirmModal: React.FC<ConfirmDeleteModalProps> = ({
Expand All @@ -14,6 +15,7 @@ const ConfirmModal: React.FC<ConfirmDeleteModalProps> = ({
message,
product,
loading,
text,
}) => (
<div className="fixed inset-0 z-40 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white py-8 px-6 rounded-lg duration-75 animate-fadeIn">
Expand All @@ -40,7 +42,7 @@ const ConfirmModal: React.FC<ConfirmDeleteModalProps> = ({
className="bg-red-600 text-white px-4 py-2 rounded"
onClick={onConfirm}
>
{loading ? "Loading..." : "Delete"}
{loading ? "Loading..." : text}
</button>
</div>
</div>
Expand Down
50 changes: 46 additions & 4 deletions src/components/dashboard/products/ProductsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Spinner from "../Spinner";
import {
deleteProduct,
fetchProducts,
isProductAvailable,
} from "../../../redux/reducers/productsSlice";
import { useAppDispatch } from "../../../redux/hooks";
import ToggleSwitch from "../ToggleSwitch";
Expand All @@ -31,12 +32,16 @@ const ProductsTable: React.FC = () => {

useEffect(() => {
const sorted = [...products].sort(
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
(a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
);
setSortedProducts(sorted);
}, [products]);

const [confirmDeleteModal, setConfirmModal] = useState(false);
const [confirmTaggleModal, setConfirmTaggleModal] = useState(false);
const [isAvailable, setIsAvailable] = useState(true);
const [taggleLoading, setTaggleLoading] = useState(false);

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

const handleToggle = (id: number) => {
console.log("id", id);
const handleCancelTaggle = () => {
setConfirmTaggleModal(false);
};

const handleTaggleClick = (product: any) => {
setSelectedProduct(product);
setConfirmTaggleModal(true);
};

const handleToggle = async () => {
if (selectedProduct !== null) {
try {
setTaggleLoading(true);
const response = await dispatch(
// @ts-ignore
isProductAvailable(selectedProduct.id),
).unwrap();
setTaggleLoading(false);
setIsAvailable(false);
setConfirmTaggleModal(false);
await dispatch(fetchProducts());
toast.success(
`Product was successfully ${isAvailable ? "Disabled" : "Enabled"}`,
);
} catch (err: any) {
toast.error(err.message);
}
}
};

const handleCancelDelete = () => {
Expand Down Expand Up @@ -166,7 +197,7 @@ const ProductsTable: React.FC = () => {
<td className="py-3 px-4">
<ToggleSwitch
checked={item.isAvailable}
onChange={() => handleToggle(item.id)}
onChange={() => handleTaggleClick(item)}
/>
</td>
<td className="px-4 py-2 whitespace-nowrap">
Expand Down Expand Up @@ -232,10 +263,21 @@ const ProductsTable: React.FC = () => {
onConfirm={handleConfirmDelete}
product={selectedProduct}
loading={loadingDelete}
text="Delete"
onCancel={handleCancelDelete}
message="Are you sure you want to delete this product?"
/>
)}
{confirmTaggleModal && (
<ConfirmModal
onConfirm={handleToggle}
product={selectedProduct}
loading={taggleLoading}
text="Confirm"
onCancel={handleCancelTaggle}
message={`Are you sure you want to ${isAvailable ? "disable" : "enable"} this product?`}
/>
)}
</div>
);
};
Expand Down
6 changes: 0 additions & 6 deletions src/pages/Wishes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,7 @@ const BuyerWishesList: React.FC = () => {
</p>
);
}
if (!loggedInUserToken) {
navigate("/login");
}

if (loggedInUserToken && loggedInUser.roleId !== 1) {
navigate("/");
}
return (
<div className="w-full px-[2%] md:px-[4%]">
<ToastContainer />
Expand Down
24 changes: 23 additions & 1 deletion src/redux/reducers/productsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,26 @@ SearchParams
},
);

export const isProductAvailable = createAsyncThunk(
"products/avail",
async (id: number, { rejectWithValue }) => {
try {
const response = await api.patch(
`/products/${id}/status`,
{},
{
headers: {
Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
},
},
);
return response.data;
} catch (err) {
return rejectWithValue(err);
}
},
);

const initialState: ProductsState = {
loading: false,
data: [],
Expand All @@ -96,7 +116,9 @@ const productsSlice = createSlice({
extraReducers: (builder) => {
builder
.addCase(fetchProducts.pending, (state) => {
state.loading = true;
if (state.data.length === 0) {
state.loading = true;
}
})
.addCase(fetchProducts.fulfilled, (state, action) => {
state.loading = false;
Expand Down
9 changes: 8 additions & 1 deletion src/redux/reducers/wishListSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ void,
});
return response.data.wishes;
} catch (error: any) {
return rejectWithValue(error.message || "Failed to fetch wishes");
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError;
return rejectWithValue(
// @ts-ignore
axiosError.response?.data?.message ?? "Unknown error occurred",
);
}
return rejectWithValue("Unknown error occurred");
}
});

Expand Down
24 changes: 24 additions & 0 deletions src/utils/isTokenExpired.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { decodeToken } from "react-jwt";

interface DecodedToken {
exp: number;
[key: string]: any;
}

const isTokenExpired = (token: string | null) => {
if (!token) {
return true;
}
try {
const decodedToken = decodeToken<DecodedToken>(token);
const currentTime = Date.now() / 1000;
if (decodedToken) {
return decodedToken.exp < currentTime;
}
return false;
} catch (error) {
return true;
}
};

export default isTokenExpired;

0 comments on commit da7bd42

Please sign in to comment.