Added Hooks

This commit is contained in:
Charles Showalter 2022-05-13 13:26:38 -07:00
parent df23d64061
commit bb8ea8cfae
19 changed files with 286 additions and 177 deletions

View File

@ -0,0 +1,11 @@
<pagination
[boundaryLinks]="true"
[totalItems]="totalCount"
(pageChanged)="onPagerChange($event)"
[itemsPerPage]="pageSize"
previousText="&lsaquo;"
nextText="&rsaquo;"
firstText="&laquo;"
lastText="&raquo;"
>
</pagination>

View File

@ -0,0 +1,22 @@
import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core';
@Component({
selector: 'app-pager',
templateUrl: './pager.component.html',
styleUrls: ['./pager.component.scss']
})
export class PagerComponent implements OnInit {
@Input() totalCount: number;
@Input() pageSize: number;
@Output() pageChanged = new EventEmitter<number>();
constructor() { }
ngOnInit(): void {
}
onPagerChange(e: any){
this.pageChanged.emit(e.page);
}
}

View File

@ -0,0 +1,10 @@
<header>
<span *ngIf="totalCount && totalCount > 0">
Showing
<strong>
{{(pageNumber-1) * pageSize+1}} -
{{pageNumber*pageSize > this.totalCount ? this.totalCount : pageNumber*pageSize}}
</strong> of <strong>{{totalCount}}</strong> Results
</span>
<span *ngIf="totalCount === 0">There are <strong>0 </strong>results for this filter.</span>
</header>

View File

