Add related products to detail page

This commit is contained in:
sdarbinyan
2026-07-05 01:07:53 +04:00
parent d7d73c2a10
commit b676cec4e9
7 changed files with 80 additions and 1 deletions

View File

@@ -188,6 +188,7 @@ export const en: Translations = {
weeksAgo: 'w. ago', weeksAgo: 'w. ago',
colour: 'Colour', colour: 'Colour',
size: 'Size', size: 'Size',
relatedProducts: 'Related products',
}, },
app: { app: {
connecting: 'Connecting to server...', connecting: 'Connecting to server...',

View File

@@ -188,6 +188,7 @@ export const hy: Translations = {
weeksAgo: 'շաբաթ առաջ', weeksAgo: 'շաբաթ առաջ',
colour: 'Գույն', colour: 'Գույն',
size: 'Չափ', size: 'Չափ',
relatedProducts: 'Նման ապրանքներ',
}, },
app: { app: {
connecting: 'Կապ սերվերի հետ...', connecting: 'Կապ սերվերի հետ...',

View File

@@ -188,6 +188,7 @@ export const ru: Translations = {
weeksAgo: 'нед. назад', weeksAgo: 'нед. назад',
colour: 'Цвет', colour: 'Цвет',
size: 'Размер', size: 'Размер',
relatedProducts: 'Похожие товары',
}, },
app: { app: {
connecting: 'Подключение к серверу...', connecting: 'Подключение к серверу...',

View File

@@ -186,6 +186,7 @@ export interface Translations {
weeksAgo: string; weeksAgo: string;
colour: string; colour: string;
size: string; size: string;
relatedProducts: string;
}; };
app: { app: {
connecting: string; connecting: string;

View File

@@ -294,6 +294,23 @@
} }
</div> </div>
</div> </div>
@if (relatedProducts().length > 0) {
<section class="related-products" aria-labelledby="novo-related-products-title">
<h2 id="novo-related-products-title">{{ 'itemDetail.relatedProducts' | translate }}</h2>
<div class="related-products-grid">
@for (related of relatedProducts(); track related.itemID) {
<app-product-card
[item]="related"
[title]="relatedItemName(related)"
[addToCartLabel]="'itemDetail.addToCart' | translate"
appearance="compact"
(addToCart)="addRelatedToCart($event.itemID, $event.event)"
/>
}
</div>
</section>
}
} }
</div> </div>
} @else { } @else {
@@ -634,6 +651,23 @@
</div> </div>
</div> </div>
} }
@if (relatedProducts().length > 0) {
<section class="related-products" aria-labelledby="dx-related-products-title">
<h2 id="dx-related-products-title">{{ 'itemDetail.relatedProducts' | translate }}</h2>
<div class="related-products-grid">
@for (related of relatedProducts(); track related.itemID) {
<app-product-card
[item]="related"
[title]="relatedItemName(related)"
[addToCartLabel]="'itemDetail.addToCart' | translate"
appearance="compact"
(addToCart)="addRelatedToCart($event.itemID, $event.event)"
/>
}
</div>
</section>
}
} }
</div> </div>
} }

View File

@@ -65,6 +65,23 @@ $dx-card-bg: #f5f3f9;
margin-bottom: 48px; margin-bottom: 48px;
} }
.related-products {
margin-top: 48px;
h2 {
margin: 0 0 20px;
color: $dx-dark;
font-size: 1.5rem;
font-weight: 800;
}
}
.related-products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 24px;
}
// Gallery: thumbnails left + main photo right // Gallery: thumbnails left + main photo right
.dx-gallery { .dx-gallery {
display: flex; display: flex;

View File

@@ -14,10 +14,11 @@ import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service'; import { TranslateService } from '../../i18n/translate.service';
import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade'; import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade';
import { ProductFacade } from '../../facades/platform/product.facade'; import { ProductFacade } from '../../facades/platform/product.facade';
import { ProductCardComponent } from '../../components/product-card/product-card.component';
@Component({ @Component({
selector: 'app-item-detail', selector: 'app-item-detail',
imports: [DecimalPipe, RouterLink, FormsModule, LangRoutePipe, TranslatePipe], imports: [DecimalPipe, RouterLink, FormsModule, LangRoutePipe, TranslatePipe, ProductCardComponent],
templateUrl: './item-detail.component.html', templateUrl: './item-detail.component.html',
styleUrls: ['./item-detail.component.scss'], styleUrls: ['./item-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
@@ -25,6 +26,7 @@ import { ProductFacade } from '../../facades/platform/product.facade';
export class ItemDetailComponent implements OnInit, OnDestroy { export class ItemDetailComponent implements OnInit, OnDestroy {
item = signal<Item | null>(null); item = signal<Item | null>(null);
selectedPhotoIndex = signal(0); selectedPhotoIndex = signal(0);
relatedProducts = signal<Item[]>([]);
loading = signal(true); loading = signal(true);
error = signal<string | null>(null); error = signal<string | null>(null);
private readonly uiRuntime = inject(UiRuntimeFacade); private readonly uiRuntime = inject(UiRuntimeFacade);
@@ -139,6 +141,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
this.item.set(item); this.item.set(item);
this.initVariantSelection(item); this.initVariantSelection(item);
this.seoService.setItemMeta(item); this.seoService.setItemMeta(item);
this.loadRelatedProducts(item);
this.loading.set(false); this.loading.set(false);
}, },
error: (err) => { error: (err) => {
@@ -163,6 +166,17 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
} }
} }
private loadRelatedProducts(item: Item): void {
this.productFacade.getRelatedProducts({
productID: item.itemID,
categoryID: item.categoryID,
count: 8
}).subscribe({
next: (result) => this.relatedProducts.set(result.items),
error: () => this.relatedProducts.set([])
});
}
selectColour(colour: string): void { selectColour(colour: string): void {
this.selectedColour.set(colour); this.selectedColour.set(colour);
// If current size is not available for the new colour, reset to first available // If current size is not available for the new colour, reset to first available
@@ -192,6 +206,12 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
} }
} }
addRelatedToCart(itemID: number, event: Event): void {
event.preventDefault();
event.stopPropagation();
this.cartService.addItem(itemID);
}
getDiscountedPrice(): number { getDiscountedPrice(): number {
const currentItem = this.item(); const currentItem = this.item();
if (!currentItem) return 0; if (!currentItem) return 0;
@@ -221,6 +241,10 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
return getTranslatedField(currentItem, 'name', lang); return getTranslatedField(currentItem, 'name', lang);
} }
relatedItemName(item: Item): string {
return getTranslatedField(item, 'name', this.languageService.currentLanguage());
}
getSimpleDescription(): string { getSimpleDescription(): string {
const currentItem = this.item(); const currentItem = this.item();
if (!currentItem) return ''; if (!currentItem) return '';