feat: add cart and product details features with tests
This commit is contained in:
parent
6219a98f9a
commit
99c2c09b8d
@ -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 },
|
||||||
];
|
];
|
||||||
|
16
src/app/cart.service.spec.ts
Normal file
16
src/app/cart.service.spec.ts
Normal file
@ -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
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
src/app/cart/cart.component.css
Normal file
0
src/app/cart/cart.component.css
Normal file
12
src/app/cart/cart.component.html
Normal file
12
src/app/cart/cart.component.html
Normal file
@ -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>
|
||||||
|
}
|
23
src/app/cart/cart.component.spec.ts
Normal file
23
src/app/cart/cart.component.spec.ts
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
});
|
21
src/app/cart/cart.component.ts
Normal file
21
src/app/cart/cart.component.ts
Normal file
@ -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
src/app/product-alerts/product-alerts.component.css
Normal file
0
src/app/product-alerts/product-alerts.component.css
Normal file
5
src/app/product-alerts/product-alerts.component.html
Normal file
5
src/app/product-alerts/product-alerts.component.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@if(product && product.price > 700){
|
||||||
|
<p>
|
||||||
|
<button (click)="notify.emit()" type="button">Notify Me</button>
|
||||||
|
</p>
|
||||||
|
}
|
23
src/app/product-alerts/product-alerts.component.spec.ts
Normal file
23
src/app/product-alerts/product-alerts.component.spec.ts
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
});
|
14
src/app/product-alerts/product-alerts.component.ts
Normal file
14
src/app/product-alerts/product-alerts.component.ts
Normal file
@ -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
src/app/product-details/product-details.component.css
Normal file
0
src/app/product-details/product-details.component.css
Normal file
7
src/app/product-details/product-details.component.html
Normal file
7
src/app/product-details/product-details.component.html
Normal file
@ -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> }
|
23
src/app/product-details/product-details.component.spec.ts
Normal file
23
src/app/product-details/product-details.component.spec.ts
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
});
|
30
src/app/product-details/product-details.component.ts
Normal file
30
src/app/product-details/product-details.component.ts
Normal file
@ -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
src/app/shipping/shipping.component.css
Normal file
0
src/app/shipping/shipping.component.css
Normal file
8
src/app/shipping/shipping.component.html
Normal file
8
src/app/shipping/shipping.component.html
Normal file
@ -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>
|
||||||
|
}
|
23
src/app/shipping/shipping.component.spec.ts
Normal file
23
src/app/shipping/shipping.component.spec.ts
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
});
|
20
src/app/shipping/shipping.component.ts
Normal file
20
src/app/shipping/shipping.component.ts
Normal file
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user