Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cart Page and Add to Cart #111

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@types/swiper": "^6.0.0",
"axios": "^1.7.2",
"axios-mock-adapter": "^1.22.0",
"chart.js": "^4.4.3",
"date-fns": "^3.6.0",
"chart.js": "^4.4.3",
"cloudinary": "^2.2.0",
Expand Down Expand Up @@ -85,7 +86,7 @@
"eslint-plugin-react": "^7.34.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"husky": "^9.0.11",
"husky": "^9.1.1",
"jest": "^29.7.0",
"jsdom": "^24.1.0",
"lint-staged": "^15.2.5",
Expand Down
5 changes: 5 additions & 0 deletions public/mastercard.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions public/visa.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 81 additions & 0 deletions src/__test__/Cart/Cart.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { configureStore } from '@reduxjs/toolkit';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import cartReducer, {
fetchCartItems,
addCartItem,
updateCartItemQuantity,
removeCartItem,
} from '@/features/Cart/cartSlice';

describe('cartSlice', () => {
let store = configureStore({ reducer: { cartItems: cartReducer } });
let httpMock: MockAdapter;

beforeEach(() => {
store = configureStore({ reducer: { cartItems: cartReducer } });
httpMock = new MockAdapter(axios);
});

afterEach(() => {
httpMock.reset();
});

it('should fetch cart items successfully', async () => {
const mockCartItems = [{ id: 1, name: 'Product 1', quantity: 2 }];
httpMock
.onGet(`${process.env.VITE_BASE_URL}/cart`)
.reply(200, { cartItems: mockCartItems });

await store.dispatch(fetchCartItems());
const state = store.getState().cartItems;
expect(state.cartItems).toEqual(mockCartItems);
expect(state.loading).toBe(false);
expect(state.error).toBeNull();
});

it('should handle adding a cart item', async () => {
const newCartItem = { id: 2, name: 'Product 2', quantity: 1 };
httpMock
.onPost(`${process.env.VITE_BASE_URL}/cart`)
.reply(200, { cartItem: newCartItem });

await store.dispatch(addCartItem({ productId: 2, quantity: 1 }));
const state = store.getState().cartItems;
expect(state.cartItems).toContainEqual(newCartItem);
expect(state.loading).toBe(false);
expect(state.error).toBeNull();
});

it('should handle updating a cart item quantity', async () => {
const mockCartItems = [{ id: 1, name: 'Product 1', quantity: 20 }];
httpMock
.onGet(`${process.env.VITE_BASE_URL}/cart`)
.reply(200, { cartItems: mockCartItems });

await store.dispatch(fetchCartItems());
const updatedCartItem = { id: 1, quantity: 3 };
httpMock.onPatch(`${process.env.VITE_BASE_URL}/cart/1`).reply(200);

await store.dispatch(updateCartItemQuantity({ itemId: 1, quantity: 3 }));
const state = store.getState().cartItems;
expect(state.cartItems.find((item) => item.id === 1)?.quantity).toBe(
updatedCartItem.quantity
);
expect(state.loading).toBe(false);
expect(state.error).toBeNull();
});

it('should handle removing a cart item', async () => {
const itemIdToRemove = 1;
httpMock.onDelete(`${process.env.VITE_BASE_URL}/cart/1`).reply(200);

await store.dispatch(removeCartItem(itemIdToRemove));
const state = store.getState().cartItems;
expect(
state.cartItems.find((item) => item.id === itemIdToRemove)
).toBeUndefined();
expect(state.loading).toBe(false);
expect(state.error).toBeNull();
});
});
2 changes: 2 additions & 0 deletions src/app/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '@/features/Auth/password';
import buyerSlice from '@/app/Dashboard/buyerSlice';
import orderSlice from './Dashboard/orderSlice';
import cartReducer from '@/features/Cart/cartSlice';

import ordersSliceReducer from '@/features/Orders/ordersSlice';

Expand All @@ -35,6 +36,7 @@ export const store = configureStore({
orders: ordersSliceReducer,
product: addProductSlice,
DeshboardProducts: dashboardProductsSlice,
cartItems: cartReducer,
},
});

Expand Down
78 changes: 68 additions & 10 deletions src/components/Cart/Cart.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,47 @@
import { useEffect, useState } from 'react';
import CartItem from './CartItem';
import HSButton from '../form/HSButton';
import ProductCard from '../home/ProductCard';
import { RootState } from '@/app/store';
import { Product } from '@/types/Product';
import {
selectProducts,
fetchProducts,
} from '@/features/Products/ProductSlice';
import { useAppDispatch, useAppSelector } from '@/app/hooks';
import { selectCartItems, fetchCartItems } from '@/features/Cart/cartSlice';

