import { Component, signal, HostListener, OnDestroy, ChangeDetectionStrategy, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CartService } from '../../services'; import { PrefetchService } from '../../services/prefetch.service'; import { Item } from '../../models'; import { Subject, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass, getTranslatedField } from '../../utils/item.utils'; import { LanguageService } from '../../services/language.service'; import { TranslatePipe } from '../../i18n/translate.pipe'; import { TranslateService } from '../../i18n/translate.service'; import { SEARCH_DEBOUNCE_MS, ITEMS_PER_PAGE, SCROLL_THRESHOLD_PX, SCROLL_DEBOUNCE_MS } from '../../config/constants'; import { ProductFacade } from '../../facades/platform/product.facade'; import { ProductCardComponent } from '../../components/product-card/product-card.component'; @Component({ selector: 'app-search', imports: [FormsModule, TranslatePipe, ProductCardComponent], templateUrl: './search.component.html', styleUrls: ['./search.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class SearchComponent implements OnDestroy { readonly minSearchLength = 3; searchQuery = ''; items = signal([]); loading = signal(false); error = signal(null); hasMore = signal(true); totalResults = signal(0); private skip = 0; private readonly count = ITEMS_PER_PAGE; private isLoadingMore = false; private searchSubject = new Subject(); private searchSubscription: Subscription; private i18n = inject(TranslateService); private readonly productFacade = inject(ProductFacade); constructor( private cartService: CartService, private prefetchService: PrefetchService ) { this.searchSubscription = this.searchSubject .pipe( debounceTime(SEARCH_DEBOUNCE_MS), distinctUntilChanged() ) .subscribe(query => { if (query.trim().length >= 3 || query.trim().length === 0) { this.performSearch(query); } }); } ngOnDestroy(): void { this.searchSubscription.unsubscribe(); this.searchSubject.complete(); if (this.scrollTimeout) clearTimeout(this.scrollTimeout); } onSearchInput(query: string): void { this.searchQuery = query; this.error.set(null); if (this.isQueryTooShort()) { this.resetSearchState(); return; } this.searchSubject.next(query); } performSearch(query: string): void { if (!query.trim()) { this.resetSearchState(); return; } this.error.set(null); this.items.set([]); this.skip = 0; this.hasMore.set(true); this.totalResults.set(0); this.loadResults(); } loadResults(): void { if (this.isLoadingMore || !this.hasMore() || !this.searchQuery.trim()) return; this.loading.set(true); this.isLoadingMore = true; this.productFacade.searchProducts({ search: this.searchQuery.trim(), count: this.count, skip: this.skip }).subscribe({ next: (response) => { // Update total results (only on first load) if (this.skip === 0) { this.totalResults.set(response.total); } // Handle empty results if (!response.items || response.items.length === 0) { this.hasMore.set(false); } else { // Check if there are more items to load if (response.items.length < this.count || this.skip + response.items.length >= response.total) { this.hasMore.set(false); } this.items.update(current => [...current, ...response.items]); this.skip += response.items.length; } this.loading.set(false); this.isLoadingMore = false; }, error: (err) => { this.error.set(this.i18n.t('home.errorTitle')); this.loading.set(false); this.isLoadingMore = false; console.error('Error searching items:', err); } }); } private resetSearchState(): void { this.items.set([]); this.loading.set(false); this.hasMore.set(false); this.totalResults.set(0); this.skip = 0; this.isLoadingMore = false; } isQueryTooShort(): boolean { const length = this.searchQuery.trim().length; return length > 0 && length < this.minSearchLength; } private scrollTimeout?: ReturnType; @HostListener('window:scroll') onScroll(): void { if (this.scrollTimeout) clearTimeout(this.scrollTimeout); this.scrollTimeout = setTimeout(() => { const scrollPosition = window.innerHeight + window.scrollY; const bottomPosition = document.documentElement.scrollHeight - SCROLL_THRESHOLD_PX; if (scrollPosition >= bottomPosition && !this.loading() && this.hasMore()) { this.loadResults(); } }, SCROLL_DEBOUNCE_MS); } addToCart(itemID: number, event: Event): void { event.preventDefault(); event.stopPropagation(); this.cartService.addItem(itemID); } onItemHover(itemID: number): void { this.prefetchService.prefetchItem(itemID); } readonly skeletonSlots = Array.from({ length: 8 }); readonly getDiscountedPrice = getDiscountedPrice; readonly getMainImage = getMainImage; readonly trackByItemId = trackByItemId; readonly getBadgeClass = getBadgeClass; private langService = inject(LanguageService); itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); } itemDesc(item: Item): string { return getTranslatedField(item, 'simpleDescription', this.langService.currentLanguage()); } }