Merge branch 'back-office-integration'

# Conflicts:
#	src/app/pages/cart/cart.component.ts
#	src/app/pages/category/category.component.html
#	src/app/pages/category/category.component.ts
#	src/app/pages/item-detail/item-detail.component.html
#	src/app/pages/item-detail/item-detail.component.ts
#	src/app/pages/legal/company-details/en/company-details-en.component.html
#	src/app/pages/legal/company-details/hy/company-details-hy.component.html
#	src/app/pages/legal/company-details/ru/company-details-ru.component.html
#	src/app/pages/legal/public-offer/en/public-offer-en.component.html
#	src/app/pages/legal/public-offer/ru/public-offer-ru.component.html
#	src/app/pages/search/search.component.ts
#	src/app/services/api.service.ts
This commit is contained in:
sdarbinyan
2026-03-24 00:18:13 +04:00
76 changed files with 6828 additions and 2331 deletions

View File

@@ -15,22 +15,21 @@
</div>
}
@if (item(); as item) {
@if (!loading()) {
@if (item() && !loading()) {
<div class="novo-item-content">
<div class="novo-gallery">
@if (item.photos && item.photos.length > 0) {
@if (item()?.photos && item()!.photos!.length > 0) {
<div class="novo-main-photo">
@if (item.photos[selectedPhotoIndex()]?.video) {
<video [src]="item.photos[selectedPhotoIndex()].url" controls></video>
@if (item()!.photos![selectedPhotoIndex()]?.video) {
<video [src]="item()!.photos![selectedPhotoIndex()].url" controls></video>
} @else {
<img [src]="item.photos[selectedPhotoIndex()].url" [alt]="item.name" />
<img [src]="item()!.photos![selectedPhotoIndex()].url" [alt]="item()!.name" />
}
</div>
@if (item.photos.length > 1) {
@if (item()!.photos!.length > 1) {
<div class="novo-thumbnails">
@for (photo of item.photos; track $index) {
@for (photo of item()!.photos!; track $index) {
<div
class="novo-thumb"
[class.active]="selectedPhotoIndex() === $index"
@@ -56,34 +55,81 @@
</div>
<div class="novo-info">
<h1 class="novo-title">{{ item.name }}</h1>
<h1 class="novo-title">{{ getItemName() }}</h1>
@if (item()!.badges && item()!.badges!.length > 0) {
<div class="novo-badges">
@for (badge of item()!.badges!; track badge) {
<span class="item-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
}
</div>
}
@if (item()!.tags && item()!.tags!.length > 0) {
<div class="novo-tags">
@for (tag of item()!.tags!; track tag) {
<span class="item-tag">#{{ tag }}</span>
}
</div>
}
<div class="novo-rating">
<span class="stars">{{ getRatingStars(item.rating) }}</span>
<span class="value">{{ item.rating }}</span>
<span class="reviews">({{ item.callbacks?.length || 0 }})</span>
<span class="stars">{{ getRatingStars(item()!.rating) }}</span>
<span class="value">{{ item()!.rating }}</span>
<span class="reviews">({{ item()!.callbacks?.length || 0 }})</span>
</div>
<div class="novo-price-block">
@if (item.discount > 0) {
@if (item()!.discount > 0) {
<div class="price-row">
<span class="old-price">{{ item.price }} {{ item.currency }}</span>
<span class="discount-badge">-{{ item.discount }}%</span>
<span class="old-price">{{ item()!.price }} {{ item()!.currency }}</span>
<span class="discount-badge">-{{ item()!.discount }}%</span>
</div>
<div class="current-price">{{ getDiscountedPrice() | number:'1.2-2' }} {{ item.currency }}</div>
<div class="current-price">{{ getDiscountedPrice() | number:'1.2-2' }} {{ item()!.currency }}</div>
} @else {
<div class="current-price">{{ item.price }} {{ item.currency }}</div>
<div class="current-price">{{ item()!.price }} {{ item()!.currency }}</div>
}
</div>
<div class="novo-stock">
<span class="stock-label">{{ 'itemDetail.stock' | translate }}</span>
<div class="stock-indicator" [class.high]="item.remainings === 'high'" [class.medium]="item.remainings === 'medium'" [class.low]="item.remainings === 'low'">
<div class="stock-indicator" [class]="getStockClass()">
<span class="dot"></span>
{{ item.remainings === 'high' ? ('itemDetail.inStock' | translate) : item.remainings === 'medium' ? ('itemDetail.mediumStock' | translate) : ('itemDetail.lowStock' | translate) }}
{{ getStockLabel() }}
</div>
@if (item()!.quantity != null) {
<span class="stock-qty">({{ item()!.quantity }} шт.)</span>
}
</div>
@if (item()!.colour || item()!.size) {
<div class="novo-variants">
@if (item()!.colour) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.colour' | translate }}:</span>
<span class="variant-chip colour-chip">{{ item()!.colour }}</span>
</div>
}
@if (item()!.size) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.size' | translate }}:</span>
<span class="variant-chip size-chip">{{ item()!.size }}</span>
</div>
}
</div>
}
@if (item()!.attributes && item()!.attributes!.length > 0) {
<div class="novo-attributes">
@for (attr of item()!.attributes!; track attr.key) {
<div class="attribute-row">
<span class="attribute-key">{{ attr.key }}</span>
<span class="attribute-value">{{ attr.value }}</span>
</div>
}
</div>
}
<button class="novo-add-cart" (click)="addToCart()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="9" cy="21" r="1"></circle>
@@ -94,14 +140,32 @@
</button>
<div class="novo-description">
<h3>{{ 'itemDetail.description' | translate }}</h3>
<div [innerHTML]="getSafeHtml(item.description)"></div>
@if (getSimpleDescription()) {
<p class="novo-simple-desc">{{ getSimpleDescription() }}</p>
}
@if (hasDescriptionFields()) {
<h3>{{ 'itemDetail.specifications' | translate }}</h3>
<table class="novo-specs-table">
<tbody>
@for (field of getTranslatedDescriptionFields(); track field.key) {
<tr>
<td class="spec-key">{{ field.key }}</td>
<td class="spec-value">{{ field.value }}</td>
</tr>
}
</tbody>
</table>
} @else {
<h3>{{ 'itemDetail.description' | translate }}</h3>
<div [innerHTML]="getSafeHtml(item()!.description)"></div>
}
</div>
</div>
</div>
<div class="novo-reviews">
<h2>{{ 'itemDetail.reviews' | translate }} ({{ item.callbacks?.length || 0 }})</h2>
<h2>{{ 'itemDetail.reviews' | translate }} ({{ item()!.callbacks?.length || 0 }})</h2>
<!-- novo Review Form -->
<div class="novo-review-form">
@@ -170,8 +234,8 @@
</div>
<div class="novo-reviews-list">
@if (item.callbacks && item.callbacks.length > 0) {
@for (review of item.callbacks; track $index) {
@if (item()!.callbacks && item()!.callbacks!.length > 0) {
@for (review of item()!.callbacks!; track review.userID) {
<div class="novo-review-card">
<div class="review-header">
<div class="reviewer-info">
@@ -190,7 +254,6 @@
}
</div>
</div>
}
}
</div>
} @else {
@@ -210,14 +273,13 @@
</div>
}
@if (item(); as item) {
@if (!loading()) {
@if (item() && !loading()) {
<div class="dx-item-content">
<!-- Gallery: thumbnails left + main photo -->
<div class="dx-gallery">
@if (item.photos && item.photos.length > 0) {
@if (item()?.photos && item()!.photos!.length > 0) {
<div class="dx-thumbnails">
@for (photo of item.photos; track $index) {
@for (photo of item()!.photos!; track $index) {
<div
class="dx-thumb"
[class.active]="selectedPhotoIndex() === $index"
@@ -231,11 +293,11 @@
</div>
}
<div class="dx-main-photo">
@if (item.photos && item.photos.length > 0) {
@if (item.photos[selectedPhotoIndex()]?.video) {
<video [src]="item.photos[selectedPhotoIndex()].url" controls></video>
@if (item()?.photos && item()!.photos!.length > 0) {
@if (item()!.photos![selectedPhotoIndex()]?.video) {
<video [src]="item()!.photos![selectedPhotoIndex()].url" controls></video>
} @else {
<img [src]="item.photos[selectedPhotoIndex()].url" [alt]="item.name" fetchpriority="high" decoding="async" />
<img [src]="item()!.photos![selectedPhotoIndex()].url" [alt]="item()!.name" fetchpriority="high" decoding="async" />
}
} @else {
<div class="dx-no-image">
@@ -252,43 +314,87 @@
<!-- Item Info -->
<div class="dx-info">
<h1 class="dx-title">{{ item.name }}</h1>
<h1 class="dx-title">{{ getItemName() }}</h1>
@if (item()!.badges && item()!.badges!.length > 0) {
<div class="dx-badges">
@for (badge of item()!.badges!; track badge) {
<span class="item-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
}
</div>
}
@if (item()!.tags && item()!.tags!.length > 0) {
<div class="dx-tags">
@for (tag of item()!.tags!; track tag) {
<span class="item-tag">#{{ tag }}</span>
}
</div>
}
<div class="dx-rating">
<div class="dx-stars">
@for (star of [1, 2, 3, 4, 5]; track star) {
<svg width="18" height="18" viewBox="0 0 24 24" [attr.fill]="star <= item.rating ? '#497671' : 'none'" [attr.stroke]="star <= item.rating ? '#497671' : '#a1b4b5'" stroke-width="2">
<svg width="18" height="18" viewBox="0 0 24 24" [attr.fill]="star <= item()!.rating ? '#497671' : 'none'" [attr.stroke]="star <= item()!.rating ? '#497671' : '#a1b4b5'" stroke-width="2">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg>
}
</div>
<span class="dx-rating-value">{{ item.rating }}</span>
<span class="dx-rating-count">({{ item.callbacks?.length || 0 }} {{ 'itemDetail.reviewsCount' | translate }})</span>
<span class="dx-rating-value">{{ item()!.rating }}</span>
<span class="dx-rating-count">({{ item()!.callbacks?.length || 0 }} {{ 'itemDetail.reviewsCount' | translate }})</span>
</div>
<div class="dx-price-block">
@if (item.discount > 0) {
@if (item()!.discount > 0) {
<div class="dx-price-row">
<span class="dx-old-price">{{ item.price }} {{ item.currency }}</span>
<span class="dx-discount-tag">-{{ item.discount }}%</span>
<span class="dx-old-price">{{ item()!.price }} {{ item()!.currency }}</span>
<span class="dx-discount-tag">-{{ item()!.discount }}%</span>
</div>
}
<div class="dx-current-price">
{{ item.discount > 0 ? (getDiscountedPrice() | number:'1.2-2') : item.price }} {{ item.currency }}
{{ item()!.discount > 0 ? (getDiscountedPrice() | number:'1.2-2') : item()!.price }} {{ item()!.currency }}
</div>
</div>
<div class="dx-stock">
<span class="dx-stock-label">{{ 'itemDetail.stock' | translate }}</span>
<span class="dx-stock-status"
[class.high]="item.remainings === 'high'"
[class.medium]="item.remainings === 'medium'"
[class.low]="item.remainings === 'low'">
<span class="dx-stock-status" [class]="getStockClass()">
<span class="dx-stock-dot"></span>
{{ item.remainings === 'high' ? ('itemDetail.inStock' | translate) : item.remainings === 'medium' ? ('itemDetail.mediumStock' | translate) : ('itemDetail.lastItems' | translate) }}
{{ getStockLabel() }}
</span>
@if (item()!.quantity != null) {
<span class="dx-stock-qty">({{ item()!.quantity }} шт.)</span>
}
</div>
@if (item()!.colour || item()!.size) {
<div class="dx-variants">
@if (item()!.colour) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.colour' | translate }}:</span>
<span class="variant-chip colour-chip">{{ item()!.colour }}</span>
</div>
}
@if (item()!.size) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.size' | translate }}:</span>
<span class="variant-chip size-chip">{{ item()!.size }}</span>
</div>
}
</div>
}
@if (item()!.attributes && item()!.attributes!.length > 0) {
<div class="dx-attributes">
@for (attr of item()!.attributes!; track attr.key) {
<div class="attribute-row">
<span class="attribute-key">{{ attr.key }}</span>
<span class="attribute-value">{{ attr.value }}</span>
</div>
}
</div>
}
<button class="dx-add-cart" (click)="addToCart()">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="9" cy="21" r="1"></circle>
@@ -299,15 +405,33 @@
</button>
<div class="dx-description">
<h2>{{ 'itemDetail.description' | translate }}</h2>
<div class="dx-description-text" [innerHTML]="getSafeHtml(item.description)"></div>
@if (getSimpleDescription()) {
<p class="dx-simple-desc">{{ getSimpleDescription() }}</p>
}
@if (hasDescriptionFields()) {
<h2>{{ 'itemDetail.specifications' | translate }}</h2>
<table class="dx-specs-table">
<tbody>
@for (field of getTranslatedDescriptionFields(); track field.key) {
<tr>
<td class="spec-key">{{ field.key }}</td>
<td class="spec-value">{{ field.value }}</td>
</tr>
}
</tbody>
</table>
} @else {
<h2>{{ 'itemDetail.description' | translate }}</h2>
<div class="dx-description-text" [innerHTML]="getSafeHtml(item()!.description)"></div>
}
</div>
</div>
</div>
<!-- Reviews Section -->
<div class="dx-reviews-section">
<h2>{{ 'itemDetail.reviews' | translate }} ({{ item.callbacks?.length || 0 }})</h2>
<h2>{{ 'itemDetail.reviews' | translate }} ({{ item()!.callbacks?.length || 0 }})</h2>
<div class="dx-review-form">
<h3>{{ 'itemDetail.leaveReview' | translate }}</h3>
@@ -368,8 +492,8 @@
</div>
<div class="dx-reviews-list">
@if (item.callbacks && item.callbacks.length > 0) {
@for (callback of item.callbacks; track $index) {
@if (item()?.callbacks && item()!.callbacks!.length > 0) {
@for (callback of item()!.callbacks; track $index) {
<div class="dx-review-card">
<div class="dx-review-header">
<div class="dx-reviewer">
@@ -400,11 +524,11 @@
</div>
<!-- Q&A Section -->
@if (item.questions && item.questions.length > 0) {
@if (item()!.questions && item()!.questions!.length > 0) {
<div class="dx-qa-section">
<h2>{{ 'itemDetail.qna' | translate }} ({{ item.questions.length }})</h2>
<h2>{{ 'itemDetail.qna' | translate }} ({{ item()!.questions!.length }})</h2>
<div class="dx-qa-list">
@for (question of item.questions; track $index) {
@for (question of item()!.questions!; track $index) {
<div class="dx-qa-card">
<div class="dx-question">
<span class="dx-qa-label q">В</span>
@@ -429,7 +553,6 @@
</div>
</div>
}
}
}
</div>
}

View File

@@ -293,6 +293,64 @@ $dx-card-bg: #f5f3f9;
}
}
// Variant chips (colour/size) — shared between dexar and novo
.dx-variants, .novo-variants {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 12px;
.variant-group {
display: flex;
align-items: center;
gap: 8px;
}
.variant-label {
font-size: 0.9rem;
color: #6b7280;
font-weight: 500;
}
.variant-chip {
display: inline-flex;
align-items: center;
padding: 6px 14px;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
border: 1.5px solid $dx-border;
background: rgba(73, 118, 113, 0.06);
color: $dx-primary;
}
}
.dx-attributes, .novo-attributes {
display: flex;
flex-wrap: wrap;
gap: 6px 16px;
margin-bottom: 14px;
padding: 10px 14px;
background: #f8fafa;
border-radius: 10px;
.attribute-row {
display: flex;
gap: 6px;
font-size: 0.85rem;
}
.attribute-key {
color: #6b7280;
&::after { content: ':'; }
}
.attribute-value {
font-weight: 600;
color: #1a1a1a;
}
}
.dx-description {
padding-top: 8px;
border-top: 1px solid $dx-border;
@@ -644,22 +702,70 @@ $dx-card-bg: #f5f3f9;
}
}
// Responsive
// ========== DEXAR RESPONSIVE ==========
// Large desktop — constrain gallery height
@media (min-width: 1201px) {
.dx-main-photo {
max-height: 560px;
}
}
// Tablet landscape / small desktop
@media (max-width: 1200px) {
.dx-item-content {
gap: 32px;
}
.dx-title {
font-size: 1.5rem;
}
}
// Tablet portrait
@media (max-width: 992px) {
.dx-item-content {
grid-template-columns: 1fr;
gap: 32px;
}
.dx-gallery {
max-width: 600px;
margin: 0 auto;
}
.dx-main-photo {
max-height: 480px;
aspect-ratio: auto;
}
.dx-add-cart {
max-width: 100%;
}
.dx-reviews-section,
.dx-qa-section {
h2 {
font-size: 1.3rem;
}
}
}
// Mobile
@media (max-width: 768px) {
.dx-item-container {
padding: 16px;
}
// On mobile: thumbnails go below main photo
.dx-item-content {
gap: 24px;
margin-bottom: 32px;
}
// Thumbnails go below main photo
.dx-gallery {
flex-direction: column;
max-width: 100%;
}
.dx-thumbnails {
@@ -668,14 +774,16 @@ $dx-card-bg: #f5f3f9;
max-height: none;
overflow-x: auto;
overflow-y: hidden;
order: 1; // put below main photo
order: 1;
scrollbar-width: none;
&::-webkit-scrollbar { display: none; }
}
.dx-main-photo {
order: 0; // main photo first
order: 0;
max-height: 400px;
aspect-ratio: auto;
}
.dx-thumb {
@@ -692,8 +800,32 @@ $dx-card-bg: #f5f3f9;
font-size: 1.8rem;
}
.dx-old-price {
font-size: 1rem;
}
.dx-add-cart {
max-width: 100%;
padding: 14px 20px;
font-size: 1rem;
}
.dx-description {
h2 {
font-size: 1.15rem;
}
}
.dx-specs-table {
.spec-key {
white-space: normal;
width: auto;
}
td {
padding: 8px 10px;
font-size: 0.85rem;
}
}
.dx-review-form {
@@ -709,21 +841,153 @@ $dx-card-bg: #f5f3f9;
width: 100%;
}
}
.dx-review-card {
padding: 16px;
}
.dx-reviews-section,
.dx-qa-section {
margin-bottom: 32px;
h2 {
font-size: 1.25rem;
margin-bottom: 16px;
}
}
.dx-qa-card {
padding: 16px;
}
.dx-question,
.dx-answer {
font-size: 0.9rem;
}
}
// Small mobile
@media (max-width: 480px) {
.dx-item-container {
padding: 12px;
}
.dx-item-content {
gap: 20px;
margin-bottom: 24px;
}
.dx-main-photo {
max-height: 300px;
border-radius: 10px;
img, video {
padding: 8px;
}
}
.dx-thumb {
width: 56px;
height: 56px;
min-width: 56px;
width: 52px;
height: 52px;
min-width: 52px;
}
.dx-title {
font-size: 1.25rem;
font-size: 1.2rem;
}
.dx-info {
gap: 16px;
}
.dx-current-price {
font-size: 1.6rem;
font-size: 1.5rem;
}
.dx-rating {
flex-wrap: wrap;
gap: 6px;
}
.dx-stock {
flex-wrap: wrap;
gap: 6px;
}
.dx-add-cart {
padding: 12px 16px;
font-size: 0.95rem;
border-radius: 10px;
}
.dx-review-form {
padding: 14px;
h3 {
font-size: 1rem;
}
}
.dx-star-selector {
.dx-star-pick {
font-size: 1.5rem;
}
}
.dx-textarea {
padding: 12px;
font-size: 0.9rem;
}
.dx-review-card {
padding: 14px;
}
.dx-reviewer-name {
font-size: 0.9rem;
}
.dx-review-text {
font-size: 0.9rem;
}
.dx-specs-table {
td {
padding: 6px 8px;
font-size: 0.8rem;
display: block;
}
.spec-key {
width: 100%;
padding-bottom: 2px;
}
.spec-value {
padding-top: 0;
}
tr {
display: flex;
flex-direction: column;
padding: 6px 0;
}
}
.dx-qa-card {
padding: 14px;
}
.dx-question,
.dx-answer {
font-size: 0.85rem;
gap: 8px;
}
.dx-qa-label {
width: 24px;
height: 24px;
font-size: 0.7rem;
}
}
@@ -1301,12 +1565,20 @@ $dx-card-bg: #f5f3f9;
}
}
// ========== NOVO RESPONSIVE ==========
// Tablet portrait
@media (max-width: 968px) {
.novo-item-content {
grid-template-columns: 1fr;
gap: 2rem;
}
.novo-gallery {
max-width: 600px;
margin: 0 auto;
}
.novo-info .novo-title {
font-size: 1.5rem;
}
@@ -1315,6 +1587,10 @@ $dx-card-bg: #f5f3f9;
font-size: 2rem;
}
.novo-info .novo-add-cart {
max-width: 100%;
}
.novo-review-form {
padding: 1.5rem;
@@ -1329,3 +1605,302 @@ $dx-card-bg: #f5f3f9;
}
}
}
// Mobile
@media (max-width: 768px) {
.novo-item-container {
padding: 1rem;
}
.novo-item-content {
gap: 1.5rem;
margin-bottom: 2rem;
}
.novo-gallery {
max-width: 100%;
.novo-main-photo {
border-radius: var(--radius-lg);
margin-bottom: 0.75rem;
}
.novo-thumbnails {
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 0.5rem;
}
}
.novo-info {
.novo-title {
font-size: 1.35rem;
}
.novo-rating {
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.novo-price-block {
padding: 1rem;
.current-price {
font-size: 1.75rem;
}
.old-price {
font-size: 0.95rem;
}
}
.novo-stock {
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.75rem;
}
.novo-add-cart {
padding: 1rem 1.5rem;
font-size: 1rem;
margin-bottom: 1.5rem;
}
.novo-description {
h3 {
font-size: 1.1rem;
}
}
}
.novo-specs-table {
.spec-key {
white-space: normal;
width: auto;
}
td {
padding: 8px 10px;
font-size: 0.85rem;
}
}
.novo-reviews {
margin-top: 2rem;
padding-top: 2rem;
h2 {
font-size: 1.35rem;
margin-bottom: 1.5rem;
}
}
.novo-review-card {
padding: 1rem;
}
.novo-review-form {
padding: 1.25rem;
h3 {
font-size: 1.1rem;
}
}
}
// Small mobile
@media (max-width: 480px) {
.novo-item-container {
padding: 0.75rem;
}
.novo-item-content {
gap: 1.25rem;
margin-bottom: 1.5rem;
}
.novo-gallery {
.novo-thumbnails {
grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
gap: 0.4rem;
}
.novo-main-photo {
border-radius: var(--radius-md);
}
}
.novo-info {
.novo-title {
font-size: 1.15rem;
}
.novo-price-block {
padding: 0.75rem;
.current-price {
font-size: 1.5rem;
}
}
.novo-stock {
padding: 0.6rem;
font-size: 0.85rem;
}
.novo-add-cart {
padding: 0.85rem 1rem;
font-size: 0.95rem;
border-radius: var(--radius-md);
}
}
.novo-review-form {
padding: 1rem;
.novo-rating-input {
.novo-star-selector {
.novo-star {
font-size: 1.6rem;
}
}
}
.novo-textarea {
padding: 0.75rem;
font-size: 0.9rem;
}
}
.novo-review-card {
padding: 0.75rem;
.review-header {
flex-direction: column;
gap: 0.5rem;
.review-stars {
align-self: flex-start;
}
}
.review-text {
font-size: 0.9rem;
}
}
.novo-specs-table {
td {
padding: 6px 8px;
font-size: 0.8rem;
display: block;
}
.spec-key {
width: 100%;
padding-bottom: 2px;
}
.spec-value {
padding-top: 0;
}
tr {
display: flex;
flex-direction: column;
padding: 6px 0;
}
}
}
// ========== BADGES, TAGS & SPECS (shared) ==========
// Badges
.novo-badges, .dx-badges {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 8px 0;
}
.item-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #fff;
&.badge-new { background: #4caf50; }
&.badge-sale { background: #f44336; }
&.badge-exclusive { background: #9c27b0; }
&.badge-hot { background: #ff5722; }
&.badge-limited { background: #ff9800; }
&.badge-bestseller { background: #2196f3; }
&.badge-featured { background: #607d8b; }
&.badge-custom { background: #78909c; }
}
// Tags
.novo-tags, .dx-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 6px 0 12px;
}
.item-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75rem;
color: #497671;
background: rgba(73, 118, 113, 0.08);
border: 1px solid rgba(73, 118, 113, 0.15);
}
// Specs table
.novo-specs-table, .dx-specs-table {
width: 100%;
border-collapse: collapse;
margin: 12px 0;
tr {
border-bottom: 1px solid #e8ecec;
&:last-child { border-bottom: none; }
}
td {
padding: 10px 12px;
font-size: 0.9rem;
vertical-align: top;
}
.spec-key {
color: #697777;
font-weight: 500;
width: 40%;
white-space: nowrap;
}
.spec-value {
color: #1e3c38;
}
}
// Simple description
.novo-simple-desc, .dx-simple-desc {
font-size: 0.95rem;
color: #697777;
line-height: 1.6;
margin-bottom: 16px;
}
// Stock quantity
.stock-qty, .dx-stock-qty {
font-size: 0.8rem;
color: #697777;
margin-left: 8px;
}

View File

@@ -2,13 +2,13 @@ import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy, inject }
import { DecimalPipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { ApiService, CartService, TelegramService, SeoService, LanguageService } from '../../services';
import { Item } from '../../models';
import { DomSanitizer } from '@angular/platform-browser';
import { ApiService, CartService, TelegramService, LanguageService, SeoService } from '../../services';
import { Item, DescriptionField } from '../../models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { Subscription } from 'rxjs';
import { environment } from '../../../environments/environment';
import { SecurityContext } from '@angular/core';
import { getDiscountedPrice } from '../../utils/item.utils';
import { getDiscountedPrice, getAllImages, getStockStatus, getBadgeClass, getTranslatedField } from '../../utils/item.utils';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service';
@@ -42,14 +42,14 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
private seoService = inject(SeoService);
private i18n = inject(TranslateService);
private langService = inject(LanguageService);
constructor(
private route: ActivatedRoute,
private apiService: ApiService,
private cartService: CartService,
private telegramService: TelegramService,
private sanitizer: DomSanitizer
private sanitizer: DomSanitizer,
private languageService: LanguageService
) {}
ngOnInit(): void {
@@ -101,7 +101,58 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
return getDiscountedPrice(currentItem);
}
getSafeHtml(html: string): string {
// BackOffice integration helpers
getItemName(): string {
const currentItem = this.item();
if (!currentItem) return '';
const lang = this.languageService.currentLanguage();
return getTranslatedField(currentItem, 'name', lang);
}
getSimpleDescription(): string {
const currentItem = this.item();
if (!currentItem) return '';
const lang = this.languageService.currentLanguage();
return getTranslatedField(currentItem, 'simpleDescription', lang);
}
hasDescriptionFields(): boolean {
const currentItem = this.item();
return !!(currentItem?.descriptionFields && currentItem.descriptionFields.length > 0);
}
getTranslatedDescriptionFields(): DescriptionField[] {
const currentItem = this.item();
if (!currentItem) return [];
const lang = this.languageService.currentLanguage();
const translation = currentItem.translations?.[lang];
if (translation?.description && translation.description.length > 0) {
return translation.description;
}
return currentItem.descriptionFields || [];
}
getStockClass(): string {
const currentItem = this.item();
if (!currentItem) return 'high';
return getStockStatus(currentItem);
}
getStockLabel(): string {
const status = this.getStockClass();
switch (status) {
case 'high': return 'В наличии';
case 'medium': return 'Заканчивается';
case 'low': return 'Последние штуки';
case 'out': return 'Нет в наличии';
default: return 'В наличии';
}
}
readonly getBadgeClass = getBadgeClass;
getSafeHtml(html: string): SafeHtml {
return this.sanitizer.sanitize(SecurityContext.HTML, html) || '';
}
@@ -124,9 +175,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
if (diffDays < 7) return `${diffDays} ${this.i18n.t('itemDetail.daysAgo')}`;
if (diffDays < 30) return `${Math.floor(diffDays / 7)} ${this.i18n.t('itemDetail.weeksAgo')}`;
const localeMap: Record<string, string> = { ru: 'ru-RU', en: 'en-US', hy: 'hy-AM' };
const locale = localeMap[this.langService.currentLanguage()] || 'ru-RU';
return date.toLocaleDateString(locale, {
return date.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'