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:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/app/shared/ui/breadcrumb/breadcrumb.component.html
Normal file
7
src/app/shared/ui/breadcrumb/breadcrumb.component.html
Normal 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>
|
||||||
19
src/app/shared/ui/breadcrumb/breadcrumb.component.scss
Normal file
19
src/app/shared/ui/breadcrumb/breadcrumb.component.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/app/shared/ui/breadcrumb/breadcrumb.component.ts
Normal file
26
src/app/shared/ui/breadcrumb/breadcrumb.component.ts
Normal 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>();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user