refactor: extract shared app-breadcrumb component (Z14)

Only breadcrumb logic anywhere in the storefront was a local signal +
inline markup inside catalog-container. Extracted a generic
shared/ui/breadcrumb component (rootLabel/items/ariaLabel inputs,
rootClick/itemClick outputs) and repointed catalog-container onto it,
removing the now-dead inline SCSS block. Future breadcrumb usages
(product detail, admin) have something to reuse instead of duplicating.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-17 21:49:54 +04:00
parent bf367fc5fe
commit 6ca672987e
6 changed files with 72 additions and 29 deletions

View File

@@ -3,13 +3,12 @@
<button type="button" class="catalog-root-link catalog-root-link-btn" (click)="browseCategories()">{{ 'catalog.title' | translate }}</button> <button type="button" class="catalog-root-link catalog-root-link-btn" (click)="browseCategories()">{{ 'catalog.title' | translate }}</button>
@if (catalogConfig().showBreadcrumbs && breadcrumb().length > 0) { @if (catalogConfig().showBreadcrumbs && breadcrumb().length > 0) {
<nav class="catalog-breadcrumb" [attr.aria-label]="'catalog.breadcrumb' | translate"> <app-breadcrumb
<button type="button" (click)="browseCategories()">{{ 'catalog.allCategories' | translate }}</button> [rootLabel]="'catalog.allCategories' | translate"
@for (category of breadcrumb(); track category.id) { [ariaLabel]="'catalog.breadcrumb' | translate"
<span aria-hidden="true">/</span> [items]="breadcrumbItems()"
<button type="button" (click)="selectCategory(category)">{{ category.title }}</button> (rootClick)="browseCategories()"
} (itemClick)="onBreadcrumbItemClick($event)" />
</nav>
} }
<app-catalog-search-box <app-catalog-search-box

View File

@@ -57,27 +57,6 @@
cursor: pointer; cursor: pointer;
} }
.catalog-breadcrumb {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
color: var(--text-secondary);
font-size: var(--font-size-md, 0.9375rem);
a,
button {
border: 0;
padding: 0;
background: transparent;
color: var(--primary-color);
font: inherit;
font-weight: var(--font-weight-bold, 700);
text-decoration: none;
cursor: pointer;
}
}
.catalog-section { .catalog-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -34,6 +34,7 @@ import { SkeletonComponent } from '../../../../shared/ui/skeleton/skeleton.compo
import { ButtonComponent } from '../../../../shared/ui/button/button.component'; import { ButtonComponent } from '../../../../shared/ui/button/button.component';
import { IconComponent } from '../../../../shared/ui/icon/icon.component'; import { IconComponent } from '../../../../shared/ui/icon/icon.component';
import { QuickViewDialogComponent } from '../../product/components/quick-view-dialog/quick-view-dialog.component'; import { QuickViewDialogComponent } from '../../product/components/quick-view-dialog/quick-view-dialog.component';
import { BreadcrumbComponent, BreadcrumbItem } from '../../../../shared/ui/breadcrumb/breadcrumb.component';
import { CatalogState, createInitialCatalogState } from '../models/catalog-state.model'; import { CatalogState, createInitialCatalogState } from '../models/catalog-state.model';
import { ProductShareService } from '../../user-experience/services/product-share.service'; import { ProductShareService } from '../../user-experience/services/product-share.service';
import { UserNotificationService } from '../../user-experience/services/user-notification.service'; import { UserNotificationService } from '../../user-experience/services/user-notification.service';
@@ -59,7 +60,8 @@ type CatalogLoadingStrategy = 'pagination' | 'loadMore' | 'infiniteScroll';
SkeletonComponent, SkeletonComponent,
ButtonComponent, ButtonComponent,
IconComponent, IconComponent,
QuickViewDialogComponent QuickViewDialogComponent,
BreadcrumbComponent
], ],
templateUrl: './catalog-container.component.html', templateUrl: './catalog-container.component.html',
styleUrls: ['./catalog-container.component.scss'], styleUrls: ['./catalog-container.component.scss'],
@@ -273,6 +275,17 @@ export class CatalogContainerComponent {
}); });
} }
readonly breadcrumbItems = computed<BreadcrumbItem[]>(() =>
this.breadcrumb().map(category => ({ id: category.id, label: category.title }))
);
onBreadcrumbItemClick(item: BreadcrumbItem): void {
const category = this.breadcrumb().find(c => c.id === item.id);
if (category) {
this.selectCategory(category);
}
}
selectCategory(category: Category): void { selectCategory(category: Category): void {
this.router.navigate([`/${this.languageService.currentLanguage()}/catalog`, category.id]); this.router.navigate([`/${this.languageService.currentLanguage()}/catalog`, category.id]);
} }

View File

@@ -0,0 +1,7 @@
<nav class="app-breadcrumb" [attr.aria-label]="ariaLabel() || null">
<button type="button" (click)="rootClick.emit()">{{ rootLabel() }}</button>
@for (item of items(); track item.id) {
<span aria-hidden="true">/</span>
<button type="button" (click)="itemClick.emit(item)">{{ item.label }}</button>
}
</nav>

View File

@@ -0,0 +1,19 @@
.app-breadcrumb {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
color: var(--text-secondary);
font-size: var(--font-size-md, 0.9375rem);
button {
border: 0;
padding: 0;
background: transparent;
color: var(--primary-color);
font: inherit;
font-weight: var(--font-weight-bold, 700);
text-decoration: none;
cursor: pointer;
}
}

View File

@@ -0,0 +1,26 @@
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
export interface BreadcrumbItem {
id: string | number;
label: string;
}
/**
* Generic breadcrumb trail. Consumer owns navigation - this component only
* renders the trail and emits which item was clicked (root or a trail item).
*/
@Component({
selector: 'app-breadcrumb',
standalone: true,
templateUrl: './breadcrumb.component.html',
styleUrl: './breadcrumb.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BreadcrumbComponent {
readonly rootLabel = input.required<string>();
readonly items = input<BreadcrumbItem[]>([]);
readonly ariaLabel = input<string>('');
readonly rootClick = output<void>();
readonly itemClick = output<BreadcrumbItem>();
}