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

Feature/rb 26 cart #35

Merged
merged 13 commits into from
Jan 6, 2025
31 changes: 28 additions & 3 deletions package-lock.json

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

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@
"jwt-decode": "^4.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.28.0"
"react-router-dom": "^6.28.0",
"us-state-codes": "^1.1.2"
},
"devDependencies": {
"@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.20",
"eslint": "^8.56.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
Expand Down
39 changes: 39 additions & 0 deletions src/api/DBRequests.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,42 @@ export const sendMessage = async (setIsLoading, chat_id, message, token) => {
);
setIsLoading(false);
};

export const addToCart = (headers, cartData, token) => {
return handleApiRequest('/api/v1/cart', headers, cartData, token);
};

export const getCart = async (setIsLoading, setCartItems, setTotals, token) => {
const url = '/api/v1/cart';

try {
setIsLoading(true);
const { data } = await axios.get(`${API_BASE_URL}${url}`, {
headers: {
Authorization: `Bearer ${token}`,
Tanyaa-a marked this conversation as resolved.
Show resolved Hide resolved
},
});

const { cart } = data;
setCartItems(cart.orderItems || []);
setTotals({
tax: cart.tax || 0,
shippingFee: cart.shippingFee || 0,
total: cart.total || 0,
});

setIsLoading(false);
} catch (error) {
setIsLoading(false);
const errorMessage =
error?.response?.data?.msg ||
Tanyaa-a marked this conversation as resolved.
Show resolved Hide resolved
error?.response?.data?.error ||
UNEXPECTED_ERROR_MESSAGE;

throw new Error(errorMessage);
}
};
export const deleteFromCart = (headers, cartItemId, token) => {
const url = `/api/v1/cart/${cartItemId}`;
return handleApiRequest(url, headers, {}, token, 'DELETE');
};
31 changes: 27 additions & 4 deletions src/pages/books/Book.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react';
import { Button } from '@headlessui/react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';

import { getBook } from '../../api/DBRequests';
import { getBook, addToCart } from '../../api/DBRequests';
import { addChat } from '../../api/DBRequests';
import { useAccount } from '../../context/AccountProvider';
import { useAuth } from '../../context/AuthProvider';
Expand Down Expand Up @@ -35,6 +35,29 @@ const Book = () => {
fetchData();
}, [id, location.state, navigate]);

const handleAddToCart = async () => {
if (!isLoggedIn) {
navigate('/sign_in');
return;
}
const headers = { 'Content-Type': 'application/json' };
try {
await addToCart(
{ headers },
{ bookId: id, price: bookData.price },
token
);
navigate('/cart');
} catch (error) {
const errorMessage = error.response?.data || error.message;
console.error(
'Error adding to cart:',
error.response?.data || error.message
);
alert(`Error adding to cart: ${errorMessage}`);
}
};

