Skip to content

Feature/cart page #25

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

Merged
merged 16 commits into from
Apr 3, 2022
32 changes: 29 additions & 3 deletions pages/cart.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
const Cart = () => {
return <h1>Cart</h1>;
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 (
<Layout headerFooter={headerFooter || {}}>
<h1 className="uppercase tracking-0.5px">Cart</h1>
<CartItemsContainer/>
</Layout>
);
}

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,
};
}
29 changes: 29 additions & 0 deletions pages/checkout.js
Original file line number Diff line number Diff line change
@@ -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 (
<Layout headerFooter={headerFooter || {}}>
<h1>Checkout</h1>
</Layout>
);
}

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,
};
}
Binary file added public/cart-spinner.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/components/cart/add-to-cart.js
Original file line number Diff line number Diff line change
@@ -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,
145 changes: 145 additions & 0 deletions src/components/cart/cart-item.js
Original file line number Diff line number Diff line change
@@ -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<boolean>}
*/
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 (
<div className="cart-item-wrap grid grid-cols-3 gap-6 mb-5 border border-brand-bright-grey p-5">
<div className="col-span-1 cart-left-col">
<figure >
<Image
width="300"
height="300"
altText={productImg?.alt ?? ''}
sourceUrl={! isEmpty( productImg?.src ) ? productImg?.src : ''} // use normal <img> attributes as props
/>
</figure>
</div>

<div className="col-span-2 cart-right-col">
<div className="flex justify-between flex-col h-full">
<div className="cart-product-title-wrap relative">
<h3 className="cart-product-title text-brand-orange">{ item?.data?.name }</h3>
{item?.data?.description ? <p>{item?.data?.description}</p> : ''}
<button className="cart-remove-item absolute right-0 top-0 px-4 py-2 flex items-center text-22px leading-22px bg-transparent border border-brand-bright-grey" onClick={ ( event ) => handleRemoveProductClick( event, item?.key ) }>&times;</button>
</div>

<footer className="cart-product-footer flex justify-between p-4 border-t border-brand-bright-grey">
<div className="">
<span className="cart-total-price">{item?.currency}{item?.line_subtotal}</span>
</div>
{ updatingProduct ? <img className="woo-next-cart-item-spinner" width="24" src="/cart-spinner.gif" alt="spinner"/> : null }
{/*Qty*/}
<div style={{ display: 'flex', alignItems: 'center' }}>
<button className="decrement-btn text-24px" onClick={( event ) => handleQtyChange( event, item?.cartKey, 'decrement' )} >-</button>
<input
type="number"
min="1"
style={{ textAlign: 'center', width: '50px', paddingRight: '0' }}
data-cart-key={ item?.data?.cartKey }
className={ `woo-next-cart-qty-input ml-3 ${ updatingProduct ? 'disabled' : '' } ` }
value={ productCount }
onChange={ ( event ) => handleQtyChange( event, item?.cartKey, '' ) }
/>
<button className="increment-btn text-20px" onClick={( event ) => handleQtyChange( event, item?.cartKey, 'increment' )}>+</button>
</div>
</footer>
</div>
</div>
</div>
)
};

export default CartItem;
90 changes: 90 additions & 0 deletions src/components/cart/cart-items-container.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className="content-wrap-cart">
{ cart ? (
<div className="woo-next-cart-table-row grid lg:grid-cols-3 gap-4">
{/*Cart Items*/ }
<div className="woo-next-cart-table lg:col-span-2 mb-md-0 mb-5">
{ cartItems.length &&
cartItems.map( ( item ) => (
<CartItem
key={ item.product_id }
item={ item }
products={ cartItems }
setCart={setCart}
/>
) ) }
</div>

{/*Cart Total*/ }
<div className="woo-next-cart-total-container lg:col-span-1 p-5 pt-0">
<h2>Cart Total</h2>
<div className="flex grid grid-cols-3 bg-gray-100 mb-4">
<p className="col-span-2 p-2 mb-0">Total({totalQty})</p>
<p className="col-span-1 p-2 mb-0">{cartItems?.[0]?.currency ?? ''}{ totalPrice }</p>
</div>

<div className="flex justify-between">
{/*Clear entire cart*/}
<div className="clear-cart">
<button
className="text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:bg-gray-600 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-700 dark:focus:ring-gray-800"
onClick={(event) => handleClearCart(event)}
disabled={isClearCartProcessing}
>
<span className="woo-next-cart">{!isClearCartProcessing ? "Clear Cart" : "Clearing..."}</span>
</button>
</div>
{/*Checkout*/}
<Link href="/checkout">
<button className="text-white duration-500 bg-brand-orange hover:bg-brand-royal-blue focus:ring-4 focus:text-brand-gunsmoke-grey font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:focus:ring-yellow-900">
<span className="woo-next-cart-checkout-txt">
Proceed to Checkout
</span>
<i className="fas fa-long-arrow-alt-right"/>
</button>
</Link>
</div>
</div>
</div>
) : (
<div className="mt-14">
<h2>No items in the cart</h2>
<Link href="/">
<button className="text-white duration-500 bg-brand-orange hover:bg-brand-royal-blue font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:focus:ring-yellow-900">
<span className="woo-next-cart-checkout-txt">
Add New Products
</span>
<i className="fas fa-long-arrow-alt-right"/>
</button>
</Link>
</div>
) }
</div>
);
};

