Open In App

Bookstore Ecommerce App using MEAN Stack

Last Updated : 08 May, 2024
Improve
Improve
Like Article
Like
Save
Share
Report

In this article, we will build a simple yet powerful bookstore application using Angular for the front end and Node.js for the back end. This project application will showcase how to set up a full-stack web application where users can view, filter, and purchase various books.

Project Preview:

Screenshot-2024-04-28-003950

Prerequisites

Approach

We will follow a modular approach, separating the frontend and backend components. The frontend will communicate with the backend through HTTP requests to fetch book data and manage the cart.

  • Display a list of books with their details like title, author, description, and price.
  • Users can add books to their cart and remove them.
  • The cart will display the total price and allow users to proceed to checkout.
  • Implement filtering and sorting options for better user experience.

Steps to Create Application

Step 1: Create server folder and navigate to server

mkdir bookstore-backend
cd bookstore-backend

Step 2: Initialize the Node Application

npm init -y

Folder Structure(Backend)

Screenshot-2024-04-28-003033

Backend Folder Strucure

Step 3: Install the required dependencies.

npm install express mongoose cors body-parser

Updated dependencies looks like

 "dependencies": {
"cors": "^2.8.5",
"express": "^4.19.2",
"mongoose": "^8.3.2"
}

Example: Create few file server.js,Book.js,data.jsonbookRoutes.js with following code given below:

JavaScript
//index.js

const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const Book = require('./Book');
const bookRoutes = require('./bookRoutes');

const app = express();
const PORT = process.env.PORT || 5000;

// Middleware
app.use(express.json());
app.use(cors());

// MongoDB connection
mongoose.connect('Your mongo DB link',
    { useNewUrlParser: true, useUnifiedTopology: true })
    .then(() => console.log('MongoDB connected'))
    .catch(err => console.error(err));

// Seed the database on server startup
const seedDatabase = async () => {
    try {
        await Book.deleteMany();
        const books = require('./data.json');
        await Book.insertMany(books);
        console.log('Database seeded successfully');
    } catch (error) {
        console.error('Error seeding database:', error);
    }
};

seedDatabase();

// Routes
app.use('/api/books', bookRoutes);

app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});
JavaScript
//book.js

const mongoose = require('mongoose');

const bookSchema = new mongoose.Schema({
    title: String,
    author: String,
    genre: String,
    description: String,
    price: Number,
    image: String
});

const Book = mongoose.model('Book', bookSchema);

module.exports = Book;
JavaScript
//bookRoutes.js

const express = require('express');
const Book = require('./Book');
const router = express.Router();

router.get('/', async (req, res) => {
    try {
        const allBooks = await Book.find();
        res.json(allBooks);
    } catch (error) {
        console.error(error);
        res.status(500)
            .json({ error: 'Internal Server Error' });
    }
});

module.exports = router;
JavaScript
//data.json
[
    {
        "title": "The Great Gatsby",
        "author": "F. Scott Fitzgerald",
        "genre": "Fiction",
        "description": "A classic novel about the American Dream",
        "price": 20,
        "image": "https://media.geeksforgeeks.org/wp-content/
            uploads/20240110011815/sutterlin-1362879_640-(1).jpg"
    },
    {
        "title": "To Kill a Mockingbird",
        "author": "Harper Lee",
        "genre": "Fiction",
        "description": "A powerful story of racial injustice and moral growth",
        "price": 15,
        "image": "https://media.geeksforgeeks.org/wp-content/
            uploads/20240110011854/reading-925589_640.jpg"
    },
    {
        "title": "1984",
        "author": "George Orwell",
        "genre": "Dystopian",
        "description": "A dystopian vision of a totalitarian future society",
        "price": 255,
        "image": "https://media.geeksforgeeks.org/wp-content/
            uploads/20240110011929/glasses-1052010_640.jpg"
    },
    {
        "title": "The Great Gatsby",
        "author": "F. Scott Fitzgerald",
        "genre": "Fiction",
        "description": "A classic novel about the American Dream",
        "price": 220,
        "image": "https://media.geeksforgeeks.org/wp-content/
            uploads/20240110011929/glasses-1052010_640.jpg"
    },
    {
        "title": "To Kill a Mockingbird",
        "author": "Harper Lee",
        "genre": "Fiction",
        "description": "A powerful story of racial injustice and moral growth",
        "price": 1115,
        "image": "https://media.geeksforgeeks.org/wp-content/
            uploads/20240110011929/glasses-1052010_640.jpg"
    },
    {
        "title": "1984",
        "author": "George Orwell",
        "genre": "Dystopian",
        "description": "A dystopian vision of a totalitarian future society",
        "price": 125,
        "image": "https://media.geeksforgeeks.org/wp-content/
            uploads/20240110011929/glasses-1052010_640.jpg"
    }
]

