cleanup: remove tenant variant logic and enforce config-driven UI
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -1,123 +1,41 @@
|
|||||||
<!-- 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">
|
<app-logo />
|
||||||
<div class="novo-footer-top">
|
<p>{{ 'footer.description' | translate }}</p>
|
||||||
<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>
|
||||||
|
|
||||||
<div class="novo-footer-bottom">
|
@for (group of footerGroups(); track group.id) {
|
||||||
<div class="novo-copyright">
|
<div class="app-footer__group">
|
||||||
<p>© {{ currentYear }} {{ brandName }}</p>
|
@if (group.title) {
|
||||||
|
<h4>{{ group.title }}</h4>
|
||||||
|
}
|
||||||
|
<ul>
|
||||||
|
@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-payment-icons">
|
}
|
||||||
<img src="/assets/images/mir-logo.svg" alt="МИР" loading="lazy" width="40" height="28" />
|
|
||||||
|
<div class="app-footer__group">
|
||||||
|
<h4>{{ 'footer.payment' | translate }}</h4>
|
||||||
|
<div class="app-footer__payments">
|
||||||
|
<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="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" />
|
<img src="/assets/images/mastercard-logo.svg" alt="Mastercard" loading="lazy" width="40" height="28" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 />
|
|
||||||
</div>
|
|
||||||
<p class="lavero-footer-desc">{{ 'footer.description' | translate }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@for (group of footerGroups(); track group.id) {
|
<div class="app-footer__bottom">
|
||||||
<div class="lavero-footer-col">
|
<p>© {{ currentYear }} {{ brandName }}. {{ 'footer.allRightsReserved' | translate }}</p>
|
||||||
@if (group.title) {
|
@if (contactEmail) {
|
||||||
<h4>{{ group.title }}</h4>
|
<a [href]="'mailto:' + contactEmail">{{ contactEmail }}</a>
|
||||||
}
|
}
|
||||||
<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>
|
</div>
|
||||||
</footer>
|
</div>
|
||||||
} @else {
|
</footer>
|
||||||
<!-- 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) {
|
|
||||||
<h4>{{ group.title }}</h4>
|
|
||||||
}
|
|
||||||
<ul>
|
|
||||||
@for (item of group.items; track item.id) {
|
|
||||||
<li>
|
|
||||||
<a [routerLink]="item.route | langRoute">{{ item.label | translate }}</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="dexar-footer-col">
|
|
||||||
<h4>{{ 'footer.payment' | translate }}</h4>
|
|
||||||
<div class="dexar-payment-logos">
|
|
||||||
<img src="/assets/images/mir-logo.svg" alt="МИР" loading="lazy" width="48" height="32" />
|
|
||||||
<img src="/assets/images/visa-logo.svg" alt="Visa" loading="lazy" width="48" height="32" />
|
|
||||||
<img src="/assets/images/mastercard-logo.svg" alt="Mastercard" loading="lazy" width="48" height="32" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dexar-footer-bottom">
|
|
||||||
<p>© {{ currentYear }} {{ brandName }}. {{ 'footer.allRightsReserved' | translate }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,560 +1,108 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer {
|
||||||
|
background: var(--text-primary, #1e3c38);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer__container {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== LAVERO FOOTER STYLES ==========
|
.app-footer__top {
|
||||||
.lavero-footer {
|
|
||||||
background: linear-gradient(135deg, #1a1208 0%, #2d1a00 100%);
|
|
||||||
color: white;
|
|
||||||
margin-top: auto;
|
|
||||||
padding: 3rem 0 0;
|
|
||||||
border-top: 3px solid var(--primary-color, #FD7300);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lavero-footer-container {
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lavero-footer-top {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2fr 1fr 1fr 1fr;
|
grid-template-columns: 2fr repeat(3, minmax(160px, 1fr));
|
||||||
gap: 3rem;
|
gap: 1.5rem;
|
||||||
padding-bottom: 3rem;
|
padding-bottom: 1.5rem;
|
||||||
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%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lavero-footer-col {
|
|
||||||
h4 {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
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;
|
|
||||||
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 {
|
|
||||||
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.dexar-payment-logos {
|
a {
|
||||||
display: flex;
|
color: rgba(255, 255, 255, 0.8);
|
||||||
gap: 10px;
|
text-decoration: none;
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 28px;
|
|
||||||
max-height: 28px;
|
|
||||||
width: auto;
|
|
||||||
max-width: 60px;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
object-fit: contain;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.85;
|
color: #fff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dexar-footer-bottom {
|
.app-footer__payments {
|
||||||
padding: 20px 0 0;
|
display: flex;
|
||||||
text-align: center;
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer__bottom {
|
||||||
|
display: flex;
|
||||||
|
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;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.app-footer__top {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dexar-footer-top {
|
.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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
|
||||||
|
|||||||
@@ -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()}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,14 +34,9 @@ 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;
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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[] = [
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user