const {
title,
coverImageUrl,
Expand Down Expand Up @@ -68,7 +91,7 @@ const Book = () => {
}

return (
<div className="mx-5 mt-3 grid justify-items-center gap-4 text-center [grid-template-areas:'header''image''box''table'] md:mt-20 md:grid-cols-[1fr_1fr_1fr] md:grid-rows-[min-content_1fr] md:gap-y-0 md:text-start md:[grid-template-areas:'image_header_box''image_table_box']">
<div className="mx-5 mt-3 grid min-h-screen justify-items-center gap-4 text-center [grid-template-areas:'header''image''box''table'] md:mt-20 md:grid-cols-[1fr_1fr_1fr] md:grid-rows-[min-content_1fr] md:gap-y-0 md:text-start md:[grid-template-areas:'image_header_box''image_table_box']">
<h1
className="w-full font-headings text-2xl font-bold"
style={{ gridArea: 'header' }}
Expand All @@ -92,8 +115,8 @@ const Book = () => {
<>
<Button
as="button"
type="button"
className="mb-2 mt-4 w-full rounded-md bg-red px-6 py-2 font-body text-xl font-semibold tracking-wide text-white transition-transform duration-150 hover:bg-redHover active:scale-95"
onClick={handleAddToCart}
className="mt-4 rounded-md bg-red px-6 py-2 text-white hover:bg-redHover"
>
Add to cart
</Button>
Expand Down
139 changes: 138 additions & 1 deletion src/pages/cart/Cart.jsx
Tanyaa-a marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,142 @@
import React, { useEffect, useState, useMemo } from 'react';

import { Link } from 'react-router-dom';
import StateCode from 'us-state-codes';

import CartItem from './CartItem';
import CartSummary from './CartSummary';
import { getCart, deleteFromCart } from '../../api/DBRequests';
import { useAuth } from '../../context/AuthProvider';

const Cart = () => {
return <></>;
const [cartItems, setCartItems] = useState([]);
const [totals, setTotals] = useState({ tax: 0, shippingFee: 0, total: 0 });
const [isLoading, setIsLoading] = useState(false);
const { token, isLoggedIn } = useAuth();

useEffect(() => {
const fetchCart = async () => {
setIsLoading(true);
try {
await getCart(setIsLoading, setCartItems, setTotals, token);
} catch (error) {
console.error('Error fetching cart:', error.message);
} finally {
setIsLoading(false);
}
};

if (token) {
fetchCart();
}
}, [token]);

const handleDelete = async (itemId) => {
try {
await deleteFromCart({}, itemId, token);
setCartItems((prev) => {
const updatedCartItems = prev.filter((item) => item._id !== itemId);

const itemsTotal = updatedCartItems.reduce(
(sum, item) => sum + (item.price || 0),
0
);
const tax = parseFloat((itemsTotal * 0.08).toFixed(2));
const shippingFee = 5.0;
const total = parseFloat((itemsTotal + tax + shippingFee).toFixed(2));
setTotals({ tax, shippingFee, total });
return updatedCartItems;
});
} catch (error) {
console.error('Error deleting item:', error.message);
}
};

const itemsBySeller = useMemo(() => {
return cartItems.reduce((acc, item) => {
const sellerKey = `${item.sellerName || 'Unknown'}, ${item.sellerLocation || 'Unknown Location'}`;
if (!acc[sellerKey]) acc[sellerKey] = [];
acc[sellerKey].push(item);
return acc;
}, {});
}, [cartItems]);

if (isLoading) {
return <p>Loading...</p>;
}
return (
<>
{isLoggedIn ? (
<div className="flex min-h-screen flex-col">
<main className="container mx-auto mt-8 px-4">
<h1 className="mb-6 font-headings text-2xl font-bold">
Shopping Cart{' '}
<span className="font-body font-normal text-blueGray">
({cartItems.length} item{cartItems.length !== 1 ? 's' : ''})
</span>
</h1>

<div className="mt-6 flex flex-col-reverse gap-4 md:flex-row">
<section className="flex-1 md:mr-4">
{Object.keys(itemsBySeller).map((seller, index) => {
const [sellerName, city, state] = seller.split(', ');

return (
<div
key={index}
className="bg-gray-50 mb-8 rounded-lg border"
>
<h2 className="rounded-t-lg border-b border-gray bg-lightBlue p-4 text-lg">
<span className="text-blueGray">{sellerName}</span>,{' '}
{city}
{state
? `, ${StateCode.getStateCodeByStateName(state) || state}`
: ''}
</h2>
{itemsBySeller[seller].map((item) => (
<CartItem
key={item._id}
item={item}
handleDelete={handleDelete}
/>
))}
</div>
);
})}
</section>

{cartItems.length > 0 && <CartSummary totals={totals} />}
</div>

<section className="mt-8">
<h2 className="text-lg font-semibold">Saved for later</h2>
<p className="mt-2">
You don&apos;t have any items saved for later.
</p>
</section>

{cartItems.length === 0 && (
<p className="mt-8 text-center">Your cart is empty</p>
)}
</main>
</div>
) : (
<div className="h-screen w-full">
<h1 className="text- mt-10 text-center font-body text-2xl">
Please login to view your cart
</h1>
<div className="mt-7 flex justify-center">
<Link
className="w-full max-w-xs rounded-md bg-red p-2 text-center font-semibold tracking-wide text-white transition-transform duration-200 hover:bg-redHover active:scale-95"
to="/sign_in"
>
Log In
</Link>
</div>
</div>
)}
</>
);
};

export default Cart;
62 changes: 62 additions & 0 deletions src/pages/cart/CartItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';

import { Button } from '@headlessui/react';
import PropTypes from 'prop-types';

const CartItem = ({
item: { coverImageUrl, title, author, price, _id },
handleDelete,
}) => {
return (
<div className="flex flex-col justify-between rounded-lg bg-white p-4 md:flex-row">
<div className="flex">
<img
src={coverImageUrl}
alt={title}
className="h-28 w-20 rounded-md object-cover shadow-sm"
/>
<div className="ml-4 flex flex-col justify-between">
<div>
<h3 className="cursor-pointer text-lg font-bold hover:underline">
{title}
</h3>
<p className="text-sm">{author}</p>
<div>
<p className="text-gray-800 mt-2 text-lg font-bold md:hidden">
${price ? price.toFixed(2) : 'N/A'}
</p>
</div>
</div>
<div className="mt-2 flex gap-2 text-blueGray">
<Button className="hover:underline">Save for later</Button>
<Button
onClick={() => handleDelete(_id)}
className="hover:underline"
>
Delete
</Button>
<Button className="hover:underline">Contact seller</Button>
</div>
</div>
</div>
<div>
<p className="text-gray-800 hidden text-lg font-bold md:block">
${price ? price.toFixed(2) : 'N/A'}
</p>
</div>
</div>
);
};

CartItem.propTypes = {
Tanyaa-a marked this conversation as resolved.
Show resolved Hide resolved
item: PropTypes.shape({
coverImageUrl: PropTypes.string,
title: PropTypes.string,
author: PropTypes.string,
price: PropTypes.number,
_id: PropTypes.string,
}).isRequired,
handleDelete: PropTypes.func.isRequired,
};

export default CartItem;
Loading