feat(builder): add project editor

This commit is contained in:
sdarbinyan
2026-07-10 13:43:53 +04:00
parent 7161a81068
commit e2c8747fcc
50 changed files with 1656 additions and 42 deletions

View File

@@ -1,11 +1,11 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { take } from 'rxjs/operators';
import { Component, ChangeDetectionStrategy, effect, inject, signal } from '@angular/core';
import { RouterLink } from '@angular/router';
import { TranslatePipe } from '../../i18n/translate.pipe';
import { LogoComponent } from '../logo/logo.component';
import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { FooterPaymentIcon, FooterResolvedGroup, FooterResolverService } from '../../core/config/footer-resolver.service';
import { ConfigService } from '../../core/config/config.service';
@Component({
selector: 'app-footer',
@@ -19,22 +19,29 @@ export class FooterComponent {
readonly paymentIcons = signal<FooterPaymentIcon[]>([]);
readonly copyrightText = signal('');
private readonly configService = inject(ConfigService);
constructor(
private readonly uiRuntime: UiRuntimeFacade,
private readonly footerResolver: FooterResolverService
) {
this.footerResolver.resolveFooterModel().pipe(take(1)).subscribe({
next: model => {
this.footerGroups.set(model.groups);
this.paymentIcons.set(model.paymentIcons);
this.copyrightText.set(model.copyrightText);
},
error: () => {
effect(() => {
this.configService.bootstrapRevision();
const bootstrap = this.configService.getBootstrapSnapshot();
if (!bootstrap) {
this.footerGroups.set([]);
this.paymentIcons.set([]);
this.copyrightText.set('');
return;
}
const model = this.footerResolver.resolveFooterModelFromBootstrap(bootstrap);
this.footerGroups.set(model.groups);
this.paymentIcons.set(model.paymentIcons);
this.copyrightText.set(model.copyrightText);
});
this.configService.loadBootstrap().subscribe();
}
currentYear = new Date().getFullYear();

View File

@@ -2,9 +2,11 @@
<header class="platform-header">
<div class="platform-header-container">
<!-- Logo -->
<a [attr.href]="homeUrl" class="platform-logo" (click)="navigateHome($event)">
<app-logo />
</a>
@if (headerConfig().showLogo) {
<a [attr.href]="homeUrl" class="platform-logo" (click)="navigateHome($event)">
<app-logo />
</a>
}
<!-- Navigation Buttons (desktop) -->
<nav class="platform-nav">
@@ -13,11 +15,17 @@
(click)="closeMenu()" class="platform-nav-btn platform-nav-btn-left">
{{ 'header.home' | translate }}
</a>
@if (headerConfig().showCategories) {
<button type="button" (click)="navigateToCatalog()" class="platform-nav-btn platform-nav-btn-left">
{{ 'header.catalog' | translate }}
</button>
}
<!-- TODO(CMS): Render backend-configured header content links here. -->
</div>
</nav>
<!-- Search Box (desktop) -->
@if (headerConfig().showSearch) {
<div class="platform-search-wrapper">
<div class="platform-search-box">
<svg class="platform-search-icon" width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -27,27 +35,30 @@
<input type="text" [placeholder]="'header.searchPlaceholder' | translate" class="platform-search-input" (click)="navigateToSearch()" readonly />
</div>
</div>
}
<!-- Search Icon (mobile only) -->
@if (headerConfig().showSearch) {
<button class="platform-search-mobile" (click)="navigateToSearch()" [attr.aria-label]="'header.search' | translate">
<svg width="22" height="22" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12Z" fill="#1e3c38" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.2929 18.2929C18.6834 17.9024 19.3166 17.9024 19.7071 18.2929L25.7071 24.2929C26.0976 24.6834 26.0976 25.3166 25.7071 25.7071C25.3166 26.0976 24.6834 26.0976 24.2929 25.7071L18.2929 19.7071C17.9024 19.3166 17.9024 18.6834 18.2929 18.2929Z" fill="#1e3c38" />
</svg>
</button>
}
<!-- Right Actions -->
<div class="platform-actions">
@if (userExperienceConfig.wishlist.enabled) {
@if (headerConfig().showWishlist && userExperienceConfig().wishlist.enabled) {
<button type="button" class="platform-ux-btn" (click)="navigateToWishlist()" [attr.aria-label]="'header.wishlist' | translate">
<span class="platform-ux-icon"></span>
@if (userExperienceConfig.wishlist.headerBadgeEnabled && wishlistCount() > 0) {
@if (userExperienceConfig().wishlist.headerBadgeEnabled && wishlistCount() > 0) {
<span class="platform-ux-badge">{{ wishlistCount() }}</span>
}
</button>
}
@if (userExperienceConfig.compare.enabled) {
@if (headerConfig().showCompare && userExperienceConfig().compare.enabled) {
<button type="button" class="platform-ux-btn" (click)="navigateToCompare()" [attr.aria-label]="'header.compare' | translate">
<span class="platform-ux-icon"></span>
@if (compareCount() > 0) {
@@ -57,6 +68,7 @@
}
<!-- Cart Button -->
@if (headerConfig().showCart) {
<a [routerLink]="'/cart' | langRoute" routerLinkActive="platform-cart-active" class="platform-cart-btn" (click)="closeMenu()">
<span class="platform-cart-icon">
<svg width="32" height="24" viewBox="0 0 48 32" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -72,16 +84,21 @@
<span class="platform-cart-total">{{ formatCartTotal(cartTotal()) }}</span>
}
</a>
}
<!-- Region Selector (desktop only) -->
@if (headerConfig().showRegion) {
<div class="platform-region-selector platform-lang-desktop">
<app-region-selector />
</div>
}
<!-- Language Selector (desktop only) -->
@if (headerConfig().showLanguages) {
<div class="platform-lang-selector platform-lang-desktop">
<app-language-selector />
</div>
}
<!-- Mobile Menu Toggle -->
<button class="platform-menu-toggle" (click)="toggleMenu()" [class.active]="menuOpen" [attr.aria-label]="menuOpen ? ('header.closeMenu' | translate) : ('header.openMenu' | translate)" [attr.aria-expanded]="menuOpen">
@@ -111,6 +128,7 @@
</svg>
</a>
@if (headerConfig().showCategories) {
<a (click)="navigateToCatalog()" class="platform-mobile-item" style="cursor: pointer;">
<svg width="24" height="24" viewBox="0 0 31 31" fill="none">
<path d="M1.9375 4.84375C1.9375 3.23867 3.23867 1.9375 4.84375 1.9375L10.6562 1.9375C12.2613 1.9375 13.5625 3.23867 13.5625 4.84375V10.6562C13.5625 12.2613 12.2613 13.5625 10.6562 13.5625H4.84375C3.23867 13.5625 1.9375 12.2613 1.9375 10.6562L1.9375 4.84375ZM4.84375 3.875C4.30872 3.875 3.875 4.30872 3.875 4.84375V10.6562C3.875 11.1913 4.30872 11.625 4.84375 11.625H10.6562C11.1913 11.625 11.625 11.1913 11.625 10.6562V4.84375C11.625 4.30872 11.1913 3.875 10.6562 3.875H4.84375ZM17.4375 4.84375C17.4375 3.23867 18.7387 1.9375 20.3438 1.9375L26.1562 1.9375C27.7613 1.9375 29.0625 3.23867 29.0625 4.84375V10.6562C29.0625 12.2613 27.7613 13.5625 26.1562 13.5625H20.3438C18.7387 13.5625 17.4375 12.2613 17.4375 10.6562V4.84375ZM20.3438 3.875C19.8087 3.875 19.375 4.30872 19.375 4.84375V10.6562C19.375 11.1913 19.8087 11.625 20.3438 11.625H26.1562C26.6913 11.625 27.125 11.1913 27.125 10.6562V4.84375C27.125 4.30872 26.6913 3.875 26.1562 3.875H20.3438ZM1.9375 20.3438C1.9375 18.7387 3.23867 17.4375 4.84375 17.4375H10.6562C12.2613 17.4375 13.5625 18.7387 13.5625 20.3438V26.1562C13.5625 27.7613 12.2613 29.0625 10.6562 29.0625H4.84375C3.23867 29.0625 1.9375 27.7613 1.9375 26.1562L1.9375 20.3438ZM4.84375 19.375C4.30872 19.375 3.875 19.8087 3.875 20.3438V26.1562C3.875 26.6913 4.30872 27.125 4.84375 27.125H10.6562C11.1913 27.125 11.625 26.6913 11.625 26.1562V20.3438C11.625 19.8087 11.1913 19.375 10.6562 19.375H4.84375ZM17.4375 20.3438C17.4375 18.7387 18.7387 17.4375 20.3438 17.4375H26.1562C27.7613 17.4375 29.0625 18.7387 29.0625 20.3438V26.1562C29.0625 27.7613 27.7613 29.0625 26.1562 29.0625H20.3438C18.7387 29.0625 17.4375 27.7613 17.4375 26.1562V20.3438ZM20.3438 19.375C19.8087 19.375 19.375 19.8087 19.375 20.3438V26.1562C19.375 26.6913 19.8087 27.125 20.3438 27.125H26.1562C26.6913 27.125 27.125 26.6913 27.125 26.1562V20.3438C27.125 19.8087 26.6913 19.375 26.1562 19.375H20.3438Z" fill="#497671" />
@@ -120,16 +138,21 @@
<path d="M1 1L7 7L1 13" stroke="#697777" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</a>
}
<!-- TODO(CMS): Render backend-configured mobile content links here. -->
<div class="platform-mobile-controls">
@if (headerConfig().showRegion) {
<div class="platform-mobile-lang">
<app-region-selector />
</div>
}
@if (headerConfig().showLanguages) {
<div class="platform-mobile-lang">
<app-language-selector />
</div>
}
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { Component, ChangeDetectionStrategy, Renderer2, inject, DOCUMENT } from '@angular/core';
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';
@@ -10,7 +10,7 @@ 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 { DEFAULT_USER_EXPERIENCE_CONFIG } from '../../shared/models/config';
import { DEFAULT_HEADER_CONFIG, DEFAULT_USER_EXPERIENCE_CONFIG } from '../../shared/models/config';
@Component({
selector: 'app-header',
@@ -33,7 +33,8 @@ export class HeaderComponent {
readonly wishlistCount = this.uxFacade.wishlistCount;
readonly compareCount = this.uxFacade.compareCount;
readonly userExperienceConfig = this.resolveUserExperienceConfig();
readonly userExperienceConfig = computed(() => this.resolveUserExperienceConfig());
readonly headerConfig = computed(() => this.resolveHeaderConfig());
constructor(private cartService: CartService, private router: Router) {
this.cartItemCount = this.cartService.itemCount;
@@ -127,6 +128,7 @@ export class HeaderComponent {
}
private resolveUserExperienceConfig() {
this.configService.bootstrapRevision();
const raw = (this.configService.getBootstrapSnapshot() as any)?.userExperience ?? {};
return {
@@ -142,6 +144,16 @@ export class HeaderComponent {
}
};
}
private resolveHeaderConfig() {
this.configService.bootstrapRevision();
const raw = (this.configService.getBootstrapSnapshot() as any)?.header ?? {};
return {
...DEFAULT_HEADER_CONFIG,
...raw,
};
}
}