feat: add cart and product details features with tests

This commit is contained in:
Jan Gleytenhoover 2024-11-12 08:11:16 +01:00
parent 6219a98f9a
commit 99c2c09b8d
Signed by: jank
GPG Key ID: B267751B8AE29EFE
23 changed files with 293 additions and 3 deletions

@ -1,7 +1,8 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)] providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient()]
}; };

@ -1,6 +1,12 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import {ProductListComponent} from './product-list/product-list.component'; import {ProductListComponent} from './product-list/product-list.component';
import { ProductDetailsComponent } from './product-details/product-details.component';
import { CartComponent } from './cart/cart.component';
import { ShippingComponent } from './shipping/shipping.component';
export const routes: Routes = [ export const routes: Routes = [
{ path: '', component: ProductListComponent }, { path: '', component: ProductListComponent },
{ path: 'products/:productId', component: ProductDetailsComponent },
{ path: 'cart', component: CartComponent },
{ path: 'shipping', component: ShippingComponent },
]; ];

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { CartService } from './cart.service';
describe('CartService', () => {
let service: CartService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CartService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

30
src/app/cart.service.ts Normal file

@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import { Product } from './products';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class CartService {
items: Product[] = [];
constructor(
private http: HttpClient,
) { }
addToCart(product: Product) {
this.items.push(product);
}
getItems() {
return this.items;
}
clearCart() {
this.items = [];
return this.items;
}
getShippingPrices() {
return this.http.get<{type: string, price: number}[]>('/assets/shiping.json');
}
}

@ -0,0 +1,12 @@
<h3>Cart</h3>
<p>
<a routerLink="/shipping">Shipping Prices</a>
</p>
@for(item of items; track item.id){
<div class="cart-item">
<span>{{ item.name }}</span>
<span>{{ item.price | currency }}</span>
</div>
}

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CartComponent } from './cart.component';
describe('CartComponent', () => {
let component: CartComponent;
let fixture: ComponentFixture<CartComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CartComponent]
})
.compileComponents();
fixture = TestBed.createComponent(CartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,21 @@
import { Component } from '@angular/core';
import { CartService } from '../cart.service';
import { Product } from '../products';
import { CurrencyPipe } from '@angular/common';
@Component({
selector: 'app-cart',
standalone: true,
imports: [CurrencyPipe],
templateUrl: './cart.component.html',
styleUrl: './cart.component.css'
})
export class CartComponent {
items: Product[] | undefined = undefined;
constructor(
private cartService: CartService,
) {
this.items = this.cartService.getItems();
}
}

@ -0,0 +1,5 @@
@if(product && product.price > 700){
<p>
<button (click)="notify.emit()" type="button">Notify Me</button>
</p>
}

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductAlertsComponent } from './product-alerts.component';
describe('ProductAlertsComponent', () => {
let component: ProductAlertsComponent;
let fixture: ComponentFixture<ProductAlertsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProductAlertsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ProductAlertsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,14 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Product } from '../products';
@Component({
selector: 'app-product-alerts',
standalone: true,
imports: [],
templateUrl: './product-alerts.component.html',
styleUrl: './product-alerts.component.css'
})
export class ProductAlertsComponent {
@Input() product: Product | undefined;
@Output() notify = new EventEmitter();
}

@ -0,0 +1,7 @@
<h2>Product Details</h2>
@if(product){ <div *ngIf="product">
<h3>{{ product.name }}</h3>
<h4>{{ product.price | currency }}</h4>
<p>{{ product.description }}</p> <button type="button" (click)="addToCart(product)">Buy</button>
</div> }

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductDetailsComponent } from './product-details.component';
describe('ProductDetailsComponent', () => {
let component: ProductDetailsComponent;
let fixture: ComponentFixture<ProductDetailsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProductDetailsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ProductDetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,30 @@
import { Component, OnInit } from '@angular/core';
import { Product, products } from '../products';
import { ActivatedRoute } from '@angular/router';
import { CurrencyPipe, NgIf } from '@angular/common';
import { CartService } from '../cart.service';
@Component({
selector: 'app-product-details',
standalone: true,
imports: [CurrencyPipe, NgIf],
templateUrl: './product-details.component.html',
styleUrl: './product-details.component.css'
})
export class ProductDetailsComponent implements OnInit {
product: Product | undefined;
constructor(private route: ActivatedRoute, private cartService: CartService) { }
addToCart(product: Product) {
this.cartService.addToCart(product);
window.alert('Your product has genn thrown into the cart!');
}
ngOnInit(): void {
const routeParams = this.route.snapshot.paramMap;
const productIdFromRoute = Number(routeParams.get('productId'));
this.product = products.find(product => product.id === productIdFromRoute);
}
}

@ -1 +1,21 @@
<h2>Products</h2> <h2>Products</h2>
@for(product of products; track product.id){
<div>
<h3>
<a [title]="product.name + ' details'" [routerLink]="['/products', product.id]">
{{ product.name }}
</a>
</h3>
@if(product.description){
<p>Description: {{product.description}}</p>
}
<button type="button" (click)="share()">
Share
</button>
<app-product-alerts [product]="product" (notify)="onNotify()">
</app-product-alerts>
</div>
}

@ -1,10 +1,12 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { products } from '../products'; import { products } from '../products';
import { ProductAlertsComponent } from '../product-alerts/product-alerts.component';
import { RouterModule } from '@angular/router';
@Component({ @Component({
selector: 'app-product-list', selector: 'app-product-list',
standalone: true, standalone: true,
imports: [], imports: [ProductAlertsComponent, RouterModule],
templateUrl: './product-list.component.html', templateUrl: './product-list.component.html',
styleUrl: './product-list.component.css' styleUrl: './product-list.component.css'
}) })
@ -14,4 +16,8 @@ export class ProductListComponent {
share() { share() {
window.alert('The product has been shared!'); window.alert('The product has been shared!');
} }
onNotify() {
window.alert("You will be notified when the product goes on sale");
}
} }

@ -0,0 +1,8 @@
<h3>Shipping Prices</h3>
@for(shipping of shippingCosts | async; track shipping.type) {
<div class="shipping-item">
<span>{{ shipping.type }}</span>
<span>{{ shipping.price | currency }}</span>
</div>
}

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ShippingComponent } from './shipping.component';
describe('ShippingComponent', () => {
let component: ShippingComponent;
let fixture: ComponentFixture<ShippingComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ShippingComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ShippingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,20 @@
import { Component } from '@angular/core';
import { CartService } from '../cart.service';
import { Observable } from 'rxjs';
import { AsyncPipe, CurrencyPipe } from '@angular/common';
@Component({
selector: 'app-shipping',
standalone: true,
imports: [AsyncPipe, CurrencyPipe],
templateUrl: './shipping.component.html',
styleUrl: './shipping.component.css'
})
export class ShippingComponent {
constructor(private cartService: CartService) { }
shippingCosts!: Observable<{ type: string, price: number }[]>;
ngOnInit(): void {
this.shippingCosts = this.cartService.getShippingPrices();
}
}

@ -2,4 +2,6 @@
<h1>My Store</h1> <h1>My Store</h1>
</a> </a>
<a class="button fancy-button"><em class="material-icons">shopping_cart</em>Checkout</a> <a routerLink="/cart" class="button fancy-button">
<em class="material-icons">shopping_cart</em>Checkout
</a>