Skip to content

Commit

Permalink
CFWEB-122-Signal-store,Shopping Cart Feature
Browse files Browse the repository at this point in the history
* adding new Ngrx Signal Store functionality
* adding shopping cart
* extending order confirmation route with route params
* setting product price to number
* fixing theme handling bug after page refresh
* fixing layout, same header style
  • Loading branch information
speti05 committed Jul 15, 2024
1 parent d485dde commit 59c6463
Show file tree
Hide file tree
Showing 27 changed files with 621 additions and 74 deletions.
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"rxjs": "7.8.1",
"stylelint-config-scss": "1.0.0-security",
"tslib": "2.6.2",
"zone.js": "0.14.4"
"zone.js": "0.14.4",
"@ngrx/signals": "^17.2.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.3.6",
Expand All @@ -57,6 +58,7 @@
"@angular/compiler-cli": "17.3.6",
"@angular/language-service": "17.3.6",
"@ngrx/schematics": "17.2.0",
"@ngrx/signals": "17.2.0",
"@playwright/test": "1.43.1",
"@types/jasmine": "5.1.4",
"@types/node": "20.10.5",
Expand Down
2 changes: 1 addition & 1 deletion src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
~ SPDX-FileCopyrightText: (c) 2022 Carl Zeiss AG
~ SPDX-License-Identifier: MIT
-->

<app-shopping-cart></app-shopping-cart>
<router-outlet></router-outlet>
4 changes: 3 additions & 1 deletion src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@

import { TestBed, waitForAsync } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { provideMockStore } from '@ngrx/store/testing';

describe('AppComponent', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [AppComponent],
})
providers: [provideMockStore({ initialState: {} })],
}),
);

