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
Merged
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
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
Expand Up @@ -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,
Expand Down
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
Expand Up @@ -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">

Expand Down Expand Up @@ -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>
Expand Down
4 changes: 2 additions & 2 deletions src/components/layout/header/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -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 }
Expand Down
4 changes: 2 additions & 2 deletions src/components/layout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/products/product.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Loading