From 495ac6a8be07f7742ef75a23ca4c5a05f072b018 Mon Sep 17 00:00:00 2001 From: Charles Showalter Date: Wed, 18 May 2022 16:28:29 -0700 Subject: [PATCH] Added Shopping Cart --- client/package-lock.json | 5 +- client/package.json | 1 + client/src/app/app-routing.module.ts | 1 + client/src/app/app.component.html | 2 +- client/src/app/app.component.ts | 16 +++- .../src/app/basket/basket-routing.module.ts | 16 ++++ client/src/app/basket/basket.component.html | 74 +++++++++++++++ client/src/app/basket/basket.component.scss | 0 client/src/app/basket/basket.component.ts | 20 ++++ client/src/app/basket/basket.module.ts | 19 ++++ client/src/app/basket/basket.service.ts | 93 +++++++++++++++++++ .../app/core/nav-bar/nav-bar.component.html | 6 +- .../app/core/nav-bar/nav-bar.component.scss | 2 +- .../src/app/core/nav-bar/nav-bar.component.ts | 7 +- .../section-header.component.html | 2 +- .../order-totals/order-totals.component.html | 20 ++++ .../order-totals/order-totals.component.scss | 0 .../order-totals/order-totals.component.ts | 20 ++++ client/src/app/shared/models/baskset.ts | 28 ++++++ client/src/app/shared/shared.module.ts | 7 +- .../product-details.component.html | 2 +- .../product-item/product-item.component.html | 2 +- .../product-item/product-item.component.ts | 7 +- client/src/index.html | 2 +- client/src/styles.scss | 4 +- 25 files changed, 335 insertions(+), 21 deletions(-) create mode 100644 client/src/app/basket/basket-routing.module.ts create mode 100644 client/src/app/basket/basket.component.html create mode 100644 client/src/app/basket/basket.component.scss create mode 100644 client/src/app/basket/basket.component.ts create mode 100644 client/src/app/basket/basket.module.ts create mode 100644 client/src/app/basket/basket.service.ts create mode 100644 client/src/app/shared/components/order-totals/order-totals.component.html create mode 100644 client/src/app/shared/components/order-totals/order-totals.component.scss create mode 100644 client/src/app/shared/components/order-totals/order-totals.component.ts create mode 100644 client/src/app/shared/models/baskset.ts diff --git a/client/package-lock.json b/client/package-lock.json index 257dea8..8d8c809 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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", diff --git a/client/package.json b/client/package.json index 2e7bbb0..86cb800 100644 --- a/client/package.json +++ b/client/package.json @@ -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" }, diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index 0805e5e..a6d5bc3 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -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'} ]; diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index 0fa3315..c9e5a27 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -1,4 +1,4 @@ - +

Loading...

diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 5558935..b5d4843 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -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') } + }); + } + } } diff --git a/client/src/app/basket/basket-routing.module.ts b/client/src/app/basket/basket-routing.module.ts new file mode 100644 index 0000000..47b4f74 --- /dev/null +++ b/client/src/app/basket/basket-routing.module.ts @@ -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 { } diff --git a/client/src/app/basket/basket.component.html b/client/src/app/basket/basket.component.html new file mode 100644 index 0000000..1d66a75 --- /dev/null +++ b/client/src/app/basket/basket.component.html @@ -0,0 +1,74 @@ +
+
+

You're shopping cart is currently empty!

