🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Build a complete e-commerce shopping cart
- Implement product catalog and search functionality
- Handle shopping cart state management
- Process checkout and payment (simulated)
- Manage user accounts and order history
- Create responsive e-commerce interface
📚 Project Overview
We'll build an e-commerce application that includes: - Product catalog with categories and search - Shopping cart with add/remove/update functionality - User authentication and account management - Checkout process with form validation - Order management and history - Payment integration (simulated)
🏗️ Project Structure
ecommerce-app/
├── package.json
├── src/
│ ├── App.js
│ ├── components/
│ │ ├── Header.js
│ │ ├── ProductCard.js
│ │ ├── ProductList.js
│ │ ├── ShoppingCart.js
│ │ ├── Checkout.js
│ │ └── OrderHistory.js
│ ├── context/
│ │ ├── CartContext.js
│ │ └── AuthContext.js
│ ├── hooks/
│ │ ├── useCart.js
│ │ └── useAuth.js
│ └── data/
│ └── products.js
└── styles/
└── App.css
💻 Complete Implementation
context/CartContext.js
import React, { createContext, useContext, useReducer } from 'react';
const CartContext = createContext();
const cartReducer = (state, action) => {
switch (action.type) {
case 'ADD_TO_CART':
const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) {
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
)
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }]
};
case 'REMOVE_FROM_CART':
return {
...state,
items: state.items.filter(item => item.id !== action.payload)
};
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
).filter(item => item.quantity > 0)
};
case 'CLEAR_CART':
return {
...state,
items: []
};
default:
return state;
}
};
export function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, {
items: [],
isOpen: false
});
const addToCart = (product) => {
dispatch({ type: 'ADD_TO_CART', payload: product });
};
const removeFromCart = (productId) => {
dispatch({ type: 'REMOVE_FROM_CART', payload: productId });
};
const updateQuantity = (productId, quantity) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id: productId, quantity } });
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
const toggleCart = () => {
dispatch({ type: 'TOGGLE_CART' });
};
const getTotalPrice = () => {
return state.items.reduce((total, item) => total + (item.price * item.quantity), 0);
};
const getTotalItems = () => {
return state.items.reduce((total, item) => total + item.quantity, 0);
};
const value = {
...state,
addToCart,
removeFromCart,
updateQuantity,
clearCart,
toggleCart,
getTotalPrice,
getTotalItems
};
return (
<CartContext.Provider value={value}>
{children}
</CartContext.Provider>
);
}
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
}
components/ProductList.js
import React, { useState, useEffect } from 'react';
import ProductCard from './ProductCard';
import { products } from '../data/products';
function ProductList() {
const [filteredProducts, setFilteredProducts] = useState(products);
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('all');
const [sortBy, setSortBy] = useState('name');
const categories = ['all', ...new Set(products.map(product => product.category))];
useEffect(() => {
let filtered = products;
// Filter by search term
if (searchTerm) {
filtered = filtered.filter(product =>
product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.description.toLowerCase().includes(searchTerm.toLowerCase())
);
}
// Filter by category
if (selectedCategory !== 'all') {
filtered = filtered.filter(product => product.category === selectedCategory);
}
// Sort products
filtered.sort((a, b) => {
switch (sortBy) {
case 'name':
return a.name.localeCompare(b.name);
case 'price-low':
return a.price - b.price;
case 'price-high':
return b.price - a.price;
case 'rating':
return b.rating - a.rating;
default:
return 0;
}
});
setFilteredProducts(filtered);
}, [searchTerm, selectedCategory, sortBy]);
return (
<div className="product-list">
<div className="product-filters">
<div className="search-bar">
<input
type="text"
placeholder="Search products..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="filter-controls">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
>
{categories.map(category => (
<option key={category} value={category}>
{category.charAt(0).toUpperCase() + category.slice(1)}
</option>
))}
</select>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value="name">Sort by Name</option>
<option value="price-low">Price: Low to High</option>
<option value="price-high">Price: High to Low</option>
<option value="rating">Sort by Rating</option>
</select>
</div>
</div>
<div className="products-grid">
{filteredProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
{filteredProducts.length === 0 && (
<div className="no-products">
<p>No products found matching your criteria.</p>
</div>
)}
</div>
);
}
export default ProductList;
components/ShoppingCart.js
import React from 'react';
import { useCart } from '../context/CartContext';
function ShoppingCart() {
const {
items,
isOpen,
toggleCart,
removeFromCart,
updateQuantity,
getTotalPrice,
getTotalItems
} = useCart();
if (!isOpen) return null;
return (
<div className="shopping-cart-overlay" onClick={toggleCart}>
<div className="shopping-cart" onClick={(e) => e.stopPropagation()}>
<div className="cart-header">
<h3>Shopping Cart ({getTotalItems()} items)</h3>
<button className="close-btn" onClick={toggleCart}>×</button>
</div>
<div className="cart-items">
{items.length === 0 ? (
<div className="empty-cart">
<p>Your cart is empty</p>
<button onClick={toggleCart}>Continue Shopping</button>
</div>
) : (
items.map(item => (
<div key={item.id} className="cart-item">
<div className="item-image">
<img src={item.image} alt={item.name} />
</div>
<div className="item-details">
<h4>{item.name}</h4>
<p className="item-price">${item.price}</p>
<div className="quantity-controls">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
disabled={item.quantity <= 1}
>
-
</button>
<span>{item.quantity}</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
>
+
</button>
</div>
</div>
<div className="item-actions">
<button
className="remove-btn"
onClick={() => removeFromCart(item.id)}
>
Remove
</button>
</div>
</div>
))
)}
</div>
{items.length > 0 && (
<div className="cart-footer">
<div className="cart-total">
<h4>Total: ${getTotalPrice().toFixed(2)}</h4>
</div>
<div className="cart-actions">
<button className="btn btn-secondary" onClick={toggleCart}>
Continue Shopping
</button>
<button className="btn btn-primary">
Proceed to Checkout
</button>
</div>
</div>
)}
</div>
</div>
);
}
export default ShoppingCart;
components/Checkout.js
import React, { useState } from 'react';
import { useCart } from '../context/CartContext';
import { useAuth } from '../context/AuthContext';
function Checkout() {
const { items, getTotalPrice, clearCart } = useCart();
const { user } = useAuth();
const [formData, setFormData] = useState({
firstName: user?.firstName || '',
lastName: user?.lastName || '',
email: user?.email || '',
address: '',
city: '',
state: '',
zipCode: '',
paymentMethod: 'credit-card',
cardNumber: '',
expiryDate: '',
cvv: ''
});
const [isProcessing, setIsProcessing] = useState(false);
const [orderComplete, setOrderComplete] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
setIsProcessing(true);
// Simulate payment processing
await new Promise(resolve => setTimeout(resolve, 2000));
// Create order
const order = {
id: Date.now(),
items: items,
total: getTotalPrice(),
customer: formData,
status: 'confirmed',
date: new Date().toISOString()
};
// Save order to localStorage (simulate database)
const orders = JSON.parse(localStorage.getItem('orders') || '[]');
orders.push(order);
localStorage.setItem('orders', JSON.stringify(orders));
setIsProcessing(false);
setOrderComplete(true);
clearCart();
};
if (orderComplete) {
return (
<div className="checkout-success">
<h2>Order Confirmed!</h2>
<p>Thank you for your purchase. Your order has been confirmed.</p>
<button className="btn btn-primary" onClick={() => window.location.href = '/'}>
Continue Shopping
</button>
</div>
);
}
return (
<div className="checkout">
<h2>Checkout</h2>
<div className="checkout-content">
<div className="order-summary">
<h3>Order Summary</h3>
{items.map(item => (
<div key={item.id} className="order-item">
<span>{item.name} x {item.quantity}</span>
<span>${(item.price * item.quantity).toFixed(2)}</span>
</div>
))}
<div className="order-total">
<strong>Total: ${getTotalPrice().toFixed(2)}</strong>
</div>
</div>
<form onSubmit={handleSubmit} className="checkout-form">
<div className="form-section">
<h3>Shipping Information</h3>
<div className="form-row">
<input
type="text"
name="firstName"
placeholder="First Name"
value={formData.firstName}
onChange={handleChange}
required
/>
<input
type="text"
name="lastName"
placeholder="Last Name"
value={formData.lastName}
onChange={handleChange}
required
/>
</div>
<input
type="email"
name="email"
placeholder="Email"
value={formData.email}
onChange={handleChange}
required
/>
<input
type="text"
name="address"
placeholder="Address"
value={formData.address}
onChange={handleChange}
required
/>
<div className="form-row">
<input
type="text"
name="city"
placeholder="City"
value={formData.city}
onChange={handleChange}
required
/>
<input
type="text"
name="state"
placeholder="State"
value={formData.state}
onChange={handleChange}
required
/>
<input
type="text"
name="zipCode"
placeholder="ZIP Code"
value={formData.zipCode}
onChange={handleChange}
required
/>
</div>
</div>
<div className="form-section">
<h3>Payment Information</h3>
<select
name="paymentMethod"
value={formData.paymentMethod}
onChange={handleChange}
>
<option value="credit-card">Credit Card</option>
<option value="debit-card">Debit Card</option>
<option value="paypal">PayPal</option>
</select>
<input
type="text"
name="cardNumber"
placeholder="Card Number"
value={formData.cardNumber}
onChange={handleChange}
required
/>
<div className="form-row">
<input
type="text"
name="expiryDate"
placeholder="MM/YY"
value={formData.expiryDate}
onChange={handleChange}
required
/>
<input
type="text"
name="cvv"
placeholder="CVV"
value={formData.cvv}
onChange={handleChange}
required
/>
</div>
</div>
<button
type="submit"
className="btn btn-primary"
disabled={isProcessing}
>
{isProcessing ? 'Processing...' : 'Complete Order'}
</button>
</form>
</div>
</div>
);
}
export default Checkout;
🎯 Key Features Implemented
- Product Catalog - Search, filter, and sort products
- Shopping Cart - Add, remove, and update quantities
- User Authentication - Login and account management
- Checkout Process - Form validation and order processing
- Order Management - Order history and status tracking
- Payment Integration - Simulated payment processing
- Responsive Design - Mobile and desktop support
🎯 Key Takeaways
- State management handles complex cart operations
- Context API provides global state access
- Form validation ensures data integrity
- Payment processing simulates real-world transactions
- Order management tracks customer purchases
🚀 Next Steps
Congratulations! You've built a complete e-commerce application. In the next lesson, we'll learn about:
- Performance Optimization - Speed up applications
- Code splitting - Load only what's needed
- Caching strategies - Reduce server requests
- Image optimization - Faster loading times
Next Lesson: Lesson 9: Performance Optimization