Start the server using the following command

node index.js

Step 4: Install Angular CLI (if not already installed)

npm install -g @angular/cli

Step 5: Create a new Angular project

ng new client --style=css --routing=false
cd client

Project Structure(Frontend)

Screenshot-2024-04-23-152355

Frontend Folder Structure

Dependencies

"dependencies": {
"@angular/animations": "^17.3.0",
"@angular/common": "^17.3.0",
"@angular/compiler": "^17.3.0",
"@angular/core": "^17.3.0",
"@angular/forms": "^17.3.0",
"@angular/platform-browser": "^17.3.0",
"@angular/platform-browser-dynamic": "^17.3.0",
"@angular/router": "^17.3.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
}

Change and update the files

  • create 3 folders insider src/app :components,models and services
  • Create files header-product.component.ts and header-product.component.css insider component folder
  • Create file book.model.ts inside models folder
  • Create file header-cart.service.ts inside services
  • create app.module.ts inside app folder
  • update code of app.component.ts,main.ts and index.html of app folder
HTML
<!-- src/index.html -->

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>Bookstore</title>
    <base href="/">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>

<body>
    <app-root></app-root>
</body>

</html>
CSS
/* components/header-product.component.css */
/* Header Styles */

.navbar {
    text-align: center;
    background-color: #2c3e50;
    color: white;
    padding: 20px 0;
}

.navbar-item {
    font-size: 24px;
    font-weight: bold;
}

