ShopKart Shopping Cart & LocalStorage

Business Scenario

Hello talented developers!

ShopKart has successfully implemented its product listing and filtering functionality. Customers can now browse products and find items they are interested in purchasing.

What’s Already Working?

Product listing and filtering are implemented.Customers can now browse products and find items they are interested in purchasing

The Challenge

Add to Cart buttons are currently only part of the product interface.

Customers need a proper shopping cart where they can:

  • Review selected products
  • Modify quantities
  • Remove unwanted items
  • See the total amount before checkout

In this lab, students will transform the existing ShopKart product-selection experience into a fully functional shopping cart.

Pre-Lab Preparation

Module:

1) Handling Side-Effects

2) Handling Forms in React

git pull origin branchName

Git Pull

Task 1 : Create the Cart Page Structure

  • We already have a Cart.jsx page in the project, but it is currently only a placeholder.

Open Cart.jsx and  Create the Cart Table structure.

1

  • We will replace it with the actual ShopKart Cart page.
function Cart() {
  return (
    <div className="cart-page">
      <div className="cart-container">

        {/* Cart Table */}
        <div className="cart-table-wrapper">
          <table className="cart-table">
            <thead>
              <tr>
                <th>PRODUCT</th>
                <th>PRICE</th>
                <th>QUANTITY</th>
                <th>TOTAL</th>
                <th>ACTION</th>
              </tr>
            </thead>

            <tbody>
              {/* Product 1 */}

              <tr className="cart-item-row">
                <td>
                  <div className="cart-product">
                    <div className="cart-product-image">
                      <img src="/images/products/headphones.png" alt="boAt Rockerz 450" />
                    </div>

                    <div className="cart-product-info">
                      <h3>boAt Rockerz 450</h3>
                      <p className="cart-category">Electronics</p>
                    </div>
                  </div>
                </td>

                <td>
                  <div className="cart-price">
                    <strong>₹1,599</strong>
                    <span className="original-price">₹1,999</span>
                  </div>
                </td>
                <td>
                  <div className="quantity-control">
                    <button type="button">−</button>
                    <span>1</span>
                    <button type="button">+</button>
                  </div>
                </td>

                <td> <strong className="item-total">₹1,599</strong> </td>

                <td>
                  <button type="button" className="delete-btn">
                    <img src="/icons/delete-icon.png" alt="Delete" />
                  </button>
                </td>
              </tr>
            </tbody>
          </table>

          {/* Clear Cart */}
          <div className="cart-footer">
            <button type="button" className="clear-cart-btn">
              <img src="/icons/clear-cart.png" alt="" /> Clear Cart
            </button>
          </div>
        </div>
);
}
export default Cart;

Create Order Summary & Continue Shopping

2

{/* Order Summary */}
<aside className="cart-sidebar">
  <div className="order-summary">
    <h2>Order Summary</h2>

    <div className="summary-row">
      <span>Subtotal (2 items)</span>
      <span>₹5,797</span>
    </div>

    <div className="summary-row discount-row">
      <span>Discount</span>
      <span>−₹1,300</span>
    </div>
  • The Order Summary shows the financial breakdown of the order.
  • Inside .cart-container Add the Order Summary and continue shopping button
    after .cart-table-wrapper:
    <div className="summary-row">
      <span>Shipping</span>
      <span className="free-shipping">FREE</span>
    </div>

    <div className="summary-divider"></div>

    <div className="summary-total">
      <span>Total Amount</span>
      <strong>₹4,497</strong>
    </div>

    <p className="saved-message">You saved ₹1,300 on this order</p>

    <button type="button" className="checkout-btn">
      Proceed to Checkout
      <span>→</span>
    </button>

    <button type="button" className="coupon-btn">
      <img src="/icons/coupon.png" alt="" />
      Apply Coupon
    </button>
  </div>

  {/* Secure Payment */}

  <div className="secure-box">
    <img src="/icons/security-icon.png" alt="" className="secure-icon" />
    <div>
      <h3>Safe & Secure Payments</h3>
      <p>100% secure payments. Easy returns. Your data is protected.</p>
    </div>
  </div>
</aside>
</div>

{/* Continue Shopping */}

<div className="cart-navigation">
  <Link to="/products" className="continue-shopping">← Continue Shopping</Link>
</div>
</div>

