feat(ux): implement sprint 11 user experience module
This commit is contained in:
@@ -10,13 +10,19 @@
|
||||
[showStock]="showAvailability"
|
||||
[showRating]="showRatings"
|
||||
[showDiscountBadge]="showDiscounts"
|
||||
[showFavoritePlaceholder]="showActionsPlaceholder"
|
||||
[showComparePlaceholder]="showActionsPlaceholder"
|
||||
[showFavoriteAction]="showActionsPlaceholder"
|
||||
[showCompareAction]="showActionsPlaceholder"
|
||||
[showShareAction]="showShareAction"
|
||||
[showQuickViewPlaceholder]="showActionsPlaceholder"
|
||||
[isFavorite]="isFavorite(product)"
|
||||
[isCompared]="isCompared(product)"
|
||||
[addToCartLabel]="'catalog.addToCart' | translate"
|
||||
(selected)="productSelected.emit(product)"
|
||||
(addToCart)="onAddToCart(product, $event.event)"
|
||||
(preview)="productPreview.emit($event)"
|
||||
(favoriteToggled)="favoriteToggled.emit(product)"
|
||||
(compareToggled)="compareToggled.emit(product)"
|
||||
(shareRequested)="shareRequested.emit(product)"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -21,10 +21,16 @@ export class CatalogProductGridComponent {
|
||||
@Input() showDiscounts = true;
|
||||
@Input() showAvailability = true;
|
||||
@Input() showActionsPlaceholder = false;
|
||||
@Input() showShareAction = false;
|
||||
@Input() favoriteIds: number[] = [];
|
||||
@Input() comparedIds: number[] = [];
|
||||
|
||||
@Output() productSelected = new EventEmitter<Product>();
|
||||
@Output() addToCart = new EventEmitter<{ product: Product; event: Event }>();
|
||||
@Output() productPreview = new EventEmitter<number>();
|
||||
@Output() favoriteToggled = new EventEmitter<Product>();
|
||||
@Output() compareToggled = new EventEmitter<Product>();
|
||||
@Output() shareRequested = new EventEmitter<Product>();
|
||||
|
||||
private readonly languageService = inject(LanguageService);
|
||||
|
||||
@@ -42,6 +48,14 @@ export class CatalogProductGridComponent {
|
||||
this.addToCart.emit({ product, event });
|
||||
}
|
||||
|
||||
isFavorite(product: Product): boolean {
|
||||
return this.favoriteIds.includes(product.itemID);
|
||||
}
|
||||
|
||||
isCompared(product: Product): boolean {
|
||||
return this.comparedIds.includes(product.itemID);
|
||||
}
|
||||
|
||||
layoutClass(): string {
|
||||
switch (this.layout) {
|
||||
case 'large-grid':
|
||||
|
||||
@@ -18,9 +18,15 @@
|
||||
[showDiscounts]="showDiscounts"
|
||||
[showAvailability]="showAvailability"
|
||||
[showActionsPlaceholder]="true"
|
||||
[showShareAction]="showShareAction"
|
||||
[favoriteIds]="favoriteIds"
|
||||
[comparedIds]="comparedIds"
|
||||
(productSelected)="productSelected.emit($event)"
|
||||
(addToCart)="addToCart.emit($event)"
|
||||
(productPreview)="productPreview.emit($event)" />
|
||||
(productPreview)="productPreview.emit($event)"
|
||||
(favoriteToggled)="favoriteToggled.emit($event)"
|
||||
(compareToggled)="compareToggled.emit($event)"
|
||||
(shareRequested)="shareRequested.emit($event)" />
|
||||
} @else {
|
||||
<div class="empty-state card">
|
||||
<h3>No results found</h3>
|
||||
|
||||
@@ -22,11 +22,17 @@ export class CatalogSearchResultsComponent {
|
||||
@Input() showRatings = true;
|
||||
@Input() showDiscounts = true;
|
||||
@Input() showAvailability = true;
|
||||
@Input() showShareAction = true;
|
||||
@Input() favoriteIds: number[] = [];
|
||||
@Input() comparedIds: number[] = [];
|
||||
|
||||
@Output() pageChange = new EventEmitter<number>();
|
||||
@Output() productSelected = new EventEmitter<Product>();
|
||||
@Output() addToCart = new EventEmitter<{ product: Product; event: Event }>();
|
||||
@Output() productPreview = new EventEmitter<number>();
|
||||
@Output() favoriteToggled = new EventEmitter<Product>();
|
||||
@Output() compareToggled = new EventEmitter<Product>();
|
||||
@Output() shareRequested = new EventEmitter<Product>();
|
||||
|
||||
get totalPages(): number {
|
||||
return Math.max(1, Math.ceil(this.total / Math.max(1, this.pageSize)));
|
||||
|
||||
@@ -24,6 +24,17 @@
|
||||
(suggestionSelected)="useSuggestion($event)"
|
||||
(recentSelected)="useRecentSearch($event)"
|
||||
(historyCleared)="clearSearchHistory()" />
|
||||
|
||||
@if (userExperienceConfig().savedSearches.enabled && savedSearches().length > 0) {
|
||||
<div class="catalog-saved-searches">
|
||||
@for (saved of savedSearches(); track saved.id) {
|
||||
<div class="catalog-saved-chip">
|
||||
<button type="button" (click)="useSavedSearch(saved.id)">{{ saved.name }}</button>
|
||||
<button type="button" class="catalog-saved-chip-remove" (click)="removeSavedSearch(saved.id)">×</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
|
||||
@if (loading()) {
|
||||
@@ -98,6 +109,10 @@
|
||||
[selected]="state().sort"
|
||||
(selectedChange)="changeSort($event)" />
|
||||
|
||||
@if (userExperienceConfig().savedSearches.enabled) {
|
||||
<button type="button" class="catalog-save-search-btn" (click)="saveCurrentSearch()">{{ 'ux.saveSearch' | translate }}</button>
|
||||
}
|
||||
|
||||
<app-catalog-layout-switcher
|
||||
[active]="state().layout"
|
||||
[available]="availableLayouts"
|
||||
@@ -122,10 +137,16 @@
|
||||
[showRatings]="catalogConfig().showRatings"
|
||||
[showDiscounts]="catalogConfig().showDiscounts"
|
||||
[showAvailability]="catalogConfig().showAvailability"
|
||||
[showShareAction]="userExperienceConfig().share.enabled"
|
||||
[favoriteIds]="favoriteIds()"
|
||||
[comparedIds]="comparedIds()"
|
||||
(pageChange)="onResultsPageChange($event)"
|
||||
(productSelected)="selectProduct($event)"
|
||||
(addToCart)="addToCart($event)"
|
||||
(productPreview)="previewProduct($event)" />
|
||||
(productPreview)="previewProduct($event)"
|
||||
(favoriteToggled)="onFavoriteToggled($event)"
|
||||
(compareToggled)="onCompareToggled($event)"
|
||||
(shareRequested)="onShareRequested($event)" />
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
@@ -12,6 +12,35 @@
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.catalog-saved-searches {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.catalog-saved-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 999px;
|
||||
background: var(--bg-primary);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.catalog-saved-chip button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.catalog-saved-chip-remove {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.catalog-root-link {
|
||||
width: fit-content;
|
||||
color: #1e3c38;
|
||||
@@ -72,6 +101,17 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.catalog-save-search-btn {
|
||||
min-height: 36px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
padding: 0 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.catalog-category-banner {
|
||||
padding: 16px;
|
||||
background: linear-gradient(120deg, color-mix(in srgb, var(--primary-color) 14%, white), #f8fbfb);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, DestroyRef, computed, inject, signal } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, DestroyRef, HostListener, computed, inject, signal } from '@angular/core';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { combineLatest } from 'rxjs';
|
||||
@@ -9,12 +9,13 @@ import { Product } from '../../../../core/products/models/product-domain.model';
|
||||
import { ConfigService } from '../../../../core/config/config.service';
|
||||
import { CategoryFacade } from '../../../../facades/platform/category.facade';
|
||||
import { ProductFacade } from '../../../../facades/platform/product.facade';
|
||||
import { UserExperienceFacade } from '../../../../facades/platform/user-experience.facade';
|
||||
import { CartService } from '../../../../services';
|
||||
import { LanguageService } from '../../../../services/language.service';
|
||||
import { PrefetchService } from '../../../../services/prefetch.service';
|
||||
import { LangRoutePipe } from '../../../../pipes/lang-route.pipe';
|
||||
import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||
import { DEFAULT_CATALOG_CONFIG } from '../../../../shared/models/config';
|
||||
import { DEFAULT_CATALOG_CONFIG, DEFAULT_USER_EXPERIENCE_CONFIG } from '../../../../shared/models/config';
|
||||
import { CatalogCategoryGridComponent } from '../components/category-grid/category-grid.component';
|
||||
import { CatalogFiltersPanelComponent, CatalogFilterStateValue } from '../components/filters-panel/filters-panel.component';
|
||||
import { CatalogLayoutSwitcherComponent } from '../components/layout-switcher/layout-switcher.component';
|
||||
@@ -23,6 +24,8 @@ import { CatalogSearchResultsComponent } from '../components/search-results/sear
|
||||
import { CatalogSortingControlComponent } from '../components/sorting-control/sorting-control.component';
|
||||
import { CatalogState, createInitialCatalogState } from '../models/catalog-state.model';
|
||||
import { CatalogSearchHistoryService } from '../services/catalog-search-history.service';
|
||||
import { ProductShareService } from '../../user-experience/services/product-share.service';
|
||||
import { UserNotificationService } from '../../user-experience/services/user-notification.service';
|
||||
|
||||
type CatalogViewMode = 'categories' | 'products';
|
||||
|
||||
@@ -55,8 +58,12 @@ export class CatalogContainerComponent {
|
||||
private readonly prefetchService = inject(PrefetchService);
|
||||
private readonly languageService = inject(LanguageService);
|
||||
private readonly searchHistoryService = inject(CatalogSearchHistoryService);
|
||||
private readonly uxFacade = inject(UserExperienceFacade);
|
||||
private readonly shareService = inject(ProductShareService);
|
||||
private readonly notifications = inject(UserNotificationService);
|
||||
|
||||
readonly catalogConfig = signal(this.resolveCatalogConfig());
|
||||
readonly userExperienceConfig = signal(this.resolveUserExperienceConfig());
|
||||
|
||||
readonly state = signal<CatalogState>(createInitialCatalogState());
|
||||
readonly categories = signal<Category[]>([]);
|
||||
@@ -71,11 +78,14 @@ export class CatalogContainerComponent {
|
||||
readonly searchSuggestions = signal<string[]>([]);
|
||||
readonly recentSearches = signal<string[]>([]);
|
||||
readonly searchHistory = signal<string[]>(this.catalogConfig().searchHistoryEnabled ? this.searchHistoryService.getHistory() : []);
|
||||
readonly savedSearches = this.uxFacade.savedSearches;
|
||||
readonly viewMode = signal<CatalogViewMode>('categories');
|
||||
readonly loading = signal(true);
|
||||
readonly loadingProducts = signal(false);
|
||||
readonly error = signal<string | null>(null);
|
||||
readonly noResults = computed(() => !this.loadingProducts() && this.viewMode() === 'products' && this.products().length === 0);
|
||||
readonly favoriteIds = computed(() => this.uxFacade.wishlist().map(item => item.product.itemID));
|
||||
readonly comparedIds = computed(() => this.uxFacade.comparedProducts().map(item => item.product.itemID));
|
||||
|
||||
readonly sortDefinitions = computed<SortDefinition[]>(() => {
|
||||
const labels: Record<string, string> = {
|
||||
@@ -97,6 +107,7 @@ export class CatalogContainerComponent {
|
||||
|
||||
private dataSubscription?: Subscription;
|
||||
private readonly backendFetchSize = 200;
|
||||
private pendingScrollY: number | null = null;
|
||||
|
||||
constructor() {
|
||||
this.destroyRef.onDestroy(() => this.dataSubscription?.unsubscribe());
|
||||
@@ -107,6 +118,11 @@ export class CatalogContainerComponent {
|
||||
.subscribe(([params, queryParams]) => {
|
||||
const categoryId = Number(params.get('id')) || null;
|
||||
const searchQuery = (queryParams.get('q') ?? '').trim();
|
||||
|
||||
if (categoryId == null && searchQuery.length === 0 && this.userExperienceConfig().continueBrowsing.enabled && this.restoreContinueBrowsing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.enterCategory(categoryId);
|
||||
|
||||
if (searchQuery.length > 0) {
|
||||
@@ -195,6 +211,7 @@ export class CatalogContainerComponent {
|
||||
|
||||
onSearchQueryChange(query: string): void {
|
||||
this.state.update(current => ({ ...current, search: query }));
|
||||
this.persistContinueBrowsing();
|
||||
|
||||
const normalized = query.trim().toLowerCase();
|
||||
if (!this.catalogConfig().suggestionsEnabled || normalized.length < 2) {
|
||||
@@ -228,6 +245,7 @@ export class CatalogContainerComponent {
|
||||
this.recentSearches.set(this.searchHistory().slice(0, 5));
|
||||
}
|
||||
|
||||
this.persistContinueBrowsing();
|
||||
this.loadCatalog();
|
||||
}
|
||||
|
||||
@@ -247,6 +265,106 @@ export class CatalogContainerComponent {
|
||||
this.recentSearches.set([]);
|
||||
}
|
||||
|
||||
onFavoriteToggled(product: Product): void {
|
||||
const result = this.uxFacade.toggleWishlist(product);
|
||||
this.notifications.show(result.added ? 'Added to wishlist' : 'Removed from wishlist', result.added ? 'success' : 'info');
|
||||
}
|
||||
|
||||
onCompareToggled(product: Product): void {
|
||||
if (this.uxFacade.isInCompare(product.itemID)) {
|
||||
this.uxFacade.removeFromCompare(product.itemID);
|
||||
this.notifications.show('Removed from compare', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const maxItems = this.userExperienceConfig().compare.maxItems;
|
||||
const result = this.uxFacade.addToCompare(product, maxItems);
|
||||
if (result.reason === 'limit') {
|
||||
this.notifications.show(`Compare limit reached (${maxItems})`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.notifications.show('Added to compare', 'success');
|
||||
}
|
||||
|
||||
async onShareRequested(product: Product): Promise<void> {
|
||||
if (!this.userExperienceConfig().share.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lang = this.languageService.currentLanguage();
|
||||
const path = `/${lang}/product/${product.itemID}`;
|
||||
const productUrl = typeof window !== 'undefined'
|
||||
? new URL(path, window.location.origin).toString()
|
||||
: path;
|
||||
|
||||
const result = await this.shareService.shareProduct(product, productUrl);
|
||||
if (result === 'native') {
|
||||
this.notifications.show('Product shared', 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
if (result === 'copied') {
|
||||
this.notifications.show('Product link copied', 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
this.notifications.show('Sharing is not supported on this device', 'warning');
|
||||
}
|
||||
|
||||
saveCurrentSearch(): void {
|
||||
if (!this.userExperienceConfig().savedSearches.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = this.state().search.trim();
|
||||
if (!query.length) {
|
||||
this.notifications.show('Enter a search query before saving', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const categoryId = this.state().filters.categoryIds[0];
|
||||
const saved = this.uxFacade.saveSearch({
|
||||
name: query,
|
||||
query,
|
||||
sort: this.state().sort,
|
||||
categoryId,
|
||||
filters: this.filterState()
|
||||
}, this.userExperienceConfig().savedSearches.maxItems);
|
||||
|
||||
this.notifications.show(`Saved search: ${saved.name}`, 'success');
|
||||
}
|
||||
|
||||
useSavedSearch(id: string): void {
|
||||
const target = this.savedSearches().find(item => item.id === id);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.viewMode.set('products');
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
search: target.query,
|
||||
sort: target.sort as CatalogState['sort'],
|
||||
pagination: {
|
||||
...current.pagination,
|
||||
page: 1,
|
||||
skip: 0
|
||||
}
|
||||
}));
|
||||
this.filterState.set(target.filters);
|
||||
this.loadCatalog(target.categoryId);
|
||||
}
|
||||
|
||||
removeSavedSearch(id: string): void {
|
||||
this.uxFacade.removeSavedSearch(id);
|
||||
}
|
||||
|
||||
@HostListener('window:scroll')
|
||||
onWindowScroll(): void {
|
||||
this.persistContinueBrowsing();
|
||||
}
|
||||
|
||||
onFilterStateChange(next: CatalogFilterStateValue): void {
|
||||
this.filterState.set(next);
|
||||
this.state.update(current => ({
|
||||
@@ -259,11 +377,13 @@ export class CatalogContainerComponent {
|
||||
}));
|
||||
|
||||
this.recomputeResults();
|
||||
this.persistContinueBrowsing();
|
||||
}
|
||||
|
||||
resetFilters(): void {
|
||||
this.filterState.set({ values: {}, ranges: {}, toggles: {} });
|
||||
this.recomputeResults();
|
||||
this.persistContinueBrowsing();
|
||||
}
|
||||
|
||||
changeSort(sortId: string): void {
|
||||
@@ -278,10 +398,12 @@ export class CatalogContainerComponent {
|
||||
}));
|
||||
|
||||
this.recomputeResults();
|
||||
this.persistContinueBrowsing();
|
||||
}
|
||||
|
||||
changeLayout(layout: CatalogLayoutMode): void {
|
||||
this.state.update(current => ({ ...current, layout }));
|
||||
this.persistContinueBrowsing();
|
||||
}
|
||||
|
||||
onResultsPageChange(page: number): void {
|
||||
@@ -295,6 +417,7 @@ export class CatalogContainerComponent {
|
||||
}));
|
||||
|
||||
this.recomputeResults();
|
||||
this.persistContinueBrowsing();
|
||||
}
|
||||
|
||||
private renderCategories(category: Category | null, categories: Category[], breadcrumb: Category[]): void {
|
||||
@@ -496,6 +619,12 @@ export class CatalogContainerComponent {
|
||||
this.searchResult.set(result);
|
||||
this.searchSummary.set(result.summary);
|
||||
this.searchSuggestions.set(this.buildSuggestions(this.state().search, sorted));
|
||||
|
||||
if (this.pendingScrollY != null && typeof window !== 'undefined') {
|
||||
const scrollY = this.pendingScrollY;
|
||||
this.pendingScrollY = null;
|
||||
setTimeout(() => window.scrollTo({ top: scrollY, behavior: 'auto' }), 0);
|
||||
}
|
||||
}
|
||||
|
||||
private buildSuggestions(query: string, products: Product[]): string[] {
|
||||
@@ -646,6 +775,96 @@ export class CatalogContainerComponent {
|
||||
};
|
||||
}
|
||||
|
||||
private resolveUserExperienceConfig() {
|
||||
const snapshot = this.configService.getBootstrapSnapshot() as any;
|
||||
const raw = snapshot?.userExperience ?? {};
|
||||
|
||||
return {
|
||||
...DEFAULT_USER_EXPERIENCE_CONFIG,
|
||||
...raw,
|
||||
wishlist: {
|
||||
...DEFAULT_USER_EXPERIENCE_CONFIG.wishlist,
|
||||
...(raw.wishlist ?? {})
|
||||
},
|
||||
compare: {
|
||||
...DEFAULT_USER_EXPERIENCE_CONFIG.compare,
|
||||
...(raw.compare ?? {}),
|
||||
maxItems: Number.isFinite(raw.compare?.maxItems)
|
||||
? Math.max(2, Number(raw.compare.maxItems))
|
||||
: DEFAULT_USER_EXPERIENCE_CONFIG.compare.maxItems
|
||||
},
|
||||
recentlyViewed: {
|
||||
...DEFAULT_USER_EXPERIENCE_CONFIG.recentlyViewed,
|
||||
...(raw.recentlyViewed ?? {})
|
||||
},
|
||||
share: {
|
||||
...DEFAULT_USER_EXPERIENCE_CONFIG.share,
|
||||
...(raw.share ?? {})
|
||||
},
|
||||
continueBrowsing: {
|
||||
...DEFAULT_USER_EXPERIENCE_CONFIG.continueBrowsing,
|
||||
...(raw.continueBrowsing ?? {})
|
||||
},
|
||||
savedSearches: {
|
||||
...DEFAULT_USER_EXPERIENCE_CONFIG.savedSearches,
|
||||
...(raw.savedSearches ?? {}),
|
||||
maxItems: Number.isFinite(raw.savedSearches?.maxItems)
|
||||
? Math.max(1, Number(raw.savedSearches.maxItems))
|
||||
: DEFAULT_USER_EXPERIENCE_CONFIG.savedSearches.maxItems
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private persistContinueBrowsing(): void {
|
||||
if (!this.userExperienceConfig().continueBrowsing.enabled || this.viewMode() !== 'products') {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = this.state();
|
||||
this.uxFacade.saveContinueBrowsing({
|
||||
routeKey: 'catalog',
|
||||
query: state.search,
|
||||
sort: state.sort,
|
||||
layout: state.layout,
|
||||
page: state.pagination.page,
|
||||
pageSize: state.pagination.count,
|
||||
categoryId: state.filters.categoryIds[0],
|
||||
filterState: this.filterState(),
|
||||
scrollY: typeof window !== 'undefined' ? window.scrollY : 0,
|
||||
updatedAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
private restoreContinueBrowsing(): boolean {
|
||||
const saved = this.uxFacade.getContinueBrowsing('catalog');
|
||||
if (!saved) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.viewMode.set('products');
|
||||
this.filterState.set(saved.filterState);
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
search: saved.query,
|
||||
sort: saved.sort as CatalogState['sort'],
|
||||
layout: saved.layout as CatalogLayoutMode,
|
||||
pagination: {
|
||||
...current.pagination,
|
||||
page: Math.max(1, saved.page),
|
||||
count: Math.max(1, saved.pageSize),
|
||||
skip: Math.max(0, (saved.page - 1) * saved.pageSize)
|
||||
},
|
||||
filters: {
|
||||
...current.filters,
|
||||
categoryIds: saved.categoryId == null ? [] : [saved.categoryId]
|
||||
}
|
||||
}));
|
||||
|
||||
this.pendingScrollY = Math.max(0, saved.scrollY || 0);
|
||||
this.loadCatalog(saved.categoryId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private setError(message: string): void {
|
||||
this.error.set(message);
|
||||
this.loading.set(false);
|
||||
|
||||
Reference in New Issue
Block a user