+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + +
+
Product
+
+
Price
+
+
quantity
+
+
Total
+
+
Remove
+
+
+ {{item.productName}} +
+
+ {{item.productName}} +
+ Type: {{item.type}} +
+
+
{{item.price | currency}} +
+ + {{item.quantity}} + +
+
{{item.price * item.quantity | currency}} + + + +
+
+
+
+
+
+ + Checkout +
+
+
+
+
+
+ diff --git a/client/src/app/basket/basket.component.scss b/client/src/app/basket/basket.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/src/app/basket/basket.component.ts b/client/src/app/basket/basket.component.ts new file mode 100644 index 0000000..42690f4 --- /dev/null +++ b/client/src/app/basket/basket.component.ts @@ -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; + + constructor(private basketService: BasketService) { } + + ngOnInit(): void { + this.basket$ = this.basketService.basket$; + } + +} diff --git a/client/src/app/basket/basket.module.ts b/client/src/app/basket/basket.module.ts new file mode 100644 index 0000000..8ce6ec5 --- /dev/null +++ b/client/src/app/basket/basket.module.ts @@ -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 { } diff --git a/client/src/app/basket/basket.service.ts b/client/src/app/basket/basket.service.ts new file mode 100644 index 0000000..8657dd0 --- /dev/null +++ b/client/src/app/basket/basket.service.ts @@ -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(null); + basket$ = this.basketSource.asObservable(); + private basketTotalSource = new BehaviorSubject(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 + } + } + +} diff --git a/client/src/app/core/nav-bar/nav-bar.component.html b/client/src/app/core/nav-bar/nav-bar.component.html index 782de57..a17bd4a 100644 --- a/client/src/app/core/nav-bar/nav-bar.component.html +++ b/client/src/app/core/nav-bar/nav-bar.component.html @@ -1,4 +1,4 @@ -
+
- + -
5
+
{{basket.items.length}}
Login Sign up diff --git a/client/src/app/core/nav-bar/nav-bar.component.scss b/client/src/app/core/nav-bar/nav-bar.component.scss index 1af87aa..3b20d52 100644 --- a/client/src/app/core/nav-bar/nav-bar.component.scss +++ b/client/src/app/core/nav-bar/nav-bar.component.scss @@ -13,7 +13,7 @@ a { text-decoration: none; - color: #343a40; + color: white; &.active { color: orange; diff --git a/client/src/app/core/nav-bar/nav-bar.component.ts b/client/src/app/core/nav-bar/nav-bar.component.ts index 6082d5b..f4fc5b3 100644 --- a/client/src/app/core/nav-bar/nav-bar.component.ts +++ b/client/src/app/core/nav-bar/nav-bar.component.ts @@ -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 - constructor() { } + constructor(private basketService: BasketService) { } ngOnInit(): void { + this.basket$ = this.basketService.basket$; } } diff --git a/client/src/app/core/section-header/section-header.component.html b/client/src/app/core/section-header/section-header.component.html index c01fe6e..5117db0 100644 --- a/client/src/app/core/section-header/section-header.component.html +++ b/client/src/app/core/section-header/section-header.component.html @@ -1,5 +1,5 @@ -
+
diff --git a/client/src/app/shared/components/order-totals/order-totals.component.html b/client/src/app/shared/components/order-totals/order-totals.component.html new file mode 100644 index 0000000..27fcf53 --- /dev/null +++ b/client/src/app/shared/components/order-totals/order-totals.component.html @@ -0,0 +1,20 @@ +
+ Order Summary +
+
+

Shipping costs will be added during checkout

+
    +
  • + Subtotal + {{totals.subtotal | currency}} +
  • +
  • + Shipping and Handling + {{totals.shipping | currency}} +
  • +
  • + Total + {{totals.total | currency}} +
  • +
+
diff --git a/client/src/app/shared/components/order-totals/order-totals.component.scss b/client/src/app/shared/components/order-totals/order-totals.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/src/app/shared/components/order-totals/order-totals.component.ts b/client/src/app/shared/components/order-totals/order-totals.component.ts new file mode 100644 index 0000000..4f75c7c --- /dev/null +++ b/client/src/app/shared/components/order-totals/order-totals.component.ts @@ -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; + + constructor(private basketService: BasketService) { } + + ngOnInit(): void { + this.basketTotal$ = this.basketService.basketTotal$; + } + +} diff --git a/client/src/app/shared/models/baskset.ts b/client/src/app/shared/models/baskset.ts new file mode 100644 index 0000000..92227da --- /dev/null +++ b/client/src/app/shared/models/baskset.ts @@ -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; +} diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 35a6add..2c7cc13 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -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 { } diff --git a/client/src/app/shop/product-details/product-details.component.html b/client/src/app/shop/product-details/product-details.component.html index a1dbed9..33604e2 100644 --- a/client/src/app/shop/product-details/product-details.component.html +++ b/client/src/app/shop/product-details/product-details.component.html @@ -10,7 +10,7 @@ 2 - +
diff --git a/client/src/app/shop/product-item/product-item.component.html b/client/src/app/shop/product-item/product-item.component.html index 76d5341..c544caa 100644 --- a/client/src/app/shop/product-item/product-item.component.html +++ b/client/src/app/shop/product-item/product-item.component.html @@ -2,7 +2,7 @@
{{product.name}}
- +
diff --git a/client/src/app/shop/product-item/product-item.component.ts b/client/src/app/shop/product-item/product-item.component.ts index 175617c..582c1bf 100644 --- a/client/src/app/shop/product-item/product-item.component.ts +++ b/client/src/app/shop/product-item/product-item.component.ts @@ -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); + } + } diff --git a/client/src/index.html b/client/src/index.html index bec4160..81abc14 100644 --- a/client/src/index.html +++ b/client/src/index.html @@ -2,7 +2,7 @@ - E-Commerce + SkiNet diff --git a/client/src/styles.scss b/client/src/styles.scss index 3642c7f..bdf79f4 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -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; }