@ -0,0 +1,18 @@
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-paging-header',
templateUrl: './paging-header.component.html',
styleUrls: ['./paging-header.component.scss']
})
export class PagingHeaderComponent implements OnInit {
@Input() pageNumber: number;
@Input() pageSize: number;
@Input() totalCount: number;
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,4 @@
export interface IBrand {
id: number;
name: string;
}

View File

@ -0,0 +1,4 @@
export interface IType {
id: number;
name: string;
}

View File

@ -0,0 +1,8 @@
export class ShopParams {
brandId: number = 0;
typeId: number = 0;
sort = 'name';
pageNumber: number = 1;
pageSize: number = 6;
search: string;
}

View File

@ -1,12 +1,24 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { PaginationModule } from 'ngx-bootstrap/pagination';
import { PagingHeaderComponent } from './components/paging-header/paging-header.component';
import { PagerComponent } from './components/pager/pager.component';
@NgModule({ @NgModule({
declarations: [], declarations: [
PagingHeaderComponent,
PagerComponent
],
imports: [ imports: [
CommonModule CommonModule,
PaginationModule.forRoot()
],
exports: [
PaginationModule,
PagingHeaderComponent,
PagerComponent
] ]
}) })
export class SharedModule { } export class SharedModule { }

View File

@ -0,0 +1,13 @@
<div class="card h-100 shadow-sm">
<img src="{{product.pictureUrl}}" alt="{{product.name}}" class="img-fluid bg-light">
<div class="card-body d-flex flex-column">
<a href="">
<h6 class="text-uppercase">{{product.name}}</h6>
</a>
<span class="mb-2">{{product.price | currency}}</span>
<div class="btn-group mt-auto">
<button type="button" class="btn btn-sm btn-outline-secondary fa fa-shopping-cart me-2"></button>
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
</div>
</div>
</div>

View File

@ -0,0 +1,17 @@
import { Component, Input, OnInit } from '@angular/core';
import { IProduct } from 'src/app/shared/models/product';
@Component({
selector: 'app-product-item',
templateUrl: './product-item.component.html',
styleUrls: ['./product-item.component.scss']
})
export class ProductItemComponent implements OnInit {
@Input() product: IProduct
constructor() { }
ngOnInit(): void {
}
}

View File

@ -2,191 +2,61 @@
<div class="row"> <div class="row">
<section class="col-3"> <section class="col-3">
<h5 class="text-warning ml-3">Sort</h5> <h5 class="text-warning ml-3">Sort</h5>
<select class="custom-select mb-3" style="width: 100%;"> <select class="form-select mb-3" style="width: 100%;" (change)="onSortSelected($event.target.value)">
<option selected>Name</option> <option *ngFor="let sort of sortOptions"
<option>Price: Low to High</option> [value]="sort.value"
<option>Price: High to Low</option> >
{{sort.name}}
</option>
</select> </select>
<h5 class="text-warning ml-3">Brands</h5> <h5 class="text-warning ml-3">Brands</h5>
<ul class="list-group my-3"> <ul class="list-group my-3">
<li class="list-group-item active" aria-current="true">An active item</li> <li class="list-group-item"
<li class="list-group-item">A second item</li> *ngFor="let brand of brands"
<li class="list-group-item">A third item</li> [class.active]="brand.id === this.shopParams.brandId"
<li class="list-group-item">A fourth item</li> [value]="brand.id"
<li class="list-group-item">And a fifth one</li> (click)="onBrandSelected(brand.id)"
>{{brand.name}}</li>
</ul> </ul>
<h5 class="text-warning ml-3">Type</h5> <h5 class="text-warning ml-3">Type</h5>
<ul class="list-group my-3"> <ul class="list-group my-3">
<li class="list-group-item active" aria-current="true">An active item</li> <li class="list-group-item"
<li class="list-group-item">A second item</li> *ngFor="let type of productType"
<li class="list-group-item">A third item</li> [class.active]="type.id === this.shopParams.typeId"
<li class="list-group-item">A fourth item</li> [value]="type.id"
<li class="list-group-item">And a fifth one</li> (click)="onTypeSelected(type.id)"
>{{type.name}}</li>
</ul> </ul>
</section> </section>
<section class="col-9"> <section class="col-9">
<div class="d-flex justify-content-between align-items-center pb-2"> <div class="d-flex justify-content-between align-items-center pb-2">
<header> <app-paging-header
<span>Showing <strong>10</strong> of <strong>18</strong> Results</span> [totalCount]="totalCount"
</header> [pageSize]="this.shopParams.pageSize"
[pageNumber]="this.shopParams.pageNumber"
></app-paging-header>
<div class="row g-2 align-items-center"> <div class="row g-2 align-items-center">
<div class="col-sm"> <div class="col-sm">
<input style="width:300px" placeholder="Search" type="text" class="form-control"> <input (keyup.enter)="onSearch()" style="width:300px" #search placeholder="Search" type="text" class="form-control">
</div> </div>
<div class="col-sm"> <div class="col-sm">
<button class="btn btn-outline-primary my-3 me-2">Search</button> <button (click)="onSearch()" class="btn btn-outline-primary my-3 me-2">Search</button>
<button class="btn btn-outline-success my-3">Reset</button> <button (click)="OnReset()" class="btn btn-outline-success my-3">Reset</button>
</div> </div>
</div> </div>
</div> </div>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3"> <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
<div class="col"> <div class="col mb-3" *ngFor="let item of products">
<div class="card shadow-sm"> <app-product-item [product]="item"></app-product-item>
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#43484b;"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#dddad6;">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#43484b;"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#dddad6;">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#43484b;"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#dddad6;">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#43484b;"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#dddad6;">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#43484b;"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#dddad6;">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#43484b;"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#dddad6;">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#43484b;"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#dddad6;">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#43484b;"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#dddad6;">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#43484b;"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#dddad6;">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div> </div>
</div> </div>
<div class="d-flex justify-content-center" *ngIf="totalCount > 0">
<app-pager
[pageSize]="shopParams.pageSize"
[totalCount]="totalCount"
(pageChanged)="onPageChange($event)"
></app-pager>
</div> </div>
</section> </section>
</div> </div>

View File

@ -1,5 +1,8 @@
import { Component, OnInit } from '@angular/core'; import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { IBrand } from '../shared/models/brand';
import { IProduct } from '../shared/models/product'; import { IProduct } from '../shared/models/product';
import { IType } from '../shared/models/producttype';
import { ShopParams } from '../shared/models/shopparams';
import { ShopService } from './shop.service'; import { ShopService } from './shop.service';
@Component({ @Component({
@ -8,19 +11,95 @@ import { ShopService } from './shop.service';
styleUrls: ['./shop.component.scss'] styleUrls: ['./shop.component.scss']
}) })
export class ShopComponent implements OnInit { export class ShopComponent implements OnInit {
@ViewChild('search', {static: true}) searchTerm: ElementRef;
products: IProduct[]; products: IProduct[];
brands: IBrand[];
productType: IType[];
shopParams = new ShopParams();
totalCount: number;
sortOptions = [
{name: 'Name', value: 'name'},
{name: 'Price: Low to High', value: 'priceAsc'},
{name: 'Price: High to Low', value: 'priceDesc'}
];
constructor(private shopService: ShopService) { } constructor(private shopService: ShopService) { }
ngOnInit(): void { ngOnInit(): void {
this.shopService.getProducts().subscribe( this.getProducts();
this.getBrands();
this.getTypes();
}
getProducts(){
this.shopService.getProducts(this.shopParams).subscribe(
{ {
next: (response) => { this.products = response.data; }, next: (response) => {
this.products = response.data;
this.shopParams.pageNumber = response.pageIndex;
this.shopParams.pageSize = response.pageSize;
this.totalCount = response.count;
},
error: (e: any) => { console.log(e); }, error: (e: any) => { console.log(e); },
complete: () => { console.log('complete'); } complete: () => { console.log('complete'); }
} }
); );
} }
getBrands(){
this.shopService.getBrands().subscribe(
{
next: (response) => { this.brands = [{id: 0, name: 'All'}, ...response]; },
error: (e: any) => { console.log(e); },
complete: () => { console.log('complete'); }
}
);
}
getTypes(){
this.shopService.getTypes().subscribe(
{
next: (response) => { this.productType = [{id: 0, name: 'All'}, ...response]; },
error: (e: any) => { console.log(e); },
complete: () => { console.log('complete'); }
}
);
}
onBrandSelected(brandId: number){
this.shopParams.brandId = brandId;
this.shopParams.pageNumber = 1;
this.getProducts();
}
onTypeSelected(typeId: number){
this.shopParams.typeId = typeId;
this.shopParams.pageNumber = 1;
this.getProducts();
}
onSortSelected(sort: string){
this.shopParams.sort = sort;
this.getProducts();
}
onPageChange(e: any){
if (this.shopParams.pageNumber !== e){
this.shopParams.pageNumber = e;
this.getProducts();
}
}
onSearch(){
this.shopParams.search = this.searchTerm.nativeElement.value;
this.shopParams.pageNumber = 1;
this.getProducts();
}
OnReset(){
this.searchTerm.nativeElement.value = '';
this.shopParams = new ShopParams();
this.getProducts();
}
} }

View File

@ -1,15 +1,19 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ShopComponent } from './shop.component'; import { ShopComponent } from './shop.component';
import { ProductItemComponent } from './product-item/product-item.component';
import { SharedModule } from '../shared/shared.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
ShopComponent ShopComponent,
ProductItemComponent
], ],
imports: [ imports: [
CommonModule CommonModule,
SharedModule
], ],
exports: [ShopComponent] exports: [ShopComponent]
}) })

