feat(carousel): implement reusable carousel for product list and enhance layout options

This commit is contained in:
2026-07-23 13:27:07 -03:00
parent 251411d200
commit 862751fec8
5 changed files with 98 additions and 41 deletions

View File

@@ -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) {

View File

@@ -32,6 +32,15 @@
&__item {
min-width: 0;
&--carousel {
width: clamp(15rem, 30vw, 18rem);
height: 100%;
}
&--carousel-row {
width: clamp(18rem, 70vw, 32rem);
}
}
}

View File

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

View File

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