E-commerce Frontend
Learning Objectives
- By the end of this project, you will be able to:
- - Manage complex application state
- - Implement shopping cart functionality
- - Build product catalog with filtering
- - Use Context API for global state
- - Handle user interactions
- - Create responsive e-commerce UI
- - Implement cart persistence
Project 2.3: E-commerce Frontend
Project Overview
Build a complete E-commerce Frontend application with shopping cart, product catalog, and complex state management. This project will help you practice advanced React concepts, state management patterns, and building complex user interfaces.
Learning Objectives
By the end of this project, you will be able to:
- Manage complex application state
- Implement shopping cart functionality
- Build product catalog with filtering
- Use Context API for global state
- Handle user interactions
- Create responsive e-commerce UI
- Implement cart persistence
Project Requirements
Core Features
- Product Catalog: Display products with images, prices, descriptions
- Product Details: View individual product information
- Shopping Cart: Add/remove items, update quantities
- Cart Persistence: Save cart to local storage
- Filter Products: Filter by category, price, search
- Cart Summary: Show total, item count
- Checkout Page: Review order before checkout
- Responsive Design: Works on all devices
Technical Requirements
- Use React with Context API
- Complex state management
- Local storage integration
- Responsive design
- Clean component architecture
- Error handling
Project Setup
# Create React app
npx create-react-app ecommerce-frontend
cd ecommerce-frontend
# Install dependencies
npm install react-router-dom
# Start development server
npm start
Project Structure
ecommerce-frontend/
├── public/
│ └── index.html
├── src/
│ ├── components/
│ │ ├── Layout.jsx
│ │ ├── Navigation.jsx
│ │ ├── ProductCard.jsx
│ │ ├── ProductList.jsx
│ │ ├── ProductDetail.jsx
│ │ ├── Cart.jsx
│ │ ├── CartItem.jsx
│ │ ├── FilterBar.jsx
│ │ └── Checkout.jsx
│ ├── context/
│ │ ├── CartContext.jsx
│ │ └── ProductContext.jsx
│ ├── pages/
│ │ ├── Home.jsx
│ │ ├── Products.jsx
│ │ ├── ProductDetailPage.jsx
│ │ └── CheckoutPage.jsx
│ ├── data/
│ │ └── products.js
│ ├── App.jsx
│ ├── App.css
│ └── index.js
└── package.json
Step-by-Step Implementation
Step 1: Products Data
// src/data/products.js
export const products = [
{
id: 1,
name: 'Wireless Headphones',
price: 99.99,
image: 'https://via.placeholder.com/300',
category: 'Electronics',
description: 'High-quality wireless headphones with noise cancellation.',
inStock: true
},
{
id: 2,
name: 'Smart Watch',
price: 199.99,
image: 'https://via.placeholder.com/300',
category: 'Electronics',
description: 'Feature-rich smartwatch with fitness tracking.',
inStock: true
},
{
id: 3,
name: 'Laptop Backpack',
price: 49.99,
image: 'https://via.placeholder.com/300',
category: 'Accessories',
description: 'Durable laptop backpack with multiple compartments.',
inStock: true
},
{
id: 4,
name: 'USB-C Cable',
price: 19.99,
image: 'https://via.placeholder.com/300',
category: 'Accessories',
description: 'Fast charging USB-C cable, 6 feet long.',
inStock: true
},
{
id: 5,
name: 'Wireless Mouse',
price: 29.99,
image: 'https://via.placeholder.com/300',
category: 'Electronics',
description: 'Ergonomic wireless mouse with long battery life.',
inStock: true
},
{
id: 6,
name: 'Mechanical Keyboard',
price: 129.99,
image: 'https://via.placeholder.com/300',
category: 'Electronics',
description: 'RGB mechanical keyboard with customizable keys.',
inStock: false
}
];
Step 2: Cart Context
// src/context/CartContext.jsx
import { createContext, useContext, useReducer, useEffect } from 'react';
const CartContext = createContext();
const cartReducer = (state, action) => {
switch (action.type) {
case 'LOAD_CART':
return action.payload;
case 'ADD_ITEM':
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_ITEM':
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
)
};
case 'CLEAR_CART':
return { items: [] };
default:
return state;
}
};
const initialState = { items: [] };
export function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, initialState);
// Load cart from localStorage
useEffect(() => {
const savedCart = localStorage.getItem('cart');
if (savedCart) {
dispatch({ type: 'LOAD_CART', payload: JSON.parse(savedCart) });
}
}, []);
// Save cart to localStorage
useEffect(() => {
localStorage.setItem('cart', JSON.stringify(state));
}, [state]);
const addItem = (product) => {
dispatch({ type: 'ADD_ITEM', payload: product });
};
const removeItem = (id) => {
dispatch({ type: 'REMOVE_ITEM', payload: id });
};
const updateQuantity = (id, quantity) => {
if (quantity <= 0) {
removeItem(id);
} else {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
}
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
const getTotal = () => {
return state.items.reduce((total, item) => {
return total + (item.price * item.quantity);
}, 0);
};
const getItemCount = () => {
return state.items.reduce((count, item) => count + item.quantity, 0);
};
return (
<CartContext.Provider
value={{
items: state.items,
addItem,
removeItem,
updateQuantity,
clearCart,
getTotal,
getItemCount
}}
>
{children}
</CartContext.Provider>
);
}
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within CartProvider');
}
return context;
}
Step 3: Product Context
// src/context/ProductContext.jsx
import { createContext, useContext, useState, useMemo } from 'react';
import { products } from '../data/products';
const ProductContext = createContext();
export function ProductProvider({ children }) {
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('All');
const [priceRange, setPriceRange] = useState([0, 1000]);
const categories = useMemo(() => {
const cats = ['All', ...new Set(products.map(p => p.category))];
return cats;
}, []);
const filteredProducts = useMemo(() => {
return products.filter(product => {
const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.description.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory = selectedCategory === 'All' || product.category === selectedCategory;
const matchesPrice = product.price >= priceRange[0] && product.price <= priceRange[1];
return matchesSearch && matchesCategory && matchesPrice;
});
}, [searchTerm, selectedCategory, priceRange]);
return (
<ProductContext.Provider
value={{
products: filteredProducts,
allProducts: products,
categories,
searchTerm,
setSearchTerm,
selectedCategory,
setSelectedCategory,
priceRange,
setPriceRange
}}
>
{children}
</ProductContext.Provider>
);
}
export function useProducts() {
const context = useContext(ProductContext);
if (!context) {
throw new Error('useProducts must be used within ProductProvider');
}
return context;
}
Step 4: Product Card Component
// src/components/ProductCard.jsx
import { Link } from 'react-router-dom';
import { useCart } from '../context/CartContext';
import './ProductCard.css';
function ProductCard({ product }) {
const { addItem } = useCart();
const handleAddToCart = (e) => {
e.preventDefault();
addItem(product);
};
return (
<div className="product-card">
<Link to={`/products/${product.id}`} className="product-link">
<div className="product-image-container">
<img src={product.image} alt={product.name} className="product-image" />
{!product.inStock && (
<div className="out-of-stock">Out of Stock</div>
)}
</div>
<div className="product-info">
<h3 className="product-name">{product.name}</h3>
<p className="product-category">{product.category}</p>
<p className="product-price">${product.price.toFixed(2)}</p>
</div>
</Link>
<button
className="add-to-cart-btn"
onClick={handleAddToCart}
disabled={!product.inStock}
>
{product.inStock ? 'Add to Cart' : 'Out of Stock'}
</button>
</div>
);
}
export default ProductCard;
Step 5: Product List Component
// src/components/ProductList.jsx
import ProductCard from './ProductCard';
import './ProductList.css';
function ProductList({ products }) {
if (products.length === 0) {
return (
<div className="no-products">
<p>No products found. Try adjusting your filters.</p>
</div>
);
}
return (
<div className="product-list">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
export default ProductList;
Step 6: Filter Bar Component
// src/components/FilterBar.jsx
import { useProducts } from '../context/ProductContext';
import './FilterBar.css';
function FilterBar() {
const {
searchTerm,
setSearchTerm,
selectedCategory,
setSelectedCategory,
categories,
priceRange,
setPriceRange
} = useProducts();
return (
<div className="filter-bar">
<div className="filter-group">
<label>Search</label>
<input
type="text"
placeholder="Search products..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
</div>
<div className="filter-group">
<label>Category</label>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="category-select"
>
{categories.map(category => (
<option key={category} value={category}>
{category}
</option>
))}
</select>
</div>
<div className="filter-group">
<label>Price Range: ${priceRange[0]} - ${priceRange[1]}</label>
<input
type="range"
min="0"
max="1000"
value={priceRange[1]}
onChange={(e) => setPriceRange([priceRange[0], parseInt(e.target.value)])}
className="price-range"
/>
</div>
</div>
);
}
export default FilterBar;
Step 7: Cart Item Component
// src/components/CartItem.jsx
import { useCart } from '../context/CartContext';
import './CartItem.css';
function CartItem({ item }) {
const { removeItem, updateQuantity } = useCart();
return (
<div className="cart-item">
<img src={item.image} alt={item.name} className="cart-item-image" />
<div className="cart-item-info">
<h4>{item.name}</h4>
<p className="cart-item-price">${item.price.toFixed(2)}</p>
</div>
<div className="cart-item-actions">
<div className="quantity-controls">
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}>
−
</button>
<span>{item.quantity}</span>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>
+
</button>
</div>
<button
className="remove-btn"
onClick={() => removeItem(item.id)}
>
Remove
</button>
</div>
<div className="cart-item-total">
${(item.price * item.quantity).toFixed(2)}
</div>
</div>
);
}
export default CartItem;
Step 8: Cart Component
// src/components/Cart.jsx
import { useCart } from '../context/CartContext';
import CartItem from './CartItem';
import { Link } from 'react-router-dom';
import './Cart.css';
function Cart() {
const { items, getTotal, clearCart } = useCart();
if (items.length === 0) {
return (
<div className="cart-empty">
<p>Your cart is empty</p>
<Link to="/products" className="btn btn-primary">
Continue Shopping
</Link>
</div>
);
}
return (
<div className="cart">
<div className="cart-header">
<h2>Shopping Cart</h2>
<button onClick={clearCart} className="clear-cart-btn">
Clear Cart
</button>
</div>
<div className="cart-items">
{items.map(item => (
<CartItem key={item.id} item={item} />
))}
</div>
<div className="cart-summary">
<div className="cart-total">
<h3>Total: ${getTotal().toFixed(2)}</h3>
</div>
<Link to="/checkout" className="btn btn-checkout">
Proceed to Checkout
</Link>
</div>
</div>
);
}
export default Cart;
Step 9: Checkout Component
// src/components/Checkout.jsx
import { useState } from 'react';
import { useCart } from '../context/CartContext';
import { useNavigate } from 'react-router-dom';
import './Checkout.css';
function Checkout() {
const { items, getTotal, clearCart } = useCart();
const navigate = useNavigate();
const [formData, setFormData] = useState({
name: '',
email: '',
address: '',
city: '',
zip: '',
cardNumber: '',
expiryDate: '',
cvv: ''
});
const [errors, setErrors] = useState({});
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const validate = () => {
const newErrors = {};
if (!formData.name.trim()) newErrors.name = 'Name is required';
if (!formData.email.trim()) newErrors.email = 'Email is required';
if (!formData.address.trim()) newErrors.address = 'Address is required';
if (!formData.city.trim()) newErrors.city = 'City is required';
if (!formData.zip.trim()) newErrors.zip = 'ZIP code is required';
if (!formData.cardNumber.trim()) newErrors.cardNumber = 'Card number is required';
if (!formData.expiryDate.trim()) newErrors.expiryDate = 'Expiry date is required';
if (!formData.cvv.trim()) newErrors.cvv = 'CVV is required';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate()) {
// In a real app, this would send data to backend
alert('Order placed successfully!');
clearCart();
navigate('/');
}
};
return (
<div className="checkout">
<h1>Checkout</h1>
<div className="checkout-content">
<div className="checkout-form-container">
<form className="checkout-form" onSubmit={handleSubmit}>
<h2>Shipping Information</h2>
<div className="form-group">
<label>Full Name</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className={errors.name ? 'error' : ''}
/>
{errors.name && <span className="error-message">{errors.name}</span>}
</div>
<div className="form-group">
<label>Email</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>
<div className="form-group">
<label>Address</label>
<input
type="text"
name="address"
value={formData.address}
onChange={handleChange}
className={errors.address ? 'error' : ''}
/>
{errors.address && <span className="error-message">{errors.address}</span>}
</div>
<div className="form-row">
<div className="form-group">
<label>City</label>
<input
type="text"
name="city"
value={formData.city}
onChange={handleChange}
className={errors.city ? 'error' : ''}
/>
{errors.city && <span className="error-message">{errors.city}</span>}
</div>
<div className="form-group">
<label>ZIP Code</label>
<input
type="text"
name="zip"
value={formData.zip}
onChange={handleChange}
className={errors.zip ? 'error' : ''}
/>
{errors.zip && <span className="error-message">{errors.zip}</span>}
</div>
</div>
<h2>Payment Information</h2>
<div className="form-group">
<label>Card Number</label>
<input
type="text"
name="cardNumber"
value={formData.cardNumber}
onChange={handleChange}
placeholder="1234 5678 9012 3456"
className={errors.cardNumber ? 'error' : ''}
/>
{errors.cardNumber && <span className="error-message">{errors.cardNumber}</span>}
</div>
<div className="form-row">
<div className="form-group">
<label>Expiry Date</label>
<input
type="text"
name="expiryDate"
value={formData.expiryDate}
onChange={handleChange}
placeholder="MM/YY"
className={errors.expiryDate ? 'error' : ''}
/>
{errors.expiryDate && <span className="error-message">{errors.expiryDate}</span>}
</div>
<div className="form-group">
<label>CVV</label>
<input
type="text"
name="cvv"
value={formData.cvv}
onChange={handleChange}
placeholder="123"
className={errors.cvv ? 'error' : ''}
/>
{errors.cvv && <span className="error-message">{errors.cvv}</span>}
</div>
</div>
<button type="submit" className="btn btn-submit">
Place Order
</button>
</form>
</div>
<div className="order-summary">
<h2>Order Summary</h2>
<div className="summary-items">
{items.map(item => (
<div key={item.id} className="summary-item">
<span>{item.name} x{item.quantity}</span>
<span>${(item.price * item.quantity).toFixed(2)}</span>
</div>
))}
</div>
<div className="summary-total">
<h3>Total: ${getTotal().toFixed(2)}</h3>
</div>
</div>
</div>
</div>
);
}
export default Checkout;
Step 10: Navigation Component
// src/components/Navigation.jsx
import { Link } from 'react-router-dom';
import { useCart } from '../context/CartContext';
import './Navigation.css';
function Navigation() {
const { getItemCount } = useCart();
const itemCount = getItemCount();
return (
<nav className="navigation">
<div className="nav-container">
<Link to="/" className="nav-logo">
E-Commerce
</Link>
<div className="nav-links">
<Link to="/" className="nav-link">Home</Link>
<Link to="/products" className="nav-link">Products</Link>
<Link to="/cart" className="nav-link cart-link">
Cart
{itemCount > 0 && <span className="cart-badge">{itemCount}</span>}
</Link>
</div>
</div>
</nav>
);
}
export default Navigation;
Step 11: Pages
// src/pages/Home.jsx
import { Link } from 'react-router-dom';
import './Home.css';
function Home() {
return (
<div className="home">
<div className="hero">
<h1>Welcome to Our Store</h1>
<p>Discover amazing products at great prices</p>
<Link to="/products" className="btn btn-primary">
Shop Now
</Link>
</div>
</div>
);
}
export default Home;
// src/pages/Products.jsx
import { useProducts } from '../context/ProductContext';
import FilterBar from '../components/FilterBar';
import ProductList from '../components/ProductList';
import './Products.css';
function Products() {
const { products } = useProducts();
return (
<div className="products-page">
<h1>Products</h1>
<FilterBar />
<ProductList products={products} />
</div>
);
}
export default Products;
// src/pages/ProductDetailPage.jsx
import { useParams, useNavigate } from 'react-router-dom';
import { useProducts } from '../context/ProductContext';
import { useCart } from '../context/CartContext';
import './ProductDetailPage.css';
function ProductDetailPage() {
const { id } = useParams();
const navigate = useNavigate();
const { allProducts } = useProducts();
const { addItem } = useCart();
const product = allProducts.find(p => p.id === parseInt(id));
if (!product) {
return <div>Product not found</div>;
}
const handleAddToCart = () => {
addItem(product);
navigate('/cart');
};
return (
<div className="product-detail-page">
<button onClick={() => navigate(-1)} className="back-btn">
← Back
</button>
<div className="product-detail">
<div className="product-detail-image">
<img src={product.image} alt={product.name} />
</div>
<div className="product-detail-info">
<h1>{product.name}</h1>
<p className="product-category">{product.category}</p>
<p className="product-price">${product.price.toFixed(2)}</p>
<p className="product-description">{product.description}</p>
<div className="product-status">
{product.inStock ? (
<span className="in-stock">In Stock</span>
) : (
<span className="out-of-stock">Out of Stock</span>
)}
</div>
<button
className="btn btn-primary add-to-cart-detail"
onClick={handleAddToCart}
disabled={!product.inStock}
>
{product.inStock ? 'Add to Cart' : 'Out of Stock'}
</button>
</div>
</div>
</div>
);
}
export default ProductDetailPage;
// src/pages/CheckoutPage.jsx
import Checkout from '../components/Checkout';
import './CheckoutPage.css';
function CheckoutPage() {
return (
<div className="checkout-page">
<Checkout />
</div>
);
}
export default CheckoutPage;
Step 12: App Component
// src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { CartProvider } from './context/CartContext';
import { ProductProvider } from './context/ProductContext';
import Layout from './components/Layout';
import Home from './pages/Home';
import Products from './pages/Products';
import ProductDetailPage from './pages/ProductDetailPage';
import Cart from './components/Cart';
import CheckoutPage from './pages/CheckoutPage';
import './App.css';
function App() {
return (
<CartProvider>
<ProductProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="products" element={<Products />} />
<Route path="products/:id" element={<ProductDetailPage />} />
<Route path="cart" element={<Cart />} />
<Route path="checkout" element={<CheckoutPage />} />
</Route>
</Routes>
</BrowserRouter>
</ProductProvider>
</CartProvider>
);
}
export default App;
Features Implementation
Complex State Management
- Context API: Global state for cart and products
- useReducer: Complex cart state management
- Local Storage: Persist cart data
- State Updates: Efficient state updates
Shopping Cart
- Add Items: Add products to cart
- Remove Items: Remove from cart
- Update Quantities: Change item quantities
- Calculate Totals: Automatic total calculation
- Persist Cart: Save to local storage
Product Catalog
- Display Products: Show all products
- Filtering: Filter by category, price, search
- Product Details: Individual product pages
- Responsive Grid: Product grid layout
Testing Your Application
Manual Testing Checklist
- [ ] View product catalog
- [ ] Filter products
- [ ] View product details
- [ ] Add items to cart
- [ ] Update cart quantities
- [ ] Remove items from cart
- [ ] View cart
- [ ] Proceed to checkout
- [ ] Complete checkout form
- [ ] Test cart persistence
Exercise: E-commerce Frontend
Instructions:
- Set up React project
- Create all components and contexts
- Implement all features
- Test thoroughly
- Customize design
Enhancement Ideas:
- Add user authentication
- Add product reviews
- Add wishlist functionality
- Add product recommendations
- Add order history
- Add payment integration
- Add shipping calculator
- Add product images gallery
Common Issues and Solutions
Issue: Cart not persisting
Solution: Check localStorage implementation in CartContext.
Issue: State not updating
Solution: Ensure Context providers wrap components correctly.
Issue: Filters not working
Solution: Check useMemo dependencies in ProductContext.
Quiz: Complex React App
-
Context API:
- A) Manages global state
- B) Doesn't manage state
- C) Both
- D) Neither
-
useReducer:
- A) Complex state management
- B) Simple state management
- C) Both
- D) Neither
-
Shopping cart:
- A) Complex state
- B) Simple state
- C) Both
- D) Neither
-
Local storage:
- A) Persists data
- B) Temporary data
- C) Both
- D) Neither
-
Product filtering:
- A) Uses useMemo
- B) Doesn't use useMemo
- C) Both
- D) Neither
Answers:
- A) Manages global state
- A) Complex state management
- A) Complex state
- A) Persists data
- A) Uses useMemo (for optimization)
Key Takeaways
- Complex State: Use Context API and useReducer
- Shopping Cart: Implement cart functionality
- Product Catalog: Build filtering and display
- State Management: Efficient state updates
- Best Practice: Clean architecture and separation
Next Steps
Congratulations! You've built an E-commerce Frontend. You now know:
- How to manage complex state
- How to implement shopping cart
- How to build product catalog
- How to create complex React apps
What's Next?
- Project 3: Full-Stack Applications
- Learn backend development
- Build REST APIs
- Create full-stack applications
Project completed! You've finished Project 2: Frontend Applications. Ready for Project 3: Full-Stack Applications!
Course Navigation
- React Todo App
- React Blog Application
- E-commerce Frontend