2026-07-10 13:43:53 +04:00
|
|
|
import { Component, ChangeDetectionStrategy, Renderer2, inject, DOCUMENT, computed } from '@angular/core';
|
2026-02-14 00:45:17 +04:00
|
|
|
import { Router, RouterLink, RouterLinkActive } from '@angular/router';
|
2026-06-21 23:42:39 +04:00
|
|
|
import { CartService } from '../../services/cart.service';
|
|
|
|
|
import { LanguageService } from '../../services/language.service';
|
2026-01-18 18:57:06 +04:00
|
|
|
import { LogoComponent } from '../logo/logo.component';
|
|
|
|
|
import { LanguageSelectorComponent } from '../language-selector/language-selector.component';
|
2026-02-28 17:18:24 +04:00
|
|
|
import { RegionSelectorComponent } from '../region-selector/region-selector.component';
|
2026-02-26 22:23:08 +04:00
|
|
|
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
2026-02-26 23:09:20 +04:00
|
|
|
import { TranslatePipe } from '../../i18n/translate.pipe';
|
2026-07-03 01:58:08 +04:00
|
|
|
import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade';
|
2026-07-09 01:13:54 +04:00
|
|
|
import { UserExperienceFacade } from '../../facades/platform/user-experience.facade';
|
|
|
|
|
import { ConfigService } from '../../core/config/config.service';
|
2026-07-10 15:55:38 +04:00
|
|
|
import { FeatureConfigService } from '../../core/config/feature-config.service';
|
2026-07-10 13:43:53 +04:00
|
|
|
import { DEFAULT_HEADER_CONFIG, DEFAULT_USER_EXPERIENCE_CONFIG } from '../../shared/models/config';
|
2026-07-10 13:52:01 +04:00
|
|
|
import { StaticPageResolverService } from '../../core/config/static-page-resolver.service';
|
fix(icons): replace hand-rolled inline SVGs with Lucide (header, search)
Header: same magnifying-glass path was hand-duplicated twice with two
different hex fills (#576463 desktop, #1e3c38 mobile) - now one
app-icon name="search", color inherited via currentColor. Wishlist/
compare buttons used bare '♥'/'⇄' text glyphs, entirely outside any
icon system - replaced with heart/scale icons. Cart icon, mobile-menu
home/catalog icons, and three duplicated inline chevron SVGs replaced
with app-icon equivalents. Cleaned up now-dead CSS that targeted the
old raw svg/path selectors.
Search: the same magnifying-glass path was hand-duplicated 4 times
(input icon, empty-query state, no-results state, no-query state) at
three sizes and three colors. Replaced all four with app-icon,
preserving each state's intended color via a color property on the
wrapper (icons default to currentColor).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 02:37:23 +04:00
|
|
|
import { IconComponent } from '../../shared/ui/icon/icon.component';
|
2026-01-18 18:57:06 +04:00
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
selector: 'app-header',
|
fix(icons): replace hand-rolled inline SVGs with Lucide (header, search)
Header: same magnifying-glass path was hand-duplicated twice with two
different hex fills (#576463 desktop, #1e3c38 mobile) - now one
app-icon name="search", color inherited via currentColor. Wishlist/
compare buttons used bare '♥'/'⇄' text glyphs, entirely outside any
icon system - replaced with heart/scale icons. Cart icon, mobile-menu
home/catalog icons, and three duplicated inline chevron SVGs replaced
with app-icon equivalents. Cleaned up now-dead CSS that targeted the
old raw svg/path selectors.
Search: the same magnifying-glass path was hand-duplicated 4 times
(input icon, empty-query state, no-results state, no-query state) at
three sizes and three colors. Replaced all four with app-icon,
preserving each state's intended color via a color property on the
wrapper (icons default to currentColor).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 02:37:23 +04:00
|
|
|
imports: [RouterLink, RouterLinkActive, LogoComponent, LanguageSelectorComponent, RegionSelectorComponent, LangRoutePipe, TranslatePipe, IconComponent],
|
2026-01-18 18:57:06 +04:00
|
|
|
templateUrl: './header.component.html',
|
2026-02-19 01:23:25 +04:00
|
|
|
styleUrls: ['./header.component.scss'],
|
|
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush
|
2026-01-18 18:57:06 +04:00
|
|
|
})
|
|
|
|
|
export class HeaderComponent {
|
|
|
|
|
cartItemCount;
|
2026-06-20 14:00:28 +04:00
|
|
|
cartTotal;
|
2026-01-18 18:57:06 +04:00
|
|
|
menuOpen = false;
|
|
|
|
|
|
2026-02-26 21:54:21 +04:00
|
|
|
private renderer = inject(Renderer2);
|
|
|
|
|
private document = inject(DOCUMENT);
|
2026-02-26 22:23:08 +04:00
|
|
|
private langService = inject(LanguageService);
|
2026-07-03 01:58:08 +04:00
|
|
|
private uiRuntime = inject(UiRuntimeFacade);
|
2026-07-09 01:13:54 +04:00
|
|
|
private uxFacade = inject(UserExperienceFacade);
|
|
|
|
|
private configService = inject(ConfigService);
|
2026-07-10 15:55:38 +04:00
|
|
|
private featureConfig = inject(FeatureConfigService);
|
2026-07-10 13:52:01 +04:00
|
|
|
private staticPageResolver = inject(StaticPageResolverService);
|
2026-07-09 01:13:54 +04:00
|
|
|
|
|
|
|
|
readonly wishlistCount = this.uxFacade.wishlistCount;
|
|
|
|
|
readonly compareCount = this.uxFacade.compareCount;
|
2026-07-10 13:43:53 +04:00
|
|
|
readonly userExperienceConfig = computed(() => this.resolveUserExperienceConfig());
|
|
|
|
|
readonly headerConfig = computed(() => this.resolveHeaderConfig());
|
2026-07-10 15:55:38 +04:00
|
|
|
readonly features = this.featureConfig.features;
|
2026-07-10 13:52:01 +04:00
|
|
|
readonly headerPages = computed(() => this.resolveHeaderPages());
|
2026-02-26 21:54:21 +04:00
|
|
|
|
2026-02-14 00:45:17 +04:00
|
|
|
constructor(private cartService: CartService, private router: Router) {
|
2026-01-18 18:57:06 +04:00
|
|
|
this.cartItemCount = this.cartService.itemCount;
|
2026-06-20 14:00:28 +04:00
|
|
|
this.cartTotal = this.cartService.totalPrice;
|
2026-01-18 18:57:06 +04:00
|
|
|
}
|
|
|
|
|
|
2026-07-03 01:58:08 +04:00
|
|
|
get brandName(): string {
|
2026-07-05 00:38:21 +04:00
|
|
|
return this.uiRuntime.marketplaceDisplayName();
|
2026-07-03 01:58:08 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get logo(): string {
|
|
|
|
|
return this.uiRuntime.logoUrl();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 13:33:52 +04:00
|
|
|
get homeUrl(): string {
|
|
|
|
|
return `/${this.langService.currentLanguage()}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 18:57:06 +04:00
|
|
|
toggleMenu(): void {
|
|
|
|
|
this.menuOpen = !this.menuOpen;
|
2026-02-26 21:54:21 +04:00
|
|
|
if (this.menuOpen) {
|
2026-07-09 02:29:12 +04:00
|
|
|
this.renderer.addClass(this.document.body, 'platform-menu-open');
|
2026-02-26 21:54:21 +04:00
|
|
|
} else {
|
2026-07-09 02:29:12 +04:00
|
|
|
this.renderer.removeClass(this.document.body, 'platform-menu-open');
|
2026-02-26 21:54:21 +04:00
|
|
|
}
|
2026-01-18 18:57:06 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
closeMenu(): void {
|
|
|
|
|
this.menuOpen = false;
|
2026-07-09 02:29:12 +04:00
|
|
|
this.renderer.removeClass(this.document.body, 'platform-menu-open');
|
2026-01-18 18:57:06 +04:00
|
|
|
}
|
2026-02-14 00:45:17 +04:00
|
|
|
|
2026-06-20 13:33:52 +04:00
|
|
|
navigateHome(event?: Event): void {
|
|
|
|
|
event?.preventDefault();
|
|
|
|
|
this.closeMenu();
|
|
|
|
|
|
|
|
|
|
const homeUrl = this.homeUrl;
|
|
|
|
|
const currentUrl = this.router.url.split('?')[0].split('#')[0];
|
|
|
|
|
|
|
|
|
|
if (currentUrl === homeUrl || currentUrl === `${homeUrl}/`) {
|
|
|
|
|
this.document.defaultView?.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.router.navigateByUrl(homeUrl).then(() => {
|
|
|
|
|
this.document.defaultView?.scrollTo({ top: 0, behavior: 'auto' });
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 00:45:17 +04:00
|
|
|
navigateToSearch(): void {
|
2026-02-26 22:23:08 +04:00
|
|
|
const lang = this.langService.currentLanguage();
|
|
|
|
|
this.router.navigate([`/${lang}/search`]);
|
2026-02-14 00:45:17 +04:00
|
|
|
}
|
2026-02-14 18:38:25 +04:00
|
|
|
|
|
|
|
|
navigateToCatalog(): void {
|
|
|
|
|
this.closeMenu();
|
2026-02-26 22:23:08 +04:00
|
|
|
const lang = this.langService.currentLanguage();
|
|
|
|
|
this.router.navigate([`/${lang}`]).then(() => {
|
2026-02-14 18:38:25 +04:00
|
|
|
setTimeout(() => {
|
2026-02-26 21:54:21 +04:00
|
|
|
this.document.getElementById('catalog')?.scrollIntoView({ behavior: 'smooth' });
|
2026-02-14 18:38:25 +04:00
|
|
|
}, 100);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-06-20 14:00:28 +04:00
|
|
|
|
2026-07-09 01:13:54 +04:00
|
|
|
navigateToWishlist(): void {
|
|
|
|
|
this.closeMenu();
|
|
|
|
|
const lang = this.langService.currentLanguage();
|
|
|
|
|
this.router.navigate([`/${lang}/wishlist`]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
navigateToCompare(): void {
|
|
|
|
|
this.closeMenu();
|
|
|
|
|
const lang = this.langService.currentLanguage();
|
|
|
|
|
this.router.navigate([`/${lang}/compare`]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 13:52:01 +04:00
|
|
|
navigateToStatic(route: string): void {
|
|
|
|
|
this.closeMenu();
|
|
|
|
|
const lang = this.langService.currentLanguage();
|
|
|
|
|
this.router.navigate([`/${lang}${route.startsWith('/') ? route : `/${route}`}`]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 14:00:28 +04:00
|
|
|
formatCartTotal(total: number): string {
|
|
|
|
|
const locale = this.langService.currentLanguage() === 'en'
|
|
|
|
|
? 'en-US'
|
|
|
|
|
: this.langService.currentLanguage() === 'hy'
|
|
|
|
|
? 'hy-AM'
|
|
|
|
|
: 'ru-RU';
|
|
|
|
|
const fractionDigits = Number.isInteger(total) ? 0 : 2;
|
|
|
|
|
const amount = new Intl.NumberFormat(locale, {
|
|
|
|
|
minimumFractionDigits: fractionDigits,
|
|
|
|
|
maximumFractionDigits: 2,
|
|
|
|
|
}).format(total);
|
|
|
|
|
const currencySymbol = this.langService.getCurrentCurrency()?.symbol ?? this.langService.currentCurrency();
|
|
|
|
|
|
|
|
|
|
return `${amount} ${currencySymbol}`;
|
|
|
|
|
}
|
2026-07-09 01:13:54 +04:00
|
|
|
|
|
|
|
|
private resolveUserExperienceConfig() {
|
2026-07-10 13:43:53 +04:00
|
|
|
this.configService.bootstrapRevision();
|
2026-07-09 01:13:54 +04:00
|
|
|
const raw = (this.configService.getBootstrapSnapshot() as any)?.userExperience ?? {};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...DEFAULT_USER_EXPERIENCE_CONFIG,
|
|
|
|
|
...raw,
|
|
|
|
|
wishlist: {
|
|
|
|
|
...DEFAULT_USER_EXPERIENCE_CONFIG.wishlist,
|
|
|
|
|
...(raw.wishlist ?? {})
|
|
|
|
|
},
|
|
|
|
|
compare: {
|
|
|
|
|
...DEFAULT_USER_EXPERIENCE_CONFIG.compare,
|
|
|
|
|
...(raw.compare ?? {})
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-07-10 13:43:53 +04:00
|
|
|
|
|
|
|
|
private resolveHeaderConfig() {
|
|
|
|
|
this.configService.bootstrapRevision();
|
|
|
|
|
const raw = (this.configService.getBootstrapSnapshot() as any)?.header ?? {};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...DEFAULT_HEADER_CONFIG,
|
|
|
|
|
...raw,
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-07-10 13:52:01 +04:00
|
|
|
|
|
|
|
|
private resolveHeaderPages() {
|
|
|
|
|
this.configService.bootstrapRevision();
|
|
|
|
|
const bootstrap = this.configService.getBootstrapSnapshot();
|
|
|
|
|
if (!bootstrap?.staticPages) {
|
|
|
|
|
return [] as Array<{ id: string; title: string; route: string; icon?: string; order: number }>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lang = this.langService.currentLanguage();
|
|
|
|
|
const pages = Object.values(bootstrap.staticPages as Record<string, any>)
|
|
|
|
|
.filter(page => page.showInHeader === true)
|
|
|
|
|
.map(page => {
|
|
|
|
|
const resolved = this.staticPageResolver.resolveByKeyFromBootstrap(bootstrap, page.id, lang);
|
|
|
|
|
return resolved ? {
|
|
|
|
|
id: resolved.id,
|
|
|
|
|
title: resolved.title,
|
|
|
|
|
route: resolved.route,
|
|
|
|
|
icon: resolved.icon,
|
|
|
|
|
order: page.order ?? 0,
|
|
|
|
|
} : null;
|
|
|
|
|
})
|
|
|
|
|
.filter((page): page is { id: string; title: string; route: string; icon: string | undefined; order: number } => page !== null);
|
|
|
|
|
|
|
|
|
|
return pages.sort((left, right) => left.order - right.order);
|
|
|
|
|
}
|
2026-01-18 18:57:06 +04:00
|
|
|
}
|
2026-07-09 02:29:12 +04:00
|
|
|
|
|
|
|
|
|