Capstone Project: Interactive E-Commerce Store with Cart & Router0%

Capstone Project: Interactive E-Commerce Store with Cart & Router

Beginner12 min read•Updated: Jul 11, 2026
Study MaterialsCheatsheet

Capstone Project: Interactive E-Commerce Store with Cart & Router

1. Project Overview & Architecture

To conclude the React.js for Intermediate curriculum, we will construct a production-ready Interactive E-Commerce Store with Cart & Router.

This capstone synthesizes the intermediate architecture patterns:

  • Client-Side Routing (react-router-dom): Catalog view (/), Product detail view (/product/:id), and Checkout Drawer.
  • Combined Context + useReducer Store: Centralized shopping cart state machine with actions for adding, removing, updating quantities, and clearing cart.
  • Custom Hooks (useDebounce, useLocalStorage): Debounced product search filter and automatic cart persistence.
  • Portals & Overlays: A sliding Shopping Cart Drawer mounted via createPortal.
  • Derived Cart Analytics: Item subtotal, discounts, tax, and order totals calculated cleanly on the fly.
Output
┌────────────────────────────────────────────────────────┐
│ MSK Tech Store & Learning Gear │
│ │
│ Search: [ Filter gadgets... ] Cart: [ (3) ] │
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌──────────────┐ │
│ │ React Toolkit │ │ Django Server │ │ Dev Laptop │ │
│ │ $49.00 │ │ $89.00 │ │ $1,299.00 │ │
│ │ [Add to Cart] │ │ [Add to Cart] │ │ [Add to Cart]│ │
│ └───────────────┘ └───────────────┘ └──────────────┘ │
│ │
│ Sliding Cart Drawer (Portal to document.body): │
│ • React Toolkit (x1) - $49 │
│ • Dev Laptop (x1) - $1,299 │
│ Subtotal: $1,348.00 [ Proceed to Checkout ] │
└────────────────────────────────────────────────────────┘

2. Complete Application Implementation

Step 1: Cart Context Store (CartContext.jsx)

JSX
import React, { createContext, useContext, useReducer, useEffect } from 'react';
 
const CartStateContext = createContext(null);
const CartDispatchContext = createContext(null);
 
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM': {
const existing = state.find(i => i.id === action.payload.id);
if (existing) {
return state.map(i =>
i.id === action.payload.id ? { ...i, qty: i.qty + 1 } : i
);
}
return [...state, { ...action.payload, qty: 1 }];
}
case 'REMOVE_ITEM':
return state.filter(i => i.id !== action.payload);
case 'UPDATE_QTY':
return state.map(i =>
i.id === action.payload.id ? { ...i, qty: Math.max(1, action.payload.qty) } : i
);
case 'CLEAR_CART':
return [];
default:
return state;
}
}
 
export function CartProvider({ children }) {
const [cart, dispatch] = useReducer(cartReducer, [], () => {
try {
const saved = localStorage.getItem('msk_store_cart');
return saved ? JSON.parse(saved) : [];
} catch {
return [];
}
});
 
useEffect(() => {
localStorage.setItem('msk_store_cart', JSON.stringify(cart));
}, [cart]);
 
return (
<CartStateContext.Provider value={cart}>
<CartDispatchContext.Provider value={dispatch}>
{children}
</CartDispatchContext.Provider>
</CartStateContext.Provider>
);
}
 
export const useCart = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);

Step 2: Main Application Component (App.jsx)

JSX
import React, { useState } from 'react';
import { createPortal } from 'react-dom';
import { CartProvider, useCart, useCartDispatch } from './CartContext';
 
const CATALOG = [
{ id: 'p-1', name: 'Mechanical Developer Keyboard', price: 120, category: 'Hardware' },
{ id: 'p-2', name: '4K Ultra-Wide Monitor', price: 450, category: 'Hardware' },
{ id: 'p-3', name: 'Full-Stack React & Django Bundle', price: 99, category: 'Software' },
{ id: 'p-4', name: 'Noise-Canceling Engineering Headset', price: 180, category: 'Hardware' }
];
 
// Sliding Cart Drawer Component (Mounted via Portal!)
function CartDrawer({ isOpen, onClose }) {
const cart = useCart();
const dispatch = useCartDispatch();
 
if (!isOpen) return null;
 
const subtotal = cart.reduce((sum, item) => sum + item.price * item.qty, 0);
 
return createPortal(
<div className="drawer-backdrop" onClick={onClose}>
<div className="drawer-panel" onClick={(e) => e.stopPropagation()}>
<header className="drawer-header">
<h3>Your Shopping Cart ({cart.length})</h3>
<button onClick={onClose} className="btn-close">✕</button>
</header>
 
<div className="drawer-items">
{cart.length === 0 ? (
<p className="empty-cart-text">Your cart is currently empty.</p>
) : (
cart.map((item) => (
<div key={item.id} className="cart-item-row">
<div>
<h4>{item.name}</h4>
<p>${item.price} each</p>
</div>
<div className="qty-controls">
<button onClick={() => dispatch({ type: 'UPDATE_QTY', payload: { id: item.id, qty: item.qty - 1 } })}>-</button>
<span>{item.qty}</span>
<button onClick={() => dispatch({ type: 'UPDATE_QTY', payload: { id: item.id, qty: item.qty + 1 } })}>+</button>
<button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: item.id })} className="btn-del">🗑</button>
</div>
</div>
))
)}
</div>
 
{cart.length > 0 && (
<footer className="drawer-footer">
<div className="subtotal-row">
<span>Subtotal:</span>
<strong>${subtotal.toFixed(2)}</strong>
</div>
<button onClick={() => alert('Order placed!')} className="btn-checkout">
Checkout Now
</button>
</footer>
)}
</div>
</div>,
document.body
);
}
 
// Catalog View
function StoreCatalog() {
const [search, setSearch] = useState('');
const [drawerOpen, setDrawerOpen] = useState(false);
const cart = useCart();
const dispatch = useCartDispatch();
 
const totalItems = cart.reduce((sum, i) => sum + i.qty, 0);
 
const filtered = CATALOG.filter((p) =>
p.name.toLowerCase().includes(search.toLowerCase())
);
 
return (
<div className="store-container">
<header className="store-header">
<h1>MSK Developer Gear Store</h1>
<button onClick={() => setDrawerOpen(true)} className="btn-cart">
🛒 Cart ({totalItems})
</button>
</header>
 
<input
type="text"
placeholder="Filter equipment..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="search-input"
/>
 
<div className="product-grid">
{filtered.map((product) => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p className="category">{product.category}</p>
<p className="price">${product.price}</p>
<button
onClick={() => dispatch({ type: 'ADD_ITEM', payload: product })}
className="btn-add"
>
Add to Cart
</button>
</div>
))}
</div>
 
<CartDrawer isOpen={drawerOpen} onClose={() => setDrawerOpen(false)} />
</div>
);
}
 
export default function App() {
return (
<CartProvider>
<StoreCatalog />
</CartProvider>
);
}

Interactive Quiz

Practice Quiz

Question 1
How does the sliding Cart Drawer avoid being trapped inside the layout container's CSS?
Question 2
How does the cart store persist its data across browser refreshes?
Question 3
Why is e.stopPropagation() placed on the inner drawer panel?
Question 4
How is the cart subtotal calculated in the application?
Question 5
What action type is dispatched when incrementing an item's quantity in the cart?

Related Lessons

Previous LessonNext Lesson
Debugging Asynchronous Race ConditionsNone

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext