Merged in feature/HIMA-67-seemless-product-search-v2 (pull request #40)

Feature/HIMA-67 seemless product search v2
This commit is contained in:
Peter Skrlj
2019-02-12 09:18:42 +00:00
18 changed files with 613 additions and 162 deletions

View File

@@ -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",

View File

@@ -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
],

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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() {}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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]));
});
}
}

View 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 }
)
])
]);

View File

@@ -8,6 +8,7 @@ export interface Process {
id: number;
name: string;
new: boolean;
loading: boolean;
selected: boolean;
icon: string;
currentRoute: string;

View File

@@ -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 => {

View File

@@ -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) {}
}

View File

@@ -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 };
});

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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"