Skip to content

Commit 226c62c

Browse files
committed
feat: checkout flow
1 parent a418777 commit 226c62c

26 files changed

+589
-40
lines changed

src/app/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
1313
import { ProductsModule } from './products/products.module';
1414
import { HttpClientModule } from '@angular/common/http';
1515
import { MatBadgeModule } from '@angular/material/badge';
16+
import { CartModule } from './cart/cart.module';
1617

1718
@NgModule({
1819
declarations: [AppComponent, HeaderComponent],
@@ -26,6 +27,7 @@ import { MatBadgeModule } from '@angular/material/badge';
2627
MatMenuModule,
2728
MatTooltipModule,
2829
ProductsModule,
30+
CartModule,
2931
HttpClientModule,
3032
MatBadgeModule,
3133
],
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<form (ngSubmit)="nextStep.emit()" [formGroup]="shippingInfo">
2+
<div class="row">
3+
<div class="col col-md-6">
4+
<mat-form-field class="w-100">
5+
<input
6+
formControlName="firstName"
7+
name="firstName"
8+
placeholder="First name"
9+
type="text"
10+
matInput
11+
required
12+
/>
13+
14+
<mat-error *ngIf="shippingInfo.getError('required', 'firstName')">
15+
First name is required!
16+
</mat-error>
17+
</mat-form-field>
18+
</div>
19+
<div class="col col-md-6">
20+
<mat-form-field class="w-100">
21+
<input
22+
formControlName="lastName"
23+
name="lastName"
24+
placeholder="Last name"
25+
type="text"
26+
matInput
27+
required
28+
/>
29+
30+
<mat-error *ngIf="shippingInfo.getError('required', 'lastName')">
31+
Last name is required!
32+
</mat-error>
33+
</mat-form-field>
34+
</div>
35+
<div class="col-12">
36+
<mat-form-field class="w-100">
37+
<input
38+
formControlName="address"
39+
name="address"
40+
placeholder="Shipping address"
41+
type="text"
42+
matInput
43+
required
44+
/>
45+
46+
<mat-error *ngIf="shippingInfo.getError('required', 'address')">
47+
Shipping address is required!
48+
</mat-error>
49+
</mat-form-field>
50+
</div>
51+
<div class="col-12">
52+
<mat-form-field class="w-100">
53+
<input
54+
formControlName="comment"
55+
name="comment"
56+
placeholder="Comment"
57+
type="text"
58+
matInput
59+
/>
60+
</mat-form-field>
61+
</div>
62+
</div>
63+
64+
<!-- Submit button to allow form submit by Enter -->
65+
<button class="d-none"></button>
66+
</form>

src/app/cart/cart-shipping-form/cart-shipping-form.component.scss

Whitespace-only changes.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { CartShippingFormComponent } from './cart-shipping-form.component';
4+
5+
describe('CartShippingFormComponent', () => {
6+
let component: CartShippingFormComponent;
7+
let fixture: ComponentFixture<CartShippingFormComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
declarations: [CartShippingFormComponent],
12+
}).compileComponents();
13+
});
14+
15+
beforeEach(() => {
16+
fixture = TestBed.createComponent(CartShippingFormComponent);
17+
component = fixture.componentInstance;
18+
fixture.detectChanges();
19+
});
20+
21+
it('should create', () => {
22+
expect(component).toBeTruthy();
23+
});
24+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Component, EventEmitter, Input, Output } from '@angular/core';
2+
import { FormGroup } from '@angular/forms';
3+
4+
@Component({
5+
selector: 'app-cart-shipping-form',
6+
templateUrl: './cart-shipping-form.component.html',
7+
styleUrls: ['./cart-shipping-form.component.scss'],
8+
})
9+
export class CartShippingFormComponent {
10+
@Input() shippingInfo!: FormGroup;
11+
12+
@Output() nextStep = new EventEmitter<void>();
13+
}

