Started Client Checkout
This commit is contained in:
parent
344ecb8762
commit
7cdf78794d
40
client/package-lock.json
generated
40
client/package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "~13.3.0",
|
"@angular/animations": "~13.3.0",
|
||||||
|
"@angular/cdk": "^13.3.7",
|
||||||
"@angular/common": "~13.3.0",
|
"@angular/common": "~13.3.0",
|
||||||
"@angular/compiler": "~13.3.0",
|
"@angular/compiler": "~13.3.0",
|
||||||
"@angular/core": "~13.3.0",
|
"@angular/core": "~13.3.0",
|
||||||
@ -355,6 +356,28 @@
|
|||||||
"@angular/core": "13.3.7"
|
"@angular/core": "13.3.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular/cdk": {
|
||||||
|
"version": "13.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-13.3.7.tgz",
|
||||||
|
"integrity": "sha512-HtGqlrt4+ikbpzooF0LT/uMW6fgRJxLRUoOwkTY1oHhfNXhQaE2p8XEUH2qshl28aCIF8r8zrb6jpd4VqC+tyg==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"parse5": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": "^13.0.0 || ^14.0.0-0",
|
||||||
|
"@angular/core": "^13.0.0 || ^14.0.0-0",
|
||||||
|
"rxjs": "^6.5.3 || ^7.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@angular/cdk/node_modules/parse5": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@angular/cli": {
|
"node_modules/@angular/cli": {
|
||||||
"version": "13.3.5",
|
"version": "13.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-13.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-13.3.5.tgz",
|
||||||
@ -11988,6 +12011,23 @@
|
|||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@angular/cdk": {
|
||||||
|
"version": "13.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-13.3.7.tgz",
|
||||||
|
"integrity": "sha512-HtGqlrt4+ikbpzooF0LT/uMW6fgRJxLRUoOwkTY1oHhfNXhQaE2p8XEUH2qshl28aCIF8r8zrb6jpd4VqC+tyg==",
|
||||||
|
"requires": {
|
||||||
|
"parse5": "^5.0.0",
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"parse5": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@angular/cli": {
|
"@angular/cli": {
|
||||||
"version": "13.3.5",
|
"version": "13.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-13.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-13.3.5.tgz",
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "~13.3.0",
|
"@angular/animations": "~13.3.0",
|
||||||
|
"@angular/cdk": "^13.3.7",
|
||||||
"@angular/common": "~13.3.0",
|
"@angular/common": "~13.3.0",
|
||||||
"@angular/compiler": "~13.3.0",
|
"@angular/compiler": "~13.3.0",
|
||||||
"@angular/core": "~13.3.0",
|
"@angular/core": "~13.3.0",
|
||||||
|
@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { map, of, ReplaySubject } from 'rxjs';
|
import { map, of, ReplaySubject } from 'rxjs';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { IAddress } from '../shared/models/address';
|
||||||
import { IUser } from '../shared/models/user';
|
import { IUser } from '../shared/models/user';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -64,4 +65,12 @@ export class AccountService {
|
|||||||
checkEmailExists(email: string){
|
checkEmailExists(email: string){
|
||||||
return this.http.get(this.baseUrl + 'account/emailexists?email=' + email);
|
return this.http.get(this.baseUrl + 'account/emailexists?email=' + email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserAddress(){
|
||||||
|
return this.http.get<IAddress>(this.baseUrl + 'account/address');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUserAddress(address: IAddress){
|
||||||
|
return this.http.put<IAddress>(this.baseUrl + 'account/address', address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import { HomeModule } from './home/home.module';
|
|||||||
import { ErrorInterceptor } from './core/interceptors/error.interceptor';
|
import { ErrorInterceptor } from './core/interceptors/error.interceptor';
|
||||||
import { NgxSpinnerModule } from 'ngx-spinner';
|
import { NgxSpinnerModule } from 'ngx-spinner';
|
||||||
import { LoadingInterceptor } from './core/interceptors/loading.interceptor';
|
import { LoadingInterceptor } from './core/interceptors/loading.interceptor';
|
||||||
|
import { JwtInterceptor } from './core/interceptors/jwt.interceptor';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -27,7 +28,8 @@ import { LoadingInterceptor } from './core/interceptors/loading.interceptor';
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
|
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
|
||||||
{provide: HTTP_INTERCEPTORS, useClass: LoadingInterceptor, multi: true}
|
{provide: HTTP_INTERCEPTORS, useClass: LoadingInterceptor, multi: true},
|
||||||
|
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true}
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
|
@ -7,58 +7,11 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 py-3 mb-1">
|
<div class="col-12 py-3 mb-1">
|
||||||
<div class="table-responsive">
|
<app-basket-summary
|
||||||
<table class="table table-striped table-hover table-light">
|
(decrement)="decrementItemQuantity($event)"
|
||||||
<thead>
|
(increment)="incrementItemQuantity($event)"
|
||||||
<tr>
|
(remove)="removeBasketItem($event)"
|
||||||
<th scope="col">
|
></app-basket-summary>
|
||||||
<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 (click)="decrementItemQuantity(item)" class="fa fa-minus-circle text-warning me-2" style="cursor: pointer;"></i>
|
|
||||||
<span class="font-weight-bold">{{item.quantity}}</span>
|
|
||||||
<i (click)="incrementItemQuantity(item)" 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 (click)="removeBasketItem(item)" class="fa fa-trash" style="font-size: 2em"></i>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { BehaviorSubject, map } from 'rxjs';
|
import { BehaviorSubject, map } from 'rxjs';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
import { Basket, IBasket, IBasketItem, IBasketTotals } from '../shared/models/baskset';
|
import { Basket, IBasket, IBasketItem, IBasketTotals } from '../shared/models/baskset';
|
||||||
|
import { IDeliveryMethods } from '../shared/models/deliveryMethods';
|
||||||
import { IProduct } from '../shared/models/product';
|
import { IProduct } from '../shared/models/product';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -14,9 +15,15 @@ export class BasketService {
|
|||||||
basket$ = this.basketSource.asObservable();
|
basket$ = this.basketSource.asObservable();
|
||||||
private basketTotalSource = new BehaviorSubject<IBasketTotals>(null);
|
private basketTotalSource = new BehaviorSubject<IBasketTotals>(null);
|
||||||
basketTotal$ = this.basketTotalSource.asObservable();
|
basketTotal$ = this.basketTotalSource.asObservable();
|
||||||
|
shipping = 0;
|
||||||
|
|
||||||
constructor(private http: HttpClient) { }
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
|
setShippingPrice(deliveryMethod: IDeliveryMethods){
|
||||||
|
this.shipping = deliveryMethod.price;
|
||||||
|
this.calculateTotals();
|
||||||
|
}
|
||||||
|
|
||||||
getBasket(id: string){
|
getBasket(id: string){
|
||||||
return this.http.get(this.baseUrl + 'basket?id=' + id).pipe(
|
return this.http.get(this.baseUrl + 'basket?id=' + id).pipe(
|
||||||
map((basket: IBasket) => {
|
map((basket: IBasket) => {
|
||||||
@ -92,7 +99,7 @@ export class BasketService {
|
|||||||
|
|
||||||
private calculateTotals(){
|
private calculateTotals(){
|
||||||
const basket = this.getCurrentBasketValue();
|
const basket = this.getCurrentBasketValue();
|
||||||
const shipping = 0;
|
const shipping = this.shipping;
|
||||||
const subtotal = basket.items.reduce((a, b) => (b.price * b.quantity) + a, 0);
|
const subtotal = basket.items.reduce((a, b) => (b.price * b.quantity) + a, 0);
|
||||||
const total = subtotal + shipping;
|
const total = subtotal + shipping;
|
||||||
this.basketTotalSource.next({
|
this.basketTotalSource.next({
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
<div class="mt-4" [formGroup]="checkoutForm">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h4>Shipping Address</h4>
|
||||||
|
<button
|
||||||
|
(click)="saveUserAddress()"
|
||||||
|
[disabled]="!checkoutForm.get('addressForm').valid || !checkoutForm.get('addressForm').dirty"
|
||||||
|
class="btn btn-outline-success mb-3">
|
||||||
|
Save as default address
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="row" formGroupName="addressForm">
|
||||||
|
<div class="form-group col-6">
|
||||||
|
<app-text-inputs [label]="'First Name'" formControlName="firstName"></app-text-inputs>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-6">
|
||||||
|
<app-text-inputs [label]="'Last Name'" formControlName="lastName"></app-text-inputs>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-6">
|
||||||
|
<app-text-inputs [label]="'Street'" formControlName="street"></app-text-inputs>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-6">
|
||||||
|
<app-text-inputs [label]="'City'" formControlName="city"></app-text-inputs>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-6">
|
||||||
|
<app-text-inputs [label]="'State'" formControlName="state"></app-text-inputs>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-6">
|
||||||
|
<app-text-inputs [label]="'Zip Code'" formControlName="zipCode"></app-text-inputs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="float-none d-flex justify-content-between flex-column flex-lg-row mb-5">
|
||||||
|
<button class="btn btn-outline-primary" routerLink="/basket">
|
||||||
|
<i class="fa fa-angle-left"></i> Back to Shopping Cart
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" cdkStepperNext>
|
||||||
|
Delivery Options <i class="fa fa-angle-right"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,29 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { FormGroup } from '@angular/forms';
|
||||||
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
import { AccountService } from 'src/app/account/account.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-checkout-address',
|
||||||
|
templateUrl: './checkout-address.component.html',
|
||||||
|
styleUrls: ['./checkout-address.component.scss']
|
||||||
|
})
|
||||||
|
export class CheckoutAddressComponent implements OnInit {
|
||||||
|
@Input() checkoutForm: FormGroup;
|
||||||
|
|
||||||
|
constructor(private accountService: AccountService, private toaster: ToastrService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
saveUserAddress(){
|
||||||
|
this.accountService.updateUserAddress(this.checkoutForm.get('addressForm').value).subscribe({
|
||||||
|
next: () => { this.toaster.success('Address Saved'); },
|
||||||
|
error: (e: any) => {
|
||||||
|
this.toaster.error(e.message);
|
||||||
|
},
|
||||||
|
complete: () => { console.log('completed'); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
<div class="mt-4" [formGroup]="checkoutForm">
|
||||||
|
<h4 class="mb-3">Choose your delivery method.</h4>
|
||||||
|
<div class="row ms-5" formGroupName="deliveryForm">
|
||||||
|
<div class="col-5 form-group" *ngFor="let method of deliveryMethods">
|
||||||
|
<input type="radio"
|
||||||
|
id="{{method.id}}"
|
||||||
|
(click)="setShippingPrice(method)"
|
||||||
|
value="{{method.id}}"
|
||||||
|
formControlName="deliveryMethod"
|
||||||
|
class="custom-control-input m-3"
|
||||||
|
>
|
||||||
|
<label for="{{method.id}}" class="custom-control-label">
|
||||||
|
<strong>{{method.shortName}} - {{method.price | currency}}</strong>
|
||||||
|
<br>
|
||||||
|
<span class="label-description">{{method.description}}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="float-none d-flex justify-content-between flex-column flex-lg-row m-3">
|
||||||
|
<button class="btn btn-outline-primary" cdkStepperPrevious>
|
||||||
|
<i class="fa fa-angle-left"></i> Back to Address
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" cdkStepperNext>
|
||||||
|
Review Order <i class="fa fa-angle-right"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,30 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { FormGroup } from '@angular/forms';
|
||||||
|
import { BasketService } from 'src/app/basket/basket.service';
|
||||||
|
import { IDeliveryMethods } from 'src/app/shared/models/deliveryMethods';
|
||||||
|
import { CheckoutService } from '../checkout.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-checkout-delivery',
|
||||||
|
templateUrl: './checkout-delivery.component.html',
|
||||||
|
styleUrls: ['./checkout-delivery.component.scss']
|
||||||
|
})
|
||||||
|
export class CheckoutDeliveryComponent implements OnInit {
|
||||||
|
@Input() checkoutForm: FormGroup;
|
||||||
|
deliveryMethods: IDeliveryMethods[];
|
||||||
|
|
||||||
|
constructor(private checkoutService: CheckoutService, private basketService: BasketService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.checkoutService.getDeliveryMethods().subscribe({
|
||||||
|
next: (dm: IDeliveryMethods[]) => { this.deliveryMethods = dm; },
|
||||||
|
error: (e: any) => { console.log(e); },
|
||||||
|
complete: () => { console.log('completed') }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setShippingPrice(deliveryMethod: IDeliveryMethods){
|
||||||
|
this.basketService.setShippingPrice(deliveryMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
<p>checkout-payments works!</p>
|
||||||
|
<div class="float-none d-flex justify-content-between flex-column flex-lg-row mb-5">
|
||||||
|
<button class="btn btn-outline-primary" cdkStepperPrevious>
|
||||||
|
<i class="fa fa-angle-left"></i> Back to Review
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary">
|
||||||
|
Complete Checkout <i class="fa fa-angle-right"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,15 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-checkout-payments',
|
||||||
|
templateUrl: './checkout-payments.component.html',
|
||||||
|
styleUrls: ['./checkout-payments.component.scss']
|
||||||
|
})
|
||||||
|
export class CheckoutPaymentsComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
<div class="container mt-4">
|
||||||
|
<app-basket-summary [isBasket]=false></app-basket-summary>
|
||||||
|
</div>
|
||||||
|
<div class="float-none d-flex justify-content-between flex-column flex-lg-row mb-5">
|
||||||
|
<button class="btn btn-outline-primary" cdkStepperPrevious>
|
||||||
|
<i class="fa fa-angle-left"></i> Back to Delivery
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" cdkStepperNext>
|
||||||
|
Payment Options <i class="fa fa-angle-right"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,15 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-checkout-review',
|
||||||
|
templateUrl: './checkout-review.component.html',
|
||||||
|
styleUrls: ['./checkout-review.component.scss']
|
||||||
|
})
|
||||||
|
export class CheckoutReviewComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
<p>checkout-success works!</p>
|
@ -0,0 +1,15 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-checkout-success',
|
||||||
|
templateUrl: './checkout-success.component.html',
|
||||||
|
styleUrls: ['./checkout-success.component.scss']
|
||||||
|
})
|
||||||
|
export class CheckoutSuccessComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,3 +1,23 @@
|
|||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<h1>Only authorized users should be able to see this.</h1>
|
<div class="row">
|
||||||
|
<div class="col-8">
|
||||||
|
<app-stepper [linearModeSelected]="false" #appStepper>
|
||||||
|
<cdk-step [label]="'Address'" [completed]="(checkoutForm.get('addressForm')).valid">
|
||||||
|
<app-checkout-address [checkoutForm]="checkoutForm"></app-checkout-address>
|
||||||
|
</cdk-step>
|
||||||
|
<cdk-step [label]="'Delivery'" [completed]="(checkoutForm.get('deliveryForm')).valid">
|
||||||
|
<app-checkout-delivery [checkoutForm]="checkoutForm"></app-checkout-delivery>
|
||||||
|
</cdk-step>
|
||||||
|
<cdk-step [label]="'Review'">
|
||||||
|
<app-checkout-review></app-checkout-review>
|
||||||
|
</cdk-step>
|
||||||
|
<cdk-step [label]="'Payment'">
|
||||||
|
<app-checkout-payments></app-checkout-payments>
|
||||||
|
</cdk-step>
|
||||||
|
</app-stepper>
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<app-order-totals></app-order-totals>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { AccountService } from '../account/account.service';
|
||||||
|
import { IAddress } from '../shared/models/address';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-checkout',
|
selector: 'app-checkout',
|
||||||
@ -6,10 +9,44 @@ import { Component, OnInit } from '@angular/core';
|
|||||||
styleUrls: ['./checkout.component.scss']
|
styleUrls: ['./checkout.component.scss']
|
||||||
})
|
})
|
||||||
export class CheckoutComponent implements OnInit {
|
export class CheckoutComponent implements OnInit {
|
||||||
|
checkoutForm: FormGroup;
|
||||||
|
|
||||||
constructor() { }
|
constructor(private fb: FormBuilder, private accountService: AccountService) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.createCheckoutForm();
|
||||||
|
this.getAddressFormValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
createCheckoutForm(){
|
||||||
|
this.checkoutForm = this.fb.group({
|
||||||
|
addressForm: this.fb.group({
|
||||||
|
firstName: [null, Validators.required],
|
||||||
|
lastName: [null, Validators.required],
|
||||||
|
street: [null, Validators.required],
|
||||||
|
city: [null, Validators.required],
|
||||||
|
state: [null, Validators.required],
|
||||||
|
zipCode: [null, Validators.required]
|
||||||
|
}),
|
||||||
|
deliveryForm: this.fb.group({
|
||||||
|
deliveryMethod: [null, Validators.required]
|
||||||
|
}),
|
||||||
|
paymenForm: this.fb.group({
|
||||||
|
nameOnCard: [null, Validators.required]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAddressFormValues(){
|
||||||
|
this.accountService.getUserAddress().subscribe({
|
||||||
|
next: (address: any) => {
|
||||||
|
if(address){
|
||||||
|
this.checkoutForm.get('addressForm').patchValue(address);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (e: any) => { console.log(e) },
|
||||||
|
complete: () => { console.log('completed') }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,28 @@ import { NgModule } from '@angular/core';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CheckoutComponent } from './checkout.component';
|
import { CheckoutComponent } from './checkout.component';
|
||||||
import { CheckoutRoutingModule } from './checkout-routing.module';
|
import { CheckoutRoutingModule } from './checkout-routing.module';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { CheckoutAddressComponent } from './checkout-address/checkout-address.component';
|
||||||
|
import { CheckoutDeliveryComponent } from './checkout-delivery/checkout-delivery.component';
|
||||||
|
import { CheckoutReviewComponent } from './checkout-review/checkout-review.component';
|
||||||
|
import { CheckoutPaymentsComponent } from './checkout-payments/checkout-payments.component';
|
||||||
|
import { CheckoutSuccessComponent } from './checkout-success/checkout-success.component';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
CheckoutComponent
|
CheckoutComponent,
|
||||||
|
CheckoutAddressComponent,
|
||||||
|
CheckoutDeliveryComponent,
|
||||||
|
CheckoutReviewComponent,
|
||||||
|
CheckoutPaymentsComponent,
|
||||||
|
CheckoutSuccessComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
CheckoutRoutingModule
|
CheckoutRoutingModule,
|
||||||
|
SharedModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CheckoutModule { }
|
export class CheckoutModule { }
|
||||||
|
@ -1,9 +1,22 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { map } from 'rxjs';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { IDeliveryMethods } from '../shared/models/deliveryMethods';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class CheckoutService {
|
export class CheckoutService {
|
||||||
|
baseUrl = environment.apiUrl;
|
||||||
|
|
||||||
constructor() { }
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
|
getDeliveryMethods(){
|
||||||
|
return this.http.get(this.baseUrl + 'orders/deliveryMethods').pipe(
|
||||||
|
map((dm: IDeliveryMethods[]) => {
|
||||||
|
return dm.sort((a, b) => b.price - a.price);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
19
client/src/app/core/interceptors/jwt.interceptor.ts
Normal file
19
client/src/app/core/interceptors/jwt.interceptor.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
|
||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtInterceptor implements HttpInterceptor {
|
||||||
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if(token) {
|
||||||
|
req = req.clone({
|
||||||
|
setHeaders: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return next.handle(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
<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 *ngIf="isBasket" 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 *ngIf="isBasket" (click)="decrementItemQuantity(item)" class="fa fa-minus-circle text-warning me-2" style="cursor: pointer;"></i>
|
||||||
|
<span class="font-weight-bold">{{item.quantity}}</span>
|
||||||
|
<i *ngIf="isBasket" (click)="incrementItemQuantity(item)" 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 *ngIf="isBasket" class="align-middle text-center">
|
||||||
|
<a class="text-danger" style="cursor: pointer">
|
||||||
|
<i (click)="removeBasketItem(item)" class="fa fa-trash" style="font-size: 2em"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
@ -0,0 +1,35 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { BasketService } from 'src/app/basket/basket.service';
|
||||||
|
import { IBasket, IBasketItem } from '../models/baskset';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-basket-summary',
|
||||||
|
templateUrl: './basket-summary.component.html',
|
||||||
|
styleUrls: ['./basket-summary.component.scss']
|
||||||
|
})
|
||||||
|
export class BasketSummaryComponent implements OnInit {
|
||||||
|
basket$: Observable<IBasket>;
|
||||||
|
@Output() decrement: EventEmitter<IBasketItem> = new EventEmitter<IBasketItem>();
|
||||||
|
@Output() increment: EventEmitter<IBasketItem> = new EventEmitter<IBasketItem>();
|
||||||
|
@Output() remove: EventEmitter<IBasketItem> = new EventEmitter<IBasketItem>();
|
||||||
|
@Input() isBasket = true;
|
||||||
|
|
||||||
|
constructor(private basketService: BasketService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.basket$ = this.basketService.basket$;
|
||||||
|
}
|
||||||
|
|
||||||
|
decrementItemQuantity(item: IBasketItem){
|
||||||
|
this.decrement.emit(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
incrementItemQuantity(item: IBasketItem){
|
||||||
|
this.increment.emit(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeBasketItem(item: IBasketItem){
|
||||||
|
this.remove.emit(item);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
<div class="container">
|
||||||
|
<ul class="nav nav-pills nav-justified">
|
||||||
|
<li class="nav-item" *ngFor="let step of steps; let i = index">
|
||||||
|
<button
|
||||||
|
(click)="onClick(i)"
|
||||||
|
[class.active]="selectedIndex === i"
|
||||||
|
class="nav-link py-3 text-uppercase font-weight-bold btn-block">
|
||||||
|
{{step.label}}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
<ng-container [ngTemplateOutlet]="selected.content"></ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,20 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { CdkStepper } from '@angular/cdk/stepper';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-stepper',
|
||||||
|
templateUrl: './stepper.component.html',
|
||||||
|
styleUrls: ['./stepper.component.scss'],
|
||||||
|
providers: [{provide: CdkStepper, useExisting: StepperComponent}]
|
||||||
|
})
|
||||||
|
export class StepperComponent extends CdkStepper implements OnInit {
|
||||||
|
@Input() linearModeSelected: boolean;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.linear = this.linearModeSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick(index: number){
|
||||||
|
this.selectedIndex = index;
|
||||||
|
}
|
||||||
|
}
|
7
client/src/app/shared/models/deliveryMethods.ts
Normal file
7
client/src/app/shared/models/deliveryMethods.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface IDeliveryMethods {
|
||||||
|
id: number;
|
||||||
|
shortName: string;
|
||||||
|
deliveryTime: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
}
|
@ -8,6 +8,10 @@ import { PagerComponent } from './components/pager/pager.component';
|
|||||||
import { OrderTotalsComponent } from './components/order-totals/order-totals.component';
|
import { OrderTotalsComponent } from './components/order-totals/order-totals.component';
|
||||||
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
|
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
|
||||||
import { TextInputsComponent } from './components/text-inputs/text-inputs.component';
|
import { TextInputsComponent } from './components/text-inputs/text-inputs.component';
|
||||||
|
import { CdkStepperModule } from '@angular/cdk/stepper';
|
||||||
|
import { StepperComponent } from './components/stepper/stepper.component';
|
||||||
|
import { BasketSummaryComponent } from './basket-summary/basket-summary.component'
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@ -15,14 +19,18 @@ import { TextInputsComponent } from './components/text-inputs/text-inputs.compon
|
|||||||
PagingHeaderComponent,
|
PagingHeaderComponent,
|
||||||
PagerComponent,
|
PagerComponent,
|
||||||
OrderTotalsComponent,
|
OrderTotalsComponent,
|
||||||
TextInputsComponent
|
TextInputsComponent,
|
||||||
|
StepperComponent,
|
||||||
|
BasketSummaryComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
PaginationModule.forRoot(),
|
PaginationModule.forRoot(),
|
||||||
CarouselModule.forRoot(),
|
CarouselModule.forRoot(),
|
||||||
BsDropdownModule.forRoot(),
|
BsDropdownModule.forRoot(),
|
||||||
ReactiveFormsModule
|
ReactiveFormsModule,
|
||||||
|
CdkStepperModule,
|
||||||
|
RouterModule
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
PaginationModule,
|
PaginationModule,
|
||||||
@ -32,7 +40,10 @@ import { TextInputsComponent } from './components/text-inputs/text-inputs.compon
|
|||||||
OrderTotalsComponent,
|
OrderTotalsComponent,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
BsDropdownModule,
|
BsDropdownModule,
|
||||||
TextInputsComponent
|
TextInputsComponent,
|
||||||
|
CdkStepperModule,
|
||||||
|
StepperComponent,
|
||||||
|
BasketSummaryComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SharedModule { }
|
export class SharedModule { }
|
||||||
|
Loading…
Reference in New Issue
Block a user