feat(carousel): implement reusable carousel for product list and enhance layout options
This commit is contained in:
@@ -1,43 +1,67 @@
|
||||
<div
|
||||
class="product-list"
|
||||
[class.product-list--row]="effectiveLayout() === 'row'"
|
||||
[class.product-list--column]="effectiveLayout() !== 'row'"
|
||||
>
|
||||
@for (item of itemData(); track item.id; let index = $index) {
|
||||
<div class="product-list__item">
|
||||
@switch (effectiveLayout()) {
|
||||
@case ('row') {
|
||||
<app-product-row-card
|
||||
[title]="item.nombre"
|
||||
[description]="item.descripcion ?? ''"
|
||||
[price]="price(item)"
|
||||
[variants]="variantsFor(item)"
|
||||
(buy)="buy.emit(item)"
|
||||
(addToCart)="emitRowCart(item, $event)"
|
||||
/>
|
||||
}
|
||||
@case ('column_with_cart') {
|
||||
<app-product-vertical-with-cart-card
|
||||
[title]="item.nombre"
|
||||
[description]="item.descripcion ?? ''"
|
||||
[price]="price(item)"
|
||||
(buy)="buy.emit(item)"
|
||||
(addToCart)="emitColumnCart(item, $event)"
|
||||
/>
|
||||
}
|
||||
@default {
|
||||
<app-product-column-with-image
|
||||
[imageUrl]="item.image ?? null"
|
||||
[title]="item.nombre"
|
||||
[originalPrice]="price(item)"
|
||||
[imagePriority]="index < 4"
|
||||
(buy)="buy.emit(item)"
|
||||
/>
|
||||
}
|
||||
<ng-template #productItem let-item let-index="index">
|
||||
<div
|
||||
class="product-list__item"
|
||||
[class.product-list__item--carousel]="groupLayout() === 'carousel'"
|
||||
[class.product-list__item--carousel-row]="
|
||||
groupLayout() === 'carousel' && effectiveLayout() === 'row'
|
||||
"
|
||||
>
|
||||
@switch (effectiveLayout()) {
|
||||
@case ('row') {
|
||||
<app-product-row-card
|
||||
[title]="item.nombre"
|
||||
[description]="item.descripcion ?? ''"
|
||||
[price]="price(item)"
|
||||
[variants]="variantsFor(item)"
|
||||
(buy)="buy.emit(item)"
|
||||
(addToCart)="emitRowCart(item, $event)"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@case ('column_with_cart') {
|
||||
<app-product-vertical-with-cart-card
|
||||
[title]="item.nombre"
|
||||
[description]="item.descripcion ?? ''"
|
||||
[price]="price(item)"
|
||||
(buy)="buy.emit(item)"
|
||||
(addToCart)="emitColumnCart(item, $event)"
|
||||
/>
|
||||
}
|
||||
@default {
|
||||
<app-product-column-with-image
|
||||
[imageUrl]="item.image ?? null"
|
||||
[title]="item.nombre"
|
||||
[originalPrice]="price(item)"
|
||||
[imagePriority]="index < 4"
|
||||
(buy)="buy.emit(item)"
|
||||
/>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@if (groupLayout() === 'carousel') {
|
||||
<app-carousel
|
||||
[items]="itemData()"
|
||||
[itemTemplate]="productItem"
|
||||
[trackBy]="trackProduct"
|
||||
[gap]="24"
|
||||
[circular]="true"
|
||||
ariaLabel="Productos destacados"
|
||||
/>
|
||||
} @else {
|
||||
<div
|
||||
class="product-list"
|
||||
[class.product-list--row]="effectiveLayout() === 'row'"
|
||||
[class.product-list--column]="effectiveLayout() !== 'row'"
|
||||
>
|
||||
@for (item of itemData(); track item.id; let index = $index) {
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="productItem"
|
||||
[ngTemplateOutletContext]="{ $implicit: item, index }"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (pagination(); as paginatedItems) {
|
||||
@if (paginatedItems.meta.last_page > 1) {
|
||||
|
||||
@@ -32,6 +32,15 @@
|
||||
|
||||
&__item {
|
||||
min-width: 0;
|
||||
|
||||
&--carousel {
|
||||
width: clamp(15rem, 30vw, 18rem);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&--carousel-row {
|
||||
width: clamp(18rem, 70vw, 32rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ApiPaginatedResponse } from '../../../core/services/api-paginated-response.interface';
|
||||
import { CatalogFeaturedItems } from '../../../core/services/catalog/catalog.interface';
|
||||
import {
|
||||
CatalogFeaturedItems,
|
||||
CatalogGroupLayout,
|
||||
} from '../../../core/services/catalog/catalog.interface';
|
||||
import { ProductListComponent, ProductListItem, ProductListLayout } from './product-list.component';
|
||||
|
||||
describe('ProductListComponent', () => {
|
||||
@@ -42,10 +45,12 @@ describe('ProductListComponent', () => {
|
||||
async function render(
|
||||
layout: ProductListLayout,
|
||||
productItems: CatalogFeaturedItems = paginatedItems(),
|
||||
groupLayout: CatalogGroupLayout = 'paginated',
|
||||
) {
|
||||
await TestBed.configureTestingModule({ imports: [ProductListComponent] }).compileComponents();
|
||||
const fixture = TestBed.createComponent(ProductListComponent);
|
||||
fixture.componentRef.setInput('layout', layout);
|
||||
fixture.componentRef.setInput('groupLayout', groupLayout);
|
||||
fixture.componentRef.setInput('items', productItems);
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -134,10 +139,21 @@ describe('ProductListComponent', () => {
|
||||
});
|
||||
|
||||
it('renders plain items without pagination', async () => {
|
||||
const fixture = await render('row', items);
|
||||
const fixture = await render('row', items, 'simple');
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(element.querySelectorAll('app-product-row-card')).toHaveLength(2);
|
||||
expect(element.querySelector('app-paginator')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders carousel groups with the reusable carousel', async () => {
|
||||
const fixture = await render('column_with_image', items, 'carousel');
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(element.querySelector('app-carousel')).not.toBeNull();
|
||||
expect(element.querySelector('.product-list')).toBeNull();
|
||||
expect(element.querySelectorAll('app-product-column-with-image')).toHaveLength(2);
|
||||
expect(element.querySelectorAll('.product-list__item--carousel')).toHaveLength(2);
|
||||
expect(element.querySelector('app-paginator')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,13 +9,16 @@ import {
|
||||
output,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { NgTemplateOutlet } from '@angular/common';
|
||||
|
||||
import {
|
||||
CatalogFeaturedItem,
|
||||
CatalogFeaturedItems,
|
||||
CatalogFeaturedItemVariant,
|
||||
CatalogGroupLayout,
|
||||
CatalogProductLayout,
|
||||
} from '../../../core/services/catalog/catalog.interface';
|
||||
import { CarouselComponent } from '../carousel/carousel.component';
|
||||
import { PaginatorComponent } from '../paginator/paginator.component';
|
||||
import { ProductColumnWithImageComponent } from '../product-column-with-image/product-column-with-image.component';
|
||||
import {
|
||||
@@ -37,6 +40,8 @@ export interface ProductListCartEvent {
|
||||
@Component({
|
||||
selector: 'app-product-list',
|
||||
imports: [
|
||||
CarouselComponent,
|
||||
NgTemplateOutlet,
|
||||
ProductColumnWithImageComponent,
|
||||
PaginatorComponent,
|
||||
ProductRowCardComponent,
|
||||
@@ -51,6 +56,7 @@ export class ProductListComponent {
|
||||
private readonly mobile = signal(false);
|
||||
|
||||
readonly layout = input.required<ProductListLayout>();
|
||||
readonly groupLayout = input.required<CatalogGroupLayout>();
|
||||
readonly items = input.required<CatalogFeaturedItems>();
|
||||
readonly loading = input(false);
|
||||
|
||||
@@ -71,6 +77,7 @@ export class ProductListComponent {
|
||||
|
||||
return Array.isArray(items) ? null : items;
|
||||
});
|
||||
protected readonly trackProduct = (_index: number, item: ProductListItem): number => item.id;
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
|
||||
Reference in New Issue
Block a user