feat(platform): sprint 11.5 standardization
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
This commit is contained in:
@@ -58,24 +58,25 @@
|
||||
background: rgba(37, 99, 235, 0.12);
|
||||
}
|
||||
|
||||
:host-context(.cart-container.novo) .delivery-selector {
|
||||
:host-context(.cart-container.alt) .delivery-selector {
|
||||
border-color: #d1fae5;
|
||||
background: #f9fffc;
|
||||
}
|
||||
|
||||
:host-context(.cart-container.novo) .delivery-label {
|
||||
:host-context(.cart-container.alt) .delivery-label {
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
:host-context(.cart-container.novo) .delivery-control select {
|
||||
:host-context(.cart-container.alt) .delivery-control select {
|
||||
border-color: #bbf7d0;
|
||||
}
|
||||
|
||||
:host-context(.cart-container.novo) .delivery-control select:focus {
|
||||
:host-context(.cart-container.alt) .delivery-control select:focus {
|
||||
border-color: #10b981;
|
||||
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.12);
|
||||
}
|
||||
|
||||
:host-context(.cart-container.novo) .delivery-meta {
|
||||
:host-context(.cart-container.alt) .delivery-meta {
|
||||
color: #4b5563;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<!-- DEXAR VERSION - Redesigned 2026 -->
|
||||
<header class="dexar-header">
|
||||
<div class="dexar-header-container">
|
||||
<!-- platform VERSION - Redesigned 2026 -->
|
||||
<header class="platform-header">
|
||||
<div class="platform-header-container">
|
||||
<!-- Logo -->
|
||||
<a [attr.href]="homeUrl" class="dexar-logo" (click)="navigateHome($event)">
|
||||
<a [attr.href]="homeUrl" class="platform-logo" (click)="navigateHome($event)">
|
||||
<app-logo />
|
||||
</a>
|
||||
|
||||
<!-- Navigation Buttons (desktop) -->
|
||||
<nav class="dexar-nav">
|
||||
<div class="dexar-nav-group">
|
||||
<a [routerLink]="'/' | langRoute" routerLinkActive="dexar-active" [routerLinkActiveOptions]="{exact: true}"
|
||||
(click)="closeMenu()" class="dexar-nav-btn dexar-nav-btn-left">
|
||||
<nav class="platform-nav">
|
||||
<div class="platform-nav-group">
|
||||
<a [routerLink]="'/' | langRoute" routerLinkActive="platform-active" [routerLinkActiveOptions]="{exact: true}"
|
||||
(click)="closeMenu()" class="platform-nav-btn platform-nav-btn-left">
|
||||
{{ 'header.home' | translate }}
|
||||
</a>
|
||||
<!-- TODO(CMS): Render backend-configured header content links here. -->
|
||||
@@ -18,18 +18,18 @@
|
||||
</nav>
|
||||
|
||||
<!-- Search Box (desktop) -->
|
||||
<div class="dexar-search-wrapper">
|
||||
<div class="dexar-search-box">
|
||||
<svg class="dexar-search-icon" width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<div class="platform-search-wrapper">
|
||||
<div class="platform-search-box">
|
||||
<svg class="platform-search-icon" width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12Z" fill="#576463" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.2929 18.2929C18.6834 17.9024 19.3166 17.9024 19.7071 18.2929L25.7071 24.2929C26.0976 24.6834 26.0976 25.3166 25.7071 25.7071C25.3166 26.0976 24.6834 26.0976 24.2929 25.7071L18.2929 19.7071C17.9024 19.3166 17.9024 18.6834 18.2929 18.2929Z" fill="#576463" />
|
||||
</svg>
|
||||
<input type="text" [placeholder]="'header.searchPlaceholder' | translate" class="dexar-search-input" (click)="navigateToSearch()" readonly />
|
||||
<input type="text" [placeholder]="'header.searchPlaceholder' | translate" class="platform-search-input" (click)="navigateToSearch()" readonly />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Icon (mobile only) -->
|
||||
<button class="dexar-search-mobile" (click)="navigateToSearch()" [attr.aria-label]="'header.search' | translate">
|
||||
<button class="platform-search-mobile" (click)="navigateToSearch()" [attr.aria-label]="'header.search' | translate">
|
||||
<svg width="22" height="22" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12Z" fill="#1e3c38" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.2929 18.2929C18.6834 17.9024 19.3166 17.9024 19.7071 18.2929L25.7071 24.2929C26.0976 24.6834 26.0976 25.3166 25.7071 25.7071C25.3166 26.0976 24.6834 26.0976 24.2929 25.7071L18.2929 19.7071C17.9024 19.3166 17.9024 18.6834 18.2929 18.2929Z" fill="#1e3c38" />
|
||||
@@ -37,54 +37,54 @@
|
||||
</button>
|
||||
|
||||
<!-- Right Actions -->
|
||||
<div class="dexar-actions">
|
||||
<div class="platform-actions">
|
||||
@if (userExperienceConfig.wishlist.enabled) {
|
||||
<button type="button" class="dexar-ux-btn" (click)="navigateToWishlist()" [attr.aria-label]="'header.wishlist' | translate">
|
||||
<span class="dexar-ux-icon">♥</span>
|
||||
<button type="button" class="platform-ux-btn" (click)="navigateToWishlist()" [attr.aria-label]="'header.wishlist' | translate">
|
||||
<span class="platform-ux-icon">♥</span>
|
||||
@if (userExperienceConfig.wishlist.headerBadgeEnabled && wishlistCount() > 0) {
|
||||
<span class="dexar-ux-badge">{{ wishlistCount() }}</span>
|
||||
<span class="platform-ux-badge">{{ wishlistCount() }}</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (userExperienceConfig.compare.enabled) {
|
||||
<button type="button" class="dexar-ux-btn" (click)="navigateToCompare()" [attr.aria-label]="'header.compare' | translate">
|
||||
<span class="dexar-ux-icon">⇄</span>
|
||||
<button type="button" class="platform-ux-btn" (click)="navigateToCompare()" [attr.aria-label]="'header.compare' | translate">
|
||||
<span class="platform-ux-icon">⇄</span>
|
||||
@if (compareCount() > 0) {
|
||||
<span class="dexar-ux-badge">{{ compareCount() }}</span>
|
||||
<span class="platform-ux-badge">{{ compareCount() }}</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- Cart Button -->
|
||||
<a [routerLink]="'/cart' | langRoute" routerLinkActive="dexar-cart-active" class="dexar-cart-btn" (click)="closeMenu()">
|
||||
<span class="dexar-cart-icon">
|
||||
<a [routerLink]="'/cart' | langRoute" routerLinkActive="platform-cart-active" class="platform-cart-btn" (click)="closeMenu()">
|
||||
<span class="platform-cart-icon">
|
||||
<svg width="32" height="24" viewBox="0 0 48 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0.5H36C42.3513 0.5 47.5 5.64873 47.5 12V20C47.5 26.3513 42.3513 31.5 36 31.5H12C5.64873 31.5 0.5 26.3513 0.5 20V12C0.5 5.64873 5.64873 0.5 12 0.5Z" fill="transparent" />
|
||||
<path d="M12 0.5H36C42.3513 0.5 47.5 5.64873 47.5 12V20C47.5 26.3513 42.3513 31.5 36 31.5H12C5.64873 31.5 0.5 26.3513 0.5 20V12C0.5 5.64873 5.64873 0.5 12 0.5Z" stroke="#677B78" />
|
||||
<path d="M10 3.9C10 3.40294 10.4029 3 10.9 3H13.6C14.013 3 14.373 3.28107 14.4731 3.68172L15.2027 6.6H36.1C36.3677 6.6 36.6216 6.7192 36.7925 6.92523C36.9635 7.13125 37.0339 7.40271 36.9846 7.66586L34.2846 22.0659C34.2048 22.4915 33.8331 22.8 33.4 22.8H31.6H19H17.2C16.7669 22.8 16.3952 22.4915 16.3154 22.0659L13.6204 7.69224L12.8973 4.8H10.9C10.4029 4.8 10 4.39706 10 3.9ZM15.5844 8.4L17.9469 21H32.6531L35.0156 8.4H15.5844ZM19 22.8C17.0118 22.8 15.4 24.4118 15.4 26.4C15.4 28.3882 17.0118 30 19 30C20.9882 30 22.6 28.3882 22.6 26.4C22.6 24.4118 20.9882 22.8 19 22.8ZM31.6 22.8C29.6118 22.8 28 24.4118 28 26.4C28 28.3882 29.6118 30 31.6 30C33.5882 30 35.2 28.3882 35.2 26.4C35.2 24.4118 33.5882 22.8 31.6 22.8ZM19 24.6C19.9941 24.6 20.8 25.4059 20.8 26.4C20.8 27.3941 19.9941 28.2 19 28.2C18.0059 28.2 17.2 27.3941 17.2 26.4C17.2 25.4059 18.0059 24.6 19 24.6ZM31.6 24.6C32.5941 24.6 33.4 25.4059 33.4 26.4C33.4 27.3941 32.5941 28.2 31.6 28.2C30.6059 28.2 29.8 27.3941 29.8 26.4C29.8 25.4059 30.6059 24.6 31.6 24.6Z" fill="#1E3C38" />
|
||||
</svg>
|
||||
@if (cartItemCount() > 0) {
|
||||
<span class="dexar-cart-badge">{{ cartItemCount() }}</span>
|
||||
<span class="platform-cart-badge">{{ cartItemCount() }}</span>
|
||||
}
|
||||
</span>
|
||||
@if (cartItemCount() > 0) {
|
||||
<span class="dexar-cart-total">{{ formatCartTotal(cartTotal()) }}</span>
|
||||
<span class="platform-cart-total">{{ formatCartTotal(cartTotal()) }}</span>
|
||||
}
|
||||
</a>
|
||||
|
||||
<!-- Region Selector (desktop only) -->
|
||||
<div class="dexar-region-selector dexar-lang-desktop">
|
||||
<div class="platform-region-selector platform-lang-desktop">
|
||||
<app-region-selector />
|
||||
</div>
|
||||
|
||||
<!-- Language Selector (desktop only) -->
|
||||
<div class="dexar-lang-selector dexar-lang-desktop">
|
||||
<div class="platform-lang-selector platform-lang-desktop">
|
||||
<app-language-selector />
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Toggle -->
|
||||
<button class="dexar-menu-toggle" (click)="toggleMenu()" [class.active]="menuOpen" [attr.aria-label]="menuOpen ? 'Close menu' : 'Open menu'" [attr.aria-expanded]="menuOpen">
|
||||
<button class="platform-menu-toggle" (click)="toggleMenu()" [class.active]="menuOpen" [attr.aria-label]="menuOpen ? ('header.closeMenu' | translate) : ('header.openMenu' | translate)" [attr.aria-expanded]="menuOpen">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
@@ -95,40 +95,42 @@
|
||||
|
||||
<!-- Mobile Menu Backdrop (outside header to blur page content) -->
|
||||
@if (menuOpen) {
|
||||
<div class="dexar-menu-backdrop" (click)="closeMenu()"></div>
|
||||
<div class="platform-menu-backdrop" (click)="closeMenu()"></div>
|
||||
}
|
||||
|
||||
<!-- Mobile Menu Panel (outside header for correct stacking) -->
|
||||
<div class="dexar-mobile-menu" [class.dexar-mobile-menu-open]="menuOpen">
|
||||
<a [routerLink]="'/' | langRoute" routerLinkActive="dexar-mobile-active" [routerLinkActiveOptions]="{exact: true}"
|
||||
(click)="closeMenu()" class="dexar-mobile-item">
|
||||
<div class="platform-mobile-menu" [class.platform-mobile-menu-open]="menuOpen">
|
||||
<a [routerLink]="'/' | langRoute" routerLinkActive="platform-mobile-active" [routerLinkActiveOptions]="{exact: true}"
|
||||
(click)="closeMenu()" class="platform-mobile-item">
|
||||
<svg width="24" height="24" viewBox="0 0 31 31" fill="none">
|
||||
<path d="M16.185 2.22124C15.8067 1.84292 15.1933 1.84292 14.815 2.22124L3.18999 13.8462C3.00831 14.0279 2.90625 14.2743 2.90625 14.5312V28.0938C2.90625 28.6288 3.33997 29.0625 3.875 29.0625H12.5938C13.1288 29.0625 13.5625 28.6288 13.5625 28.0938V20.3438H17.4375V28.0938C17.4375 28.6288 17.8712 29.0625 18.4062 29.0625H27.125C27.66 29.0625 28.0938 28.6288 28.0938 28.0938V14.5312C28.0938 14.2743 27.9917 14.0279 27.81 13.8462L25.1875 11.2237V4.84375C25.1875 4.30872 24.7538 3.875 24.2188 3.875H22.2812C21.7462 3.875 21.3125 4.30872 21.3125 4.84375V7.34873L16.185 2.22124ZM4.84375 27.125V14.9325L15.5 4.27627L26.1562 14.9325V27.125H19.375V19.375C19.375 18.84 18.9413 18.4062 18.4062 18.4062H12.5938C12.0587 18.4062 11.625 18.84 11.625 19.375V27.125H4.84375Z" fill="#497671" />
|
||||
</svg>
|
||||
<span>{{ 'header.home' | translate }}</span>
|
||||
<svg class="dexar-mobile-chevron" width="8" height="14" viewBox="0 0 8 14" fill="none">
|
||||
<svg class="platform-mobile-chevron" width="8" height="14" viewBox="0 0 8 14" fill="none">
|
||||
<path d="M1 1L7 7L1 13" stroke="#697777" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a (click)="navigateToCatalog()" class="dexar-mobile-item" style="cursor: pointer;">
|
||||
<a (click)="navigateToCatalog()" class="platform-mobile-item" style="cursor: pointer;">
|
||||
<svg width="24" height="24" viewBox="0 0 31 31" fill="none">
|
||||
<path d="M1.9375 4.84375C1.9375 3.23867 3.23867 1.9375 4.84375 1.9375L10.6562 1.9375C12.2613 1.9375 13.5625 3.23867 13.5625 4.84375V10.6562C13.5625 12.2613 12.2613 13.5625 10.6562 13.5625H4.84375C3.23867 13.5625 1.9375 12.2613 1.9375 10.6562L1.9375 4.84375ZM4.84375 3.875C4.30872 3.875 3.875 4.30872 3.875 4.84375V10.6562C3.875 11.1913 4.30872 11.625 4.84375 11.625H10.6562C11.1913 11.625 11.625 11.1913 11.625 10.6562V4.84375C11.625 4.30872 11.1913 3.875 10.6562 3.875H4.84375ZM17.4375 4.84375C17.4375 3.23867 18.7387 1.9375 20.3438 1.9375L26.1562 1.9375C27.7613 1.9375 29.0625 3.23867 29.0625 4.84375V10.6562C29.0625 12.2613 27.7613 13.5625 26.1562 13.5625H20.3438C18.7387 13.5625 17.4375 12.2613 17.4375 10.6562V4.84375ZM20.3438 3.875C19.8087 3.875 19.375 4.30872 19.375 4.84375V10.6562C19.375 11.1913 19.8087 11.625 20.3438 11.625H26.1562C26.6913 11.625 27.125 11.1913 27.125 10.6562V4.84375C27.125 4.30872 26.6913 3.875 26.1562 3.875H20.3438ZM1.9375 20.3438C1.9375 18.7387 3.23867 17.4375 4.84375 17.4375H10.6562C12.2613 17.4375 13.5625 18.7387 13.5625 20.3438V26.1562C13.5625 27.7613 12.2613 29.0625 10.6562 29.0625H4.84375C3.23867 29.0625 1.9375 27.7613 1.9375 26.1562L1.9375 20.3438ZM4.84375 19.375C4.30872 19.375 3.875 19.8087 3.875 20.3438V26.1562C3.875 26.6913 4.30872 27.125 4.84375 27.125H10.6562C11.1913 27.125 11.625 26.6913 11.625 26.1562V20.3438C11.625 19.8087 11.1913 19.375 10.6562 19.375H4.84375ZM17.4375 20.3438C17.4375 18.7387 18.7387 17.4375 20.3438 17.4375H26.1562C27.7613 17.4375 29.0625 18.7387 29.0625 20.3438V26.1562C29.0625 27.7613 27.7613 29.0625 26.1562 29.0625H20.3438C18.7387 29.0625 17.4375 27.7613 17.4375 26.1562V20.3438ZM20.3438 19.375C19.8087 19.375 19.375 19.8087 19.375 20.3438V26.1562C19.375 26.6913 19.8087 27.125 20.3438 27.125H26.1562C26.6913 27.125 27.125 26.6913 27.125 26.1562V20.3438C27.125 19.8087 26.6913 19.375 26.1562 19.375H20.3438Z" fill="#497671" />
|
||||
</svg>
|
||||
<span>{{ 'header.catalog' | translate }}</span>
|
||||
<svg class="dexar-mobile-chevron" width="8" height="14" viewBox="0 0 8 14" fill="none">
|
||||
<svg class="platform-mobile-chevron" width="8" height="14" viewBox="0 0 8 14" fill="none">
|
||||
<path d="M1 1L7 7L1 13" stroke="#697777" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- TODO(CMS): Render backend-configured mobile content links here. -->
|
||||
|
||||
<div class="dexar-mobile-controls">
|
||||
<div class="dexar-mobile-lang">
|
||||
<div class="platform-mobile-controls">
|
||||
<div class="platform-mobile-lang">
|
||||
<app-region-selector />
|
||||
</div>
|
||||
<div class="dexar-mobile-lang">
|
||||
<div class="platform-mobile-lang">
|
||||
<app-language-selector />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.header {
|
||||
.header {
|
||||
background: white;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
position: sticky;
|
||||
@@ -237,8 +237,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ========== novo HEADER STYLES ==========
|
||||
.novo-header {
|
||||
// ========== alt HEADER STYLES ==========
|
||||
.alt-header {
|
||||
background: white;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
position: sticky;
|
||||
@@ -248,7 +248,7 @@
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.novo-header-container {
|
||||
.alt-header-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 2rem;
|
||||
@@ -257,39 +257,39 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.novo-left {
|
||||
.alt-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.novo-logo {
|
||||
.alt-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
text-decoration: none;
|
||||
|
||||
.novo-brand {
|
||||
.alt-brand {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
&:hover .novo-brand {
|
||||
&:hover .alt-brand {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.novo-nav {
|
||||
.alt-nav {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.novo-nav-links {
|
||||
.alt-nav-links {
|
||||
display: flex;
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
||||
.novo-link {
|
||||
.alt-link {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
@@ -310,7 +310,7 @@
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.novo-active {
|
||||
&.alt-active {
|
||||
color: var(--primary-color);
|
||||
|
||||
&::after {
|
||||
@@ -320,7 +320,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-right {
|
||||
.alt-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -328,11 +328,11 @@
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.novo-mobile-controls {
|
||||
.alt-mobile-controls {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.novo-cart {
|
||||
.alt-cart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -344,13 +344,13 @@
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover,
|
||||
&.novo-cart-active {
|
||||
&.alt-cart-active {
|
||||
color: var(--primary-color);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.novo-cart-icon {
|
||||
.alt-cart-icon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -362,7 +362,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-cart-badge {
|
||||
.alt-cart-badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
@@ -380,14 +380,14 @@
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.novo-cart-total {
|
||||
.alt-cart-total {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.novo-menu-toggle {
|
||||
.alt-menu-toggle {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
@@ -404,7 +404,7 @@
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
&.novo-active {
|
||||
&.alt-active {
|
||||
span:nth-child(1) {
|
||||
transform: rotate(45deg) translate(6px, 6px);
|
||||
}
|
||||
@@ -418,7 +418,7 @@
|
||||
}
|
||||
|
||||
@media (max-width: 968px) {
|
||||
.novo-nav {
|
||||
.alt-nav {
|
||||
position: fixed;
|
||||
top: 65px;
|
||||
left: 0;
|
||||
@@ -430,19 +430,19 @@
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s;
|
||||
|
||||
&.novo-nav-open {
|
||||
&.alt-nav-open {
|
||||
max-height: calc(100dvh - 65px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.novo-nav-links {
|
||||
.alt-nav-links {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.novo-link {
|
||||
.alt-link {
|
||||
padding: 1rem 2rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
text-align: center;
|
||||
@@ -452,23 +452,23 @@
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.novo-active {
|
||||
&.alt-active {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.novo-right {
|
||||
.alt-right {
|
||||
flex: 0 0 auto;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.novo-right > app-region-selector,
|
||||
.novo-right > app-language-selector {
|
||||
.alt-right > app-region-selector,
|
||||
.alt-right > app-language-selector {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.novo-mobile-controls {
|
||||
.alt-mobile-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -477,23 +477,23 @@
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.novo-menu-toggle {
|
||||
.alt-menu-toggle {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.novo-header-container {
|
||||
.alt-header-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.novo-logo .novo-brand {
|
||||
.alt-logo .alt-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== DEXAR REDESIGN 2026 STYLES ==========
|
||||
.dexar-header {
|
||||
// ========== platform REDESIGN 2026 STYLES ==========
|
||||
.platform-header {
|
||||
background: rgba(117, 121, 124, 0.1);
|
||||
padding: 8px 0;
|
||||
position: sticky;
|
||||
@@ -503,7 +503,7 @@
|
||||
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.dexar-header-container {
|
||||
.platform-header-container {
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
padding: 0 40px;
|
||||
@@ -513,7 +513,7 @@
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.dexar-logo {
|
||||
.platform-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
@@ -527,17 +527,17 @@
|
||||
}
|
||||
|
||||
// Navigation Buttons Group
|
||||
.dexar-nav {
|
||||
.platform-nav {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dexar-nav-group {
|
||||
.platform-nav-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dexar-nav-btn {
|
||||
.platform-nav-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -559,36 +559,36 @@
|
||||
color: #1e3c38;
|
||||
}
|
||||
|
||||
&.dexar-active {
|
||||
&.platform-active {
|
||||
background: #497671;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.dexar-nav-btn-left {
|
||||
.platform-nav-btn-left {
|
||||
border-radius: 10px 0 0 10px;
|
||||
padding: 6px 32px;
|
||||
}
|
||||
|
||||
.dexar-nav-btn-middle {
|
||||
.platform-nav-btn-middle {
|
||||
padding: 6px 40px;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.dexar-nav-btn-right {
|
||||
.platform-nav-btn-right {
|
||||
border-radius: 0 10px 10px 0;
|
||||
padding: 6px 28px;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
// Search Box
|
||||
.dexar-search-wrapper {
|
||||
.platform-search-wrapper {
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.dexar-search-box {
|
||||
.platform-search-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
@@ -602,13 +602,13 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dexar-search-icon {
|
||||
.platform-search-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dexar-search-input {
|
||||
.platform-search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
@@ -625,14 +625,14 @@
|
||||
}
|
||||
|
||||
// Actions
|
||||
.dexar-actions {
|
||||
.platform-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dexar-ux-btn {
|
||||
.platform-ux-btn {
|
||||
position: relative;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
@@ -647,19 +647,19 @@
|
||||
transition: transform 0.18s ease, border-color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.dexar-ux-btn:hover {
|
||||
.platform-ux-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: #497671;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.dexar-ux-icon {
|
||||
.platform-ux-icon {
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dexar-ux-badge {
|
||||
.platform-ux-badge {
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
top: -6px;
|
||||
@@ -676,7 +676,7 @@
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.dexar-cart-btn {
|
||||
.platform-cart-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -696,12 +696,12 @@
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&.dexar-cart-active {
|
||||
&.platform-cart-active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dexar-cart-icon {
|
||||
.platform-cart-icon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -710,7 +710,7 @@
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.dexar-cart-badge {
|
||||
.platform-cart-badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -10px;
|
||||
@@ -729,7 +729,7 @@
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.dexar-cart-total {
|
||||
.platform-cart-total {
|
||||
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 10px;
|
||||
@@ -738,23 +738,23 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dexar-lang-selector {
|
||||
.platform-lang-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Mobile Menu Backdrop
|
||||
.dexar-menu-backdrop {
|
||||
.platform-menu-backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Mobile Menu Panel
|
||||
.dexar-mobile-menu {
|
||||
.platform-mobile-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Mobile Menu Items
|
||||
.dexar-mobile-item {
|
||||
.platform-mobile-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
@@ -784,7 +784,7 @@
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&.dexar-mobile-active {
|
||||
&.platform-mobile-active {
|
||||
background: #497671;
|
||||
color: #ffffff;
|
||||
border-color: #497671;
|
||||
@@ -793,7 +793,7 @@
|
||||
fill: #ffffff;
|
||||
}
|
||||
|
||||
.dexar-mobile-chevron path {
|
||||
.platform-mobile-chevron path {
|
||||
stroke: #ffffff;
|
||||
}
|
||||
}
|
||||
@@ -808,19 +808,19 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dexar-mobile-chevron {
|
||||
.platform-mobile-chevron {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.dexar-mobile-lang {
|
||||
.platform-mobile-lang {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dexar-mobile-controls {
|
||||
.platform-mobile-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -829,7 +829,7 @@
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.dexar-menu-toggle {
|
||||
.platform-menu-toggle {
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
display: none;
|
||||
@@ -864,7 +864,7 @@
|
||||
}
|
||||
|
||||
// Mobile Search Icon
|
||||
.dexar-search-mobile {
|
||||
.platform-search-mobile {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -882,101 +882,101 @@
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 1200px) {
|
||||
.dexar-header-container {
|
||||
.platform-header-container {
|
||||
padding: 0 32px;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.dexar-nav-btn {
|
||||
.platform-nav-btn {
|
||||
font-size: 18px;
|
||||
padding: 8px 32px !important;
|
||||
}
|
||||
|
||||
.dexar-nav-btn-left {
|
||||
.platform-nav-btn-left {
|
||||
padding: 8px 32px !important;
|
||||
}
|
||||
|
||||
.dexar-nav-btn-middle {
|
||||
.platform-nav-btn-middle {
|
||||
padding: 8px 40px !important;
|
||||
}
|
||||
|
||||
.dexar-nav-btn-right {
|
||||
.platform-nav-btn-right {
|
||||
padding: 8px 28px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.dexar-header-container {
|
||||
.platform-header-container {
|
||||
padding: 0 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.dexar-logo ::ng-deep .logo-img {
|
||||
.platform-logo ::ng-deep .logo-img {
|
||||
width: 120px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.dexar-nav-btn {
|
||||
.platform-nav-btn {
|
||||
font-size: 16px;
|
||||
padding: 8px 24px !important;
|
||||
}
|
||||
|
||||
.dexar-nav-btn-left {
|
||||
.platform-nav-btn-left {
|
||||
padding: 8px 24px !important;
|
||||
}
|
||||
|
||||
.dexar-nav-btn-middle {
|
||||
.platform-nav-btn-middle {
|
||||
padding: 8px 28px !important;
|
||||
}
|
||||
|
||||
.dexar-nav-btn-right {
|
||||
.platform-nav-btn-right {
|
||||
padding: 8px 20px !important;
|
||||
}
|
||||
|
||||
.dexar-search-box {
|
||||
.platform-search-box {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.dexar-search-input {
|
||||
.platform-search-input {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dexar-header-container {
|
||||
.platform-header-container {
|
||||
gap: 12px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
// Hide desktop nav
|
||||
.dexar-nav {
|
||||
.platform-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Hide desktop language selector
|
||||
.dexar-lang-desktop {
|
||||
.platform-lang-desktop {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.dexar-search-wrapper {
|
||||
.platform-search-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dexar-search-mobile {
|
||||
.platform-search-mobile {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.dexar-menu-toggle {
|
||||
.platform-menu-toggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.dexar-logo ::ng-deep .logo-img {
|
||||
.platform-logo ::ng-deep .logo-img {
|
||||
width: 100px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
// Mobile Menu Backdrop — full-screen, blurred, blocks interaction
|
||||
.dexar-menu-backdrop {
|
||||
// Mobile Menu Backdrop — full-screen, blurred, blocks interaction
|
||||
.platform-menu-backdrop {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -987,11 +987,11 @@
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
z-index: 998;
|
||||
animation: dexar-fade-in 0.25s ease;
|
||||
animation: platform-fade-in 0.25s ease;
|
||||
}
|
||||
|
||||
// Mobile Menu Panel
|
||||
.dexar-mobile-menu {
|
||||
.platform-mobile-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -1016,7 +1016,7 @@
|
||||
opacity: 0;
|
||||
transition: max-height 0.35s ease, opacity 0.25s ease, padding 0.35s ease;
|
||||
|
||||
&.dexar-mobile-menu-open {
|
||||
&.platform-mobile-menu-open {
|
||||
max-height: calc(100dvh - 84px);
|
||||
padding: 28px 20px 32px;
|
||||
opacity: 1;
|
||||
@@ -1024,28 +1024,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dexar-mobile-item {
|
||||
.platform-mobile-item {
|
||||
width: 395px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.dexar-mobile-controls {
|
||||
.platform-mobile-controls {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.dexar-mobile-menu {
|
||||
.platform-mobile-menu {
|
||||
width: 100vw;
|
||||
border-radius: 0 0 13px 13px;
|
||||
}
|
||||
|
||||
.dexar-mobile-item {
|
||||
.platform-mobile-item {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dexar-fade-in {
|
||||
@keyframes platform-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -55,15 +55,15 @@ export class HeaderComponent {
|
||||
toggleMenu(): void {
|
||||
this.menuOpen = !this.menuOpen;
|
||||
if (this.menuOpen) {
|
||||
this.renderer.addClass(this.document.body, 'dexar-menu-open');
|
||||
this.renderer.addClass(this.document.body, 'platform-menu-open');
|
||||
} else {
|
||||
this.renderer.removeClass(this.document.body, 'dexar-menu-open');
|
||||
this.renderer.removeClass(this.document.body, 'platform-menu-open');
|
||||
}
|
||||
}
|
||||
|
||||
closeMenu(): void {
|
||||
this.menuOpen = false;
|
||||
this.renderer.removeClass(this.document.body, 'dexar-menu-open');
|
||||
this.renderer.removeClass(this.document.body, 'platform-menu-open');
|
||||
}
|
||||
|
||||
navigateHome(event?: Event): void {
|
||||
@@ -143,3 +143,5 @@ export class HeaderComponent {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -307,8 +307,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Novo theme styles
|
||||
.novo-theme {
|
||||
// alt theme styles
|
||||
.alt-theme {
|
||||
::ng-deep {
|
||||
.p-carousel {
|
||||
.p-carousel-prev,
|
||||
@@ -371,3 +371,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
}
|
||||
|
||||
// Light theme adjustments
|
||||
:host-context(.novo-header),
|
||||
:host-context(.alt-header),
|
||||
:host-context(.header) {
|
||||
.language-button {
|
||||
border-color: rgba(0, 0, 0, 0.2);
|
||||
@@ -152,9 +152,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Dexar header specific styles
|
||||
:host-context(.dexar-header),
|
||||
:host-context(.dexar-mobile-menu) {
|
||||
// platform header specific styles
|
||||
:host-context(.platform-header),
|
||||
:host-context(.platform-mobile-menu) {
|
||||
.language-selector {
|
||||
width: 56px;
|
||||
height: 28px;
|
||||
@@ -246,8 +246,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.dexar-header),
|
||||
:host-context(.dexar-mobile-menu) {
|
||||
:host-context(.platform-header),
|
||||
:host-context(.platform-mobile-menu) {
|
||||
.language-selector {
|
||||
width: 56px;
|
||||
height: 28px;
|
||||
@@ -261,9 +261,9 @@
|
||||
|
||||
// Inside mobile menu: use inline dropdown instead of absolute
|
||||
// to avoid being clipped by overflow: hidden on the menu panel
|
||||
:host-context(.dexar-mobile-menu),
|
||||
:host-context(.dexar-mobile-lang),
|
||||
:host-context(.novo-mobile-controls) {
|
||||
:host-context(.platform-mobile-menu),
|
||||
:host-context(.platform-mobile-lang),
|
||||
:host-context(.alt-mobile-controls) {
|
||||
.language-selector {
|
||||
width: auto;
|
||||
height: auto;
|
||||
@@ -412,8 +412,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Light / Novo / Dexar theme adjustments for currency
|
||||
:host-context(.novo-header),
|
||||
// Light / alt / platform theme adjustments for currency
|
||||
:host-context(.alt-header),
|
||||
:host-context(.header) {
|
||||
.currency-button {
|
||||
border-color: rgba(0, 0, 0, 0.2);
|
||||
@@ -438,8 +438,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.dexar-header),
|
||||
:host-context(.dexar-mobile-menu) {
|
||||
:host-context(.platform-header),
|
||||
:host-context(.platform-mobile-menu) {
|
||||
.currency-button {
|
||||
padding: 4px 8px;
|
||||
gap: 3px;
|
||||
@@ -465,3 +465,5 @@
|
||||
&.active { background: rgba(73, 118, 113, 0.1); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<button class="add-to-cart-btn" type="button" (click)="onAddToCart($event)" [attr.aria-label]="addToCartLabel + ': ' + (title || item.name)">
|
||||
{{ addToCartLabel }}
|
||||
<button class="add-to-cart-btn" type="button" (click)="onAddToCart($event)" [attr.aria-label]="(addToCartLabel || ('catalog.addToCart' | translate)) + ': ' + (title || item.name)">
|
||||
{{ addToCartLabel || ('catalog.addToCart' | translate) }}
|
||||
</button>
|
||||
</article>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DecimalPipe } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { Product } from '../../core/products/models/product-domain.model';
|
||||
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
import { getBadgeClass, getDiscountedPrice, getMainImage } from '../../utils/item.utils';
|
||||
|
||||
export type ProductCardAppearance = 'standard' | 'compact';
|
||||
@@ -10,7 +11,7 @@ export type ProductCardAppearance = 'standard' | 'compact';
|
||||
@Component({
|
||||
selector: 'app-product-card',
|
||||
standalone: true,
|
||||
imports: [DecimalPipe, RouterLink, LangRoutePipe],
|
||||
imports: [DecimalPipe, RouterLink, LangRoutePipe, TranslatePipe],
|
||||
templateUrl: './product-card.component.html',
|
||||
styleUrls: ['./product-card.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
@@ -19,7 +20,7 @@ export class ProductCardComponent {
|
||||
@Input({ required: true }) item!: Product;
|
||||
@Input() title = '';
|
||||
@Input() description = '';
|
||||
@Input() addToCartLabel = 'Add to cart';
|
||||
@Input() addToCartLabel = '';
|
||||
@Input() appearance: ProductCardAppearance = 'standard';
|
||||
@Input() showDescription = false;
|
||||
@Input() showStock = true;
|
||||
|
||||
27
src/app/core/search/models/search-state.model.ts
Normal file
27
src/app/core/search/models/search-state.model.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { CatalogLayoutMode } from '../../products/models/catalog-experience.model';
|
||||
import { ProductSort } from '../../products/models/product-domain.model';
|
||||
import { SearchFilterState } from './search.model';
|
||||
|
||||
export interface SearchState {
|
||||
text: string;
|
||||
sort: ProductSort | 'discount';
|
||||
layout: CatalogLayoutMode;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
filters: SearchFilterState;
|
||||
}
|
||||
|
||||
export function createInitialSearchState(pageSize = 24): SearchState {
|
||||
return {
|
||||
text: '',
|
||||
sort: 'relevance',
|
||||
layout: 'grid',
|
||||
page: 1,
|
||||
pageSize,
|
||||
filters: {
|
||||
values: {},
|
||||
ranges: {},
|
||||
toggles: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
76
src/app/core/search/models/search.model.ts
Normal file
76
src/app/core/search/models/search.model.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Product, ProductSort } from '../../products/models/product-domain.model';
|
||||
|
||||
export type SearchFilterType =
|
||||
| 'checkbox'
|
||||
| 'radio'
|
||||
| 'toggle'
|
||||
| 'range'
|
||||
| 'slider'
|
||||
| 'color'
|
||||
| 'size'
|
||||
| 'rating'
|
||||
| 'availability';
|
||||
|
||||
export interface SearchQuery {
|
||||
text: string;
|
||||
categoryIds: number[];
|
||||
subcategoryIds: string[];
|
||||
filters: SearchFilterState;
|
||||
sort: ProductSort | 'discount';
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface SearchResult<TItem = Product> {
|
||||
query: SearchQuery;
|
||||
items: TItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface FilterOption {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
count?: number;
|
||||
colorHex?: string;
|
||||
availability?: 'in-stock' | 'low-stock' | 'out-of-stock';
|
||||
rating?: number;
|
||||
}
|
||||
|
||||
export interface FilterGroup {
|
||||
id: string;
|
||||
label: string;
|
||||
type: SearchFilterType;
|
||||
options: FilterOption[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface SortOption {
|
||||
id: ProductSort | 'discount';
|
||||
label: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface SearchSuggestion {
|
||||
id: string;
|
||||
text: string;
|
||||
kind: 'live' | 'recent' | 'popular';
|
||||
}
|
||||
|
||||
export interface SearchHistory {
|
||||
items: string[];
|
||||
recent: string[];
|
||||
popular: string[];
|
||||
}
|
||||
|
||||
export interface SearchFilterState {
|
||||
values: Record<string, string[]>;
|
||||
ranges: Record<string, { min?: number; max?: number }>;
|
||||
toggles: Record<string, boolean>;
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SearchHistory } from '../models/search.model';
|
||||
|
||||
const STORAGE_KEY = 'marketplace.catalog.search.history';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CatalogSearchHistoryService {
|
||||
export class SearchHistoryService {
|
||||
private readonly maxSize = 10;
|
||||
|
||||
getHistory(): string[] {
|
||||
@@ -26,25 +27,43 @@ export class CatalogSearchHistoryService {
|
||||
}
|
||||
}
|
||||
|
||||
push(term: string): string[] {
|
||||
getSnapshot(popular: string[]): SearchHistory {
|
||||
const items = this.getHistory();
|
||||
return {
|
||||
items,
|
||||
recent: items.slice(0, 5),
|
||||
popular,
|
||||
};
|
||||
}
|
||||
|
||||
push(term: string, popular: string[]): SearchHistory {
|
||||
const normalized = term.trim();
|
||||
if (!normalized.length) {
|
||||
return this.getHistory();
|
||||
return this.getSnapshot(popular);
|
||||
}
|
||||
|
||||
const history = [normalized, ...this.getHistory().filter(entry => entry.toLowerCase() !== normalized.toLowerCase())]
|
||||
.slice(0, this.maxSize);
|
||||
|
||||
this.save(history);
|
||||
return history;
|
||||
|
||||
return {
|
||||
items: history,
|
||||
recent: history.slice(0, 5),
|
||||
popular,
|
||||
};
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
clear(popular: string[]): SearchHistory {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
return {
|
||||
items: [],
|
||||
recent: [],
|
||||
popular,
|
||||
};
|
||||
}
|
||||
|
||||
private save(history: string[]): void {
|
||||
425
src/app/facades/platform/search.facade.ts
Normal file
425
src/app/facades/platform/search.facade.ts
Normal file
@@ -0,0 +1,425 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { ParamMap, Params } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Product, ProductListResult } from '../../core/products/models/product-domain.model';
|
||||
import { ProductFacade } from './product.facade';
|
||||
import { TranslateService } from '../../i18n/translate.service';
|
||||
import { SearchHistoryService } from '../../core/search/services/search-history.service';
|
||||
import { SearchFilterState, SearchHistory, SearchQuery, SearchResult, SearchSuggestion, SortOption, FilterGroup } from '../../core/search/models/search.model';
|
||||
import { SearchState } from '../../core/search/models/search-state.model';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SearchFacade {
|
||||
private readonly productFacade = inject(ProductFacade);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly historyService = inject(SearchHistoryService);
|
||||
|
||||
private readonly metadataMemo = new Map<string, FilterGroup[]>();
|
||||
|
||||
readonly popularSearches: SearchSuggestion[] = [
|
||||
{ id: 'popular-smartphones', text: 'Smartphones', kind: 'popular' },
|
||||
{ id: 'popular-sneakers', text: 'Sneakers', kind: 'popular' },
|
||||
{ id: 'popular-headphones', text: 'Headphones', kind: 'popular' },
|
||||
{ id: 'popular-laptops', text: 'Laptops', kind: 'popular' },
|
||||
];
|
||||
|
||||
getSearchHistory(): SearchHistory {
|
||||
return this.historyService.getSnapshot(this.popularSearches.map(entry => entry.text));
|
||||
}
|
||||
|
||||
pushSearchHistory(term: string): SearchHistory {
|
||||
return this.historyService.push(term, this.popularSearches.map(entry => entry.text));
|
||||
}
|
||||
|
||||
clearSearchHistory(): SearchHistory {
|
||||
return this.historyService.clear(this.popularSearches.map(entry => entry.text));
|
||||
}
|
||||
|
||||
loadCatalog(query: SearchQuery): Observable<ProductListResult> {
|
||||
return this.productFacade.loadCatalog({
|
||||
keyword: query.text,
|
||||
categoryIDs: query.categoryIds,
|
||||
subcategoryIDs: query.subcategoryIds,
|
||||
minPrice: query.filters.ranges['price']?.min,
|
||||
maxPrice: query.filters.ranges['price']?.max,
|
||||
discountOnly: query.filters.toggles['discount'],
|
||||
newOnly: query.filters.toggles['new'],
|
||||
sort: query.sort,
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
});
|
||||
}
|
||||
|
||||
createSortOptions(availableSorts: string[]): SortOption[] {
|
||||
const labels: Record<string, string> = {
|
||||
relevance: this.translate.t('catalog.sortRelevance'),
|
||||
latest: this.translate.t('catalog.sortLatest'),
|
||||
price_asc: this.translate.t('catalog.sortPriceAsc'),
|
||||
price_desc: this.translate.t('catalog.sortPriceDesc'),
|
||||
rating: this.translate.t('catalog.sortRating'),
|
||||
popular: this.translate.t('catalog.sortPopular'),
|
||||
discount: this.translate.t('catalog.sortDiscount'),
|
||||
};
|
||||
|
||||
return availableSorts.map(id => ({
|
||||
id: id as SortOption['id'],
|
||||
label: labels[id] ?? id,
|
||||
enabled: true,
|
||||
}));
|
||||
}
|
||||
|
||||
buildFilterGroups(products: Product[], enabledFilters: string[]): FilterGroup[] {
|
||||
const enabled = new Set(enabledFilters);
|
||||
const key = `${enabledFilters.join('|')}::${products.map(item => item.itemID).join(',')}`;
|
||||
const cached = this.metadataMemo.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const priceValues = products.map(product => product.price).filter(value => Number.isFinite(value));
|
||||
const brands = this.collectUnique(products.flatMap(product => product.tags ?? []).filter(tag => !tag.startsWith('new') && !tag.startsWith('sale')));
|
||||
const colors = this.collectUnique(products.flatMap(product => [product.colour, ...(product.itemDetails?.map(detail => detail.colour || detail.color) ?? [])]).filter(Boolean) as string[]);
|
||||
const sizes = this.collectUnique(products.flatMap(product => [product.size, ...(product.itemDetails?.map(detail => detail.size) ?? [])]).filter(Boolean) as string[]);
|
||||
const categories = this.collectUnique(products.map(product => String(product.categoryID)).filter(Boolean));
|
||||
const subcategories = this.collectUnique(products.map(product => product.subcategoryId ?? '').filter(Boolean));
|
||||
const attributes = this.collectUnique(products.flatMap(product => (product.descriptionFields ?? []).map(field => field.key)));
|
||||
|
||||
const groups: FilterGroup[] = [
|
||||
{
|
||||
id: 'price',
|
||||
label: this.translate.t('catalog.filterPrice'),
|
||||
type: 'range',
|
||||
min: priceValues.length ? Math.min(...priceValues) : 0,
|
||||
max: priceValues.length ? Math.max(...priceValues) : 0,
|
||||
step: 1,
|
||||
options: [],
|
||||
enabled: enabled.has('price'),
|
||||
},
|
||||
{
|
||||
id: 'availability',
|
||||
label: this.translate.t('catalog.filterAvailability'),
|
||||
type: 'availability',
|
||||
options: [
|
||||
{ id: 'in-stock', label: this.translate.t('catalog.filterInStock'), value: 'in-stock', availability: 'in-stock' },
|
||||
{ id: 'low-stock', label: this.translate.t('catalog.filterLowStock'), value: 'low-stock', availability: 'low-stock' },
|
||||
{ id: 'out-of-stock', label: this.translate.t('catalog.filterOutOfStock'), value: 'out-of-stock', availability: 'out-of-stock' },
|
||||
],
|
||||
enabled: enabled.has('availability'),
|
||||
},
|
||||
{
|
||||
id: 'rating',
|
||||
label: this.translate.t('catalog.filterRating'),
|
||||
type: 'rating',
|
||||
options: [5, 4, 3, 2, 1].map(value => ({
|
||||
id: `rating-${value}`,
|
||||
label: this.translate.t('catalog.filterStars', { count: value }),
|
||||
value: String(value),
|
||||
rating: value,
|
||||
})),
|
||||
enabled: enabled.has('rating'),
|
||||
},
|
||||
{
|
||||
id: 'brand',
|
||||
label: this.translate.t('catalog.filterBrand'),
|
||||
type: 'checkbox',
|
||||
options: brands.map(value => ({ id: `brand-${value}`, label: value, value })),
|
||||
enabled: enabled.has('brand'),
|
||||
},
|
||||
{
|
||||
id: 'category',
|
||||
label: this.translate.t('catalog.filterCategory'),
|
||||
type: 'radio',
|
||||
options: categories.map(value => ({ id: `cat-${value}`, label: this.translate.t('catalog.filterCategoryValue', { value }), value })),
|
||||
enabled: enabled.has('category'),
|
||||
},
|
||||
{
|
||||
id: 'subcategory',
|
||||
label: this.translate.t('catalog.filterSubcategory'),
|
||||
type: 'checkbox',
|
||||
options: subcategories.map(value => ({ id: `sub-${value}`, label: value, value })),
|
||||
enabled: enabled.has('subcategory'),
|
||||
},
|
||||
{
|
||||
id: 'discount',
|
||||
label: this.translate.t('catalog.filterDiscount'),
|
||||
type: 'toggle',
|
||||
options: [],
|
||||
enabled: enabled.has('discount'),
|
||||
},
|
||||
{
|
||||
id: 'new',
|
||||
label: this.translate.t('catalog.filterNew'),
|
||||
type: 'toggle',
|
||||
options: [],
|
||||
enabled: enabled.has('new'),
|
||||
},
|
||||
{
|
||||
id: 'color',
|
||||
label: this.translate.t('catalog.filterColor'),
|
||||
type: 'color',
|
||||
options: colors.map(value => ({ id: `color-${value}`, label: value, value, colorHex: this.toColorHex(value) })),
|
||||
enabled: enabled.has('color'),
|
||||
},
|
||||
{
|
||||
id: 'size',
|
||||
label: this.translate.t('catalog.filterSize'),
|
||||
type: 'size',
|
||||
options: sizes.map(value => ({ id: `size-${value}`, label: value, value })),
|
||||
enabled: enabled.has('size'),
|
||||
},
|
||||
{
|
||||
id: 'attributes',
|
||||
label: this.translate.t('catalog.filterAttributes'),
|
||||
type: 'checkbox',
|
||||
options: attributes.map(value => ({ id: `attr-${value}`, label: value, value })),
|
||||
enabled: enabled.has('attributes'),
|
||||
},
|
||||
];
|
||||
|
||||
this.metadataMemo.set(key, groups);
|
||||
if (this.metadataMemo.size > 15) {
|
||||
const firstKey = this.metadataMemo.keys().next().value;
|
||||
if (firstKey) {
|
||||
this.metadataMemo.delete(firstKey);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
buildLiveSuggestions(query: string, products: Product[], limit = 6): SearchSuggestion[] {
|
||||
const normalized = query.trim().toLowerCase();
|
||||
if (normalized.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.collectUnique(
|
||||
products
|
||||
.map(product => product.name)
|
||||
.filter(name => name.toLowerCase().includes(normalized))
|
||||
.slice(0, limit)
|
||||
).map((text, index) => ({ id: `live-${index}`, text, kind: 'live' }));
|
||||
}
|
||||
|
||||
applyFilters(products: Product[], filterState: SearchFilterState, searchText: string): Product[] {
|
||||
const query = searchText.trim().toLowerCase();
|
||||
|
||||
return products.filter(product => {
|
||||
if (query.length > 0) {
|
||||
const haystack = `${product.name} ${product.simpleDescription ?? ''}`.toLowerCase();
|
||||
if (!haystack.includes(query)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const priceRange = filterState.ranges['price'];
|
||||
if (priceRange?.min != null && product.price < priceRange.min) {
|
||||
return false;
|
||||
}
|
||||
if (priceRange?.max != null && product.price > priceRange.max) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterState.toggles['discount'] && !(product.discount > 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterState.toggles['new'] && !(product.badges ?? []).some(badge => badge.toLowerCase().includes('new'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedRatings = filterState.values['rating'] ?? [];
|
||||
if (selectedRatings.length > 0 && !selectedRatings.includes(String(Math.floor(product.rating || 0)))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedAvailability = filterState.values['availability'] ?? [];
|
||||
if (selectedAvailability.length > 0) {
|
||||
const availability = product.remainings === 'out'
|
||||
? 'out-of-stock'
|
||||
: product.remainings === 'low'
|
||||
? 'low-stock'
|
||||
: 'in-stock';
|
||||
|
||||
if (!selectedAvailability.includes(availability)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const selectedBrands = filterState.values['brand'] ?? [];
|
||||
if (selectedBrands.length > 0) {
|
||||
const tags = product.tags ?? [];
|
||||
if (!selectedBrands.some(brand => tags.includes(brand))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const selectedCategories = filterState.values['category'] ?? [];
|
||||
if (selectedCategories.length > 0 && !selectedCategories.includes(String(product.categoryID))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedSubcategories = filterState.values['subcategory'] ?? [];
|
||||
if (selectedSubcategories.length > 0 && !selectedSubcategories.includes(product.subcategoryId ?? '')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedColors = filterState.values['color'] ?? [];
|
||||
if (selectedColors.length > 0) {
|
||||
const colors = this.collectUnique([product.colour ?? '', ...(product.itemDetails?.map(detail => detail.colour || detail.color || '') ?? [])]);
|
||||
if (!selectedColors.some(color => colors.includes(color))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const selectedSizes = filterState.values['size'] ?? [];
|
||||
if (selectedSizes.length > 0) {
|
||||
const sizes = this.collectUnique([product.size ?? '', ...(product.itemDetails?.map(detail => detail.size || '') ?? [])]);
|
||||
if (!selectedSizes.some(size => sizes.includes(size))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const selectedAttributes = filterState.values['attributes'] ?? [];
|
||||
if (selectedAttributes.length > 0) {
|
||||
const keys = (product.descriptionFields ?? []).map(field => field.key);
|
||||
if (!selectedAttributes.some(attribute => keys.includes(attribute))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
applySort(products: Product[], sort: SearchState['sort']): Product[] {
|
||||
const sorted = [...products];
|
||||
|
||||
switch (sort) {
|
||||
case 'price_asc':
|
||||
return sorted.sort((a, b) => a.price - b.price);
|
||||
case 'price_desc':
|
||||
return sorted.sort((a, b) => b.price - a.price);
|
||||
case 'rating':
|
||||
return sorted.sort((a, b) => (b.rating || 0) - (a.rating || 0));
|
||||
case 'popular':
|
||||
return sorted.sort((a, b) => (b.callbacks?.length ?? 0) - (a.callbacks?.length ?? 0));
|
||||
case 'discount':
|
||||
return sorted.sort((a, b) => (b.discount || 0) - (a.discount || 0));
|
||||
case 'latest':
|
||||
case 'relevance':
|
||||
default:
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
|
||||
paginateProducts(products: Product[], page: number, pageSize: number): { items: Product[]; skip: number; page: number; pageSize: number } {
|
||||
const safePage = Math.max(1, page);
|
||||
const safePageSize = Math.max(1, pageSize);
|
||||
const skip = (safePage - 1) * safePageSize;
|
||||
|
||||
return {
|
||||
items: products.slice(skip, skip + safePageSize),
|
||||
skip,
|
||||
page: safePage,
|
||||
pageSize: safePageSize,
|
||||
};
|
||||
}
|
||||
|
||||
buildResult(state: SearchState, total: number, items: Product[]): SearchResult<Product> {
|
||||
return {
|
||||
query: {
|
||||
text: state.text,
|
||||
categoryIds: [],
|
||||
subcategoryIds: [],
|
||||
filters: state.filters,
|
||||
sort: state.sort,
|
||||
page: state.page,
|
||||
pageSize: state.pageSize,
|
||||
},
|
||||
items,
|
||||
total,
|
||||
page: state.page,
|
||||
pageSize: state.pageSize,
|
||||
summary: state.text.trim().length > 0
|
||||
? this.translate.t('catalog.searchResultsSummary', { query: state.text.trim(), total })
|
||||
: this.translate.t('catalog.searchProductsFound', { total }),
|
||||
};
|
||||
}
|
||||
|
||||
toQueryParams(state: SearchState): Params {
|
||||
return {
|
||||
q: state.text || null,
|
||||
sort: state.sort || null,
|
||||
page: state.page > 1 ? state.page : null,
|
||||
layout: state.layout !== 'grid' ? state.layout : null,
|
||||
fv: this.serializeObject(state.filters.values),
|
||||
fr: this.serializeObject(state.filters.ranges),
|
||||
ft: this.serializeObject(state.filters.toggles),
|
||||
};
|
||||
}
|
||||
|
||||
fromQueryParams(paramMap: ParamMap): Partial<SearchState> {
|
||||
const values = this.deserializeObject<Record<string, string[]>>(paramMap.get('fv')) ?? {};
|
||||
const ranges = this.deserializeObject<Record<string, { min?: number; max?: number }>>(paramMap.get('fr')) ?? {};
|
||||
const toggles = this.deserializeObject<Record<string, boolean>>(paramMap.get('ft')) ?? {};
|
||||
const page = Number(paramMap.get('page'));
|
||||
|
||||
return {
|
||||
text: (paramMap.get('q') ?? '').trim(),
|
||||
sort: (paramMap.get('sort') as SearchState['sort']) ?? 'relevance',
|
||||
layout: (paramMap.get('layout') as SearchState['layout']) ?? 'grid',
|
||||
page: Number.isFinite(page) && page > 0 ? page : 1,
|
||||
filters: {
|
||||
values,
|
||||
ranges,
|
||||
toggles,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private serializeObject(value: unknown): string | null {
|
||||
if (!value || typeof value !== 'object' || Object.keys(value as object).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return encodeURIComponent(JSON.stringify(value));
|
||||
}
|
||||
|
||||
private deserializeObject<T>(value: string | null): T | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(decodeURIComponent(value)) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private collectUnique(values: string[]): string[] {
|
||||
return [...new Set(values.map(value => value.trim()).filter(Boolean))];
|
||||
}
|
||||
|
||||
private toColorHex(value: string): string {
|
||||
const normalized = value.toLowerCase();
|
||||
const palette: Record<string, string> = {
|
||||
black: '#111111',
|
||||
white: '#ffffff',
|
||||
red: '#ef4444',
|
||||
blue: '#3b82f6',
|
||||
green: '#22c55e',
|
||||
yellow: '#eab308',
|
||||
orange: '#f97316',
|
||||
purple: '#8b5cf6',
|
||||
pink: '#ec4899',
|
||||
gray: '#6b7280',
|
||||
grey: '#6b7280',
|
||||
brown: '#92400e',
|
||||
beige: '#d6c6a8',
|
||||
};
|
||||
|
||||
return palette[normalized] ?? '#94a3b8';
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,21 @@
|
||||
</div>
|
||||
|
||||
@for (filter of definitions; track filter.id) {
|
||||
@if (filter.enabled !== false) {
|
||||
@if (filter.enabled) {
|
||||
<section class="filter-group">
|
||||
<h4>{{ filter.label }}</h4>
|
||||
|
||||
@if (filter.type === 'multi-select') {
|
||||
@if (filter.type === 'checkbox' || filter.type === 'availability') {
|
||||
<div class="options">
|
||||
@for (option of filter.options ?? []; track option.id) {
|
||||
@for (option of filter.options; track option.id) {
|
||||
<label class="option-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="isSelected(filter.id, option.value)"
|
||||
(change)="toggleOption(filter.id, option.value)" />
|
||||
@if (filter.type === 'availability') {
|
||||
<span class="availability-dot" [attr.data-availability]="option.availability ?? option.value" aria-hidden="true"></span>
|
||||
}
|
||||
<span>{{ option.label }}</span>
|
||||
@if (option.count != null) {
|
||||
<small>{{ option.count }}</small>
|
||||
@@ -25,6 +28,21 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (filter.type === 'radio') {
|
||||
<div class="options">
|
||||
@for (option of filter.options; track option.id) {
|
||||
<label class="option-check">
|
||||
<input
|
||||
type="radio"
|
||||
[name]="'radio-' + filter.id"
|
||||
[checked]="isSelected(filter.id, option.value)"
|
||||
(change)="selectSingleOption(filter.id, option.value)" />
|
||||
<span>{{ option.label }}</span>
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (filter.type === 'range') {
|
||||
<div class="range-inputs">
|
||||
<input
|
||||
@@ -40,6 +58,60 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (filter.type === 'slider') {
|
||||
<div class="slider-inputs">
|
||||
<input
|
||||
type="range"
|
||||
[min]="filter.min ?? 0"
|
||||
[max]="filter.max ?? 1000"
|
||||
[step]="filter.step ?? 1"
|
||||
[value]="state.ranges[filter.id]?.max ?? filter.max ?? 1000"
|
||||
(input)="updateSlider(filter.id, $any($event.target).value)" />
|
||||
<span>{{ state.ranges[filter.id]?.max ?? filter.max ?? 0 }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (filter.type === 'color') {
|
||||
<div class="color-options">
|
||||
@for (option of filter.options; track option.id) {
|
||||
<button
|
||||
type="button"
|
||||
class="color-swatch"
|
||||
[class.active]="isSelected(filter.id, option.value)"
|
||||
[style.background]="option.colorHex ?? '#94a3b8'"
|
||||
[attr.aria-label]="option.label"
|
||||
(click)="toggleOption(filter.id, option.value)"></button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (filter.type === 'size') {
|
||||
<div class="size-options">
|
||||
@for (option of filter.options; track option.id) {
|
||||
<button
|
||||
type="button"
|
||||
class="size-chip"
|
||||
[class.active]="isSelected(filter.id, option.value)"
|
||||
(click)="toggleOption(filter.id, option.value)">{{ option.label }}</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (filter.type === 'rating') {
|
||||
<div class="rating-options">
|
||||
@for (option of filter.options; track option.id) {
|
||||
<button
|
||||
type="button"
|
||||
class="rating-chip"
|
||||
[class.active]="isSelected(filter.id, option.value)"
|
||||
(click)="toggleOption(filter.id, option.value)">
|
||||
<span aria-hidden="true">★</span>
|
||||
<span>{{ option.label }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (filter.type === 'toggle') {
|
||||
<label class="option-check">
|
||||
<input
|
||||
|
||||
@@ -37,13 +37,32 @@
|
||||
|
||||
.option-check {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-columns: auto auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.availability-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
.availability-dot[data-availability='in-stock'] {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.availability-dot[data-availability='low-stock'] {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.availability-dot[data-availability='out-of-stock'] {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.range-inputs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -57,6 +76,59 @@
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.slider-inputs {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.slider-inputs input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.slider-inputs span {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.color-options,
|
||||
.size-options,
|
||||
.rating-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-swatch.active {
|
||||
border-color: var(--text-primary);
|
||||
}
|
||||
|
||||
.size-chip,
|
||||
.rating-chip {
|
||||
min-height: 30px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 999px;
|
||||
background: var(--bg-primary);
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.size-chip.active,
|
||||
.rating-chip.active {
|
||||
border-color: var(--primary-color);
|
||||
background: color-mix(in srgb, var(--primary-color) 8%, white);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.catalog-filters {
|
||||
gap: 12px;
|
||||
@@ -65,4 +137,8 @@
|
||||
.range-inputs {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.option-check {
|
||||
grid-template-columns: auto auto 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FilterDefinition } from '../../../../../core/products/models/catalog-experience.model';
|
||||
import { FilterGroup, SearchFilterState } from '../../../../../core/search/models/search.model';
|
||||
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
||||
|
||||
export interface CatalogFilterStateValue {
|
||||
values: Record<string, string[]>;
|
||||
ranges: Record<string, { min?: number; max?: number }>;
|
||||
toggles: Record<string, boolean>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-catalog-filters-panel',
|
||||
standalone: true,
|
||||
@@ -18,10 +12,10 @@ export interface CatalogFilterStateValue {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CatalogFiltersPanelComponent {
|
||||
@Input() definitions: FilterDefinition[] = [];
|
||||
@Input() state: CatalogFilterStateValue = { values: {}, ranges: {}, toggles: {} };
|
||||
@Input() definitions: FilterGroup[] = [];
|
||||
@Input() state: SearchFilterState = { values: {}, ranges: {}, toggles: {} };
|
||||
|
||||
@Output() stateChange = new EventEmitter<CatalogFilterStateValue>();
|
||||
@Output() stateChange = new EventEmitter<SearchFilterState>();
|
||||
|
||||
isSelected(filterId: string, optionValue: string): boolean {
|
||||
return (this.state.values[filterId] ?? []).includes(optionValue);
|
||||
@@ -42,6 +36,16 @@ export class CatalogFiltersPanelComponent {
|
||||
});
|
||||
}
|
||||
|
||||
selectSingleOption(filterId: string, optionValue: string): void {
|
||||
this.stateChange.emit({
|
||||
...this.state,
|
||||
values: {
|
||||
...this.state.values,
|
||||
[filterId]: [optionValue],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateRange(filterId: string, key: 'min' | 'max', rawValue: string): void {
|
||||
const value = rawValue.trim().length ? Number(rawValue) : undefined;
|
||||
const current = this.state.ranges[filterId] ?? {};
|
||||
@@ -58,6 +62,20 @@ export class CatalogFiltersPanelComponent {
|
||||
});
|
||||
}
|
||||
|
||||
updateSlider(filterId: string, value: string): void {
|
||||
const numeric = Number(value);
|
||||
this.stateChange.emit({
|
||||
...this.state,
|
||||
ranges: {
|
||||
...this.state.ranges,
|
||||
[filterId]: {
|
||||
...this.state.ranges[filterId],
|
||||
max: Number.isFinite(numeric) ? numeric : undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateToggle(filterId: string, checked: boolean): void {
|
||||
this.stateChange.emit({
|
||||
...this.state,
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
<input
|
||||
type="search"
|
||||
[ngModel]="query"
|
||||
(ngModelChange)="queryChange.emit($event)"
|
||||
(ngModelChange)="onQueryInput($event)"
|
||||
(keydown)="onKeydown($event)"
|
||||
name="query"
|
||||
[placeholder]="'catalog.searchPlaceholder' | translate"
|
||||
autocomplete="off" />
|
||||
<button type="button" class="search-clear-btn" [disabled]="!query" (click)="clearQuery()" [attr.aria-label]="'catalog.clearSearch' | translate">×</button>
|
||||
<button type="submit" [disabled]="loading">{{ 'catalog.searchSubmit' | translate }}</button>
|
||||
</form>
|
||||
|
||||
@@ -14,8 +16,19 @@
|
||||
<div class="suggestions">
|
||||
<strong>{{ 'catalog.suggestionsTitle' | translate }}</strong>
|
||||
<div class="chip-list">
|
||||
@for (item of suggestions; track $index) {
|
||||
<button type="button" class="chip" (click)="selectSuggestion(item)">{{ item }}</button>
|
||||
@for (item of suggestions; track item.id) {
|
||||
<button type="button" class="chip" [class.active]="$index === activeSuggestionIndex" (click)="selectSuggestion(item.text)">{{ item.text }}</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (popularSearches.length > 0) {
|
||||
<div class="popular">
|
||||
<strong>{{ 'catalog.popularSearchesTitle' | translate }}</strong>
|
||||
<div class="chip-list">
|
||||
@for (item of popularSearches; track $index) {
|
||||
<button type="button" class="chip" (click)="onRecentSelected(item)">{{ item }}</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -26,7 +39,7 @@
|
||||
<strong>{{ 'catalog.recentSearchesTitle' | translate }}</strong>
|
||||
<div class="chip-list">
|
||||
@for (item of recentSearches; track $index) {
|
||||
<button type="button" class="chip" (click)="recentSelected.emit(item)">{{ item }}</button>
|
||||
<button type="button" class="chip" (click)="onRecentSelected(item)">{{ item }}</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,7 +53,7 @@
|
||||
</div>
|
||||
<div class="chip-list">
|
||||
@for (item of searchHistory; track $index) {
|
||||
<button type="button" class="chip" (click)="recentSelected.emit(item)">{{ item }}</button>
|
||||
<button type="button" class="chip" (click)="onRecentSelected(item)">{{ item }}</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
.search-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,17 @@
|
||||
transition: transform 0.2s ease, filter 0.2s ease;
|
||||
}
|
||||
|
||||
.search-clear-btn {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.search-form button:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
filter: brightness(0.96);
|
||||
@@ -65,6 +76,12 @@
|
||||
background: color-mix(in srgb, var(--primary-color) 8%, white);
|
||||
}
|
||||
|
||||
.chip.active {
|
||||
border-color: var(--primary-color);
|
||||
color: var(--text-primary);
|
||||
background: color-mix(in srgb, var(--primary-color) 10%, white);
|
||||
}
|
||||
|
||||
.history-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -87,10 +104,11 @@
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.search-form {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.search-form button {
|
||||
.search-form button[type='submit'] {
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { SearchSuggestion } from '../../../../../core/search/models/search.model';
|
||||
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
||||
|
||||
@Component({
|
||||
@@ -13,8 +14,9 @@ import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
||||
export class CatalogSearchBoxComponent {
|
||||
@Input() query = '';
|
||||
@Input() loading = false;
|
||||
@Input() suggestions: string[] = [];
|
||||
@Input() suggestions: SearchSuggestion[] = [];
|
||||
@Input() recentSearches: string[] = [];
|
||||
@Input() popularSearches: string[] = [];
|
||||
@Input() searchHistory: string[] = [];
|
||||
@Input() noResults = false;
|
||||
|
||||
@@ -24,12 +26,60 @@ export class CatalogSearchBoxComponent {
|
||||
@Output() recentSelected = new EventEmitter<string>();
|
||||
@Output() historyCleared = new EventEmitter<void>();
|
||||
|
||||
activeSuggestionIndex = -1;
|
||||
|
||||
onSubmit(event: Event): void {
|
||||
event.preventDefault();
|
||||
this.searchSubmit.emit(this.query.trim());
|
||||
}
|
||||
|
||||
onQueryInput(value: string): void {
|
||||
this.queryChange.emit(value);
|
||||
this.activeSuggestionIndex = -1;
|
||||
}
|
||||
|
||||
selectSuggestion(value: string): void {
|
||||
this.suggestionSelected.emit(value);
|
||||
this.activeSuggestionIndex = -1;
|
||||
}
|
||||
|
||||
clearQuery(): void {
|
||||
this.queryChange.emit('');
|
||||
this.searchSubmit.emit('');
|
||||
this.activeSuggestionIndex = -1;
|
||||
}
|
||||
|
||||
onKeydown(event: KeyboardEvent): void {
|
||||
if (this.suggestions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
this.activeSuggestionIndex = Math.min(this.suggestions.length - 1, this.activeSuggestionIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
this.activeSuggestionIndex = Math.max(-1, this.activeSuggestionIndex - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && this.activeSuggestionIndex >= 0) {
|
||||
event.preventDefault();
|
||||
const active = this.suggestions[this.activeSuggestionIndex];
|
||||
this.selectSuggestion(active.text);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
this.activeSuggestionIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
onRecentSelected(value: string): void {
|
||||
this.recentSelected.emit(value);
|
||||
this.activeSuggestionIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { SortDefinition } from '../../../../../core/products/models/catalog-experience.model';
|
||||
import { SortOption } from '../../../../../core/search/models/search.model';
|
||||
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
||||
|
||||
@Component({
|
||||
@@ -12,7 +12,7 @@ import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CatalogSortingControlComponent {
|
||||
@Input() options: SortDefinition[] = [];
|
||||
@Input() options: SortOption[] = [];
|
||||
@Input() selected = 'relevance';
|
||||
|
||||
@Output() selectedChange = new EventEmitter<string>();
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
[loading]="loadingProducts()"
|
||||
[suggestions]="searchSuggestions()"
|
||||
[recentSearches]="recentSearches()"
|
||||
[popularSearches]="popularSearches()"
|
||||
[searchHistory]="searchHistory()"
|
||||
[noResults]="noResults()"
|
||||
(queryChange)="onSearchQueryChange($event)"
|
||||
|
||||
@@ -5,11 +5,11 @@ import { A11yModule } from '@angular/cdk/a11y';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Category } from '../../../../core/categories/models/category-domain.model';
|
||||
import { CatalogLayoutMode, FilterDefinition, SearchCriteria, SearchResult, SortDefinition, toSearchResult } from '../../../../core/products/models/catalog-experience.model';
|
||||
import { CatalogLayoutMode } from '../../../../core/products/models/catalog-experience.model';
|
||||
import { Product } from '../../../../core/products/models/product-domain.model';
|
||||
import { ConfigService } from '../../../../core/config/config.service';
|
||||
import { CategoryFacade } from '../../../../facades/platform/category.facade';
|
||||
import { ProductFacade } from '../../../../facades/platform/product.facade';
|
||||
import { SearchFacade } from '../../../../facades/platform/search.facade';
|
||||
import { UserExperienceFacade } from '../../../../facades/platform/user-experience.facade';
|
||||
import { CartService } from '../../../../services';
|
||||
import { LanguageService } from '../../../../services/language.service';
|
||||
@@ -18,15 +18,16 @@ import { LangRoutePipe } from '../../../../pipes/lang-route.pipe';
|
||||
import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||
import { TranslateService } from '../../../../i18n/translate.service';
|
||||
import { DEFAULT_CATALOG_CONFIG, DEFAULT_USER_EXPERIENCE_CONFIG } from '../../../../shared/models/config';
|
||||
import { FilterGroup, SearchFilterState, SearchQuery, SearchResult, SearchSuggestion, SortOption } from '../../../../core/search/models/search.model';
|
||||
import { SearchState } from '../../../../core/search/models/search-state.model';
|
||||
import { CatalogCategoryGridComponent } from '../components/category-grid/category-grid.component';
|
||||
import { CatalogEmptyStateComponent } from '../components/catalog-empty-state/catalog-empty-state.component';
|
||||
import { CatalogFiltersPanelComponent, CatalogFilterStateValue } from '../components/filters-panel/filters-panel.component';
|
||||
import { CatalogFiltersPanelComponent } from '../components/filters-panel/filters-panel.component';
|
||||
import { CatalogLayoutSwitcherComponent } from '../components/layout-switcher/layout-switcher.component';
|
||||
import { CatalogSearchBoxComponent } from '../components/search-box/search-box.component';
|
||||
import { CatalogSearchResultsComponent } from '../components/search-results/search-results.component';
|
||||
import { CatalogSortingControlComponent } from '../components/sorting-control/sorting-control.component';
|
||||
import { CatalogState, createInitialCatalogState } from '../models/catalog-state.model';
|
||||
import { CatalogSearchHistoryService } from '../services/catalog-search-history.service';
|
||||
import { ProductShareService } from '../../user-experience/services/product-share.service';
|
||||
import { UserNotificationService } from '../../user-experience/services/user-notification.service';
|
||||
|
||||
@@ -58,11 +59,10 @@ export class CatalogContainerComponent {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly configService = inject(ConfigService);
|
||||
private readonly categoryFacade = inject(CategoryFacade);
|
||||
private readonly productFacade = inject(ProductFacade);
|
||||
private readonly searchFacade = inject(SearchFacade);
|
||||
private readonly cartService = inject(CartService);
|
||||
private readonly prefetchService = inject(PrefetchService);
|
||||
private readonly languageService = inject(LanguageService);
|
||||
private readonly searchHistoryService = inject(CatalogSearchHistoryService);
|
||||
private readonly uxFacade = inject(UserExperienceFacade);
|
||||
private readonly shareService = inject(ProductShareService);
|
||||
private readonly notifications = inject(UserNotificationService);
|
||||
@@ -78,12 +78,13 @@ export class CatalogContainerComponent {
|
||||
readonly subcategoryChips = signal<Category[]>([]);
|
||||
readonly breadcrumb = signal<Category[]>([]);
|
||||
readonly searchSummary = signal('');
|
||||
readonly searchResult = signal<SearchResult | null>(null);
|
||||
readonly filterDefinitions = signal<FilterDefinition[]>([]);
|
||||
readonly filterState = signal<CatalogFilterStateValue>({ values: {}, ranges: {}, toggles: {} });
|
||||
readonly searchSuggestions = signal<string[]>([]);
|
||||
readonly searchResult = signal<SearchResult<Product> | null>(null);
|
||||
readonly filterDefinitions = signal<FilterGroup[]>([]);
|
||||
readonly filterState = signal<SearchFilterState>({ values: {}, ranges: {}, toggles: {} });
|
||||
readonly searchSuggestions = signal<SearchSuggestion[]>([]);
|
||||
readonly recentSearches = signal<string[]>([]);
|
||||
readonly searchHistory = signal<string[]>(this.catalogConfig().searchHistoryEnabled ? this.searchHistoryService.getHistory() : []);
|
||||
readonly popularSearches = signal<string[]>([]);
|
||||
readonly searchHistory = signal<string[]>([]);
|
||||
readonly savedSearches = this.uxFacade.savedSearches;
|
||||
readonly viewMode = signal<CatalogViewMode>('categories');
|
||||
readonly loading = signal(true);
|
||||
@@ -109,19 +110,7 @@ export class CatalogContainerComponent {
|
||||
readonly favoriteIds = computed(() => this.uxFacade.wishlist().map(item => item.product.itemID));
|
||||
readonly comparedIds = computed(() => this.uxFacade.comparedProducts().map(item => item.product.itemID));
|
||||
|
||||
readonly sortDefinitions = computed<SortDefinition[]>(() => {
|
||||
const labels: Record<string, string> = {
|
||||
relevance: this.translate.t('catalog.sortRelevance'),
|
||||
latest: this.translate.t('catalog.sortLatest'),
|
||||
price_asc: this.translate.t('catalog.sortPriceAsc'),
|
||||
price_desc: this.translate.t('catalog.sortPriceDesc'),
|
||||
rating: this.translate.t('catalog.sortRating'),
|
||||
popular: this.translate.t('catalog.sortPopular'),
|
||||
discount: this.translate.t('catalog.sortDiscount')
|
||||
};
|
||||
|
||||
return this.catalogConfig().availableSorts.map((id: string) => ({ id, label: labels[id] ?? id, enabled: true }));
|
||||
});
|
||||
readonly sortDefinitions = computed<SortOption[]>(() => this.searchFacade.createSortOptions(this.catalogConfig().availableSorts));
|
||||
|
||||
readonly availableLayouts: CatalogLayoutMode[] = ['grid', 'large-grid', 'compact-grid', 'list'];
|
||||
|
||||
@@ -130,30 +119,39 @@ export class CatalogContainerComponent {
|
||||
private dataSubscription?: Subscription;
|
||||
private readonly backendFetchSize = 200;
|
||||
private pendingScrollY: number | null = null;
|
||||
private syncingUrl = false;
|
||||
|
||||
constructor() {
|
||||
this.destroyRef.onDestroy(() => this.dataSubscription?.unsubscribe());
|
||||
this.recentSearches.set(this.searchHistory().slice(0, 5));
|
||||
if (this.catalogConfig().searchHistoryEnabled) {
|
||||
const snapshot = this.searchFacade.getSearchHistory();
|
||||
this.searchHistory.set(snapshot.items);
|
||||
this.recentSearches.set(snapshot.recent);
|
||||
this.popularSearches.set(snapshot.popular);
|
||||
} else {
|
||||
this.popularSearches.set(this.searchFacade.popularSearches.map(item => item.text));
|
||||
}
|
||||
|
||||
combineLatest([this.route.paramMap, this.route.queryParamMap])
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(([params, queryParams]) => {
|
||||
const categoryId = Number(params.get('id')) || null;
|
||||
const searchQuery = (queryParams.get('q') ?? '').trim();
|
||||
|
||||
if (categoryId == null && searchQuery.length === 0 && this.userExperienceConfig().continueBrowsing.enabled && this.restoreContinueBrowsing()) {
|
||||
if (this.syncingUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.enterCategory(categoryId);
|
||||
const categoryId = Number(params.get('id')) || null;
|
||||
const queryPatch = this.searchFacade.fromQueryParams(queryParams);
|
||||
const hasQueryState = this.hasSearchState(queryPatch);
|
||||
|
||||
if (searchQuery.length > 0) {
|
||||
this.submitSearch(searchQuery);
|
||||
if (categoryId == null && !hasQueryState && this.userExperienceConfig().continueBrowsing.enabled && this.restoreContinueBrowsing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.enterCategory(categoryId, queryPatch, hasQueryState);
|
||||
});
|
||||
}
|
||||
|
||||
enterCategory(categoryId: number | null): void {
|
||||
enterCategory(categoryId: number | null, queryPatch?: Partial<SearchState>, forceProductsMode = false): void {
|
||||
this.dataSubscription?.unsubscribe();
|
||||
this.loading.set(true);
|
||||
this.loadingProducts.set(false);
|
||||
@@ -179,7 +177,9 @@ export class CatalogContainerComponent {
|
||||
},
|
||||
});
|
||||
|
||||
if (categoryId == null) {
|
||||
this.applySearchStatePatch(queryPatch);
|
||||
|
||||
if (categoryId == null && !forceProductsMode) {
|
||||
this.dataSubscription = this.categoryFacade.getRootCategories()
|
||||
.subscribe({
|
||||
next: categories => this.renderCategories(null, categories, []),
|
||||
@@ -188,10 +188,18 @@ export class CatalogContainerComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
if (categoryId == null && forceProductsMode) {
|
||||
this.viewMode.set('products');
|
||||
this.loadCatalog();
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedCategoryId = categoryId as number;
|
||||
|
||||
this.dataSubscription = combineLatest([
|
||||
this.categoryFacade.getCategoryById(categoryId),
|
||||
this.categoryFacade.getChildren(categoryId),
|
||||
this.categoryFacade.getBreadcrumb(categoryId),
|
||||
this.categoryFacade.getCategoryById(selectedCategoryId),
|
||||
this.categoryFacade.getChildren(selectedCategoryId),
|
||||
this.categoryFacade.getBreadcrumb(selectedCategoryId),
|
||||
]).subscribe({
|
||||
next: ([category, children, breadcrumb]) => {
|
||||
this.breadcrumb.set(breadcrumb);
|
||||
@@ -203,7 +211,7 @@ export class CatalogContainerComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadProducts(categoryId);
|
||||
this.loadProducts(selectedCategoryId);
|
||||
},
|
||||
error: () => this.setError('catalog.error'),
|
||||
});
|
||||
@@ -235,18 +243,12 @@ export class CatalogContainerComponent {
|
||||
this.state.update(current => ({ ...current, search: query }));
|
||||
this.persistContinueBrowsing();
|
||||
|
||||
const normalized = query.trim().toLowerCase();
|
||||
if (!this.catalogConfig().suggestionsEnabled || normalized.length < 2) {
|
||||
if (!this.catalogConfig().suggestionsEnabled) {
|
||||
this.searchSuggestions.set([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const suggestions = this.rawProducts()
|
||||
.map(product => product.name)
|
||||
.filter(name => name.toLowerCase().includes(normalized))
|
||||
.slice(0, 6);
|
||||
|
||||
this.searchSuggestions.set([...new Set(suggestions)]);
|
||||
this.searchSuggestions.set(this.searchFacade.buildLiveSuggestions(query, this.rawProducts()));
|
||||
}
|
||||
|
||||
submitSearch(query: string): void {
|
||||
@@ -263,10 +265,13 @@ export class CatalogContainerComponent {
|
||||
}));
|
||||
|
||||
if (this.catalogConfig().searchHistoryEnabled && normalized.length > 0) {
|
||||
this.searchHistory.set(this.searchHistoryService.push(normalized));
|
||||
this.recentSearches.set(this.searchHistory().slice(0, 5));
|
||||
const snapshot = this.searchFacade.pushSearchHistory(normalized);
|
||||
this.searchHistory.set(snapshot.items);
|
||||
this.recentSearches.set(snapshot.recent);
|
||||
this.popularSearches.set(snapshot.popular);
|
||||
}
|
||||
|
||||
this.syncUrlFromState();
|
||||
this.persistContinueBrowsing();
|
||||
this.loadCatalog();
|
||||
}
|
||||
@@ -282,9 +287,10 @@ export class CatalogContainerComponent {
|
||||
}
|
||||
|
||||
clearSearchHistory(): void {
|
||||
this.searchHistoryService.clear();
|
||||
this.searchHistory.set([]);
|
||||
this.recentSearches.set([]);
|
||||
const snapshot = this.searchFacade.clearSearchHistory();
|
||||
this.searchHistory.set(snapshot.items);
|
||||
this.recentSearches.set(snapshot.recent);
|
||||
this.popularSearches.set(snapshot.popular);
|
||||
}
|
||||
|
||||
onFavoriteToggled(product: Product): void {
|
||||
@@ -375,9 +381,14 @@ export class CatalogContainerComponent {
|
||||
...current.pagination,
|
||||
page: 1,
|
||||
skip: 0
|
||||
},
|
||||
filters: {
|
||||
...current.filters,
|
||||
categoryIds: target.categoryId == null ? [] : [target.categoryId],
|
||||
}
|
||||
}));
|
||||
this.filterState.set(target.filters);
|
||||
this.syncUrlFromState();
|
||||
this.loadCatalog(target.categoryId);
|
||||
}
|
||||
|
||||
@@ -406,7 +417,7 @@ export class CatalogContainerComponent {
|
||||
}
|
||||
}
|
||||
|
||||
onFilterStateChange(next: CatalogFilterStateValue): void {
|
||||
onFilterStateChange(next: SearchFilterState): void {
|
||||
this.filterState.set(next);
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
@@ -418,12 +429,14 @@ export class CatalogContainerComponent {
|
||||
}));
|
||||
|
||||
this.recomputeResults();
|
||||
this.syncUrlFromState();
|
||||
this.persistContinueBrowsing();
|
||||
}
|
||||
|
||||
resetFilters(): void {
|
||||
this.filterState.set({ values: {}, ranges: {}, toggles: {} });
|
||||
this.recomputeResults();
|
||||
this.syncUrlFromState();
|
||||
this.persistContinueBrowsing();
|
||||
}
|
||||
|
||||
@@ -439,11 +452,13 @@ export class CatalogContainerComponent {
|
||||
}));
|
||||
|
||||
this.recomputeResults();
|
||||
this.syncUrlFromState();
|
||||
this.persistContinueBrowsing();
|
||||
}
|
||||
|
||||
changeLayout(layout: CatalogLayoutMode): void {
|
||||
this.state.update(current => ({ ...current, layout }));
|
||||
this.syncUrlFromState();
|
||||
this.persistContinueBrowsing();
|
||||
}
|
||||
|
||||
@@ -495,6 +510,7 @@ export class CatalogContainerComponent {
|
||||
}));
|
||||
|
||||
this.recomputeResults();
|
||||
this.syncUrlFromState();
|
||||
this.persistContinueBrowsing();
|
||||
}
|
||||
|
||||
@@ -515,14 +531,22 @@ export class CatalogContainerComponent {
|
||||
|
||||
private loadCatalog(categoryId?: number): void {
|
||||
this.loadingProducts.set(true);
|
||||
const categoryID = categoryId ?? this.state().category?.id ?? this.state().filters.categoryIds[0] ?? null;
|
||||
const criteria = this.buildCriteria(categoryID);
|
||||
const categoryID = categoryId ?? this.state().category?.id ?? this.state().filters.categoryIds[0];
|
||||
const query: SearchQuery = {
|
||||
text: this.state().search,
|
||||
categoryIds: categoryID == null ? [] : [categoryID],
|
||||
subcategoryIds: [],
|
||||
filters: this.filterState(),
|
||||
sort: this.state().sort,
|
||||
page: 1,
|
||||
pageSize: this.backendFetchSize,
|
||||
};
|
||||
|
||||
this.dataSubscription = this.productFacade.loadCatalog(criteria)
|
||||
this.dataSubscription = this.searchFacade.loadCatalog(query)
|
||||
.subscribe({
|
||||
next: result => {
|
||||
this.rawProducts.set(result.items);
|
||||
this.buildFilterDefinitions(result.items);
|
||||
this.filterDefinitions.set(this.searchFacade.buildFilterGroups(result.items, this.catalogConfig().enabledFilters));
|
||||
this.recomputeResults();
|
||||
this.loading.set(false);
|
||||
this.loadingProducts.set(false);
|
||||
@@ -531,119 +555,6 @@ export class CatalogContainerComponent {
|
||||
});
|
||||
}
|
||||
|
||||
private buildCriteria(categoryId: number | null): SearchCriteria {
|
||||
const state = this.state();
|
||||
|
||||
return {
|
||||
keyword: state.search,
|
||||
categoryIDs: categoryId == null ? [] : [categoryId],
|
||||
sort: state.sort,
|
||||
page: 1,
|
||||
pageSize: this.backendFetchSize,
|
||||
minPrice: this.filterState().ranges['price']?.min,
|
||||
maxPrice: this.filterState().ranges['price']?.max,
|
||||
discountOnly: this.filterState().toggles['discount'],
|
||||
newOnly: this.filterState().toggles['new']
|
||||
};
|
||||
}
|
||||
|
||||
private buildFilterDefinitions(products: Product[]): void {
|
||||
const enabled = new Set(this.catalogConfig().enabledFilters);
|
||||
const priceValues = products.map(product => product.price).filter(price => Number.isFinite(price));
|
||||
const brands = this.collectUnique(products.flatMap(product => product.tags ?? []).filter(tag => !tag.startsWith('new') && !tag.startsWith('sale')));
|
||||
const colors = this.collectUnique(products.flatMap(product => [product.colour, ...(product.itemDetails?.map(detail => detail.colour || detail.color) ?? [])]).filter(Boolean) as string[]);
|
||||
const sizes = this.collectUnique(products.flatMap(product => [product.size, ...(product.itemDetails?.map(detail => detail.size) ?? [])]).filter(Boolean) as string[]);
|
||||
const categories = this.collectUnique(products.map(product => String(product.categoryID)).filter(Boolean));
|
||||
const subcategories = this.collectUnique(products.map(product => product.subcategoryId ?? '').filter(Boolean));
|
||||
const attributes = this.collectUnique(products.flatMap(product => (product.descriptionFields ?? []).map(field => field.key)));
|
||||
const ratings = [5, 4, 3, 2, 1];
|
||||
|
||||
const definitions: FilterDefinition[] = [
|
||||
{
|
||||
id: 'price',
|
||||
label: this.translate.t('catalog.filterPrice'),
|
||||
type: 'range',
|
||||
min: priceValues.length ? Math.min(...priceValues) : 0,
|
||||
max: priceValues.length ? Math.max(...priceValues) : 0,
|
||||
enabled: enabled.has('price')
|
||||
},
|
||||
{
|
||||
id: 'availability',
|
||||
label: this.translate.t('catalog.filterAvailability'),
|
||||
type: 'multi-select',
|
||||
options: [
|
||||
{ id: 'in-stock', label: this.translate.t('catalog.filterInStock'), value: 'in-stock' },
|
||||
{ id: 'low-stock', label: this.translate.t('catalog.filterLowStock'), value: 'low-stock' },
|
||||
{ id: 'out-of-stock', label: this.translate.t('catalog.filterOutOfStock'), value: 'out-of-stock' }
|
||||
],
|
||||
enabled: enabled.has('availability')
|
||||
},
|
||||
{
|
||||
id: 'rating',
|
||||
label: this.translate.t('catalog.filterRating'),
|
||||
type: 'multi-select',
|
||||
options: ratings.map(value => ({ id: `rating-${value}`, label: this.translate.t('catalog.filterStars', { count: value }), value: String(value) })),
|
||||
enabled: enabled.has('rating')
|
||||
},
|
||||
{
|
||||
id: 'brand',
|
||||
label: this.translate.t('catalog.filterBrand'),
|
||||
type: 'multi-select',
|
||||
options: brands.map(value => ({ id: `brand-${value}`, label: value, value })),
|
||||
enabled: enabled.has('brand')
|
||||
},
|
||||
{
|
||||
id: 'category',
|
||||
label: this.translate.t('catalog.filterCategory'),
|
||||
type: 'multi-select',
|
||||
options: categories.map(value => ({ id: `cat-${value}`, label: this.translate.t('catalog.filterCategoryValue', { value }), value })),
|
||||
enabled: enabled.has('category')
|
||||
},
|
||||
{
|
||||
id: 'subcategory',
|
||||
label: this.translate.t('catalog.filterSubcategory'),
|
||||
type: 'multi-select',
|
||||
options: subcategories.map(value => ({ id: `sub-${value}`, label: value, value })),
|
||||
enabled: enabled.has('subcategory')
|
||||
},
|
||||
{
|
||||
id: 'discount',
|
||||
label: this.translate.t('catalog.filterDiscount'),
|
||||
type: 'toggle',
|
||||
enabled: enabled.has('discount')
|
||||
},
|
||||
{
|
||||
id: 'new',
|
||||
label: this.translate.t('catalog.filterNew'),
|
||||
type: 'toggle',
|
||||
enabled: enabled.has('new')
|
||||
},
|
||||
{
|
||||
id: 'color',
|
||||
label: this.translate.t('catalog.filterColor'),
|
||||
type: 'multi-select',
|
||||
options: colors.map(value => ({ id: `color-${value}`, label: value, value })),
|
||||
enabled: enabled.has('color')
|
||||
},
|
||||
{
|
||||
id: 'size',
|
||||
label: this.translate.t('catalog.filterSize'),
|
||||
type: 'multi-select',
|
||||
options: sizes.map(value => ({ id: `size-${value}`, label: value, value })),
|
||||
enabled: enabled.has('size')
|
||||
},
|
||||
{
|
||||
id: 'attributes',
|
||||
label: this.translate.t('catalog.filterAttributes'),
|
||||
type: 'multi-select',
|
||||
options: attributes.map(value => ({ id: `attr-${value}`, label: value, value })),
|
||||
enabled: enabled.has('attributes')
|
||||
}
|
||||
];
|
||||
|
||||
this.filterDefinitions.set(definitions);
|
||||
}
|
||||
|
||||
private recomputeResults(): void {
|
||||
if (this.rawProducts().length === 0) {
|
||||
this.products.set([]);
|
||||
@@ -663,40 +574,29 @@ export class CatalogContainerComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = this.applyFilters(this.rawProducts());
|
||||
const sorted = this.applySort(filtered);
|
||||
const filtered = this.searchFacade.applyFilters(this.rawProducts(), this.filterState(), this.state().search);
|
||||
const sorted = this.searchFacade.applySort(filtered, this.state().sort);
|
||||
const pagination = this.state().pagination;
|
||||
const page = Math.max(1, pagination.page);
|
||||
const pageSize = Math.max(1, pagination.count);
|
||||
const skip = (page - 1) * pageSize;
|
||||
const paged = sorted.slice(skip, skip + pageSize);
|
||||
const paged = this.searchFacade.paginateProducts(sorted, page, pageSize);
|
||||
|
||||
this.products.set(paged);
|
||||
this.products.set(paged.items);
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
pagination: {
|
||||
...current.pagination,
|
||||
total: sorted.length,
|
||||
skip,
|
||||
hasMore: skip + pageSize < sorted.length
|
||||
skip: paged.skip,
|
||||
hasMore: paged.skip + paged.pageSize < sorted.length
|
||||
}
|
||||
}));
|
||||
|
||||
const result = toSearchResult({
|
||||
items: paged,
|
||||
total: sorted.length,
|
||||
count: pageSize,
|
||||
skip
|
||||
}, {
|
||||
keyword: this.state().search,
|
||||
page,
|
||||
pageSize,
|
||||
sort: this.state().sort
|
||||
});
|
||||
const result = this.searchFacade.buildResult(this.toSearchState(), sorted.length, paged.items);
|
||||
|
||||
this.searchResult.set(result);
|
||||
this.searchSummary.set(result.summary);
|
||||
this.searchSuggestions.set(this.buildSuggestions(this.state().search, sorted));
|
||||
this.searchSuggestions.set(this.searchFacade.buildLiveSuggestions(this.state().search, sorted));
|
||||
|
||||
if (this.pendingScrollY != null && typeof window !== 'undefined') {
|
||||
const scrollY = this.pendingScrollY;
|
||||
@@ -705,138 +605,64 @@ export class CatalogContainerComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private buildSuggestions(query: string, products: Product[]): string[] {
|
||||
const normalized = query.trim().toLowerCase();
|
||||
if (!this.catalogConfig().suggestionsEnabled || normalized.length < 2) {
|
||||
return [];
|
||||
private toSearchState(): SearchState {
|
||||
const current = this.state();
|
||||
|
||||
return {
|
||||
text: current.search,
|
||||
sort: current.sort,
|
||||
layout: current.layout,
|
||||
page: current.pagination.page,
|
||||
pageSize: current.pagination.count,
|
||||
filters: this.filterState(),
|
||||
};
|
||||
}
|
||||
|
||||
private applySearchStatePatch(patch?: Partial<SearchState>): void {
|
||||
if (!patch) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.collectUnique(
|
||||
products
|
||||
.map(product => product.name)
|
||||
.filter(name => name.toLowerCase().includes(normalized))
|
||||
.slice(0, 6)
|
||||
if (patch.filters) {
|
||||
this.filterState.set(patch.filters);
|
||||
}
|
||||
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
search: patch.text ?? current.search,
|
||||
sort: patch.sort ?? current.sort,
|
||||
layout: patch.layout ?? current.layout,
|
||||
pagination: {
|
||||
...current.pagination,
|
||||
page: patch.page ?? current.pagination.page,
|
||||
count: patch.pageSize ?? current.pagination.count,
|
||||
skip: Math.max(0, ((patch.page ?? current.pagination.page) - 1) * (patch.pageSize ?? current.pagination.count)),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
private hasSearchState(patch: Partial<SearchState>): boolean {
|
||||
return Boolean(
|
||||
patch.text?.length ||
|
||||
patch.sort ||
|
||||
patch.layout ||
|
||||
(patch.page && patch.page > 1) ||
|
||||
patch.filters
|
||||
);
|
||||
}
|
||||
|
||||
private applyFilters(products: Product[]): Product[] {
|
||||
const filterState = this.filterState();
|
||||
const query = this.state().search.trim().toLowerCase();
|
||||
|
||||
return products.filter(product => {
|
||||
if (query.length > 0) {
|
||||
const haystack = `${product.name} ${product.simpleDescription ?? ''}`.toLowerCase();
|
||||
if (!haystack.includes(query)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const priceRange = filterState.ranges['price'];
|
||||
if (priceRange?.min != null && product.price < priceRange.min) {
|
||||
return false;
|
||||
}
|
||||
if (priceRange?.max != null && product.price > priceRange.max) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterState.toggles['discount'] && !(product.discount > 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterState.toggles['new'] && !(product.badges ?? []).some(badge => badge.toLowerCase().includes('new'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedRatings = filterState.values['rating'] ?? [];
|
||||
if (selectedRatings.length > 0 && !selectedRatings.includes(String(Math.floor(product.rating || 0)))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedAvailability = filterState.values['availability'] ?? [];
|
||||
if (selectedAvailability.length > 0) {
|
||||
const availability = product.remainings === 'out'
|
||||
? 'out-of-stock'
|
||||
: product.remainings === 'low'
|
||||
? 'low-stock'
|
||||
: 'in-stock';
|
||||
|
||||
if (!selectedAvailability.includes(availability)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const selectedBrands = filterState.values['brand'] ?? [];
|
||||
if (selectedBrands.length > 0) {
|
||||
const tags = product.tags ?? [];
|
||||
if (!selectedBrands.some(brand => tags.includes(brand))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const selectedCategories = filterState.values['category'] ?? [];
|
||||
if (selectedCategories.length > 0 && !selectedCategories.includes(String(product.categoryID))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedSubcategories = filterState.values['subcategory'] ?? [];
|
||||
if (selectedSubcategories.length > 0 && !selectedSubcategories.includes(product.subcategoryId ?? '')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedColors = filterState.values['color'] ?? [];
|
||||
if (selectedColors.length > 0) {
|
||||
const colors = this.collectUnique([product.colour ?? '', ...(product.itemDetails?.map(detail => detail.colour || detail.color || '') ?? [])]);
|
||||
if (!selectedColors.some(color => colors.includes(color))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const selectedSizes = filterState.values['size'] ?? [];
|
||||
if (selectedSizes.length > 0) {
|
||||
const sizes = this.collectUnique([product.size ?? '', ...(product.itemDetails?.map(detail => detail.size || '') ?? [])]);
|
||||
if (!selectedSizes.some(size => sizes.includes(size))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const selectedAttributes = filterState.values['attributes'] ?? [];
|
||||
if (selectedAttributes.length > 0) {
|
||||
const keys = (product.descriptionFields ?? []).map(field => field.key);
|
||||
if (!selectedAttributes.some(attribute => keys.includes(attribute))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
private syncUrlFromState(): void {
|
||||
const queryParams = this.searchFacade.toQueryParams(this.toSearchState());
|
||||
this.syncingUrl = true;
|
||||
void this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams,
|
||||
replaceUrl: true,
|
||||
}).finally(() => {
|
||||
this.syncingUrl = false;
|
||||
});
|
||||
}
|
||||
|
||||
private applySort(products: Product[]): Product[] {
|
||||
const sort = this.state().sort;
|
||||
const sorted = [...products];
|
||||
|
||||
switch (sort) {
|
||||
case 'price_asc':
|
||||
return sorted.sort((a, b) => a.price - b.price);
|
||||
case 'price_desc':
|
||||
return sorted.sort((a, b) => b.price - a.price);
|
||||
case 'rating':
|
||||
return sorted.sort((a, b) => (b.rating || 0) - (a.rating || 0));
|
||||
case 'popular':
|
||||
return sorted.sort((a, b) => (b.callbacks?.length ?? 0) - (a.callbacks?.length ?? 0));
|
||||
case 'discount':
|
||||
return sorted.sort((a, b) => (b.discount || 0) - (a.discount || 0));
|
||||
case 'latest':
|
||||
case 'relevance':
|
||||
default:
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
|
||||
private collectUnique(values: string[]): string[] {
|
||||
return [...new Set(values.map(value => value.trim()).filter(Boolean))];
|
||||
}
|
||||
|
||||
private resolveCatalogConfig() {
|
||||
const snapshot = this.configService.getBootstrapSnapshot() as any;
|
||||
const raw = snapshot?.catalog ?? {};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<table class="compare-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Attribute</th>
|
||||
<th>{{ 'ux.compareAttribute' | translate }}</th>
|
||||
@for (product of products; track product.itemID) {
|
||||
<th>
|
||||
<div class="compare-product-title">{{ product.name }}</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, computed } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, Input, computed, inject } from '@angular/core';
|
||||
import { Product } from '../../../../../core/products/models/product-domain.model';
|
||||
import { TranslateService } from '../../../../../i18n/translate.service';
|
||||
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
||||
|
||||
interface CompareRow {
|
||||
key: string;
|
||||
@@ -11,12 +13,14 @@ interface CompareRow {
|
||||
@Component({
|
||||
selector: 'app-compare-table',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
imports: [TranslatePipe],
|
||||
templateUrl: './compare-table.component.html',
|
||||
styleUrls: ['./compare-table.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CompareTableComponent {
|
||||
private readonly i18n = inject(TranslateService);
|
||||
|
||||
@Input() products: Product[] = [];
|
||||
@Input() hideIdentical = false;
|
||||
@Input() highlightDifferences = true;
|
||||
@@ -28,12 +32,12 @@ export class CompareTableComponent {
|
||||
}
|
||||
|
||||
const baseRows: CompareRow[] = [
|
||||
this.toRow('price', 'Price', products.map(product => `${product.price.toFixed(2)} ${product.currency}`)),
|
||||
this.toRow('rating', 'Rating', products.map(product => `${(product.rating ?? 0).toFixed(1)}`)),
|
||||
this.toRow('stock', 'Stock', products.map(product => product.remainings ?? 'unknown')),
|
||||
this.toRow('discount', 'Discount', products.map(product => `${product.discount ?? 0}%`)),
|
||||
this.toRow('color', 'Color', products.map(product => product.colour ?? '—')),
|
||||
this.toRow('size', 'Size', products.map(product => product.size ?? '—'))
|
||||
this.toRow('price', this.i18n.t('ux.comparePrice'), products.map(product => `${product.price.toFixed(2)} ${product.currency}`)),
|
||||
this.toRow('rating', this.i18n.t('ux.compareRating'), products.map(product => `${(product.rating ?? 0).toFixed(1)}`)),
|
||||
this.toRow('stock', this.i18n.t('ux.compareStock'), products.map(product => product.remainings ?? this.i18n.t('ux.compareUnknown'))),
|
||||
this.toRow('discount', this.i18n.t('ux.compareDiscount'), products.map(product => `${product.discount ?? 0}%`)),
|
||||
this.toRow('color', this.i18n.t('ux.compareColor'), products.map(product => product.colour ?? '—')),
|
||||
this.toRow('size', this.i18n.t('ux.compareSize'), products.map(product => product.size ?? '—'))
|
||||
];
|
||||
|
||||
const dynamicKeys = [...new Set(products.flatMap(product => (product.descriptionFields ?? []).map(field => field.key)))];
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ProductCardComponent } from '../../../../../components/product-card/pro
|
||||
import { Product } from '../../../../../core/products/models/product-domain.model';
|
||||
import { UserExperienceFacade } from '../../../../../facades/platform/user-experience.facade';
|
||||
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
||||
import { TranslateService } from '../../../../../i18n/translate.service';
|
||||
import { CartService } from '../../../../../services';
|
||||
import { UserNotificationService } from '../../services/user-notification.service';
|
||||
|
||||
@@ -15,13 +16,14 @@ import { UserNotificationService } from '../../services/user-notification.servic
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class RecentlyViewedStripComponent {
|
||||
@Input() title = 'Recently Viewed';
|
||||
@Input() title = '';
|
||||
@Input() maxItems = 8;
|
||||
@Input() showClear = false;
|
||||
|
||||
private readonly uxFacade = inject(UserExperienceFacade);
|
||||
private readonly cartService = inject(CartService);
|
||||
private readonly notifications = inject(UserNotificationService);
|
||||
private readonly translate = inject(TranslateService);
|
||||
|
||||
readonly items = computed(() => this.uxFacade.recentlyViewed().slice(0, Math.max(1, this.maxItems)));
|
||||
readonly hasItems = computed(() => this.items().length > 0);
|
||||
@@ -30,12 +32,12 @@ export class RecentlyViewedStripComponent {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.cartService.addItem(itemID);
|
||||
this.notifications.show('Added to cart', 'success');
|
||||
this.notifications.show(this.translate.t('catalog.addToCart'), 'success');
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.uxFacade.clearRecentlyViewed();
|
||||
this.notifications.show('Recently viewed cleared', 'info');
|
||||
this.notifications.show(this.translate.t('ux.clearRecentlyViewed'), 'info');
|
||||
}
|
||||
|
||||
itemProduct(index: number): Product {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { RouterLink } from '@angular/router';
|
||||
import { ProductCardComponent } from '../../../../../components/product-card/product-card.component';
|
||||
import { UserExperienceFacade } from '../../../../../facades/platform/user-experience.facade';
|
||||
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
||||
import { TranslateService } from '../../../../../i18n/translate.service';
|
||||
import { LangRoutePipe } from '../../../../../pipes/lang-route.pipe';
|
||||
import { CartService } from '../../../../../services';
|
||||
import { UserNotificationService } from '../../services/user-notification.service';
|
||||
@@ -18,6 +19,7 @@ import { UserNotificationService } from '../../services/user-notification.servic
|
||||
export class WishlistPageComponent {
|
||||
private readonly cartService = inject(CartService);
|
||||
private readonly uxFacade = inject(UserExperienceFacade);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly notifications = inject(UserNotificationService);
|
||||
|
||||
readonly favorites = this.uxFacade.wishlist;
|
||||
@@ -27,7 +29,7 @@ export class WishlistPageComponent {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.cartService.addItem(itemId);
|
||||
this.notifications.show('Added to cart', 'success');
|
||||
this.notifications.show(this.translate.t('ux.addedToCart'), 'success');
|
||||
}
|
||||
|
||||
removeFromWishlist(itemId: number): void {
|
||||
@@ -37,11 +39,11 @@ export class WishlistPageComponent {
|
||||
}
|
||||
|
||||
this.uxFacade.toggleWishlist(target.product);
|
||||
this.notifications.show('Removed from wishlist', 'info');
|
||||
this.notifications.show(this.translate.t('ux.wishlistRemoved'), 'info');
|
||||
}
|
||||
|
||||
clearWishlist(): void {
|
||||
this.uxFacade.clearWishlist();
|
||||
this.notifications.show('Wishlist cleared', 'info');
|
||||
this.notifications.show(this.translate.t('ux.wishlistCleared'), 'info');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ export const en: Translations = {
|
||||
catalog: 'Catalog',
|
||||
wishlist: 'Wishlist',
|
||||
compare: 'Compare',
|
||||
openMenu: 'Open menu',
|
||||
closeMenu: 'Close menu',
|
||||
},
|
||||
footer: {
|
||||
description: 'A modern marketplace for comfortable shopping',
|
||||
@@ -102,6 +104,8 @@ export const en: Translations = {
|
||||
paymentTimeoutDesc: 'We did not receive payment confirmation within 3 minutes.',
|
||||
autoClose: 'Window will close automatically...',
|
||||
confirmClear: 'Are you sure you want to clear the cart?',
|
||||
removeItem: 'Remove item',
|
||||
paymentQrAlt: 'Payment QR code',
|
||||
acceptTerms: 'Please accept the offer terms, return policy, and warranty terms to proceed with the order.',
|
||||
copyError: 'Copy error:',
|
||||
emailSuccess: 'Email sent successfully! Check your inbox.',
|
||||
@@ -175,11 +179,15 @@ export const en: Translations = {
|
||||
enabled: 'Enabled',
|
||||
searchPlaceholder: 'Search products, brands, categories',
|
||||
searchSubmit: 'Search',
|
||||
clearSearch: 'Clear',
|
||||
suggestionsTitle: 'Suggestions',
|
||||
popularSearchesTitle: 'Popular searches',
|
||||
recentSearchesTitle: 'Recent searches',
|
||||
searchHistoryTitle: 'Search history',
|
||||
clearHistory: 'Clear',
|
||||
searchNoResultsHint: 'No results found for this query. Try broader keywords.',
|
||||
searchProductsFound: '{{total}} products found',
|
||||
searchResultsSummary: '{{total}} results for "{{query}}"',
|
||||
sortBy: 'Sort by',
|
||||
sortRelevance: 'Recommended',
|
||||
sortLatest: 'Newest',
|
||||
@@ -331,6 +339,39 @@ export const en: Translations = {
|
||||
retryConnection: 'Retry',
|
||||
pageTitle: 'Marketplace of goods and services',
|
||||
},
|
||||
platform: {
|
||||
loadingPage: 'Loading platform page...',
|
||||
emptyConfig: 'No page configuration found.',
|
||||
},
|
||||
backoffice: {
|
||||
title: 'Backoffice Sandbox',
|
||||
subtitle: 'Business data management domain (mocked for platform foundation).',
|
||||
loading: 'Loading backoffice data...',
|
||||
products: 'Products',
|
||||
categories: 'Categories',
|
||||
otherDomains: 'Other Domains',
|
||||
sku: 'SKU',
|
||||
titleColumn: 'Title',
|
||||
price: 'Price',
|
||||
status: 'Status',
|
||||
itemsCount: 'items',
|
||||
orders: 'Orders',
|
||||
customers: 'Customers',
|
||||
inventory: 'Inventory',
|
||||
media: 'Media',
|
||||
settings: 'Settings',
|
||||
notAvailable: 'n/a',
|
||||
},
|
||||
builder: {
|
||||
title: 'Builder Sandbox',
|
||||
subtitle: 'Configuration-only editing surface.',
|
||||
branding: 'Branding',
|
||||
brandName: 'Brand Name',
|
||||
featureFlags: 'Feature Flags',
|
||||
},
|
||||
widgets: {
|
||||
unavailable: 'Widget unavailable',
|
||||
},
|
||||
carousel: {
|
||||
loading: 'Loading products...',
|
||||
addToCart: 'Add to cart',
|
||||
@@ -360,6 +401,7 @@ export const en: Translations = {
|
||||
wishlistEmptyTitle: 'Your wishlist is empty',
|
||||
wishlistEmptyDescription: 'Save products to revisit them anytime.',
|
||||
clearWishlist: 'Clear wishlist',
|
||||
wishlistCleared: 'Wishlist cleared',
|
||||
compareTitle: 'Compare Products',
|
||||
compareEmptyTitle: 'No products to compare',
|
||||
compareEmptyDescription: 'Add products from catalog cards to compare specs and prices.',
|
||||
@@ -378,7 +420,17 @@ export const en: Translations = {
|
||||
productShared: 'Product shared',
|
||||
productLinkCopied: 'Product link copied',
|
||||
sharingUnsupported: 'Sharing is not supported on this device',
|
||||
addedToCart: 'Added to cart',
|
||||
enterQueryBeforeSave: 'Enter a search query before saving',
|
||||
savedSearchNamed: 'Saved search: {{name}}',
|
||||
recentlyViewedTitle: 'Recently Viewed',
|
||||
compareAttribute: 'Attribute',
|
||||
comparePrice: 'Price',
|
||||
compareRating: 'Rating',
|
||||
compareStock: 'Stock',
|
||||
compareDiscount: 'Discount',
|
||||
compareColor: 'Color',
|
||||
compareSize: 'Size',
|
||||
compareUnknown: 'unknown',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,6 +10,8 @@ export const hy: Translations = {
|
||||
catalog: 'Կատալոգ',
|
||||
wishlist: 'Ընտրյալ',
|
||||
compare: 'Համեմատում',
|
||||
openMenu: 'Բացել մենյուն',
|
||||
closeMenu: 'Փակել մենյուն',
|
||||
},
|
||||
footer: {
|
||||
description: 'Ժամանակակից մարքեթփլեյս հարմար գնումների համար',
|
||||
@@ -102,6 +104,8 @@ export const hy: Translations = {
|
||||
paymentTimeoutDesc: 'Մենք չստացանք վճարման հաստատում 3 րոպեի ընթացքում։',
|
||||
autoClose: 'Պատուհանը կփակվի ավտոմատ...',
|
||||
confirmClear: 'Վստա՞հ եք, որ ցանկանում եք մաքրել զամբյուղը',
|
||||
removeItem: 'Հեռացնել ապրանքը',
|
||||
paymentQrAlt: 'Վճարման QR կոդ',
|
||||
acceptTerms: 'Խնդրում ենք ընդունել պայմանները՝ պատվերը հաստատելու համար։',
|
||||
copyError: 'Պատճենման սխալ՝',
|
||||
emailSuccess: 'Email-ը հաջողությամբ ուղարկվեց։ Ստուգեք ձեր փոստը։',
|
||||
@@ -175,11 +179,15 @@ export const hy: Translations = {
|
||||
enabled: 'Միացված',
|
||||
searchPlaceholder: 'Որոնել ապրանքներ, բրենդներ, կատեգորիաներ',
|
||||
searchSubmit: 'Որոնել',
|
||||
clearSearch: 'Մաքրել',
|
||||
suggestionsTitle: 'Առաջարկներ',
|
||||
popularSearchesTitle: 'Հանրաճանաչ որոնումներ',
|
||||
recentSearchesTitle: 'Վերջին որոնումներ',
|
||||
searchHistoryTitle: 'Որոնման պատմություն',
|
||||
clearHistory: 'Մաքրել',
|
||||
searchNoResultsHint: 'Արդյունքներ չկան։ Փորձեք ավելի լայն բանալի բառեր։',
|
||||
searchProductsFound: 'Գտնված ապրանքներ՝ {{total}}',
|
||||
searchResultsSummary: '{{total}} արդյունք "{{query}}" հարցմամբ',
|
||||
sortBy: 'Տեսակավորել ըստ',
|
||||
sortRelevance: 'Առաջարկվող',
|
||||
sortLatest: 'Նորության',
|
||||
@@ -331,6 +339,39 @@ export const hy: Translations = {
|
||||
retryConnection: 'Փորձել կրկին',
|
||||
pageTitle: 'Ապրանքների և ծառայությունների մարքեթփլեյս',
|
||||
},
|
||||
platform: {
|
||||
loadingPage: 'Պլատֆորմի էջի բեռնում...',
|
||||
emptyConfig: 'Էջի կոնֆիգուրացիա չի գտնվել։',
|
||||
},
|
||||
backoffice: {
|
||||
title: 'Backoffice Sandbox',
|
||||
subtitle: 'Բիզնես տվյալների կառավարման դոմեն (mock պլատֆորմի հիմքի համար):',
|
||||
loading: 'Backoffice տվյալների բեռնում...',
|
||||
products: 'Ապրանքներ',
|
||||
categories: 'Կատեգորիաներ',
|
||||
otherDomains: 'Այլ դոմեններ',
|
||||
sku: 'SKU',
|
||||
titleColumn: 'Անվանում',
|
||||
price: 'Գին',
|
||||
status: 'Կարգավիճակ',
|
||||
itemsCount: 'հատ',
|
||||
orders: 'Պատվերներ',
|
||||
customers: 'Հաճախորդներ',
|
||||
inventory: 'Պահեստ',
|
||||
media: 'Մեդիա',
|
||||
settings: 'Կարգավորումներ',
|
||||
notAvailable: 'չկա',
|
||||
},
|
||||
builder: {
|
||||
title: 'Builder Sandbox',
|
||||
subtitle: 'Միայն կոնֆիգուրացիայի խմբագրման մակերես։',
|
||||
branding: 'Բրենդինգ',
|
||||
brandName: 'Բրենդի անվանում',
|
||||
featureFlags: 'Feature flags',
|
||||
},
|
||||
widgets: {
|
||||
unavailable: 'Վիջեթը հասանելի չէ',
|
||||
},
|
||||
carousel: {
|
||||
loading: 'Ապրանքների բեռնում...',
|
||||
addToCart: 'Ավելացնել զամբյուղ',
|
||||
@@ -360,6 +401,7 @@ export const hy: Translations = {
|
||||
wishlistEmptyTitle: 'Ընտրյալները դատարկ են',
|
||||
wishlistEmptyDescription: 'Պահպանեք ապրանքները, որպեսզի արագ վերադառնաք դրանց։',
|
||||
clearWishlist: 'Մաքրել ընտրյալները',
|
||||
wishlistCleared: 'Ընտրյալները մաքրվել են',
|
||||
compareTitle: 'Ապրանքների համեմատում',
|
||||
compareEmptyTitle: 'Համեմատելու ապրանքներ չկան',
|
||||
compareEmptyDescription: 'Ավելացրեք ապրանքներ կատալոգից՝ բնութագրերը և գները համեմատելու համար։',
|
||||
@@ -378,7 +420,17 @@ export const hy: Translations = {
|
||||
productShared: 'Ապրանքը կիսվել է',
|
||||
productLinkCopied: 'Ապրանքի հղումը պատճենվել է',
|
||||
sharingUnsupported: 'Այս սարքում կիսվելու հնարավորությունը չի աջակցվում',
|
||||
addedToCart: 'Ավելացվեց զամբյուղում',
|
||||
enterQueryBeforeSave: 'Մինչ պահպանումը մուտքագրեք որոնման հարցում',
|
||||
savedSearchNamed: 'Պահպանված որոնում՝ {{name}}',
|
||||
recentlyViewedTitle: 'Վերջին դիտվածներ',
|
||||
compareAttribute: 'Ատրիբուտ',
|
||||
comparePrice: 'Գին',
|
||||
compareRating: 'Վարկանիշ',
|
||||
compareStock: 'Առկայություն',
|
||||
compareDiscount: 'Զեղչ',
|
||||
compareColor: 'Գույն',
|
||||
compareSize: 'Չափ',
|
||||
compareUnknown: 'անհայտ',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,6 +10,8 @@ export const ru: Translations = {
|
||||
catalog: 'Каталог',
|
||||
wishlist: 'Избранное',
|
||||
compare: 'Сравнение',
|
||||
openMenu: 'Открыть меню',
|
||||
closeMenu: 'Закрыть меню',
|
||||
},
|
||||
footer: {
|
||||
description: 'Современный маркетплейс для комфортных покупок',
|
||||
@@ -102,6 +104,8 @@ export const ru: Translations = {
|
||||
paymentTimeoutDesc: 'Мы не получили подтверждение оплаты в течение 3 минут.',
|
||||
autoClose: 'Окно закроется автоматически...',
|
||||
confirmClear: 'Вы уверены, что хотите очистить корзину?',
|
||||
removeItem: 'Удалить товар',
|
||||
paymentQrAlt: 'QR-код для оплаты',
|
||||
acceptTerms: 'Пожалуйста, примите условия оферты, политику возврата и возврата для подтверждения оформления заказа.',
|
||||
copyError: 'Ошибка копирования:',
|
||||
emailSuccess: 'Email успешно отправлен! Проверьте свою почту.',
|
||||
@@ -175,11 +179,15 @@ export const ru: Translations = {
|
||||
enabled: 'Включено',
|
||||
searchPlaceholder: 'Поиск товаров, брендов и категорий',
|
||||
searchSubmit: 'Найти',
|
||||
clearSearch: 'Очистить',
|
||||
suggestionsTitle: 'Подсказки',
|
||||
popularSearchesTitle: 'Популярные запросы',
|
||||
recentSearchesTitle: 'Недавние запросы',
|
||||
searchHistoryTitle: 'История поиска',
|
||||
clearHistory: 'Очистить',
|
||||
searchNoResultsHint: 'Ничего не найдено. Попробуйте более общий запрос.',
|
||||
searchProductsFound: 'Найдено товаров: {{total}}',
|
||||
searchResultsSummary: '{{total}} результатов по запросу "{{query}}"',
|
||||
sortBy: 'Сортировать по',
|
||||
sortRelevance: 'Рекомендуемые',
|
||||
sortLatest: 'Новизне',
|
||||
@@ -331,6 +339,39 @@ export const ru: Translations = {
|
||||
retryConnection: 'Повторить попытку',
|
||||
pageTitle: 'Маркетплейс товаров и услуг',
|
||||
},
|
||||
platform: {
|
||||
loadingPage: 'Загрузка страницы платформы...',
|
||||
emptyConfig: 'Конфигурация страницы не найдена.',
|
||||
},
|
||||
backoffice: {
|
||||
title: 'Песочница Backoffice',
|
||||
subtitle: 'Домен управления бизнес-данными (mock для платформенного фундамента).',
|
||||
loading: 'Загрузка данных backoffice...',
|
||||
products: 'Товары',
|
||||
categories: 'Категории',
|
||||
otherDomains: 'Другие домены',
|
||||
sku: 'SKU',
|
||||
titleColumn: 'Название',
|
||||
price: 'Цена',
|
||||
status: 'Статус',
|
||||
itemsCount: 'шт.',
|
||||
orders: 'Заказы',
|
||||
customers: 'Клиенты',
|
||||
inventory: 'Склад',
|
||||
media: 'Медиа',
|
||||
settings: 'Настройки',
|
||||
notAvailable: 'н/д',
|
||||
},
|
||||
builder: {
|
||||
title: 'Песочница Builder',
|
||||
subtitle: 'Поверхность редактирования только конфигурации.',
|
||||
branding: 'Брендинг',
|
||||
brandName: 'Название бренда',
|
||||
featureFlags: 'Фичи-флаги',
|
||||
},
|
||||
widgets: {
|
||||
unavailable: 'Виджет недоступен',
|
||||
},
|
||||
carousel: {
|
||||
loading: 'Загрузка товаров...',
|
||||
addToCart: 'Добавить в корзину',
|
||||
@@ -360,6 +401,7 @@ export const ru: Translations = {
|
||||
wishlistEmptyTitle: 'Избранное пока пусто',
|
||||
wishlistEmptyDescription: 'Сохраняйте товары, чтобы быстро вернуться к ним позже.',
|
||||
clearWishlist: 'Очистить избранное',
|
||||
wishlistCleared: 'Избранное очищено',
|
||||
compareTitle: 'Сравнение товаров',
|
||||
compareEmptyTitle: 'Нет товаров для сравнения',
|
||||
compareEmptyDescription: 'Добавьте товары из каталога, чтобы сравнить характеристики и цену.',
|
||||
@@ -378,7 +420,17 @@ export const ru: Translations = {
|
||||
productShared: 'Товар отправлен',
|
||||
productLinkCopied: 'Ссылка на товар скопирована',
|
||||
sharingUnsupported: 'На этом устройстве нет поддержки шаринга',
|
||||
addedToCart: 'Добавлено в корзину',
|
||||
enterQueryBeforeSave: 'Введите поисковый запрос перед сохранением',
|
||||
savedSearchNamed: 'Сохраненный поиск: {{name}}',
|
||||
recentlyViewedTitle: 'Недавно просмотренные',
|
||||
compareAttribute: 'Атрибут',
|
||||
comparePrice: 'Цена',
|
||||
compareRating: 'Рейтинг',
|
||||
compareStock: 'Наличие',
|
||||
compareDiscount: 'Скидка',
|
||||
compareColor: 'Цвет',
|
||||
compareSize: 'Размер',
|
||||
compareUnknown: 'неизвестно',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface Translations {
|
||||
catalog: string;
|
||||
wishlist: string;
|
||||
compare: string;
|
||||
openMenu: string;
|
||||
closeMenu: string;
|
||||
};
|
||||
footer: {
|
||||
description: string;
|
||||
@@ -100,6 +102,8 @@ export interface Translations {
|
||||
paymentTimeoutDesc: string;
|
||||
autoClose: string;
|
||||
confirmClear: string;
|
||||
removeItem: string;
|
||||
paymentQrAlt: string;
|
||||
acceptTerms: string;
|
||||
copyError: string;
|
||||
emailSuccess: string;
|
||||
@@ -173,11 +177,15 @@ export interface Translations {
|
||||
enabled: string;
|
||||
searchPlaceholder: string;
|
||||
searchSubmit: string;
|
||||
clearSearch: string;
|
||||
suggestionsTitle: string;
|
||||
popularSearchesTitle: string;
|
||||
recentSearchesTitle: string;
|
||||
searchHistoryTitle: string;
|
||||
clearHistory: string;
|
||||
searchNoResultsHint: string;
|
||||
searchProductsFound: string;
|
||||
searchResultsSummary: string;
|
||||
sortBy: string;
|
||||
sortRelevance: string;
|
||||
sortLatest: string;
|
||||
@@ -329,6 +337,39 @@ export interface Translations {
|
||||
retryConnection: string;
|
||||
pageTitle: string;
|
||||
};
|
||||
platform: {
|
||||
loadingPage: string;
|
||||
emptyConfig: string;
|
||||
};
|
||||
backoffice: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
loading: string;
|
||||
products: string;
|
||||
categories: string;
|
||||
otherDomains: string;
|
||||
sku: string;
|
||||
titleColumn: string;
|
||||
price: string;
|
||||
status: string;
|
||||
itemsCount: string;
|
||||
orders: string;
|
||||
customers: string;
|
||||
inventory: string;
|
||||
media: string;
|
||||
settings: string;
|
||||
notAvailable: string;
|
||||
};
|
||||
builder: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
branding: string;
|
||||
brandName: string;
|
||||
featureFlags: string;
|
||||
};
|
||||
widgets: {
|
||||
unavailable: string;
|
||||
};
|
||||
carousel: {
|
||||
loading: string;
|
||||
addToCart: string;
|
||||
@@ -358,6 +399,7 @@ export interface Translations {
|
||||
wishlistEmptyTitle: string;
|
||||
wishlistEmptyDescription: string;
|
||||
clearWishlist: string;
|
||||
wishlistCleared: string;
|
||||
compareTitle: string;
|
||||
compareEmptyTitle: string;
|
||||
compareEmptyDescription: string;
|
||||
@@ -376,7 +418,17 @@ export interface Translations {
|
||||
productShared: string;
|
||||
productLinkCopied: string;
|
||||
sharingUnsupported: string;
|
||||
addedToCart: string;
|
||||
enterQueryBeforeSave: string;
|
||||
savedSearchNamed: string;
|
||||
recentlyViewedTitle: string;
|
||||
compareAttribute: string;
|
||||
comparePrice: string;
|
||||
compareRating: string;
|
||||
compareStock: string;
|
||||
compareDiscount: string;
|
||||
compareColor: string;
|
||||
compareSize: string;
|
||||
compareUnknown: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, Input, inject } from '@angular/core';
|
||||
import { NgComponentOutlet } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
@@ -9,6 +9,7 @@ import { ResolvedWidget } from '../../widgets/contracts/widget-component.contrac
|
||||
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';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dynamic-page-layout',
|
||||
@@ -34,10 +35,19 @@ import { Category } from '../../core/categories/models/category-domain.model';
|
||||
[style.--section-align]="section.layout?.align ?? null"
|
||||
>
|
||||
@for (widget of section.widgets; track widget.id) {
|
||||
@if (isWidgetPermitted(widget.source)) {
|
||||
<div
|
||||
class="dynamic-widget"
|
||||
[attr.data-widget-type]="widget.type"
|
||||
[attr.data-widget-title]="widget.source.title ?? null"
|
||||
[attr.data-widget-subtitle]="widget.source.subtitle ?? null"
|
||||
[attr.data-widget-animation]="widget.source.animation?.name ?? null"
|
||||
[style.--widget-padding]="widget.padding ?? null"
|
||||
[style.--widget-animation-duration]="widget.source.animation?.duration ?? null"
|
||||
[style.--widget-animation-delay]="widget.source.animation?.delay ?? null"
|
||||
[style.--widget-animation-timing]="widget.source.animation?.timingFunction ?? null"
|
||||
[style.animation-name]="widget.source.animation?.name ?? null"
|
||||
[ngStyle]="widget.source.style ?? null"
|
||||
[class.dynamic-widget--desktop-hidden]="widget.visibility?.desktop === false"
|
||||
[class.dynamic-widget--tablet-hidden]="widget.visibility?.tablet === false"
|
||||
[class.dynamic-widget--mobile-hidden]="widget.visibility?.mobile === false"
|
||||
@@ -50,6 +60,7 @@ import { Category } from '../../core/categories/models/category-domain.model';
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,6 +93,9 @@ import { Category } from '../../core/categories/models/category-domain.model';
|
||||
|
||||
.dynamic-widget {
|
||||
padding: var(--widget-padding, 0);
|
||||
animation-duration: var(--widget-animation-duration, initial);
|
||||
animation-delay: var(--widget-animation-delay, initial);
|
||||
animation-timing-function: var(--widget-animation-timing, initial);
|
||||
}
|
||||
|
||||
.dynamic-section[data-section-layout='grid'] .dynamic-section__content {
|
||||
@@ -116,6 +130,8 @@ import { Category } from '../../core/categories/models/category-domain.model';
|
||||
export class DynamicPageLayoutComponent {
|
||||
@Input() model: PageRenderModel | null = null;
|
||||
|
||||
private readonly authService = inject(AuthService);
|
||||
|
||||
constructor(
|
||||
private readonly widgetHost: WidgetHostService,
|
||||
private readonly router: Router,
|
||||
@@ -129,4 +145,17 @@ export class DynamicPageLayoutComponent {
|
||||
onCategorySelected(category: Category): void {
|
||||
this.router.navigate([`/${this.languageService.currentLanguage()}/catalog`, category.id]);
|
||||
}
|
||||
|
||||
isWidgetPermitted(widget: WidgetRenderNode['source']): boolean {
|
||||
const permissions = widget.permissions;
|
||||
if (!permissions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (permissions.requireAuthenticated && !this.authService.isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { BackofficeFacade } from '../../facades/backoffice/backoffice.facade';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-backoffice-dashboard',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, TranslatePipe],
|
||||
template: `
|
||||
<main class="backoffice-dashboard">
|
||||
<h1>Backoffice Sandbox</h1>
|
||||
<p>Business data management domain (mocked for platform foundation).</p>
|
||||
<h1>{{ 'backoffice.title' | translate }}</h1>
|
||||
<p>{{ 'backoffice.subtitle' | translate }}</p>
|
||||
|
||||
@if (loading()) {
|
||||
<section class="card">Loading backoffice data...</section>
|
||||
<section class="card">{{ 'backoffice.loading' | translate }}</section>
|
||||
} @else {
|
||||
<section class="card">
|
||||
<h2>Products</h2>
|
||||
<h2>{{ 'backoffice.products' | translate }}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>SKU</th>
|
||||
<th>Title</th>
|
||||
<th>Price</th>
|
||||
<th>Status</th>
|
||||
<th>{{ 'backoffice.sku' | translate }}</th>
|
||||
<th>{{ 'backoffice.titleColumn' | translate }}</th>
|
||||
<th>{{ 'backoffice.price' | translate }}</th>
|
||||
<th>{{ 'backoffice.status' | translate }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -31,7 +32,7 @@ import { BackofficeFacade } from '../../facades/backoffice/backoffice.facade';
|
||||
<td>{{ product.sku }}</td>
|
||||
<td>{{ product.title }}</td>
|
||||
<td>{{ product.price.amount }} {{ product.price.currency }}</td>
|
||||
<td>{{ product.stockStatus ?? 'n/a' }}</td>
|
||||
<td>{{ product.stockStatus ?? ('backoffice.notAvailable' | translate) }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@@ -39,22 +40,22 @@ import { BackofficeFacade } from '../../facades/backoffice/backoffice.facade';
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Categories</h2>
|
||||
<h2>{{ 'backoffice.categories' | translate }}</h2>
|
||||
<ul>
|
||||
@for (category of categories(); track category.id) {
|
||||
<li>{{ category.title }} ({{ category.itemsCount ?? 0 }})</li>
|
||||
<li>{{ category.title }} ({{ category.itemsCount ?? 0 }} {{ 'backoffice.itemsCount' | translate }})</li>
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="card placeholders">
|
||||
<h2>Other Domains</h2>
|
||||
<h2>{{ 'backoffice.otherDomains' | translate }}</h2>
|
||||
<div class="chips">
|
||||
<span>Orders</span>
|
||||
<span>Customers</span>
|
||||
<span>Inventory</span>
|
||||
<span>Media</span>
|
||||
<span>Settings</span>
|
||||
<span>{{ 'backoffice.orders' | translate }}</span>
|
||||
<span>{{ 'backoffice.customers' | translate }}</span>
|
||||
<span>{{ 'backoffice.inventory' | translate }}</span>
|
||||
<span>{{ 'backoffice.media' | translate }}</span>
|
||||
<span>{{ 'backoffice.settings' | translate }}</span>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
@@ -2,21 +2,22 @@ import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BuilderConfigFacade } from '../../facades/builder/builder-config.facade';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-builder-sandbox',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
imports: [CommonModule, FormsModule, TranslatePipe],
|
||||
template: `
|
||||
<main class="builder-sandbox">
|
||||
<h1>Builder Sandbox</h1>
|
||||
<p>Configuration-only editing surface.</p>
|
||||
<h1>{{ 'builder.title' | translate }}</h1>
|
||||
<p>{{ 'builder.subtitle' | translate }}</p>
|
||||
|
||||
@if (branding(); as brand) {
|
||||
<section class="builder-card">
|
||||
<h2>Branding</h2>
|
||||
<h2>{{ 'builder.branding' | translate }}</h2>
|
||||
<label>
|
||||
Brand Name
|
||||
{{ 'builder.brandName' | translate }}
|
||||
<input type="text" [ngModel]="brand.brandName" (ngModelChange)="onBrandNameChange($event)" />
|
||||
</label>
|
||||
</section>
|
||||
@@ -24,7 +25,7 @@ import { BuilderConfigFacade } from '../../facades/builder/builder-config.facade
|
||||
|
||||
@if (featureFlags(); as flags) {
|
||||
<section class="builder-card">
|
||||
<h2>Feature Flags</h2>
|
||||
<h2>{{ 'builder.featureFlags' | translate }}</h2>
|
||||
@for (flag of featureFlagKeys(); track flag) {
|
||||
<label class="flag-row">
|
||||
<input type="checkbox" [checked]="flags[flag]" (change)="onFlagToggle(flag, $event)" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="cart-container dexar">
|
||||
<div class="cart-container platform">
|
||||
<div class="cart-header">
|
||||
<h1>{{ 'cart.title' | translate }}</h1>
|
||||
@if (itemCount() > 0) {
|
||||
@@ -37,7 +37,7 @@
|
||||
<div class="item-info">
|
||||
<div class="item-header">
|
||||
<a [routerLink]="['/product', item.itemID] | langRoute" class="item-name">{{ itemName(item) }}</a>
|
||||
<button class="remove-btn" (click)="removeItem(item)" title="Remove">
|
||||
<button class="remove-btn" (click)="removeItem(item)" [attr.title]="'cart.removeItem' | translate">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
@@ -225,7 +225,7 @@
|
||||
@if (qrCodeUrl()) {
|
||||
<div class="qr-section">
|
||||
<div class="qr-wrapper">
|
||||
<img [src]="qrCodeUrl()" alt="QR код для оплаты" class="qr-code" />
|
||||
<img [src]="qrCodeUrl()" [attr.alt]="'cart.paymentQrAlt' | translate" class="qr-code" />
|
||||
<div class="scan-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -272,7 +272,7 @@
|
||||
|
||||
@if (paymentStatus() === 'success') {
|
||||
<div class="payment-status-screen success">
|
||||
<div class="success-icon">✓</div>
|
||||
<div class="success-icon">?</div>
|
||||
<h2>{{ 'cart.paymentSuccess' | translate }}</h2>
|
||||
<!-- <p class="success-text">{{ 'cart.paymentSuccessDesc' | translate }}</p> -->
|
||||
|
||||
@@ -331,7 +331,7 @@
|
||||
|
||||
@if (paymentStatus() === 'timeout') {
|
||||
<div class="payment-status-screen timeout">
|
||||
<div class="timeout-icon">⏱</div>
|
||||
<div class="timeout-icon">?</div>
|
||||
<h2>{{ 'cart.paymentTimeout' | translate }}</h2>
|
||||
<p>{{ 'cart.paymentTimeoutDesc' | translate }}</p>
|
||||
<p class="auto-close">{{ 'cart.autoClose' | translate }}</p>
|
||||
@@ -355,3 +355,5 @@
|
||||
}
|
||||
|
||||
<app-telegram-login />
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Base styles (Dexar theme)
|
||||
.cart-container.dexar {
|
||||
// Base styles (platform theme)
|
||||
.cart-container.platform {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
@@ -7,8 +7,8 @@
|
||||
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
// Novo theme - Modern green design
|
||||
.cart-container.novo {
|
||||
// alt theme - Modern green design
|
||||
.cart-container.alt {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
@@ -16,7 +16,7 @@
|
||||
background: linear-gradient(to bottom, rgba(16, 185, 129, 0.02) 0%, transparent 300px);
|
||||
}
|
||||
|
||||
.cart-container.dexar .cart-header {
|
||||
.cart-container.platform .cart-header {
|
||||
margin-bottom: 28px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -64,7 +64,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.cart-container.novo .cart-header {
|
||||
.cart-container.alt .cart-header {
|
||||
margin-bottom: 32px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -113,8 +113,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Empty cart - Dexar
|
||||
.cart-container.dexar .empty-cart {
|
||||
// Empty cart - platform
|
||||
.cart-container.platform .empty-cart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -170,8 +170,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Empty cart - Novo
|
||||
.cart-container.novo .empty-cart {
|
||||
// Empty cart - alt
|
||||
.cart-container.alt .empty-cart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -230,8 +230,8 @@
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
// Novo wider summary
|
||||
.cart-container.novo .cart-content {
|
||||
// alt wider summary
|
||||
.cart-container.alt .cart-content {
|
||||
grid-template-columns: minmax(0, 1fr) 400px;
|
||||
gap: 32px;
|
||||
}
|
||||
@@ -243,8 +243,8 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// Novo larger gap
|
||||
.cart-container.novo .cart-items {
|
||||
// alt larger gap
|
||||
.cart-container.alt .cart-items {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@@ -266,13 +266,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Novo rounded corners
|
||||
.cart-container.novo .cart-item-wrapper {
|
||||
// alt rounded corners
|
||||
.cart-container.alt .cart-item-wrapper {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
// Dexar cart item
|
||||
.cart-container.dexar .cart-item {
|
||||
// platform cart item
|
||||
.cart-container.platform .cart-item {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
background: white;
|
||||
@@ -402,8 +402,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Novo cart item - modern green theme
|
||||
.cart-container.novo .cart-item {
|
||||
// alt cart item - modern green theme
|
||||
.cart-container.alt .cart-item {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
background: white;
|
||||
@@ -563,8 +563,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Dexar quantity controls
|
||||
.cart-container.dexar .quantity-controls {
|
||||
// platform quantity controls
|
||||
.cart-container.platform .quantity-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
@@ -607,8 +607,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Novo quantity controls - modern green
|
||||
.cart-container.novo .quantity-controls {
|
||||
// alt quantity controls - modern green
|
||||
.cart-container.alt .quantity-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
@@ -676,8 +676,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Dexar Cart Summary
|
||||
.cart-container.dexar .cart-summary {
|
||||
// platform Cart Summary
|
||||
.cart-container.platform .cart-summary {
|
||||
position: sticky;
|
||||
top: 90px;
|
||||
background: #f5f3f9;
|
||||
@@ -827,8 +827,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Novo Cart Summary - Green Modern
|
||||
.cart-container.novo .cart-summary {
|
||||
// alt Cart Summary - Green Modern
|
||||
.cart-container.alt .cart-summary {
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
background: white;
|
||||
@@ -1054,21 +1054,21 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.cart-container.dexar .cart-summary .delivery-warning {
|
||||
.cart-container.platform .cart-summary .delivery-warning {
|
||||
color: #9a6700;
|
||||
background: #fff8e8;
|
||||
border: 1px solid #f1ddb2;
|
||||
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.cart-container.novo .cart-summary .delivery-warning {
|
||||
.cart-container.alt .cart-summary .delivery-warning {
|
||||
color: #92400e;
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
|
||||
// Dexar checkbox colors
|
||||
.cart-container.dexar .terms-agreement .checkbox-container {
|
||||
// platform checkbox colors
|
||||
.cart-container.platform .terms-agreement .checkbox-container {
|
||||
input[type="checkbox"]:checked ~ .checkmark {
|
||||
background: #497671;
|
||||
border-color: #497671;
|
||||
@@ -1102,8 +1102,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Novo checkbox colors - green
|
||||
.cart-container.novo .terms-agreement .checkbox-container {
|
||||
// alt checkbox colors - green
|
||||
.cart-container.alt .terms-agreement .checkbox-container {
|
||||
input[type="checkbox"]:checked ~ .checkmark {
|
||||
background: #10b981;
|
||||
border-color: #10b981;
|
||||
@@ -1619,7 +1619,7 @@
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.cart-container.novo .cart-content {
|
||||
.cart-container.alt .cart-content {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
@@ -1631,13 +1631,13 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cart-container.novo,
|
||||
.cart-container.dexar {
|
||||
.cart-container.alt,
|
||||
.cart-container.platform {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.cart-container.novo .cart-header,
|
||||
.cart-container.dexar .cart-header {
|
||||
.cart-container.alt .cart-header,
|
||||
.cart-container.platform .cart-header {
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
@@ -1652,7 +1652,7 @@
|
||||
|
||||
.payment-modal {
|
||||
padding: 24px;
|
||||
padding-top: 56px; // Дополнительный отступ сверху для кнопки закрытия
|
||||
padding-top: 56px; // Äîïîëíèòåëüíûé îòñòóï ñâåðõó äëÿ êíîïêè çàêðûòèÿ
|
||||
}
|
||||
|
||||
.close-modal-btn {
|
||||
@@ -1710,17 +1710,17 @@
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
novo THEME STYLES - Modern Cart Design
|
||||
alt THEME STYLES - Modern Cart Design
|
||||
============================================= */
|
||||
|
||||
.novo-cart-container {
|
||||
.alt-cart-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
min-height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
.novo-cart-header {
|
||||
.alt-cart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -1739,7 +1739,7 @@
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.novo-clear-btn {
|
||||
.alt-clear-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
@@ -1769,7 +1769,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-empty-cart {
|
||||
.alt-empty-cart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -1804,7 +1804,7 @@
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.novo-shop-btn {
|
||||
.alt-shop-btn {
|
||||
display: inline-block;
|
||||
padding: 0.875rem 2rem;
|
||||
background: linear-gradient(135deg, #10b981, #14b8a6, #06b6d4);
|
||||
@@ -1822,20 +1822,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-cart-content {
|
||||
.alt-cart-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 400px;
|
||||
gap: 2rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.novo-cart-items {
|
||||
.alt-cart-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.novo-cart-item {
|
||||
.alt-cart-item {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
@@ -1850,7 +1850,7 @@
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.novo-item-image {
|
||||
.alt-item-image {
|
||||
flex-shrink: 0;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
@@ -1872,7 +1872,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-item-details {
|
||||
.alt-item-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1898,7 +1898,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-remove-btn {
|
||||
.alt-remove-btn {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1971,7 +1971,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-quantity {
|
||||
.alt-quantity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
@@ -2021,7 +2021,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-cart-summary {
|
||||
.alt-cart-summary {
|
||||
position: sticky;
|
||||
top: 80px;
|
||||
}
|
||||
@@ -2078,7 +2078,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-checkout-btn {
|
||||
.alt-checkout-btn {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
margin-top: 1.25rem;
|
||||
@@ -2108,10 +2108,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-terms-agreement {
|
||||
.alt-terms-agreement {
|
||||
margin-top: 1rem;
|
||||
|
||||
.novo-checkbox-container {
|
||||
.alt-checkbox-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
@@ -2127,7 +2127,7 @@
|
||||
height: 0;
|
||||
width: 0;
|
||||
|
||||
&:checked ~ .novo-checkmark {
|
||||
&:checked ~ .alt-checkmark {
|
||||
background: #10b981;
|
||||
border-color: #10b981;
|
||||
|
||||
@@ -2137,7 +2137,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-checkmark {
|
||||
.alt-checkmark {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
height: 20px;
|
||||
@@ -2162,7 +2162,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-terms-text {
|
||||
.alt-terms-text {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
|
||||
@@ -2182,23 +2182,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* novo Cart Responsive */
|
||||
/* alt Cart Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.novo-cart-content {
|
||||
.alt-cart-content {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
.novo-cart-summary {
|
||||
.alt-cart-summary {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.novo-cart-container {
|
||||
.alt-cart-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.novo-cart-header {
|
||||
.alt-cart-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
@@ -2208,22 +2208,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-cart-item {
|
||||
.alt-cart-item {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.novo-item-image {
|
||||
.alt-item-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.novo-item-details {
|
||||
.alt-item-details {
|
||||
.item-bottom {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0.75rem;
|
||||
|
||||
.novo-quantity {
|
||||
.alt-quantity {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@@ -2238,3 +2238,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
<app-dynamic-page-layout [model]="pageModel()" />
|
||||
} @else {
|
||||
<section class="home-empty section">
|
||||
<div class="page-container">No page configuration found.</div>
|
||||
<div class="page-container">{{ 'platform.emptyConfig' | translate }}</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// ========== SHARED ANIMATIONS ==========
|
||||
// ========== SHARED ANIMATIONS ==========
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
@@ -19,28 +19,28 @@
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// ========== NOVO HOME PAGE STYLES ==========
|
||||
.novo-home {
|
||||
// ========== alt HOME PAGE STYLES ==========
|
||||
.alt-home {
|
||||
min-height: calc(100vh - 200px);
|
||||
animation: fadeIn 0.6s ease;
|
||||
}
|
||||
|
||||
.novo-hero {
|
||||
.alt-hero {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||
padding: 6rem 2rem;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&.novo-hero-compact {
|
||||
&.alt-hero-compact {
|
||||
padding: 2.5rem 2rem;
|
||||
|
||||
.novo-hero-title {
|
||||
.alt-hero-title {
|
||||
font-size: 2.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.novo-hero-subtitle {
|
||||
.alt-hero-subtitle {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
@@ -69,14 +69,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-hero-content {
|
||||
.alt-hero-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.novo-hero-title {
|
||||
.alt-hero-title {
|
||||
font-size: 3.5rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
@@ -85,14 +85,14 @@
|
||||
animation: slideDown 0.8s ease;
|
||||
}
|
||||
|
||||
.novo-hero-subtitle {
|
||||
.alt-hero-subtitle {
|
||||
font-size: 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
margin: 0 0 2.5rem 0;
|
||||
animation: slideDown 0.8s ease 0.1s both;
|
||||
}
|
||||
|
||||
.novo-hero-btn {
|
||||
.alt-hero-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
@@ -121,12 +121,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-loading {
|
||||
.alt-loading {
|
||||
text-align: center;
|
||||
padding: 5rem 2rem;
|
||||
}
|
||||
|
||||
.novo-spinner {
|
||||
.alt-spinner {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 5px solid var(--bg-secondary);
|
||||
@@ -136,11 +136,11 @@
|
||||
margin: 0 auto 1.5rem;
|
||||
}
|
||||
|
||||
.novo-error {
|
||||
.alt-error {
|
||||
text-align: center;
|
||||
padding: 5rem 2rem;
|
||||
|
||||
.novo-error-icon {
|
||||
.alt-error-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
@@ -156,7 +156,7 @@
|
||||
margin: 0 0 2rem 0;
|
||||
}
|
||||
|
||||
.novo-retry-btn {
|
||||
.alt-retry-btn {
|
||||
padding: 0.875rem 2rem;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
@@ -174,13 +174,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-categories {
|
||||
.alt-categories {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 5rem 2rem;
|
||||
}
|
||||
|
||||
.novo-section-header {
|
||||
.alt-section-header {
|
||||
text-align: center;
|
||||
margin-bottom: 4rem;
|
||||
|
||||
@@ -198,13 +198,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-empty {
|
||||
.alt-empty {
|
||||
text-align: center;
|
||||
padding: 5rem 2rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-xl);
|
||||
|
||||
.novo-empty-icon {
|
||||
.alt-empty-icon {
|
||||
font-size: 5rem;
|
||||
opacity: 0.5;
|
||||
margin-bottom: 2rem;
|
||||
@@ -223,13 +223,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-categories-grid {
|
||||
.alt-categories-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.novo-category-card {
|
||||
.alt-category-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
@@ -245,23 +245,23 @@
|
||||
box-shadow: var(--shadow-lg);
|
||||
border-color: var(--primary-color);
|
||||
|
||||
.novo-category-image {
|
||||
.alt-category-image {
|
||||
img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.novo-category-placeholder {
|
||||
.alt-category-placeholder {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.novo-category-arrow {
|
||||
.alt-category-arrow {
|
||||
transform: translateX(8px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.novo-category-image {
|
||||
.alt-category-image {
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-secondary);
|
||||
@@ -277,7 +277,7 @@
|
||||
transition: transform 0.4s;
|
||||
}
|
||||
|
||||
.novo-category-placeholder {
|
||||
.alt-category-placeholder {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
@@ -295,7 +295,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-category-info {
|
||||
.alt-category-info {
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -308,7 +308,7 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.novo-category-count {
|
||||
.alt-category-count {
|
||||
font-weight: 600;
|
||||
font-size: clamp(11px, 1vw, 13px);
|
||||
color: var(--text-secondary);
|
||||
@@ -318,47 +318,47 @@
|
||||
}
|
||||
|
||||
@media (max-width: 968px) {
|
||||
.novo-hero {
|
||||
.alt-hero {
|
||||
padding: 4rem 2rem;
|
||||
}
|
||||
|
||||
.novo-hero-title {
|
||||
.alt-hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.novo-hero-subtitle {
|
||||
.alt-hero-subtitle {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.novo-categories-grid {
|
||||
.alt-categories-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.novo-hero {
|
||||
.alt-hero {
|
||||
padding: 3rem 1rem;
|
||||
}
|
||||
|
||||
.novo-hero-title {
|
||||
.alt-hero-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.novo-hero-subtitle {
|
||||
.alt-hero-subtitle {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.novo-hero-btn {
|
||||
.alt-hero-btn {
|
||||
padding: 0.875rem 2rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.novo-categories {
|
||||
.alt-categories {
|
||||
padding: 3rem 1rem;
|
||||
}
|
||||
|
||||
.novo-section-header {
|
||||
.alt-section-header {
|
||||
margin-bottom: 2.5rem;
|
||||
|
||||
h2 {
|
||||
@@ -370,24 +370,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.novo-categories-grid {
|
||||
.alt-categories-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.novo-category-card:hover {
|
||||
.alt-category-card:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== DEXAR REDESIGN 2026 STYLES ==========
|
||||
.dexar-home {
|
||||
// ========== platform REDESIGN 2026 STYLES ==========
|
||||
.platform-home {
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
// Hero Section
|
||||
.dexar-hero {
|
||||
.platform-hero {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
@@ -403,7 +403,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dexar-hero-overlay {
|
||||
.platform-hero-overlay {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -417,7 +417,7 @@
|
||||
padding: 0 56px;
|
||||
}
|
||||
|
||||
.dexar-hero-content {
|
||||
.platform-hero-content {
|
||||
max-width: 600px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -425,7 +425,7 @@
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
}
|
||||
|
||||
.dexar-hero-title {
|
||||
.platform-hero-title {
|
||||
font-weight: 500;
|
||||
font-size: 42px;
|
||||
color: var(--text-primary);
|
||||
@@ -434,7 +434,7 @@
|
||||
animation: fadeInUp 0.8s ease-out 0.1s both;
|
||||
}
|
||||
|
||||
.dexar-hero-subtitle {
|
||||
.platform-hero-subtitle {
|
||||
font-weight: 500;
|
||||
font-size: 24px;
|
||||
color: var(--text-primary);
|
||||
@@ -443,7 +443,7 @@
|
||||
animation: fadeInUp 0.8s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
.dexar-hero-tagline {
|
||||
.platform-hero-tagline {
|
||||
font-weight: 500;
|
||||
font-size: 24px;
|
||||
color: var(--text-primary);
|
||||
@@ -452,14 +452,14 @@
|
||||
animation: fadeInUp 0.8s ease-out 0.3s both;
|
||||
}
|
||||
|
||||
.dexar-hero-actions {
|
||||
.platform-hero-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
animation: fadeInUp 0.8s ease-out 0.4s both;
|
||||
}
|
||||
|
||||
.dexar-btn-primary {
|
||||
.platform-btn-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -488,7 +488,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dexar-btn-secondary {
|
||||
.platform-btn-secondary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -529,15 +529,15 @@
|
||||
}
|
||||
|
||||
// Loading & Error States
|
||||
.dexar-loading,
|
||||
.dexar-error {
|
||||
.platform-loading,
|
||||
.platform-error {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.dexar-spinner {
|
||||
.platform-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #f3f3f3;
|
||||
@@ -547,7 +547,7 @@
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
.dexar-error {
|
||||
.platform-error {
|
||||
button {
|
||||
margin-top: 20px;
|
||||
padding: 12px 28px;
|
||||
@@ -569,27 +569,27 @@
|
||||
}
|
||||
|
||||
// Categories Section
|
||||
.dexar-categories {
|
||||
.platform-categories {
|
||||
max-width: 1440px;
|
||||
margin: 50px auto;
|
||||
padding: 0 56px;
|
||||
}
|
||||
|
||||
.dexar-categories-title {
|
||||
.platform-categories-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 40px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dexar-empty-categories {
|
||||
.platform-empty-categories {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
|
||||
.dexar-empty-icon {
|
||||
.platform-empty-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.5;
|
||||
@@ -609,7 +609,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dexar-categories-grid {
|
||||
.platform-categories-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 30px;
|
||||
@@ -617,7 +617,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dexar-category-card {
|
||||
.platform-category-card {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -627,17 +627,17 @@
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
|
||||
.dexar-category-image {
|
||||
.platform-category-image {
|
||||
box-shadow: 0 6px 8px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.dexar-category-info {
|
||||
.platform-category-info {
|
||||
box-shadow: 0 6px 8px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dexar-category-image {
|
||||
.platform-category-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 4 / 3;
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -662,7 +662,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dexar-category-fallback {
|
||||
.platform-category-fallback {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@@ -674,7 +674,7 @@
|
||||
background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%);
|
||||
}
|
||||
|
||||
.dexar-category-info {
|
||||
.platform-category-info {
|
||||
width: 100%;
|
||||
border: 1px solid var(--border-color);
|
||||
border-top: none;
|
||||
@@ -689,7 +689,7 @@
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.dexar-category-name {
|
||||
.platform-category-name {
|
||||
font-weight: 600;
|
||||
font-size: clamp(14px, 1.4vw, 18px);
|
||||
color: var(--text-primary);
|
||||
@@ -704,7 +704,7 @@
|
||||
min-height: calc(2 * 1.3em);
|
||||
}
|
||||
|
||||
.dexar-category-count {
|
||||
.platform-category-count {
|
||||
font-weight: 600;
|
||||
font-size: clamp(11px, 1vw, 13px);
|
||||
color: var(--text-secondary);
|
||||
@@ -713,10 +713,10 @@
|
||||
}
|
||||
|
||||
// Wide category card (spans 2 columns)
|
||||
.dexar-category-card--wide {
|
||||
.platform-category-card--wide {
|
||||
grid-column: span 2;
|
||||
|
||||
.dexar-category-image {
|
||||
.platform-category-image {
|
||||
aspect-ratio: 8 / 3;
|
||||
|
||||
img {
|
||||
@@ -727,82 +727,82 @@
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 1200px) {
|
||||
.dexar-hero {
|
||||
.platform-hero {
|
||||
height: 380px;
|
||||
}
|
||||
|
||||
.dexar-hero-overlay {
|
||||
.platform-hero-overlay {
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
.dexar-hero-title {
|
||||
.platform-hero-title {
|
||||
font-size: 38px;
|
||||
}
|
||||
|
||||
.dexar-hero-subtitle,
|
||||
.dexar-hero-tagline {
|
||||
.platform-hero-subtitle,
|
||||
.platform-hero-tagline {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.dexar-categories {
|
||||
.platform-categories {
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
.dexar-categories-grid {
|
||||
.platform-categories-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.dexar-hero {
|
||||
.platform-hero {
|
||||
height: 340px;
|
||||
}
|
||||
|
||||
.dexar-hero-overlay {
|
||||
.platform-hero-overlay {
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
.dexar-hero-title {
|
||||
.platform-hero-title {
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
.dexar-hero-subtitle,
|
||||
.dexar-hero-tagline {
|
||||
.platform-hero-subtitle,
|
||||
.platform-hero-tagline {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.dexar-btn-primary,
|
||||
.dexar-btn-secondary {
|
||||
.platform-btn-primary,
|
||||
.platform-btn-secondary {
|
||||
height: 44px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.dexar-btn-primary {
|
||||
.platform-btn-primary {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.dexar-btn-secondary {
|
||||
.platform-btn-secondary {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.dexar-categories {
|
||||
.platform-categories {
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
.dexar-categories-grid {
|
||||
.platform-categories-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dexar-hero {
|
||||
.platform-hero {
|
||||
height: 320px;
|
||||
background-position: right center;
|
||||
}
|
||||
|
||||
.dexar-hero-overlay {
|
||||
.platform-hero-overlay {
|
||||
padding: 0 20px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
@@ -811,23 +811,23 @@
|
||||
);
|
||||
}
|
||||
|
||||
.dexar-hero-title {
|
||||
.platform-hero-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.dexar-hero-subtitle,
|
||||
.dexar-hero-tagline {
|
||||
.platform-hero-subtitle,
|
||||
.platform-hero-tagline {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.dexar-hero-actions {
|
||||
.platform-hero-actions {
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dexar-btn-primary,
|
||||
.dexar-btn-secondary {
|
||||
.platform-btn-primary,
|
||||
.platform-btn-secondary {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 180px;
|
||||
@@ -836,65 +836,65 @@
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.dexar-categories {
|
||||
.platform-categories {
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.dexar-categories-title {
|
||||
.platform-categories-title {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.dexar-categories-grid {
|
||||
.platform-categories-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.dexar-category-info {
|
||||
.platform-category-info {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.dexar-hero {
|
||||
.platform-hero {
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.dexar-hero-title {
|
||||
.platform-hero-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.dexar-hero-subtitle,
|
||||
.dexar-hero-tagline {
|
||||
.platform-hero-subtitle,
|
||||
.platform-hero-tagline {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.dexar-hero-actions {
|
||||
.platform-hero-actions {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dexar-btn-primary,
|
||||
.dexar-btn-secondary {
|
||||
.platform-btn-primary,
|
||||
.platform-btn-secondary {
|
||||
height: 38px;
|
||||
font-size: 13px;
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
.dexar-categories {
|
||||
.platform-categories {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.dexar-categories-grid {
|
||||
.platform-categories-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dexar-category-info {
|
||||
.platform-category-info {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.dexar-category-card:hover {
|
||||
.platform-category-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
@@ -921,3 +921,4 @@
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@ import { Router } from '@angular/router';
|
||||
import { DynamicPageLayoutComponent } from '../../layouts/containers/dynamic-page-layout.component';
|
||||
import { PageRenderModel } from '../../dynamic-renderer/page-renderer/page-renderer.model';
|
||||
import { WebsiteRuntimeFacade } from '../../facades/website/website-runtime.facade';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
standalone: true,
|
||||
imports: [DynamicPageLayoutComponent],
|
||||
imports: [DynamicPageLayoutComponent, TranslatePipe],
|
||||
templateUrl: './home.component.html',
|
||||
styleUrls: ['./home.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@use 'sass:color';
|
||||
@use 'sass:color';
|
||||
|
||||
// ========== DEXAR ITEM DETAIL - Redesigned 2026 ==========
|
||||
// ========== platform ITEM DETAIL - Redesigned 2026 ==========
|
||||
$dx-font: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
$dx-dark: #1e3c38;
|
||||
$dx-primary: #497671;
|
||||
@@ -310,8 +310,8 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
// Variant chips (colour/size) — shared between dexar and novo
|
||||
.dx-variants, .novo-variants {
|
||||
// Variant chips (colour/size) — shared between platform and alt
|
||||
.dx-variants, .alt-variants {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
@@ -374,7 +374,7 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.dx-attributes, .novo-attributes {
|
||||
.dx-attributes, .alt-attributes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 16px;
|
||||
@@ -400,7 +400,7 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.dx-delivery, .novo-delivery {
|
||||
.dx-delivery, .alt-delivery {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
@@ -410,20 +410,20 @@ $dx-card-bg: #f5f3f9;
|
||||
background: #f8fafa;
|
||||
}
|
||||
|
||||
.dx-delivery-title, .novo-delivery-title {
|
||||
.dx-delivery-title, .alt-delivery-title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: $dx-dark;
|
||||
}
|
||||
|
||||
.dx-delivery-list, .novo-delivery-list {
|
||||
.dx-delivery-list, .alt-delivery-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dx-delivery-option, .novo-delivery-option {
|
||||
.dx-delivery-option, .alt-delivery-option {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
@@ -433,7 +433,7 @@ $dx-card-bg: #f5f3f9;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.dx-delivery-option-main, .novo-delivery-option-main {
|
||||
.dx-delivery-option-main, .alt-delivery-option-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
@@ -441,14 +441,14 @@ $dx-card-bg: #f5f3f9;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.dx-delivery-price, .novo-delivery-price {
|
||||
.dx-delivery-price, .alt-delivery-price {
|
||||
flex-shrink: 0;
|
||||
font-weight: 700;
|
||||
color: $dx-primary;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.dx-delivery-chip, .novo-delivery-chip {
|
||||
.dx-delivery-chip, .alt-delivery-chip {
|
||||
display: inline-flex;
|
||||
align-self: flex-start;
|
||||
padding: 6px 12px;
|
||||
@@ -810,9 +810,9 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== DEXAR RESPONSIVE ==========
|
||||
// ========== platform RESPONSIVE ==========
|
||||
|
||||
// Large desktop — constrain gallery height
|
||||
// Large desktop — constrain gallery height
|
||||
@media (min-width: 1201px) {
|
||||
.dx-main-photo {
|
||||
max-height: 560px;
|
||||
@@ -1099,20 +1099,20 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== novo ITEM DETAIL STYLES ==========
|
||||
.novo-item-container {
|
||||
// ========== alt ITEM DETAIL STYLES ==========
|
||||
.alt-item-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.novo-loading,
|
||||
.novo-error {
|
||||
.alt-loading,
|
||||
.alt-error {
|
||||
text-align: center;
|
||||
padding: 4rem 1rem;
|
||||
}
|
||||
|
||||
.novo-spinner {
|
||||
.alt-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid var(--border-color);
|
||||
@@ -1122,7 +1122,7 @@ $dx-card-bg: #f5f3f9;
|
||||
margin: 0 auto 1.5rem;
|
||||
}
|
||||
|
||||
.novo-back-link {
|
||||
.alt-back-link {
|
||||
display: inline-block;
|
||||
margin-top: 1.5rem;
|
||||
padding: 0.75rem 2rem;
|
||||
@@ -1140,15 +1140,15 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-item-content {
|
||||
.alt-item-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 3rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.novo-gallery {
|
||||
.novo-main-photo {
|
||||
.alt-gallery {
|
||||
.alt-main-photo {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
background: var(--bg-secondary);
|
||||
@@ -1165,7 +1165,7 @@ $dx-card-bg: #f5f3f9;
|
||||
background: white;
|
||||
}
|
||||
|
||||
&.novo-no-image {
|
||||
&.alt-no-image {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -1183,13 +1183,13 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-thumbnails {
|
||||
.alt-thumbnails {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.novo-thumb {
|
||||
.alt-thumb {
|
||||
aspect-ratio: 1;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
@@ -1235,8 +1235,8 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-info {
|
||||
.novo-title {
|
||||
.alt-info {
|
||||
.alt-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
@@ -1244,7 +1244,7 @@ $dx-card-bg: #f5f3f9;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.novo-rating {
|
||||
.alt-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
@@ -1266,7 +1266,7 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-price-block {
|
||||
.alt-price-block {
|
||||
background: var(--bg-secondary);
|
||||
padding: 1.5rem;
|
||||
border-radius: var(--radius-lg);
|
||||
@@ -1302,7 +1302,7 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-stock {
|
||||
.alt-stock {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
@@ -1347,7 +1347,7 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-add-cart {
|
||||
.alt-add-cart {
|
||||
width: 100%;
|
||||
padding: 1.25rem 2rem;
|
||||
background: var(--primary-color);
|
||||
@@ -1375,7 +1375,7 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-description {
|
||||
.alt-description {
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
@@ -1392,7 +1392,7 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-reviews {
|
||||
.alt-reviews {
|
||||
margin-top: 3rem;
|
||||
padding-top: 3rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
@@ -1404,12 +1404,12 @@ $dx-card-bg: #f5f3f9;
|
||||
margin: 0 0 2rem 0;
|
||||
}
|
||||
|
||||
.novo-reviews-list {
|
||||
.alt-reviews-list {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.novo-review-card {
|
||||
.alt-review-card {
|
||||
background: white;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
@@ -1461,7 +1461,7 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-no-reviews {
|
||||
.alt-no-reviews {
|
||||
text-align: center;
|
||||
padding: 3rem 2rem;
|
||||
color: var(--text-secondary);
|
||||
@@ -1471,8 +1471,8 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
/* novo Review Form */
|
||||
.novo-review-form {
|
||||
/* alt Review Form */
|
||||
.alt-review-form {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
@@ -1486,7 +1486,7 @@ $dx-card-bg: #f5f3f9;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
|
||||
.novo-rating-input {
|
||||
.alt-rating-input {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
label {
|
||||
@@ -1497,11 +1497,11 @@ $dx-card-bg: #f5f3f9;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.novo-star-selector {
|
||||
.alt-star-selector {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
.novo-star {
|
||||
.alt-star {
|
||||
font-size: 2rem;
|
||||
color: #d1d5db;
|
||||
cursor: pointer;
|
||||
@@ -1520,7 +1520,7 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-textarea {
|
||||
.alt-textarea {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -1545,14 +1545,14 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-form-actions {
|
||||
.alt-form-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.novo-anonymous-toggle {
|
||||
.alt-anonymous-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
@@ -1571,13 +1571,13 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-username-preview {
|
||||
.alt-username-preview {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.novo-submit-review-btn {
|
||||
.alt-submit-review-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
@@ -1612,7 +1612,7 @@ $dx-card-bg: #f5f3f9;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.novo-spinner-small {
|
||||
.alt-spinner-small {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
@@ -1623,7 +1623,7 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-status-message {
|
||||
.alt-status-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
@@ -1673,40 +1673,40 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== NOVO RESPONSIVE ==========
|
||||
// ========== alt RESPONSIVE ==========
|
||||
|
||||
// Tablet portrait
|
||||
@media (max-width: 968px) {
|
||||
.novo-item-content {
|
||||
.alt-item-content {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.novo-gallery {
|
||||
.alt-gallery {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.novo-info .novo-title {
|
||||
.alt-info .alt-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.novo-info .novo-price-block .current-price {
|
||||
.alt-info .alt-price-block .current-price {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.novo-info .novo-add-cart {
|
||||
.alt-info .alt-add-cart {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.novo-review-form {
|
||||
.alt-review-form {
|
||||
padding: 1.5rem;
|
||||
|
||||
.novo-form-actions {
|
||||
.alt-form-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
.novo-submit-review-btn {
|
||||
.alt-submit-review-btn {
|
||||
margin-left: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -1716,41 +1716,41 @@ $dx-card-bg: #f5f3f9;
|
||||
|
||||
// Mobile
|
||||
@media (max-width: 768px) {
|
||||
.novo-item-container {
|
||||
.alt-item-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.novo-item-content {
|
||||
.alt-item-content {
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.novo-gallery {
|
||||
.alt-gallery {
|
||||
max-width: 100%;
|
||||
|
||||
.novo-main-photo {
|
||||
.alt-main-photo {
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.novo-thumbnails {
|
||||
.alt-thumbnails {
|
||||
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-info {
|
||||
.novo-title {
|
||||
.alt-info {
|
||||
.alt-title {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
.novo-rating {
|
||||
.alt-rating {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.novo-price-block {
|
||||
.alt-price-block {
|
||||
padding: 1rem;
|
||||
|
||||
.current-price {
|
||||
@@ -1762,26 +1762,26 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-stock {
|
||||
.alt-stock {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.novo-add-cart {
|
||||
.alt-add-cart {
|
||||
padding: 1rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.novo-description {
|
||||
.alt-description {
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.novo-specs-table {
|
||||
.alt-specs-table {
|
||||
.spec-key {
|
||||
white-space: normal;
|
||||
width: auto;
|
||||
@@ -1793,7 +1793,7 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-reviews {
|
||||
.alt-reviews {
|
||||
margin-top: 2rem;
|
||||
padding-top: 2rem;
|
||||
|
||||
@@ -1803,11 +1803,11 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-review-card {
|
||||
.alt-review-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.novo-review-form {
|
||||
.alt-review-form {
|
||||
padding: 1.25rem;
|
||||
|
||||
h3 {
|
||||
@@ -1818,32 +1818,32 @@ $dx-card-bg: #f5f3f9;
|
||||
|
||||
// Small mobile
|
||||
@media (max-width: 480px) {
|
||||
.novo-item-container {
|
||||
.alt-item-container {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.novo-item-content {
|
||||
.alt-item-content {
|
||||
gap: 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.novo-gallery {
|
||||
.novo-thumbnails {
|
||||
.alt-gallery {
|
||||
.alt-thumbnails {
|
||||
grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.novo-main-photo {
|
||||
.alt-main-photo {
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
}
|
||||
|
||||
.novo-info {
|
||||
.novo-title {
|
||||
.alt-info {
|
||||
.alt-title {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.novo-price-block {
|
||||
.alt-price-block {
|
||||
padding: 0.75rem;
|
||||
|
||||
.current-price {
|
||||
@@ -1851,36 +1851,36 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-stock {
|
||||
.alt-stock {
|
||||
padding: 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.novo-add-cart {
|
||||
.alt-add-cart {
|
||||
padding: 0.85rem 1rem;
|
||||
font-size: 0.95rem;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
}
|
||||
|
||||
.novo-review-form {
|
||||
.alt-review-form {
|
||||
padding: 1rem;
|
||||
|
||||
.novo-rating-input {
|
||||
.novo-star-selector {
|
||||
.novo-star {
|
||||
.alt-rating-input {
|
||||
.alt-star-selector {
|
||||
.alt-star {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.novo-textarea {
|
||||
.alt-textarea {
|
||||
padding: 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-review-card {
|
||||
.alt-review-card {
|
||||
padding: 0.75rem;
|
||||
|
||||
.review-header {
|
||||
@@ -1897,7 +1897,7 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
}
|
||||
|
||||
.novo-specs-table {
|
||||
.alt-specs-table {
|
||||
td {
|
||||
padding: 6px 8px;
|
||||
font-size: 0.8rem;
|
||||
@@ -1924,7 +1924,7 @@ $dx-card-bg: #f5f3f9;
|
||||
// ========== BADGES, TAGS & SPECS (shared) ==========
|
||||
|
||||
// Badges
|
||||
.novo-badges, .dx-badges {
|
||||
.alt-badges, .dx-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
@@ -1952,7 +1952,7 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
|
||||
// Tags
|
||||
.novo-tags, .dx-tags {
|
||||
.alt-tags, .dx-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
@@ -1970,7 +1970,7 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
|
||||
// Specs table
|
||||
.novo-specs-table, .dx-specs-table {
|
||||
.alt-specs-table, .dx-specs-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 12px 0;
|
||||
@@ -1999,7 +1999,7 @@ $dx-card-bg: #f5f3f9;
|
||||
}
|
||||
|
||||
// Simple description
|
||||
.novo-simple-desc, .dx-simple-desc {
|
||||
.alt-simple-desc, .dx-simple-desc {
|
||||
font-size: 0.95rem;
|
||||
color: #697777;
|
||||
line-height: 1.6;
|
||||
@@ -2012,3 +2012,4 @@ $dx-card-bg: #f5f3f9;
|
||||
color: #697777;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,21 +3,22 @@ import { Router } from '@angular/router';
|
||||
import { DynamicPageLayoutComponent } from '../../layouts/containers/dynamic-page-layout.component';
|
||||
import { PageRenderModel } from '../../dynamic-renderer/page-renderer/page-renderer.model';
|
||||
import { WebsiteRuntimeFacade } from '../../facades/website/website-runtime.facade';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-platform-home',
|
||||
standalone: true,
|
||||
imports: [DynamicPageLayoutComponent],
|
||||
imports: [DynamicPageLayoutComponent, TranslatePipe],
|
||||
template: `
|
||||
@if (loading()) {
|
||||
<section class="platform-loading section">
|
||||
<div class="page-container">Loading platform page...</div>
|
||||
<div class="page-container">{{ 'platform.loadingPage' | translate }}</div>
|
||||
</section>
|
||||
} @else if (model()) {
|
||||
<app-dynamic-page-layout [model]="model()" />
|
||||
} @else {
|
||||
<section class="platform-empty section">
|
||||
<div class="page-container">No page configuration found.</div>
|
||||
<div class="page-container">{{ 'platform.emptyConfig' | translate }}</div>
|
||||
</section>
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -4,6 +4,19 @@ export interface WidgetActionConfig {
|
||||
payload?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface WidgetAnimationConfig {
|
||||
name?: string;
|
||||
duration?: string;
|
||||
delay?: string;
|
||||
timingFunction?: string;
|
||||
}
|
||||
|
||||
export interface WidgetPermissionsConfig {
|
||||
requireAuthenticated?: boolean;
|
||||
roles?: string[];
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
export interface WidgetVisibilityConfig {
|
||||
desktop?: boolean;
|
||||
tablet?: boolean;
|
||||
@@ -14,9 +27,14 @@ export interface WidgetConfig {
|
||||
id: string;
|
||||
type: string;
|
||||
version: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
order?: number;
|
||||
padding?: string;
|
||||
visibility?: WidgetVisibilityConfig;
|
||||
animation?: WidgetAnimationConfig;
|
||||
style?: Record<string, string>;
|
||||
permissions?: WidgetPermissionsConfig;
|
||||
props: Record<string, unknown>;
|
||||
actions?: Record<string, WidgetActionConfig>;
|
||||
featureFlag?: string;
|
||||
|
||||
@@ -3,6 +3,18 @@ import { ThemeCssVariables } from '../tokens/theme-css-variable.model';
|
||||
import { THEME_VARIABLE_MAP } from '../tokens/theme-variable-map';
|
||||
|
||||
export function mapThemeConfigToCssVariables(theme: ThemeConfig): ThemeCssVariables {
|
||||
const spacingScale = Array.isArray(theme.spacing.scale) && theme.spacing.scale.length > 0
|
||||
? theme.spacing.scale
|
||||
: [0.25, 0.5, 1, 1.5, 2];
|
||||
|
||||
const spacingValues = spacingScale
|
||||
.slice(0, 5)
|
||||
.map(multiplier => `${Math.max(0, Number(multiplier) || 0) * theme.spacing.unit}px`);
|
||||
|
||||
while (spacingValues.length < 5) {
|
||||
spacingValues.push(`${theme.spacing.unit}px`);
|
||||
}
|
||||
|
||||
const vars: ThemeCssVariables = {
|
||||
[THEME_VARIABLE_MAP.primary]: theme.palette.primary,
|
||||
[THEME_VARIABLE_MAP.secondary]: theme.palette.secondary,
|
||||
@@ -16,6 +28,14 @@ export function mapThemeConfigToCssVariables(theme: ThemeConfig): ThemeCssVariab
|
||||
[THEME_VARIABLE_MAP.backgroundPrimary]: theme.palette.backgroundPrimary,
|
||||
[THEME_VARIABLE_MAP.backgroundSecondary]: theme.palette.backgroundSecondary,
|
||||
[THEME_VARIABLE_MAP.border]: theme.palette.border,
|
||||
[THEME_VARIABLE_MAP.spaceXs]: spacingValues[0],
|
||||
[THEME_VARIABLE_MAP.spaceSm]: spacingValues[1],
|
||||
[THEME_VARIABLE_MAP.spaceMd]: spacingValues[2],
|
||||
[THEME_VARIABLE_MAP.spaceLg]: spacingValues[3],
|
||||
[THEME_VARIABLE_MAP.spaceXl]: spacingValues[4],
|
||||
[THEME_VARIABLE_MAP.transitionFast]: '120ms ease',
|
||||
[THEME_VARIABLE_MAP.transitionNormal]: '180ms ease',
|
||||
[THEME_VARIABLE_MAP.transitionSlow]: '300ms ease',
|
||||
'--app-font-family': theme.typography.primaryFontFamily,
|
||||
'--app-font-size': `${theme.typography.baseFontSize}px`,
|
||||
'--app-spacing-unit': `${theme.spacing.unit}px`
|
||||
|
||||
@@ -10,5 +10,13 @@ export const THEME_VARIABLE_MAP = {
|
||||
textSecondary: '--text-secondary',
|
||||
backgroundPrimary: '--bg-primary',
|
||||
backgroundSecondary: '--bg-secondary',
|
||||
border: '--border-color'
|
||||
border: '--border-color',
|
||||
spaceXs: '--space-xs',
|
||||
spaceSm: '--space-sm',
|
||||
spaceMd: '--space-md',
|
||||
spaceLg: '--space-lg',
|
||||
spaceXl: '--space-xl',
|
||||
transitionFast: '--transition-fast',
|
||||
transitionNormal: '--transition-normal',
|
||||
transitionSlow: '--transition-slow'
|
||||
} as const;
|
||||
|
||||
@@ -8,6 +8,16 @@ export interface WidgetSettingsSchema {
|
||||
required?: string[];
|
||||
}
|
||||
|
||||
export interface WidgetMetadataSupport {
|
||||
title: boolean;
|
||||
subtitle: boolean;
|
||||
visibility: boolean;
|
||||
layout: boolean;
|
||||
animation: boolean;
|
||||
style: boolean;
|
||||
permissions: boolean;
|
||||
}
|
||||
|
||||
export interface WidgetManifestEntry {
|
||||
type: string;
|
||||
version: string;
|
||||
@@ -16,6 +26,7 @@ export interface WidgetManifestEntry {
|
||||
supportedDataSources: WidgetDataSourceName[];
|
||||
settingsSchema: WidgetSettingsSchema;
|
||||
defaultSettings: Record<string, unknown>;
|
||||
metadataSupport?: WidgetMetadataSupport;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,8 +50,31 @@ export class DataSourceResolverService {
|
||||
}
|
||||
|
||||
private resolveSettings(definition: WidgetManifestEntry | undefined, widget: WidgetConfig): Record<string, unknown> {
|
||||
const metadataSettings: Record<string, unknown> = {};
|
||||
|
||||
if (widget.title != null) {
|
||||
metadataSettings['title'] = widget.title;
|
||||
}
|
||||
|
||||
if (widget.subtitle != null) {
|
||||
metadataSettings['subtitle'] = widget.subtitle;
|
||||
}
|
||||
|
||||
if (widget.animation != null) {
|
||||
metadataSettings['animation'] = widget.animation;
|
||||
}
|
||||
|
||||
if (widget.style != null) {
|
||||
metadataSettings['style'] = widget.style;
|
||||
}
|
||||
|
||||
if (widget.permissions != null) {
|
||||
metadataSettings['permissions'] = widget.permissions;
|
||||
}
|
||||
|
||||
return {
|
||||
...(definition?.defaultSettings ?? {}),
|
||||
...metadataSettings,
|
||||
...(widget.props ?? {})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, Input, inject } from '@angular/core';
|
||||
import { SectionConfig } from '../../shared/models/config';
|
||||
import { RecentlyViewedStripComponent } from '../../features/website/user-experience/components/recently-viewed-strip/recently-viewed-strip.component';
|
||||
import { TranslateService } from '../../i18n/translate.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recently-viewed-widget',
|
||||
@@ -16,12 +17,14 @@ import { RecentlyViewedStripComponent } from '../../features/website/user-experi
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class RecentlyViewedWidgetComponent {
|
||||
private readonly translate = inject(TranslateService);
|
||||
|
||||
@Input() section: SectionConfig | null = null;
|
||||
@Input() data: { settings?: Record<string, unknown> } | null = null;
|
||||
|
||||
get title(): string {
|
||||
const raw = this.data?.settings?.['title'];
|
||||
return typeof raw === 'string' && raw.trim().length > 0 ? raw : 'Recently Viewed';
|
||||
return typeof raw === 'string' && raw.trim().length > 0 ? raw : this.translate.t('ux.recentlyViewedTitle');
|
||||
}
|
||||
|
||||
get maxItems(): number {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-unknown-widget',
|
||||
standalone: true,
|
||||
imports: [TranslatePipe],
|
||||
template: `
|
||||
<section class="unknown-widget" role="status" aria-live="polite">
|
||||
<strong>Widget unavailable</strong>
|
||||
<strong>{{ 'widgets.unavailable' | translate }}</strong>
|
||||
<p>{{ widgetType }}@{{ widgetVersion }}</p>
|
||||
</section>
|
||||
`,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"subtitle": "Configuration-driven multi-tenant commerce",
|
||||
"ctaLabel": "Start Shopping"
|
||||
},
|
||||
"metadataSupport": {"title": true, "subtitle": true, "visibility": true, "layout": true, "animation": true, "style": true, "permissions": true},
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -41,6 +42,7 @@
|
||||
"source": "root",
|
||||
"emptyMessage": "No categories available"
|
||||
},
|
||||
"metadataSupport": {"title": true, "subtitle": true, "visibility": true, "layout": true, "animation": true, "style": true, "permissions": true},
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -67,6 +69,7 @@
|
||||
"count": 8,
|
||||
"actionLabel": "Select"
|
||||
},
|
||||
"metadataSupport": {"title": true, "subtitle": true, "visibility": true, "layout": true, "animation": true, "style": true, "permissions": true},
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -88,6 +91,7 @@
|
||||
"source": "featured",
|
||||
"count": 8
|
||||
},
|
||||
"metadataSupport": {"title": true, "subtitle": true, "visibility": true, "layout": true, "animation": true, "style": true, "permissions": true},
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -106,6 +110,7 @@
|
||||
"defaultSettings": {
|
||||
"links": []
|
||||
},
|
||||
"metadataSupport": {"title": true, "subtitle": true, "visibility": true, "layout": true, "animation": true, "style": true, "permissions": true},
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -123,6 +128,7 @@
|
||||
"defaultSettings": {
|
||||
"links": []
|
||||
},
|
||||
"metadataSupport": {"title": true, "subtitle": true, "visibility": true, "layout": true, "animation": true, "style": true, "permissions": true},
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -144,6 +150,7 @@
|
||||
"maxItems": 8,
|
||||
"showClear": false
|
||||
},
|
||||
"metadataSupport": {"title": true, "subtitle": true, "visibility": true, "layout": true, "animation": true, "style": true, "permissions": true},
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -163,6 +170,7 @@
|
||||
}
|
||||
},
|
||||
"defaultSettings": {},
|
||||
"metadataSupport": {"title": true, "subtitle": true, "visibility": true, "layout": true, "animation": true, "style": true, "permissions": true},
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
@@ -180,6 +188,7 @@
|
||||
"defaultSettings": {
|
||||
"html": ""
|
||||
},
|
||||
"metadataSupport": {"title": true, "subtitle": true, "visibility": true, "layout": true, "animation": true, "style": true, "permissions": true},
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
@@ -198,7 +207,9 @@
|
||||
"defaultSettings": {
|
||||
"logos": []
|
||||
},
|
||||
"metadataSupport": {"title": true, "subtitle": true, "visibility": true, "layout": true, "animation": true, "style": true, "permissions": true},
|
||||
"enabled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -36,11 +36,21 @@
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
--shadow-lg: 0 12px 32px rgba(73, 118, 113, 0.2);
|
||||
|
||||
--space-xs: 4px;
|
||||
--space-sm: 8px;
|
||||
--space-md: 16px;
|
||||
--space-lg: 24px;
|
||||
--space-xl: 32px;
|
||||
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 13px;
|
||||
--radius-xl: 22px;
|
||||
|
||||
--transition-fast: 120ms ease;
|
||||
--transition-normal: 180ms ease;
|
||||
--transition-slow: 300ms ease;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -69,7 +79,7 @@ body {
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
|
||||
&.dexar-menu-open {
|
||||
&.platform-menu-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@@ -93,7 +103,7 @@ canvas {
|
||||
|
||||
/* Smooth Transitions */
|
||||
a, button, input, textarea, select {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: all var(--transition-slow);
|
||||
}
|
||||
|
||||
/* Responsive Typography */
|
||||
@@ -134,7 +144,7 @@ a, button, input, textarea, select {
|
||||
.page-container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 16px;
|
||||
padding: var(--space-lg) var(--space-md);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -142,7 +152,7 @@ a, button, input, textarea, select {
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 16px;
|
||||
padding: var(--space-lg) var(--space-md);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -154,7 +164,7 @@ a, button, input, textarea, select {
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.card {
|
||||
@@ -162,7 +172,7 @@ a, button, input, textarea, select {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: transform 180ms ease, box-shadow 180ms ease;
|
||||
transition: transform var(--transition-normal), box-shadow var(--transition-normal);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
@@ -173,7 +183,7 @@ a, button, input, textarea, select {
|
||||
.product-card,
|
||||
.item-card,
|
||||
.catalog-product-card {
|
||||
transition: transform 180ms ease, box-shadow 180ms ease;
|
||||
transition: transform var(--transition-normal), box-shadow var(--transition-normal);
|
||||
}
|
||||
|
||||
.product-card:hover,
|
||||
@@ -194,7 +204,7 @@ a, button, input, textarea, select {
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: transform 180ms ease, box-shadow 180ms ease, background-color 180ms ease, color 180ms ease, border-color 180ms ease;
|
||||
transition: transform var(--transition-normal), box-shadow var(--transition-normal), background-color var(--transition-normal), color var(--transition-normal), border-color var(--transition-normal);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
@@ -353,3 +363,5 @@ a, button, input, textarea, select {
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user