This commit is contained in:
sdarbinyan
2026-03-24 02:46:58 +04:00
parent 5ed255dddb
commit 44553f5bd4
6 changed files with 163 additions and 48 deletions

View File

@@ -82,12 +82,12 @@
<div class="novo-price-block">
@if (item()!.discount > 0) {
<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>
</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 {
<div class="current-price">{{ item()!.price }} {{ item()!.currency }}</div>
<div class="current-price">{{ effectivePrice() }} {{ effectiveCurrency() }}</div>
}
</div>
@@ -97,23 +97,37 @@
<span class="dot"></span>
{{ getStockLabel() }}
</div>
@if (item()!.quantity != null) {
<span class="stock-qty">({{ item()!.quantity }} шт.)</span>
@if (effectiveRemaining() != null) {
<span class="stock-qty">({{ effectiveRemaining() }} шт.)</span>
}
</div>
@if (item()!.colour || item()!.size) {
@if (availableColours().length || availableSizes().length || item()!.colour || item()!.size) {
<div class="novo-variants">
@if (item()!.colour) {
@if (availableColours().length) {
<div class="variant-group">
<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>
}
@if (item()!.size) {
@if (availableSizes().length) {
<div class="variant-group">
<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>
@@ -347,12 +361,12 @@
<div class="dx-price-block">
@if (item()!.discount > 0) {
<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>
</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') : effectivePrice() }} {{ effectiveCurrency() }}
</div>
</div>
@@ -362,23 +376,37 @@
<span class="dx-stock-dot"></span>
{{ getStockLabel() }}
</span>
@if (item()!.quantity != null) {
<span class="dx-stock-qty">({{ item()!.quantity }} шт.)</span>
@if (effectiveRemaining() != null) {
<span class="dx-stock-qty">({{ effectiveRemaining() }} шт.)</span>
}
</div>
@if (item()!.colour || item()!.size) {
@if (availableColours().length || availableSizes().length || item()!.colour || item()!.size) {
<div class="dx-variants">
@if (item()!.colour) {
@if (availableColours().length) {
<div class="variant-group">
<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>
}
@if (item()!.size) {
@if (availableSizes().length) {
<div class="variant-group">
<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>
@@ -405,7 +433,8 @@
</button>
<div class="dx-description">
@if (getSimpleDescription()) {
<!-- @if (getSimpleDescription()) { -->
@if (false) {
<p class="dx-simple-desc">{{ getSimpleDescription() }}</p>
}

View File

@@ -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);
}
}
}

View File

@@ -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<string | null>(null);
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 = {
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 {

View File

@@ -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 || [];

View File

@@ -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);
},

View File

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