Added Shopping Cart
This commit is contained in:
parent
bcbad19601
commit
495ac6a8be
5
client/package-lock.json
generated
5
client/package-lock.json
generated
@ -24,6 +24,7 @@
|
||||
"ngx-toastr": "^14.3.0",
|
||||
"rxjs": "~7.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
"uuid": "^8.3.2",
|
||||
"xng-breadcrumb": "^7.2.0",
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
@ -11213,7 +11214,6 @@
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
@ -19945,8 +19945,7 @@
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
},
|
||||
"validate-npm-package-name": {
|
||||
"version": "3.0.0",
|
||||
|
@ -26,6 +26,7 @@
|
||||
"ngx-toastr": "^14.3.0",
|
||||
"rxjs": "~7.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
"uuid": "^8.3.2",
|
||||
"xng-breadcrumb": "^7.2.0",
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
|
@ -11,6 +11,7 @@ const routes: Routes = [
|
||||
{path: 'server-error', component: ServerErrorComponent, data: {breadcrumb: 'Server Error'}},
|
||||
{path: 'not-found', component: NotFoundComponent, data: {breadcrumb: 'Not Found'}},
|
||||
{path: 'shop', loadChildren: ()=> import('./shop/shop.module').then(mod => mod.ShopModule), data: {breadcrumb: 'Shop'}},
|
||||
{path: 'basket', loadChildren: ()=> import('./basket/basket.module').then(mod => mod.BasketModule), data: {breadcrumb: 'Shopping Cart'}},
|
||||
{path: '**', redirectTo: 'not-found', pathMatch: 'full'}
|
||||
];
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
<ngx-spinner>
|
||||
<ngx-spinner type="ball-clip-rotate-multiple">
|
||||
<h3>Loading...</h3>
|
||||
</ngx-spinner>
|
||||
<app-nav-bar></app-nav-bar>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { BasketService } from './basket/basket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@ -6,9 +7,18 @@ import { Component, OnInit } from '@angular/core';
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
title = 'E-Commerce';
|
||||
title = 'SkiNet';
|
||||
|
||||
constructor() {}
|
||||
constructor(private basketService: BasketService) {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
ngOnInit(): void {
|
||||
const basketId = localStorage.getItem('basket_id');
|
||||
if(basketId){
|
||||
this.basketService.getBasket(basketId).subscribe({
|
||||
next: () => { console.log('initialized basket') },
|
||||
error: (e: any) => { console.log(e) },
|
||||
complete: () => { console.log('Complete') }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
16
client/src/app/basket/basket-routing.module.ts
Normal file
16
client/src/app/basket/basket-routing.module.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { BasketComponent } from './basket.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{path: '', component: BasketComponent}
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
RouterModule.forChild(routes)
|
||||
],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class BasketRoutingModule { }
|
74
client/src/app/basket/basket.component.html
Normal file
74
client/src/app/basket/basket.component.html
Normal file
@ -0,0 +1,74 @@
|
||||
<div class="container mt-5">
|
||||
<div *ngIf="(basket$ | async) === null">
|
||||
<P>You're shopping cart is currently empty!</P>
|
||||
</div>
|
||||
<div *ngIf="basket$ | async">
|
||||
<div class="pb-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 py-3 mb-1">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover table-light">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<div class="p-2 px-3 text-uppercase">Product</div>
|
||||
</th>
|
||||
<th class="text-center" scope="col">
|
||||
<div class="p-2 px-3 text-uppercase">Price</div>
|
||||
</th>
|
||||
<th class="text-center" scope="col">
|
||||
<div class="p-2 px-3 text-uppercase">quantity</div>
|
||||
</th>
|
||||
<th class="text-center" scope="col">
|
||||
<div class="p-2 px-3 text-uppercase">Total</div>
|
||||
</th>
|
||||
<th class="text-center" scope="col">
|
||||
<div class="p-2 px-3 text-uppercase">Remove</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of (basket$ | async).items">
|
||||
<th scope="row">
|
||||
<div class="p-2">
|
||||
<img class="img-fluid" style="max-height: 50px" src="{{item.pictureUrl}}" alt="{{item.productName}}">
|
||||
<div class="ms-3 d-inline-block align-middle">
|
||||
<h5 class="mb-0">
|
||||
<a class="text-dark" routerLink="/shop/{{item.id}}">{{item.productName}}</a>
|
||||
</h5>
|
||||
<span class="text-muted font-weight-normal font-italic d-block">Type: {{item.type}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td class="align-middle text-center">{{item.price | currency}}</td>
|
||||
<td class="align-middle text-center">
|
||||
<div class="d-flex-align-items-center">
|
||||
<i class="fa fa-minus-circle text-warning me-2" style="cursor: pointer;"></i>
|
||||
<span class="font-weight-bold">{{item.quantity}}</span>
|
||||
<i class="fa fa-plus-circle text-warning mx-2" style="cursor: pointer;"></i>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle text-center">{{item.price * item.quantity | currency}}</td>
|
||||
<td class="align-middle text-center">
|
||||
<a class="text-danger" style="cursor: pointer">
|
||||
<i class="fa fa-trash" style="font-size: 2em"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 offset-6">
|
||||
<app-order-totals></app-order-totals>
|
||||
<a routerLink="/checkout" class="btn btn-outline-primary w-100">Checkout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
0
client/src/app/basket/basket.component.scss
Normal file
0
client/src/app/basket/basket.component.scss
Normal file
20
client/src/app/basket/basket.component.ts
Normal file
20
client/src/app/basket/basket.component.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { IBasket } from '../shared/models/baskset';
|
||||
import { BasketService } from './basket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-basket',
|
||||
templateUrl: './basket.component.html',
|
||||
styleUrls: ['./basket.component.scss']
|
||||
})
|
||||
export class BasketComponent implements OnInit {
|
||||
basket$: Observable<IBasket>;
|
||||
|
||||
constructor(private basketService: BasketService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.basket$ = this.basketService.basket$;
|
||||
}
|
||||
|
||||
}
|
19
client/src/app/basket/basket.module.ts
Normal file
19
client/src/app/basket/basket.module.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BasketComponent } from './basket.component';
|
||||
import { BasketRoutingModule } from './basket-routing.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
BasketComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
BasketRoutingModule,
|
||||
SharedModule
|
||||
]
|
||||
})
|
||||
export class BasketModule { }
|
93
client/src/app/basket/basket.service.ts
Normal file
93
client/src/app/basket/basket.service.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, map } from 'rxjs';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { Basket, IBasket, IBasketItem, IBasketTotals } from '../shared/models/baskset';
|
||||
import { IProduct } from '../shared/models/product';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class BasketService {
|
||||
baseUrl = environment.apiUrl;
|
||||
private basketSource = new BehaviorSubject<IBasket>(null);
|
||||
basket$ = this.basketSource.asObservable();
|
||||
private basketTotalSource = new BehaviorSubject<IBasketTotals>(null);
|
||||
basketTotal$ = this.basketTotalSource.asObservable();
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
getBasket(id: string){
|
||||
return this.http.get(this.baseUrl + 'basket?id=' + id).pipe(
|
||||
map((basket: IBasket) => {
|
||||
this.basketSource.next(basket);
|
||||
this.calculateTotals();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setBasket(basket: IBasket){
|
||||
return this.http.post(this.baseUrl + 'basket', basket).subscribe({
|
||||
next: (response: IBasket) => {
|
||||
this.basketSource.next(response);
|
||||
this.calculateTotals();
|
||||
},
|
||||
error: (e: any) => { console.log(e); },
|
||||
complete: () => { console.log('complete'); }
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentBasketValue(){
|
||||
return this.basketSource.value;
|
||||
}
|
||||
|
||||
addItemToBasket(item: IProduct, quantity = 1){
|
||||
const itemToAdd: IBasketItem = this.mapProductItemToBasketItem(item, quantity);
|
||||
const basket = this.getCurrentBasketValue() ?? this.createBasket();
|
||||
basket.items = this.addOrUpdateItem(basket.items, itemToAdd, quantity);
|
||||
this.setBasket(basket);
|
||||
}
|
||||
|
||||
private calculateTotals(){
|
||||
const basket = this.getCurrentBasketValue();
|
||||
const shipping = 0;
|
||||
const subtotal = basket.items.reduce((a, b) => (b.price * b.quantity) + a, 0);
|
||||
const total = subtotal + shipping;
|
||||
this.basketTotalSource.next({
|
||||
shipping,
|
||||
total,
|
||||
subtotal
|
||||
});
|
||||
}
|
||||
|
||||
private addOrUpdateItem(items: IBasketItem[], itemToAdd: IBasketItem, quantity: number): IBasketItem[] {
|
||||
const index = items.findIndex(i => i.id === itemToAdd.id);
|
||||
if(index === -1){
|
||||
itemToAdd.quantity = quantity;
|
||||
items.push(itemToAdd);
|
||||
} else {
|
||||
items[index].quantity += quantity;
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private createBasket(): IBasket {
|
||||
const basket = new Basket();
|
||||
localStorage.setItem('basket_id', basket.id);
|
||||
return basket;
|
||||
}
|
||||
|
||||
private mapProductItemToBasketItem(item: IProduct, quantity: number): IBasketItem {
|
||||
return {
|
||||
id: item.id,
|
||||
productName: item.name,
|
||||
price: item.price,
|
||||
pictureUrl: item.pictureUrl,
|
||||
quantity,
|
||||
brand: item.productBrand,
|
||||
type: item.productType
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
<div class="d-flex flex-column flex-md-row align-items-center justify-content-between p-3 px-md-4 mb-3 border-bottom border-dark fixed-top" style="background-color: #0d0d0e;">
|
||||
<div class="d-flex flex-column flex-md-row align-items-center justify-content-between bg-light p-3 px-md-4 mb-3 border-bottom border-dark fixed-top">
|
||||
<img class="logo" src="/assets/images/logo.png" style="max-height: 70px;" alt="logo" routerLink="/">
|
||||
<nav class="me-3 my-md-0 mr-md-3 text-uppercase" style="font-size: larger;">
|
||||
<a class="me-3 py-2" [routerLink]="['/']" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Home</a>
|
||||
@ -6,9 +6,9 @@
|
||||
<a class="me-3 py-2" routerLink="/test-error" routerLinkActive="active">Errors</a>
|
||||
</nav>
|
||||
<div class="d-flex align-items-center">
|
||||
<a class="position-relative">
|
||||
<a routerLink="/basket" class="position-relative">
|
||||
<i class="fa fa-shopping-cart fa-2x me-3 text-dark"></i>
|
||||
<div class="cart-no">5</div>
|
||||
<div *ngIf="(basket$ | async) as basket" class="cart-no">{{basket.items.length}}</div>
|
||||
</a>
|
||||
<a class="btn btn-outline-secondary me-3" href="#">Login</a>
|
||||
<a class="btn btn-outline-secondary me-3" href="#">Sign up</a>
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #343a40;
|
||||
color: white;
|
||||
|
||||
&.active {
|
||||
color: orange;
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BasketService } from 'src/app/basket/basket.service';
|
||||
import { IBasket } from 'src/app/shared/models/baskset';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nav-bar',
|
||||
@ -6,10 +9,12 @@ import { Component, OnInit } from '@angular/core';
|
||||
styleUrls: ['./nav-bar.component.scss']
|
||||
})
|
||||
export class NavBarComponent implements OnInit {
|
||||
basket$ : Observable<IBasket>
|
||||
|
||||
constructor() { }
|
||||
constructor(private basketService: BasketService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.basket$ = this.basketService.basket$;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<ng-container *ngIf="(breadcrumb$ | async) as breadcrumbs">
|
||||
<section *ngIf="breadcrumbs.length > 0 && breadcrumbs[breadcrumbs.length-1].label !== 'Home'" class="py-5" style="margin-top: 105px;">
|
||||
<section *ngIf="breadcrumbs.length > 0 && breadcrumbs[breadcrumbs.length-1].label !== 'Home'" class="py-5" style="background-color: rgb(43, 43, 43); margin-top: 105px;">
|
||||
<div class="container">
|
||||
<div class="row d-flex align-item-center">
|
||||
<div class="col-9">
|
||||
|
@ -0,0 +1,20 @@
|
||||
<div class="bg-light px-4 py-3 text-uppercase font-weight-bold">
|
||||
Order Summary
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<p class="font-italic mb-4">Shipping costs will be added during checkout</p>
|
||||
<ul class="list-unstyled mb-4" *ngIf="(basketTotal$ | async) as totals">
|
||||
<li class="d-flex justify-content-between py-3 border-bottom">
|
||||
<strong class="text-muted">Subtotal</strong>
|
||||
<strong>{{totals.subtotal | currency}}</strong>
|
||||
</li>
|
||||
<li class="d-flex justify-content-between py-3 border-bottom">
|
||||
<strong class="text-muted">Shipping and Handling</strong>
|
||||
<strong>{{totals.shipping | currency}}</strong>
|
||||
</li>
|
||||
<li class="d-flex justify-content-between py-3 border-bottom">
|
||||
<strong class="text-muted">Total</strong>
|
||||
<strong>{{totals.total | currency}}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
@ -0,0 +1,20 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BasketService } from 'src/app/basket/basket.service';
|
||||
import { IBasketTotals } from '../../models/baskset';
|
||||
|
||||
@Component({
|
||||
selector: 'app-order-totals',
|
||||
templateUrl: './order-totals.component.html',
|
||||
styleUrls: ['./order-totals.component.scss']
|
||||
})
|
||||
export class OrderTotalsComponent implements OnInit {
|
||||
basketTotal$: Observable<IBasketTotals>;
|
||||
|
||||
constructor(private basketService: BasketService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.basketTotal$ = this.basketService.basketTotal$;
|
||||
}
|
||||
|
||||
}
|
28
client/src/app/shared/models/baskset.ts
Normal file
28
client/src/app/shared/models/baskset.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export interface IBasketItem {
|
||||
id: number;
|
||||
productName: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
pictureUrl: string;
|
||||
brand: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IBasket {
|
||||
id: string;
|
||||
items: IBasketItem[];
|
||||
}
|
||||
|
||||
export class Basket implements IBasket {
|
||||
id = uuidv4();
|
||||
items: IBasketItem[] = [];
|
||||
|
||||
}
|
||||
|
||||
export interface IBasketTotals {
|
||||
shipping: number;
|
||||
subtotal: number;
|
||||
total: number;
|
||||
}
|
@ -4,13 +4,15 @@ import { PaginationModule } from 'ngx-bootstrap/pagination';
|
||||
import { CarouselModule } from 'ngx-bootstrap/carousel';
|
||||
import { PagingHeaderComponent } from './components/paging-header/paging-header.component';
|
||||
import { PagerComponent } from './components/pager/pager.component';
|
||||
import { OrderTotalsComponent } from './components/order-totals/order-totals.component';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
PagingHeaderComponent,
|
||||
PagerComponent
|
||||
PagerComponent,
|
||||
OrderTotalsComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -21,7 +23,8 @@ import { PagerComponent } from './components/pager/pager.component';
|
||||
PaginationModule,
|
||||
PagingHeaderComponent,
|
||||
PagerComponent,
|
||||
CarouselModule
|
||||
CarouselModule,
|
||||
OrderTotalsComponent
|
||||
]
|
||||
})
|
||||
export class SharedModule { }
|
||||
|
@ -10,7 +10,7 @@
|
||||
<i class="fa fa-minus-circle text-warning me-2" style="cursor: pointer; font-size: 2em"></i>
|
||||
<span class="font-weight-bold" style="font-size: 1.5em;">2</span>
|
||||
<i class="fa fa-plus-circle text-warning mx-2" style="cursor: pointer; font-size: 2em"></i>
|
||||
<button class="btn btn-outline-secondary btn-lg ms-4">Ad to Cart</button>
|
||||
<button class="btn btn-outline-secondary btn-lg ms-4">Add to Cart</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-5">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="image position-relative" style="cursor: pointer;">
|
||||
<img src="{{product.pictureUrl}}" alt="{{product.name}}" class="img-fluid bg-light">
|
||||
<div class="d-flex align-items-center justify-content-center hover-overlay">
|
||||
<button type="button" class="btn btn-sm btn-primary fa fa-shopping-cart me-2"></button>
|
||||
<button (click)="addItemToBasket()" type="button" class="btn btn-sm btn-primary fa fa-shopping-cart me-2"></button>
|
||||
<button routerLink="/shop/{{product.id}}" type="button" class="btn btn-sm btn-primary">View</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { BasketService } from 'src/app/basket/basket.service';
|
||||
import { IProduct } from 'src/app/shared/models/product';
|
||||
|
||||
@Component({
|
||||
@ -9,9 +10,13 @@ import { IProduct } from 'src/app/shared/models/product';
|
||||
export class ProductItemComponent implements OnInit {
|
||||
@Input() product: IProduct
|
||||
|
||||
constructor() { }
|
||||
constructor(private basketService: BasketService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
addItemToBasket(){
|
||||
this.basketService.addItemToBasket(this.product);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>E-Commerce</title>
|
||||
<title>SkiNet</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
|
@ -19,9 +19,9 @@ label.xng-breadcrumb-trail {
|
||||
|
||||
.xng-breadcrumb-separator {
|
||||
padding: 0 0;
|
||||
color: #343a40;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.xng-breadcrumb-link, a.xng-breadcrumb-link {
|
||||
color: #343a40;
|
||||
color: white;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user