Skip to content

Commit

Permalink
feat(wishes):buyer should be able wish a products
Browse files Browse the repository at this point in the history
-Buyer should wish a products
-Buyer should add a wished products to cart
-Seller should be able to see a wished products

[Deliver #187419141]
  • Loading branch information
niyobertin committed Jul 21, 2024
1 parent a738f89 commit 763fa5c
Show file tree
Hide file tree
Showing 14 changed files with 388 additions and 40 deletions.
84 changes: 84 additions & 0 deletions src/__test__/improveTest.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import MockAdapter from "axios-mock-adapter";
import { configureStore } from "@reduxjs/toolkit";

import cartReducer, {
cartManage,
cartDelete,
addToCart,
removeFromCart,
updateCarts,
increaseQuantity,
decreaseQuantity,
} from "../redux/reducers/cartSlice";
import api from "../redux/api/action";

const mock = new MockAdapter(api);

describe("test improvement on cart", () => {
beforeEach(() => {
mock.reset();
});

it("cartManage dispatches fulfilled action when data is returned", async () => {
const store = configureStore({ reducer: { carts: cartReducer } });
const mockCartItems = [{ id: 1, name: "Item 1", quantity: 1 }];
mock.onGet("/carts").reply(200, { userCart: { items: mockCartItems } });

await store.dispatch(cartManage());
const state = store.getState();
expect(state.carts.data).toEqual(mockCartItems);
expect(state.carts.isLoading).toBe(false);
expect(state.carts.error).toBe(false);
});

it("cartManage dispatches rejected action on failed request", async () => {
const store = configureStore({ reducer: { carts: cartReducer } });
mock.onGet("/carts").networkError();

await store.dispatch(cartManage());
const state = store.getState();
expect(state.carts.data).toEqual([]);
expect(state.carts.isLoading).toBe(false);
expect(state.carts.error).toBe(true);
});

it("cartDelete dispatches rejected action on failed request", async () => {
const store = configureStore({ reducer: { carts: cartReducer } });
mock.onDelete("/carts").networkError();

await store.dispatch(cartDelete());
const state = store.getState();
expect(state.carts.delete.error).toBe(true);
});

it("addToCart dispatches fulfilled action when item is added", async () => {
const store = configureStore({ reducer: { carts: cartReducer } });
const newCartItem = { id: 2, name: "Item 2", quantity: 2 };
mock.onPost("/carts").reply(200, newCartItem);

await store.dispatch(addToCart({ productId: 2, quantity: 2 }));
const state = store.getState();
expect(state.carts.data).toContainEqual(newCartItem);
expect(state.carts.add.isLoading).toBe(false);
expect(state.carts.add.error).toBeNull();
});

it("addToCart dispatches rejected action on failed request", async () => {
const store = configureStore({ reducer: { carts: cartReducer } });
mock.onPost("/carts").reply(500, { message: "Internal Server Error" });

await store.dispatch(addToCart({ productId: 999, quantity: 1 }));
const state = store.getState();
expect(state.carts.add.isLoading).toBe(false);
});

it("removeFromCart dispatches fulfilled action when item is removed", async () => {
const store = configureStore({ reducer: { carts: cartReducer } });
const productId = 2;
mock.onPut("/carts").reply(200, { id: productId });
// @ts-ignore
await store.dispatch(removeFromCart(productId));
const state = store.getState();
expect(state.carts.remove.error).toBe(false);
});
});
106 changes: 106 additions & 0 deletions src/__test__/wishListSlice.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import MockAdapter from "axios-mock-adapter";
import { configureStore } from "@reduxjs/toolkit";

import wishListSlice, {
fetchWishes,
addWish,
deleteWish,
} from "../redux/reducers/wishListSlice";
import api from "../redux/api/action";

const mock = new MockAdapter(api);

describe("wishesSlice test", () => {
beforeEach(() => {
mock.reset();
});

it("fetchWishes dispatches fulfilled action when data is returned", async () => {
const store = configureStore({ reducer: { wishes: wishListSlice } });
const mockWishes = [
{
id: 1,
product: {
id: 1,
name: "Product",
images: [],
stockQuantity: 10,
price: 100,
},
user: { name: "User", userName: "username", email: "[email protected]" },
},
];
mock.onGet(`/wishes`).reply(200, { wishes: mockWishes });

await store.dispatch(fetchWishes());
const state = store.getState();
expect(state.wishes.wishes).toEqual(mockWishes);
expect(state.wishes.isLoading).toBe(false);
expect(state.wishes.error).toBeNull();
});

it("fetchWishes dispatches rejected action on failed request", async () => {
const store = configureStore({ reducer: { wishes: wishListSlice } });
mock.onGet(`/wishes`).networkError();

await store.dispatch(fetchWishes());
const state = store.getState();
expect(state.wishes.wishes).toEqual([]);
expect(state.wishes.isLoading).toBe(false);
});

it("addWish dispatches fulfilled action when a wish is added", async () => {
const store = configureStore({ reducer: { wishes: wishListSlice } });
const newWish = {
id: 2,
product: {
id: 2,
name: "Product2",
images: [],
stockQuantity: 5,
price: 200,
},
user: { name: "User2", userName: "user2", email: "[email protected]" },
};
mock.onPost(`/wishes`).reply(200, newWish);

await store.dispatch(addWish({ productId: 2 }));
const state = store.getState();
expect(state.wishes.wishes).toContainEqual(newWish);
expect(state.wishes.isLoading).toBe(false);
expect(state.wishes.error).toBeNull();
});

it("addWish dispatches rejected action on failed request", async () => {
const store = configureStore({ reducer: { wishes: wishListSlice } });
mock.onPost(`/wishes`).reply(500, { message: "Internal Server Error" });

await store.dispatch(addWish({ productId: 999 }));
const state = store.getState();
expect(state.wishes.isLoading).toBe(false);
expect(state.wishes.error).toEqual("Internal Server Error");
});

it("deleteWish dispatches fulfilled action when a wish is removed", async () => {
const store = configureStore({ reducer: { wishes: wishListSlice } });
mock.onDelete(`/products/2/wishes`).reply(200);

await store.dispatch(deleteWish({ productId: 2 }));
const state = store.getState();
expect(state.wishes.wishes).not.toContainEqual(
expect.objectContaining({ id: 2 }),
);
expect(state.wishes.isLoading).toBe(false);
expect(state.wishes.error).toBeNull();
});

it("deleteWish dispatches rejected action on failed request", async () => {
const store = configureStore({ reducer: { wishes: wishListSlice } });
mock.onDelete(`/products/999/wishes`).reply(404, { message: "Not Found" });

await store.dispatch(deleteWish({ productId: 999 }));
const state = store.getState();
expect(state.wishes.isLoading).toBe(false);
expect(state.wishes.error).toEqual("Not Found");
});
});
9 changes: 7 additions & 2 deletions src/components/cards/ProductCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ const ProductCard: React.FC<IProductCardProps> = ({ product }) => {
const { wishes } = useSelector((state: RootState) => state.wishes);
const dispatch = useAppDispatch();
const navigate = useNavigate();

const loggedInUserToken = localStorage.getItem("accessToken");
let loggedInUser;
if (loggedInUserToken) {
// @ts-ignore
loggedInUser = JSON.parse(atob(loggedInUserToken.split(".")[1]));
}
const formatPrice = (price: number) => {
if (price < 1000) {
return price.toString();
Expand Down Expand Up @@ -216,7 +221,7 @@ const ProductCard: React.FC<IProductCardProps> = ({ product }) => {
className="bg-white"
sx={{ paddingY: 0.5, paddingX: 0.5 }}
>
{!localStorage.getItem("accessToken") ? (
{!loggedInUserToken || loggedInUser.roleId !== 1 ? (
""
) : loadWishes ? (
<Spinner color="pink" aria-label="Pink spinner example" />
Expand Down
13 changes: 9 additions & 4 deletions src/components/common/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,22 @@ interface ISerachProps {

const Header: React.FC<ISerachProps> = ({ searchQuery, setSearchQuery }) => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const { profile } = useSelector((state: RootState) => state.usersProfile);
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
};

let userInfo;
const accessToken = localStorage.getItem("accessToken");
if (accessToken) {
userInfo = JSON.parse(atob(accessToken.split(".")[1]));
}
const dispatch = useAppDispatch();

useEffect(() => {
// @ts-ignore
dispatch(getProfile());
}, [dispatch]);

useEffect(() => {
if (accessToken) {
try {
Expand Down Expand Up @@ -106,7 +111,7 @@ const Header: React.FC<ISerachProps> = ({ searchQuery, setSearchQuery }) => {
""
) : (
<div className="absolute w-5 h-5 bg-red-500 -top-3 -right-3 rounded-full text-center text-white text-[12px] flex justify-center items-center">
<span className="">{userCart.length}</span>
<span className="">{userCart && userCart.length}</span>
</div>
)}
</Stack>
Expand All @@ -117,10 +122,10 @@ const Header: React.FC<ISerachProps> = ({ searchQuery, setSearchQuery }) => {
<CiUser className="text-[24px] text-black" />
<Stack className="flex flex-col">
<span className="ml-2 font-semibold text-[12px]">
{userInfo.name}
{profile && profile.fullName}
</span>
<span className="ml-2 font-semibold text-[12px]">
{userInfo.email}
{profile && profile?.email}
</span>
</Stack>
</Link>
Expand Down
7 changes: 4 additions & 3 deletions src/components/dashboard/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ interface SidebarProps {
const SideBar: React.FC<SidebarProps> = ({ isOpen }) => {
const location = useLocation();

const getLinkClass = (path: string) => (location.pathname === path ? "text-primary" : "text-dark-gray");
const getLinkClass = (path: string) =>
(location.pathname === path ? "text-primary" : "text-dark-gray");

return (
<div
Expand Down Expand Up @@ -71,9 +72,9 @@ const SideBar: React.FC<SidebarProps> = ({ isOpen }) => {
</p>
</NavLink>
<NavLink
to="/wishes"
to="/dashboard/wishes"
className={`py-2.5 px-4 flex items-center gap-2 text-lg rounded transition duration-200 ${getLinkClass(
"/wishes",
"/dashboard/wishes",
)}`}
>
<MdInsertChartOutlined className="text-xl" />
Expand Down
100 changes: 100 additions & 0 deletions src/components/dashboard/wishesTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";

import { fetchWishes } from "../../redux/reducers/wishListSlice";
import { RootState, AppDispatch } from "../../redux/store";

import Spinner from "./Spinner";

const WishesTable: React.FC = () => {
const dispatch: AppDispatch = useDispatch();
const { wishes, isLoading, error } = useSelector(
(state: RootState) => state.wishes,
);
useEffect(() => {
const fetchData = async () => {
try {
await dispatch(fetchWishes());
} catch (error) {
console.error(error);
}
};

fetchData();
}, [dispatch]);

return (
<div className="relative">
<div className="overflow-x-scroll ">
<table className="min-w-full bg-white border-collapse">
<thead className="border-b-2">
<tr className="px-3 bg-light-blue rounded-lg">
<th className="pr-6 rounded-l-[12px] py-2 pl-4 text-sm font-medium text-left whitespace-nowrap text-black">
Product
</th>
<th className="py-2 px-4 text-sm font-medium text-left text-black whitespace-nowrap">
Stock Quantity
</th>
<th className="py-2 px-4 text-sm font-medium text-left text-black whitespace-nowrap">
Price
</th>
<th className="py-2 px-4 text-sm font-medium text-left text-black whitespace-nowrap">
Buyer
</th>
<th className="py-2 px-4 text-sm font-medium text-left text-black whitespace-nowrap">
Email
</th>
</tr>
</thead>
<tbody>
{isLoading ? (
<tr>
<td colSpan={9} className="text-center py-4">
<Spinner />
</td>
</tr>
) : wishes.length === 0 ? (
<tr>
<td
colSpan={6}
className="text-center py-10 text-dark-gray dark:text-white"
>
No products found.
</td>
</tr>
) : (
wishes.map((item) => (
<tr key={item.id} className="px-2">
<td className="pl-4 pr-10 xl:pr-10 py-2 flex items-center whitespace-nowrap space-x-3">
<img
src={item.product.images[0]}
alt={item.product.name}
className="w-10 h-10 object-cover mr-2"
/>
<span className="text-dark-gray text-sm">
{item?.product.name}
</span>
</td>
<td className="py-3 px-4 text-gray-700 text-sm">
{item.product.stockQuantity}
</td>
<td className="py-3 px-4 text-gray-700 text-sm">
{item.product.price}
</td>
<td className="py-3 px-4 text-gray-700 text-sm whitespace-nowrap">
{item.user.name}
</td>
<td className="py-3 px-4 text-sm text-gray-700">
{item.user.email}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
);
};

export default WishesTable;
Loading

0 comments on commit 763fa5c

Please sign in to comment.