CAtegory component making

This commit is contained in:
sdarbinyan
2026-07-05 01:24:54 +04:00
parent b9f4103c8e
commit d2f0f0de54
18 changed files with 636 additions and 215 deletions

View File

@@ -62,8 +62,8 @@
</div>
} @else {
<div class="novo-categories-grid">
@for (category of topLevelCategories(); track category.categoryID) {
<a [routerLink]="['/category', category.categoryID] | langRoute" class="novo-category-card">
@for (category of topLevelCategories(); track category.id) {
<a [routerLink]="['/category', category.id] | langRoute" class="novo-category-card">
<div class="novo-category-image">
@if (category.icon) {
<img [src]="category.icon" [alt]="categoryName(category)" loading="lazy" />
@@ -75,8 +75,8 @@
</div>
<div class="novo-category-info">
<h3>{{ categoryName(category) }}</h3>
@if (getItemCount(category.categoryID)) {
<p class="novo-category-count">{{ 'home.itemsCount' | translate:{ count: getItemCount(category.categoryID) } }}</p>
@if (getItemCount(category.id)) {
<p class="novo-category-count">{{ 'home.itemsCount' | translate:{ count: getItemCount(category.id) } }}</p>
}
</div>
</a>
@@ -150,14 +150,11 @@
</div>
} @else {
<div class="dexar-categories-grid">
@for (category of topLevelCategories(); track category.categoryID) {
<a [routerLink]="['/category', category.categoryID] | langRoute"
class="dexar-category-card"
[class.dexar-category-card--wide]="isWideCategory(category.categoryID)">
@for (category of topLevelCategories(); track category.id) {
<a [routerLink]="['/category', category.id] | langRoute"
class="dexar-category-card">
<div class="dexar-category-image">
@if (isWideCategory(category.categoryID) && category.wideBanner) {
<img [src]="category.wideBanner" [alt]="categoryName(category)" loading="lazy" decoding="async" />
} @else if (category.icon) {
@if (category.icon) {
<img [src]="category.icon" [alt]="categoryName(category)" loading="lazy" decoding="async" />
} @else {
<div class="dexar-category-fallback">{{ categoryName(category).charAt(0) }}</div>
@@ -165,7 +162,7 @@
</div>
<div class="dexar-category-info">
<h3 class="dexar-category-name">{{ categoryName(category) }}</h3>
<p class="dexar-category-count">{{ 'home.itemsCount' | translate:{ count: getItemCount(category.categoryID) } }}</p>
<p class="dexar-category-count">{{ 'home.itemsCount' | translate:{ count: getItemCount(category.id) } }}</p>
</div>
</a>
}

View File

@@ -1,13 +1,12 @@
import { Component, OnInit, OnDestroy, signal, computed, ChangeDetectionStrategy } from '@angular/core';
import { Component, OnInit, signal, computed, ChangeDetectionStrategy } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { LanguageService } from '../../services';
import { Category } from '../../models';
import { getTranslatedCategoryName } from '../../utils/item.utils';
import { ItemsCarouselComponent } from '../../components/items-carousel/items-carousel.component';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade';
import { ProductFacade } from '../../facades/platform/product.facade';
import { CategoryFacade } from '../../facades/platform/category.facade';
import { Category } from '../../core/categories/models/category-domain.model';
@Component({
selector: 'app-home',
@@ -16,16 +15,15 @@ import { ProductFacade } from '../../facades/platform/product.facade';
styleUrls: ['./home.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HomeComponent implements OnInit, OnDestroy {
export class HomeComponent implements OnInit {
constructor(
private router: Router,
private langService: LanguageService,
private readonly uiRuntime: UiRuntimeFacade,
private readonly productFacade: ProductFacade
private readonly categoryFacade: CategoryFacade
) {}
categories = signal<Category[]>([]);
wideCategories = signal<Set<number>>(new Set());
loading = signal(true);
error = signal<string | null>(null);
readonly skeletonSlots = Array.from({ length: 6 });
@@ -33,7 +31,6 @@ export class HomeComponent implements OnInit, OnDestroy {
// Memoized computed values for performance
topLevelCategories = computed(() => {
return this.categories()
.filter(cat => cat.parentID === 0)
.filter(cat => this.isDisplayableTopLevelCategory(cat))
.sort((a, b) => (a.priority ?? Infinity) - (b.priority ?? Infinity));
});
@@ -41,7 +38,7 @@ export class HomeComponent implements OnInit, OnDestroy {
// Memoized item count lookup
private itemCountMap = computed(() => {
const map = new Map<number, number>();
this.categories().forEach(cat => map.set(cat.categoryID, cat.itemCount || 0));
this.categories().forEach(cat => map.set(cat.id, cat.itemCount || 0));
return map;
});
@@ -49,11 +46,9 @@ export class HomeComponent implements OnInit, OnDestroy {
private subcategoriesCache = computed(() => {
const cache = new Map<number, Category[]>();
this.categories().forEach(cat => {
if (cat.parentID !== 0 && this.isDisplayableFlatSubcategory(cat)) {
if (!cache.has(cat.parentID)) {
cache.set(cat.parentID, []);
}
cache.get(cat.parentID)!.push(cat);
const children = cat.children.filter(child => this.isDisplayableFlatSubcategory(child));
if (children.length > 0) {
cache.set(cat.id, children);
}
});
return cache;
@@ -71,21 +66,13 @@ export class HomeComponent implements OnInit, OnDestroy {
this.loadCategories();
}
ngOnDestroy(): void {
this.pendingImages.forEach(img => {
img.onload = null;
img.onerror = null;
});
this.pendingImages.clear();
}
loadCategories(): void {
this.loading.set(true);
this.productFacade.getCategories().subscribe({
this.error.set(null);
this.categoryFacade.getRootCategories().subscribe({
next: (categories) => {
this.categories.set(categories);
this.loading.set(false);
this.detectWideImages(categories);
},
error: (err) => {
this.error.set('Failed to load categories');
@@ -95,65 +82,35 @@ export class HomeComponent implements OnInit, OnDestroy {
});
}
getItemCount(categoryID: number): number {
return this.itemCountMap().get(categoryID) || 0;
getItemCount(categoryId: number): number {
return this.itemCountMap().get(categoryId) || 0;
}
getSubCategories(parentID: number): Category[] {
return this.subcategoriesCache().get(parentID) || [];
getSubCategories(parentId: number): Category[] {
return this.subcategoriesCache().get(parentId) || [];
}
private isDisplayableFlatSubcategory(category: Category): boolean {
return category.visible !== false
&& ((category.itemCount ?? 0) > 0 || (category.subcategories?.length ?? 0) > 0);
&& ((category.itemCount ?? 0) > 0 || category.children.length > 0);
}
private isDisplayableTopLevelCategory(category: Category): boolean {
return category.visible !== false
&& (
(category.itemCount ?? 0) > 0
|| (category.categoriesCount ?? 0) > 0
|| (category.subcategories?.length ?? 0) > 0
|| this.getSubCategories(category.categoryID).length > 0
|| category.children.length > 0
|| this.getSubCategories(category.id).length > 0
);
}
isWideCategory(categoryID: number): boolean {
return this.wideCategories().has(categoryID);
}
private pendingImages = new Set<HTMLImageElement>();
private detectWideImages(categories: Category[]): void {
const topLevel = categories.filter(c => c.parentID === 0);
topLevel.forEach(cat => {
if (!cat.wideBanner) return;
const img = new Image();
this.pendingImages.add(img);
img.onload = () => {
this.pendingImages.delete(img);
const ratio = img.naturalWidth / img.naturalHeight;
if (ratio > 2) {
this.wideCategories.update(set => {
const next = new Set(set);
next.add(cat.categoryID);
return next;
});
}
};
img.onerror = () => this.pendingImages.delete(img);
img.src = cat.wideBanner;
});
}
navigateToSearch(): void {
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}/search`]);
}
categoryName(cat: Category): string {
return getTranslatedCategoryName(cat, this.langService.currentLanguage());
return cat.translations[this.langService.currentLanguage()]?.title ?? cat.title;
}
scrollToCatalog(): void {