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