export default CartItemsContainer;
8 changes: 4 additions & 4 deletions src/components/layout/footer/index.js
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ const Footer = ({footer}) => {
}, []);

return (
<footer className="bg-blue-500 p-6">
<footer className="footer bg-blue-500 p-6">
<div className="container mx-auto">
<div className="flex flex-wrap -mx-1 overflow-hidden text-white">

@@ -61,10 +61,10 @@ const Footer = ({footer}) => {
</div>
<div className="w-full lg:w-3/4 flex justify-end">
{ !isEmpty( socialLinks ) && isArray( socialLinks ) ? (
<ul className="flex item-center">
<ul className="flex item-center mb-0">
{ socialLinks.map( socialLink => (
<li key={socialLink?.iconName} className="ml-4">
<a href={ socialLink?.iconUrl || '/' } target="_blank" title={socialLink?.iconName}>
<li key={socialLink?.iconName} className="no-dots-list mb-0 flex items-center">
<a href={ socialLink?.iconUrl || '/' } target="_blank" title={socialLink?.iconName} className="ml-2 inline-block">
{ getIconComponentByName( socialLink?.iconName ) }
<span className="sr-only">{socialLink?.iconName}</span>
</a>
4 changes: 2 additions & 2 deletions src/components/layout/header/index.js
Original file line number Diff line number Diff line change
@@ -38,7 +38,7 @@ const Header = ( { header } ) => {
<Link href="/">
<a className="font-semibold text-xl tracking-tight">{ siteTitle || 'WooNext' }</a>
</Link>
{ siteDescription ? <p>{ siteDescription }</p> : null }
{ siteDescription ? <p className="mb-0">{ siteDescription }</p> : null }
</span>
</div>
<div className="block lg:hidden">
@@ -53,7 +53,7 @@ const Header = ( { header } ) => {
<div className="text-sm font-medium uppercase lg:flex-grow">
{ ! isEmpty( headerMenuItems ) && headerMenuItems.length ? headerMenuItems.map( menuItem => (
<Link key={ menuItem?.ID } href={ menuItem?.url ?? '/' }>
<a className="block mt-4 lg:inline-block lg:mt-0 text-black hover:text-black mr-10"
<a className="block mt-4 lg:inline-block lg:mt-0 hover:text-brand-royal-blue duration-500 mr-10"
dangerouslySetInnerHTML={ { __html: menuItem.title } }/>
</Link>
) ) : null }
4 changes: 2 additions & 2 deletions src/components/layout/index.js
Original file line number Diff line number Diff line change
@@ -6,9 +6,9 @@ const Layout = ({children, headerFooter}) => {
const { header, footer } = headerFooter || {};
return (
<AppProvider>
<div >
<div>
<Header header={header}/>
<main className="container mx-auto py-4">
<main className="container mx-auto py-4 min-h-50vh">
{children}
</main>
<Footer footer={footer}/>
2 changes: 1 addition & 1 deletion src/components/products/product.js
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ const Product = ( { product } ) => {
width="380"
height="380"
/>
<h3 className="font-bold uppercase my-2">{ product?.name ?? '' }</h3>
<h6 className="font-bold uppercase my-2 tracking-0.5px">{ product?.name ?? '' }</h6>
<div className="mb-4" dangerouslySetInnerHTML={{ __html: sanitize( product?.price_html ?? '' ) }}/>
</a>
</Link>
Loading