Support nested marketplace categories

This commit is contained in:
sdarbinyan
2026-07-05 01:09:55 +04:00
parent b676cec4e9
commit 3974eefbfe
7 changed files with 109 additions and 5 deletions

View File

@@ -150,6 +150,9 @@ export const en: Translations = {
emptyDesc: 'There are no subcategories in this section yet, but they will appear soon', emptyDesc: 'There are no subcategories in this section yet, but they will appear soon',
goHome: 'Go home', goHome: 'Go home',
itemsInCategory: 'Items in this category', itemsInCategory: 'Items in this category',
childrenCount: '{{count}} categories',
productsCount: '{{count}} products',
includesProducts: 'Includes products',
}, },
itemDetail: { itemDetail: {
loading: 'Loading...', loading: 'Loading...',

View File

@@ -150,6 +150,9 @@ export const hy: Translations = {
emptyDesc: 'Այս բաժնում դեռ ենթակատեգորիաներ չկան', emptyDesc: 'Այս բաժնում դեռ ենթակատեգորիաներ չկան',
goHome: 'Գլխավոր', goHome: 'Գլխավոր',
itemsInCategory: 'Ապրանքներ այս կատեգորիայում', itemsInCategory: 'Ապրանքներ այս կատեգորիայում',
childrenCount: '{{count}} կատեգորիա',
productsCount: '{{count}} ապրանք',
includesProducts: 'Կան ապրանքներ',
}, },
itemDetail: { itemDetail: {
loading: 'Բեռնում...', loading: 'Բեռնում...',

View File

@@ -150,6 +150,9 @@ export const ru: Translations = {
emptyDesc: 'В этом разделе ещё нет подкатегорий, но скоро они появятся', emptyDesc: 'В этом разделе ещё нет подкатегорий, но скоро они появятся',
goHome: 'На главную', goHome: 'На главную',
itemsInCategory: 'Товары в этой категории', itemsInCategory: 'Товары в этой категории',
childrenCount: '{{count}} категорий',
productsCount: '{{count}} товаров',
includesProducts: 'Есть товары',
}, },
itemDetail: { itemDetail: {
loading: 'Загрузка...', loading: 'Загрузка...',

View File

@@ -148,6 +148,9 @@ export interface Translations {
emptyDesc: string; emptyDesc: string;
goHome: string; goHome: string;
itemsInCategory: string; itemsInCategory: string;
childrenCount: string;
productsCount: string;
includesProducts: string;
}; };
itemDetail: { itemDetail: {
loading: string; loading: string;

View File

@@ -32,8 +32,16 @@
</div> </div>
<div class="category-info"> <div class="category-info">
<h3 class="category-name">{{ sub.name }}</h3> <h3 class="category-name">{{ sub.name }}</h3>
@if (sub.itemCount) { <div class="category-meta">
<span class="category-count">{{ sub.itemCount }}</span> @if (subcategoryChildCount(sub) > 0) {
<span>{{ 'subcategories.childrenCount' | translate:{ count: subcategoryChildCount(sub) } }}</span>
}
@if (subcategoryItemCount(sub) > 0) {
<span>{{ 'subcategories.productsCount' | translate:{ count: subcategoryItemCount(sub) } }}</span>
}
</div>
@if (sub.hasItems && subcategoryChildCount(sub) > 0) {
<span class="category-mixed">{{ 'subcategories.includesProducts' | translate }}</span>
} }
</div> </div>
</a> </a>
@@ -55,6 +63,14 @@
</div> </div>
<div class="category-info"> <div class="category-info">
<h3 class="category-name">{{ categoryName(cat) }}</h3> <h3 class="category-name">{{ categoryName(cat) }}</h3>
<div class="category-meta">
@if ((cat.categoriesCount ?? 0) > 0) {
<span>{{ 'subcategories.childrenCount' | translate:{ count: cat.categoriesCount ?? 0 } }}</span>
}
@if ((cat.itemCount ?? 0) > 0) {
<span>{{ 'subcategories.productsCount' | translate:{ count: cat.itemCount ?? 0 } }}</span>
}
</div>
</div> </div>
</a> </a>
} }

View File

@@ -241,6 +241,34 @@
color: #697777; color: #697777;
} }
.category-meta {
display: flex;
flex-wrap: wrap;
gap: 6px;
min-height: 20px;
font-family: "DM Sans", sans-serif;
font-size: 0.8rem;
color: #697777;
span {
padding: 2px 7px;
border-radius: 999px;
background: rgba(73, 118, 113, 0.1);
color: #3d635f;
white-space: nowrap;
}
}
.category-mixed {
width: fit-content;
padding: 2px 7px;
border-radius: 999px;
background: #eef1f1;
color: #697777;
font-family: "DM Sans", sans-serif;
font-size: 0.75rem;
}
// Items section within subcategories page // Items section within subcategories page
.category-items-section { .category-items-section {
margin-top: 40px; margin-top: 40px;

View File

@@ -10,6 +10,8 @@ import { TranslateService } from '../../i18n/translate.service';
import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass, getTranslatedField, getTranslatedCategoryName } from '../../utils/item.utils'; import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass, getTranslatedField, getTranslatedCategoryName } from '../../utils/item.utils';
import { ProductFacade } from '../../facades/platform/product.facade'; import { ProductFacade } from '../../facades/platform/product.facade';
type CategoryNode = Category | Subcategory;
@Component({ @Component({
selector: 'app-subcategories', selector: 'app-subcategories',
imports: [DecimalPipe, RouterLink, LangRoutePipe, TranslatePipe], imports: [DecimalPipe, RouterLink, LangRoutePipe, TranslatePipe],
@@ -59,12 +61,14 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
this.productFacade.getCategories().subscribe({ this.productFacade.getCategories().subscribe({
next: (cats) => { next: (cats) => {
this.categories.set(cats); this.categories.set(cats);
const parent = cats.find(c => c.categoryID === parentID); const parent = this.findCategoryNode(cats, parentID);
this.parentName.set(parent ? getTranslatedCategoryName(parent, this.langService.currentLanguage()) : this.i18n.t('home.categoriesTitle')); this.parentName.set(parent ? this.nodeName(parent) : this.i18n.t('home.categoriesTitle'));
// Check for nested subcategories from API response (backOffice format) // Check for nested subcategories from API response (backOffice format)
const nested = parent?.subcategories || []; const nested = parent?.subcategories || [];
const visibleNested = nested.filter(s => this.isDisplayableNestedSubcategory(s)); const visibleNested = nested
.filter(s => this.isDisplayableNestedSubcategory(s))
.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
// Also check flat legacy subcategories // Also check flat legacy subcategories
const flatSubs = cats.filter(c => c.parentID === parentID && this.isDisplayableFlatSubcategory(c)); const flatSubs = cats.filter(c => c.parentID === parentID && this.isDisplayableFlatSubcategory(c));
@@ -125,6 +129,42 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
); );
} }
private findCategoryNode(categories: Category[], categoryID: number): CategoryNode | undefined {
for (const category of categories) {
if (category.categoryID === categoryID || Number(category.id) === categoryID) {
return category;
}
const child = this.findSubcategoryNode(category.subcategories ?? [], categoryID);
if (child) {
return child;
}
}
return undefined;
}
private findSubcategoryNode(subcategories: Subcategory[], categoryID: number): Subcategory | undefined {
for (const subcategory of subcategories) {
if (Number(subcategory.id) === categoryID || Number(subcategory.categoryId) === categoryID) {
return subcategory;
}
const child = this.findSubcategoryNode(subcategory.subcategories ?? [], categoryID);
if (child) {
return child;
}
}
return undefined;
}
private nodeName(node: CategoryNode): string {
return 'categoryID' in node
? getTranslatedCategoryName(node, this.langService.currentLanguage())
: node.name;
}
hasSubcategories(): boolean { hasSubcategories(): boolean {
return this.subcategories().length > 0 || this.nestedSubcategories().length > 0; return this.subcategories().length > 0 || this.nestedSubcategories().length > 0;
} }
@@ -144,6 +184,14 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
return sub.id; return sub.id;
} }
subcategoryChildCount(subcategory: Subcategory): number {
return subcategory.subcategories?.length ?? 0;
}
subcategoryItemCount(subcategory: Subcategory): number {
return subcategory.itemCount ?? 0;
}
readonly getDiscountedPrice = getDiscountedPrice; readonly getDiscountedPrice = getDiscountedPrice;
readonly getMainImage = getMainImage; readonly getMainImage = getMainImage;
readonly trackByItemId = trackByItemId; readonly trackByItemId = trackByItemId;