Files
marketplaces/src/app/pages/search/search.component.ts
2026-02-26 23:09:20 +04:00

140 lines
4.3 KiB
TypeScript

import { Component, signal, HostListener, OnDestroy, ChangeDetectionStrategy, inject } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { ApiService, CartService } from '../../services';
import { Item } from '../../models';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { getDiscountedPrice, getMainImage, trackByItemId } from '../../utils/item.utils';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service';
@Component({
selector: 'app-search',
imports: [DecimalPipe, FormsModule, RouterLink, LangRoutePipe, TranslatePipe],
templateUrl: './search.component.html',
styleUrls: ['./search.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SearchComponent implements OnDestroy {
searchQuery = '';
items = signal<Item[]>([]);
loading = signal(false);
error = signal<string | null>(null);
hasMore = signal(true);
totalResults = signal<number>(0);
private skip = 0;
private readonly count = 20;
private isLoadingMore = false;
private searchSubject = new Subject<string>();
private searchSubscription: Subscription;
private i18n = inject(TranslateService);
constructor(
private apiService: ApiService,
private cartService: CartService
) {
this.searchSubscription = this.searchSubject
.pipe(
debounceTime(300),
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.searchSubject.next(query);
}
performSearch(query: string): void {
if (!query.trim()) {
this.items.set([]);
this.hasMore.set(true);
this.totalResults.set(0);
return;
}
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.apiService.searchItems(this.searchQuery, this.count, 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 scrollTimeout?: ReturnType<typeof setTimeout>;
@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 - 500;
if (scrollPosition >= bottomPosition && !this.loading() && this.hasMore()) {
this.loadResults();
}
}, 100);
}
addToCart(itemID: number, event: Event): void {
event.preventDefault();
event.stopPropagation();
this.cartService.addItem(itemID);
}
readonly getDiscountedPrice = getDiscountedPrice;
readonly getMainImage = getMainImage;
readonly trackByItemId = trackByItemId;
}