E-commerce Frontend
The capstone project. Products, cart, checkout — a real-world shopping experience.
Architecture
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();Product Listing
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.
1 / 2
Shopping Cart
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);
}Checkout
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();
}State Management
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 displayFAQ
Common questions about this project.