import { CommonModule } from '@angular/common'; import { Component, DestroyRef, Injector, computed, inject, input, signal, } from '@angular/core'; import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; import { Router, RouterLink } from '@angular/router'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { catchError, combineLatest, finalize, forkJoin, of, switchMap, tap, } from 'rxjs'; import { SeoService } from '../../core/services/seo.service'; import { LanguageService } from '../../core/services/language.service'; import { AppButtonComponent } from '../../shared/components/app-button/app-button.component'; import { AppCardComponent } from '../../shared/components/app-card/app-card.component'; import { ProductCardComponent } from './components/product-card/product-card.component'; import { ShopCategoryDetail, ShopCategoryNavNode, ShopCategoryTree, ShopCartItem, ShopProductSummary, ShopService, } from './services/shop.service'; import { ShopRouteService } from './services/shop-route.service'; @Component({ selector: 'app-shop-page', standalone: true, imports: [ CommonModule, TranslateModule, RouterLink, AppButtonComponent, AppCardComponent, ProductCardComponent, ], templateUrl: './shop-page.component.html', styleUrl: './shop-page.component.scss', }) export class ShopPageComponent { private readonly destroyRef = inject(DestroyRef); private readonly injector = inject(Injector); private readonly router = inject(Router); private readonly translate = inject(TranslateService); private readonly seoService = inject(SeoService); private readonly languageService = inject(LanguageService); private readonly shopRouteService = inject(ShopRouteService); readonly shopService = inject(ShopService); readonly categorySlug = input(); readonly loading = signal(true); readonly error = signal(null); readonly categories = signal([]); readonly categoryNodes = signal([]); readonly selectedCategory = signal(null); readonly products = signal([]); readonly cartMutating = signal(false); readonly busyLineItemId = signal(null); readonly cart = this.shopService.cart; readonly cartLoading = this.shopService.cartLoading; readonly cartItemCount = this.shopService.cartItemCount; readonly currentCategorySlug = computed( () => this.selectedCategory()?.slug ?? this.categorySlug() ?? null, ); readonly cartItems = computed(() => (this.cart()?.items ?? []).filter( (item) => item.lineItemType === 'SHOP_PRODUCT', ), ); readonly cartHasItems = computed(() => this.cartItems().length > 0); constructor() { if (!this.shopService.cartLoaded()) { this.shopService .loadCart() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ error: () => { this.shopService.cart.set(null); }, }); } combineLatest([ toObservable(this.categorySlug, { injector: this.injector }), toObservable(this.languageService.currentLang, { injector: this.injector, }), ]) .pipe( tap(() => { this.loading.set(true); this.error.set(null); }), switchMap(([categorySlug]) => forkJoin({ categories: this.shopService.getCategories(), catalog: this.shopService.getProductCatalog(categorySlug ?? null), }).pipe( catchError((error) => { this.categories.set([]); this.categoryNodes.set([]); this.selectedCategory.set(null); this.products.set([]); this.error.set( error?.status === 404 ? 'SHOP.NOT_FOUND' : 'SHOP.LOAD_ERROR', ); this.applyDefaultSeo(); return of(null); }), finalize(() => this.loading.set(false)), ), ), takeUntilDestroyed(this.destroyRef), ) .subscribe((result) => { if (!result) { return; } this.categories.set(result.categories); this.categoryNodes.set( this.shopService.flattenCategoryTree( result.categories, result.catalog.category?.slug ?? this.categorySlug() ?? null, ), ); this.selectedCategory.set(result.catalog.category ?? null); this.products.set(result.catalog.products); this.applySeo(result.catalog.category ?? null); }); } productCartQuantity(productId: string): number { return this.shopService.quantityForProduct(productId); } cartItemName(item: ShopCartItem): string { return ( item.displayName || item.shopProductName || item.originalFilename || '-' ); } cartItemVariant(item: ShopCartItem): string | null { return item.shopVariantLabel || item.shopVariantColorName || null; } cartItemColor(item: ShopCartItem): string | null { return item.shopVariantColorName || item.colorCode || null; } cartItemColorHex(item: ShopCartItem): string { return item.shopVariantColorHex || '#c9ced6'; } navigateToCategory(slug?: string | null): void { this.router.navigate(this.shopRouteService.shopRootCommands(slug)); } increaseQuantity(item: ShopCartItem): void { this.updateItemQuantity(item, (item.quantity ?? 0) + 1); } decreaseQuantity(item: ShopCartItem): void { const nextQuantity = Math.max(1, (item.quantity ?? 1) - 1); this.updateItemQuantity(item, nextQuantity); } removeItem(item: ShopCartItem): void { this.cartMutating.set(true); this.busyLineItemId.set(item.id); this.shopService .removeCartItem(item.id) .pipe( finalize(() => { this.cartMutating.set(false); this.busyLineItemId.set(null); }), takeUntilDestroyed(this.destroyRef), ) .subscribe({ error: () => { this.error.set('SHOP.CART_UPDATE_ERROR'); }, }); } clearCart(): void { this.cartMutating.set(true); this.busyLineItemId.set(null); this.shopService .clearCart() .pipe( finalize(() => { this.cartMutating.set(false); }), takeUntilDestroyed(this.destroyRef), ) .subscribe({ error: () => { this.error.set('SHOP.CART_UPDATE_ERROR'); }, }); } goToCheckout(): void { const sessionId = this.shopService.cartSessionId(); if (!sessionId) { return; } this.router.navigate(['/checkout'], { queryParams: { session: sessionId, }, }); } trackByCategory(_index: number, item: ShopCategoryNavNode): string { return item.id; } trackByProduct(_index: number, product: ShopProductSummary): string { return product.id; } trackByCartItem(_index: number, item: ShopCartItem): string { return item.id; } private updateItemQuantity(item: ShopCartItem, quantity: number): void { this.cartMutating.set(true); this.busyLineItemId.set(item.id); this.shopService .updateCartItem(item.id, quantity) .pipe( finalize(() => { this.cartMutating.set(false); this.busyLineItemId.set(null); }), takeUntilDestroyed(this.destroyRef), ) .subscribe({ error: () => { this.error.set('SHOP.CART_UPDATE_ERROR'); }, }); } private applySeo(category: ShopCategoryDetail | null): void { if (!category) { this.applyDefaultSeo(); return; } const title = category.seoTitle || `${category.name} | ${this.translate.instant('SHOP.TITLE')} | 3D fab`; const description = category.seoDescription || category.description || this.translate.instant('SHOP.CATALOG_META_DESCRIPTION'); const robots = category.indexable === false ? 'noindex, nofollow' : 'index, follow'; this.seoService.applyPageSeo({ title, description, robots, ogTitle: category.ogTitle || title, ogDescription: category.ogDescription || description, }); } private applyDefaultSeo(): void { const title = `${this.translate.instant('SHOP.TITLE')} | 3D fab`; const description = this.translate.instant('SHOP.CATALOG_META_DESCRIPTION'); this.seoService.applyPageSeo({ title, description, robots: 'index, follow', ogTitle: title, ogDescription: description, }); } }