export default function Cart() {
const products: Product[] = useAppSelector((state: RootState) =>
selectProducts(state)
);
const cartItems = useAppSelector((state: RootState) =>
selectCartItems(state)
);
const total = cartItems.reduce(
(acc, item) => acc + item.product.salesPrice * item.quantity,
0
);
const [viewAll, setViewAll] = useState(false);
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(fetchProducts());
dispatch(fetchCartItems());
}, [dispatch]);
return (
<div className="flex flex-col max-w-screen-lg mx-auto">
<div className="flex w-full justify-end items-center pr-32 md:pr-48 py-6">
<div className="flex flex-col max-w-screen-lg mx-auto items-center">
<div className="flex w-full justify-between items-center py-6 max-w-screen-md sticky top-0 bg-white">
<h1 className="text-2xl font-bold px-8">Products in cart</h1>
<div className="flex gap-48 text-sm font-light text-gray-500">
<span>3 products</span>
<button type="button" className="flex gap-2">
<span>View all</span>
<span>{cartItems.length} products</span>
<button
type="button"
className="gap-2 flex items-center"
onClick={() => setViewAll((prev) => !prev)}
>
<span>{viewAll ? 'view few' : 'View all'}</span>
<svg
className={`${viewAll ? 'hidden' : 'flex'}`}
width="16"
height="16"
viewBox="0 0 16 16"
Expand All @@ -27,17 +58,44 @@ export default function Cart() {
</div>
</div>
<div className="w-fit">
<CartItem price={59.2} name="Canon Camera" />
<CartItem price={47} name="Galaxy Fold Z6" />
<CartItem price={98} name="Digital Television" />
<div className="flex justify-end gap-20 py-6 items-center">
{viewAll &&
cartItems.map((item) => (
<CartItem
id={item.id}
quantity={item.quantity}
price={item.product.salesPrice}
name={item.product.name}
key={item.id}
/>
))}
{!viewAll &&
cartItems
.slice(0, 3)
.map((item) => (
<CartItem
id={item.id}
quantity={item.quantity}
price={item.product.salesPrice}
name={item.product.name}
key={item.id}
/>
))}
<div className="flex justify-end gap-20 py-6 items-center sticky bottom-0 bg-white">
<div className="flex gap-2 items-center">
<h2 className="text-2xl font-bold text-gray-900">Total:</h2>
<span className="text-xl font-medium text-primary">$20088</span>
<span className="text-xl font-medium text-primary">${total}</span>
</div>
<HSButton title="CHECKOUT" />
</div>
</div>
<div className="flex flex-col gap-12">
<div>Recommended Products</div>
<div className="flex justify-between">
{products.slice(0, 4).map((product) => (
<ProductCard product={product} key={product.id} />
))}
</div>
</div>
</div>
);
}
31 changes: 25 additions & 6 deletions src/components/Cart/CartItem.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import { useState } from 'react';
import {
updateCartItemQuantity,
removeCartItem,
} from '@/features/Cart/cartSlice';
import { useAppDispatch } from '@/app/hooks';

interface CartProps {
id: number;
price: number;
name: string;
quantity: number;
}

function CartItem({ price, name }: CartProps) {
const [quantity, setQuantity] = useState(0);
function CartItem({ id, price, name, quantity }: CartProps) {
const dispatch = useAppDispatch();
const [size, setSize] = useState<'M' | 'S' | 'L'>('M');

const handleQuantityChange = (amount: number) => {
setQuantity((prevQuantity) => Math.max(0, prevQuantity + amount));
if (quantity + amount < 1) {
dispatch(removeCartItem(id));
} else {
dispatch(
updateCartItemQuantity({ itemId: id, quantity: amount + quantity })
);
}
};

const handleSize = (newSize: 'M' | 'S' | 'L') => {
Expand Down Expand Up @@ -82,7 +95,7 @@ function CartItem({ price, name }: CartProps) {
>
-
</button>
<span>{quantity}</span>
<span>{Math.round(quantity)}</span>
<button
type="button"
onClick={() => handleQuantityChange(1)}
Expand All @@ -95,10 +108,16 @@ function CartItem({ price, name }: CartProps) {
</div>
</div>
<div className="flex flex-col gap-6 py-4 items-end w-64 justify-between">
<button type="button" className="text-red-500 text-lg font-medium">
<button
type="button"
className="text-red-500 text-lg font-medium"
onClick={() => dispatch(removeCartItem(id))}
>
Remove
</button>
<span className="font-bold text-xl mt-4">${price * quantity}</span>
<span className="font-bold text-xl mt-4">
${Math.round(price * quantity)}
</span>
</div>
</div>
);
Expand Down
31 changes: 20 additions & 11 deletions src/components/home/ProductCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useNavigate } from 'react-router';
import { useAppDispatch, useAppSelector } from '@/app/hooks';
import { addToWishlist } from '@/features/Products/ProductSlice';
import { Product } from '@/types/Product';
import { addCartItem } from '@/features/Cart/cartSlice';

interface ProductCardProps {
product: Product;
Expand Down Expand Up @@ -139,18 +140,26 @@ function ProductCard({ product }: ProductCardProps) {
${product.regularPrice}
</span>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
className="text-white h-10 w-10 rounded p-2 cursor-pointer"
viewBox="0 0 256 256"
data-testid="addToCart"
style={{ backgroundColor: '6D31ED' }}
<button
type="button"
onClick={() =>
dispatch(addCartItem({ productId: product.id, quantity: 1 }))
}
>
<path
fill="currentColor"
d="M222 128a6 6 0 0 1-6 6h-82v82a6 6 0 0 1-12 0v-82H40a6 6 0 0 1 0-12h82V40a6 6 0 0 1 12 0v82h82a6 6 0 0 1 6 6"
/>
</svg>
{' '}
<svg
xmlns="http://www.w3.org/2000/svg"
className="text-white h-10 w-10 rounded p-2 cursor-pointer"
viewBox="0 0 256 256"
data-testid="addToCart"
style={{ backgroundColor: '6D31ED' }}
>
<path
fill="currentColor"
d="M222 128a6 6 0 0 1-6 6h-82v82a6 6 0 0 1-12 0v-82H40a6 6 0 0 1 0-12h82V40a6 6 0 0 1 12 0v82h82a6 6 0 0 1 6 6"
/>
</svg>
</button>
</div>
</div>
</div>
Expand Down
Loading
Loading