E-Commerce Project Screenshot

E-Commerce Project

This is a demo e-commerce store built with the MERN stack, featuring product listings, a shopping cart, a basic checkout flow, user accounts, and an admin panel. The goal was to demonstrate full-stack skills, scalability, and UI/UX design.

👉 View Live Project

You can view the github repo here.


🔧 Features Built

Product Listing

Browse products with images, descriptions, and prices.

Product Listing

Shopping Cart

Add products to the cart and view total price.

Shopping Cart

Checkout Flow

Proceed to checkout with user authentication.

Checkout Flow

User Accounts

Create and manage user accounts.

User Accounts

Admin Panel

Manage products, orders, and users.

Admin Panel

📚 Lessons Learned

This project helped reinforce API design, authentication flows, and working with modular state management (Zustand). I also learned how to refine UI decisions based on user expectations.

🚀 Tech Stack

  • Frontend: React (Vite) + TypeScript + Zustand
  • Backend: Node.js + Express + MongoDB
  • Styling: TailwindCSS
  • Hosting: Vercel (Frontend), Railway (Backend)

📐 Design Patterns

One of the things that I really wanted to do well with this project was to make sure that I was using the right design patterns and structure. I refactored the structure multiple times during development as features were added and changed. These changes caused me to take new factors into consideration, such as how to best handle seperation of concerns, and how to best handle the state of the application. I ended up using Zustand for state management, and I found that it worked really well with the structure I had in place. I also used a lot of custom hooks to handle the logic of the application, which helped to keep the components clean and easy to read.

📦 File Structure

src
|── api
│   ├── admin
│   ├── anon
│   ├── cart
│   ├── order
│   ├── etc
├── components
│   ├── breadcrumb
│   ├── card
│   ├── cartIcon
│   ├── confirmDialog
│   ├── etc
├── hooks
│   ├── useAddToCart
│   ├── useRequest
├── pages
│   ├── 404
│   ├── adminDashboard
│   ├── adminLogin
│   ├── cart
│   ├── etc
├── services
│   ├── adminService
│   ├── anonService
│   ├── appService
│   ├── etc
├── store
│   ├── adminStore
│   ├── cartStore
│   ├── cookieStore
│   ├── etc
The simple flow of the frontend is as follows:
  • The user interacts with the UI, loading specific pages using react-router.
  • The page uses hooks to fetch data needed for the page, either using a service or the api directly.
  • The service handles fetches data via the appropriate api and any business logic needed for the page, such as formatting the data, handling errors, and caching the data in the store.
  • The api formats the request sent to the backend service and returns the response.
  • The service will either place the data in a store or return it directly to the page depending on the buisiness logic.
  • The page will render the data using the appropriate components.

Each part of the application is responsible for a specific task, and it can be tested and debugged independently.

Pages

The pages are responsible for rendering the UI and handling the user interactions.

Example: Orders Page

import { useNavigate } from "react-router-dom";
import { useEffect } from "react";
import useOrderStore from "../store/orderStore";
import { fetchOrders } from "../services/orderService";
import Card from "../components/card";
import { useRequest } from "../hooks/useRequest";
import LoadingSpinner from "../components/loadingSpinner";

const Orders = () => {
  const { orders } = useOrderStore();
  const navigate = useNavigate();
  const { error, loading, execute } = useRequest<void>();

  useEffect(() => {
    execute(fetchOrders);
  }, [execute]);

  if (loading) {
    return (
      <div className="flex justify-center mt-6">
        <LoadingSpinner />
      </div>
    );
  }
  if (error) {
    return (
      <div className="p-4 text-danger">
        <p>{error}</p>
      </div>
    );
  }
  return (
    <div>
      <div className="header">
        <h2>My Orders</h2>
      </div>
      <div className=" p-6 max-w-4xl mx-auto">
        {orders.length === 0 ? (
          <p className="text-center">No orders found.</p>
        ) : (
          <div className="space-y-4">
            {orders.map((order) => (
              <Card
                className="hover:scale-110 hover:shadow-2xl"
                key={order._id}
                title={`Order #${order._id}`}
                onClick={() => navigate(`/orders/${order._id}`)}
                footer={`Total: $${order.totalPrice.toFixed(2)}`}
              >
                <p className="mb-2">
                  Status: <span className="font-semibold">{order.status}</span>
                </p>
              </Card>
            ))}
          </div>
        )}
      </div>
    </div>
  );
};

export default Orders;

This page is responsible for rendering the orders page. It uses the useOrderStore hook to get the orders from the store, and it uses the useRequest hook to fetch the orders from the server. It then renders the orders in a list. If there are no orders, it shows a message saying that there are no orders found. If there is an error, it shows the error message. If it is loading, it shows a loading spinner.


