Added Shopping Cart

This commit is contained in:
Charles Showalter 2022-05-18 16:28:29 -07:00
parent bcbad19601
commit 495ac6a8be
25 changed files with 335 additions and 21 deletions

View File

@ -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",

View File

@ -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"
},

View File

@ -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'}
];

View File

@ -1,4 +1,4 @@
<ngx-spinner>
<ngx-spinner type="ball-clip-rotate-multiple">
<h3>Loading...</h3>
</ngx-spinner>
<app-nav-bar></app-nav-bar>

View File

@ -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') }
});
}
}
}

View 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 { }

View 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>

View 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$;
}
}

View 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 { }

View 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
}
}
}

View File

@ -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>

View File

@ -13,7 +13,7 @@
a {
text-decoration: none;
color: #343a40;
color: white;
&.active {
color: orange;

View File

@ -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$;
}
}

View File

@ -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">

View File

@ -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>

View File

@ -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$;
}
}

View 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;
}

View File

@ -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 { }

View File

@ -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">

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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">

View File

@ -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;
}