cleanup: remove tenant variant logic and enforce config-driven UI

This commit is contained in:
sdarbinyan
2026-07-05 04:07:25 +04:00
parent 79a12c0ec2
commit 6550250d13
19 changed files with 153 additions and 1102 deletions

View File

@@ -1,19 +1,16 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core'; import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { TranslateService } from '../../i18n/translate.service'; import { TranslateService } from '../../i18n/translate.service';
import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade';
@Component({ @Component({
selector: 'app-back-button', selector: 'app-back-button',
template: ` template: `
@if (!isMarketplaceNovo) {
<button class="dexar-back-btn" (click)="goBack()" [attr.aria-label]="i18n.t('itemDetail.back')"> <button class="dexar-back-btn" (click)="goBack()" [attr.aria-label]="i18n.t('itemDetail.back')">
<svg width="37" height="24" viewBox="0 0 37 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="37" height="24" viewBox="0 0 37 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.73 11.46c-.97-.52-.97-1.4 0-1.92L20.39 1.21c1.48-.79 3.89-.19 3.89.95v3.74h6.94c1.28 0 2.31.59 2.31 1.31v6.56c0 .73-1.03 1.31-2.31 1.31h-6.94v3.74c0 1.15-2.42 1.74-3.89.96L4.73 11.46Z" <path d="M4.73 11.46c-.97-.52-.97-1.4 0-1.92L20.39 1.21c1.48-.79 3.89-.19 3.89.95v3.74h6.94c1.28 0 2.31.59 2.31 1.31v6.56c0 .73-1.03 1.31-2.31 1.31h-6.94v3.74c0 1.15-2.42 1.74-3.89.96L4.73 11.46Z"
fill="#497671" fill-opacity="0.75" stroke="white" stroke-opacity="0.6" stroke-linejoin="round"/> fill="#497671" fill-opacity="0.75" stroke="white" stroke-opacity="0.6" stroke-linejoin="round"/>
</svg> </svg>
</button> </button>
}
`, `,
styles: [` styles: [`
.dexar-back-btn { .dexar-back-btn {
@@ -62,13 +59,8 @@ import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade';
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class BackButtonComponent { export class BackButtonComponent {
private readonly uiRuntime = inject(UiRuntimeFacade);
i18n = inject(TranslateService); i18n = inject(TranslateService);
get isMarketplaceNovo(): boolean {
return this.uiRuntime.isMarketplaceVariant('novo');
}
constructor(private location: Location) {} constructor(private location: Location) {}
goBack(): void { goBack(): void {

View File

@@ -1,95 +1,13 @@
<!-- novo VERSION - Modern Clean Footer --> <footer class="app-footer">
@if (isMarketplaceNovo) { <div class="app-footer__container page-container">
<div class="app-footer__top">
<footer class="novo-footer"> <div class="app-footer__brand">
<div class="novo-footer-container">
<div class="novo-footer-top">
<div class="novo-footer-col">
<h4>{{ brandName }}</h4>
<p class="novo-footer-desc">{{ 'footer.description' | translate }}</p>
</div>
@for (group of footerGroups(); track group.id) {
<div class="novo-footer-col">
@if (group.title) {
<h4>{{ group.title }}</h4>
}
<ul class="novo-footer-links">
@for (item of group.items; track item.id) {
<li>
<a [routerLink]="item.route | langRoute">{{ item.label | translate }}</a>
</li>
}
</ul>
</div>
}
</div>
<div class="novo-footer-bottom">
<div class="novo-copyright">
<p>© {{ currentYear }} {{ brandName }}</p>
</div>
<div class="novo-payment-icons">
<img src="/assets/images/mir-logo.svg" alt="МИР" loading="lazy" width="40" height="28" />
<img src="/assets/images/visa-logo.svg" alt="Visa" loading="lazy" width="40" height="28" />
<img src="/assets/images/mastercard-logo.svg" alt="Mastercard" loading="lazy" width="40" height="28" />
</div>
</div>
</div>
</footer>
} @else if (isMarketplaceLavero) {
<!-- LAVERO VERSION -->
<footer class="lavero-footer">
<div class="lavero-footer-container">
<div class="lavero-footer-top">
<div class="lavero-footer-col lavero-footer-brand">
<div class="lavero-footer-logo">
<app-logo /> <app-logo />
</div> <p>{{ 'footer.description' | translate }}</p>
<p class="lavero-footer-desc">{{ 'footer.description' | translate }}</p>
</div> </div>
@for (group of footerGroups(); track group.id) { @for (group of footerGroups(); track group.id) {
<div class="lavero-footer-col"> <div class="app-footer__group">
@if (group.title) {
<h4>{{ group.title }}</h4>
}
<ul class="lavero-footer-links">
@for (item of group.items; track item.id) {
<li>
<a [routerLink]="item.route | langRoute">{{ item.label | translate }}</a>
</li>
}
</ul>
</div>
}
</div>
<div class="lavero-footer-bottom">
<div class="lavero-copyright">
<p>© {{ currentYear }} {{ brandName }}</p>
</div>
<div class="lavero-payment-icons">
<img src="/assets/images/mir-logo.svg" alt="МИР" loading="lazy" width="40" height="28" />
<img src="/assets/images/visa-logo.svg" alt="Visa" loading="lazy" width="40" height="28" />
<img src="/assets/images/mastercard-logo.svg" alt="Mastercard" loading="lazy" width="40" height="28" />
</div>
</div>
</div>
</footer>
} @else {
<!-- DEXAR VERSION - Redesigned -->
<footer class="dexar-footer">
<div class="dexar-footer-bg">
<div class="dexar-footer-container">
<div class="dexar-footer-top">
<div class="dexar-footer-logo">
<app-logo />
</div>
<div class="dexar-footer-columns">
@for (group of footerGroups(); track group.id) {
<div class="dexar-footer-col">
@if (group.title) { @if (group.title) {
<h4>{{ group.title }}</h4> <h4>{{ group.title }}</h4>
} }
@@ -103,21 +21,21 @@
</div> </div>
} }
<div class="dexar-footer-col"> <div class="app-footer__group">
<h4>{{ 'footer.payment' | translate }}</h4> <h4>{{ 'footer.payment' | translate }}</h4>
<div class="dexar-payment-logos"> <div class="app-footer__payments">
<img src="/assets/images/mir-logo.svg" alt="МИР" loading="lazy" width="48" height="32" /> <img src="/assets/images/mir-logo.svg" alt="MIR" loading="lazy" width="40" height="28" />
<img src="/assets/images/visa-logo.svg" alt="Visa" loading="lazy" width="48" height="32" /> <img src="/assets/images/visa-logo.svg" alt="Visa" loading="lazy" width="40" height="28" />
<img src="/assets/images/mastercard-logo.svg" alt="Mastercard" loading="lazy" width="48" height="32" /> <img src="/assets/images/mastercard-logo.svg" alt="Mastercard" loading="lazy" width="40" height="28" />
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="dexar-footer-bottom"> <div class="app-footer__bottom">
<p>&copy; {{ currentYear }} {{ brandName }}. {{ 'footer.allRightsReserved' | translate }}</p> <p>&copy; {{ currentYear }} {{ brandName }}. {{ 'footer.allRightsReserved' | translate }}</p>
</div> @if (contactEmail) {
<a [href]="'mailto:' + contactEmail">{{ contactEmail }}</a>
}
</div> </div>
</div> </div>
</footer> </footer>
}

View File

@@ -1,560 +1,108 @@
:host { :host {
display: block; display: block;
width: 100%; width: 100%;
}
// ========== LAVERO FOOTER STYLES ==========
.lavero-footer {
background: linear-gradient(135deg, #1a1208 0%, #2d1a00 100%);
color: white;
margin-top: auto; margin-top: auto;
padding: 3rem 0 0;
border-top: 3px solid var(--primary-color, #FD7300);
} }
.lavero-footer-container { .app-footer {
max-width: 1400px; background: var(--text-primary, #1e3c38);
margin: 0 auto; color: #fff;
padding: 0 2rem;
} }
.lavero-footer-top { .app-footer__container {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
gap: 3rem;
padding-bottom: 3rem;
border-bottom: 1px solid rgba(253, 115, 0, 0.2);
}
.lavero-footer-brand {
display: flex;
flex-direction: column;
gap: 1rem;
}
.lavero-footer-logo {
width: 140px;
height: 48px;
app-logo {
display: block;
width: 100%; width: 100%;
height: 100%;
}
} }
.lavero-footer-col { .app-footer__top {
h4 { display: grid;
font-size: 1.1rem; grid-template-columns: 2fr repeat(3, minmax(160px, 1fr));
font-weight: 600;
margin-bottom: 1.25rem;
color: var(--primary-color, #FD7300);
}
.lavero-footer-desc {
color: rgba(255, 255, 255, 0.65);
line-height: 1.6;
font-size: 0.9rem;
}
}
.lavero-footer-links {
list-style: none;
padding: 0;
margin: 0;
li {
margin-bottom: 0.75rem;
a {
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
font-size: 0.9rem;
transition: color 0.2s, transform 0.2s;
display: inline-block;
&:hover {
color: var(--primary-color, #FD7300);
transform: translateX(4px);
}
}
}
}
.lavero-footer-bottom {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 0;
}
.lavero-copyright {
p {
color: rgba(255, 255, 255, 0.5);
font-size: 0.85rem;
margin: 0;
}
}
.lavero-payment-icons {
display: flex;
gap: 0.75rem;
align-items: center;
img {
height: 26px;
width: auto;
background: white;
padding: 0.25rem 0.5rem;
border-radius: 5px;
opacity: 0.85;
transition: opacity 0.2s;
&:hover {
opacity: 1;
}
}
}
@media (max-width: 968px) {
.lavero-footer-top {
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
}
@media (max-width: 600px) {
.lavero-footer {
padding: 2rem 0 0;
}
.lavero-footer-container {
padding: 0 1rem;
}
.lavero-footer-top {
grid-template-columns: 1fr;
gap: 1.5rem; gap: 1.5rem;
padding-bottom: 1.5rem; padding-bottom: 1.5rem;
}
.lavero-footer-bottom {
flex-direction: column;
gap: 1rem;
padding: 1rem 0;
text-align: center;
}
}
.footer {
background-color: #1a1a1a;
color: #ffffff;
margin-top: auto;
padding: 3rem 0 1rem;
border-top: 1px solid #333;
.footer-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
}
.footer-section {
h3 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
color: #ffffff;
}
ul {
list-style: none;
padding: 0;
margin: 0;
li {
margin-bottom: 0.5rem;
a {
color: #b0b0b0;
text-decoration: none;
transition: color 0.2s ease;
font-size: 0.95rem;
&:hover {
color: #ffffff;
}
}
}
}
&.payment-systems {
.payment-logos {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
.payment-logo {
height: 40px;
width: auto;
background: white;
padding: 0.5rem;
border-radius: 4px;
}
}
}
}
.footer-bottom {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 2rem 0;
margin-top: 2rem;
border-top: 1px solid #333;
text-align: center;
p {
color: #808080;
font-size: 0.9rem;
margin: 0;
}
}
@media (max-width: 768px) {
padding: 2rem 0 1rem;
.footer-container {
grid-template-columns: 1fr;
gap: 1.5rem;
padding: 0 1rem;
}
.footer-section {
h3 {
font-size: 1rem;
}
}
}
}
// ========== novo FOOTER STYLES ==========
.novo-footer {
background: var(--text-primary);
color: white;
margin-top: auto;
padding: 3rem 0 0;
}
.novo-footer-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 2rem;
}
.novo-footer-top {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
gap: 3rem;
padding-bottom: 3rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.novo-footer-col {
h4 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1.25rem;
color: white;
}
.novo-footer-desc {
color: rgba(255, 255, 255, 0.7);
line-height: 1.6;
font-size: 0.95rem;
}
}
.novo-footer-links {
list-style: none;
padding: 0;
margin: 0;
li {
margin-bottom: 0.75rem;
a {
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
font-size: 0.95rem;
transition: all 0.3s;
display: inline-block;
&:hover {
color: white;
transform: translateX(4px);
}
}
}
}
.novo-footer-bottom {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 0;
}
.novo-copyright {
p {
color: rgba(255, 255, 255, 0.6);
font-size: 0.9rem;
margin: 0;
}
}
.novo-payment-icons {
display: flex;
gap: 0.75rem;
align-items: center;
img {
height: 28px;
width: auto;
background: white;
padding: 0.25rem 0.5rem;
border-radius: 6px;
opacity: 0.9;
transition: opacity 0.3s;
&:hover {
opacity: 1;
}
}
}
@media (max-width: 968px) {
.novo-footer-top {
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
}
@media (max-width: 600px) {
.novo-footer {
padding: 2rem 0 0;
}
.novo-footer-container {
padding: 0 1rem;
}
.novo-footer-top {
grid-template-columns: 1fr;
gap: 2rem;
padding-bottom: 2rem;
}
.novo-footer-bottom {
flex-direction: column;
gap: 1rem;
padding: 1rem 0;
text-align: center;
}
}
// ========== DEXAR FOOTER STYLES ==========
.dexar-footer {
margin-top: auto;
}
.dexar-footer-bg {
background-image: url('/assets/images/footer_bg.webp');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
inset: 0;
background: rgba(30, 60, 56, 0.85);
}
}
.dexar-footer-container {
position: relative;
z-index: 1;
max-width: 1440px;
margin: 0 auto;
padding: 48px 40px 24px;
}
.dexar-footer-top {
display: flex;
gap: 48px;
padding-bottom: 32px;
border-bottom: 1px solid rgba(255, 255, 255, 0.15); border-bottom: 1px solid rgba(255, 255, 255, 0.15);
} }
.dexar-footer-logo { .app-footer__brand {
flex-shrink: 0;
display: flex;
align-items: flex-start;
padding-top: 4px;
.dexar-footer-logo-desktop {
display: block;
height: 40px;
width: auto;
filter: brightness(0) invert(1);
}
.dexar-footer-logo-mobile {
display: none;
height: 32px;
width: auto;
filter: brightness(0) invert(1);
}
}
.dexar-footer-columns {
flex: 1;
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); gap: 0.75rem;
gap: 32px;
p {
margin: 0;
color: rgba(255, 255, 255, 0.8);
line-height: 1.5;
}
} }
.dexar-footer-col { .app-footer__group {
h4 { h4 {
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0 0 0.75rem;
font-size: 15px; font-size: 0.95rem;
font-weight: 600; color: #fff;
color: #ffffff;
margin: 0 0 16px 0;
} }
ul { ul {
list-style: none; list-style: none;
padding: 0;
margin: 0; margin: 0;
padding: 0;
li { display: grid;
margin-bottom: 10px; gap: 0.5rem;
}
a { a {
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: rgba(255, 255, 255, 0.8);
font-size: 13px;
font-weight: 400;
color: rgba(255, 255, 255, 0.7);
text-decoration: none; text-decoration: none;
transition: color 0.2s ease;
&:hover { &:hover {
color: #ffffff; color: #fff;
}
}
} }
} }
} }
.dexar-payment-logos { .app-footer__payments {
display: flex; display: flex;
gap: 10px; gap: 0.5rem;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center;
img { img {
height: 28px;
max-height: 28px;
width: auto;
max-width: 60px;
background: rgba(255, 255, 255, 0.9);
padding: 4px 8px;
border-radius: 4px; border-radius: 4px;
transition: opacity 0.2s; background: #fff;
object-fit: contain; padding: 2px 6px;
&:hover {
opacity: 0.85;
}
} }
} }
.dexar-footer-bottom { .app-footer__bottom {
padding: 20px 0 0; display: flex;
text-align: center; justify-content: space-between;
align-items: center;
gap: 1rem;
padding-top: 1rem;
padding-bottom: 0.5rem;
p { p {
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
margin: 0; margin: 0;
color: rgba(255, 255, 255, 0.7);
font-size: 0.85rem;
}
a {
color: rgba(255, 255, 255, 0.85);
text-decoration: none;
font-size: 0.85rem;
} }
} }
// Responsive @media (max-width: 960px) {
@media (max-width: 992px) { .app-footer__top {
.dexar-footer-container { grid-template-columns: repeat(2, minmax(0, 1fr));
padding: 40px 32px 20px; }
} }
.dexar-footer-top { @media (max-width: 640px) {
.app-footer__top {
grid-template-columns: 1fr;
}
.app-footer__bottom {
flex-direction: column; flex-direction: column;
gap: 32px; align-items: flex-start;
}
.dexar-footer-columns {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.dexar-footer-bg {
background-image: url('/assets/images/footer_bg_mobile.webp');
}
.dexar-footer-container {
padding: 32px 20px 16px;
}
.dexar-footer-logo {
.dexar-footer-logo-desktop {
display: none;
}
.dexar-footer-logo-mobile {
display: block;
}
}
.dexar-footer-columns {
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
.dexar-footer-col h4 {
font-size: 14px;
margin-bottom: 12px;
}
.dexar-footer-col ul li a {
font-size: 12px;
}
}
@media (max-width: 480px) {
.dexar-footer-container {
padding: 24px 16px 12px;
}
.dexar-footer-columns {
grid-template-columns: repeat(2, 1fr);
gap: 16px;
} }
} }

View File

@@ -58,14 +58,6 @@ export class FooterComponent {
return this.uiRuntime.contactEmail(); return this.uiRuntime.contactEmail();
} }
get isMarketplaceNovo(): boolean {
return this.uiRuntime.isMarketplaceVariant('novo');
}
get isMarketplaceLavero(): boolean {
return this.uiRuntime.isMarketplaceVariant('lavero');
}
private resolveFooterGroups(bootstrap: BootstrapConfig): FooterResolvedGroup[] { private resolveFooterGroups(bootstrap: BootstrapConfig): FooterResolvedGroup[] {
const footer = bootstrap.navigation?.footer ?? []; const footer = bootstrap.navigation?.footer ?? [];
const lang = this.languageService.currentLanguage(); const lang = this.languageService.currentLanguage();

View File

@@ -1,60 +1,3 @@
<!-- novo VERSION - Modern Minimalist Header -->
@if (isMarketplaceNovo) {
<header class="novo-header">
<div class="novo-header-container">
<div class="novo-left">
<a [attr.href]="homeUrl" class="novo-logo" (click)="navigateHome($event)">
<app-logo />
<!-- <span class="novo-brand">{{ brandName }}</span> -->
</a>
</div>
<nav class="novo-nav" [class.novo-nav-open]="menuOpen">
<div class="novo-nav-links">
<a [routerLink]="'/' | langRoute" routerLinkActive="novo-active" [routerLinkActiveOptions]="{exact: true}" (click)="closeMenu()" class="novo-link">
{{ 'header.home' | translate }}
</a>
<a [routerLink]="'/search' | langRoute" routerLinkActive="novo-active" (click)="closeMenu()" class="novo-link">
{{ 'header.search' | translate }}
</a>
<!-- TODO(CMS): Render backend-configured header content links here. -->
</div>
<div class="novo-mobile-controls">
<app-region-selector />
<app-language-selector />
</div>
</nav>
<div class="novo-right">
<app-region-selector />
<app-language-selector />
<a [routerLink]="'/cart' | langRoute" routerLinkActive="novo-cart-active" class="novo-cart" (click)="closeMenu()" [attr.aria-label]="'header.cart' | translate">
<span class="novo-cart-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="9" cy="21" r="1"></circle>
<circle cx="20" cy="21" r="1"></circle>
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
</svg>
@if (cartItemCount() > 0) {
<span class="novo-cart-badge">{{ cartItemCount() }}</span>
}
</span>
@if (cartItemCount() > 0) {
<span class="novo-cart-total">{{ formatCartTotal(cartTotal()) }}</span>
}
</a>
<button class="menu-toggle novo-menu-toggle" (click)="toggleMenu()" [class.active]="menuOpen" [class.novo-active]="menuOpen" [attr.aria-label]="menuOpen ? 'Close menu' : 'Open menu'" [attr.aria-expanded]="menuOpen">
<span></span>
<span></span>
<span></span>
</button>
</div>
</div>
</header>
} @else {
<!-- DEXAR VERSION - Redesigned 2026 --> <!-- DEXAR VERSION - Redesigned 2026 -->
<header class="dexar-header"> <header class="dexar-header">
<div class="dexar-header-container"> <div class="dexar-header-container">
@@ -171,4 +114,3 @@
</div> </div>
</div> </div>
</div> </div>
}

View File

@@ -39,10 +39,6 @@ export class HeaderComponent {
return this.uiRuntime.logoUrl(); return this.uiRuntime.logoUrl();
} }
get isMarketplaceNovo(): boolean {
return this.uiRuntime.isMarketplaceVariant('novo');
}
get homeUrl(): string { get homeUrl(): string {
return `/${this.langService.currentLanguage()}`; return `/${this.langService.currentLanguage()}`;
} }

View File

@@ -1,4 +1,4 @@
<div class="carousel-container" [class.novo-theme]="isMarketplaceNovo"> <div class="carousel-container">
@if (loading()) { @if (loading()) {
<div class="carousel-loading"> <div class="carousel-loading">
<div class="spinner"></div> <div class="spinner"></div>

View File

@@ -10,7 +10,6 @@ import { getDiscountedPrice, getMainImage, getBadgeClass, getTranslatedField } f
import { LanguageService } from '../../services/language.service'; import { LanguageService } from '../../services/language.service';
import { LangRoutePipe } from '../../pipes/lang-route.pipe'; import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe'; import { TranslatePipe } from '../../i18n/translate.pipe';
import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade';
import { ProductFacade } from '../../facades/platform/product.facade'; import { ProductFacade } from '../../facades/platform/product.facade';
@Component({ @Component({
@@ -23,7 +22,6 @@ import { ProductFacade } from '../../facades/platform/product.facade';
export class ItemsCarouselComponent implements OnInit { export class ItemsCarouselComponent implements OnInit {
products = signal<Item[]>([]); products = signal<Item[]>([]);
loading = signal(true); loading = signal(true);
private readonly uiRuntime = inject(UiRuntimeFacade);
private readonly productFacade = inject(ProductFacade); private readonly productFacade = inject(ProductFacade);
responsiveOptions: { breakpoint: string; numVisible: number; numScroll: number }[] | undefined; responsiveOptions: { breakpoint: string; numVisible: number; numScroll: number }[] | undefined;
@@ -103,7 +101,6 @@ export class ItemsCarouselComponent implements OnInit {
readonly getBadgeClass = getBadgeClass; readonly getBadgeClass = getBadgeClass;
private langService = inject(LanguageService); private langService = inject(LanguageService);
get isMarketplaceNovo(): boolean { return this.uiRuntime.isMarketplaceVariant('novo'); }
itemName(product: Item): string { return getTranslatedField(product, 'name', this.langService.currentLanguage()); } itemName(product: Item): string { return getTranslatedField(product, 'name', this.langService.currentLanguage()); }
addToCart(event: Event, item: Item): void { addToCart(event: Event, item: Item): void {

View File

@@ -1,7 +1,6 @@
import { Injectable, signal } from '@angular/core'; import { Injectable, signal } from '@angular/core';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { ConfigService } from '../../core/config/config.service'; import { ConfigService } from '../../core/config/config.service';
import { environment } from '../../../environments/environment';
interface UiRuntimeState { interface UiRuntimeState {
marketplaceName: string; marketplaceName: string;
@@ -9,18 +8,16 @@ interface UiRuntimeState {
logoUrl: string; logoUrl: string;
contactEmail: string; contactEmail: string;
themeId: string; themeId: string;
marketplaceVariant: string;
} }
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class UiRuntimeFacade { export class UiRuntimeFacade {
private readonly state = signal<UiRuntimeState>({ private readonly state = signal<UiRuntimeState>({
marketplaceName: environment.brandName, marketplaceName: '',
marketplaceDisplayName: environment.brandFullName, marketplaceDisplayName: '',
logoUrl: environment.logo, logoUrl: '',
contactEmail: environment.contactEmail, contactEmail: '',
themeId: environment.theme, themeId: ''
marketplaceVariant: this.resolveMarketplaceVariant(environment.theme)
}); });
constructor(private readonly configService: ConfigService) { constructor(private readonly configService: ConfigService) {
@@ -30,9 +27,8 @@ export class UiRuntimeFacade {
marketplaceName: bootstrap.branding.brandName, marketplaceName: bootstrap.branding.brandName,
marketplaceDisplayName: bootstrap.branding.brandName, marketplaceDisplayName: bootstrap.branding.brandName,
logoUrl: bootstrap.branding.logoUrl, logoUrl: bootstrap.branding.logoUrl,
contactEmail: bootstrap.branding.supportEmail ?? environment.contactEmail, contactEmail: bootstrap.branding.supportEmail ?? '',
themeId: bootstrap.theme.themeId, themeId: bootstrap.theme.themeId
marketplaceVariant: this.resolveMarketplaceVariant(bootstrap.theme.themeId)
}); });
} }
}); });
@@ -57,17 +53,4 @@ export class UiRuntimeFacade {
themeId(): string { themeId(): string {
return this.state().themeId; return this.state().themeId;
} }
marketplaceVariant(): string {
return this.state().marketplaceVariant;
}
isMarketplaceVariant(variant: string): boolean {
return this.marketplaceVariant() === variant;
}
private resolveMarketplaceVariant(themeId: string): string {
void themeId;
return 'default';
}
} }

View File

@@ -1,11 +1,14 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { NgComponentOutlet } from '@angular/common'; import { NgComponentOutlet } from '@angular/common';
import { Router } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { PageRenderModel } from '../../dynamic-renderer/page-renderer/page-renderer.model'; import { PageRenderModel } from '../../dynamic-renderer/page-renderer/page-renderer.model';
import { WidgetRenderNode } from '../../dynamic-renderer/widget-host/widget-host.model'; import { WidgetRenderNode } from '../../dynamic-renderer/widget-host/widget-host.model';
import { ResolvedWidget } from '../../widgets/contracts/widget-component.contract'; import { ResolvedWidget } from '../../widgets/contracts/widget-component.contract';
import { WidgetHostService } from '../../dynamic-renderer/widget-host/widget-host.service'; import { WidgetHostService } from '../../dynamic-renderer/widget-host/widget-host.service';
import { LanguageService } from '../../services/language.service';
import { Category } from '../../core/categories/models/category-domain.model';
@Component({ @Component({
selector: 'app-dynamic-page-layout', selector: 'app-dynamic-page-layout',
@@ -33,7 +36,7 @@ import { WidgetHostService } from '../../dynamic-renderer/widget-host/widget-hos
@for (widget of section.widgets; track widget.id) { @for (widget of section.widgets; track widget.id) {
<div class="dynamic-widget" [attr.data-widget-type]="widget.type"> <div class="dynamic-widget" [attr.data-widget-type]="widget.type">
@if (resolveWidget(widget, section, model.id) | async; as resolved) { @if (resolveWidget(widget, section, model.id) | async; as resolved) {
<ng-container *ngComponentOutlet="$any(resolved).component; inputs: { section: $any(resolved).section, data: $any(resolved).data }"></ng-container> <ng-container *ngComponentOutlet="$any(resolved).component; inputs: { section: $any(resolved).section, data: $any(resolved).data }; outputs: { categorySelected: onCategorySelected.bind(this) }"></ng-container>
} }
</div> </div>
} }
@@ -78,9 +81,17 @@ import { WidgetHostService } from '../../dynamic-renderer/widget-host/widget-hos
export class DynamicPageLayoutComponent { export class DynamicPageLayoutComponent {
@Input() model: PageRenderModel | null = null; @Input() model: PageRenderModel | null = null;
constructor(private readonly widgetHost: WidgetHostService) {} constructor(
private readonly widgetHost: WidgetHostService,
private readonly router: Router,
private readonly languageService: LanguageService
) {}
resolveWidget(widget: WidgetRenderNode, section: PageRenderModel['sections'][number], pageId: string): Observable<ResolvedWidget> { resolveWidget(widget: WidgetRenderNode, section: PageRenderModel['sections'][number], pageId: string): Observable<ResolvedWidget> {
return this.widgetHost.resolveWidget(widget.source, section.source, pageId); return this.widgetHost.resolveWidget(widget.source, section.source, pageId);
} }
onCategorySelected(category: Category): void {
this.router.navigate([`/${this.languageService.currentLanguage()}/catalog`, category.id]);
}
} }

View File

@@ -1,4 +1,4 @@
<div [class]="isMarketplaceNovo ? 'cart-container novo' : 'cart-container dexar'"> <div class="cart-container dexar">
<div class="cart-header"> <div class="cart-header">
<h1>{{ 'cart.title' | translate }}</h1> <h1>{{ 'cart.title' | translate }}</h1>
@if (itemCount() > 0) { @if (itemCount() > 0) {

View File

@@ -15,7 +15,6 @@ import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe'; import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service'; import { TranslateService } from '../../i18n/translate.service';
import { PAYMENT_POLL_INTERVAL_MS, PAYMENT_MAX_CHECKS, PAYMENT_TIMEOUT_CLOSE_MS, LINK_COPIED_DURATION_MS } from '../../config/constants'; import { PAYMENT_POLL_INTERVAL_MS, PAYMENT_MAX_CHECKS, PAYMENT_TIMEOUT_CLOSE_MS, LINK_COPIED_DURATION_MS } from '../../config/constants';
import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade';
type PaymentMethod = 'qr' | 'card'; type PaymentMethod = 'qr' | 'card';
@@ -35,15 +34,10 @@ export class CartComponent implements OnDestroy {
hasDeliveryPrice; hasDeliveryPrice;
allRequiredDeliveriesSelected; allRequiredDeliveriesSelected;
termsAccepted = false; termsAccepted = false;
private readonly uiRuntime = inject(UiRuntimeFacade);
private i18n = inject(TranslateService); private i18n = inject(TranslateService);
private authService = inject(AuthService); private authService = inject(AuthService);
get isMarketplaceNovo(): boolean {
return this.uiRuntime.isMarketplaceVariant('novo');
}
isAuthenticated = this.authService.isAuthenticated; isAuthenticated = this.authService.isAuthenticated;
// Swipe state // Swipe state

View File

@@ -1,319 +1,3 @@
@if (isMarketplaceNovo) {
<!-- novo VERSION - Modern Design -->
<div class="novo-item-container">
@if (loading()) {
<div class="novo-loading">
<div class="novo-spinner"></div>
<p>{{ 'itemDetail.loading' | translate }}</p>
</div>
}
@if (error()) {
<div class="novo-error">
<p>{{ error() }}</p>
<a [routerLink]="'/' | langRoute" class="novo-back-link">{{ 'itemDetail.back' | translate }}</a>
</div>
}
@if (item() && !loading()) {
<div class="novo-item-content">
<div class="novo-gallery">
@if (item()?.photos && item()!.photos!.length > 0) {
<div class="novo-main-photo">
@if (item()!.photos![selectedPhotoIndex()]?.video) {
<video [src]="item()!.photos![selectedPhotoIndex()].url" controls></video>
} @else {
<img [src]="item()!.photos![selectedPhotoIndex()].url" [alt]="item()!.name" />
}
</div>
@if (item()!.photos!.length > 1) {
<div class="novo-thumbnails">
@for (photo of item()!.photos!; track $index) {
<div
class="novo-thumb"
[class.active]="selectedPhotoIndex() === $index"
(click)="selectPhoto($index)">
@if (photo.video) {
<div class="video-indicator"></div>
}
<img [src]="photo.url" [alt]="'Photo ' + ($index + 1)" loading="lazy" />
</div>
}
</div>
}
} @else {
<div class="novo-main-photo novo-no-image">
<svg width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<path d="M21 15l-5-5L5 21"></path>
</svg>
<p>{{ 'itemDetail.noImage' | translate }}</p>
</div>
}
</div>
<div class="novo-info">
<h1 class="novo-title">{{ getItemName() }}</h1>
@if (item()!.badges && item()!.badges!.length > 0) {
<div class="novo-badges">
@for (badge of item()!.badges!; track badge) {
<span class="item-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
}
</div>
}
@if (item()!.tags && item()!.tags!.length > 0) {
<div class="novo-tags">
@for (tag of item()!.tags!; track tag) {
<span class="item-tag">#{{ tag }}</span>
}
</div>
}
<div class="novo-rating">
<span class="stars">{{ getRatingStars(item()!.rating) }}</span>
<span class="value">{{ item()!.rating }}</span>
<span class="reviews">({{ item()!.callbacks?.length || 0 }})</span>
</div>
<div class="novo-price-block">
@if (item()!.discount > 0) {
<div class="price-row">
<span class="old-price">{{ effectivePrice() }} {{ effectiveCurrency() }}</span>
<span class="discount-badge">-{{ item()!.discount }}%</span>
</div>
<div class="current-price">{{ getDiscountedPrice() | number:'1.2-2' }} {{ effectiveCurrency() }}</div>
} @else {
<div class="current-price">{{ effectivePrice() }} {{ effectiveCurrency() }}</div>
}
</div>
<div class="novo-stock">
<span class="stock-label">{{ 'itemDetail.stock' | translate }}</span>
<div class="stock-indicator" [class]="getStockClass()">
<span class="dot"></span>
{{ getStockLabel() }}
</div>
@if (effectiveRemaining() != null) {
<span class="stock-qty">({{ effectiveRemaining() }} шт.)</span>
}
</div>
@if (availableColours().length || availableSizes().length || item()!.colour || (item()!.size && item()!.size!.toLowerCase() !== 'default')) {
<div class="novo-variants">
@if (availableColours().length) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.colour' | translate }}:</span>
@for (c of availableColours(); track c) {
<span class="colour-swatch" [class.active]="selectedColour() === c" [style.background-color]="c" [title]="c" (click)="selectColour(c)"></span>
}
</div>
} @else if (item()!.colour) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.colour' | translate }}:</span>
<span class="colour-swatch active" [style.background-color]="item()!.colour" [title]="item()!.colour"></span>
</div>
}
@if (availableSizes().length) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.size' | translate }}:</span>
@for (s of availableSizes(); track s) {
<span class="variant-chip size-chip" [class.active]="selectedSize() === s" (click)="selectSize(s)">{{ s }}</span>
}
</div>
} @else if (item()!.size && item()!.size!.toLowerCase() !== 'default') {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.size' | translate }}:</span>
<span class="variant-chip size-chip active">{{ item()!.size }}</span>
</div>
}
</div>
}
@if (item()!.attributes && item()!.attributes!.length > 0) {
<div class="novo-attributes">
@for (attr of item()!.attributes!; track attr.key) {
<div class="attribute-row">
<span class="attribute-key">{{ attr.key }}</span>
<span class="attribute-value">{{ attr.value }}</span>
</div>
}
</div>
}
@if (hasDeliveryInfo()) {
<div class="novo-delivery">
<h3 class="novo-delivery-title">{{ 'cart.deliveryLabel' | translate }}</h3>
@if (isDigitalDelivery()) {
<div class="novo-delivery-chip">{{ 'cart.digitalDelivery' | translate }}</div>
} @else {
<div class="novo-delivery-list">
@for (option of deliveryOptions(); track trackByDeliveryOption($index, option)) {
<div class="novo-delivery-option">
<div class="novo-delivery-option-main">
@if (option.deliveryPlace) {
<span>{{ 'cart.deliveryPlace' | translate }}: {{ option.deliveryPlace }}</span>
}
@if (option.deliveryTime) {
<span>{{ 'cart.deliveryTime' | translate }}: {{ option.deliveryTime }}</span>
}
</div>
<span class="novo-delivery-price">{{ option.deliveryPrice | number:'1.2-2' }} {{ getDeliveryCurrency() }}</span>
</div>
}
</div>
}
</div>
}
<button class="novo-add-cart" (click)="addToCart()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="9" cy="21" r="1"></circle>
<circle cx="20" cy="21" r="1"></circle>
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
</svg>
{{ 'itemDetail.addToCart' | translate }}
</button>
<div class="novo-description">
@if (getSimpleDescription()) {
<p class="novo-simple-desc">{{ getSimpleDescription() }}</p>
}
@if (hasDescriptionFields()) {
<h3>{{ 'itemDetail.specifications' | translate }}</h3>
<table class="novo-specs-table">
<tbody>
@for (field of getTranslatedDescriptionFields(); track field.key) {
<tr>
<td class="spec-key">{{ field.key }}</td>
<td class="spec-value">{{ field.value }}</td>
</tr>
}
</tbody>
</table>
} @else {
<h3>{{ 'itemDetail.description' | translate }}</h3>
<div [innerHTML]="getSafeHtml(item()!.description)"></div>
}
</div>
</div>
</div>
<div class="novo-reviews">
<h2>{{ 'itemDetail.reviews' | translate }} ({{ item()!.callbacks?.length || 0 }})</h2>
<!-- novo Review Form -->
<div class="novo-review-form">
<h3>{{ 'itemDetail.yourReview' | translate }}</h3>
<div class="novo-rating-input">
<label>{{ 'itemDetail.rating' | translate }}</label>
<div class="novo-star-selector">
@for (star of [1, 2, 3, 4, 5]; track star) {
<span
class="novo-star"
[class.selected]="newReview.rating >= star"
(click)="setRating(star)">
</span>
}
</div>
</div>
<textarea
[(ngModel)]="newReview.comment"
[placeholder]="'itemDetail.reviewPlaceholder' | translate"
rows="4"
class="novo-textarea">
</textarea>
<div class="novo-form-actions">
<label class="novo-anonymous-toggle">
<input type="checkbox" [(ngModel)]="newReview.anonymous">
<span>{{ 'itemDetail.anonymous' | translate }}</span>
</label>
@if (!newReview.anonymous && getUserDisplayName()) {
<span class="novo-username-preview">{{ getUserDisplayName() }}</span>
}
<button
class="novo-submit-review-btn"
(click)="submitReview()"
[disabled]="!newReview.rating || !newReview.comment.trim() || reviewSubmitStatus() === 'loading'"
[class.submitting]="reviewSubmitStatus() === 'loading'">
@if (reviewSubmitStatus() === 'loading') {
<span class="novo-spinner-small"></span>
{{ 'itemDetail.submitting' | translate }}
} @else {
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
</svg>
{{ 'itemDetail.submit' | translate }}
}
</button>
</div>
@if (reviewSubmitStatus() === 'success') {
<div class="novo-status-message success">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M20 6L9 17l-5-5"/>
</svg>
{{ 'itemDetail.reviewSuccess' | translate }}
</div>
}
@if (reviewSubmitStatus() === 'error') {
<div class="novo-status-message error">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
{{ 'itemDetail.reviewError' | translate }}
</div>
}
</div>
<div class="novo-reviews-list">
@if (item()!.callbacks && item()!.callbacks!.length > 0) {
@for (review of item()!.callbacks!; track review.userID) {
<div class="novo-review-card">
<div class="review-header">
<div class="reviewer-info">
<span class="reviewer-name">{{ review.userID ? review.userID : ('itemDetail.defaultUser' | translate) }}</span>
@if (review.timestamp) {
<span class="review-date">{{ formatDate(review.timestamp) }}</span>
}
</div>
<span class="review-stars">{{ getRatingStars(review.rating || 0) }}</span>
</div>
<p class="review-text">{{ review.content }}</p>
</div>
}
} @else {
<p class="novo-no-reviews">{{ 'itemDetail.noReviews' | translate }}</p>
}
</div>
</div>
@if (relatedProducts().length > 0) {
<section class="related-products" aria-labelledby="novo-related-products-title">
<h2 id="novo-related-products-title">{{ 'itemDetail.relatedProducts' | translate }}</h2>
<div class="related-products-grid">
@for (related of relatedProducts(); track related.itemID) {
<app-product-card
[item]="related"
[title]="relatedItemName(related)"
[addToCartLabel]="'itemDetail.addToCart' | translate"
appearance="compact"
(addToCart)="addRelatedToCart($event.itemID, $event.event)"
/>
}
</div>
</section>
}
}
</div>
} @else {
<!-- DEXAR VERSION - Redesigned 2026 --> <!-- DEXAR VERSION - Redesigned 2026 -->
<div class="dx-item-container"> <div class="dx-item-container">
@if (loading()) { @if (loading()) {
@@ -342,7 +26,7 @@
[class.active]="selectedPhotoIndex() === $index" [class.active]="selectedPhotoIndex() === $index"
(click)="selectPhoto($index)"> (click)="selectPhoto($index)">
@if (photo.video) { @if (photo.video) {
<div class="dx-video-badge"></div> <div class="dx-video-badge">?</div>
} }
<img [src]="photo.url" [alt]="('itemDetail.photo' | translate) + ' ' + ($index + 1)" loading="lazy" decoding="async" /> <img [src]="photo.url" [alt]="('itemDetail.photo' | translate) + ' ' + ($index + 1)" loading="lazy" decoding="async" />
</div> </div>
@@ -420,7 +104,7 @@
{{ getStockLabel() }} {{ getStockLabel() }}
</span> </span>
@if (effectiveRemaining() != null) { @if (effectiveRemaining() != null) {
<span class="dx-stock-qty">({{ effectiveRemaining() }} шт.)</span> <span class="dx-stock-qty">({{ effectiveRemaining() }} øò.)</span>
} }
</div> </div>
@@ -541,7 +225,7 @@
class="dx-star-pick" class="dx-star-pick"
[class.selected]="newReview.rating >= star" [class.selected]="newReview.rating >= star"
(click)="setRating(star)"> (click)="setRating(star)">
?
</span> </span>
} }
</div> </div>
@@ -629,11 +313,11 @@
@for (question of item()!.questions!; track $index) { @for (question of item()!.questions!; track $index) {
<div class="dx-qa-card"> <div class="dx-qa-card">
<div class="dx-question"> <div class="dx-question">
<span class="dx-qa-label q">В</span> <span class="dx-qa-label q">Â</span>
<span>{{ question.question }}</span> <span>{{ question.question }}</span>
</div> </div>
<div class="dx-answer"> <div class="dx-answer">
<span class="dx-qa-label a">О</span> <span class="dx-qa-label a">Î</span>
<span>{{ question.answer }}</span> <span>{{ question.answer }}</span>
</div> </div>
<div class="dx-qa-votes"> <div class="dx-qa-votes">
@@ -670,4 +354,3 @@
} }
} }
</div> </div>
}

View File

@@ -12,7 +12,6 @@ import { getStockStatus, getBadgeClass, getTranslatedField } from '../../utils/i
import { LangRoutePipe } from '../../pipes/lang-route.pipe'; import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe'; import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service'; import { TranslateService } from '../../i18n/translate.service';
import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade';
import { ProductFacade } from '../../facades/platform/product.facade'; import { ProductFacade } from '../../facades/platform/product.facade';
import { ProductCardComponent } from '../../components/product-card/product-card.component'; import { ProductCardComponent } from '../../components/product-card/product-card.component';
@@ -29,11 +28,6 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
relatedProducts = signal<Item[]>([]); relatedProducts = signal<Item[]>([]);
loading = signal(true); loading = signal(true);
error = signal<string | null>(null); error = signal<string | null>(null);
private readonly uiRuntime = inject(UiRuntimeFacade);
get isMarketplaceNovo(): boolean {
return this.uiRuntime.isMarketplaceVariant('novo');
}
// Variant selection // Variant selection
selectedColour = signal<string | null>(null); selectedColour = signal<string | null>(null);

View File

@@ -179,7 +179,9 @@ export class ApiService {
private resolveImageUrl(url: string): string { private resolveImageUrl(url: string): string {
if (!url) return ''; if (!url) return '';
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/')) return url; if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/')) return url;
const origin = `https://${environment.domain}`; const origin = typeof window !== 'undefined'
? window.location.origin
: '';
if (url.startsWith('./')) return `${origin}/${url.slice(2)}`; if (url.startsWith('./')) return `${origin}/${url.slice(2)}`;
return `${origin}/${url}`; return `${origin}/${url}`;
} }

View File

@@ -2,7 +2,6 @@ import { Injectable, signal, computed, effect, Injector } from '@angular/core';
import { DeliveryOption, CartItem } from '../models'; import { DeliveryOption, CartItem } from '../models';
import { getDiscountedPrice } from '../utils/item.utils'; import { getDiscountedPrice } from '../utils/item.utils';
import { normalizeDeliveryOption, normalizeOptionalNumber } from '../utils/normalization.utils'; import { normalizeDeliveryOption, normalizeOptionalNumber } from '../utils/normalization.utils';
import { environment } from '../../environments/environment';
import type { } from '../types/telegram.types'; import type { } from '../types/telegram.types';
type CartVariant = { colour?: string; size?: string; price?: number; currency?: string }; type CartVariant = { colour?: string; size?: string; price?: number; currency?: string };
@@ -11,7 +10,7 @@ type CartVariant = { colour?: string; size?: string; price?: number; currency?:
providedIn: 'root' providedIn: 'root'
}) })
export class CartService { export class CartService {
private readonly STORAGE_KEY = `${environment.brandName.toLowerCase().replace(/\s+/g, '_')}_cart`; private readonly STORAGE_KEY = 'marketplace_cart';
private cartItems = signal<CartItem[]>([]); private cartItems = signal<CartItem[]>([]);
private isTelegram = typeof window !== 'undefined' && !!window.Telegram?.WebApp; private isTelegram = typeof window !== 'undefined' && !!window.Telegram?.WebApp;
private addingItems = new Set<number>(); private addingItems = new Set<number>();

View File

@@ -1,6 +1,5 @@
import { Injectable, signal } from '@angular/core'; import { Injectable, signal } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { environment } from '../../environments/environment';
export interface Language { export interface Language {
code: string; code: string;
@@ -20,8 +19,7 @@ export interface Currency {
providedIn: 'root' providedIn: 'root'
}) })
export class LanguageService { export class LanguageService {
// @ts-ignore private currentLanguageSignal = signal<string>('ru');
private currentLanguageSignal = signal<string>(environment.defaultLanguage || 'ru');
private currentCurrencySignal = signal<string>('RUB'); private currentCurrencySignal = signal<string>('RUB');
languages: Language[] = [ languages: Language[] = [

View File

@@ -1,8 +1,8 @@
import { Injectable, inject, DOCUMENT } from '@angular/core'; import { Injectable, inject, DOCUMENT } from '@angular/core';
import { Meta, Title } from '@angular/platform-browser'; import { Meta, Title } from '@angular/platform-browser';
import { environment } from '../../environments/environment';
import { Item } from '../models'; import { Item } from '../models';
import { getDiscountedPrice, getMainImage } from '../utils/item.utils'; import { getDiscountedPrice, getMainImage } from '../utils/item.utils';
import { UiRuntimeFacade } from '../facades/runtime/ui-runtime.facade';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@@ -11,9 +11,15 @@ export class SeoService {
private meta = inject(Meta); private meta = inject(Meta);
private title = inject(Title); private title = inject(Title);
private doc = inject(DOCUMENT); private doc = inject(DOCUMENT);
private readonly uiRuntime = inject(UiRuntimeFacade);
private readonly siteUrl = `https://${environment.domain}`; private get siteUrl(): string {
private readonly siteName = environment.brandFullName; return this.doc?.location?.origin ?? '';
}
private get siteName(): string {
return this.uiRuntime.marketplaceDisplayName() || 'Marketplace';
}
/** /**
* Set Open Graph & Twitter Card meta tags for a product/item page. * Set Open Graph & Twitter Card meta tags for a product/item page.

View File

@@ -1,11 +1,9 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input, inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { Router } from '@angular/router';
import { SectionConfig } from '../../shared/models/config'; import { SectionConfig } from '../../shared/models/config';
import { CatalogCategoryGridComponent } from '../../features/website/catalog/components/category-grid/category-grid.component'; import { CatalogCategoryGridComponent } from '../../features/website/catalog/components/category-grid/category-grid.component';
import { CategoriesWidgetData } from '../contracts/widget-data.contract'; import { CategoriesWidgetData } from '../contracts/widget-data.contract';
import { Category } from '../../core/categories/models/category-domain.model'; import { Category } from '../../core/categories/models/category-domain.model';
import { LanguageService } from '../../services/language.service';
@Component({ @Component({
selector: 'app-categories-widget', selector: 'app-categories-widget',
@@ -38,13 +36,11 @@ import { LanguageService } from '../../services/language.service';
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class CategoriesWidgetComponent { export class CategoriesWidgetComponent {
private readonly router = inject(Router);
private readonly languageService = inject(LanguageService);
@Input() section: SectionConfig | null = null; @Input() section: SectionConfig | null = null;
@Input() data: CategoriesWidgetData | null = null; @Input() data: CategoriesWidgetData | null = null;
@Output() categorySelected = new EventEmitter<Category>();
onCategorySelected(category: Category): void { onCategorySelected(category: Category): void {
this.router.navigate([`/${this.languageService.currentLanguage()}/catalog`, category.id]); this.categorySelected.emit(category);
} }
} }