Style the Cart Page

3

Since we now use Link, don't forget to add the import.

Add Cart Page Route

4

  • Now we connect the Cart page to React Router.
  • In App.jsx add :
<Route path="/cart" element={<Cart />} />

Connect Navbar Cart Icon to /cart

5

<Link to="/cart" className="cart-wrapper">
  
  <img src="/icons/cart-icon.png" alt="Cart" className="nav-icon" />
  <span className="cart-count"> 3 </span>
  
</Link>

Move Cart State to App.jsx

6

In App.jsx : import useReducer

7

  • Now we begin making the Cart functional.
  • The cart needs to be shared between:

Products --> App --> Cart --> Navbar

  • The cart state will be maintained in App.jsx because App.jsx is the common parent of: Navbar, Products, Cart
  • This allows us to pass cart data through props.
import React, { useEffect, useReducer} from "react";

Then inside App component :

const [cart, dispatch] = useReducer(
  cartReducer,
  []
);

Why useReducer?

The shopping cart has multiple related actions that need to update the same cart state.

  • Add Product
  • Increase Quantity
  • Decrease Quantity
  • Remove Product
  • Clear Cart

Instead of creating separate state logic for every operation, useReducer gives us one central place to manage all cart state changes.

Task 2 :  Create Cart Reducer Logics

Create the reducer logic for Adding Item to cart

1

