These days, ordering apps are more popular, especially since the outbreak of the pandemic, as they allow for the delivery of goods to your door from your preferred locations without the need to leave the house. So, let's add a Drink order app to our portfolio and resume, to make a significant impact.
In this tutorial, we'll use React's key features and hooks to create order app UI and cart functionality from scratch.
There are some essential features that we are going to use throughout the React application. It’s going to take place as follows.
- Reusable card components for displaying product information
- The context for access state
- Reducer for managing complex states like add-to-cart and remove-from-cart functionalities
- Cart container UI
- Refs for quantity controls
- Cart Badge
- Handle cart actions
- Implement Cart as Modal with Portals
- Handle Add to Cart button click event
These key concepts will help us manage a list of drinks, quantity, and cart-related actions.
In this tutorial, I'll save data into objects to display items.
React Installation and Setting up the New Project
Let's install React on a local server. There are various tools and frameworks like vite.js and Gatsby, but we'll use the create-react-app tool for this project because it's more beginner-friendly.
Open your favorite terminal and type create-react-app and then folder name, I'll name my folder 'refreshCall'.
npx create-react-app refreshCall
See my detailed post on local environment setup for in-depth explanation.
React Project Directory Structure
When you are done with installing, you will see that the React project has been installed with the folder name refreshCall or whatever you gave it earlier. Now, navigate to this folder and open it with your favorite editor.
I already reviewed the best text editors for you in my other post; check out which suits you best.
Before we get into coding, let's get familiar with folder structure so you won’t be confused throughout this tutorial.
In the refreshCall folder, we've got the subdirectory src, where we'll add files and folders. I structured my project idea, and the src folder will look like this:
We have three subfolders - components, context, and assets.
The components folder contains reusable components used to build UI. In the context folder we'll create the files for managing complex state and the assets folder will handle images.
I'll recommend installing React Developer Tools, it helps debug code easily.
Building Reusable UI Components
I’ll divide the layout of the ordering App into separate components known as building blocks. It’s a step-by-step breakdown.
Note - Remember to import every nested component and CSS module at the top of the file where you want to use them.
Header UI Component
Usually, all applications contain a Header area. We’ll add a header at the top with the background, which contains a logo and a cart button to show the number of items in the cart, and I'll also include the title and content about this application.
Look at the folder structure: A layout folder contains Header.js, Remember to start names with capital letters. Update the Header file with the code below.
import logo from '../../assests/img/refreshcall.png'
import CartButton from './CartButton'
const Header = (props) => {
return (
<header>
<nav>
<img src={logo} alt="refresh call" />
<CartButton />
</nav>
<div>
<h2>Quench your thirst with our <span>refreshing</span> selection </h2>
<p>
Discover the perfect balance of taste and refreshment in every sip of our handcrafted drinks.
</p>
</div>
</header>
)
}
export default Header
The Header contains the nested component CartButton, it will handle the toggle modal that contains the cart itself. For UI, we have a cart icon and badge.
import CartIcon from './cart/CartIcon'
const CartButton = (props) => {
return (
<button>
<span>
<CartIcon />
</span>
<span>0</span>
</button>
)
}
export default CartButton
Later, I’ll manage the number of items that the cart contains.
Here is the SVG code for the cart icon. This file is inside the Cart folder.
src/components/cart/CartIcon.js
const CartIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="square" strokeLinejoin="round"><circle cx="10" cy="20.5" r="1"/><circle cx="18" cy="20.5" r="1"/><path d="M2.5 2.5h3l2.7 12.4a2 2 0 0 0 2 1.6h7.7a2 2 0 0 0 2-1.6l1.6-8.4H7.1"/></svg>
)
}
export default CartIcon
Implementing CSS Module in React Project
When we handle multiple stylesheets, some conflicts may arise from naming conventions since CSS rules follow the global scope. As a result, I will use the CSS module throughout the App since it lets you use the same class names across different files.
A CSS module is a simple CSS file, we just need to add .module before the .css extension to the file, i.e. style.css as style.module.css.
Let’s do it practically, Inside the layout folder, we have a header.module.css file, code is going to be as follows -
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@500&display=swap');
.header {
max-width: 100%;
height: 400px;
background: url("../../assests/banner.jpg") no-repeat center/cover;
}
.nav-items {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 5rem;
}
.content {
font-family: 'Montserrat', sans-serif;
font-size: 1.25rem;
text-align: center;
}
.content p {
font-size: 1rem;
}
.content-color {
color: #670D49;
}
As the module follows the rules of local scope, we can have a header class in a different component with a totally different style.
To use these styles in that specific component, import the Header.module.css file at the top.
Import styles from ‘./Header.module.css’
Instead of styles, you can use headerStyle or any other related name. CSS classes are defined as className props with a given name in JSX.
We insert classes similar to using local variables in Javascript. If a class is a single name, we access the selector class as `styles.header` and if it uses a hyphen, we use a square bracket, i.e. `style[‘nav-items’]
import CartButton from './CartButton'
import styles from './Header.module.css'
import logo from '../../assests/img/refreshcall.png'
const Header = (props) => {
return (
<header className={styles.header}>
<nav className={styles['nav-items']}>
<img src={logo} alt="refresh call" />
<CartButton />
</nav>
<div className={styles.content}>
<h2>Quench your thirst with our <span className={styles['content-color']}>refreshing</span> selection </h2>
<p>
Discover the perfect balance of taste and refreshment in every sip of our handcrafted drinks.
</p>
</div>
</header>
)
}
export default Header
We have a CartButton.module.css file in the layout folder, and we will use these codes to apply the style to the CartButton similarly.
.cart-container {
position: relative;
background: #0D1835;
padding: .5rem;
border-radius: 5px;
cursor: pointer;
}
.badge {
width: 25px;
height: 25px;
position: absolute;
border-radius: 50%;
color: #fff;
background: #670D49;
top: -12px;
right: -12px;
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 12px;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0,0,0, 0.4);
}
Insert Module and access these local classes as follows -
import CartIcon from './cart/CartIcon'
import style from './CartButton.module.css'
const CartButton = (props) => {
return (
<button className={style['cart-container']}>
<span>
<CartIcon />
</span>
<span className={style.badge}>0</span>
</button>
)
}
export default CartButton
Now go ahead and import the Header component into App component, Which will look like this.
App.js
import Header from './components/layouts/Header'
function App() {
return (
<div>
<Header />
</div>
)
}
export default App
Store Dummy Data and Update UI
Now let’s store a list of different types of drinks in a simple JSON format. Here we’ll store the product information as dummy data in an array object, and at the top, we’ll also import images of drinks that we want to display on a page.
In our folder directory structure, we've got the file MenuItems.js, and all the following codes are going to be inside -
src/components/drinkCard/MenuItems.js
import style from './MenuItems.module.css'
import blueLemonade from '../assests/drinksItem/img1.jpg'
import blueBerrieSmoothie from '../assests/drinksItem/img2.jpg'
import cocolateShake from '../assests/drinksItem/img3.jpg'
import lemonade from '../assests/drinksItem/img4.jpg'
import majito from '../assests/drinksItem/img5.jpg'
import mintTea from '../assests/drinksItem/img6.jpg'
import raspberrySmoothie from '../assests/drinksItem/img7.jpg'
import strawberryShake from '../assests/drinksItem/img8.jpg'
import DrinksCard from './DrinksCard'
const DRINKS_DATA = [
{
id: 1,
name: 'Blue Lemonade',
describe: 'Cool and tangy delight',
price: 2.0,
image: blueLemonade
},
{
id: 2,
name: 'Blueberries smoothie',
describe: 'Creamy and refreshing',
price: 2.5,
image: blueBerrieSmoothie
},
{
id: 3,
name: 'Chocolate shake',
describe: 'Heavenly Chocolate Milkshake for Chocoholics',
price: 2.1,
image: cocolateShake
},
{
id: 4,
name: 'Lemonade',
describe: 'Enjoy the Tangy Refreshment of Lemonade',
price: 2,
image: lemonade
},
{
id: 5,
name: 'Majito',
describe: 'Combines the flavors with a vibrant blue color twist.',
price: 1.5,
image: majito
},
{
id: 6,
name: 'Mint Tea',
describe: 'Naturally, sweet and free of caffeine',
price: 1.9,
image: mintTea
},
{
id: 7,
name: 'Rasberry Smoothie',
describe: 'Burst of Flavors with a Raspberry Smoothie',
price: 2.7,
image: raspberrySmoothie
},
{
id: 8,
name: 'Strawberry Shake',
describe: 'Sweet Creamy Delight',
price: 2.4,
image: strawberryShake
}
]
const MenuItems = () => {
return (
<section className={style.items}>/* ... */</section>
)
}
export default MenuItems
Inside MenuItems component we've got an object array of data. To transform that data into an array of components, we’ll use the Javascript method map().
<section className={style.items}>
{DRINKS_DATA.map(drink =>
<DrinksCard
key={drink.id}
id={drink.id}
image={drink.image}
name={drink.name}
describe={drink.describe}
price={drink.price}
/>
)}
</section>
Inside the map method, we return the DrinksCard component, and we use props to pass data from the MenuItems component to the child component.
The key is set to a unique id for each drink item list for rendering.
We encouraged creating separate components to split code, as it should be clean and not too crowded.
We also have a file named MenuItems.module.file and it’s going to be as follows -
.items {
max-width: 65rem;
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
margin: 1rem auto;
grid-gap: 20px;
}
@media (max-width: 768px) {
.items {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
}
@media (max-width: 480px) {
.items {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
}
Now we can also import MenuItems in the App component after the Header component.
App.js
import Header from './components/layout/Header'
import MenuItems from './components/drinkCard/MenuItems'
function App() {
return (
<div>
<Header />
<MenuItems />
</div>
)
}
Card UI for Rendering Data
To display the information such as name, image, description, quantity controller form, and add button, I’ll utilize the card layout for each drink item.
import style from './DrinksCard.module.css'
const DrinksCard = (props) => {
return (
<div className={style["drinks-card"]}>
<img src={props.image} alt={props.name} className={style["drinks-card__image"]} />
<div className={style["drinks-card__content"]}>
<h3 className={style["drinks-card__name"]}>{props.name}</h3>
<p className={style["drinks-card__describe"]}>{props.describe}</p>
</div>
<div className={style['drinks-card__footer']}>
<div className={style["drinks-card__quantity"]}>
{ /* quantity controller */ }
</div>
<div className={style.price}>
1$
</div>
<button className={style["drinks-card__add-btn"]}>Add</button>
</div>
</div>
)
}
export default DrinksCard
We passed props as an argument so we could read properties from a parent component.
Quantity Increment & Decrement Button
To Allow increasing and decreasing quantities, let’s add a nested component called QuantityController inside DrinksCard. We've got a div wrapper with className “drinks-card__quantity”, inside this update with the child component.
<div className={style["drinks-card__quantity"]}>
<QuantityController />
</div>
Our QunatityController component will look like this.
import style from './QuantityController.module.css'
const QuantityController = (props) => {
return
<>
<button className={style["quantity__btn"]}>-</button>
<input
type='number'
min="0"
className={style["quantity_number"]}
/>
<button className={style["quantity__btn"]}>+</button>
</>
)
})
export default QuantityController
Here we've got buttons for decreasing and increasing quantities and input to display the number of quantities. I’m going to come back to this file again. Let’s go to the DrinksCard component to add logic to this file.
To handle quantity, we’ll use state update logic and refs to track the current quantity value.
import { useContext, useRef, useState } from 'react'
import style from './DrinksCard.module.css'
import CartContext from '../CartContext/CartContext'
import QuantityController from './QuantityController'
const DrinksCard = (props) => {
const contextItems = useContext(CartContext)
const [itemQuantity, setItemQuantity] = useState(1)
const quantRef = useRef(null)
const price = props.price.toFixed(2)
const amount = +price
const handleQuanIncrease = () => {
setItemQuantity(prevQuant => (prevQuant + 1))
}
const hadndleQuanDecrease = () => {
if (itemQuantity > 1) {
setItemQuantity(prevQuant =>Math.max(prevQuant - 1, 0))
}
}
const handleQuantityChange = () => {
const updateQuantity = parseInt(quantRef.current.value)
setItemQuantity(updateQuantity)
}
const hadndleAddToCart = () => {
const quantity = parseInt(quantRef.current.value)
const item = {
id: props.id,
image: props.image,
name: props.name,
price: amount,
quantity: quantity
}
contextItems.addItem(item)
}
return (
<div className={style["drinks-card"]}>
<img src={props.image} alt={props.name} className={style["drinks-card__image"]} />
<div className={style["drinks-card__content"]}>
<h3 className={style["drinks-card__name"]}>{props.name}</h3>
<p className={style["drinks-card__describe"]}>{props.describe}</p>
</div>
<div className={style['drinks-card__footer']}>
<div className={style["drinks-card__quantity"]}>
<QuantityController
ref={quantRef}
onAdd={handleQuanIncrease}
onRemove={hadndleQuanDecrease}
onChange={handleQuantityChange}
quantityNumber={itemQuantity} />
</div>
<div className={style.price}>
{`$${amount * itemQuantity}`}
</div>
<button className={style["drinks-card__add-btn"]} onClick={hadndleAddToCart}>Add</button>
</div>
</div>
)
}
export default DrinksCard
In the DrinksCard component, we’ve created an event handler that handles the logic to increase or decrease when the button is clicked. The number of items can’t go below 1.
We used toFixed() on price, this is a Javascript method and is used to format a number using fixed-point notation. It converted the number to a string, so I added the `+` operator.
There, instead of the hard-coded price, we dynamically calculated the price by its quantity.
<div className={style.price}>
{`$${amount * itemQuantity}`}
</div>
Inside QuantityController, forward refs and read all the properties from the parent component.
import style from './QuantityController.module.css'
import { forwardRef } from 'react'
const QuantityController = forwardRef((props, ref) => {
return (
<>
<button className={style["quantity__btn"]} onClick={props.onRemove}>-</button>
<input
ref={ref}
type='number'
min="0"
value={props.quantityNumber}
onChange={props.onChange}
className={style["quantity_number"]}
/>
<button className={style["quantity__btn"]} onClick={props.onAdd}>+</button>
</>
)
})
export default QuantityController
We've got the styling for DrinksCard and QuantityController for a visually appealing design.
.drinks-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
background-image: linear-gradient(180deg, transparent, #0D1835 70%);
color: #f1f1f1;
margin-bottom: 1rem;
width: 280px;
height: auto;
border-radius: 8px;
border: 1px solid #ddd;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.4);
transition: transform 0.3s;
}
.drinks-card__image {
width: 100%;
height: 180px;
object-fit: cover;
object-position: center;
border-radius: 0 0 25% 0;
}
.drinks-card__content {
padding: 10px 20px;
}
.drinks-card__name {
font-size: 18px;
font-weight: 700;
letter-spacing: 2px;
}
.drinks-card__discribe {
font-size: 14px;
margin-bottom: 20px;
}
.drinks-card__footer {
display: flex;
width: calc(100% - 20px);
padding: 10px;
justify-content: space-between;
align-items: center;
}
.drinks-card__quantity {
border-radius: 2px;
background: #ccc;
font-size: 1rem;
font-weight: 400;
color: #000;
}
.drinks-card__add-btn {
background: #670D49;
padding: 10px 20px;
color: #fff;
border: 0;
box-shadow: 0 2px 4px rgba(103,13,73, 0.9);
}
.drinks-card:hover {
transform: translateY(10px);
}
.quantity_number {
display: inline-block;
width: 25px;
height: 100%;
font-size: 16px;
margin: 0 10px;
text-align: center;
border: none;
background: transparent;
}
.quantity__btn {
border: none;
background-color: transparent;
font-size: 16px;
font-weight: 500;
margin: 4px 6px;
cursor: pointer;
}
Building the Cart Functionality
Cart functionality is the main highlight of our Drink Order project, where we’ll display the list of added products. We’ll handle Cart functionality in different places.
- Cart component, where we’ll display added products.
- Product page, where we’ll handle adding drinks to the cart.
Typically, we use props to pass down state or event handler functions from parent to child components, but since we need to access the cart state and functions in various components, we’ll use React Context.
A context makes information available to the component hierarchy. We’ll manage the cart state with context so any child components that need to use that state can access it.
Implementing state management to handle Cart Items
Let’s create a context for our cart. In React, we can use createContext method to initialize the context.
import { createContext } from "react"
const CartContext = createContext({
items: [],
totalAmount: 0,
addItem: (item) => {},
removeItem: (id) => {}
})
export default CartContext
Here, we’ve created context and provided default values of empty items, totalItems : 0, and functions for updating the cart. React will update these default values when the context changes.
Next, we’ll declare Provider in a separate file where we’ll handle state. Import CartContext into this file.
Import CartContext from ‘./CartContext’
Const CartProvider = ({children }) => {
Return (
<CartContext.Provider>{children}</CartContext.Provider>
)
Export { CartProvider}
To retrieve cart information for all components in the hierarchy tree, we’ll modify the App component and wrap Provider around children components. Let’s update App component.
App.js
import Header from './components/layout/Header'
import MenuItems from './components/drinkCard/MenuItems';
import { CartProvider } from './CartContext/CartProvider';
function App () {
return (
<CartProvider>
<Header />
<MenuItems />
</CartProvider>
)
}
Our next step will be to handle various actions such as adding items to the shopping cart, removing items from the cart, and clearing cart items.
To Handle these many actions, we’ll use react essential hook useReducer, which will manage state updates for the cart.
Managing Cart Actions with useReducer
In CartProvider, let's declare the initial state and declare cartReducer outside of CartProvider component functions. You can use a separate file for cartReducer as well.
import { useReducer } from "react"
const cartInitialState = {
items: [],
totalPrice: 0
}
Const cartReducer = (state, action) => {}
The reducer specifies how the state gets updated. Now that we can declare the useReducer hook inside CartProvider, let’s update this file.
const CartProvider = ({children}) => {
const [state, dispatch] = useReducer(cartReducer, cartInitialState)
// add items to the cart
const handleAddItem = item => {
dispatch({
type: 'ADD_TO_CART',
item: item
})
}
// remove items from the cart
const handleRemoveItem = id => {
dispatch({
type: 'REMOVE_FROM_CART',
id: id
})
}
// clear cart items
const handleClearItem = () => {
dispatch({
type: 'CLEAR_CART_ITEMS'
})
}
return (
<CartContext.Provider value={contextValue}>
{children}
</CartContext.Provider>
)
}
Here, we’ve defined functions: handleAddItem, handleRemoveItem, and handleClearItem, which dispatch corresponding actions to update the product to the cart.
Whenever we create a provider, we need to specify the value of the context that we want access to for the consumer. We’ll pass an object to the value attribute, which contains items, total price, and updating functions.
const contextValue = {
items: state.items,
total: state.totalPrice,
addItem: handleAddItem,
removeItem: handleRemoveItem,
clearItem: handleClearItem
}
return (
<CartContext.Provider value={contextValue}>
{children}
</CartContext.Provider>
)
In reducer method, we’ll handle these three actions that will look like this -
Const cartReducer = (state, action) => {
switch(action.type) {
case 'ADD_TO_CART':
// Handle adding items to the cart //
break;
case 'REMOVE_FROM_CART':
// Handle removing items from the cart //
break;
case ‘CLEAR_CART_ITEMS’:
// Handle clearing items from the cart //
default:
return state
}
}
Let’s work with these cases one by one.
Add Items To Shopping Cart
To add products to the cart, we’ll update ADD_TO_CART action. First, we’ll check if the item with the same ID already exists; if it does, we’ll only update its quantity, and if not, we’ll add drink as a new product in the shopping cart.
case 'ADD_TO_CART':
const {id, price, quantity} = action.item
const existingCartItem = state.items.find(item => item.id === id)
const cartItemsTotal = state.totalPrice + price * quantity
if (existingCartItem) {
// if item exist to cart increment its quantity
const cartItems = state.items.map((item) => {
if(item.id === id) {
return {...item, quantity: item.quantity + quantity}
}
return item
})
return {
...state,
items: cartItems,
totalPrice: cartItemsTotal
}
}
const updatedCartItems = [...state.items, action.item]
return {
...state,
items: updatedCartItems,
totalPrice: cartItemsTotal
}
Remove Item from Shopping Cart
To remove a drink item from the cart, we’ll update the REMOVE_FROM_CART action in the reducer method.
case 'REMOVE_FROM_CART':
const cartItem = state.items.find(item => item.id === action.id)
const updatedTotalPrice = state.totalPrice - cartItem.price
if (cartItem && cartItem.quantity > 1) {
const updatedItems = state.items.map(item => {
if (item.id === action.id) {
return {...item, quantity: item.quantity - 1}
}
return item
})
return {
...state,
items: updatedItems,
totalPrice: updatedTotalPrice
}
} else {
const updatedItems = state.items.filter(item => item.id !== action.id)
return {...state, items: updatedItems, totalPrice: updatedTotalPrice}
}
Here, we’re mapping over each drink item array and decreasing the quantity if the ID matches, and we also update totalPrice by subtracting the price of cart item.
If we have one item in the cart, the dispatching action is going to delete that specific drink from the cart.
Clear Items in Cart
To clear all the items in cart container, we’ll update CLEAR_CART_ITEMS in reducer method.
case 'CLEAR_CART_ITEMS':
return {
...state,
items: [],
totalPrice: 0,
}
Here, we’ve passed an empty array and updated the total price to 0.
The reducer method will look like this with all actions inside.
const cartReducer = (state, action) => {
switch(action.type) {
case 'ADD_TO_CART':
const {id, price, quantity} = action.item
const existingCartItem = state.items.find(item => item.id === id)
const cartItemsTotal = state.totalPrice + price * quantity
if (existingCartItem) {
// if item exist to cart increment its quantity
const cartItems = state.items.map((item) => {
if(item.id === id) {
return {...item, quantity: item.quantity + quantity}
}
return item
})
return {
...state,
items: cartItems,
totalPrice: cartItemsTotal
}
}
const updatedCartItems = [...state.items, action.item]
return {
...state,
items: updatedCartItems,
totalPrice: cartItemsTotal
}
case 'REMOVE_FROM_CART':
const cartItem = state.items.find(item => item.id === action.id)
const updatedTotalPrice = state.totalPrice - cartItem.price
if (cartItem && cartItem.quantity > 1) {
const updatedItems = state.items.map(item => {
if (item.id === action.id) {
return {...item, quantity: item.quantity - 1}
}
return item
})
return {
...state,
items: updatedItems,
totalPrice: updatedTotalPrice
}
} else {
const updatedItems = state.items.filter(item => item.id !== action.id)
return {...state, items: updatedItems, totalPrice: updatedTotalPrice}
}
case 'CLEAR_CART_ITEMS':
return {
...state,
items: [],
totalPrice: 0,
}
default:
return state
}
}
Adding Products to the Cart
To allow us to add drinks to the cart, we need to handle Add to Cart button click event. Update DrinksCard component in the following way.
src/components/drinkCard/DrinksCard.js
import { useContext, useRef, useState } from 'react'
import style from './DrinksCard.module.css'
import CartContext from '../CartContext/CartContext'
import QuantityController from './QuantityController'
const DrinksCard = (props) => {
const contextItems = useContext(CartContext)
const [itemQuantity, setItemQuantity] = useState(1)
const quantRef = useRef(null)
const price = props.price.toFixed(2)
const amount = +price
const handleQuanIncrease = () => {
setItemQuantity(prevQuant => (prevQuant + 1))
}
const hadndleQuanDecrease = () => {
if (itemQuantity > 1) {
setItemQuantity(prevQuant =>Math.max(prevQuant - 1, 0))
}
}
const handleQuantityChange = () => {
const updateQuantity = parseInt(quantRef.current.value)
setItemQuantity(updateQuantity)
}
const handleAddToCart = () => {
const quantity = parseInt(quantRef.current.value)
const item = {
id: props.id,
image: props.image,
name: props.name,
price: amount,
quantity: quantity
}
contextItems.addItem(item)
}
return (
<div className={style["drinks-card"]}>
<img src={props.image} alt={props.name} className={style["drinks-card__image"]} />
<div className={style["drinks-card__content"]}>
<h3 className={style["drinks-card__name"]}>{props.name}</h3>
<p className={style["drinks-card__describe"]}>{props.describe}</p>
</div>
<div className={style['drinks-card__footer']}>
<div className={style["drinks-card__quantity"]}>
<QuantityController
ref={quantRef}
onAdd={handleQuanIncrease}
onRemove={hadndleQuanDecrease}
onChange={handleQuantityChange}
quantityNumber={itemQuantity} />
</div>
<div className={style.price}>
{`$${amount * itemQuantity}`}
</div>
<button className={style["drinks-card__add-btn"]} onClick={handleAddToCart}>Add</button>
</div>
</div>
)
}
export default DrinksCard
Here, we’ve defined the handleAddToCart function and passed this handler function to “add-to-cart” button.
The notable changes are related to the context, so we imported CartContext into this file and are consuming the context with object contextItems.
import CartContext from '../CartContext/CartContext'
const contextItems = useContext(CartContext)
Inside the function, we use quantRef.current.value to access the current value and parse the value to an integer with parseInt to ensure that quantity is a number.
const quantity = parseInt(quantRef.current.value)
The handleAddToCart function contains updated item objects with relevant information which we pass to the addItems that we retrieve from contextItems. As a result cart context will be updated and all components that need the cart will be re-rendered
const handleAddToCart = () => {
const quantity = parseInt(quantRef.current.value)
const item = {
id: props.id,
image: props.image,
name: props.name,
price: amount,
quantity: quantity
}
contextItems.addItem(item)
}
We are done with the DrinksCard component. Now whenever we click the Add button, it’ll trigger the dispatch action and update cart items. You can pass the console and see if it’s working correctly.
Cart UI
To display the cart drink items we’ll create a cart container and inside we’ll display the list of items, cart total price, and checkout button. This is the complete UI of the Cart.
import CartContext from "../../CartContext/CartContext"
import CartItem from './CartItem'
import style from './Cart.module.css'
import { useContext } from "react"
const Cart = (props) => {
const contextItems = useContext(CartContext)
return (
<div className={style.cartModalContainer}>
<header className={style.cartHeader}>
<button className={style.cartBackBtn}>
×
</button>
<h2>Cart</h2>
<button className={style.cartClear}>
clear All
</button>
</header>
<div className={style.cartItemsContainer}>
{contextItems.items.length === 0 ? (
<p>There is no items in the cart</p>
) : (
<ul className={style.cartItems}>
{contextItems.items.map((item) => {
return <CartItem
key={item.id}
id={item.id}
name={item.name}
price={item.price}
image={item.image}
quantity={item.quantity}
onAdd={() => handleIncrements(item.id)}
onRemove={() => handleDecrement(item.id)}
/>
})}
</ul>
)}
<div className={style.total}>
<span>Total</span>
<span>$17</span>
</div>
<div className={style.cartDevider}></div>
<button className={style.checkoutBtn}>Check Out</button>
</div>
</div>
)
}
export default Cart
We consumed the cart context and created the contextItems object.
const contextItems = useContext(CartContext)
We loop over the items that we retrieve from contextItems and render each item with drink information as a prop.
This is a module.css file that contains CSS styling.
src/components/cart/Cart.module.css
.cartModalContainer {
background-color: #c1c1c1;
width: 430px;
}
.cartHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.2rem;
color: #2d2d2d;
padding: 0.9rem;
}
.cartBackBtn {
background-color: transparent;
border: none;
cursor: pointer;
font-size: 1.5rem;
font-weight: 800;
color: #000;
}
.cartHeader h2 {
margin: 0;
}
.cartClear {
background-color: #f7f5f5;
border-radius: 4px;
padding: 0.5rem;
border: 0;
cursor: pointer;
}
.cartItemsContainer {
/* background-color: #111; */
border-top-left-radius: 35px;
border-top-right-radius: 35px;
box-sizing: content-box;
background-color: #333;
padding: 1.5rem;
color: #e5e3e3;
height: 100vh;
}
.cartItems {
padding: 0;
margin: 0;
max-height: 25rem;
overflow: auto;
}
.total {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 18px;
font-style: italic;
font-weight: bold;
margin: 1rem 0;
}
.cartDevider {
border: 1px solid #ccc;
margin: 2rem 0;
}
.checkoutBtn {
background: #670D49;
display: block;
width: 100%;
padding: 0.9rem;
border-radius: 25px;
border: 0;
box-shadow: 0 4px 8px rgba(0,0,0, 0.1);
color: #fff;
font-size: 0.9em;
font-weight: 400;
letter-spacing: 1px;
}
Here, we’ve also nested the component CartItem for individual cart drinks.
import style from './CartItem.module.css'
const CartItem = (props) => {
const price = `$${props.price * props.quantity}`
return (
<li className={style.cartItem}>
<div className={style.cartDetails}>
<img src={props.image} alt={props.name} />
<div className={style.cartInfo}>
<h3>{props.name}</h3>
<span className={style.price}>{price}</span>
</div>
</div>
<div className={style.itemQuantity}>
<button onClick={props.onRemove}>-</button>
<span>{props.quantity}</span>
<button onClick={props.onAdd}>+</button>
</div>
</li>
)
}
export default CartItem
We multiply the drink price by the number of that drink item to get the total cost of each drink. For the onClick event handler, we have onAdd and onRemove props handlers; we will start working on this soon.
Styles for cart items are contained in the CartItem.module.css file.
.cartItem {
background: #3e3e3e;
color: #fff;
border: 1px solid black;
margin-bottom: 1rem;
padding: 0.9rem;
list-style-type: none;
display: flex;
justify-content: space-between;
align-items: center;
}
.cartDetails {
display: flex;
align-items: center;
}
.cartDetails img {
width: 50px;
height: 50px;
border-radius: 50%;
}
.cartInfo {
display: flex;
flex-direction: column;
padding: 0 0.9rem;
}
.itemQuantity {
background-color: #565656;
}
.itemQuantity button {
background-color: transparent;
border: none;
color: #fff;
font-size: 16px;
font-weight: bold;
}
Everything is in order, and we can add drink items to the shopping cart. The shopping cart will look like this.
Clear cart item
Now that we have a cart user interface, let us work with a state where clicking the clear button will remove every item from the cart container at once.
Inside the Cart component, We consume the cart state from the context with the useContext hook. Let’s specify handleClearItem function in the Cart functional component.
const handleClearItem = () => {
contextItems.clearItem()
}
The function calls clearItem(), which triggers the dispatch action and updates the cart state to an empty array.
Inside JSX pass the handler function to clear-all button.
<button className={style.cartClear} onClick={() => handleClearItem(props.item)}>
Clear All
</button>
Now when you click the clear-all button should remove all the items form the cart.
Increase and decrease the number of products in your cart
Let us begin by incrementing and decrementing the number of drink items in the cart on a button click. Declare the function handleIncrement inside the Cart functional component.
const handleIncrements = (itemId) => {
const item = contextItems.items.find(item => item.id === itemId)
if (item) {
const updatedItem = {...item, quantity: 1}
contextItems.addItem(updatedItem)
}
}
If the cart contains one product with multiple quantities, the quantity will be reduced by one, but if the product only has one quantity, the product will be deleted from the cart.
Inside the JSX, pass these handler function as props to CartItem.
return <CartItem
key={item.id}
id={item.id}
name={item.name}
price={item.price}
image={item.image}
quantity={item.quantity}
onAdd={() => handleIncrements(item.id)}
onRemove={() => handleDecrement(item.id)}
/>
Here, we used handler functions as an anonymous function and passed item.id as an argument. When the increment button is clicked, the handleIncrements function is invoked, and when the decrease button is clicked, the handleDecrements function removes one item.
Total Price in Cart
The final step is to get the subtotal and display it in the cart. In order to accomplish this, we will retrieve the total property from the cartContext object and apply the toFixed method to it.
const subtotal = `$${contextItems.total.toFixed(2)}`
Let’s specify the subtotal const inside the JSX, and it’ll output the total price for the drinks in the cart container.
<div className={style.total}>
<span>Total</span>
<span>{subtotal}</span>
</div>
This is the whole Cart component with all the code written here that I explained earlier.
import CartContext from "../../CartContext/CartContext"
import CartItem from './CartItem'
import style from './Cart.module.css'
import { useContext } from "react"
const Cart = (props) => {
const contextItems = useContext(CartContext)
const subtotal = `$${contextItems.total.toFixed(2)}`
const handleClearItem = () => {
contextItems.clearItem()
}
const handleIncrements = (itemId) => {
const item = contextItems.items.find(item => item.id === itemId)
if (item) {
const updatedItem = {...item, quantity: 1}
contextItems.addItem(updatedItem)
}
}
const handleDecrement = (id) => {
contextItems.removeItem(id)
}
return (
<div className={style.cartModalContainer}>
<header className={style.cartHeader}>
<button className={style.cartBackBtn}>
×
</button>
<h2>Cart</h2>
<button className={style.cartClear} onClick={() => handleClearItem(props.item)}>
clear All
</button>
</header>
<div className={style.cartItemsContainer}>
{contextItems.items.length === 0 ? (
<p>There is no items in the cart</p>
) : (
<ul className={style.cartItems}>
{contextItems.items.map((item) => {
return <CartItem
key={item.id}
id={item.id}
name={item.name}
price={item.price}
image={item.image}
quantity={item.quantity}
onAdd={() => handleIncrements(item.id)}
onRemove={() => handleDecrement(item.id)}
/>
})}
</ul>
)}
<div className={style.total}>
<span>Total</span>
<span>{subtotal}</span>
</div>
<div className={style.cartDevider}></div>
<button className={style.checkoutBtn}>Check Out</button>
</div>
</div>
)
}
export default Cart
Update Cart Button badge
We’ve successfully implemented add-to-cart functionality in our Drinks Ordering App, but how will we know how many items we added to the cart? For this, we’ll apply some logic inside the CartButton component and update the number of items that are in the cart on the cart button.
Let’s access the cart state from the CartContext with the useContext hook. remember to import CartContext and useContext hook at the top of the CartButton component.
import CartContext from '../CartContext/CartContext'
import { useContext } from 'react'
const cartContext = useContext(CartContext)
You can retrieve an item array from the cartContext object.
To display the total count of drinks, call the reduce method on items; it will execute the function for array elements and return the accumulated result.
const totalQuantity = cartValue.items.reduce((total, item) => {
return total + item.quantity
}, 0)
Now pass the const totalQuantity inside the span and wrap it with curly braces.
<span className={style.badge}>{totalQuantity}</span>
If the array is empty, the value 0 will be displayed. Here is the complete code of CartButton component.
import CartIcon from './cart/CartIcon'
import style from './CartButton.module.css'
import CartContext from '../CartContext/CartContext'
import { useContext } from 'react'
const CartButton = (props) => {
const cartValue = useContext(CartContext)
const totalQuantity = cartValue.items.reduce((total, item) => {
return total + item.quantity
}, 0)
return (
<button className={style['cart-container']} onClick={props.onClick}>
<span>
<CartIcon />
</span>
<span className={style.badge}>{totalQuantity}</span>
</button>
)
}
export default CartButton
Now, when you click the add-to-cart button, the badge will update and the number will be increased.
Cart Modal using Portal
Let’s start by creating a modal dialog box and placing our shopping cart inside of it, which we can toggle using the shopping cart button.
We’ll use the React Portal to render shopping cart; it’ll allow cart modal to appear on top of other components.
To render the portal, we must first define another DOM node. Place an HTML element with an id in the index.html file.
<div id="overlay-root"></div>
<div id="root"></div>
I gave you a directory structure where I place the Modal.js file inside the layout folder, there we’ll render the backdrop shadow and the cart.
import { createPortal } from "react-dom"
import { Fragment } from "react"
import style from './Modal.module.css'
import Cart from "./cart/Cart"
const Modal = (props) => {
return (
<Fragment>
{createPortal(
<div className={style.backdrop} onClick={props.onClose} />,
document.getElementById('overlay-root')
)}
{createPortal(
<div className={style.cartModal}>
<Cart onClose={props.onClose} />
</div>,
document.getElementById('overlay-root')
)}
</Fragment>
)
}
export default Modal
The createPortal is a built-in API in React that allows the creation of a portal that accepts JSX and DOM nodes. Because there in our code are two portals, side by side to display backdrop shadow and cart, we wrapped these with Fragment.
The following is the CSS module to style Modal.
.backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
z-index: 20;
background-color: rgba(0, 0, 0, 0.5);
}
.cartModal {
position: fixed;
top: 10vh;
right: 0;
display: flex;
flex-direction: column;
z-index: 30;
height: 100vh;
max-width: 100%;
animation: slide-out 500ms forwards;
}
@keyframes slide-out {
from {
transform: translateX(100%);
}
to {
transform: translateX(0%);
}
}
Now, In the App component, I’ll import my Modal and also we’ll update the App to write logic for opening and closing the Modal on button click.
const [toggleCart, setToggleCart] = useState(false)
const toggleCartHandler = () => {
setToggleCart(prevState => !prevState)
}
const closeCartHandler = () => {
setToggleCart(false)
}
We’ve defined toggleCartHandler function to toggle a modal dialog on a button click, and closeCartHandler to close the modal that contains the cart and dropshadow.
Now in JSX, we’ll use the conditional operator && to toggle visibility.
{toggleCart && <Modal onClose={closeCartHandler} />}
<Header onToggle={toggleCartHandler} />
The onClose and onToggle are props that we are passing to its child components. The whole App component will look like this.
import Header from './components/layout/Header'
import MenuItems from './components/layout/MenuItems';
import { CartProvider } from './CartContext/CartProvider';
import Modal from './components/Modal';
import { useState } from 'react';
function App() {
const [toggleCart, setToggleCart] = useState(false)
const toggleCartHandler = () => {
setToggleCart(prevState => !prevState)
}
const closeCartHandler = () => {
setToggleCart(false)
}
return (
<CartProvider>
{toggleCart && <Modal onClose={closeCartHandler} />}
<Header onToggle={toggleCartHandler} />
<MenuItems />
</CartProvider>
);
}
export default App;
Now inside the Modal component pass props as an argument.
src/components/layout/Modal.js
{createPortal(
<div className={style.backdrop} onClick={props.onClose} />,
document.getElementById('overlay-root')
)}
This will close the modal dialog box even when we click on backdrop shadow.
Now also pass props onClose in Cart because a close button is located inside the Cart component.
{createPortal(
<div className={style.cartModal}>
<Cart onClose={props.onClose} />
</div>,
document.getElementById('overlay-root')
)}
Inside the Cart component, we’ve a close button, I want this button interactable when we click on it. Passing the props as argument and using the onClose property in onClick event handler will help us to accomplish the desired result.
<button className={style.cartBackBtn} onClick={props.onClose}>
×
</button>
Remember, we want to enable the toggle functionality when the shopping cart button is clicked. Let’s pass the props to the CartButton component.
To use the toggleCartHandler function inside CartButton we have passed props to Header and Header component will pass props to CartButton component which is nested inside Header. This is called props drilling but no worry passing props through a number of components is okay if it’s a small application.
<nav className={styles['nav-items']}>
<img src={logo} alt="refresh call" />
<CartButton onClick={props.onToggle} />
</nav>
<button className={style['cart-container']} onClick={props.onClick}> …. </button>
The shopping modal will now slide in and become visible on the screen when you click the shopping button. It will disappear once you click the close button or when you click outside of the modal.
Conclusion
In this tutorial, we used React's core functionality to build a simple drink-ordering application.
We created a user interface for a drink ordering app that included functionality for adding drinks to the cart as well as editing item quantities, removing entire items from the cart, and clearing the cart.
Our primary motivation for developing this simple application was so that we could utilize its key features.
0 Comments:
Post a Comment