Components

The components are responsible for rendering specific parts of the UI. They are reusable and can be used in different pages.

Example: ProductCard Component

import { useNavigate } from "react-router-dom";
import { Product } from "../types";
import { useAddToCart } from "../hooks/useAddToCart";
import Card from "./card";
import clsx from "clsx";

const ProductCard = ({ product, className }: { product: Product, className?: string }) => {
  const { added, handleAddToCart } = useAddToCart();
  const navigate = useNavigate();

  const footer = (
    <button
      className='btn btn-primary'
      onClick={() => handleAddToCart(product, 1)}
    >
      {added ? "Added" : "Add to Cart"}
    </button>
  )
  return (
    <Card className={clsx("text-center", className)} title={product.name} footer={footer} onClick={() => navigate(`/products/${product._id}`)}>
      <h3>{product.price}</h3>
      <img src={product.imageUrl} alt={product.name} className="w-full h-48 object-contain" />
    </Card>

  );
};

export default ProductCard;

This component is responsible for rendering the product card. It uses the useAddToCart hook to handle adding the product to the cart, and it uses the useNavigate hook to navigate to the product details page when the card is clicked. It also uses the Card component to render the card.


Services

The services are responsible for handling the business logic of the application. They call the API and handle the responses. If we are storing the data in a global store, that is done here as well.

Example: Order Service

import * as api from "../api/order";
import { logError } from "../utils/logging";
import { getOrders, setOrders } from "../utils/storage";

/**
 * Fetches all orders from the server and sets them in the store.
 */
export const fetchOrders = async () => {
  try {
    const response = await api.getOrders();
    setOrders(response);
  } catch (error) {
    logError("fetchOrders", error);
    throw error;
  }
};

This service function is responsible for fetching the orders from the server and setting them in the store. It uses the api module to call the API and handle the response. If there is an error, it logs the error and throws it.


Store

The store is responsible for storing the data in a global state. It uses Zustand to create the store and handle the state management.

Example: Cart Store

import { create } from "zustand";
import { Cart } from "../types";

interface CartState {
  cart: Cart;
  setCart: (cart: Cart) => void;
}

const useCartStore = create<CartState>((set) => ({
  cart: { _id: "", user: "", items: [], total: 0 },
  setCart: (cart) => set({ cart }),
}));

export default useCartStore;

This store is responsible for storing the cart data in a global state. It uses Zustand to create the store and handle the state management. It has a cart state and a setCart function to update the cart. The store is kept very simple on purpose.


Hooks

The hooks are responsible for handling the reusable logic in the pages and components.

Example: useRequest Hook

import { useState, useCallback } from "react";

export const useRequest = <T,>() => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [data, setData] = useState<T | null>(null);

  const execute = useCallback(async (requestFn: () => Promise<T>) => {
    setLoading(true);
    setError(null);
    try {
      const result = await requestFn();
      setData(result);
    } catch (err) {
      setError("Something went wrong. Please try again.");
      console.error(err);
    } finally {
      setLoading(false);
    }
  }, []);

  return { data, loading, error, execute };
};

This hook is responsible for handling the API requests and managing the loading and error states. It uses the useState and useCallback hooks to manage the state and handle the requests. It returns the data, loading, error, and execute function to call the API.


API

The API is responsible for handling the requests and responses from the server. It uses Axios to handle the requests and responses.

Example: Order API

import axios from "./axiosConfig";
import { Order } from "../types";
import { authHeader } from "../utils";

// Fetch all orders
export const getOrders = async (): Promise<Order[]> => {
  const response = await axios.get("/orders", authHeader());
  return response.data;
};

// Place an order
export const placeOrder = async (): Promise<Order> => {
  const response = await axios.post("/orders", null, authHeader());
  return response.data;
};

// Get order by ID
export const getOrderById = async (orderId: string): Promise<Order> => {
  const response = await axios.get(`/orders/${orderId}`, authHeader());
  return response.data;
};

This API module is responsible for handling the requests and responses from the server. It uses Axios to handle the requests and responses. It has functions to fetch all orders, place an order, and get an order by ID. It uses the authHeader function to add the authorization header to the requests.

📡 API

The API is built using Express and MongoDB, and it follows RESTful principles. The API is designed to be modular and scalable, with a clear separation of concerns. The API is also designed to be easy to use and understand, with clear documentation and error handling.

The API is built using the following endpoints:

📸 Screenshots

E-Commerce Project ScreenshotE-Commerce Project ScreenshotE-Commerce Project ScreenshotE-Commerce Project Screenshot