.header {
    background-color: #34495e;
    color: #fff;
    padding: 10px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.cart {
    position: relative;
    cursor: pointer;
}

.cart span {
    margin-right: 5px;
}

.cart-items {
    position: absolute;
    top: calc(100% + 5px);
    right: 0;
    background-color: #2c3e50;
    border: 1px solid #34495e;
    padding: 10px;
    display: flex;
    flex-direction: column;
}

.cart-items p {
    margin: 5px 0;
}

/* Product List Styles */
.product-list {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-around;
    padding: 20px 0;
}

.product-card {
    width: 30%;
    margin-bottom: 20px;
    background-color: #ecf0f1;
    border-radius: 10px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    transition: transform 0.3s ease;
}

.product-card img {
    width: 100%;
    border-top-left-radius: 10px;
    border-top-right-radius: 10px;
}

.product-card:hover {
    transform: translateY(-5px);
}

.product-details {
    padding: 15px;
}

.product-details h3 {
    margin-top: 0;
    font-size: 20px;
}

.product-details p {
    margin: 5px 0;
}

.product-details button {
    background-color: #2ecc71;
    color: #fff;
    border: none;
    padding: 10px 20px;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s;
}

.product-details button:hover {
    background-color: #27ae60;
}
JavaScript
//components/header-product.component.ts

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { HeaderCartService } from '../services/header-cart.service';
import { Book } from '../models/book.model';

@Component({
    selector: 'app-header-product',
    template: `
    <nav class="navbar">
      <div class="navbar-brand">
        <span class="navbar-item">
            GFG Bookstore by Ashish Regmi</span>
      </div>
    </nav>

    <!-- Header template -->
    <div class="header">
        <!-- Filter and Sort options -->
        <div class="options">
            <div class="filter-options">
                <label for="minPrice">Min Price:</label>
                <input type="number" id="minPrice" 
                [(ngModel)]="minPrice">
                <label for="maxPrice">Max Price:</label>
                <input type="number" id="maxPrice" 
                [(ngModel)]="maxPrice">
                <button (click)="filterProducts()">Filter</button>
  
                <label for="sort">Sort by:</label>
                <select id="sort" [(ngModel)]="sortBy"
                 (change)="sortProducts()">
                    <option value="priceLowToHigh">
                        Price: Low to High</option>
                    <option value="priceHighToLow">
                        Price: High to Low</option>
                </select>
            </div>
        </div>

        <!-- Cart -->
        <div class="cart" (click)="toggleCart() ">
            <span>Cart ({{ cartItems.length }})</span>
            <div *ngIf="showCart" class="cart-items"
             (click)="preventClose($event)">
                <div *ngFor="let item of cartItems">
                    <p>{{ item.title }} - {{ item.price }}</p>
                    <button (click)="removeFromCart(item)">Remove</button>
                </div>
                <div>Total Price: {{ calculateTotalPrice() }}</div>
            </div>
        </div>
    </div>

    <!-- Product list template -->
    <div class="product-list">
        <div *ngFor="let product of filteredProducts" class="product-card">
            <img [src]="product.image" alt="Book Cover">
            <div class="product-details">
                <h3>{{ product.title }}</h3>
                <p>{{ product.description }}</p>
                <p>Price: {{ product.price }} Rs</p>
                <button (click)="addToCart(product)">Add to Cart</button>
            </div>
        </div>
    </div>
  `,
    styleUrls: ['./header-product.component.css']
})
export class HeaderProductComponent implements OnInit {
    products: Book[] = [];
    cartItems: Book[] = [];
    showCart: boolean = false;
    totalPrice: number = 0;
    minPrice: number = 0;
    maxPrice: number = 0;
    filteredProducts: Book[] = [];
    sortBy: string = 'priceLowToHigh';

    constructor(private http: HttpClient,
        private cartService: HeaderCartService) { }

    ngOnInit(): void {
        // Fetch products from the backend
        this.http.get<Book[]>('http://localhost:5000/api/books')
            .subscribe(products => {
                this.products = products;
                this.filteredProducts = products;
            });

        // Subscribe to changes in the cart
        this.cartService.cart$.subscribe(cartItems => {
            this.cartItems = cartItems;
        });
    }

    addToCart(product: Book) {
        this.cartService.addToCart(product);
        this.calculateTotalPrice();
    }

    removeFromCart(product: Book) {
        this.cartService.removeFromCart(product);
        this.calculateTotalPrice();
    }

    toggleCart() {
        this.showCart = !this.showCart;
    }

    calculateTotalPrice(): number {
        return this.cartItems.reduce((total, item) =>
            total + item.price, 0);
    }

    filterProducts() {
        this.filteredProducts = this.products.filter(product => {
            return product.price >= this.minPrice &&
                product.price <= this.maxPrice;
        });
        this.sortProducts();
    }

    sortProducts() {
        if (this.sortBy === 'priceLowToHigh') {
            this.filteredProducts.sort((a, b) => a.price - b.price);
        } else if (this.sortBy === 'priceHighToLow') {
            this.filteredProducts.sort((a, b) => b.price - a.price);
        }
    }

    preventClose(event: MouseEvent) {
        event.stopPropagation();
    }
}
JavaScript
// models/book.model.ts

export interface Book {
    _id: string;
    title: string;
    author: string;
    genre: string;
    description: string;
    price: number;
    image: string;
}
JavaScript
// services/header-cart.service.ts

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Book } from '../models/book.model';

@Injectable({
    providedIn: 'root'
})
export class HeaderCartService {
    private cart: Book[] = [];
    private cartSubject = new BehaviorSubject
    <Book[]>(this.cart);
    cart$ = this.cartSubject.asObservable();

    constructor(private http: HttpClient) { }

    addToCart(book: Book) {
        this.cart.push(book);
        this.cartSubject.next(this.cart);
    }

    removeFromCart(book: Book) {
        const index = this.cart.findIndex
        (item => item._id === book._id);
        if (index !== -1) {
            this.cart.splice(index, 1);
            this.cartSubject.next(this.cart);
        }
    }
}
JavaScript
// client/src/app/app.component.ts

import { Component } from '@angular/core';

@Component({
    selector: 'app-root',
    template: `
    <app-header-product></app-header-product>
  `,
})
export class AppComponent { }
JavaScript
// src/app/app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';

import { AppComponent } from './app.component';
import { HeaderProductComponent } from './components/header-product.component';
import { HeaderCartService } from './services/header-cart.service';

@NgModule({
    declarations: [
        AppComponent,
        HeaderProductComponent
    ],
    imports: [
        BrowserModule,
        FormsModule, // Add FormsModule here
        HttpClientModule
    ],
    providers: [HeaderCartService],
    bootstrap: [AppComponent]
})
export class AppModule { }
JavaScript
// client/src/main.ts

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.error(err));

Start the Angular App using the following command.

ng serve

Output

bb



Like Article
Suggest improvement
Previous
Next
Share your thoughts in the comments

Similar Reads