src/app/cart/cart.component.html

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,124 @@
1-
<p>cart works!</p>
1+
<div class="container">
2+
<div class="row">
3+
<div class="col mx-auto py-3 py-md-5">
4+
<mat-card>
5+
<h1 class="text-center pt-4" mat-card-title>Checkout</h1>
6+
7+
<mat-card-content>
8+
<mat-vertical-stepper #stepper [linear]="true">
9+
<!-- Review your cart STEP -->
10+
<mat-step label="Review your cart" [completed]="cartEmpty$ | async">
11+
<ng-container *ngIf="cartEmpty$ | async; else emptyCartTemplate">
12+
<ng-container
13+
*ngIf="(loading$ | async) === false"
14+
[ngTemplateOutlet]="orderSummaryTemplate"
15+
[ngTemplateOutletContext]="{ showControls: true }"
16+
></ng-container>
17+
18+
<div *ngIf="loading$ | async" class="py-5">
19+
<mat-spinner [diameter]="40" class="mx-auto"></mat-spinner>
20+
</div>
21+
22+
<div class="text-right">
23+
<button
24+
color="primary"
25+
class="text-uppercase"
26+
mat-flat-button
27+
matStepperNext
28+
>
29+
next
30+
</button>
31+
</div>
32+
</ng-container>
33+
</mat-step>
34+
35+
<!-- Shipping address STEP -->
36+
<mat-step label="Shipping address" [stepControl]="shippingInfo">
37+
<h2>Shipping address</h2>
38+
39+
<app-cart-shipping-form
40+
(nextStep)="stepper.next()"
41+
[shippingInfo]="shippingInfo"
42+
></app-cart-shipping-form>
43+
44+
<div class="text-right">
45+
<button class="text-uppercase" mat-button matStepperPrevious>
46+
back
47+
</button>
48+
<button
49+
color="primary"
50+
class="text-uppercase"
51+
mat-flat-button
52+
matStepperNext
53+
>
54+
next
55+
</button>
56+
</div>
57+
</mat-step>
58+
59+
<!-- Review your order STEP -->
60+
<mat-step label="Review your order">
61+
<ng-container
62+
[ngTemplateOutlet]="orderSummaryTemplate"
63+
[ngTemplateOutletContext]="{ showControls: false }"
64+
></ng-container>
65+
66+
<div class="row">
67+
<div class="col col-md-6">
68+
<h2>Shipping</h2>
69+
<p>{{ fullName }}</p>
70+
<p>{{ address }}</p>
71+
</div>
72+
<div class="col col-md-6">
73+
<h2>Comment</h2>
74+
<p>{{ comment }}</p>
75+
</div>
76+
</div>
77+
78+
<div class="text-right">
79+
<button class="text-uppercase" mat-button matStepperPrevious>
80+
back
81+
</button>
82+
<button color="primary" class="text-uppercase" mat-flat-button>
83+
place order
84+
</button>
85+
</div>
86+
</mat-step>
87+
</mat-vertical-stepper>
88+
</mat-card-content>
89+
</mat-card>
90+
</div>
91+
</div>
92+
</div>
93+
94+
<ng-template #orderSummaryTemplate let-showControls="showControls">
95+
<h2>Order summary</h2>
96+
97+
<ng-container>
98+
<app-product-item-checkout
99+
*ngFor="let product of products$ | async"
100+
(add)="add(product.id)"
101+
(remove)="remove(product.id)"
102+
[product]="product"
103+
[hideControls]="!showControls"
104+
></app-product-item-checkout>
105+
</ng-container>
106+
107+
<div class="row">
108+
<h3 class="col flex-grow-1">Shipping</h3>
109+
<span class="col flex-grow-0">Free</span>
110+
</div>
111+
112+
<div class="row">
113+
<h3 class="col flex-grow-1">Total</h3>
114+
<b class="col flex-grow-0" style="font-size: 18px">{{
115+
totalPrice$ | async | number: "1.2-2" | currency
116+
}}</b>
117+
</div>
118+
</ng-template>
119+
120+
<ng-template #emptyCartTemplate>
121+
<div class="lead">
122+
The cart is empty. Didn't you like anything in our shop?
123+
</div>
124+
</ng-template>

src/app/cart/cart.component.ts

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,87 @@
11
import { Component, OnInit } from '@angular/core';
2+
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
3+
import { STEPPER_GLOBAL_OPTIONS } from '@angular/cdk/stepper';
4+
import { CheckoutService } from './checkout.service';
5+
import { ProductCheckout } from '../products/product.interface';
6+
import { BehaviorSubject, Observable } from 'rxjs';
7+
import { CartService } from './cart.service';
8+
import { map, shareReplay, tap } from 'rxjs/operators';
29

