Skip to content

Commit 014eb6f

Browse files
authored
Merge pull request #25 from imranhsayed/feature/cart-page
Feature/cart page
2 parents d175042 + 43f246d commit 014eb6f

38 files changed

+2401
-18
lines changed

pages/cart.js

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
1-
const Cart = () => {
2-
return <h1>Cart</h1>;
1+
import Layout from '../src/components/layout';
2+
import { HEADER_FOOTER_ENDPOINT } from '../src/utils/constants/endpoints';
3+
import axios from 'axios';
4+
import CartItemsContainer from '../src/components/cart/cart-items-container';
5+
6+
export default function Cart({ headerFooter }) {
7+
return (
8+
<Layout headerFooter={headerFooter || {}}>
9+
<h1 className="uppercase tracking-0.5px">Cart</h1>
10+
<CartItemsContainer/>
11+
</Layout>
12+
);
313
}
414

5-
export default Cart
15+
export async function getStaticProps() {
16+
17+
const { data: headerFooterData } = await axios.get( HEADER_FOOTER_ENDPOINT );
18+
19+
return {
20+
props: {
21+
headerFooter: headerFooterData?.data ?? {},
22+
},
23+
24+
/**
25+
* Revalidate means that if a new request comes to server, then every 1 sec it will check
26+
* if the data is changed, if it is changed then it will update the
27+
* static file inside .next folder with the new data, so that any 'SUBSEQUENT' requests should have updated data.
28+
*/
29+
revalidate: 1,
30+
};
31+
}

pages/checkout.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Layout from '../src/components/layout';
2+
import { HEADER_FOOTER_ENDPOINT } from '../src/utils/constants/endpoints';
3+
import axios from 'axios';
4+
5+
export default function Checkout({ headerFooter }) {
6+
return (
7+
<Layout headerFooter={headerFooter || {}}>
8+
<h1>Checkout</h1>
9+
</Layout>
10+
);
11+
}
12+
13+
export async function getStaticProps() {
14+
15+
const { data: headerFooterData } = await axios.get( HEADER_FOOTER_ENDPOINT );
16+
17+
return {
18+
props: {
19+
headerFooter: headerFooterData?.data ?? {},
20+
},
21+
22+
/**
23+
* Revalidate means that if a new request comes to server, then every 1 sec it will check
24+
* if the data is changed, if it is changed then it will update the
25+
* static file inside .next folder with the new data, so that any 'SUBSEQUENT' requests should have updated data.
26+
*/
27+
revalidate: 1,
28+
};
29+
}

public/cart-spinner.gif

33.2 KB
Loading

src/components/cart/add-to-cart.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const AddToCart = ( { product } ) => {
1111
const [ isAddedToCart, setIsAddedToCart ] = useState( false );
1212
const [ loading, setLoading ] = useState( false );
1313
const addToCartBtnClasses = cx(
14-
'text-gray-800 font-semibold py-2 px-4 border border-gray-400 rounded shadow',
14+
'duration-500 text-gray-800 font-semibold py-2 px-4 border border-gray-400 rounded shadow',
1515
{
1616
'bg-white hover:bg-gray-100': ! loading,
1717
'bg-gray-200': loading,

src/components/cart/cart-item.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import React, { useEffect, useState, useRef } from 'react';
2+
import {isEmpty} from "lodash";
3+
import Image from '../image';
4+
import { deleteCartItem, updateCart } from '../../utils/cart';
5+
6+
const CartItem = ( {
7+
item,
8+
products,
9+
setCart
10+
} ) => {
11+
12+
const [productCount, setProductCount] = useState( item.quantity );
13+
const [updatingProduct, setUpdatingProduct] = useState( false );
14+
const [removingProduct, setRemovingProduct] = useState( false );
15+
const productImg = item?.data?.images?.[0] ?? '';
16+
17+
/**
18+
* Do not allow state update on an unmounted component.
19+
*
20+
* isMounted is used so that we can set it's value to false
21+
* when the component is unmounted.
22+
* This is done so that setState ( e.g setRemovingProduct ) in asynchronous calls
23+
* such as axios.post, do not get executed when component leaves the DOM
24+
* due to product/item deletion.
25+
* If we do not do this as unsubscription, we will get
26+
* "React memory leak warning- Can't perform a React state update on an unmounted component"
27+
*
28+
* @see https://dev.to/jexperton/how-to-fix-the-react-memory-leak-warning-d4i
29+
* @type {React.MutableRefObject<boolean>}
30+
*/
31+
const isMounted = useRef( false );
32+
33+
useEffect( () => {
34+
isMounted.current = true
35+
36+
// When component is unmounted, set isMounted.current to false.
37+
return () => {
38+
isMounted.current = false
39+
}
40+
}, [] )
41+
42+
/*
43+
* Handle remove product click.
44+
*
45+
* @param {Object} event event
46+
* @param {Integer} Product Id.
47+
*
48+
* @return {void}
49+
*/
50+
const handleRemoveProductClick = ( event, cartKey ) => {
51+
event.stopPropagation();
52+
53+
// If the component is unmounted, or still previous item update request is in process, then return.
54+
if ( !isMounted || updatingProduct ) {
55+
return;
56+
}
57+
58+
deleteCartItem( cartKey, setCart, setRemovingProduct );
59+
};
60+
61+
/*
62+
* When user changes the qty from product input update the cart in localStorage
63+
* Also update the cart in global context
64+
*
65+
* @param {Object} event event
66+
*
67+
* @return {void}
68+
*/
69+
const handleQtyChange = ( event, cartKey, type ) => {
70+
71+
if ( process.browser ) {
72+
73+
event.stopPropagation();
74+
let newQty;
75+
76+
// If the previous cart request is still updatingProduct or removingProduct, then return.
77+
if ( updatingProduct || removingProduct || ( 'decrement' === type && 1 === productCount ) ) {
78+
return;
79+
}
80+
81+
if ( !isEmpty( type ) ) {
82+
newQty = 'increment' === type ? productCount + 1 : productCount - 1;
83+
} else {
84+
// 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 )
85+
newQty = ( event.target.value ) ? parseInt( event.target.value ) : 1;
86+
}
87+
88+
// Set the new qty in state.
89+
setProductCount( newQty );
90+
91+
if ( products.length ) {
92+
updateCart(item?.key, newQty, setCart, setUpdatingProduct);
93+
}
94+
95+
}
96+
};
97+
98+
return (
99+
<div className="cart-item-wrap grid grid-cols-3 gap-6 mb-5 border border-brand-bright-grey p-5">
100+
<div className="col-span-1 cart-left-col">
101+
<figure >
102+
<Image
103+
width="300"
104+
height="300"
105+
altText={productImg?.alt ?? ''}
106+
sourceUrl={! isEmpty( productImg?.src ) ? productImg?.src : ''} // use normal <img> attributes as props
107+
/>
108+
</figure>
109+
</div>
110+
111+
<div className="col-span-2 cart-right-col">
112+
<div className="flex justify-between flex-col h-full">
113+
<div className="cart-product-title-wrap relative">
114+
<h3 className="cart-product-title text-brand-orange">{ item?.data?.name }</h3>
115+
{item?.data?.description ? <p>{item?.data?.description}</p> : ''}
116+
<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>
117+
</div>
118+
119+
<footer className="cart-product-footer flex justify-between p-4 border-t border-brand-bright-grey">
120+
<div className="">
121+
<span className="cart-total-price">{item?.currency}{item?.line_subtotal}</span>
122+
</div>
123+
{ updatingProduct ? <img className="woo-next-cart-item-spinner" width="24" src="/cart-spinner.gif" alt="spinner"/> : null }
124+
{/*Qty*/}
125+
<div style={{ display: 'flex', alignItems: 'center' }}>
126+
<button className="decrement-btn text-24px" onClick={( event ) => handleQtyChange( event, item?.cartKey, 'decrement' )} >-</button>
127+
<input
128+
type="number"
129+
min="1"
130+
style={{ textAlign: 'center', width: '50px', paddingRight: '0' }}
131+
data-cart-key={ item?.data?.cartKey }
132+
className={ `woo-next-cart-qty-input ml-3 ${ updatingProduct ? 'disabled' : '' } ` }
133+
value={ productCount }
134+
onChange={ ( event ) => handleQtyChange( event, item?.cartKey, '' ) }
135+
/>
136+
<button className="increment-btn text-20px" onClick={( event ) => handleQtyChange( event, item?.cartKey, 'increment' )}>+</button>
137+
</div>
138+
</footer>
139+
</div>
140+
</div>
141+
</div>
142+
)
143+
};
144+
145+
export default CartItem;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React, { useContext, useState } from 'react';
2+
import { AppContext } from '../context';
3+
import CartItem from './cart-item';
4+
5+
import Link from 'next/link';
6+
import { clearCart } from '../../utils/cart';
7+
8+
const CartItemsContainer = () => {
9+
const [ cart, setCart ] = useContext( AppContext );
10+
const { cartItems, totalPrice, totalQty } = cart || {};
11+
const [ isClearCartProcessing, setClearCartProcessing ] = useState( false );
12+
13+
// Clear the entire cart.
14+
const handleClearCart = ( event ) => {
15+
event.stopPropagation();
16+
17+
if (isClearCartProcessing) {
18+
return;
19+
}
20+
21+
clearCart( setCart, setClearCartProcessing );
22+
23+
};
24+
25+
return (
26+
<div className="content-wrap-cart">
27+
{ cart ? (
28+
<div className="woo-next-cart-table-row grid lg:grid-cols-3 gap-4">
29+
{/*Cart Items*/ }
30+
<div className="woo-next-cart-table lg:col-span-2 mb-md-0 mb-5">
31+
{ cartItems.length &&
32+
cartItems.map( ( item ) => (
33+
<CartItem
34+
key={ item.product_id }
35+
item={ item }
36+
products={ cartItems }
37+
setCart={setCart}
38+
/>
39+
) ) }
40+
</div>
41+
42+
{/*Cart Total*/ }
43+
<div className="woo-next-cart-total-container lg:col-span-1 p-5 pt-0">
44+
<h2>Cart Total</h2>
45+
<div className="flex grid grid-cols-3 bg-gray-100 mb-4">
46+
<p className="col-span-2 p-2 mb-0">Total({totalQty})</p>
47+
<p className="col-span-1 p-2 mb-0">{cartItems?.[0]?.currency ?? ''}{ totalPrice }</p>
48+
</div>
49+
50+
<div className="flex justify-between">
51+
{/*Clear entire cart*/}
52+
<div className="clear-cart">
53+
<button
54+
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"
55+
onClick={(event) => handleClearCart(event)}
56+
disabled={isClearCartProcessing}
57+
>
58+
<span className="woo-next-cart">{!isClearCartProcessing ? "Clear Cart" : "Clearing..."}</span>
59+
</button>
60+
</div>
61+
{/*Checkout*/}
62+
<Link href="/checkout">
63+
<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">
64+
<span className="woo-next-cart-checkout-txt">
65+
Proceed to Checkout
66+
</span>
67+
<i className="fas fa-long-arrow-alt-right"/>
68+
</button>
69+
</Link>
70+
</div>
71+
</div>
72+
</div>
73+
) : (
74+
<div className="mt-14">
75+
<h2>No items in the cart</h2>
76+
<Link href="/">
77+
<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">
78+
<span className="woo-next-cart-checkout-txt">
79+
Add New Products
80+
</span>
81+
<i className="fas fa-long-arrow-alt-right"/>
82+
</button>
83+
</Link>
84+
</div>
85+
) }
86+
</div>
87+
);
88+
};
89+
90+
export default CartItemsContainer;

src/components/layout/footer/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const Footer = ({footer}) => {
2222
}, []);
2323

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

@@ -61,10 +61,10 @@ const Footer = ({footer}) => {
6161
</div>
6262
<div className="w-full lg:w-3/4 flex justify-end">
6363
{ !isEmpty( socialLinks ) && isArray( socialLinks ) ? (
64-
<ul className="flex item-center">
64+
<ul className="flex item-center mb-0">
6565
{ socialLinks.map( socialLink => (
66-
<li key={socialLink?.iconName} className="ml-4">
67-
<a href={ socialLink?.iconUrl || '/' } target="_blank" title={socialLink?.iconName}>
66+
<li key={socialLink?.iconName} className="no-dots-list mb-0 flex items-center">
67+
<a href={ socialLink?.iconUrl || '/' } target="_blank" title={socialLink?.iconName} className="ml-2 inline-block">
6868
{ getIconComponentByName( socialLink?.iconName ) }
6969
<span className="sr-only">{socialLink?.iconName}</span>
7070
</a>

src/components/layout/header/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const Header = ( { header } ) => {
3838
<Link href="/">
3939
<a className="font-semibold text-xl tracking-tight">{ siteTitle || 'WooNext' }</a>
4040
</Link>
41-
{ siteDescription ? <p>{ siteDescription }</p> : null }
41+
{ siteDescription ? <p className="mb-0">{ siteDescription }</p> : null }
4242
</span>
4343
</div>
4444
<div className="block lg:hidden">
@@ -53,7 +53,7 @@ const Header = ( { header } ) => {
5353
<div className="text-sm font-medium uppercase lg:flex-grow">
5454
{ ! isEmpty( headerMenuItems ) && headerMenuItems.length ? headerMenuItems.map( menuItem => (
5555
<Link key={ menuItem?.ID } href={ menuItem?.url ?? '/' }>
56-
<a className="block mt-4 lg:inline-block lg:mt-0 text-black hover:text-black mr-10"
56+
<a className="block mt-4 lg:inline-block lg:mt-0 hover:text-brand-royal-blue duration-500 mr-10"
5757
dangerouslySetInnerHTML={ { __html: menuItem.title } }/>
5858
</Link>
5959
) ) : null }

src/components/layout/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ const Layout = ({children, headerFooter}) => {
66
const { header, footer } = headerFooter || {};
77
return (
88
<AppProvider>
9-
<div >
9+
<div>
1010
<Header header={header}/>
11-
<main className="container mx-auto py-4">
11+
<main className="container mx-auto py-4 min-h-50vh">
1212
{children}
1313
</main>
1414
<Footer footer={footer}/>

src/components/products/product.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const Product = ( { product } ) => {
2424
width="380"
2525
height="380"
2626
/>
27-
<h3 className="font-bold uppercase my-2">{ product?.name ?? '' }</h3>
27+
<h6 className="font-bold uppercase my-2 tracking-0.5px">{ product?.name ?? '' }</h6>
2828
<div className="mb-4" dangerouslySetInnerHTML={{ __html: sanitize( product?.price_html ?? '' ) }}/>
2929
</a>
3030
</Link>

0 commit comments

Comments
 (0)