function cartReducer(state, action) {

  switch (action.type) {
    case "ADD_ITEM": {

      const existingItem = state.find((item) => item.id === action.payload.id);

      if (existingItem) {
        
        return state.map((item) =>
          item.id === action.payload.id
            ? {
                ...item,
                quantity: item.quantity + 1
              }
            : item
        );

      }

      return [
        ...state,
        {
          ...action.payload,
          quantity: 1
        }
      ];
    }
  • The cartReducer function receives two things:
  1. state → The current cart data.
  2. action → Describes what the user wants to do.
  • switch (action.type) { -> Check which action was requested decides which logic should run.

ADD_ITEM                    → Add product
INCREASE_QUANTITY    → Increase quantity
REMOVE_ITEM              → Remove product

  • ADD_ITEM Logic :
  1. Check if the product already exists using its id.
  2. If it exists: increase its quantity by 1.
  3. If it doesn't exist: add the product to the cart with quantity: 1.

Create the reducer logic for Increasing item quantity

2

  case "INCREASE_QUANTITY":
      return state.map((item) =>
        item.id === action.payload
          ? {
              ...item,
              quantity: item.quantity + 1
            }
          : item
      );
  • This case runs when the user clicks the + button.
  • state contains the current cart items.
  • map() goes through each item in the cart and creates a new array.
  • item.id === action.payload checks whether the current item's ID matches the product ID sent in the action.
  • If the ID matches:
  1. ...item keeps all the existing product information.
  2. increases the quantity by 1.

Create the reducer logic for Decreasing item quantity

3

  case "DECREASE_QUANTITY":
      return state
        .map((item) =>
          item.id === action.payload
            ? {
                ...item, quantity: item.quantity - 1
              }
            : item
        )
        .filter(
          (item) => item.quantity > 0
        );
  • This case runs when the user clicks the  button.
  • The payload contains the ID of the product whose quantity should decrease.
  • map() checks every item in the cart and compares its ID with the selected product ID.
  • If the ID matches: The product's quantity is reduced by 1.

Create the reducer logic for Removing item

4

 case "REMOVE_ITEM":

      return state.filter(
        (item) => item.id !== action.payload
      );
  • This case runs when the user clicks the Delete / Remove button.
  • payload → contains the ID of the product to remove.
  • filter() checks every item in the cart and creates a new array containing only the items that satisfy the condition.
  • item.id !== action.payload This means : Keep the item only when its ID is not equal to the
    ID we want to remove.

Create the reducer logic for clear cart

5

case "CLEAR_CART":

      return [];

Create the reducer logic for clear cart

6

case "LOAD_CART":
  return action.payload;
 default:
      return state;
  }
}

Connect Cart State with Products

7

Pass dispatch to Products:

<Route path="/products" 
  element={<Products dispatch={dispatch}/>}
/>

Make Add to Cart Functional

8

<button className="add-cart-button"
  onClick={() => {
    dispatch({ type: "ADD_ITEM", payload: product });
  }} > 🛒 Add to Cart
</button>
  • Now update Product Component :
function Products({ dispatch }) {
  • Replace the existing static button inside the product card that we  generated using filteredProducts.map()

Pass cart to Cart

9

<Route path="/cart"
  element={
    <Cart
      cart={cart}
      dispatch={dispatch}
    />
  }
/>
  • Now pass cart and dispatch to Cart:
  • Receive Cart Data in Cart.jsx
function Cart({ cart, dispatch }) {

Now cart contains the products that were added through the Products page.

Task 3 :  Display Cart Dynamically

  • Currently we have:
<tbody>

  {/* Product 1 */}
  <tr className="cart-item-row"> ... </tr>
  {/* Product 2 */}
  <tr className="cart-item-row">  ... </tr>

</tbody>

These rows are static.

  • We don't want to manually write:
Product 1
Product 2
Product 3
Product 4
  • Because the user can add any number of products.

Use map() to Display Cart Products

1

<tbody>
  {cart.map((item) => (
    <tr className="cart-item-row" key={item.id}>
      <td>
        <div className="cart-product">
          <div className="cart-product-image">
            <img src={item.image} alt={item.name} />
          </div>
          <div className="cart-product-info">
            <h3>{item.name}</h3>
            <p className="cart-category">{item.category}</p>
          </div>
        </div>
      </td>

      <td>
        <div className="cart-price">
          <strong>₹{item.price.toLocaleString("en-IN")}</strong>
          <span className="original-price">₹{item.originalPrice.toLocaleString("en-IN")}
          </span>
        </div>
      </td>

      <td>
        <div className="quantity-control">
          <button type="button">−</button>
          <span>{item.quantity}</span>
          <button type="button">+</button>
        </div>
      </td>

      <td>
        <strong className="item-total">
          ₹{(item.price * item.quantity).toLocaleString("en-IN")}
        </strong>
      </td>
      <td>
        <button type="button" className="delete-btn">
          <img src="/icons/delete-icon.png" alt="Delete" />
        </button>
      </td>
    </tr>
  ))}
</tbody>

Notice what is happening

  • Instead of writing: <h3>boAt Rockerz 450</h3>
  • We now write: <h3>{item.name}</h3> which comes from the cart state.

Add Quantity controls and delete button

2

Now that products are being displayed dynamically, we will make the − and + buttons functional.

  • Inside the dynamically rendered product row we already have quantity control buttons
  • The buttons currently don't do anything.
  • Replace the Increase Quantity button with :
  • Replace the Decrease Quantity button with :
 <button type="button"
    onClick={() =>
      dispatch({ type: "INCREASE_QUANTITY", 
      payload: item.id })
    }
  >
    +
  </button>
  <button type="button"
    onClick={() =>
      dispatch({ type: "DECREASE_QUANTITY", 
      payload: item.id })
    }
  >
    −
  </button>
  • Replace the Remove product button with :
<button type="button" className="delete-btn"
  onClick={() =>
    dispatch({ type: "REMOVE_ITEM", payload: item.id})
  }
>
<img src="/icons/delete-icon.png" alt="Delete"/>
</button>
<button type="button" className="clear-cart-btn"
  onClick={() =>
    dispatch({
      type: "CLEAR_CART"
    })
  }>
  
  <img src="/icons/clear-cart.png" alt=""/>
  Clear Cart
  
</button>

Add Clear Cart

3

  • Now we will make the Clear Cart button remove all products at once.
  • Replace the existing clear cart button with

Calculate Cart Amounts

4

  • Now that the cart is dynamic, we need to calculate the values shown in the Order Summary.
  • The values can no longer remain hard-coded.
  1. Calculate Subtotal

Add this inside Cart.jsx, before the return:

const subtotal = cart.reduce(
  (total, item) =>
    total +
    Number(item.price.replace(/,/g, "")) * item.quantity,
  0
);
  • reduce() goes through every cart item and combines their values into one total.

2. Calculate Discount

 let discountPercentage = 0;

  if (subtotal >= 5000) {
    discountPercentage = 25;
  } 
else if (subtotal >= 3500) {
    discountPercentage = 20;
  } 
else if (subtotal >= 2000) {
    discountPercentage = 10;
  }

  const discount =
    subtotal * discountPercentage / 100;

3. Calculate Shipping

const shipping =
  subtotal >= 499 || subtotal === 0
    ? 0
    : 40;

4. Calculate total amount

const totalAmount = subtotal - discount + shipping;

Connect Order Summary Dynamically

5

  • Now we replace the static Order Summary values with the calculated values.
const shipping =
  subtotal >= 499 || subtotal === 0
    ? 0
    : 40;

Task 4 :  Show Order Placed Successfully Popup

Add Popup State

1

At the top of Cart.jsx, import useState.

import React, { useState } from "react";

Then, inside the component :

  const [orderPlaced, setOrderPlaced] = useState(false);
  • Initially:orderPlaced = false So the popup is not visible.

Handle Proceed to Checkout

2

<button type="button" className="checkout-btn"
  onClick={() => setOrderPlaced(true)} > Proceed to Checkout
  <span>→</span>
</button>

Add onClick event to the button

Create the Popup UI

3

{orderPlaced && (

  <div className="order-popup-overlay">
    <div className="order-popup">
      <div className="success-icon"> ✓ </div>
      <h2> Order Placed Successfully!</h2>
      <p>  Your order has been placed successfully. </p>
      <button type="button" className="popup-ok-btn" 
      onClick={() => setOrderPlaced(false)} > OK
      </button>

    </div>
  </div>
)}
  • Add this inside the main return, preferably near the end of .cart-page, before its closing </div>.

Add Popup CSS

4

Add this to your existing Cart.css:

Task 5 :  Introduce Empty Cart UI

Add Conditional Rendering

1

{cart.length === 0 ? (
  <div className="empty-cart">
        <div className="empty-cart-icon">
      🛒
    </div>

    <h2> Your Cart is Empty </h2>

    <p> Add some products to your cart
      to continue shopping.
    </p>

    <Link to="/products" className="continue-shopping-btn">
      Continue Shopping
    </Link>

  </div>
) : (
  <>
    {/* Cart Table */}
    {/* Order Summary */}
  </>
)}

Style the Account Section

2

Connect Cart Count to Navbar

3

  • Update Navbar component to
function Navbar({userName, cart =[]}) {
  • Then inside the component ,calculate:
const cartCount = cart.reduce(
  (total, item) =>
    total + item.quantity,
  0
);
  • Then replace the cart count span with :
<span className="cart-count">
  {cartCount}
</span>

Pass Cart State to Navbar

4

  • In App.jsx, pass the cart state to the Navbar component:
<Navbar
  userName={userName}
  cart={cart}
/>

Task 6 :  Make Cart data Persistent

  • Now We will use localstorage for our cart
  • Without LocalStorage, the cart becomes empty when the browser is refreshed,
  • With LocalStorage, the cart data is saved and automatically restored after the browser is refreshed.

Create the cartLoaded State

1

  • Add the following after the cart reducer:

const [cartLoaded, setCartLoaded] =  useState(false);
  • cartLoaded keeps track of whether the saved cart has been restored from LocalStorage.
  • cartLoaded = false

This means: "The cart has not been loaded from LocalStorage yet."

  • After the restore operation is completed: setCartLoaded(true);
    This means:"The cart has been restored, so it is safe to save cart changes."

Use cartLoaded During Cart Restoration

2

useEffect(() => {
  const savedCart = localStorage.getItem("cart");
  if (savedCart) {
    dispatch({ type: "LOAD_CART", payload: JSON.parse(savedCart)
    });
      }
  setCartLoaded(true);
}, []);

Use cartLoaded Before Saving

3

useEffect(() => {

  if (!cartLoaded) {
    
    return;
    
  }

  localStorage.setItem(
    "cart",
    JSON.stringify(cart)
  );

  
}, [cart, cartLoaded]);
  • The cart should only be saved after the initial loading process is complete:

We are done with this lab. The latest source code has been uploaded to GitHub. You can access the latest commit using the link below: 

 

Great job!

Your ShopKart Shopping Cart & LocalStorage is now working smoothly with product management, quantity controls, order calculations, cart persistence, and checkout confirmation.

Checkpoint

   Git Push

git push origin branchName

Next-Lab Preparation

Module:

1) Handling Side-Effects

2) React Context & Reducers

React lab 6

By Content ITV

React lab 6

  • 112