View File

@ -1,6 +1,10 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { IBrand } from '../shared/models/brand';
import { IPagination } from '../shared/models/pagination'; import { IPagination } from '../shared/models/pagination';
import { IType } from '../shared/models/producttype';
import { map } from 'rxjs/operators'
import { ShopParams } from '../shared/models/shopparams';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -10,7 +14,38 @@ export class ShopService {
constructor(private http: HttpClient) { } constructor(private http: HttpClient) { }
getProducts(){ getProducts(shopParams: ShopParams){
return this.http.get<IPagination>(this.baseURL + 'products'); let params = new HttpParams();
if (shopParams.brandId !== 0){
params = params.append('brandId', shopParams.brandId.toString());
}
if(shopParams.typeId !== 0){
params = params.append('typeId', shopParams.typeId.toString());
}
if (shopParams.search){
params = params.append('search', shopParams.search);
}
params = params.append('sort', shopParams.sort);
params = params.append('pageIndex', shopParams.pageNumber.toString());
params = params.append('pageIndex', shopParams.pageSize.toString());
return this.http.get<IPagination>(this.baseURL + 'products', {observe: 'response', params})
.pipe(
map(response => {
return response.body;
})
);
}
getBrands(){
return this.http.get<IBrand[]>(this.baseURL + 'products/brands');
}
getTypes(){
return this.http.get<IType[]>(this.baseURL + 'products/types');
} }
} }

View File

@ -18,6 +18,8 @@
] ]
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"strictTemplates": true,
"strictDomEventTypes": false,
"enableI18nLegacyMessageIdFormat": false "enableI18nLegacyMessageIdFormat": false
} }
} }