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 display

FAQ

Common questions about this project.