Files
marketplaces/src/app/components/header/header.component.ts
sdarbinyan 14c72d1a6a feat: extract auth into @marketplaces/auth package, add backoffice admin provisioning spec
- ADR-0001: decision to extract auth/payment into shared @marketplaces/* packages
- Scaffold packages/auth, packages/payment; @marketplaces/auth now holds the real
  telegram (customer+admin QR/session) and ed25519 (future admin challenge/response)
  auth implementation, pushed to sources.vitanova.network/sdarbinyan/vitanovaPackages
- Rewire ~30 call sites to import from @marketplaces/auth; delete migrated originals
  from core/auth, core/admin-auth, services/, models/
- Replace environment coupling with AUTH_API_URL/TELEGRAM_BOT_USERNAME injection
  tokens and isDevMode(); wired as file:packages/auth pending registry publish
- Add TRACK-S §8: bootstrap per-marketplace admin login + marketplace-scoped
  sub-admin invite/role endpoints
- Build, arch:check:boundaries, and full test suite (103/103) all green

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 01:05:16 +04:00

210 lines
7.2 KiB
TypeScript

import { Component, ChangeDetectionStrategy, Renderer2, inject, DOCUMENT, computed } from '@angular/core';
import { Router, RouterLink, RouterLinkActive } from '@angular/router';
import { CartService } from '../../services/cart.service';
import { LanguageService } from '../../services/language.service';
import { LogoComponent } from '../logo/logo.component';
import { LanguageSelectorComponent } from '../language-selector/language-selector.component';
import { RegionSelectorComponent } from '../region-selector/region-selector.component';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade';
import { UserExperienceFacade } from '../../facades/platform/user-experience.facade';
import { ConfigService } from '../../core/config/config.service';
import { FeatureConfigService } from '../../core/config/feature-config.service';
import { DEFAULT_HEADER_CONFIG, DEFAULT_USER_EXPERIENCE_CONFIG } from '../../shared/models/config';
import { StaticPageResolverService } from '../../core/config/static-page-resolver.service';
import { IconComponent } from '../../shared/ui/icon/icon.component';
import { AuthService } from '@marketplaces/auth';
import { TelegramLoginComponent } from '../telegram-login/telegram-login.component';
@Component({
selector: 'app-header',
imports: [RouterLink, RouterLinkActive, LogoComponent, LanguageSelectorComponent, RegionSelectorComponent, LangRoutePipe, TranslatePipe, IconComponent, TelegramLoginComponent],
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderComponent {
cartItemCount;
cartTotal;
menuOpen = false;
private renderer = inject(Renderer2);
private document = inject(DOCUMENT);
private langService = inject(LanguageService);
private uiRuntime = inject(UiRuntimeFacade);
private uxFacade = inject(UserExperienceFacade);
private configService = inject(ConfigService);
private featureConfig = inject(FeatureConfigService);
private staticPageResolver = inject(StaticPageResolverService);
private authService = inject(AuthService);
readonly isAuthenticated = this.authService.isAuthenticated;
readonly wishlistCount = this.uxFacade.wishlistCount;
readonly compareCount = this.uxFacade.compareCount;
readonly userExperienceConfig = computed(() => this.resolveUserExperienceConfig());
readonly headerConfig = computed(() => this.resolveHeaderConfig());
readonly features = this.featureConfig.features;
readonly headerPages = computed(() => this.resolveHeaderPages());
constructor(private cartService: CartService, private router: Router) {
this.cartItemCount = this.cartService.itemCount;
this.cartTotal = this.cartService.totalPrice;
}
get brandName(): string {
return this.uiRuntime.marketplaceDisplayName();
}
get logo(): string {
return this.uiRuntime.logoUrl();
}
get homeUrl(): string {
return `/${this.langService.currentLanguage()}`;
}
toggleMenu(): void {
this.menuOpen = !this.menuOpen;
if (this.menuOpen) {
this.renderer.addClass(this.document.body, 'platform-menu-open');
} else {
this.renderer.removeClass(this.document.body, 'platform-menu-open');
}
}
closeMenu(): void {
this.menuOpen = false;
this.renderer.removeClass(this.document.body, 'platform-menu-open');
}
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' });
});
}
navigateToSearch(): void {
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}/search`]);
}
navigateToCatalog(): void {
this.closeMenu();
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}`]).then(() => {
setTimeout(() => {
this.document.getElementById('catalog')?.scrollIntoView({ behavior: 'smooth' });
}, 100);
});
}
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`]);
}
login(): void {
this.authService.requestLogin();
}
logout(): void {
this.authService.logout();
}
navigateToStatic(route: string): void {
this.closeMenu();
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}${route.startsWith('/') ? route : `/${route}`}`]);
}
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}`;
}
private resolveUserExperienceConfig() {
this.configService.bootstrapRevision();
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 ?? {})
}
};
}
private resolveHeaderConfig() {
this.configService.bootstrapRevision();
const raw = (this.configService.getBootstrapSnapshot() as any)?.header ?? {};
return {
...DEFAULT_HEADER_CONFIG,
...raw,
};
}
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);
}
}