Skip to content

Commit b26ddba

Browse files
committed
m15: complete app
1 parent 506a19a commit b26ddba

22 files changed

+443
-119
lines changed

src/app/app.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { StoreDevtoolsModule } from '@ngrx/store-devtools';
1717
import { reducer } from './reducer';
1818
import { EffectsModule } from '@ngrx/effects';
1919
import { ProductEffects } from './effects';
20+
import { RatingModule } from './rating/rating.module';
2021

2122
@NgModule({
2223
declarations: [AppComponent],
@@ -33,6 +34,7 @@ import { ProductEffects } from './effects';
3334
StoreModule.forRoot({ product: reducer }),
3435
EffectsModule.forRoot([ProductEffects]),
3536
StoreDevtoolsModule.instrument({ maxAge: 50 }),
37+
RatingModule,
3638
],
3739
bootstrap: [AppComponent],
3840
})

src/app/cart-details/actions.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Action } from '@ngrx/store';
2+
3+
export const REMOVE_ITEM = '[Cart Details] remove one item';
4+
export class RemoveItem implements Action {
5+
readonly type = REMOVE_ITEM;
6+
7+
constructor(readonly itemId: string) {}
8+
}
9+
10+
export const REMOVE_ALL = '[Cart Details] remove all items';
11+
export class RemoveAll implements Action {
12+
readonly type = REMOVE_ALL;
13+
}
14+
15+
export const PURCHASE_ITEMS = '[Cart Details] purchase all items';
16+
export class PurchaseItems implements Action {
17+
readonly type = PURCHASE_ITEMS;
18+
19+
constructor(readonly payload: { id: string; quantity: number }[]) {}
20+
}
21+
22+
export type All = RemoveItem | RemoveAll | PurchaseItems;
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { Component } from '@angular/core';
2-
import { combineLatest, Observable } from 'rxjs';
3-
import { map } from 'rxjs/operators';
2+
import { Observable } from 'rxjs';
43

54
import { CartProduct } from '../model/product';
6-
import { CartService } from '../services/cart.service';
7-
import { ProductService } from '../services/product.service';
85
import { Store } from '@ngrx/store';
96
import * as selectors from '../cart/selectors';
7+
import * as actions from './actions';
108