it('should create the app', waitForAsync(() => {
Expand Down
3 changes: 2 additions & 1 deletion src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { ShoppingCartComponent } from '@app/shared/components/shopping-cart/shopping-cart.component';

@Component({
selector: 'app-root',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [RouterOutlet],
imports: [RouterOutlet, ShoppingCartComponent],
templateUrl: './app.component.html',
})
export class AppComponent {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
~ SPDX-License-Identifier: MIT
-->

<header *ngIf="product$ | async as product">
<app-theme-switcher></app-theme-switcher>
<h1>{{ product.title }}</h1>
</header>

<div class="container">
<div class="row">
<div class="product-details" *ngIf="product$ | async as product">
Expand All @@ -12,19 +17,22 @@
<h2>{{ product.title }}</h2>

<h3>Price</h3>
<p>{{ product.price }}</p>
<p>{{ product.price }}</p>

<div>
<h3>Number of items</h3>
<button class="confirmation-button" (click)="decreaseProductNumber()">-</button>
<button class="counter-button" (click)="decreaseProductNumber()">-</button>
{{ productNumber() }}
<button class="confirmation-button" (click)="increseProductNumber()">+</button>
<button class="counter-button" (click)="increseProductNumber()">+</button>
</div>

<h3>Ingredients</h3>
<p>{{ product.description }}</p>

<button class="confirmation-button" (click)="showConfirmation()">Buy Online</button>
<div class="button-container">
<button (click)="showConfirmation()">Buy Online</button>
<button (click)="addToCart()">Add to cart</button>
</div>
</div>
</div>
<button class="back-button" (click)="backToProductOverview()">Back to all products</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

@use '../../../../../support/styling/common-styles' as common;

header {
@include common.header-style;
}

div.container {
margin-top: 25px;
margin-bottom: 25px;
Expand Down Expand Up @@ -50,12 +54,21 @@ div.container {
}
}

.confirmation-button {
margin-bottom: 50px;
@include common.button-primary;
.button-container {
display: flex;
gap: 20px;
margin: 50px 0;

button {
@include common.button-primary;
}
}

.back-button {
margin: 50px 0;
@include common.button-secondary;
}

.counter-button {
@include common.button-primary;
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,25 @@ describe('ProductDetailComponent', () => {
it('should dispatch navigate action to order page', () => {
component.showConfirmation();

expect(store.dispatch).toHaveBeenCalledWith(navigate({ url: '/order' }));
expect(store.dispatch).toHaveBeenCalledWith(
navigate({
url: '/order',
navigationExtras: {
state: {
products: [
{
id: fixture.debugElement.componentInstance.loadedProduct.id,
title: fixture.debugElement.componentInstance.loadedProduct.title,
image: fixture.debugElement.componentInstance.loadedProduct.image,
price: fixture.debugElement.componentInstance.loadedProduct.price,
description: fixture.debugElement.componentInstance.loadedProduct.description,
count: fixture.debugElement.componentInstance.productNumber(),
},
],
},
},
}),
);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,77 @@
* SPDX-License-Identifier: MIT
*/

import { ChangeDetectionStrategy, Component, OnInit, signal } from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
inject,
OnDestroy,
OnInit,
signal,
} from '@angular/core';
import { Store } from '@ngrx/store';

import { selectCurrentProductDetails } from '@app/catalog/product/store/product.selectors';
import { navigate } from '@app/shared/navigation/navigation.actions';
import { StateWithCatalog } from '@app/catalog/store/catalog.reducer';
import { loadProductDetails } from '@app/catalog/product/store/product.actions';
import { ActivatedRoute } from '@angular/router';
import { ShoppingCartStore } from '@app/shared/signal-store/shopping-cart.store';
import { Product } from '@models/product';
import { Subscription } from 'rxjs';

@Component({
selector: 'app-product-detail',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './product-detail.component.html',
styleUrls: ['./product-detail.component.scss'],
})
export class ProductDetailComponent implements OnInit {
export class ProductDetailComponent implements OnInit, OnDestroy {
product$ = this.store.select(selectCurrentProductDetails);
private loadedProduct: Product;
private loadProductSubscription: Subscription;

public shoppingCartSignalStore = inject(ShoppingCartStore);

productNumber = signal(1);

constructor(private store: Store<StateWithCatalog>, private route: ActivatedRoute) {}
constructor(
private store: Store<StateWithCatalog>,
private route: ActivatedRoute,
private cdr: ChangeDetectorRef,
) {}

backToProductOverview(): void {
this.store.dispatch(navigate({ url: '/' }));
}

showConfirmation(): void {
this.store.dispatch(navigate({ url: '/order' }));
// it's quite strange to navigate through a store's effect,but this is an old pattern in the codebase
this.store.dispatch(
navigate({
url: '/order',
navigationExtras: {
state: {
products: [
{
id: this.loadedProduct.id,
title: this.loadedProduct.title,
image: this.loadedProduct.image,
price: this.loadedProduct.price,
description: this.loadedProduct.description,
count: this.productNumber(),
},
],
},
},
}),
);
}

addToCart(): void {
this.shoppingCartSignalStore.addProduct({ ...this.loadedProduct, count: this.productNumber() });
this.cdr.markForCheck();
}

increseProductNumber(): void {
Expand All @@ -41,8 +84,17 @@ export class ProductDetailComponent implements OnInit {
this.productNumber.update((c) => (c > 1 ? c - 1 : 1));
}

ngOnInit() {
public ngOnInit() {
const productId: number = Number.parseInt(this.route.snapshot.paramMap.get('id'));
this.store.dispatch(loadProductDetails({ productId: productId }));
this.loadProductSubscription = this.product$.subscribe({
next: (product: Product) => (this.loadedProduct = product),
error: (error: Error) =>
console.log(`Error while loading product in ProductDetailsComponent: ${error}`),
});
}

public ngOnDestroy() {
this.loadProductSubscription.unsubscribe();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,10 @@
* SPDX-License-Identifier: MIT
*/

header {
background-color: var(--color-highlight);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: var(--color-highlight-text);
letter-spacing: 0.2em;
padding: 20px;

h1 {
font-size: 65px;
font-weight: 100;
margin: 30px 0;
}
@use '../../../../../support/styling/common-styles' as common;

h2 {
font-size: 35px;
font-weight: 100;
}
}

app-theme-switcher {
align-self: end;
header {
@include common.header-style;
}

div.container {
Expand Down
9 changes: 5 additions & 4 deletions src/app/catalog/product/product.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
* SPDX-License-Identifier: MIT
*/

import { CommonModule } from '@angular/common';
import { AsyncPipe, CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ChangeDetectorRef, NgModule } from '@angular/core';
import { provideEffects } from '@ngrx/effects';

import { ProductDetailComponent } from '@app/catalog/product/components/product-detail/product-detail.component';
Expand All @@ -15,17 +15,18 @@ import { ProductEffects } from '@app/catalog/product/store/product.effects';
import { RecommendationModule } from '@app/catalog/recommendation/recommendation.module';
import { ThemeSwitcherComponent } from '@app/shared/components/theme/theme-switcher.component';
import { ProductComponent } from '@app/shared/components/product/product.component';
import { ShoppingCartStore } from '@app/shared/signal-store/shopping-cart.store';

@NgModule({
imports: [
CommonModule,
HttpClientModule,
RecommendationModule,
ProductComponent,
ThemeSwitcherComponent,
ProductComponent,
],
declarations: [ProductDetailComponent, ProductMasterComponent],
exports: [ProductDetailComponent, ProductMasterComponent],
providers: [ProductService, provideEffects(ProductEffects)],
providers: [ProductService, provideEffects(ProductEffects), AsyncPipe, ShoppingCartStore],
})
export class ProductModule {}
Loading

0 comments on commit 59c6463

Please sign in to comment.