diff --git a/pages/cart.js b/pages/cart.js index 560f340..278a8d5 100644 --- a/pages/cart.js +++ b/pages/cart.js @@ -1,5 +1,31 @@ -const Cart = () => { - return

Cart

; +import Layout from '../src/components/layout'; +import { HEADER_FOOTER_ENDPOINT } from '../src/utils/constants/endpoints'; +import axios from 'axios'; +import CartItemsContainer from '../src/components/cart/cart-items-container'; + +export default function Cart({ headerFooter }) { + return ( + +

Cart

+ +
+ ); } -export default Cart +export async function getStaticProps() { + + const { data: headerFooterData } = await axios.get( HEADER_FOOTER_ENDPOINT ); + + return { + props: { + headerFooter: headerFooterData?.data ?? {}, + }, + + /** + * Revalidate means that if a new request comes to server, then every 1 sec it will check + * if the data is changed, if it is changed then it will update the + * static file inside .next folder with the new data, so that any 'SUBSEQUENT' requests should have updated data. + */ + revalidate: 1, + }; +} diff --git a/pages/checkout.js b/pages/checkout.js new file mode 100644 index 0000000..f612bdc --- /dev/null +++ b/pages/checkout.js @@ -0,0 +1,29 @@ +import Layout from '../src/components/layout'; +import { HEADER_FOOTER_ENDPOINT } from '../src/utils/constants/endpoints'; +import axios from 'axios'; + +export default function Checkout({ headerFooter }) { + return ( + +

Checkout

+
+ ); +} + +export async function getStaticProps() { + + const { data: headerFooterData } = await axios.get( HEADER_FOOTER_ENDPOINT ); + + return { + props: { + headerFooter: headerFooterData?.data ?? {}, + }, + + /** + * Revalidate means that if a new request comes to server, then every 1 sec it will check + * if the data is changed, if it is changed then it will update the + * static file inside .next folder with the new data, so that any 'SUBSEQUENT' requests should have updated data. + */ + revalidate: 1, + }; +} diff --git a/public/cart-spinner.gif b/public/cart-spinner.gif new file mode 100644 index 0000000..703cccb Binary files /dev/null and b/public/cart-spinner.gif differ diff --git a/src/components/cart/add-to-cart.js b/src/components/cart/add-to-cart.js index 7f922d6..3f0c08b 100644 --- a/src/components/cart/add-to-cart.js +++ b/src/components/cart/add-to-cart.js @@ -11,7 +11,7 @@ const AddToCart = ( { product } ) => { const [ isAddedToCart, setIsAddedToCart ] = useState( false ); const [ loading, setLoading ] = useState( false ); const addToCartBtnClasses = cx( - 'text-gray-800 font-semibold py-2 px-4 border border-gray-400 rounded shadow', + 'duration-500 text-gray-800 font-semibold py-2 px-4 border border-gray-400 rounded shadow', { 'bg-white hover:bg-gray-100': ! loading, 'bg-gray-200': loading, diff --git a/src/components/cart/cart-item.js b/src/components/cart/cart-item.js new file mode 100644 index 0000000..0c006ef --- /dev/null +++ b/src/components/cart/cart-item.js @@ -0,0 +1,145 @@ +import React, { useEffect, useState, useRef } from 'react'; +import {isEmpty} from "lodash"; +import Image from '../image'; +import { deleteCartItem, updateCart } from '../../utils/cart'; + +const CartItem = ( { + item, + products, + setCart + } ) => { + + const [productCount, setProductCount] = useState( item.quantity ); + const [updatingProduct, setUpdatingProduct] = useState( false ); + const [removingProduct, setRemovingProduct] = useState( false ); + const productImg = item?.data?.images?.[0] ?? ''; + + /** + * Do not allow state update on an unmounted component. + * + * isMounted is used so that we can set it's value to false + * when the component is unmounted. + * This is done so that setState ( e.g setRemovingProduct ) in asynchronous calls + * such as axios.post, do not get executed when component leaves the DOM + * due to product/item deletion. + * If we do not do this as unsubscription, we will get + * "React memory leak warning- Can't perform a React state update on an unmounted component" + * + * @see https://dev.to/jexperton/how-to-fix-the-react-memory-leak-warning-d4i + * @type {React.MutableRefObject} + */ + const isMounted = useRef( false ); + + useEffect( () => { + isMounted.current = true + + // When component is unmounted, set isMounted.current to false. + return () => { + isMounted.current = false + } + }, [] ) + + /* + * Handle remove product click. + * + * @param {Object} event event + * @param {Integer} Product Id. + * + * @return {void} + */ + const handleRemoveProductClick = ( event, cartKey ) => { + event.stopPropagation(); + + // If the component is unmounted, or still previous item update request is in process, then return. + if ( !isMounted || updatingProduct ) { + return; + } + + deleteCartItem( cartKey, setCart, setRemovingProduct ); + }; + + /* + * When user changes the qty from product input update the cart in localStorage + * Also update the cart in global context + * + * @param {Object} event event + * + * @return {void} + */ + const handleQtyChange = ( event, cartKey, type ) => { + + if ( process.browser ) { + + event.stopPropagation(); + let newQty; + + // If the previous cart request is still updatingProduct or removingProduct, then return. + if ( updatingProduct || removingProduct || ( 'decrement' === type && 1 === productCount ) ) { + return; + } + + if ( !isEmpty( type ) ) { + newQty = 'increment' === type ? productCount + 1 : productCount - 1; + } else { + // If the user tries to delete the count of product, set that to 1 by default ( This will not allow him to reduce it less than zero ) + newQty = ( event.target.value ) ? parseInt( event.target.value ) : 1; + } + + // Set the new qty in state. + setProductCount( newQty ); + + if ( products.length ) { + updateCart(item?.key, newQty, setCart, setUpdatingProduct); + } + + } + }; + + return ( +
+
+
+ attributes as props + /> +
+
+ +
+
+
+

{ item?.data?.name }

+ {item?.data?.description ?

{item?.data?.description}

: ''} + +
+ +
+
+ {item?.currency}{item?.line_subtotal} +
+ { updatingProduct ? spinner : null } + {/*Qty*/} +
+ + handleQtyChange( event, item?.cartKey, '' ) } + /> + +
+
+
+
+
+ ) +}; + +export default CartItem; diff --git a/src/components/cart/cart-items-container.js b/src/components/cart/cart-items-container.js new file mode 100644 index 0000000..c9cc1e0 --- /dev/null +++ b/src/components/cart/cart-items-container.js @@ -0,0 +1,90 @@ +import React, { useContext, useState } from 'react'; +import { AppContext } from '../context'; +import CartItem from './cart-item'; + +import Link from 'next/link'; +import { clearCart } from '../../utils/cart'; + +const CartItemsContainer = () => { + const [ cart, setCart ] = useContext( AppContext ); + const { cartItems, totalPrice, totalQty } = cart || {}; + const [ isClearCartProcessing, setClearCartProcessing ] = useState( false ); + + // Clear the entire cart. + const handleClearCart = ( event ) => { + event.stopPropagation(); + + if (isClearCartProcessing) { + return; + } + + clearCart( setCart, setClearCartProcessing ); + + }; + + return ( +
+ { cart ? ( +
+ {/*Cart Items*/ } +
+ { cartItems.length && + cartItems.map( ( item ) => ( + + ) ) } +
+ + {/*Cart Total*/ } +
+

Cart Total

+
+

Total({totalQty})

+

{cartItems?.[0]?.currency ?? ''}{ totalPrice }

+
+ +
+ {/*Clear entire cart*/} +
+ +
+ {/*Checkout*/} + + + +
+
+
+ ) : ( +
+

No items in the cart

+ + + +
+ ) } +
+ ); +}; + +export default CartItemsContainer; diff --git a/src/components/layout/footer/index.js b/src/components/layout/footer/index.js index a566138..0939454 100644 --- a/src/components/layout/footer/index.js +++ b/src/components/layout/footer/index.js @@ -22,7 +22,7 @@ const Footer = ({footer}) => { }, []); return ( -