changes
This commit is contained in:
@@ -82,12 +82,12 @@
|
|||||||
<div class="novo-price-block">
|
<div class="novo-price-block">
|
||||||
@if (item()!.discount > 0) {
|
@if (item()!.discount > 0) {
|
||||||
<div class="price-row">
|
<div class="price-row">
|
||||||
<span class="old-price">{{ item()!.price }} {{ item()!.currency }}</span>
|
<span class="old-price">{{ effectivePrice() }} {{ effectiveCurrency() }}</span>
|
||||||
<span class="discount-badge">-{{ item()!.discount }}%</span>
|
<span class="discount-badge">-{{ item()!.discount }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="current-price">{{ getDiscountedPrice() | number:'1.2-2' }} {{ item()!.currency }}</div>
|
<div class="current-price">{{ getDiscountedPrice() | number:'1.2-2' }} {{ effectiveCurrency() }}</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="current-price">{{ item()!.price }} {{ item()!.currency }}</div>
|
<div class="current-price">{{ effectivePrice() }} {{ effectiveCurrency() }}</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -97,23 +97,37 @@
|
|||||||
<span class="dot"></span>
|
<span class="dot"></span>
|
||||||
{{ getStockLabel() }}
|
{{ getStockLabel() }}
|
||||||
</div>
|
</div>
|
||||||
@if (item()!.quantity != null) {
|
@if (effectiveRemaining() != null) {
|
||||||
<span class="stock-qty">({{ item()!.quantity }} шт.)</span>
|
<span class="stock-qty">({{ effectiveRemaining() }} шт.)</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (item()!.colour || item()!.size) {
|
@if (availableColours().length || availableSizes().length || item()!.colour || item()!.size) {
|
||||||
<div class="novo-variants">
|
<div class="novo-variants">
|
||||||
@if (item()!.colour) {
|
@if (availableColours().length) {
|
||||||
<div class="variant-group">
|
<div class="variant-group">
|
||||||
<span class="variant-label">{{ 'itemDetail.colour' | translate }}:</span>
|
<span class="variant-label">{{ 'itemDetail.colour' | translate }}:</span>
|
||||||
<span class="variant-chip colour-chip">{{ item()!.colour }}</span>
|
@for (c of availableColours(); track c) {
|
||||||
|
<span class="variant-chip colour-chip" [class.active]="selectedColour() === c" (click)="selectColour(c)">{{ c }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
} @else if (item()!.colour) {
|
||||||
|
<div class="variant-group">
|
||||||
|
<span class="variant-label">{{ 'itemDetail.colour' | translate }}:</span>
|
||||||
|
<span class="variant-chip colour-chip active">{{ item()!.colour }}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (item()!.size) {
|
@if (availableSizes().length) {
|
||||||
<div class="variant-group">
|
<div class="variant-group">
|
||||||
<span class="variant-label">{{ 'itemDetail.size' | translate }}:</span>
|
<span class="variant-label">{{ 'itemDetail.size' | translate }}:</span>
|
||||||
<span class="variant-chip size-chip">{{ item()!.size }}</span>
|
@for (s of availableSizes(); track s) {
|
||||||
|
<span class="variant-chip size-chip" [class.active]="selectedSize() === s" (click)="selectSize(s)">{{ s }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
} @else if (item()!.size) {
|
||||||
|
<div class="variant-group">
|
||||||
|
<span class="variant-label">{{ 'itemDetail.size' | translate }}:</span>
|
||||||
|
<span class="variant-chip size-chip active">{{ item()!.size }}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -347,12 +361,12 @@
|
|||||||
<div class="dx-price-block">
|
<div class="dx-price-block">
|
||||||
@if (item()!.discount > 0) {
|
@if (item()!.discount > 0) {
|
||||||
<div class="dx-price-row">
|
<div class="dx-price-row">
|
||||||
<span class="dx-old-price">{{ item()!.price }} {{ item()!.currency }}</span>
|
<span class="dx-old-price">{{ effectivePrice() }} {{ effectiveCurrency() }}</span>
|
||||||
<span class="dx-discount-tag">-{{ item()!.discount }}%</span>
|
<span class="dx-discount-tag">-{{ item()!.discount }}%</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div class="dx-current-price">
|
<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') : effectivePrice() }} {{ effectiveCurrency() }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -362,23 +376,37 @@
|
|||||||
<span class="dx-stock-dot"></span>
|
<span class="dx-stock-dot"></span>
|
||||||
{{ getStockLabel() }}
|
{{ getStockLabel() }}
|
||||||
</span>
|
</span>
|
||||||
@if (item()!.quantity != null) {
|
@if (effectiveRemaining() != null) {
|
||||||
<span class="dx-stock-qty">({{ item()!.quantity }} шт.)</span>
|
<span class="dx-stock-qty">({{ effectiveRemaining() }} шт.)</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (item()!.colour || item()!.size) {
|
@if (availableColours().length || availableSizes().length || item()!.colour || item()!.size) {
|
||||||
<div class="dx-variants">
|
<div class="dx-variants">
|
||||||
@if (item()!.colour) {
|
@if (availableColours().length) {
|
||||||
<div class="variant-group">
|
<div class="variant-group">
|
||||||
<span class="variant-label">{{ 'itemDetail.colour' | translate }}:</span>
|
<span class="variant-label">{{ 'itemDetail.colour' | translate }}:</span>
|
||||||
<span class="variant-chip colour-chip">{{ item()!.colour }}</span>
|
@for (c of availableColours(); track c) {
|
||||||
|
<span class="variant-chip colour-chip" [class.active]="selectedColour() === c" (click)="selectColour(c)">{{ c }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
} @else if (item()!.colour) {
|
||||||
|
<div class="variant-group">
|
||||||
|
<span class="variant-label">{{ 'itemDetail.colour' | translate }}:</span>
|
||||||
|
<span class="variant-chip colour-chip active">{{ item()!.colour }}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (item()!.size) {
|
@if (availableSizes().length) {
|
||||||
<div class="variant-group">
|
<div class="variant-group">
|
||||||
<span class="variant-label">{{ 'itemDetail.size' | translate }}:</span>
|
<span class="variant-label">{{ 'itemDetail.size' | translate }}:</span>
|
||||||
<span class="variant-chip size-chip">{{ item()!.size }}</span>
|
@for (s of availableSizes(); track s) {
|
||||||
|
<span class="variant-chip size-chip" [class.active]="selectedSize() === s" (click)="selectSize(s)">{{ s }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
} @else if (item()!.size) {
|
||||||
|
<div class="variant-group">
|
||||||
|
<span class="variant-label">{{ 'itemDetail.size' | translate }}:</span>
|
||||||
|
<span class="variant-chip size-chip active">{{ item()!.size }}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -405,7 +433,8 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="dx-description">
|
<div class="dx-description">
|
||||||
@if (getSimpleDescription()) {
|
<!-- @if (getSimpleDescription()) { -->
|
||||||
|
@if (false) {
|
||||||
<p class="dx-simple-desc">{{ getSimpleDescription() }}</p>
|
<p class="dx-simple-desc">{{ getSimpleDescription() }}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -322,6 +322,18 @@ $dx-card-bg: #f5f3f9;
|
|||||||
border: 1.5px solid $dx-border;
|
border: 1.5px solid $dx-border;
|
||||||
background: rgba(73, 118, 113, 0.06);
|
background: rgba(73, 118, 113, 0.06);
|
||||||
color: $dx-primary;
|
color: $dx-primary;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(73, 118, 113, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: $dx-primary;
|
||||||
|
background: rgba(73, 118, 113, 0.18);
|
||||||
|
box-shadow: 0 0 0 2px rgba(73, 118, 113, 0.25);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy, inject } from '@angular/core';
|
import { Component, OnInit, OnDestroy, signal, computed, ChangeDetectionStrategy, inject } from '@angular/core';
|
||||||
import { DecimalPipe } from '@angular/common';
|
import { DecimalPipe } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||||
import { ApiService, CartService, TelegramService, LanguageService, SeoService } from '../../services';
|
import { ApiService, CartService, TelegramService, LanguageService, SeoService } from '../../services';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
import { Item, DescriptionField } from '../../models';
|
import { Item, ItemDetail, DescriptionField } from '../../models';
|
||||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
@@ -28,6 +28,55 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
|
|||||||
error = signal<string | null>(null);
|
error = signal<string | null>(null);
|
||||||
isnovo = environment.theme === 'novo';
|
isnovo = environment.theme === 'novo';
|
||||||
|
|
||||||
|
// Variant selection
|
||||||
|
selectedColour = signal<string | null>(null);
|
||||||
|
selectedSize = signal<string | null>(null);
|
||||||
|
|
||||||
|
availableColours = computed(() => {
|
||||||
|
const details = this.item()?.itemDetails;
|
||||||
|
if (!details?.length) return [] as string[];
|
||||||
|
const unique = [...new Set(details.map(d => d.colour || d.color).filter((c): c is string => !!c))];
|
||||||
|
return unique;
|
||||||
|
});
|
||||||
|
|
||||||
|
availableSizes = computed(() => {
|
||||||
|
const details = this.item()?.itemDetails;
|
||||||
|
if (!details?.length) return [] as string[];
|
||||||
|
// If a colour is selected, only show sizes available for that colour
|
||||||
|
const colour = this.selectedColour();
|
||||||
|
const filtered = colour
|
||||||
|
? details.filter(d => (d.colour || d.color) === colour)
|
||||||
|
: details;
|
||||||
|
const unique = [...new Set(filtered.map(d => d.size).filter((s): s is string => !!s))];
|
||||||
|
return unique;
|
||||||
|
});
|
||||||
|
|
||||||
|
selectedDetail = computed<ItemDetail | null>(() => {
|
||||||
|
const details = this.item()?.itemDetails;
|
||||||
|
if (!details?.length) return null;
|
||||||
|
const colour = this.selectedColour();
|
||||||
|
const size = this.selectedSize();
|
||||||
|
return details.find(d =>
|
||||||
|
(!colour || (d.colour || d.color) === colour) &&
|
||||||
|
(!size || d.size === size)
|
||||||
|
) ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
effectivePrice = computed(() => {
|
||||||
|
const detail = this.selectedDetail();
|
||||||
|
return detail?.price ?? this.item()?.price ?? 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
effectiveCurrency = computed(() => {
|
||||||
|
const detail = this.selectedDetail();
|
||||||
|
return detail?.currency ?? this.item()?.currency ?? '';
|
||||||
|
});
|
||||||
|
|
||||||
|
effectiveRemaining = computed(() => {
|
||||||
|
const detail = this.selectedDetail();
|
||||||
|
return detail?.remaining ?? this.item()?.quantity ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
newReview = {
|
newReview = {
|
||||||
rating: 0,
|
rating: 0,
|
||||||
comment: '',
|
comment: '',
|
||||||
@@ -75,6 +124,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
|
|||||||
this.apiService.getItem(itemID).subscribe({
|
this.apiService.getItem(itemID).subscribe({
|
||||||
next: (item) => {
|
next: (item) => {
|
||||||
this.item.set(item);
|
this.item.set(item);
|
||||||
|
this.initVariantSelection(item);
|
||||||
this.seoService.setItemMeta(item);
|
this.seoService.setItemMeta(item);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
},
|
},
|
||||||
@@ -86,6 +136,33 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private initVariantSelection(item: Item): void {
|
||||||
|
// Auto-select the first available colour and size from itemDetails
|
||||||
|
const details = item.itemDetails;
|
||||||
|
if (details?.length) {
|
||||||
|
const firstColour = details[0].colour || details[0].color || null;
|
||||||
|
this.selectedColour.set(firstColour);
|
||||||
|
const firstSize = details[0].size || null;
|
||||||
|
this.selectedSize.set(firstSize);
|
||||||
|
} else {
|
||||||
|
this.selectedColour.set(item.colour ?? null);
|
||||||
|
this.selectedSize.set(item.size ?? null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectColour(colour: string): void {
|
||||||
|
this.selectedColour.set(colour);
|
||||||
|
// If current size is not available for the new colour, reset to first available
|
||||||
|
const sizes = this.availableSizes();
|
||||||
|
if (sizes.length && this.selectedSize() && !sizes.includes(this.selectedSize()!)) {
|
||||||
|
this.selectedSize.set(sizes[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectSize(size: string): void {
|
||||||
|
this.selectedSize.set(size);
|
||||||
|
}
|
||||||
|
|
||||||
selectPhoto(index: number): void {
|
selectPhoto(index: number): void {
|
||||||
this.selectedPhotoIndex.set(index);
|
this.selectedPhotoIndex.set(index);
|
||||||
}
|
}
|
||||||
@@ -93,14 +170,21 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
|
|||||||
addToCart(): void {
|
addToCart(): void {
|
||||||
const currentItem = this.item();
|
const currentItem = this.item();
|
||||||
if (currentItem) {
|
if (currentItem) {
|
||||||
this.cartService.addItem(currentItem.itemID);
|
this.cartService.addItem(currentItem.itemID, 1, {
|
||||||
|
colour: this.selectedColour() ?? undefined,
|
||||||
|
size: this.selectedSize() ?? undefined,
|
||||||
|
price: this.effectivePrice(),
|
||||||
|
currency: this.effectiveCurrency()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getDiscountedPrice(): number {
|
getDiscountedPrice(): number {
|
||||||
const currentItem = this.item();
|
const currentItem = this.item();
|
||||||
if (!currentItem) return 0;
|
if (!currentItem) return 0;
|
||||||
return getDiscountedPrice(currentItem);
|
const price = this.effectivePrice();
|
||||||
|
const discount = currentItem.discount || 0;
|
||||||
|
return discount > 0 ? price * (1 - discount / 100) : price;
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackOffice integration helpers
|
// BackOffice integration helpers
|
||||||
@@ -115,8 +199,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
|
|||||||
getSimpleDescription(): string {
|
getSimpleDescription(): string {
|
||||||
const currentItem = this.item();
|
const currentItem = this.item();
|
||||||
if (!currentItem) return '';
|
if (!currentItem) return '';
|
||||||
const lang = this.languageService.currentLanguage();
|
return currentItem.simpleDescription || currentItem.description || '';
|
||||||
return getTranslatedField(currentItem, 'simpleDescription', lang);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasDescriptionFields(): boolean {
|
hasDescriptionFields(): boolean {
|
||||||
|
|||||||
@@ -24,12 +24,13 @@ export class ApiService {
|
|||||||
return map[apiLang] || apiLang.toLowerCase();
|
return map[apiLang] || apiLang.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolve relative image URLs (e.g. ./images/x.webp) against API base */
|
/** Resolve relative image URLs (e.g. ./images/x.webp) against site origin */
|
||||||
private resolveImageUrl(url: string): string {
|
private resolveImageUrl(url: string): string {
|
||||||
if (!url) return '';
|
if (!url) return '';
|
||||||
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/')) return url;
|
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/')) return url;
|
||||||
if (url.startsWith('./')) return `${this.baseUrl}/${url.slice(2)}`;
|
const origin = `https://${environment.domain}`;
|
||||||
return `${this.baseUrl}/${url}`;
|
if (url.startsWith('./')) return `${origin}/${url.slice(2)}`;
|
||||||
|
return `${origin}/${url}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,17 +107,6 @@ export class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map backend descriptions[] → translations (multi-lang descriptions)
|
|
||||||
if (raw.descriptions && Array.isArray(raw.descriptions)) {
|
|
||||||
item.descriptions = raw.descriptions;
|
|
||||||
if (!item.translations) item.translations = {};
|
|
||||||
for (const entry of raw.descriptions) {
|
|
||||||
const lang = this.normalizeLang(entry.language);
|
|
||||||
if (!item.translations[lang]) item.translations[lang] = {};
|
|
||||||
item.translations[lang].simpleDescription = entry.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preserve attributes from backend
|
// Preserve attributes from backend
|
||||||
item.attributes = raw.attributes || [];
|
item.attributes = raw.attributes || [];
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export class CartService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
addItem(itemID: number, quantity: number = 1): void {
|
addItem(itemID: number, quantity: number = 1, variant?: { colour?: string; size?: string; price?: number; currency?: string }): void {
|
||||||
// Prevent duplicate API calls for same item
|
// Prevent duplicate API calls for same item
|
||||||
if (this.addingItems.has(itemID)) return;
|
if (this.addingItems.has(itemID)) return;
|
||||||
|
|
||||||
@@ -118,7 +118,14 @@ export class CartService {
|
|||||||
this.addingItems.add(itemID);
|
this.addingItems.add(itemID);
|
||||||
this.apiService.getItem(itemID).subscribe({
|
this.apiService.getItem(itemID).subscribe({
|
||||||
next: (item) => {
|
next: (item) => {
|
||||||
const cartItem: CartItem = { ...item, quantity };
|
const cartItem: CartItem = {
|
||||||
|
...item,
|
||||||
|
quantity,
|
||||||
|
...(variant?.colour != null && { colour: variant.colour }),
|
||||||
|
...(variant?.size != null && { size: variant.size }),
|
||||||
|
...(variant?.price != null && { price: variant.price }),
|
||||||
|
...(variant?.currency != null && { currency: variant.currency }),
|
||||||
|
};
|
||||||
this.cartItems.set([...this.cartItems(), cartItem]);
|
this.cartItems.set([...this.cartItems(), cartItem]);
|
||||||
this.addingItems.delete(itemID);
|
this.addingItems.delete(itemID);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -83,12 +83,6 @@ export function getTranslatedField(
|
|||||||
const val = entry?.value || (entry as any)?.valuue || '';
|
const val = entry?.value || (entry as any)?.valuue || '';
|
||||||
if (val) return val;
|
if (val) return val;
|
||||||
}
|
}
|
||||||
if (field === 'simpleDescription' && item.descriptions?.length) {
|
|
||||||
const entry = item.descriptions.find(d => d.language === lang || d.language === lang.toUpperCase() || (lang === 'hy' && d.language === 'AM'));
|
|
||||||
const val = entry?.value || (entry as any)?.valuue || '';
|
|
||||||
if (val) return val;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Fallback to base field
|
// 3. Fallback to base field
|
||||||
if (field === 'name') return item.name;
|
if (field === 'name') return item.name;
|
||||||
if (field === 'simpleDescription') return item.simpleDescription || item.description || '';
|
if (field === 'simpleDescription') return item.simpleDescription || item.description || '';
|
||||||
|
|||||||
Reference in New Issue
Block a user