Loading...
Loading...
The capstone project. Products, cart, checkout — a real-world shopping experience.
Module 11 · JavaScript Projects
Theory becomes skill through building. These guided projects — calculator, todo app, weather app, and more — combine everything you've learned into real applications you can extend, share, and put in a portfolio.
Learn E-commerce Frontend in JavaScript — covering Architecture, Product Listing, Shopping Cart, Checkout with interactive visual examples and step-by-step explanations.
Reading about JavaScript is not the same as building with it. These guided projects combine variables, functions, DOM manipulation, async fetch, and state management into applications you can run, extend, and showcase.
Each project walks through architecture decisions, common pitfalls, and incremental steps. By the end you will have portfolio-ready apps and the confidence to start your own projects from scratch.
Understanding e-commerce frontend is essential for every JavaScript developer. It shows up in frontend UI code, backend APIs, and framework internals — and getting it wrong leads to bugs that are hard to trace. This chapter builds intuition with visuals so the behavior sticks.
Page-based routing with shared state across views.
// E-commerce app architecture: // Pages: Home → Product List → Product Detail → Cart → Checkout // // Structure: // ecommerce/ // ├── index.html // ├── style.css // ├── js/ // │ ├── app.js ← router + init // │ ├── store.js ← global state (cart, products) // │ ├── api.js ← fetch products // │ ├── router.js ← SPA routing // │ └── pages/ // │ ├── home.js // │ ├── products.js // │ ├── product-detail.js // │ ├── cart.js // │ └── checkout.js // Simple SPA router (hash-based): class Router { #routes = new Map(); #appEl = null; constructor(appEl) { this.#appEl = appEl; window.addEventListener("hashchange", () => this.resolve()); } add(path, handler) { this.#routes.set(path, handler); return this; // chainable } resolve() { const hash = window.location.hash.slice(1) || "/"; const [path, ...params] = hash.split("/").filter(Boolean); const route = this.#routes.get("/" + (path || "")); if (route) { this.#appEl.innerHTML = ""; route(this.#appEl, params); } else { this.#appEl.innerHTML = "<h1>404 — Page not found</h1>"; } } navigate(path) { window.location.hash = path; } } // Usage: const router = new Router(document.querySelector("#app")); router .add("/", renderHomePage) .add("/products", renderProductsPage) .add("/product", renderProductDetailPage) .add("/cart", renderCartPage) .add("/checkout", renderCheckoutPage) .resolve();
Fetch products, filter, sort, and paginate.
// Fetch products from API (or mock data): async function fetchProducts() { // Using FakeStoreAPI for demo: const response = await fetch("https://fakestoreapi.com/products"); if (!response.ok) throw new Error("Failed to fetch products"); return response.json(); } // Product card rendering: function renderProductCard(product) { return ` <article class="product-card" data-id="${product.id}"> <div class="product-image"> <img src="${product.image}" alt="${escapeHtml(product.title)}" loading="lazy"> </div> <div class="product-info"> <h3 class="product-title">${escapeHtml(product.title)}</h3> <div class="product-rating"> ${renderStars(product.rating.rate)} <span>(${product.rating.count})</span> </div> <p class="product-price">$${product.price.toFixed(2)}</p> <button class="add-to-cart-btn" data-id="${product.id}"> Add to Cart </button> </div> </article> `; } function renderStars(rating) { const full = Math.floor(rating); const half = rating % 1 >= 0.5 ? 1 : 0; const empty = 5 - full - half; return "★".repeat(full) + (half ? "½" : "") + "☆".repeat(empty); }
Fetch products from an API, render as cards with image, title, rating, and price. Lazy-load images for performance.
Add, remove, update quantities, and calculate totals.
// Cart data structure: // cart = [{ productId: 1, quantity: 2 }, ...] const store = { cart: [], products: [], // cached product data addToCart(productId) { const existing = this.cart.find(item => item.productId === productId); if (existing) { existing.quantity++; } else { this.cart.push({ productId, quantity: 1 }); } this.save(); this.notify(); }, removeFromCart(productId) { this.cart = this.cart.filter(item => item.productId !== productId); this.save(); this.notify(); }, updateQuantity(productId, quantity) { if (quantity <= 0) { this.removeFromCart(productId); return; } const item = this.cart.find(i => i.productId === productId); if (item) item.quantity = quantity; this.save(); this.notify(); }, getCartItems() { return this.cart.map(item => { const product = this.products.find(p => p.id === item.productId); return { ...item, product }; }).filter(item => item.product); // remove items with missing products }, getCartTotal() { return this.getCartItems().reduce( (total, item) => total + item.product.price * item.quantity, 0 ); }, getCartCount() { return this.cart.reduce((count, item) => count + item.quantity, 0); }, save() { localStorage.setItem("cart", JSON.stringify(this.cart)); }, load() { try { this.cart = JSON.parse(localStorage.getItem("cart") || "[]"); } catch { this.cart = []; } }, }; // Cart badge in header: function updateCartBadge() { const count = store.getCartCount(); const badge = document.querySelector(".cart-badge"); badge.textContent = count; badge.classList.toggle("hidden", count === 0); }
Multi-step form with validation and order summary.
// Checkout steps: Shipping → Payment → Review → Confirmation let checkoutStep = 1; const checkoutData = { shipping: {}, payment: {}, }; function renderCheckoutPage(appEl) { const items = store.getCartItems(); if (items.length === 0) { appEl.innerHTML = '<p>Your cart is empty.</p>'; return; } appEl.innerHTML = ` <div class="checkout"> <div class="steps"> <span class="${checkoutStep >= 1 ? "active" : ""}">Shipping</span> <span class="${checkoutStep >= 2 ? "active" : ""}">Payment</span> <span class="${checkoutStep >= 3 ? "active" : ""}">Review</span> </div> <div class="checkout-content"></div> <aside class="order-summary"> <h3>Order Summary</h3> ${items.map(i => ` <div class="summary-item"> <span>${escapeHtml(i.product.title)} × ${i.quantity}</span> <span>$${(i.product.price * i.quantity).toFixed(2)}</span> </div> `).join("")} <div class="summary-total"> <strong>Total: $${store.getCartTotal().toFixed(2)}</strong> </div> </aside> </div> `; renderCheckoutStep(); } // Form validation: function validateShipping(data) { const errors = {}; if (!data.name?.trim()) errors.name = "Name is required"; if (!data.email?.match(/^[^@]+@[^@]+\.[^@]+$/)) errors.email = "Valid email required"; if (!data.address?.trim()) errors.address = "Address is required"; if (!data.city?.trim()) errors.city = "City is required"; if (!data.zip?.match(/^\d{5}(-\d{4})?$/)) errors.zip = "Valid ZIP required"; return { valid: Object.keys(errors).length === 0, errors }; } // Step navigation: function nextStep() { if (checkoutStep === 1) { const form = document.querySelector(".shipping-form"); const data = Object.fromEntries(new FormData(form)); const { valid, errors } = validateShipping(data); if (!valid) { showFormErrors(errors); return; } checkoutData.shipping = data; } checkoutStep++; renderCheckoutStep(); } function prevStep() { checkoutStep = Math.max(1, checkoutStep - 1); renderCheckoutStep(); }
Pub/Sub pattern to sync UI across pages.
// Simple reactive store with subscribers: class Store { #state = {}; #listeners = new Set(); constructor(initialState) { this.#state = { ...initialState }; } getState() { return { ...this.#state }; // return copy } setState(updates) { this.#state = { ...this.#state, ...updates }; this.#notify(); } subscribe(listener) { this.#listeners.add(listener); return () => this.#listeners.delete(listener); // unsubscribe } #notify() { this.#listeners.forEach(fn => fn(this.#state)); } } // Create app store: const appStore = new Store({ cart: [], products: [], user: null, loading: false, }); // Components subscribe to store changes: const unsubHeader = appStore.subscribe((state) => { updateCartBadge(state.cart.length); }); const unsubProductPage = appStore.subscribe((state) => { if (state.products.length) renderProducts(state.products); }); // Actions modify state: function addToCartAction(productId) { const { cart } = appStore.getState(); const existing = cart.find(i => i.productId === productId); if (existing) { appStore.setState({ cart: cart.map(i => i.productId === productId ? { ...i, quantity: i.quantity + 1 } : i ), }); } else { appStore.setState({ cart: [...cart, { productId, quantity: 1 }] }); } } // Persist to localStorage on every change: appStore.subscribe((state) => { localStorage.setItem("app-state", JSON.stringify({ cart: state.cart, })); }); // Key patterns in this project: // ✓ SPA routing (hash-based) // ✓ Product catalog with filter/sort/search // ✓ Shopping cart with localStorage persistence // ✓ Multi-step checkout with validation // ✓ Pub/Sub state management (mini Redux) // ✓ Pagination // ✓ Lazy loading images // ✓ Form validation with error display
Common questions about e-commerce frontend.