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 || '';