mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
Merged in feature/HIMA-67-seemless-product-search-v2 (pull request #40)
Feature/HIMA-67 seemless product search v2
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~7.2.0",
|
||||
"@angular/cdk": "7.0.0",
|
||||
"@angular/common": "~7.2.0",
|
||||
"@angular/compiler": "~7.2.0",
|
||||
"@angular/core": "~7.2.0",
|
||||
|
||||
@@ -34,7 +34,16 @@ import { FilterState } from './core/store/state/filter.state';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { AutocompleteState } from './core/store/state/autocomplete.state';
|
||||
const states = [FeedState, ProcessState, BreadcrumbsState, FilterState, AutocompleteState];
|
||||
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
|
||||
const states = [
|
||||
FeedState,
|
||||
ProcessState,
|
||||
BreadcrumbsState,
|
||||
FilterState,
|
||||
AutocompleteState
|
||||
];
|
||||
|
||||
export function _configInitializer(conf: ConfigService) {
|
||||
// load config from /assets/config.json
|
||||
@@ -75,7 +84,8 @@ export function _feedServiceEndpointProviderFactory(conf: ConfigService) {
|
||||
FeedServiceModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
SharedModule
|
||||
SharedModule,
|
||||
ScrollingModule
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@@ -104,7 +114,7 @@ export function _feedServiceEndpointProviderFactory(conf: ConfigService) {
|
||||
provide: FEED_SERVICE_ENDPOINT,
|
||||
useFactory: _feedServiceEndpointProviderFactory,
|
||||
deps: [ConfigService]
|
||||
},
|
||||
}
|
||||
// { provide: CatSearchService, useClass: CatSearchMockService }, // Uncomment if u want to use the CatSearchMockService
|
||||
// { provide: FeedService, useClass: FeedMockService } // Uncomment if u want to use the FeedMockService
|
||||
],
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="card-container">
|
||||
<div class="icon-container">
|
||||
<div class="product-icon"></div>
|
||||
</div>
|
||||
<div class="content-container">
|
||||
<div class="author align-left">
|
||||
<span>author</span>
|
||||
</div>
|
||||
<div class="title-price">
|
||||
<div class="title align-left">
|
||||
<div>
|
||||
<span>title </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="price align-right">
|
||||
<span>--</span>
|
||||
<span class="currency"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="type-stock">
|
||||
<div class="type align-left">
|
||||
<div class="type-text align-left">
|
||||
<span>type</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,160 @@
|
||||
@import '../../../assets/scss/variables';
|
||||
.card-container {
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto;
|
||||
grid-gap: 7vh;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
padding: 24px;
|
||||
min-height: 134px;
|
||||
}
|
||||
|
||||
.recommanded {
|
||||
background-color: $important-notification;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.product-icon {
|
||||
width: 69px;
|
||||
height: 100%;
|
||||
background-color: #e9edf9;
|
||||
color: #e9edf9;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
opacity: 1;
|
||||
animation: load 1.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes load {
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.author span {
|
||||
font-size: 16px;
|
||||
background-color: #e9edf9;
|
||||
color: #e9edf9;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.title-price {
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
grid-gap: 8vh;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.title span {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
background-color: #e9edf9;
|
||||
color: #e9edf9;
|
||||
width: 500px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.price span {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
background-color: #e9edf9;
|
||||
color: #e9edf9;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.title-grid {
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto;
|
||||
grid-gap: 2vh;
|
||||
}
|
||||
|
||||
.rec-icon-container {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.currency {
|
||||
margin-left: 5px;
|
||||
display: inline-block;
|
||||
background-color: #e9edf9;
|
||||
color: #e9edf9;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.type-stock {
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.type {
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto;
|
||||
grid-gap: 2vh;
|
||||
background-color: #e9edf9;
|
||||
color: #e9edf9;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.type-text {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.available-stock {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto auto;
|
||||
grid-gap: 2vh;
|
||||
}
|
||||
|
||||
.available-icon-container {
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.stock-icon {
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.available-text span {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stock span {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.publisher-order {
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.publisher-serial {
|
||||
display: grid;
|
||||
grid-template-columns: max-content min-content auto;
|
||||
grid-gap: 2vh;
|
||||
}
|
||||
|
||||
.publisher-serial span {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.order span {
|
||||
font-size: 16px;
|
||||
color: #a7b9cb;
|
||||
}
|
||||
|
||||
.type-icon-container {
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.publisher {
|
||||
max-width: 300px;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-card-loading',
|
||||
templateUrl: './product-card-loading.component.html',
|
||||
styleUrls: ['./product-card-loading.component.scss']
|
||||
})
|
||||
export class ProductCardLoadingComponent implements OnInit {
|
||||
ngOnInit() {}
|
||||
}
|
||||
@@ -1,183 +1,166 @@
|
||||
@import "../../../assets/scss/variables";
|
||||
@import '../../../assets/scss/variables';
|
||||
|
||||
.card-container {
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto;
|
||||
grid-gap: 7vh;
|
||||
background-color: white;
|
||||
padding: 24px;
|
||||
margin-top: 10px;
|
||||
border-radius: 5px;
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto;
|
||||
grid-gap: 7vh;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
padding: 24px;
|
||||
min-height: 134px;
|
||||
}
|
||||
|
||||
.recommanded {
|
||||
background-color: $important-notification;
|
||||
color: white;
|
||||
background-color: $important-notification;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.product-icon {
|
||||
width: 69px;
|
||||
width: 69px;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
|
||||
.author span {
|
||||
font-size: 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.title-price {
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
grid-gap: 8vh;
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
grid-gap: 8vh;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.title span {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.price span {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.title-grid {
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto;
|
||||
grid-gap: 2vh;
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto;
|
||||
grid-gap: 2vh;
|
||||
}
|
||||
|
||||
.rec-icon-container {
|
||||
padding-top: 4px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.currency {
|
||||
margin-left: 5px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.type-stock {
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.type {
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto;
|
||||
grid-gap: 2vh;
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto;
|
||||
grid-gap: 2vh;
|
||||
}
|
||||
|
||||
.type-text {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.available-stock {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto auto;
|
||||
grid-gap: 2vh;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto auto;
|
||||
grid-gap: 2vh;
|
||||
}
|
||||
|
||||
.available-icon-container {
|
||||
padding-top: 2px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.stock-icon {
|
||||
width: 15px;
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.available-text span {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stock span {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.publisher-order {
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.publisher-serial {
|
||||
display: grid;
|
||||
grid-template-columns: max-content min-content auto;
|
||||
grid-gap: 2vh;
|
||||
display: grid;
|
||||
grid-template-columns: max-content min-content auto;
|
||||
grid-gap: 2vh;
|
||||
}
|
||||
|
||||
.publisher-serial span {
|
||||
font-size: 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.order span {
|
||||
font-size: 16px;
|
||||
color: #a7b9cb;
|
||||
font-size: 16px;
|
||||
color: #a7b9cb;
|
||||
}
|
||||
|
||||
.type-icon-container {
|
||||
padding-top: 3px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.publisher {
|
||||
max-width: 300px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
|
||||
/* Portrait */
|
||||
@media only screen
|
||||
and (min-device-width: 768px)
|
||||
and (max-device-width: 1024px)
|
||||
and (orientation: portrait)
|
||||
and (-webkit-min-device-pixel-ratio: 2) {
|
||||
.title span {
|
||||
max-width: 370px;
|
||||
}
|
||||
@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: portrait) and (-webkit-min-device-pixel-ratio: 2) {
|
||||
.title span {
|
||||
max-width: 370px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Landscape */
|
||||
@media only screen
|
||||
and (min-device-width: 768px)
|
||||
and (max-device-width: 1024px)
|
||||
and (orientation: landscape)
|
||||
and (-webkit-min-device-pixel-ratio: 2) {
|
||||
.title span {
|
||||
max-width: 810px;
|
||||
}
|
||||
@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: landscape) and (-webkit-min-device-pixel-ratio: 2) {
|
||||
.title span {
|
||||
max-width: 810px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Portrait */
|
||||
/* Declare the same value for min- and max-width to avoid colliding with desktops */
|
||||
/* Source: https://medium.com/connect-the-dots/css-media-queries-for-ipad-pro-8cad10e17106*/
|
||||
@media only screen
|
||||
and (min-device-width: 1024px)
|
||||
and (max-device-width: 1024px)
|
||||
and (orientation: portrait)
|
||||
and (-webkit-min-device-pixel-ratio: 2) {
|
||||
.title span {
|
||||
max-width: 565px;
|
||||
}
|
||||
@media only screen and (min-device-width: 1024px) and (max-device-width: 1024px) and (orientation: portrait) and (-webkit-min-device-pixel-ratio: 2) {
|
||||
.title span {
|
||||
max-width: 565px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Landscape */
|
||||
/* Declare the same value for min- and max-width to avoid colliding with desktops */
|
||||
/* Source: https://medium.com/connect-the-dots/css-media-queries-for-ipad-pro-8cad10e17106*/
|
||||
@media only screen
|
||||
and (min-device-width: 1366px)
|
||||
and (max-device-width: 1366px)
|
||||
and (orientation: landscape)
|
||||
and (-webkit-min-device-pixel-ratio: 2) {
|
||||
.title span {
|
||||
max-width: 900px;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-device-width: 1366px) and (max-device-width: 1366px) and (orientation: landscape) and (-webkit-min-device-pixel-ratio: 2) {
|
||||
.title span {
|
||||
max-width: 900px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
<div class="result-container">
|
||||
<app-filter></app-filter>
|
||||
<div
|
||||
id="start"
|
||||
infiniteScroll
|
||||
[infiniteScrollDistance]="1"
|
||||
[infiniteScrollThrottle]="50"
|
||||
(scrolled)="onScroll()"
|
||||
[scrollWindow]="true"
|
||||
>
|
||||
<app-product-card *ngFor="let product of products" [product]="product">
|
||||
</app-product-card>
|
||||
<div *ngIf="!ds || (ds.loading && !ds.results)">
|
||||
<div [@stagger]="'yes'">
|
||||
<div *ngFor="let dummy of dummies" [style.marginTop.px]="10">
|
||||
<app-product-card-loading> </app-product-card-loading>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- [style.padding.px]="30" -->
|
||||
|
||||
<cdk-virtual-scroll-viewport itemSize="180" class="viewport">
|
||||
<div *cdkVirtualFor="let product of ds" class="product-item">
|
||||
<app-product-card
|
||||
[product]="product"
|
||||
*ngIf="product != null; else loadingComponent"
|
||||
>
|
||||
</app-product-card>
|
||||
<ng-template #loadingComponent>
|
||||
<app-product-card-loading></app-product-card-loading>
|
||||
</ng-template>
|
||||
</div>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
<app-loading
|
||||
loading="loading"
|
||||
*ngIf="!ds || ds.loading"
|
||||
[style.marginTop.px]="60"
|
||||
[style.marginBottom.px]="60"
|
||||
loading="true"
|
||||
text="Inhalte werden geladen"
|
||||
></app-loading>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
app-product-card-loading {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
app-filter {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.result-container {
|
||||
height: calc(100% - 100px);
|
||||
}
|
||||
.viewport {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
height: 180px;
|
||||
overflow: hidden;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,49 @@
|
||||
import { FilterState } from './../../core/store/state/filter.state';
|
||||
import { ProductService } from './../../core/services/product.service';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Search } from 'src/app/core/models/search.model';
|
||||
import { Process } from 'src/app/core/models/process.model';
|
||||
import { Product } from 'src/app/core/models/product.model';
|
||||
import { Router } from '@angular/router';
|
||||
import { map, filter } from 'rxjs/operators';
|
||||
import { map, filter, take } from 'rxjs/operators';
|
||||
import { Select, Store } from '@ngxs/store';
|
||||
import { ItemDTO } from 'dist/cat-service/lib/dtos';
|
||||
import { Observable } from 'rxjs';
|
||||
import { GetProducts } from 'src/app/core/store/actions/product.actions';
|
||||
import { Observable, BehaviorSubject, Subscription } from 'rxjs';
|
||||
import {
|
||||
GetProducts,
|
||||
SetProducts
|
||||
} from 'src/app/core/store/actions/product.actions';
|
||||
import { ProcessState } from 'src/app/core/store/state/process.state';
|
||||
import {
|
||||
AddSearch,
|
||||
AllowProductLoad
|
||||
} from 'src/app/core/store/actions/process.actions';
|
||||
import { ProductMapping } from 'src/app/core/mappings/product.mapping';
|
||||
import { staggerAnimation } from './stagger.animation';
|
||||
import { DataSource, CollectionViewer } from '@angular/cdk/collections';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search-results',
|
||||
templateUrl: './search-results.component.html',
|
||||
styleUrls: ['./search-results.component.scss']
|
||||
styleUrls: ['./search-results.component.scss'],
|
||||
animations: [staggerAnimation]
|
||||
})
|
||||
export class SearchResultsComponent implements OnInit {
|
||||
@Select(ProcessState.getCurrentProcess) process$;
|
||||
currentSearch: Search;
|
||||
products: Product[];
|
||||
loading = true;
|
||||
@Select(ProcessState.getProducts) products$: Observable<ItemDTO[]>;
|
||||
skip = 0;
|
||||
|
||||
dummies = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
ds: SearchDataSource;
|
||||
|
||||
constructor(
|
||||
private store: Store,
|
||||
private router: Router,
|
||||
private productMapping: ProductMapping
|
||||
) { }
|
||||
private productMapping: ProductMapping,
|
||||
private productService: ProductService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.loadCurrentSearch();
|
||||
@@ -39,8 +51,23 @@ export class SearchResultsComponent implements OnInit {
|
||||
this.router.navigate(['dashboard']);
|
||||
return;
|
||||
}
|
||||
this.store.dispatch(new GetProducts(this.currentSearch));
|
||||
this.loadProducts();
|
||||
this.ds = new SearchDataSource(
|
||||
this.productService,
|
||||
this.currentSearch.query,
|
||||
this.store,
|
||||
[]
|
||||
);
|
||||
|
||||
this.store.select(FilterState.getFilters).subscribe(fil => {
|
||||
if (!!fil) {
|
||||
this.ds = new SearchDataSource(
|
||||
this.productService,
|
||||
this.currentSearch.query,
|
||||
this.store,
|
||||
fil
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadCurrentSearch() {
|
||||
@@ -68,9 +95,94 @@ export class SearchResultsComponent implements OnInit {
|
||||
filter(f => Array.isArray(f)),
|
||||
map(items => items.map(item => this.productMapping.fromItemDTO(item)))
|
||||
)
|
||||
.subscribe(
|
||||
data => (this.products = data),
|
||||
() => this.loading = false
|
||||
);
|
||||
.subscribe(data => (this.products = data));
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchDataSource extends DataSource<Product | undefined> {
|
||||
private pageSize = 10;
|
||||
private cachedData = Array.from<Product>({ length: 0 });
|
||||
private cachedItemsDTO = Array.from<ItemDTO>({ length: 0 });
|
||||
private fetchedPages = new Set<number>();
|
||||
private dataStream = new BehaviorSubject<(Product | undefined)[]>(
|
||||
this.cachedData
|
||||
);
|
||||
private subscription = new Subscription();
|
||||
public loading = true;
|
||||
public results = false;
|
||||
private productMapping = new ProductMapping();
|
||||
|
||||
constructor(
|
||||
private searchService: ProductService,
|
||||
private search: string,
|
||||
private store: Store,
|
||||
private filters: any[]
|
||||
) {
|
||||
super();
|
||||
}
|
||||
connect(
|
||||
collectionViewer: CollectionViewer
|
||||
): Observable<(Product | undefined)[]> {
|
||||
this.subscription.add(
|
||||
collectionViewer.viewChange.subscribe(range => {
|
||||
const startPage = this.getPageForIndex(range.start);
|
||||
const endPage = this.getPageForIndex(range.end - 1);
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
this.fetchPage(i);
|
||||
}
|
||||
})
|
||||
);
|
||||
this.fetchPage(0);
|
||||
|
||||
return this.dataStream;
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
private getPageForIndex(index: number): number {
|
||||
return Math.floor(index / this.pageSize);
|
||||
}
|
||||
|
||||
private fetchPage(page: number) {
|
||||
if (page == 0) {
|
||||
this.results = false;
|
||||
}
|
||||
if (this.fetchedPages.has(page)) {
|
||||
return;
|
||||
}
|
||||
this.fetchedPages.add(page);
|
||||
this.loading = true;
|
||||
|
||||
this.searchService
|
||||
.searchItemsWithPagination(
|
||||
this.search,
|
||||
page * this.pageSize,
|
||||
this.pageSize,
|
||||
this.filters
|
||||
)
|
||||
.pipe(take(1))
|
||||
.subscribe(data => {
|
||||
this.loading = false;
|
||||
this.results = true;
|
||||
|
||||
if (page === 0) {
|
||||
this.cachedData = Array.from<Product>({ length: data.hits });
|
||||
this.cachedItemsDTO = Array.from<ItemDTO>({ length: data.hits });
|
||||
}
|
||||
this.cachedItemsDTO.splice(
|
||||
page * this.pageSize,
|
||||
this.pageSize,
|
||||
...data.result
|
||||
);
|
||||
this.cachedData.splice(
|
||||
page * this.pageSize,
|
||||
this.pageSize,
|
||||
...data.result.map(item => this.productMapping.fromItemDTO(item))
|
||||
);
|
||||
this.dataStream.next(this.cachedData);
|
||||
this.store.dispatch(new SetProducts([...this.cachedItemsDTO]));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
21
src/app/components/search-results/stagger.animation.ts
Normal file
21
src/app/components/search-results/stagger.animation.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
trigger,
|
||||
transition,
|
||||
stagger,
|
||||
animate,
|
||||
style,
|
||||
query
|
||||
} from '@angular/animations';
|
||||
|
||||
export const staggerAnimation = trigger('stagger', [
|
||||
transition('* => *', [
|
||||
query(
|
||||
':enter',
|
||||
[
|
||||
style({ opacity: 0 }),
|
||||
stagger(100, [animate(300, style({ opacity: 1 }))])
|
||||
],
|
||||
{ optional: true }
|
||||
)
|
||||
])
|
||||
]);
|
||||
@@ -8,6 +8,7 @@ export interface Process {
|
||||
id: number;
|
||||
name: string;
|
||||
new: boolean;
|
||||
loading: boolean;
|
||||
selected: boolean;
|
||||
icon: string;
|
||||
currentRoute: string;
|
||||
|
||||
@@ -14,47 +14,54 @@ import { map } from 'rxjs/operators';
|
||||
import { Search } from '../models/search.model';
|
||||
import { FilterMapping } from '../mappings/filter.mapping';
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ProductService {
|
||||
|
||||
searchResponse$: Observable<PagedApiResponse<ItemDTO>>;
|
||||
|
||||
constructor(
|
||||
private searchService: CatSearchService,
|
||||
private filterMapping: FilterMapping
|
||||
) { }
|
||||
) {}
|
||||
|
||||
persistLastSearchToLocalStorage(param: string) {
|
||||
// get recent searches from local storage
|
||||
const recentSearches: RecentArticleSearch[] = JSON.parse(localStorage.getItem('recent_searches'));
|
||||
const recentSearches: RecentArticleSearch[] = JSON.parse(
|
||||
localStorage.getItem('recent_searches')
|
||||
);
|
||||
/*
|
||||
* check if there are search items in local storage, if there are not add search to local storage
|
||||
* else check if current search already exists in loacl storage, if exist delete it (deletion is made becouse
|
||||
* we want every search to display in LIFO order).
|
||||
* finaly push the new search at the end of the local storage array
|
||||
*/
|
||||
* check if there are search items in local storage, if there are not add search to local storage
|
||||
* else check if current search already exists in loacl storage, if exist delete it (deletion is made becouse
|
||||
* we want every search to display in LIFO order).
|
||||
* finaly push the new search at the end of the local storage array
|
||||
*/
|
||||
if (recentSearches) {
|
||||
const searches = [...recentSearches.filter((data) => {
|
||||
return data.name !== param;
|
||||
}), <RecentArticleSearch>{
|
||||
id: recentSearches[recentSearches.length - 1].id + 1,
|
||||
name: param
|
||||
}];
|
||||
const searches = [
|
||||
...recentSearches.filter(data => {
|
||||
return data.name !== param;
|
||||
}),
|
||||
<RecentArticleSearch>{
|
||||
id: recentSearches[recentSearches.length - 1].id + 1,
|
||||
name: param
|
||||
}
|
||||
];
|
||||
localStorage.setItem('recent_searches', JSON.stringify(searches));
|
||||
} else {
|
||||
const searches = [<RecentArticleSearch>{
|
||||
id: 1,
|
||||
name: param
|
||||
}];
|
||||
const searches = [
|
||||
<RecentArticleSearch>{
|
||||
id: 1,
|
||||
name: param
|
||||
}
|
||||
];
|
||||
localStorage.setItem('recent_searches', JSON.stringify(searches));
|
||||
}
|
||||
}
|
||||
|
||||
getRecentSearches(): Observable<RecentArticleSearch[]> {
|
||||
const recentSearches: RecentArticleSearch[] = JSON.parse(localStorage.getItem('recent_searches'));
|
||||
const recentSearches: RecentArticleSearch[] = JSON.parse(
|
||||
localStorage.getItem('recent_searches')
|
||||
);
|
||||
return of(recentSearches);
|
||||
}
|
||||
// placeholder service method for calling product search API
|
||||
@@ -66,13 +73,16 @@ export class ProductService {
|
||||
// service method for calling product search API
|
||||
searchItems(search: Search): Observable<ItemDTO[]> {
|
||||
this.persistLastSearchToLocalStorage(search.query);
|
||||
const queryToken = <QueryTokenDTO> {
|
||||
input: {qs: search.query},
|
||||
const queryToken = <QueryTokenDTO>{
|
||||
input: { qs: search.query },
|
||||
skip: search.skip,
|
||||
take: search.take
|
||||
};
|
||||
|
||||
const queryWithFilters = this.filterMapping.toQueryTokenDto(queryToken, search.fitlers);
|
||||
const queryWithFilters = this.filterMapping.toQueryTokenDto(
|
||||
queryToken,
|
||||
search.fitlers
|
||||
);
|
||||
return this.searchService.search(queryWithFilters).pipe(
|
||||
map(response => {
|
||||
if (response.error) {
|
||||
@@ -84,6 +94,31 @@ export class ProductService {
|
||||
);
|
||||
}
|
||||
|
||||
searchItemsWithPagination(
|
||||
query,
|
||||
skip,
|
||||
size,
|
||||
filters
|
||||
): Observable<PagedApiResponse<ItemDTO>> {
|
||||
const queryToken = <QueryTokenDTO>{
|
||||
input: { qs: query },
|
||||
skip: skip,
|
||||
take: size
|
||||
};
|
||||
const queryWithFilters = this.filterMapping.toQueryTokenDto(
|
||||
queryToken,
|
||||
filters
|
||||
);
|
||||
return this.searchService.search(queryWithFilters).pipe(
|
||||
map(response => {
|
||||
if (response.error) {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getItemById(id: number): Observable<ItemDTO> {
|
||||
return this.searchService.getById(id).pipe(
|
||||
map(response => {
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import { ItemDTO } from 'projects/cat-service/src/lib';
|
||||
import { Search } from '../../models/search.model';
|
||||
import { Product } from '../../models/product.model';
|
||||
|
||||
export const LOAD_RECENT_PRODUCTS = '[PRODUCTS] Load recent';
|
||||
export const GET_PRODUCTS = '[PRODUCTS] Get';
|
||||
export const SET_PRODUCTS = '[PRODUCTS] Set';
|
||||
export const ADD_SELECTED_PRODUCT = '[PRODUCTS] Add selected';
|
||||
|
||||
export class LoadRecentProducts {
|
||||
static readonly type = LOAD_RECENT_PRODUCTS;
|
||||
static readonly type = LOAD_RECENT_PRODUCTS;
|
||||
|
||||
constructor() {}
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
export class GetProducts {
|
||||
static readonly type = GET_PRODUCTS;
|
||||
static readonly type = GET_PRODUCTS;
|
||||
|
||||
constructor(public payload: Search) {}
|
||||
constructor(public payload: Search) {}
|
||||
}
|
||||
|
||||
export class SetProducts {
|
||||
static readonly type = SET_PRODUCTS;
|
||||
|
||||
constructor(public payload: ItemDTO[]) {}
|
||||
}
|
||||
|
||||
export class AddSelectedProduct {
|
||||
static readonly type = ADD_SELECTED_PRODUCT;
|
||||
static readonly type = ADD_SELECTED_PRODUCT;
|
||||
|
||||
constructor(public payload: ItemDTO) {}
|
||||
constructor(public payload: ItemDTO) {}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ import { RecentArticleSearch } from '../../models/recent-article-search.model';
|
||||
import {
|
||||
GetProducts,
|
||||
LoadRecentProducts,
|
||||
AddSelectedProduct
|
||||
AddSelectedProduct,
|
||||
SetProducts
|
||||
} from '../actions/product.actions';
|
||||
import { ItemDTO } from 'dist/cat-service/lib/dtos';
|
||||
import { getCurrentProcess } from '../../utils/process.util';
|
||||
@@ -388,6 +389,12 @@ export class ProcessState {
|
||||
...state
|
||||
});
|
||||
} else {
|
||||
ctx.patchState({
|
||||
...state,
|
||||
processes: state.processes.map(p =>
|
||||
p.id !== currentProcess.id ? p : { ...currentProcess, loading: true }
|
||||
)
|
||||
});
|
||||
this.productService.searchItems(payload).subscribe((items: ItemDTO[]) => {
|
||||
if (items) {
|
||||
ctx.patchState({
|
||||
@@ -407,6 +414,19 @@ export class ProcessState {
|
||||
});
|
||||
}
|
||||
}
|
||||
@Action(SetProducts, { cancelUncompleted: true })
|
||||
setProducts(ctx: StateContext<ProcessStateModel>, { payload }: SetProducts) {
|
||||
const state = ctx.getState();
|
||||
const currentProcess = getCurrentProcess(state.processes);
|
||||
|
||||
ctx.patchState({
|
||||
...state,
|
||||
processes: this.changeProducResultsForCurrentProcess(
|
||||
state.processes,
|
||||
payload
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
@Action(LoadRecentProducts)
|
||||
loadRecentProducts(ctx: StateContext<ProcessStateModel>) {
|
||||
@@ -485,7 +505,7 @@ export class ProcessState {
|
||||
): Process[] {
|
||||
const newProcessState = processes.map(process => {
|
||||
if (process.selected === true) {
|
||||
return { ...process, itemsDTO: items };
|
||||
return { ...process, itemsDTO: items, loading: false };
|
||||
}
|
||||
return { ...process };
|
||||
});
|
||||
@@ -498,7 +518,11 @@ export class ProcessState {
|
||||
): Process[] {
|
||||
const newProcessState = processes.map(process => {
|
||||
if (process.selected === true) {
|
||||
return { ...process, itemsDTO: [...process.itemsDTO, ...items] };
|
||||
return {
|
||||
...process,
|
||||
itemsDTO: [...process.itemsDTO, ...items],
|
||||
loading: false
|
||||
};
|
||||
}
|
||||
return { ...process };
|
||||
});
|
||||
|
||||
@@ -24,6 +24,8 @@ import { ProductDetailsComponent } from '../components/product-details/product-d
|
||||
import { CheckoutComponent } from '../components/checkout/checkout.component';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { ProcessModule } from './process/process.module';
|
||||
import { ProductCardLoadingComponent } from '../components/product-card-loading/product-card-loading.component';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -36,11 +38,13 @@ import { ProcessModule } from './process/process.module';
|
||||
SearchResultsComponent,
|
||||
ProductCardComponent,
|
||||
ProductDetailsComponent,
|
||||
CheckoutComponent
|
||||
CheckoutComponent,
|
||||
ProductCardLoadingComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
AppRoutingModule,
|
||||
ScrollingModule,
|
||||
FormsModule,
|
||||
SharedModule,
|
||||
NewsletterSignupModule,
|
||||
|
||||
@@ -63,6 +63,8 @@
|
||||
margin-bottom: 60px;
|
||||
display: flex;
|
||||
flex: row;
|
||||
margin-left: -20px;
|
||||
margin-right: -20px;
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
@@ -87,6 +89,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.text {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.additional {
|
||||
flex: 0.0001;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
@import "../../../assets/scss/variables";
|
||||
@import '../../../assets/scss/variables';
|
||||
|
||||
.content-body {
|
||||
height:100%;
|
||||
background-color: $hima-content-color;
|
||||
padding-top: 150px;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
background-color: $hima-content-color;
|
||||
padding-top: 150px;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
position: absolute;
|
||||
width: calc(100% - 30px);
|
||||
height: calc(100% - 230px);
|
||||
top: 0;
|
||||
}
|
||||
|
||||
14
yarn.lock
14
yarn.lock
@@ -128,6 +128,14 @@
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
"@angular/cdk@7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-7.0.0.tgz#b98d7378e84fed1af30c460aa91af4ada2cf252b"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
optionalDependencies:
|
||||
parse5 "^5.0.0"
|
||||
|
||||
"@angular/cli@~7.2.1":
|
||||
version "7.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-7.2.2.tgz#408dc7cd69931301c108ee2637836f0e9e7e3f02"
|
||||
@@ -4958,6 +4966,10 @@ parse5@4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
|
||||
|
||||
parse5@^5.0.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
|
||||
|
||||
parseqs@0.0.5:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
|
||||
@@ -6658,7 +6670,7 @@ tsickle@>=0.34.0:
|
||||
mkdirp "^0.5.1"
|
||||
source-map "^0.7.3"
|
||||
|
||||
tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
|
||||
tslib@^1.7.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user