diff --git a/src/app/pages/item-detail/item-detail.component.html b/src/app/pages/item-detail/item-detail.component.html
index 513f82a..0329b3c 100644
--- a/src/app/pages/item-detail/item-detail.component.html
+++ b/src/app/pages/item-detail/item-detail.component.html
@@ -82,12 +82,12 @@
@if (item()!.discount > 0) {
- {{ item()!.price }} {{ item()!.currency }}
+ {{ effectivePrice() }} {{ effectiveCurrency() }}
-{{ item()!.discount }}%
-
{{ getDiscountedPrice() | number:'1.2-2' }} {{ item()!.currency }}
+
{{ getDiscountedPrice() | number:'1.2-2' }} {{ effectiveCurrency() }}
} @else {
-
{{ item()!.price }} {{ item()!.currency }}
+
{{ effectivePrice() }} {{ effectiveCurrency() }}
}
@@ -97,23 +97,37 @@
{{ getStockLabel() }}
- @if (item()!.quantity != null) {
- ({{ item()!.quantity }} шт.)
+ @if (effectiveRemaining() != null) {
+ ({{ effectiveRemaining() }} шт.)
}
- @if (item()!.colour || item()!.size) {
+ @if (availableColours().length || availableSizes().length || item()!.colour || item()!.size) {
- @if (item()!.colour) {
+ @if (availableColours().length) {
{{ 'itemDetail.colour' | translate }}:
- {{ item()!.colour }}
+ @for (c of availableColours(); track c) {
+ {{ c }}
+ }
+
+ } @else if (item()!.colour) {
+
+ {{ 'itemDetail.colour' | translate }}:
+ {{ item()!.colour }}
}
- @if (item()!.size) {
+ @if (availableSizes().length) {
{{ 'itemDetail.size' | translate }}:
- {{ item()!.size }}
+ @for (s of availableSizes(); track s) {
+ {{ s }}
+ }
+
+ } @else if (item()!.size) {
+
+ {{ 'itemDetail.size' | translate }}:
+ {{ item()!.size }}
}
@@ -347,12 +361,12 @@
@if (item()!.discount > 0) {
- {{ item()!.price }} {{ item()!.currency }}
+ {{ effectivePrice() }} {{ effectiveCurrency() }}
-{{ item()!.discount }}%
}
- {{ item()!.discount > 0 ? (getDiscountedPrice() | number:'1.2-2') : item()!.price }} {{ item()!.currency }}
+ {{ item()!.discount > 0 ? (getDiscountedPrice() | number:'1.2-2') : effectivePrice() }} {{ effectiveCurrency() }}
@@ -362,23 +376,37 @@
{{ getStockLabel() }}
- @if (item()!.quantity != null) {
- ({{ item()!.quantity }} шт.)
+ @if (effectiveRemaining() != null) {
+ ({{ effectiveRemaining() }} шт.)
}
- @if (item()!.colour || item()!.size) {
+ @if (availableColours().length || availableSizes().length || item()!.colour || item()!.size) {
- @if (item()!.colour) {
+ @if (availableColours().length) {
{{ 'itemDetail.colour' | translate }}:
- {{ item()!.colour }}
+ @for (c of availableColours(); track c) {
+ {{ c }}
+ }
+
+ } @else if (item()!.colour) {
+
+ {{ 'itemDetail.colour' | translate }}:
+ {{ item()!.colour }}
}
- @if (item()!.size) {
+ @if (availableSizes().length) {
{{ 'itemDetail.size' | translate }}:
- {{ item()!.size }}
+ @for (s of availableSizes(); track s) {
+ {{ s }}
+ }
+
+ } @else if (item()!.size) {
+
+ {{ 'itemDetail.size' | translate }}:
+ {{ item()!.size }}
}
@@ -405,7 +433,8 @@
- @if (getSimpleDescription()) {
+
+ @if (false) {
{{ getSimpleDescription() }}
}
diff --git a/src/app/pages/item-detail/item-detail.component.scss b/src/app/pages/item-detail/item-detail.component.scss
index b78282f..b895b12 100644
--- a/src/app/pages/item-detail/item-detail.component.scss
+++ b/src/app/pages/item-detail/item-detail.component.scss
@@ -322,6 +322,18 @@ $dx-card-bg: #f5f3f9;
border: 1.5px solid $dx-border;
background: rgba(73, 118, 113, 0.06);
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);
+ }
}
}
diff --git a/src/app/pages/item-detail/item-detail.component.ts b/src/app/pages/item-detail/item-detail.component.ts
index 19b925b..24b63da 100644
--- a/src/app/pages/item-detail/item-detail.component.ts
+++ b/src/app/pages/item-detail/item-detail.component.ts
@@ -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 { FormsModule } from '@angular/forms';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { ApiService, CartService, TelegramService, LanguageService, SeoService } from '../../services';
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 { Subscription } from 'rxjs';
import { environment } from '../../../environments/environment';
@@ -27,6 +27,55 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
loading = signal(true);
error = signal
(null);
isnovo = environment.theme === 'novo';
+
+ // Variant selection
+ selectedColour = signal(null);
+ selectedSize = signal(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(() => {
+ 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 = {
rating: 0,
@@ -75,6 +124,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
this.apiService.getItem(itemID).subscribe({
next: (item) => {
this.item.set(item);
+ this.initVariantSelection(item);
this.seoService.setItemMeta(item);
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 {
this.selectedPhotoIndex.set(index);
}
@@ -93,14 +170,21 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
addToCart(): void {
const currentItem = this.item();
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 {
const currentItem = this.item();
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
@@ -115,8 +199,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
getSimpleDescription(): string {
const currentItem = this.item();
if (!currentItem) return '';
- const lang = this.languageService.currentLanguage();
- return getTranslatedField(currentItem, 'simpleDescription', lang);
+ return currentItem.simpleDescription || currentItem.description || '';
}
hasDescriptionFields(): boolean {
diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts
index 758966b..1073fa5 100644
--- a/src/app/services/api.service.ts
+++ b/src/app/services/api.service.ts
@@ -24,12 +24,13 @@ export class ApiService {
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 {
if (!url) return '';
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/')) return url;
- if (url.startsWith('./')) return `${this.baseUrl}/${url.slice(2)}`;
- return `${this.baseUrl}/${url}`;
+ const origin = `https://${environment.domain}`;
+ 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
item.attributes = raw.attributes || [];
diff --git a/src/app/services/cart.service.ts b/src/app/services/cart.service.ts
index c040016..aef8878 100644
--- a/src/app/services/cart.service.ts
+++ b/src/app/services/cart.service.ts
@@ -103,7 +103,7 @@ export class CartService {
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
if (this.addingItems.has(itemID)) return;
@@ -118,7 +118,14 @@ export class CartService {
this.addingItems.add(itemID);
this.apiService.getItem(itemID).subscribe({
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.addingItems.delete(itemID);
},
diff --git a/src/app/utils/item.utils.ts b/src/app/utils/item.utils.ts
index 3ea8261..f6115d7 100644
--- a/src/app/utils/item.utils.ts
+++ b/src/app/utils/item.utils.ts
@@ -83,12 +83,6 @@ export function getTranslatedField(
const val = entry?.value || (entry as any)?.valuue || '';
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
if (field === 'name') return item.name;
if (field === 'simpleDescription') return item.simpleDescription || item.description || '';