Add related products to detail page
This commit is contained in:
@@ -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...',
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ export const hy: Translations = {
|
|||||||
weeksAgo: 'շաբաթ առաջ',
|
weeksAgo: 'շաբաթ առաջ',
|
||||||
colour: 'Գույն',
|
colour: 'Գույն',
|
||||||
size: 'Չափ',
|
size: 'Չափ',
|
||||||
|
relatedProducts: 'Նման ապրանքներ',
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
connecting: 'Կապ սերվերի հետ...',
|
connecting: 'Կապ սերվերի հետ...',
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ export const ru: Translations = {
|
|||||||
weeksAgo: 'нед. назад',
|
weeksAgo: 'нед. назад',
|
||||||
colour: 'Цвет',
|
colour: 'Цвет',
|
||||||
size: 'Размер',
|
size: 'Размер',
|
||||||
|
relatedProducts: 'Похожие товары',
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
connecting: 'Подключение к серверу...',
|
connecting: 'Подключение к серверу...',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 '';
|
||||||
|
|||||||
Reference in New Issue
Block a user