119
@Component({
1210
selector: 'app-cart-details',
@@ -20,22 +18,17 @@ export class CartDetailsComponent {
2018

2119
total$ = this.store.select(selectors.getCartTotal);
2220

23-
constructor(
24-
private readonly cartService: CartService,
25-
private readonly store: Store<{}>
26-
) {}
21+
constructor(private readonly store: Store<{}>) {}
2722

2823
removeOne(id: string) {
29-
this.cartService.removeOne(id);
24+
this.store.dispatch(new actions.RemoveItem(id));
3025
}
3126

3227
removeAll() {
33-
this.cartService.removeAll();
28+
this.store.dispatch(new actions.RemoveAll());
3429
}
3530

3631
purchase(products: CartProduct[]) {
37-
this.cartService.purchase(
38-
products.map(p => ({ id: p.id, quantity: p.quantity }))
39-
);
32+
this.store.dispatch(new actions.PurchaseItems(products));
4033
}
4134
}

src/app/cart/actions.ts

+41-1
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,49 @@ export class FetchCartItemsError implements Action {
2929
readonly type = FETCH_CART_ITEMS_ERROR;
3030
}
3131

32+
export const REMOVE_ITEM_SUCCESS = '[Cart API] remove one item success';
33+
export class RemoveItemSuccess implements Action {
34+
readonly type = REMOVE_ITEM_SUCCESS;
35+
}
36+
37+
export const REMOVE_ITEM_ERROR = '[Cart API] remove one item error';
38+
export class RemoveItemError implements Action {
39+
readonly type = REMOVE_ITEM_ERROR;
40+
41+
constructor(readonly itemId: string) {}
42+
}
43+
44+
export const REMOVE_ALL_SUCCESS = '[Cart API] remove all items success';
45+
export class RemoveAllSuccess implements Action {
46+
readonly type = REMOVE_ALL_SUCCESS;
47+
}
48+
49+
export const REMOVE_ALL_ERROR = '[Cart API] remove all items error';
50+
export class RemoveAllError implements Action {
51+
readonly type = REMOVE_ALL_ERROR;
52+
53+
constructor(readonly itemIds: string[]) {}
54+
}
55+
56+
export const PURCHASE_ITEMS_SUCCESS = '[Cart API] purchase all items success';
57+
export class PurchaseItemsSuccess implements Action {
58+
readonly type = PURCHASE_ITEMS_SUCCESS;
59+
}
60+
61+
export const PURCHASE_ITEMS_ERROR = '[Cart API] purchase all items error';
62+
export class PurchaseItemsError implements Action {
63+
readonly type = PURCHASE_ITEMS_ERROR;
64+
}
65+
3266
export type All =
3367
| AddItemSuccess
3468
| AddItemError
3569
| FetchCartItems
3670
| FetchCartItemsSuccess
37-
| FetchCartItemsError;
71+
| FetchCartItemsError
72+
| RemoveItemSuccess
73+
| RemoveItemError
74+
| RemoveAllSuccess
75+
| RemoveAllError
76+
| PurchaseItemsSuccess
77+
| PurchaseItemsError;

src/app/cart/cart.component.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
import { Component } from '@angular/core';
2-
import { map, startWith } from 'rxjs/operators';
3-
4-
import { CartService } from '../services/cart.service';
52
import { Store } from '@ngrx/store';
63

74
import * as selectors from './selectors';
@@ -13,9 +10,7 @@ import * as actions from './actions';
1310
styleUrls: ['./cart.component.scss'],
1411
})
1512
export class CartComponent {
16-
cartItemsCounter$ = this.store
17-
.select(selectors.getCartItemsCount)
18-
.pipe(startWith('?'));
13+
cartItemsCounter$ = this.store.select(selectors.getCartItemsCount);
1914

2015
constructor(private readonly store: Store<{}>) {
2116
this.store.dispatch(new actions.FetchCartItems());

src/app/cart/effects.ts

+86-3
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,35 @@
11
import { Actions, Effect, ofType } from '@ngrx/effects';
22
import { CartService } from '../services/cart.service';
3-
import { Action } from '@ngrx/store';
3+
import { Action, Store } from '@ngrx/store';
44
import { Observable, defer, of, timer } from 'rxjs';
55

66
import * as cartActions from './actions';
77
import * as productDetailsActions from '../product-details/actions';
8-
import { switchMap, map, catchError, concatMap } from 'rxjs/operators';
8+
import * as cartDetailsActions from '../cart-details/actions';
9+
import {
10+
switchMap,
11+
map,
12+
catchError,
13+
concatMap,
14+
withLatestFrom,
15+
mapTo,
16+
} from 'rxjs/operators';
917
import { Injectable } from '@angular/core';
1018
import { MatSnackBar } from '@angular/material';
1119

20+
import * as selectors from './selectors';
21+
import { Router } from '@angular/router';
22+
1223
const REFRESH_CART_ITEMS_INTEVAL_MS = 20 * 1000; // 20 seconds
1324

1425
@Injectable()
1526
export class CartEffects {
1627
constructor(
1728
private readonly actions$: Actions,
1829
private readonly cartService: CartService,
19-
private readonly snackBar: MatSnackBar
30+
private readonly store: Store<{}>,
31+
private readonly snackBar: MatSnackBar,
32+
private readonly router: Router
2033
) {}
2134

2235
@Effect()
@@ -42,6 +55,76 @@ export class CartEffects {
4255
)
4356
);
4457

58+
@Effect()
59+
removeCartItem: Observable<Action> = this.actions$.pipe(
60+
ofType<cartDetailsActions.RemoveItem>(cartDetailsActions.REMOVE_ITEM),
61+
concatMap(({ itemId }) =>
62+
this.cartService.removeOne(itemId).pipe(
63+
map(() => new cartActions.RemoveItemSuccess()),
64+
// passing the itemId to the Error, so it can be restored.
65+
catchError(() => of(new cartActions.RemoveItemError(itemId)))
66+
)
67+
)
68+
);
69+
70+
@Effect()
71+
removeAllItems: Observable<Action> = this.actions$.pipe(
72+
ofType<cartDetailsActions.RemoveAll>(cartDetailsActions.REMOVE_ALL),
73+
withLatestFrom(this.store.select(selectors.getCartItemsIds)),
74+
concatMap(([action, ids]) =>
75+
this.cartService.removeAll().pipe(
76+
map(() => new cartActions.RemoveAllSuccess()),
77+
// passing the itemId to the Error, so it can be restored.
78+
catchError(() => of(new cartActions.RemoveAllError(ids)))
79+
)
80+
)
81+
);
82+
83+
@Effect()
84+
purchaseItems: Observable<Action> = this.actions$.pipe(
85+
ofType<cartDetailsActions.PurchaseItems>(cartDetailsActions.PURCHASE_ITEMS),
86+
concatMap(({ payload }) =>
87+
this.cartService.purchase(payload).pipe(
88+
map(() => new cartActions.PurchaseItemsSuccess()),
89+
catchError(() => of(new cartActions.PurchaseItemsError()))
90+
)
91+
)
92+
);
93+
94+
@Effect()
95+
handlePurchaseSuccess: Observable<Action> = this.actions$.pipe(
96+
ofType<cartActions.PurchaseItemsSuccess>(
97+
cartActions.PURCHASE_ITEMS_SUCCESS
98+
),
99+
map(() => {
100+
this.router.navigate(['/home']);
101+
// Setting the timeout, so that angular would re-run change detection.
102+
setTimeout(
103+
() =>
104+
this.snackBar.open('Items purchased!', 'Success', {
105+
duration: 2500,
106+
}),
107+
0
108+
);
109+
}),
110+
mapTo(new cartActions.FetchCartItems())
111+
);
112+
113+
@Effect({ dispatch: false })
114+
purchaseItemsError = this.actions$.pipe(
115+
ofType<cartActions.PurchaseItemsError>(cartActions.PURCHASE_ITEMS_ERROR),
116+
map(() => {
117+
// Setting the timeout, so that angular would re-run change detection.
118+
setTimeout(
119+
() =>
120+
this.snackBar.open('Could not purchase items', 'Error', {
121+
duration: 2500,
122+
}),
123+
0
124+
);
125+
})
126+
);
127+
45128
@Effect({ dispatch: false })
46129
handleFetchError = this.actions$.pipe(
47130
ofType<cartActions.AddItemError>(cartActions.ADD_ITEM_ERROR),

src/app/cart/reducer.ts

+23-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Product } from '../model/product';
2-
import * as productDetailsActions from '../product-details/actions';
31
import * as actions from './actions';
2+
import * as productDetailsActions from '../product-details/actions';
3+
import * as cartDetailsActions from '../cart-details/actions';
44

55
export const CART_FEATURE_KEY = 'Cart feature';
66

@@ -18,7 +18,7 @@ export const initState: CartState = {
1818

1919
export function reducer(
2020
state: CartState = initState,
21-
action: actions.All | productDetailsActions.All
21+
action: actions.All | productDetailsActions.All | cartDetailsActions.All
2222
): CartState {
2323
switch (action.type) {
2424
case productDetailsActions.ADD_ITEM: {
@@ -31,14 +31,28 @@ export function reducer(
3131
state.cartItemsIds.splice(indexOfItemId, 1);
3232
// Force array to mutate.
3333
const newCartItemsIds = [...state.cartItemsIds];
34-
return {
35-
cartItemsIds: newCartItemsIds,
36-
};
34+
return { cartItemsIds: newCartItemsIds };
35+
}
36+
case cartDetailsActions.REMOVE_ITEM: {
37+
const indexOfItemId = state.cartItemsIds.indexOf(action.itemId);
38+
// Remove the element.
39+
state.cartItemsIds.splice(indexOfItemId, 1);
40+
// Force array to mutate.
41+
const newCartItemsIds = [...state.cartItemsIds];
42+
return { cartItemsIds: newCartItemsIds };
43+
}
44+
case actions.REMOVE_ITEM_ERROR: {
45+
const newCartItemsIds = [...state.cartItemsIds, action.itemId];
46+
return { cartItemsIds: newCartItemsIds };
3747
}
3848
case actions.FETCH_CART_ITEMS_SUCCESS: {
39-
return {
40-
cartItemsIds: action.itemIds,
41-
};
49+
return { cartItemsIds: action.itemIds };
50+
}
51+
case cartDetailsActions.REMOVE_ALL: {
52+
return { cartItemsIds: [] };
53+
}
54+
case actions.REMOVE_ALL_ERROR: {
55+
return { cartItemsIds: action.itemIds };
4256
}
4357
default: {
4458
return state;
+14-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
<mat-icon
2-
*ngFor="let star of [1,2,3,4,5]"
3-
(mouseenter)="starMouseEnter(star)"
4-
(mouseleave)="starMouseLeave()"
5-
(click)="rate(star)"
6-
[color]="(rated || mouseOver) ? 'accent' : 'primary'">
7-
{{(star
8-
<=( starOver || rated || rating)) ? 'star' : 'star_border'}}
9-
</mat-icon>
1+
<div *ngIf="isLoading; else loaded">
2+
<mat-spinner diameter="24"></mat-spinner>
3+
</div>
4+
<ng-template #loaded>
5+
<mat-icon
6+
*ngFor="let star of [1,2,3,4,5]"
7+
(mouseenter)="starMouseEnter(star)"
8+
(mouseleave)="starMouseLeave()"
9+
(click)="rate(star)"
10+
[color]="(rated || mouseOver) ? 'accent' : 'primary'">
11+
{{(star
12+
<=( starOver || rated || rating)) ? 'star' : 'star_border'}}
13+
</mat-icon>
14+
</ng-template>

src/app/common/stars/stars.component.ts

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export class StarsComponent {
1919

2020
@Input() rated = 0;
2121
@Input() rating = 0;
22+
@Input() isLoading = false;
2223

2324
@Output() ratingChange = new EventEmitter<number>();
2425

src/app/common/stars/stars.module.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { CommonModule } from '@angular/common';
22
import { NgModule } from '@angular/core';
3-
import { MatIconModule } from '@angular/material';
3+
import { MatIconModule, MatProgressSpinnerModule } from '@angular/material';
44

55
import { StarsComponent } from './stars.component';
66

77
@NgModule({
8-
imports: [CommonModule, MatIconModule],
8+
imports: [CommonModule, MatIconModule, MatProgressSpinnerModule],
99
declarations: [StarsComponent],
1010
exports: [StarsComponent],
1111
})

src/app/model/rating.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface RatingScore {
2+
id: string;
3+
rating: number;
4+
}
5+
6+
export interface LoadableRatingScore extends Partial<RatingScore> {
7+
isLoading: boolean;
8+
}

0 commit comments

Comments
 (0)