Skip to content

Commit

Permalink
Integrate new shop apis + bug fixes (#229)
Browse files Browse the repository at this point in the history
* fix: use product slug as URL for single product item

* chore: add slug to ProductCard types

* fix: use slug as product URL

* fix: update api URL in useSwagList hook

* feat: integrate api endpoints

* fix: properly add selected item to localstorage

* fix: add id, category, slug and image to cart data

* fix: multiply cart quantity by price of one unit

* feat: display cart products from local storage

* fix: delete dummy cart products

* fix: delete from cart functionality

* refactor: item quantity button

* feat: add more dummy images

* fix: linting issues

* fix: use product slug as URL

* fix: display 'item' or items depending on stock no

* feat: handle different API status appropriately in BrowseProducts.jsx

* feat: handle different API status appropriately in NewProducts.jsx

* feat: display different message if no products are available

* feat: handle different API statuses appropriately in /shop/item/:slug

* refactor: PopularItemsSection component

* feat: handle API statuses appropriately in PopularItemsSection component

* fix: linting issues

* fix: prevent user from adding item to cart without selecting size

* fix: 'image undefined' error if selected product variant lacks images

* feat: display shopping cart when user clicks CartIcon

* feat: prevent user from adding more items than available stock

* fix: 'Maximum update depth exceeded' error in CartDrawer

* fix: use UUID as item id in cart to enforce uniqueness

* feat: update api endpoints in useCartSwagg and useCartProducts hooks

* feat: add item to backend cart when user clicks AddToCart btn

* feat: delete item from backend cart when user deletes an item locally

* feat: update api endpoint for useDeleteSwag mutation hook

* fix: linting issues

* feat: update api endpoint for useMakeOrder mutation hook

* feat: update payload data for making an order

* feat: create useDeleteAllSwag mutation hook

* feat: clear backend cart after order

* fix: clear backend cart after successful order

* feat: clear local cart after successful order

* fix: linting issues

* fix: item count update bug

* fix: linting issue

* fix: use item slug as URL in carousel

* fix: display item price from API in carousel

* fix: linting issues

---------

Co-authored-by: sonylomo <sonylomo1@gmail.com>
  • Loading branch information
alvyynm and sonylomo authored Sep 9, 2024
1 parent 2b09e14 commit c28a4bb
Show file tree
Hide file tree
Showing 16 changed files with 477 additions and 263 deletions.
11 changes: 10 additions & 1 deletion src/components/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { LazyLoadImage } from "react-lazy-load-image-component";
import { Link, useLocation } from "react-router-dom";

import logo from "../assets/images/sytLogo.png";
import CartDrawer from "./shop/CartDrawer";
import CartIcon from "./shop/CartIcon";

const navLinks = [
Expand Down Expand Up @@ -52,6 +53,7 @@ const navLinks = [

function Header() {
const [showNavlinks, setShowNavlinks] = useState(false);
const [open, setOpen] = useState(false);

const { pathname } = useLocation();

Expand All @@ -66,7 +68,13 @@ function Header() {
{/* mobile menu */}
<div className="flex gap-4 items-center">
<div className="flex md:hidden">
<CartIcon />
<button
type="button"
aria-label="open cart"
onClick={() => setOpen(true)}
>
<CartIcon />
</button>
</div>
{showNavlinks ? (
<button
Expand Down Expand Up @@ -139,6 +147,7 @@ function Header() {
})}
</nav>
</header>
<CartDrawer open={open} setOpen={setOpen} />
</div>
);
}
Expand Down
114 changes: 31 additions & 83 deletions src/components/shop/CartDrawer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,59 +6,27 @@ import { IoIosCloseCircleOutline } from "react-icons/io";
import { RiDeleteBin6Line } from "react-icons/ri";
import { LazyLoadImage } from "react-lazy-load-image-component";
import { Link, useNavigate } from "react-router-dom";
import { useDeleteSwag } from "../../hooks/Mutations/shop/useCartSwagg";
import formatPrice from "../../utilities/formatPrice";

function CartDrawer({ open, setOpen }) {
const navigate = useNavigate();

// Get the JSON string from localStorage
const [cartProducts, setCartProducts] = useState([]);

const dummyCartData = [
{
id: "271fcc1c-0337-44f4-9449-2bc35b6ffd01",
name: "Cityscape Jacket",
description:
"Introducing our Cityscape Jacket: a blend of urban flair and unbeatable comfort. Crafted with premium materials, it offers sleek design and weather resistance for city adventures. Stay stylish and protected with adjustable features and convenient pockets. Upgrade your urban wardrobe today!",
category: "Jackets",
image:
"https://apis.spaceyatech.com/media/product_images/main-sample_copy_Fud5OzF.png",
price: "3000.00",
stock: 10,
color: "brown",
},
{
id: "232437b9-3e64-4cad-a6c3-08158e118207",
name: "Cityscape Jacket - Mid",
description:
"Introducing our Cityscape Jacket: a blend of urban flair and unbeatable comfort. Crafted with premium materials, it offers sleek design and weather resistance for city adventures. Stay stylish and protected with adjustable features and convenient pockets. Upgrade your urban wardrobe today!",
category: "Jackets",
image:
"https://apis.spaceyatech.com/media/product_images/main-sample_copy_BRv17MK.png",
price: "1800.00",
stock: 11,
color: "brown",
},
{
id: "9cd9a601-0ed9-4685-8633-4b04e0811fc7",
name: "SYT Hoodie",
description:
"Unleash your tech-savvy style with our Tech-Fit Hoodie. Designed for the modern individual, it seamlessly integrates functionality and fashion. Crafted with cutting-edge materials, it offers unrivaled comfort and durability. Elevate your wardrobe with this essential piece that effortlessly combines innovation and style.",
category: "Hoodies",
image:
"https://apis.spaceyatech.com/media/product_images/sample1_copy_PXgn3MX.png",
price: "2000.00",
stock: 10,
color: "white",
},
];
const [cartProducts, setCartProducts] = useState(() => {
// Initialize state with the value from localStorage if it exists
const storedProducts = localStorage.getItem("swagList");
return storedProducts ? JSON.parse(storedProducts) : [];
});

useEffect(() => {
const storage = localStorage.getItem("swagList")
? JSON.parse(localStorage.getItem("swagList"))
: null;
setCartProducts(storage);
}, [cartProducts]);
if (open) {
const storedProducts = localStorage.getItem("swagList");
if (storedProducts) {
setCartProducts(JSON.parse(storedProducts));
}
}
}, [open]);

// const { data: cartProducts, isSuccess } = useProductsInCart();
useEffect(() => {
Expand All @@ -78,31 +46,23 @@ function CartDrawer({ open, setOpen }) {
};
}, []);

// const { mutate: deleteSwag } = useDeleteSwag();
const { mutate: removeSwagFromCart } = useDeleteSwag();

const deleteFromLocalStorage = (cartItemId) => {
// Parse it back to an array of objects
const swagList = cartProducts;

const idxToDelete = swagList.findIndex(
(swag) => swag.swagg_id === cartItemId
// Create a new array by filtering out the item to delete
const updatedSwagList = cartProducts.filter(
(swag) => swag.id !== cartItemId
);

// Check if the object was found
if (idxToDelete !== -1) {
// Remove the object from the swagList
swagList.splice(idxToDelete, 1);

// Convert the updated list to a JSON string
setCartProducts(JSON.stringify(swagList));
// Update the state with the new array
setCartProducts(updatedSwagList);

// Store the updated list back to localStorage
localStorage.setItem("swagList", cartProducts);
}
// Convert the updated list to a JSON string and store it in localStorage
localStorage.setItem("swagList", JSON.stringify(updatedSwagList));
};

const handleDeleteSwag = (cartItemId) => {
// deleteSwag(cartItemId);
removeSwagFromCart(cartItemId);
deleteFromLocalStorage(cartItemId);
};

Expand Down Expand Up @@ -167,25 +127,18 @@ function CartDrawer({ open, setOpen }) {
<div className="flow-root">
<ul className="-my-6 divide-y divide-gray-200 border-b">
{/* {isSuccess && */}
{dummyCartData?.length > 0 &&
(dummyCartData?.cart_items
? dummyCartData.cart_items
: dummyCartData
)?.map((cartProduct) => (
{cartProducts?.length > 0 &&
cartProducts?.map((cartProduct) => (
<li
key={crypto.randomUUID()}
className="flex py-6 space-x-4 sm:space-x-16"
>
<div className="h-32 w-28 flex-shrink-0 overflow-hidden rounded-2xl">
<LazyLoadImage
src={`${
cartProduct.image ||
cartProduct.product?.image
cartProduct.image || cartProduct?.image
}`}
alt={
cartProduct.name ||
cartProduct.product?.name
}
alt={cartProduct.name}
className="h-full w-full object-cover object-center"
/>
</div>
Expand All @@ -197,20 +150,18 @@ function CartDrawer({ open, setOpen }) {
<div className="flex justify-between">
<p className="flex justify-between items-center gap-1 font-medium bg-[#FEF3F2] text-[#B42318] text-sm rounded-full px-2 py-1">
<CiShoppingTag />
Hoodies
{cartProduct.category}
</p>
</div>
<h3>
<p className="text-base md:text-xl text-[#656767]">
{" "}
<Link
to={`/shop/item/${
cartProduct.productId ||
cartProduct.swagg_id
cartProduct.slug
}`}
>
{cartProduct.name ||
cartProduct.product?.name}
{cartProduct.name}
</Link>
</p>
</h3>
Expand All @@ -219,18 +170,15 @@ function CartDrawer({ open, setOpen }) {
type="button"
className="flex justify-end"
onClick={() => {
handleDeleteSwag(
cartProduct.id ||
cartProduct.swagg_id
);
handleDeleteSwag(cartProduct.id);
}}
>
{/* Delete icon */}
<RiDeleteBin6Line className="h-6 w-6 text-[#FC5555]" />
</button>
</div>
<div className="flex flex-row justify-between text-[#656767] text-sm sm:text-base font-medium">
<p>Qty: 1</p>
<p>Qty: {cartProduct.orderUnits}</p>
<p>
KES {formatPrice(cartProduct.price)}
</p>
Expand Down
12 changes: 9 additions & 3 deletions src/components/shop/Counter.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import PropTypes from "prop-types";

function Counter({ className, setCount, count }) {
function Counter({ className, setCount, count, maxStock }) {
const increment = () => setCount((prevCount) => prevCount + 1);
const decrement = () =>
setCount((prevCount) => (prevCount > 1 ? prevCount - 1 : prevCount));
// console.log(count)
return (
<div className={`flex rounded-lg ${className}`}>
<button
type="button"
data-action="decrement"
className="cursor-pointer outline-none w-20 border-y border-l border-l-[#EAECF0] border-y-[#EAECF0] rounded-l-md border-r"
onClick={() => setCount(count > 1 ? count - 1 : 1)}
onClick={decrement}
>
<span className=" text-base"></span>
</button>
Expand All @@ -18,10 +22,11 @@ function Counter({ className, setCount, count }) {
{count}
</p>
<button
disabled={maxStock === count || count > maxStock}
type="button"
data-action="increment"
className="cursor-pointer outline-none w-20 border-y border-r border-r-[#EAECF0] border-y-[#EAECF0] rounded-r-md border-l"
onClick={() => setCount(count + 1)}
onClick={increment}
>
<span className="text-base">+</span>
</button>
Expand All @@ -39,4 +44,5 @@ Counter.propTypes = {
className: PropTypes.string,
setCount: PropTypes.func.isRequired,
count: PropTypes.number.isRequired,
maxStock: PropTypes.number.isRequired,
};
8 changes: 5 additions & 3 deletions src/components/shop/ProductCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function ProductCard({ product }) {
return (
<div className="border rounded-lg p-2.5 pr-1.5 shadow-sm group hover:bg-green-dark/10 bg-white flex flex-col gap-4 ">
<Link
to={`/shop/item/${product.id}`}
to={`/shop/item/${product.slug}`}
className="aspect-h-1 aspect-w-1 w-full bg-gray-200 lg:aspect-none group-hover:opacity-75 lg:h-96"
>
<LazyLoadImage
Expand All @@ -31,7 +31,7 @@ function ProductCard({ product }) {
/>
</Link>
<Link
to={`/shop/item/${product.id}`}
to={`/shop/item/${product.slug}`}
className="flex justify-between pr-1"
>
<h3 className="text-md uppercase font-medium text-gray-600">
Expand All @@ -41,7 +41,9 @@ function ProductCard({ product }) {
{totalStock > 0 ? (
<p className="text-green-dark font-medium text-sm px-1">
<span> {totalStock}</span>
<span className="ml-2">items left</span>
<span className="ml-2">
{totalStock === 1 ? "item" : "items"} left
</span>
</p>
) : (
<div className=" text-red-800 p-1 rounded-lg bg-red-800/20 font-bold text-sm">
Expand Down
34 changes: 31 additions & 3 deletions src/hooks/Mutations/shop/useCartSwagg.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const useAddSwagToCart = () => {

return useMutation({
mutationFn: async (cartItems) => {
const response = await publicAxios.post("/cart/swaggs/", cartItems, {
const response = await publicAxios.post("/cart-items/", cartItems, {
headers: {
"Content-Type": "application/json",
// Authorization: `Bearer ${auth?.access}`,
Expand All @@ -36,7 +36,7 @@ const useDeleteSwag = () => {

return useMutation({
mutationFn: async (id) => {
const response = await publicAxios.delete(`/cart/swaggs/${id}/`, {
const response = await publicAxios.delete(`/cart-items/${id}/`, {
headers: {
"Content-Type": "application/json",
// Authorization: `Bearer ${auth?.access}`,
Expand All @@ -58,4 +58,32 @@ const useDeleteSwag = () => {
});
};

export { useAddSwagToCart, useDeleteSwag };
const useDeleteAllSwag = () => {
const { logout } = useAuth();
const queryClient = useQueryClient();

return useMutation({
mutationFn: async () => {
const response = await publicAxios.delete("/cart-items/clear_cart/", {
headers: {
"Content-Type": "application/json",
// Authorization: `Bearer ${auth?.access}`,
},
});

return response.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["productsInCart"] });
},
onError: (error) => {
// eslint-disable-next-line no-console
console.error("Unable to delete all cart items");
if (error.response.status === 401) {
logout();
}
},
});
};

export { useAddSwagToCart, useDeleteSwag, useDeleteAllSwag };
12 changes: 6 additions & 6 deletions src/hooks/Mutations/shop/useMakeOrder.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import privateAxios from "../../../api/privateAxios";
import useAuth from "../../useAuth";
// import useAuth from "../../useAuth";

// POST: https://apis.spaceyatech.com/api/orders/
// POST: https://apis.spaceyatech.com/api/checkout/
const useMakeOrder = () => {
const { auth, logout } = useAuth();
// const { auth, logout } = useAuth();
const queryClient = useQueryClient();

return useMutation({
mutationFn: async (customerInfo) => {
const response = await privateAxios.post("/orders/", customerInfo, {
const response = await privateAxios.post("/checkout/", customerInfo, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${auth?.access}`,
// Authorization: `Bearer ${auth?.access}`,
},
});
return response.data;
Expand All @@ -25,7 +25,7 @@ const useMakeOrder = () => {
// eslint-disable-next-line no-console
console.error("Unable to add availability");
if (error.response.status === 401) {
logout();
// logout();
}
},
});
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/Queries/shop/useCartProducts.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const fetchProductsInCart = async () => {
// const { access } = authObject;

try {
const response = await publicAxios.get("/cart/swaggs/", {
const response = await publicAxios.get("/cart-items/", {
headers: {
"Content-Type": "application/json",
// Authorization: `Bearer ${access}`,
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/Queries/shop/useSwagList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const useSwagList = () =>

const fetchSingleSwag = async (id) => {
try {
const response = await publicAxios.get(`/swaggs/${id}`);
const response = await publicAxios.get(`/swaggsnew/${id}`);
return response.data;
} catch (error) {
toast.error("Error fetching swag list");
Expand Down
Loading

0 comments on commit c28a4bb

Please sign in to comment.