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 24, 2024
1 parent 1614227 commit 875a081
Show file tree
Hide file tree
Showing 17 changed files with 653 additions and 123 deletions.
179 changes: 105 additions & 74 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"axios-mock-adapter": "^1.22.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"expect-puppeteer": "^10.0.0",
"flowbite-react": "^0.10.1",
"gsap": "^3.12.5",
"install": "^0.13.0",
"jest-environment-jsdom": "^29.7.0",
Expand Down
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");
});
});
21 changes: 13 additions & 8 deletions src/components/cards/ProductCard.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { IconButton, Rating, Typography } from "@mui/material";
import { FaEye } from "react-icons/fa";
import { CiHeart } from "react-icons/ci";
import { IoHeartSharp } from "react-icons/io5";
import React, { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { ToastContainer, toast } from "react-toastify";
import { AxiosError } from "axios";
import { useSelector } from "react-redux";
import { Spinner } from "flowbite-react";

import Spinner from "../dashboard/Spinner";
import {
addWish,
fetchWishes,
Expand Down 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,18 +221,18 @@ 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" />
<Spinner />
) : alreadyWished ? (
<CiHeart
className="text-white bg-[#DB4444] p-2 rounded-full text-[30px]"
<IoHeartSharp
className="text-[#DB4444] bg-white p-2 rounded-full text-[30px]"
data-testid="like-btn"
onClick={handleAddWish}
/>
) : (
<CiHeart
<IoHeartSharp
className="text-black bg-white p-2 rounded-full text-[30px]"
data-testid="like-btn"
onClick={handleAddWish}
Expand Down
32 changes: 29 additions & 3 deletions src/components/common/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Link } from "react-router-dom";
import { useSelector } from "react-redux";
import { IoMdHeartEmpty } from "react-icons/io";

import { fetchWishes } from "../../../redux/reducers/wishListSlice";
import { getProfile } from "../../../redux/reducers/profileSlice";
import Logo from "../auth/Logo";
import { RootState } from "../../../redux/store";
Expand All @@ -23,17 +24,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 @@ -64,6 +70,18 @@ const Header: React.FC<ISerachProps> = ({ searchQuery, setSearchQuery }) => {
dispatch(cartManage());
}, [dispatches]);
const userCart = useSelector((state: RootState) => state.cart.data);

useEffect(() => {
const fetchData = async () => {
try {
await dispatch(fetchWishes());
} catch (error) {
console.error(error);
}
};
fetchData();
}, [dispatch]);
const { wishes } = useSelector((state: RootState) => state.wishes);
return (
<Stack className="px-[5%] bg-white w-full relative" id="header">
<Stack className=" justify-between gap-64 items-center" direction="row">
Expand Down Expand Up @@ -109,15 +127,23 @@ const Header: React.FC<ISerachProps> = ({ searchQuery, setSearchQuery }) => {
</Stack>
</Stack>
<Stack>
<IoMdHeartEmpty className="text-[24px] text-black" />
{localStorage.getItem("accessToken") && userInfo.roleId === 2 ? (
<Link to="dashboard/wishes">
<IoMdHeartEmpty className="text-[24px] cursor-pointer text-black" />
</Link>
) : (
<Link to="/wishes">
<IoMdHeartEmpty className="text-[24px] cursor-pointer text-black" />
</Link>
)}
</Stack>
<Stack className="">
{isLoggedIn ? (
<Link to="/profile" className="flex items-center">
<LuUser className="text-[24px] text-black" />
<Stack className="flex flex-col">
<span className="ml-2 font-semibold text-[12px]">
{userInfo.name.split(" ")[0]}
{profile && profile.fullName}
</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
Loading

0 comments on commit 875a081

Please sign in to comment.