Back to: Angular Tutorials For Beginners and Professionals
Real-Time Mini E-Commerce Application using Angular
Now, we will develop a Real-Time Mini E-Commerce Application, built with Angular, to understand how a modern shopping application works end-to-end. This application also brings together concepts such as routing, state handling, guards, resolvers, and services.
In this application, users can browse products freely, view detailed product information, manage a shopping cart, and complete a checkout process using a simple authentication mechanism. The design intentionally mirrors real-world platforms like Amazon and Flipkart, where browsing is public but sensitive actions, such as checkout, are protected. The app uses Angular routing, guards, resolvers, services, and local storage in a clean and architecturally correct way. Let us look at the pages we will develop for this application.
Login Page:
The Login page provides a simple demo authentication experience for the user. It collects the customer’s name and email and stores them locally to simulate a logged-in session. This page is required only when the user attempts to access protected actions, such as checkout. If the user was redirected here from another page, the application automatically sends them back to the original page after successful login, ensuring a smooth user experience.

Products Listing Page:
The Products Listing page is the main entry point of the application. It displays all available products in a clean card-based layout and allows users to search by product name or brand, filter by category, and sort by rating or price. These filters are reflected in the URL using query parameters, making the page state shareable and bookmark-friendly. Users can view product details or add items directly to the cart without logging in.

Product Details Page:
The Product Details page shows complete information about a selected product, including pricing, discounts, delivery details, stock availability, highlights, and warranty. Product data is loaded using a route resolver, ensuring the page opens only after valid data is available. Users can choose a quantity, add the product to the cart, or use the “Buy Now” option, which takes them directly to checkout.

Cart Page:
The Cart page allows users to review all selected products before placing an order. Users can increase or decrease quantities, remove items, clear the cart, and view a detailed price summary including discounts and shipping charges. The cart data is stored in local storage, ensuring persistence even if the page is refreshed. This page is intentionally kept public so users can build their cart before deciding to log in.

Checkout Page:
The Checkout page is a protected page accessible only to logged-in users. It collects delivery address details, performs basic validation, and displays a final order summary on the side. For simplicity, the application supports Cash on Delivery (COD) as the payment method. Once the user confirms the order, the cart is cleared, and the user is redirected to the order success page.

Order Placed Successful Page:
The Order Success page confirms that the order has been placed successfully. It is accessible only through the checkout flow and prevents direct access via URL or page refresh. The page reassures the user with a clear success message and provides options to continue shopping or return to the cart, completing the shopping journey gracefully.