310
@Component({
411
selector: 'app-cart',
512
templateUrl: './cart.component.html',
613
styleUrls: ['./cart.component.scss'],
14+
providers: [
15+
{
16+
provide: STEPPER_GLOBAL_OPTIONS,
17+
useValue: { displayDefaultIndicatorType: false },
18+
},
19+
],
720
})
821
export class CartComponent implements OnInit {
9-
constructor() {}
22+
loading$ = new BehaviorSubject(true);
23+
products$!: Observable<ProductCheckout[]>;
24+
totalPrice$!: Observable<number>;
25+
totalInCart$!: Observable<number>;
26+
cartEmpty$!: Observable<boolean>;
1027

11-
ngOnInit(): void {}
28+
shippingInfo!: FormGroup;
29+
30+
constructor(
31+
private readonly fb: FormBuilder,
32+
private readonly checkoutService: CheckoutService,
33+
private readonly cartService: CartService
34+
) {}
35+
36+
get fullName(): string {
37+
const { firstName, lastName } = this.shippingInfo.value;
38+
return `${firstName} ${lastName}`;
39+
}
40+
41+
get address(): string {
42+
return this.shippingInfo.value.address;
43+
}
44+
45+
get comment(): string {
46+
return this.shippingInfo.value.comment;
47+
}
48+
49+
ngOnInit(): void {
50+
this.shippingInfo = this.fb.group({
51+
lastName: ['', Validators.required],
52+
firstName: ['', Validators.required],
53+
address: ['', Validators.required],
54+
comment: '',
55+
});
56+
57+
this.products$ = this.checkoutService.getProductsForCheckout().pipe(
58+
tap(() => this.loading$.next(false)),
59+
shareReplay({
60+
refCount: true,
61+
bufferSize: 1,
62+
})
63+
);
64+
65+
this.totalPrice$ = this.products$.pipe(
66+
map((products) => {
67+
const total = products.reduce((acc, val) => acc + val.totalPrice, 0);
68+
return +total.toFixed(2);
69+
}),
70+
shareReplay({
71+
refCount: true,
72+
bufferSize: 1,
73+
})
74+
);
75+
76+
this.totalInCart$ = this.cartService.totalInCart$;
77+
this.cartEmpty$ = this.totalInCart$.pipe(map((count) => count > 0));
78+
}
79+
80+
add(id: string): void {
81+
this.cartService.addItem(id);
82+
}
83+
84+
remove(id: string): void {
85+
this.cartService.removeItem(id);
86+
}
1287
}

src/app/cart/cart.module.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,35 @@ import { NgModule } from '@angular/core';
22
import { CommonModule } from '@angular/common';
33
import { CartComponent } from './cart.component';
44
import { CartRoutingModule } from './cart-routing.module';
5+
import { MatCardModule } from '@angular/material/card';
6+
import { MatStepperModule } from '@angular/material/stepper';
7+
import { MatButtonModule } from '@angular/material/button';
8+
import { ReactiveFormsModule } from '@angular/forms';
9+
import { MatFormFieldModule } from '@angular/material/form-field';
10+
import { MatInputModule } from '@angular/material/input';
11+
import { ProductItemCheckoutComponent } from './product-item-checkout/product-item-checkout.component';
12+
import { CartCountControlsModule } from '../core/cart-count-controls/cart-count-controls.module';
13+
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
14+
import { CartShippingFormComponent } from './cart-shipping-form/cart-shipping-form.component';
515

616
@NgModule({
7-
declarations: [CartComponent],
8-
imports: [CommonModule, CartRoutingModule],
17+
declarations: [
18+
CartComponent,
19+
ProductItemCheckoutComponent,
20+
CartShippingFormComponent,
21+
],
22+
imports: [
23+
CommonModule,
24+
CartRoutingModule,
25+
MatCardModule,
26+
MatStepperModule,
27+
MatButtonModule,
28+
ReactiveFormsModule,
29+
MatFormFieldModule,
30+
MatInputModule,
31+
CartCountControlsModule,
32+
MatProgressSpinnerModule,
33+
],
934
exports: [CartComponent],
1035
})
1136
export class CartModule {}

src/app/cart/cart.service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Injectable } from '@angular/core';
22
import { BehaviorSubject, Observable } from 'rxjs';
3-
import { map } from 'rxjs/operators';
3+
import { map, shareReplay } from 'rxjs/operators';
44

55
@Injectable({
66
providedIn: 'root',
@@ -20,6 +20,10 @@ export class CartService {
2020
}
2121

2222
return values.reduce((acc, val) => acc + val, 0);
23+
}),
24+
shareReplay({
25+
refCount: true,
26+
bufferSize: 1,
2327
})
2428
);
2529

0 commit comments

Comments
 (0)