Let us proceed and implement the above application step by step.
Step 1 – Project Initialization and Environment Setup
Create a new Angular application named MiniCart, run it locally, and confirm the development server is working. This step ensures your base project is ready before you add folders, routing, UI, or business logic.
- ng new MiniCart
- cd MiniCart
- ng serve -o
Step 2: Designing a Scalable Folder Structure
A well-structured folder layout is essential for maintaining large applications. Here, the project is divided into core, models, services, and pages, each serving a specific purpose. This separation makes the application easier to understand, scale, and maintain as new features are added. So, create the following folders:
- src/app/core/
- src/app/models/
- src/app/services/
- src/app/pages/
Step 3 – Integrating Bootstrap for Professional UI
Bootstrap is added via CDN links to quickly create a clean, responsive user interface. This allows us to focus on application logic rather than spending time writing custom CSS for layouts, navigation bars, cards, and forms.
src/index.html
This file adds Bootstrap CSS, Bootstrap Icons, and Bootstrap JS via CDN so the entire app can use responsive layouts, navbars, cards, icons, and modal behaviours without extra setup. So, open src/index.html file and copy-paste the following code.
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>MiniCart</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> </head> <body> <app-root></app-root> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> </body> </html>
Step 4: Adding Custom Style
Add a few global CSS rules to improve the overall look (light background, cleaner cards, consistent product image height). This keeps the UI polished while still relying mostly on Bootstrap. So, open src/style.css, and copy-paste the following code.
body { background: #f7f7f9; }
.card { border: 0; }
.product-img{
height: 360px;
}
Step 5: Generating Application Pages
In this step, Angular components are generated for all major pages, including Login, Products, Product Details, Cart, Checkout, Order Success, and Not Found. Each page represents a real screen in an e-commerce application and is implemented as a standalone component for better modularity.
- ng g c pages/login
- ng g c pages/products
- ng g c pages/product-details
- ng g c pages/cart
- ng g c pages/checkout
- ng g c pages/order-success
- ng g c pages/not-found
Step 6 – Defining Shared Data Models
Models define the shape of data used across the application. The Product and CartItem models ensure consistency when handling product information and cart data. This approach improves readability and reduces bugs caused by mismatched data structures.
Product Model
Create a TypeScript class named product.ts within the src/app/models folder, and then copy-paste the following code. Defines the Product interface and the ProductCategory type, ensuring that every product displayed on listing and details pages follows a consistent structure.
export type ProductCategory = 'Mobiles' | 'Laptops' | 'Shoes' | 'Books';
export interface Product {
id: number;
name: string;
category: ProductCategory;
brand: string;
sku: string;
mrp: number;
discountPercent: number;
price: number;
rating: number;
inStock: boolean;
stockQty: number;
freeDelivery: boolean;
deliveryDays: number;
warrantyMonths: number;
sellerName: string;
imageUrl: string;
description: string;
highlights: string[];
}
CartItem Model
Create a TypeScript class named cart-item.ts within the src/app/models folder, and then copy-paste the following code. Defines the CartItem interface, which represents a product in the cart, including pricing details, quantity, and the date it was added—making cart calculations predictable and easy.
export interface CartItem {
productId: number;
name: string;
imageUrl: string;
brand: string;
sku: string;
mrp: number;
unitPrice: number;
discountPercent: number;
qty: number;
addedAt: string;
}
Step 7: Build Core Services (Auth, Products, Cart)
Services encapsulate business logic and shared state.
- The AuthService manages login status and customer information.
- The ProductService provides product data to the application.
- The CartService handles cart operations like add, remove, update, and price calculation.
Using services keeps components lightweight and focused only on UI responsibilities.
Auth Service
Create a TypeScript class named auth.service.ts within the src/app/services folder, and then copy-paste the following code. Manages demo authentication using localStorage—login, logout, logged-in checks, and customer retrieval—so guards and the navbar can react to auth state.
import { Injectable } from '@angular/core';
export interface Customer {
name: string;
email: string;
}
@Injectable({ providedIn: 'root' })
export class AuthService {
private key = 'minicart_customer';
login(customer: Customer): void {
localStorage.setItem(this.key, JSON.stringify(customer));
}
logout(): void {
localStorage.removeItem(this.key);
localStorage.removeItem('minicart_cart'); // optional: clear cart on logout
}
isLoggedIn(): boolean {
return !!localStorage.getItem(this.key);
}
getCustomer(): Customer | null {
const raw = localStorage.getItem(this.key);
return raw ? JSON.parse(raw) as Customer : null;
}
}
Product Service
Create a TypeScript class named product.service.ts within the src/app/services folder, and then copy-paste the following code. Provides product data (in-memory array) and helper methods such as getAll() and getById(), letting you focus on UI and routing behavior without building a backend.
import { Injectable } from '@angular/core';
import { Product } from '../models/product';
@Injectable({ providedIn: 'root' })
export class ProductService {
private readonly products: Product[] = [
{ id: 101, name: 'Galaxy S Series (128GB)', category: 'Mobiles', brand: 'Samsung', sku: 'MOB-SAMS-S-128', mrp: 62999, discountPercent: 12, price: 55439, rating: 4.6, inStock: true, stockQty: 18, freeDelivery: true, deliveryDays: 2, warrantyMonths: 12, sellerName: 'MiniCart Retail', imageUrl: 'https://placehold.jp/640x420.png?text=Galaxy+S', description: 'Smooth performance, great camera, long battery life.', highlights: ['AMOLED display', 'Fast charging', 'Great low-light camera', '5G ready'] },
{ id: 102, name: 'UltraBook Pro (i5, 16GB, 512GB)', category: 'Laptops', brand: 'Acer', sku: 'LAP-ACR-ULTRA-I5', mrp: 99999, discountPercent: 15, price: 84999, rating: 4.7, inStock: true, stockQty: 9, freeDelivery: true, deliveryDays: 3, warrantyMonths: 12, sellerName: 'MiniCart Retail', imageUrl: 'https://placehold.jp/640x420.png?text=UltraBook+Pro', description: 'Lightweight, premium build, fast SSD and great battery.', highlights: ['16GB RAM', '512GB SSD', 'FHD IPS display', 'Backlit keyboard'] },
{ id: 103, name: 'Running Shoes X (Men)', category: 'Shoes', brand: 'Nike', sku: 'SHO-NIKE-RUN-X', mrp: 5999, discountPercent: 35, price: 3899, rating: 4.3, inStock: true, stockQty: 26, freeDelivery: false, deliveryDays: 4, warrantyMonths: 0, sellerName: 'Sports Hub', imageUrl: 'https://placehold.jp/640x420.png?text=Running+Shoes', description: 'Comfort fit, durable outsole, stylish everyday runner.', highlights: ['Breathable mesh', 'Cushioned sole', 'Durable outsole', 'Lightweight'] },
{ id: 104, name: 'Clean Code (Paperback)', category: 'Books', brand: 'Pearson', sku: 'BOK-CLN-CODE', mrp: 899, discountPercent: 20, price: 719, rating: 4.8, inStock: true, stockQty: 60, freeDelivery: true, deliveryDays: 2, warrantyMonths: 0, sellerName: 'BookTown', imageUrl: 'https://placehold.jp/640x420.png?text=Clean+Code', description: 'Classic book on writing maintainable, readable code.', highlights: ['Best practices', 'Refactoring mindset', 'Real examples', 'Team-friendly code'] },
{ id: 105, name: 'Noise-Cancel Headphones', category: 'Mobiles', brand: 'Sony', sku: 'AUD-SONY-NC-01', mrp: 19999, discountPercent: 25, price: 14999, rating: 4.4, inStock: false, stockQty: 0, freeDelivery: true, deliveryDays: 5, warrantyMonths: 12, sellerName: 'AudioMart', imageUrl: 'https://placehold.jp/640x420.png?text=Headphones', description: 'Immersive sound with strong noise cancellation.', highlights: ['Noise cancellation', '20+ hours battery', 'Comfort fit', 'Fast pairing'] },
{ id: 106, name: 'Everyday Backpack (25L)', category: 'Books', brand: 'Wildcraft', sku: 'BAG-WILD-25L', mrp: 1799, discountPercent: 28, price: 1299, rating: 4.1, inStock: true, stockQty: 40, freeDelivery: false, deliveryDays: 4, warrantyMonths: 6, sellerName: 'Outdoor Store', imageUrl: 'https://placehold.jp/640x420.png?text=Backpack', description: 'Rugged backpack for office, travel, and daily use.', highlights: ['Laptop sleeve', 'Water resistant', 'Strong zippers', 'Lightweight'] }
];
getAll(): Product[] {
return [...this.products];
}
getById(id: number): Product | null {
return this.products.find(p => p.id === id) ?? null;
}
}
Cart Service
Create a TypeScript class named cart.service.ts within the src/app/services folder, and then copy-paste the following code. Implements all cart operations (add/increase/decrease/remove/clear) and computes totals (discount, shipping, grand total). It also persists the cart in localStorage so the cart survives page refresh.
import { Injectable } from '@angular/core';
import { CartItem } from '../models/cart-item';
import { Product } from '../models/product';
export interface CartSummary {
totalItems: number;
subtotalMrp: number;
subtotalPayable: number;
discount: number;
shipping: number;
grandTotal: number;
}
@Injectable({ providedIn: 'root' })
export class CartService {
private key = 'minicart_cart';
private items: CartItem[] = this.read();
private read(): CartItem[] {
const raw = localStorage.getItem(this.key);
return raw ? JSON.parse(raw) as CartItem[] : [];
}
private save(): void {
localStorage.setItem(this.key, JSON.stringify(this.items));
}
getCart(): CartItem[] {
return this.items.map(i => ({ ...i }));
}
add(product: Product, qty: number = 1): void {
const existing = this.items.find(x => x.productId === product.id);
if (existing) { existing.qty += qty; this.save(); return; }
this.items.unshift({
productId: product.id,
name: product.name,
imageUrl: product.imageUrl,
brand: product.brand,
sku: product.sku,
mrp: product.mrp,
unitPrice: product.price,
discountPercent: product.discountPercent,
qty,
addedAt: new Date().toISOString()
});
this.save();
}
increase(productId: number): void {
const i = this.items.find(x => x.productId === productId);
if (i) { i.qty += 1; this.save(); }
}
decrease(productId: number): void {
const i = this.items.find(x => x.productId === productId);
if (!i) return;
i.qty -= 1;
if (i.qty <= 0) this.remove(productId);
else this.save();
}
remove(productId: number): void {
this.items = this.items.filter(i => i.productId !== productId);
this.save();
}
clear(): void {
this.items = [];
this.save();
}
summary(): CartSummary {
const subtotalMrp = this.items.reduce((s, i) => s + i.mrp * i.qty, 0);
const subtotalPayable = this.items.reduce((s, i) => s + i.unitPrice * i.qty, 0);
const discount = subtotalMrp - subtotalPayable;
const shipping = this.items.length === 0 ? 0 : (subtotalPayable >= 999 ? 0 : 49);
const totalItems = this.items.reduce((s, i) => s + i.qty, 0);
const grandTotal = subtotalPayable + shipping;
return { totalItems, subtotalMrp, subtotalPayable, discount, shipping, grandTotal };
}
}
Step 8: Add Guards + Resolver for Real Routing Behaviour
In this step, we introduce Angular guards and resolvers.
- Route guards restrict access to sensitive pages, such as Checkout and Order Success. If a user is not logged in, they are redirected to the login page.
- Resolvers ensure that product data is fetched before navigating to the product details page, resulting in a smoother user experience.
This improves both security and user experience.
Auth Guard (Protect Products/Cart/Checkout)
Create a TypeScript class named auth.guard.ts within the src/app/core folder, and then copy-paste the following code. Protects secured routes by checking the login state; if the user isn’t logged in, it redirects to the login page and preserves the original URL using returnUrl so the flow continues after login.
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) return true;
router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
return false;
};
Product Resolver
Create a TypeScript class named product.resolver.ts within the src/app/core folder, and then copy-paste the following code. Loads the product using the route param id before the product details page opens, so the details screen renders with data already available and can safely redirect if the product is not found.
import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { ProductService } from '../services/product.service';
import { Product } from '../models/product';
export const productResolver: ResolveFn<Product | null> = (route) => {
const api = inject(ProductService);
const id = Number(route.paramMap.get('id'));
return api.getById(id);
};
Step 9 – Configuring Application Routing
Routing defines how users move between pages. Public routes such as Products, Product Details, and Cart are accessible without login, while protected routes require authentication. This setup reflects real-world e-commerce behaviour and improves usability. So, open src/app/app.routes.ts, and then copy-paste the following code.
import { Routes } from '@angular/router';
import { Login } from './pages/login/login';
import { Products } from './pages/products/products';
import { ProductDetails } from './pages/product-details/product-details';
import { Cart } from './pages/cart/cart';
import { Checkout } from './pages/checkout/checkout';
import { NotFound } from './pages/not-found/not-found';
import { OrderSuccess } from './pages/order-success/order-success';
import { authGuard } from './core/auth.guard';
import { productResolver } from './core/product.resolver';
export const routes: Routes = [
// Default landing page should be Products (public)
{ path: '', redirectTo: 'products', pathMatch: 'full' },
{ path: 'login', component: Login },
// PUBLIC routes (no guard)
{ path: 'products', component: Products },
{
path: 'products/:id',
component: ProductDetails,
resolve: { product: productResolver }
},
{ path: 'cart', component: Cart },
// PROTECTED routes (guard required)
{ path: 'checkout', component: Checkout, canActivate: [authGuard] },
{ path: 'order-success', component: OrderSuccess, canActivate: [authGuard] },
{ path: '**', component: NotFound }
];
Step 10: Provide Router Using App Configuration
Register routing configuration using Angular’s application config approach. This enables routing to work in a standalone component setup. This is the “wiring” that connects your route definitions to the application. So, open src/app/app.config.ts, and then copy-paste the following code.
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)]
};
Step 11: Start the App Using main.ts
Bootstraps the application using bootstrapApplication, which starts the Angular app using your root component and configuration. This is the modern Angular way to start standalone apps. So, open src/app/main.ts, and then copy-paste the following code.
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));
Step 12: Building the Root Component and Layout
The root component defines the global layout, including the navigation bar. The navbar dynamically changes based on login state — showing Login for guests and Logout with the user’s name for authenticated users. The cart count is also displayed in real time.
Root Component
Open src/app/app.ts, and then copy-paste the following code. This is the root component class that powers the top navbar. It reads the login state and customer name from AuthService, the cart count from CartService, and exposes helper methods such as isLoggedIn(), customerName(), cartCount(), and logout() so the UI can react instantly.
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink, RouterLinkActive, Router } from '@angular/router';
import { CommonModule } from '@angular/common';
import { AuthService } from './services/auth.service';
import { CartService } from './services/cart.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive],
templateUrl: './app.html'
})
export class App {
constructor(
private auth: AuthService,
private cart: CartService,
private router: Router
) {}
customerName(): string {
return this.auth.getCustomer()?.name ?? '';
}
isLoggedIn(): boolean {
return this.auth.isLoggedIn();
}
cartCount(): number {
return this.cart.summary().totalItems;
}
logout(): void {
this.auth.logout();
this.router.navigate(['/products']);
}
}
Root Template
Open src/app/app.html, and then copy-paste the following code. This is the main layout template that appears on every page. It contains the Bootstrap navbar and a <router-outlet> where page components load, and it uses Angular control flow (@if/@else) to show Login or Hi, Name + Logout.
<nav class="navbar navbar-expand-lg bg-dark navbar-dark sticky-top shadow-sm">
<div class="container">
<a class="navbar-brand fw-semibold" routerLink="/products">MiniCart</a>
<button
class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#nav" aria-controls="nav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div id="nav" class="collapse navbar-collapse">
<!-- Always visible: Products + Cart -->
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" routerLink="/products" routerLinkActive="active">Products</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="/cart" routerLinkActive="active">
Cart <span class="badge bg-warning text-dark ms-1">{{ cartCount() }}</span>
</a>
</li>
</ul>
<!-- Use Angular control flow (@if/@else) -->
<div class="d-flex align-items-center gap-2">
@if (isLoggedIn()) {
<span class="text-light small">Hi, {{ customerName() }}</span>
<button class="btn btn-outline-light btn-sm" (click)="logout()">
Logout
</button>
} @else {
<a class="btn btn-outline-light btn-sm" routerLink="/login">
Login
</a>
}
</div>
</div>
</div>
</nav>
<div class="container my-4">
<router-outlet></router-outlet>
</div>
Step 13: Create Login Flow
The login page simulates authentication using a name and an email address. Once logged in, user information is stored locally. If the user previously attempted to access a protected route, they are redirected back after a successful login.
Login Component
Open src/app/pages/login/login.ts, and then copy-paste the following code. This component handles the application’s customer login functionality. It collects the user’s name and email, performs basic validation, and stores the login information using the authentication service. After successful login, the user is redirected either to the originally requested page or to the products page.
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'app-login',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './login.html'
})
export class Login {
name = '';
email = '';
error = '';
constructor(
private auth: AuthService,
private router: Router,
private route: ActivatedRoute
) {}
ngOnInit(): void {
if (this.auth.isLoggedIn()) {
this.router.navigate(['/products']);
}
}
login(): void {
this.error = '';
const nm = this.name.trim();
const em = this.email.trim().toLowerCase();
if (!nm || !em) {
this.error = 'Please enter your Name and Email.';
return;
}
// demo login
this.auth.login({ name: nm, email: em });
const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl');
this.router.navigateByUrl(returnUrl || '/products');
}
}
Login Template
Open src/app/pages/login/login.html, and then copy-paste the following code. This template provides a simple, clean login form for users. It contains input fields for name and email, displays validation errors when required, and includes a login button to initiate authentication.
<div class="row justify-content-center">
<div class="col-12 col-md-7 col-lg-5">
<div class="card shadow-sm">
<div class="card-body p-4">
<h2 class="mb-1">Customer Login</h2>
<p class="text-muted mb-4">Login to browse products and place orders (Demo login).</p>
@if (error) {
<div class="alert alert-danger">{{ error }}</div>
}
<div class="mb-3">
<label class="form-label">Full Name</label>
<input class="form-control" [(ngModel)]="name" placeholder="e.g., Pranaya Rout" />
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input class="form-control" [(ngModel)]="email" placeholder="e.g., pranaya@example.com" />
</div>
<div class="d-grid">
<button class="btn btn-primary" (click)="login()">Login</button>
</div>
</div>
</div>
</div>
</div>
Step 14: Build the Product Listing with Search, Filter, and Sort
This page displays all available products with search, category filtering, and sorting options. Users can view product details or add items directly to the cart. Query parameters are used to preserve filter state in the URL.
Product Component
Open src/app/pages/products/products.ts, and then copy-paste the following code. This component displays the list of available products. It fetches product data from the product service, applies search, category filtering, and sorting using query parameters, and allows users to add products directly to the cart.
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { ProductService } from '../../services/product.service';
import { Product, ProductCategory } from '../../models/product';
import { CartService } from '../../services/cart.service';
@Component({
selector: 'app-products',
standalone: true,
imports: [CommonModule, FormsModule, RouterLink],
templateUrl: './products.html'
})
export class Products implements OnInit {
all: Product[] = [];
view: Product[] = [];
q = '';
cat: ProductCategory | 'All' = 'All';
sort: 'ratingDesc' | 'priceAsc' | 'priceDesc' = 'ratingDesc';
// Popup state (Angular-controlled)
isCartPopupOpen = false;
popupTitle = 'Added to Cart';
popupMessage = '';
constructor(
private api: ProductService,
private cart: CartService,
private route: ActivatedRoute,
private router: Router
) {}
ngOnInit(): void {
this.all = this.api.getAll();
this.route.queryParamMap.subscribe(qp => {
this.q = qp.get('q') ?? '';
this.cat = (qp.get('cat') as any) ?? 'All';
this.sort = (qp.get('sort') as any) ?? 'ratingDesc';
this.apply();
});
}
apply(): void {
const keyword = this.q.trim().toLowerCase();
let items = keyword
? this.all.filter(p => (p.name + ' ' + p.brand).toLowerCase().includes(keyword))
: [...this.all];
if (this.cat !== 'All') items = items.filter(p => p.category === this.cat);
if (this.sort === 'priceAsc') items.sort((a, b) => a.price - b.price);
if (this.sort === 'priceDesc') items.sort((a, b) => b.price - a.price);
if (this.sort === 'ratingDesc') items.sort((a, b) => b.rating - a.rating);
this.view = items;
}
updateQueryParams(): void {
this.router.navigate([], {
relativeTo: this.route,
queryParams: {
q: this.q || null,
cat: this.cat !== 'All' ? this.cat : null,
sort: this.sort || null
},
queryParamsHandling: 'merge'
});
}
// Add-to-cart stays on page + opens center popup
addQuick(p: Product): void {
this.cart.add(p, 1);
this.popupTitle = 'Added to Cart';
this.popupMessage = `"${p.name}" has been added to your cart successfully.`;
this.isCartPopupOpen = true;
}
closeCartPopup(): void {
this.isCartPopupOpen = false;
}
goToCartFromPopup(): void {
this.isCartPopupOpen = false;
this.router.navigate(['/cart']);
}
}
Product Template
Open src/app/pages/products/products.html, and then copy-paste the following code. This template renders the product listing page using Bootstrap cards. It shows product details such as name, price, discount, rating, and stock status, along with filtering controls and buttons to view details or add items to the cart.
<!-- Added-to-cart message -->
<!-- @if (addedMsg) {
<div class="alert alert-success shadow-sm d-flex justify-content-between align-items-center">
<div class="fw-semibold">{{ addedMsg }}</div>
<div class="d-flex gap-2">
<a class="btn btn-sm btn-outline-success" routerLink="/cart">View Cart</a>
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="addedMsg=''">Dismiss</button>
</div>
</div>
} -->
<!-- Header -->
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div>
<h2 class="mb-1">Products</h2>
<div class="text-muted">Browse products, view details, and add to cart.</div>
</div>
<div class="d-flex gap-2">
<a class="btn btn-outline-primary" routerLink="/cart">
<span class="me-1">🛒</span> Go to Cart
</a>
</div>
</div>
<!-- Filters -->
<div class="card shadow-sm mb-3">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-12 col-lg-5">
<label class="form-label">Search (Showing <b>{{ view.length }}</b> Product(s))</label>
<div class="input-group">
<span class="input-group-text">🔎</span>
<input class="form-control"
[(ngModel)]="q"
(input)="updateQueryParams()"
placeholder="Search by product name or brand..." />
</div>
</div>
<div class="col-12 col-md-6 col-lg-3">
<label class="form-label">Category</label>
<select class="form-select" [(ngModel)]="cat" (change)="updateQueryParams()">
<option value="All">All</option>
<option value="Mobiles">Mobiles</option>
<option value="Laptops">Laptops</option>
<option value="Shoes">Shoes</option>
<option value="Books">Books</option>
</select>
</div>
<div class="col-12 col-md-6 col-lg-3">
<label class="form-label">Sort</label>
<select class="form-select" [(ngModel)]="sort" (change)="updateQueryParams()">
<option value="ratingDesc">Rating: High to Low</option>
<option value="priceAsc">Price: Low to High</option>
<option value="priceDesc">Price: High to Low</option>
</select>
</div>
<div class="col-12 col-lg-1 d-grid">
<button class="btn btn-sm btn-outline-primary"
type="button"
(click)="q=''; cat='All'; sort='ratingDesc'; updateQueryParams(); apply();">
Clear Filters
</button>
</div>
</div>
</div>
</div>
<!-- Grid -->
<div class="row g-3">
@for (p of view; track p.id) {
<div class="col-12 col-md-6 col-xl-4">
<div class="card h-100 shadow-sm overflow-hidden">
<!-- Image with fixed height for uniform cards -->
<div class="bg-light" style="height: 180px; overflow: hidden;">
<img [src]="p.imageUrl"
alt="product"
style="width:100%; height:180px; object-fit:cover;" />
</div>
<div class="card-body">
<!-- Badges row -->
<div class="d-flex flex-wrap gap-2 mb-2">
<span class="badge text-bg-dark">{{ p.category }}</span>
@if (p.discountPercent > 0) {
<span class="badge text-bg-success">-{{ p.discountPercent }}% OFF</span>
}
@if (p.inStock) {
<span class="badge text-bg-primary">In Stock</span>
} @else {
<span class="badge text-bg-secondary">Out of Stock</span>
}
</div>
<!-- Title -->
<div class="d-flex justify-content-between align-items-start gap-2">
<div>
<h5 class="mb-1">{{ p.name }}</h5>
<div class="text-muted small">{{ p.brand }} • SKU: {{ p.sku }}</div>
</div>
<!-- Rating chip -->
<span class="badge rounded-pill text-bg-warning text-dark">
★ {{ p.rating }}
</span>
</div>
<!-- Price -->
<div class="mt-2">
<span class="fw-bold fs-5">{{ p.price | currency:'INR':'symbol':'1.0-0' }}</span>
<span class="text-muted ms-2 text-decoration-line-through">
{{ p.mrp | currency:'INR':'symbol':'1.0-0' }}
</span>
</div>
<!-- Delivery info -->
<div class="small text-muted mt-2">
<span class="me-2">🚚 ETA: <b>{{ p.deliveryDays }}</b> day(s)</span>
<span>
@if (p.freeDelivery) { Free Delivery } @else { Delivery Charges Apply }
</span>
</div>
<hr class="my-3" />
<!-- Actions -->
<div class="d-flex gap-2">
<a class="btn btn-primary w-100" [routerLink]="['/products', p.id]">View Details</a>
<button class="btn btn-outline-success"
style="min-width: 120px;"
[disabled]="!p.inStock"
(click)="addQuick(p)">
+ Cart
</button>
</div>
</div>
</div>
</div>
} @empty {
<div class="col-12">
<div class="alert alert-warning shadow-sm">No products found.</div>
</div>
}
</div>
<!-- Angular Controlled Modal Popup -->
@if (isCartPopupOpen) {
<!-- Backdrop -->
<div class="modal-backdrop fade show"></div>
<!-- Modal -->
<div class="modal fade show d-block" tabindex="-1" role="dialog" aria-modal="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content shadow">
<div class="modal-header">
<h5 class="modal-title">{{ popupTitle }}</h5>
<button type="button" class="btn-close" aria-label="Close" (click)="closeCartPopup()"></button>
</div>
<div class="modal-body">
<div class="d-flex gap-3 align-items-start">
<div class="text-success" style="font-size: 38px; line-height: 1;">
<i class="bi bi-check-circle-fill"></i>
</div>
<div>
<div class="fw-semibold">Item added successfully</div>
<div class="text-muted small">{{ popupMessage }}</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-primary" (click)="closeCartPopup()">
Continue Shopping
</button>
<button type="button" class="btn btn-outline-success" (click)="goToCartFromPopup()">
Go to Cart
</button>
</div>
</div>
</div>
</div>
}
Step 15: Build Product Details with Route Params + Resolver
The product details page shows comprehensive information, including pricing, discounts, delivery details, highlights, and stock availability. Users can add items to the cart or proceed directly to checkout using the “Buy Now” option.
Product Details Component
Open src/app/pages/product-details/product-details.ts, and then copy-paste the following code. This component displays detailed information for a selected product. It receives product data using a route resolver, allows users to select quantity, add the product to the cart, or proceed directly to checkout.
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { Product } from '../../models/product';
import { CartService } from '../../services/cart.service';
@Component({
selector: 'app-product-details',
standalone: true,
imports: [CommonModule, RouterLink, FormsModule],
templateUrl: './product-details.html'
})
export class ProductDetails implements OnInit {
product: Product | null = null;
qty = 1;
addedMsg = '';
constructor(
private route: ActivatedRoute,
private router: Router,
private cart: CartService
) {}
ngOnInit(): void {
this.route.data.subscribe(d => {
this.product = d['product'] ?? null;
if (!this.product) this.router.navigate(['/not-found']);
});
}
// Add-to-cart: stay here + message
addToCart(): void {
if (!this.product) return;
this.cart.add(this.product, this.qty);
this.addedMsg = `Added "${this.product.name}" (Qty: ${this.qty}) to cart.`;
setTimeout(() => (this.addedMsg = ''), 2500);
}
// Buy Now: add then go checkout
buyNow(): void {
if (!this.product) return;
this.cart.add(this.product, this.qty);
this.router.navigate(['/checkout']);
}
}
Product Details Template
Open src/app/pages/product-details/product-details.html, and then copy-paste the following code. This template presents full product information, including images, pricing, delivery details, highlights, and warranty. It also includes action buttons to add the product to the cart or buy it immediately.
@if (product) {
<!-- Added to cart message -->
@if (addedMsg) {
<div class="alert alert-success shadow-sm d-flex justify-content-between align-items-center">
<div>
<div class="fw-semibold">{{ addedMsg }}</div>
<div class="small text-muted">You can continue shopping or view your cart.</div>
</div>
<div class="d-flex gap-2">
<a class="btn btn-sm btn-outline-success" routerLink="/cart">View Cart</a>
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="addedMsg=''">Dismiss</button>
</div>
</div>
}
<!-- Header -->
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div>
<h2 class="mb-1">{{ product.name }}</h2>
<div class="text-muted">
{{ product.brand }} • SKU: <span class="fw-semibold">{{ product.sku }}</span>
</div>
</div>
<div class="d-flex gap-2">
<a class="btn btn-outline-primary" routerLink="/products">← Back to Products</a>
<a class="btn btn-outline-success" routerLink="/cart">Go to Cart</a>
</div>
</div>
<div class="row g-4">
<!-- LEFT: Image -->
<div class="col-12 col-lg-5">
<div class="card shadow-sm">
<div class="p-3">
<div class="bg-light border rounded overflow-hidden product-img">
<img [src]="product.imageUrl"
alt="product"
style="width:100%; height:100%; object-fit:cover;" />
</div>
<div class="d-flex flex-wrap gap-2 mt-3">
<span class="badge text-bg-dark">{{ product.category }}</span>
<span class="badge text-bg-warning text-dark">
★ {{ product.rating }}
</span>
@if (product.discountPercent > 0) {
<span class="badge text-bg-success">-{{ product.discountPercent }}% OFF</span>
}
@if (product.inStock) {
<span class="badge text-bg-primary">In Stock</span>
} @else {
<span class="badge text-bg-secondary">Out of Stock</span>
}
</div>
</div>
</div>
</div>
<!-- RIGHT: Core info + Actions -->
<div class="col-12 col-lg-7">
<div class="card shadow-sm">
<div class="card-body">
<!-- Price block -->
<div class="d-flex flex-wrap justify-content-between align-items-end gap-3">
<div>
<div class="d-flex flex-wrap align-items-end gap-2">
<div class="display-6 fw-bold mb-0">
{{ product.price | currency:'INR':'symbol':'1.0-0' }}
</div>
<div class="text-muted">
<span class="text-decoration-line-through">
{{ product.mrp | currency:'INR':'symbol':'1.0-0' }}
</span>
</div>
</div>
@if (product.discountPercent > 0) {
<div class="small text-success fw-semibold">
You save {{ (product.mrp - product.price) | currency:'INR':'symbol':'1.0-0' }}
</div>
}
</div>
<!-- Stock qty -->
<div class="text-muted">
@if (product.inStock) {
<span class="badge text-bg-primary">Available: {{ product.stockQty }}</span>
} @else {
<span class="badge text-bg-secondary">Currently Unavailable</span>
}
</div>
</div>
<!-- Key info chips -->
<div class="d-flex flex-wrap gap-2 mt-3">
<span class="badge rounded-pill text-bg-light border text-dark">
🚚 Delivery: {{ product.deliveryDays }} day(s)
</span>
<span class="badge rounded-pill text-bg-light border text-dark">
@if (product.freeDelivery) { Free Delivery } @else { Delivery Charges Apply }
</span>
<span class="badge rounded-pill text-bg-light border text-dark">
🛡 Warranty: {{ product.warrantyMonths }} month(s)
</span>
<span class="badge rounded-pill text-bg-light border text-dark">
🏪 Seller: {{ product.sellerName }}
</span>
</div>
<!-- Actions row -->
<div class="card border-0 bg-light mt-4">
<div class="card-body">
<div class="d-flex flex-wrap gap-2 align-items-end">
<div style="width: 160px;">
<label class="form-label mb-1">Quantity</label>
<select class="form-select" [(ngModel)]="qty">
<option [ngValue]="1">1</option>
<option [ngValue]="2">2</option>
<option [ngValue]="3">3</option>
<option [ngValue]="4">4</option>
</select>
</div>
<button class="btn btn-success"
(click)="addToCart()"
[disabled]="!product.inStock">
Add to Cart
</button>
<button class="btn btn-primary"
(click)="buyNow()"
[disabled]="!product.inStock">
Buy Now
</button>
<a class="btn btn-outline-primary ms-auto" routerLink="/cart">
View Cart
</a>
</div>
<div class="small text-muted mt-2">
Add to Cart keeps you here. Buy Now takes you to checkout.
</div>
</div>
</div>
<!-- Tabs: Details on demand (professional + reduces forced scroll) -->
<ul class="nav nav-tabs mt-4" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabDesc" type="button" role="tab">
Description
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabHighlights" type="button" role="tab">
Highlights
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabDelivery" type="button" role="tab">
Delivery & Warranty
</button>
</li>
</ul>
<div class="tab-content border border-top-0 rounded-bottom p-3 bg-white">
<!-- Description -->
<div class="tab-pane fade show active" id="tabDesc" role="tabpanel">
<p class="text-muted mb-0">
{{ product.description }}
</p>
</div>
<!-- Highlights -->
<div class="tab-pane fade" id="tabHighlights" role="tabpanel">
<div class="row g-2 text-muted">
@for (h of product.highlights; track h) {
<div class="col-12 col-md-6">
<div class="d-flex gap-2">
<span class="text-success fw-bold">✓</span>
<span>{{ h }}</span>
</div>
</div>
}
</div>
</div>
<!-- Delivery & Warranty -->
<div class="tab-pane fade" id="tabDelivery" role="tabpanel">
<div class="text-muted">
<div class="mb-2"><b>Delivery ETA:</b> {{ product.deliveryDays }} day(s)</div>
<div class="mb-2">
<b>Delivery:</b>
@if (product.freeDelivery) { Free Delivery } @else { Delivery Charges Apply }
</div>
<div class="mb-2"><b>Warranty:</b> {{ product.warrantyMonths }} month(s)</div>
<div><b>Seller:</b> {{ product.sellerName }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
Step 16: Build the Cart Page with Quantity Controls + Summary
The cart page allows users to review selected items, adjust quantities, remove products, and view a calculated order summary. This page remains accessible even without a login, allowing users to prepare their cart before checkout.
Cart Component
Open src/app/pages/cart/cart.ts, and then copy-paste the following code. This component manages the shopping cart functionality. It retrieves cart items from the cart service, allows quantity adjustments, item removal, and calculates the order summary before navigating to checkout.
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, RouterLink } from '@angular/router';
import { CartService, CartSummary } from '../../services/cart.service';
import { CartItem } from '../../models/cart-item';
@Component({
selector: 'app-cart',
standalone: true,
imports: [CommonModule, RouterLink],
templateUrl: './cart.html'
})
export class Cart implements OnInit {
items: CartItem[] = [];
sum!: CartSummary;
constructor(private cart: CartService, private router: Router) {}
ngOnInit(): void {
this.refresh();
}
refresh(): void {
this.items = this.cart.getCart();
this.sum = this.cart.summary();
}
inc(id: number): void { this.cart.increase(id); this.refresh(); }
dec(id: number): void { this.cart.decrease(id); this.refresh(); }
remove(id: number): void { this.cart.remove(id); this.refresh(); }
clear(): void { this.cart.clear(); this.refresh(); }
lineTotal(i: CartItem): number {
return i.unitPrice * i.qty;
}
goCheckout(): void {
if (this.items.length === 0) return;
this.router.navigate(['/checkout']);
}
}
Cart Template
Open src/app/pages/cart/cart.html, and then copy-paste the following code. This template displays all items added to the cart in a structured layout. It shows product details, quantity controls, pricing breakdown, savings, and a clear button to proceed to checkout or continue shopping.
<!-- Header -->
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div>
<h2 class="mb-1">Your Cart</h2>
<div class="text-muted">
Review items, update quantity, and checkout securely.
@if (items.length > 0) {
<span class="ms-2 small">({{ sum.totalItems }} item(s))</span>
}
</div>
</div>
<div class="d-flex gap-2">
<a class="btn btn-outline-primary" routerLink="/products">← Continue Shopping</a>
<button class="btn btn-outline-danger"
(click)="clear()"
[disabled]="items.length === 0">
Clear Cart
</button>
</div>
</div>
@if (items.length === 0) {
<!-- Empty state -->
<div class="card shadow-sm">
<div class="card-body text-center p-5">
<div class="display-6 mb-2">🛒</div>
<h4 class="mb-2">Your cart is empty</h4>
<p class="text-muted mb-4">Looks like you haven’t added anything yet.</p>
<a class="btn btn-primary" routerLink="/products">Browse Products</a>
</div>
</div>
} @else {
<div class="row g-3">
<!-- LEFT: Items -->
<div class="col-12 col-lg-8">
@for (i of items; track i.productId) {
<div class="card shadow-sm mb-3">
<div class="card-body">
<div class="d-flex gap-3 align-items-start">
<!-- Image -->
<div class="bg-light border rounded overflow-hidden"
style="width: 96px; height: 96px;">
<img [src]="i.imageUrl"
alt="item"
style="width:100%; height:100%; object-fit:cover;" />
</div>
<!-- Details -->
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-start gap-2">
<div>
<div class="fw-semibold fs-5">{{ i.name }}</div>
<div class="text-muted small">{{ i.brand }} • SKU: {{ i.sku }}</div>
</div>
<!-- Remove -->
<button class="btn btn-sm btn-outline-danger"
title="Remove item"
(click)="remove(i.productId)">
Remove
</button>
</div>
<!-- Price row -->
<div class="d-flex flex-wrap align-items-center gap-2 mt-2">
<div class="fw-bold">
{{ i.unitPrice | currency:'INR':'symbol':'1.0-0' }}
</div>
<div class="text-muted small text-decoration-line-through">
{{ i.mrp | currency:'INR':'symbol':'1.0-0' }}
</div>
@if (i.discountPercent > 0) {
<span class="badge text-bg-success">-{{ i.discountPercent }}%</span>
}
<div class="ms-auto text-muted small">
Line Total:
<b>{{ lineTotal(i) | currency:'INR':'symbol':'1.0-0' }}</b>
</div>
</div>
<!-- Qty -->
<div class="d-flex flex-wrap align-items-center gap-2 mt-3">
<div class="text-muted small">Qty</div>
<div class="btn-group" role="group" aria-label="Quantity">
<button class="btn btn-outline-success btn-sm" (click)="dec(i.productId)">−</button>
<button class="btn btn-primary btn-sm" disabled style="min-width:44px;">
{{ i.qty }}
</button>
<button class="btn btn-outline-success btn-sm" (click)="inc(i.productId)">+</button>
</div>
<div class="text-muted small ms-2">
Added: {{ i.addedAt | date:'medium' }}
</div>
</div>
</div>
</div>
</div>
</div>
}
</div>
<!-- RIGHT: Summary -->
<div class="col-12 col-lg-4">
<div class="card shadow-sm">
<div class="card-body">
<h5 class="mb-3">Order Summary</h5>
<div class="d-flex justify-content-between">
<span class="text-muted">MRP Subtotal</span>
<span>{{ sum.subtotalMrp | currency:'INR':'symbol':'1.0-0' }}</span>
</div>
<div class="d-flex justify-content-between mt-2">
<span class="text-muted">Discount</span>
<span class="text-success">
-{{ sum.discount | currency:'INR':'symbol':'1.0-0' }}
</span>
</div>
<div class="d-flex justify-content-between mt-2">
<span class="text-muted">Subtotal</span>
<span>{{ sum.subtotalPayable | currency:'INR':'symbol':'1.0-0' }}</span>
</div>
<div class="d-flex justify-content-between mt-2">
<span class="text-muted">Shipping</span>
<span>
@if (sum.shipping === 0) { <b>Free</b> }
@else { {{ sum.shipping | currency:'INR':'symbol':'1.0-0' }} }
</span>
</div>
<hr />
<div class="d-flex justify-content-between align-items-center">
<span class="fw-semibold">Grand Total</span>
<span class="fw-bold fs-5">
{{ sum.grandTotal | currency:'INR':'symbol':'1.0-0' }}
</span>
</div>
<div class="small text-success mt-2">
You saved {{ sum.discount | currency:'INR':'symbol':'1.0-0' }} on this order
</div>
<div class="d-grid mt-3">
<button class="btn btn-success" (click)="goCheckout()">Proceed to Checkout</button>
</div>
</div>
</div>
</div>
</div>
}
Step 17: Checkout Page
The checkout page is protected by authentication. Users must log in before accessing it. This page collects delivery details, validates input, and displays a final order summary before placing the order.
Checkout Component
Open src/app/pages/checkout/checkout.ts, and then copy-paste the following code. This component handles the checkout process for authenticated users. It collects delivery address details, validates user input, prepares order information, clears the cart after successful submission, and navigates to the order success page.
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { CartService, CartSummary } from '../../services/cart.service';
import { AuthService } from '../../services/auth.service';
import { CartItem } from '../../models/cart-item';
type PaymentMode = 'COD';
@Component({
selector: 'app-checkout',
standalone: true,
imports: [CommonModule, FormsModule, RouterLink],
templateUrl: './checkout.html'
})
export class Checkout implements OnInit {
items: CartItem[] = [];
sum!: CartSummary;
// Address (simple + practical)
fullName = '';
phone = '';
addressLine = '';
city = '';
state = '';
pincode = '';
paymentMode: PaymentMode = 'COD';
error = '';
placing = false;
constructor(
private cart: CartService,
private auth: AuthService,
private router: Router
) {}
ngOnInit(): void {
this.items = this.cart.getCart();
this.sum = this.cart.summary();
// If cart is empty, go back
if (this.items.length === 0) this.router.navigate(['/cart']);
// Prefill name
const customer = this.auth.getCustomer();
if (customer?.name) this.fullName = customer.name;
}
private isValid(): boolean {
const nm = this.fullName.trim();
const ph = this.phone.trim();
const ad = this.addressLine.trim();
const ct = this.city.trim();
const st = this.state.trim();
const pc = this.pincode.trim();
if (!nm || !ph || !ad || !ct || !st || !pc) {
this.error = 'Please fill all address fields.';
return false;
}
// very basic validations (demo)
if (!/^\d{10}$/.test(ph)) {
this.error = 'Phone number must be 10 digits.';
return false;
}
if (!/^\d{6}$/.test(pc)) {
this.error = 'Pincode must be 6 digits.';
return false;
}
return true;
}
placeOrder(): void {
this.error = '';
if (this.items.length === 0) {
this.error = 'Your cart is empty.';
return;
}
if (!this.isValid()) return;
this.placing = true;
// Create a simple order id for demo
const orderId = 'ORD-' + Math.floor(100000 + Math.random() * 900000);
// Prepare data to show on success page
const successState = {
orderId,
placedAtIso: new Date().toISOString(),
totalItems: this.sum.totalItems,
grandTotal: this.sum.grandTotal,
fullName: this.fullName.trim(),
phone: this.phone.trim(),
addressLine: this.addressLine.trim(),
city: this.city.trim(),
state: this.state.trim(),
pincode: this.pincode.trim(),
paymentMode: 'COD' as const
};
// Clear cart AFTER preparing summary
this.cart.clear();
// Redirect to success page with navigation state
this.router.navigate(['/order-success'], { state: successState });
}
}
Checkout Template
Open src/app/pages/checkout/checkout.html, and then copy-paste the following code. This template displays the checkout form and order summary side by side. It allows users to enter delivery information, select a payment method, review the final price, and place the order securely.
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div>
<h2 class="mb-1">Checkout</h2>
<div class="text-muted">Delivery address + COD payment</div>
</div>
<a class="btn btn-outline-primary" routerLink="/cart">← Back to Cart</a>
</div>
@if (error) {
<div class="alert alert-danger shadow-sm py-2">{{ error }}</div>
}
<div class="row g-3">
<!-- LEFT: Address + Payment -->
<div class="col-12 col-lg-7">
<div class="card shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">Delivery Address</h5>
<span class="badge text-bg-light border text-dark">Step 1 of 2</span>
</div>
<!-- Compact form using floating labels -->
<div class="row g-2">
<div class="col-12 col-md-6">
<div class="form-floating">
<input class="form-control" id="fullName" [(ngModel)]="fullName" placeholder="Full Name" />
<label for="fullName">Full Name</label>
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-floating">
<input class="form-control" id="phone" [(ngModel)]="phone" placeholder="Phone" />
<label for="phone">Phone (10 digits)</label>
</div>
</div>
<div class="col-12">
<div class="form-floating">
<textarea class="form-control" id="addressLine" rows="2"
style="height: 70px;"
[(ngModel)]="addressLine"
placeholder="Address"></textarea>
<label for="addressLine">Address (House no, street, landmark)</label>
</div>
</div>
<div class="col-12 col-md-4">
<div class="form-floating">
<input class="form-control" id="city" [(ngModel)]="city" placeholder="City" />
<label for="city">City</label>
</div>
</div>
<div class="col-12 col-md-4">
<div class="form-floating">
<input class="form-control" id="state" [(ngModel)]="state" placeholder="State" />
<label for="state">State</label>
</div>
</div>
<div class="col-12 col-md-4">
<div class="form-floating">
<input class="form-control" id="pincode" [(ngModel)]="pincode" placeholder="Pincode" />
<label for="pincode">Pincode (6 digits)</label>
</div>
</div>
</div>
<hr class="my-3" />
<!-- Payment -->
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">Payment</h5>
<span class="badge text-bg-light border text-dark">Step 2 of 2</span>
</div>
<div class="card border-0 bg-light">
<div class="card-body py-2">
<div class="form-check">
<input class="form-check-input" type="radio" id="cod" [value]="'COD'" [(ngModel)]="paymentMode" />
<label class="form-check-label fw-semibold" for="cod">
Cash on Delivery (COD)
</label>
</div>
<div class="small text-muted mt-1">
COD only for this demo app.
</div>
</div>
</div>
</div>
</div>
</div>
<!-- RIGHT: Summary -->
<div class="col-12 col-lg-5">
<div class="card shadow-sm checkout-summary">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">Order Summary</h5>
<span class="small text-muted">{{ sum.totalItems }} item(s)</span>
</div>
<div class="d-flex justify-content-between">
<span class="text-muted">MRP Subtotal</span>
<span>{{ sum.subtotalMrp | currency:'INR':'symbol':'1.0-0' }}</span>
</div>
<div class="d-flex justify-content-between mt-2">
<span class="text-muted">Discount</span>
<span class="text-success">-{{ sum.discount | currency:'INR':'symbol':'1.0-0' }}</span>
</div>
<div class="d-flex justify-content-between mt-2">
<span class="text-muted">Subtotal</span>
<span>{{ sum.subtotalPayable | currency:'INR':'symbol':'1.0-0' }}</span>
</div>
<div class="d-flex justify-content-between mt-2">
<span class="text-muted">Shipping</span>
<span>
@if (sum.shipping === 0) { <b>Free</b> }
@else { {{ sum.shipping | currency:'INR':'symbol':'1.0-0' }} }
</span>
</div>
<hr class="my-3" />
<div class="d-flex justify-content-between align-items-center">
<span class="fw-semibold">Grand Total</span>
<span class="fw-bold fs-5">{{ sum.grandTotal | currency:'INR':'symbol':'1.0-0' }}</span>
</div>
<div class="small text-success mt-2">
You saved {{ sum.discount | currency:'INR':'symbol':'1.0-0' }}
</div>
<div class="d-grid mt-3">
<button class="btn btn-success" (click)="placeOrder()" [disabled]="placing">
Place Order (COD)
</button>
</div>
</div>
</div>
</div>
</div>
Step 18: Order Success Page
After placing an order, users are redirected to the order success page. This page confirms the order placement and prevents direct access through refresh or manual URL entry, ensuring proper flow control.
Order Success Component
Open src/app/pages/order-success/order-success.ts, and then copy-paste the following code. This component represents the final confirmation page after an order is placed. It ensures that the page is accessed only through a valid checkout flow and redirects users if accessed directly.
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, RouterLink } from '@angular/router';
@Component({
selector: 'app-order-success',
standalone: true,
imports: [CommonModule, RouterLink],
templateUrl: './order-success.html'
})
export class OrderSuccess implements OnInit {
constructor(private router: Router) {}
ngOnInit(): void {
// If user refreshed or opened directly, state will be empty
const st = history.state;
if (!st || !st.orderId) {
this.router.navigate(['/products']);
}
}
}
Order Success Template
Open src/app/pages/order-success/order-success.html, and then copy-paste the following code. This template shows a confirmation message indicating that the order has been placed successfully. It provides navigation options to continue shopping or return to the cart.
<div class="card shadow-sm">
<div class="card-body text-center p-4 p-lg-5">
<div class="text-success mb-3">
<i class="bi bi-check-circle-fill" style="font-size: 72px;"></i>
</div>
<h2 class="mb-2">Order Placed Successfully</h2>
<p class="text-muted mb-4">
Thank you for your purchase. Your order has been confirmed.
</p>
<div class="d-flex flex-wrap justify-content-center gap-2">
<a class="btn btn-primary" routerLink="/products">Continue Shopping</a>
<a class="btn btn-outline-secondary" routerLink="/cart">Back to Cart</a>
</div>
</div>
</div>
Step 19: Not Found Page (Wildcard **)
A user-friendly 404 page is shown when an invalid route is accessed. This improves usability and provides navigation options to return to valid pages.
Not Found Component
Open src/app/pages/not-found/not-found.ts, and then copy-paste the following code. This component handles invalid or unknown routes in the application. It ensures users see a friendly message instead of a blank screen when they navigate to a nonexistent page.
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-not-found',
standalone: true,
imports: [RouterLink],
templateUrl: './not-found.html'
})
export class NotFound {}
Not Found Template
Open src/app/pages/not-found/not-found.html, and then copy-paste the following code. This template displays a simple 404 error message with clear navigation buttons that guide users back to valid pages, such as Products or Login.
<div class="text-center p-4 p-lg-5 bg-light rounded-3 border shadow-sm">
<h1 class="display-6 fw-bold mb-2">404 - Page Not Found</h1>
<p class="text-muted mb-4">The page you are looking for does not exist.</p>
<div class="d-flex justify-content-center gap-2">
<a class="btn btn-primary" routerLink="/products">Go to Products</a>
<a class="btn btn-outline-secondary" routerLink="/login">Login</a>
</div>
</div>
Conclusion
This MiniCart Real-Time E-Commerce Application, built with Angular, is more than just a demo project—it is a practical blueprint for real-world Angular applications. By completing this project, learners understand how to design clean navigation flows, manage application state, protect routes, and structure code for scalability.
This application bridges the gap between theoretical Angular concepts and real production scenarios, making it an excellent foundation for advanced topics such as API integration, JWT authentication, payment gateways, and backend communication.
