feat(store-home-page): refactor to use catalog structure and update product loading logic

This commit is contained in:
2026-07-20 16:08:18 -03:00
parent 5165ea0716
commit b0e97fc577
8 changed files with 261 additions and 345 deletions

View File

@@ -1,3 +1,5 @@
import { ApiPaginatedResponse } from '../api-paginated-response.interface';
export interface Product {
id: number;
category_id: number;
@@ -53,3 +55,29 @@ export interface ProductDetail extends Product {
variants_map: ProductVariantMap[];
variant: ProductVariant | null;
}
export type CatalogProductLayout = 'row' | 'column_with_image' | 'column_with_cart';
export interface CatalogFeaturedItemVariant {
id: number;
stock_tecnico: number | null;
values: Record<string, string>;
}
export interface CatalogFeaturedItem {
id: number;
nombre: string;
descripcion?: string | null;
precio: number | string;
image?: string | null;
stock_tecnico?: number | null;
variants?: CatalogFeaturedItemVariant[];
}
export interface CatalogFeaturedGroup {
id: number;
title: string;
layout: CatalogProductLayout;
group_order: number;
items: ApiPaginatedResponse<CatalogFeaturedItem[]>;
}

View File

@@ -6,16 +6,17 @@ import { ApiPaginationQueryParams } from '../api-pagination-query-params.interfa
import { ApiPaginatedResponse } from '../api-paginated-response.interface';
import { ApiResponse } from '../api-response.interface';
import { TenantService } from '../tenant.service';
import { Product, ProductDetail } from './catalog.interface';
import {
CatalogFeaturedGroup,
CatalogFeaturedItem,
Product,
ProductDetail,
} from './catalog.interface';
type HttpParamValue =
| string
| number
| boolean
| readonly (string | number | boolean)[];
type HttpParamValue = string | number | boolean | readonly (string | number | boolean)[];
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class CatalogService {
private readonly http = inject(HttpClient);
@@ -25,13 +26,23 @@ export class CatalogService {
return this.tenantService.getTenantApiUrl();
}
getProductos(params?: ApiPaginationQueryParams): Observable<ApiPaginatedResponse<Product[]>> {
return this.http.get<ApiPaginatedResponse<Product[]>>(`${this.tenantApiUrl}/productos`, {
params: this.buildHttpParams(params),
});
}
getProductos(
params?: ApiPaginationQueryParams
): Observable<ApiPaginatedResponse<Product[]>> {
return this.http.get<ApiPaginatedResponse<Product[]>>(
`${this.tenantApiUrl}/productos`,
{ params: this.buildHttpParams(params) }
getCatalog(): Observable<CatalogFeaturedGroup[]> {
return this.http.get<CatalogFeaturedGroup[]>(`${this.tenantApiUrl}/catalog`);
}
getFeaturedGroupItems(
featuredGroupId: number,
params?: ApiPaginationQueryParams,
): Observable<ApiPaginatedResponse<CatalogFeaturedItem[]>> {
return this.http.get<ApiPaginatedResponse<CatalogFeaturedItem[]>>(
`${this.tenantApiUrl}/catalog/featured-groups/${featuredGroupId}/items`,
{ params: this.buildHttpParams(params) },
);
}
@@ -40,7 +51,7 @@ export class CatalogService {
if (variantId) {
params = params.set('variant_id', variantId);
}
return this.http
.get<ApiResponse<ProductDetail>>(`${this.tenantApiUrl}/productos/${id}`, { params })
.pipe(map((response) => response.data));
@@ -59,7 +70,7 @@ export class CatalogService {
return acc;
},
{}
{},
);
return new HttpParams({ fromObject });

View File

@@ -1,51 +1,28 @@
@if (tenant()?.hero_config || tenant()?.event_config) {
<app-hero-banner
<app-hero-banner
[heroConfig]="tenant()?.hero_config"
[eventConfig]="tenant()?.event_config"
></app-hero-banner>
}
<app-store-section title="Productos">
<div class="d-grid gap-4">
@if (error()) {
<p class="alert text-center mb-0 store-home__alert-error">{{ error() }}</p>
} @else if (loading() && !productCards().length) {
<p class="alert alert-light border text-center mb-0">Cargando productos...</p>
} @else if (!productCards().length) {
<p class="alert alert-light border text-center mb-0">
No hay productos disponibles en este momento.
</p>
} @else {
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-4 g-4">
@for (product of productCards(); track product.id; let index = $index) {
<div class="col">
<app-product-column-with-image
[title]="product.title"
[originalPrice]="product.originalPrice"
[discount]="product.discount"
[transferPrice]="product.transferPrice"
[imageUrl]="product.imageUrl"
[imagePriority]="index < priorityImageCount"
(buy)="onBuyProduct(product.id)"
/>
</div>
}
</div>
}
@if (loading() && productCards().length) {
<p class="alert alert-light border text-center mb-0 py-2">Actualizando productos...</p>
}
@if (totalPages() > 1) {
<div class="d-flex justify-content-center">
<app-paginator
[page]="currentPage()"
[totalPages]="totalPages()"
[disabled]="loading()"
(pageChange)="onPageChange($event)"
/>
</div>
}
</div>
</app-store-section>
@if (error()) {
<p class="alert text-center mb-0 store-home__alert-error">{{ error() }}</p>
} @else if (loading()) {
<p class="alert alert-light border text-center mb-0">Cargando productos...</p>
} @else if (!catalog().length) {
<p class="alert alert-light border text-center mb-0">
No hay productos disponibles en este momento.
</p>
} @else {
@for (group of catalog(); track group.id) {
<app-store-section [title]="group.title">
<app-product-list
[layout]="group.layout"
[items]="group.items"
[loading]="isGroupLoading(group.id)"
(buy)="onBuyProduct($event)"
(pageChange)="onPageChange(group.id, $event)"
/>
</app-store-section>
}
}

View File

@@ -4,196 +4,157 @@ import { Subject, of } from 'rxjs';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ApiPaginatedResponse } from '../../../../core/services/api-paginated-response.interface';
import { Product } from '../../../../core/services/catalog/catalog.interface';
import {
CatalogFeaturedGroup,
CatalogFeaturedItem,
} from '../../../../core/services/catalog/catalog.interface';
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
import { StoreHomePageComponent } from './store-home-page.component';
import {
STORE_HOME_PRODUCTS_ERROR_MESSAGE,
StoreHomeProductsResolvedData,
StoreHomeCatalogResolvedData,
} from './store-home-page.resolver';
function createPaginatedResponse(
products: Product[],
currentPage: number,
lastPage: number,
): ApiPaginatedResponse<Product[]> {
function createItemsPage(
items: CatalogFeaturedItem[],
currentPage = 1,
lastPage = 1,
): ApiPaginatedResponse<CatalogFeaturedItem[]> {
return {
data: products,
data: items,
meta: {
current_page: currentPage,
from: products.length ? 1 : null,
from: items.length ? 1 : null,
last_page: lastPage,
links: [],
path: '/productos',
path: '/catalog/featured-groups/1/items',
per_page: 12,
to: products.length || null,
total: products.length,
to: items.length || null,
total: items.length,
},
links: {
first: '/productos?page=1',
last: `/productos?page=${lastPage}`,
prev: currentPage > 1 ? `/productos?page=${currentPage - 1}` : null,
next: currentPage < lastPage ? `/productos?page=${currentPage + 1}` : null,
first: '/catalog/featured-groups/1/items?page=1',
last: `/catalog/featured-groups/1/items?page=${lastPage}`,
prev: currentPage > 1 ? `/catalog/featured-groups/1/items?page=${currentPage - 1}` : null,
next:
currentPage < lastPage ? `/catalog/featured-groups/1/items?page=${currentPage + 1}` : null,
},
};
}
function createResolvedData(
response: ApiPaginatedResponse<Product[]>,
): StoreHomeProductsResolvedData {
return {
response,
error: null,
};
}
function provideActivatedRoute(productsData: StoreHomeProductsResolvedData) {
return {
provide: ActivatedRoute,
useValue: {
snapshot: {
data: { productsData },
},
},
};
}
describe('StoreHomePageComponent', () => {
const pageOneProducts: Product[] = [
function createCatalog(
items = pageOneItems,
currentPage = 1,
lastPage = 2,
): CatalogFeaturedGroup[] {
return [
{
id: 1,
category_id: 10,
brand_id: null,
slug: 'auriculares-bluetooth',
nombre: 'Auriculares Bluetooth',
descripcion: 'Auriculares bluetooth de prueba',
precio: '24999',
category: 'Tecnologia',
brand: null,
images: ['/catalog/auriculares.jpg'],
},
{
id: 2,
category_id: 11,
brand_id: null,
slug: 'teclado-mecanico',
nombre: 'Teclado Mecanico',
descripcion: 'Teclado mecanico de prueba',
precio: '18999',
category: 'Tecnologia',
brand: null,
images: [],
id: 7,
title: 'Destacados',
layout: 'column_with_image',
group_order: 0,
items: createItemsPage(items, currentPage, lastPage),
},
];
}
beforeEach(() => {
vi.restoreAllMocks();
});
function provideActivatedRoute(catalogData: StoreHomeCatalogResolvedData) {
return {
provide: ActivatedRoute,
useValue: { snapshot: { data: { catalogData } } },
};
}
it('renders the products resolved by the route before component init', async () => {
const pageOneItems: CatalogFeaturedItem[] = [
{
id: 1,
nombre: 'Auriculares Bluetooth',
precio: '24999.00',
image: '/catalog/auriculares.jpg',
},
{
id: 2,
nombre: 'Teclado Mecanico',
precio: '18999.00',
image: null,
},
];
describe('StoreHomePageComponent', () => {
beforeEach(() => vi.restoreAllMocks());
it('renders the CatalogNew groups resolved by the route', async () => {
const catalogServiceStub = {
getProductos: vi.fn(),
getCatalog: vi.fn(),
getFeaturedGroupItems: vi.fn(),
};
await TestBed.configureTestingModule({
imports: [StoreHomePageComponent],
providers: [
provideActivatedRoute(createResolvedData(createPaginatedResponse(pageOneProducts, 1, 3))),
{
provide: CatalogService,
useValue: catalogServiceStub,
},
provideActivatedRoute({ response: createCatalog(), error: null }),
{ provide: CatalogService, useValue: catalogServiceStub },
],
}).compileComponents();
const fixture = TestBed.createComponent(StoreHomePageComponent);
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
expect(catalogServiceStub.getProductos).not.toHaveBeenCalled();
expect(element.querySelector('.store-section__title')?.textContent?.trim()).toBe('Productos');
expect(catalogServiceStub.getCatalog).not.toHaveBeenCalled();
expect(element.querySelector('.store-section__title')?.textContent?.trim()).toBe('Destacados');
expect(element.querySelectorAll('app-product-column-with-image')).toHaveLength(2);
expect(element.textContent).toContain('Auriculares Bluetooth');
expect(element.textContent).toContain('Teclado Mecanico');
expect(element.querySelector('[data-testid="paginator-status"]')?.textContent?.trim()).toBe(
'1/3',
'1/2',
);
expect(element.querySelector('img')?.getAttribute('fetchpriority')).toBe('high');
});
it('requests the next page when the paginator emits a page change', async () => {
it('requests another page for the selected featured group', async () => {
const pageTwoItems = [{ id: 3, nombre: 'Mouse Gamer', precio: '15999.00', image: null }];
const catalogServiceStub = {
getProductos: vi.fn().mockReturnValueOnce(
of(
createPaginatedResponse(
[
{
id: 3,
category_id: 12,
brand_id: null,
slug: 'mouse-gamer',
nombre: 'Mouse Gamer',
descripcion: 'Mouse gamer de prueba',
precio: '15999',
category: 'Tecnologia',
brand: null,
images: [],
},
],
2,
3,
),
),
),
getCatalog: vi.fn(),
getFeaturedGroupItems: vi.fn().mockReturnValue(of(createItemsPage(pageTwoItems, 2, 2))),
};
await TestBed.configureTestingModule({
imports: [StoreHomePageComponent],
providers: [
provideActivatedRoute(createResolvedData(createPaginatedResponse(pageOneProducts, 1, 3))),
{
provide: CatalogService,
useValue: catalogServiceStub,
},
provideActivatedRoute({ response: createCatalog(), error: null }),
{ provide: CatalogService, useValue: catalogServiceStub },
],
}).compileComponents();
const fixture = TestBed.createComponent(StoreHomePageComponent);
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
(element.querySelector('[data-testid="paginator-next"]') as HTMLButtonElement).click();
fixture.detectChanges();
expect(catalogServiceStub.getProductos).toHaveBeenCalledTimes(1);
expect(catalogServiceStub.getProductos).toHaveBeenCalledWith({ page: 2 });
expect(element.querySelector('[data-testid="paginator-status"]')?.textContent?.trim()).toBe(
'2/3',
);
expect(catalogServiceStub.getFeaturedGroupItems).toHaveBeenCalledWith(7, { page: 2 });
expect(element.textContent).toContain('Mouse Gamer');
expect(element.querySelector('[data-testid="paginator-status"]')?.textContent?.trim()).toBe(
'2/2',
);
});
it('disables the paginator while a new page request is in flight', async () => {
const nextPageSubject = new Subject<ApiPaginatedResponse<Product[]>>();
it('disables only the group paginator while its page request is in flight', async () => {
const nextPage = new Subject<ApiPaginatedResponse<CatalogFeaturedItem[]>>();
const catalogServiceStub = {
getProductos: vi.fn().mockReturnValueOnce(nextPageSubject.asObservable()),
getCatalog: vi.fn(),
getFeaturedGroupItems: vi.fn().mockReturnValue(nextPage.asObservable()),
};
await TestBed.configureTestingModule({
imports: [StoreHomePageComponent],
providers: [
provideActivatedRoute(createResolvedData(createPaginatedResponse(pageOneProducts, 1, 3))),
{
provide: CatalogService,
useValue: catalogServiceStub,
},
provideActivatedRoute({ response: createCatalog(), error: null }),
{ provide: CatalogService, useValue: catalogServiceStub },
],
}).compileComponents();
const fixture = TestBed.createComponent(StoreHomePageComponent);
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
(element.querySelector('[data-testid="paginator-next"]') as HTMLButtonElement).click();
fixture.detectChanges();
@@ -203,10 +164,9 @@ describe('StoreHomePageComponent', () => {
(button) => (button as HTMLButtonElement).disabled,
),
).toBe(true);
expect(element.textContent).toContain('Actualizando productos...');
nextPageSubject.next(createPaginatedResponse(pageOneProducts, 2, 3));
nextPageSubject.complete();
nextPage.next(createItemsPage(pageOneItems, 2, 2));
nextPage.complete();
fixture.detectChanges();
expect(
@@ -216,58 +176,41 @@ describe('StoreHomePageComponent', () => {
).toBe(true);
});
it('shows an empty-state message and hides the paginator when there are no products', async () => {
const catalogServiceStub = {
getProductos: vi.fn(),
};
it('shows an empty state when CatalogNew returns no groups', async () => {
const catalogServiceStub = { getCatalog: vi.fn(), getFeaturedGroupItems: vi.fn() };
await TestBed.configureTestingModule({
imports: [StoreHomePageComponent],
providers: [
provideActivatedRoute(createResolvedData(createPaginatedResponse([], 1, 1))),
{
provide: CatalogService,
useValue: catalogServiceStub,
},
provideActivatedRoute({ response: [], error: null }),
{ provide: CatalogService, useValue: catalogServiceStub },
],
}).compileComponents();
const fixture = TestBed.createComponent(StoreHomePageComponent);
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).toContain('No hay productos disponibles en este momento.');
expect(element.querySelector('app-paginator')).toBeNull();
expect(catalogServiceStub.getProductos).not.toHaveBeenCalled();
expect((fixture.nativeElement as HTMLElement).textContent).toContain(
'No hay productos disponibles en este momento.',
);
});
it('shows an error message when the route resolver cannot load the catalog', async () => {
const catalogServiceStub = {
getProductos: vi.fn(),
};
it('shows an error when the catalog resolver fails', async () => {
const catalogServiceStub = { getCatalog: vi.fn(), getFeaturedGroupItems: vi.fn() };
await TestBed.configureTestingModule({
imports: [StoreHomePageComponent],
providers: [
provideActivatedRoute({
response: null,
error: STORE_HOME_PRODUCTS_ERROR_MESSAGE,
}),
{
provide: CatalogService,
useValue: catalogServiceStub,
},
provideActivatedRoute({ response: null, error: STORE_HOME_PRODUCTS_ERROR_MESSAGE }),
{ provide: CatalogService, useValue: catalogServiceStub },
],
}).compileComponents();
const fixture = TestBed.createComponent(StoreHomePageComponent);
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).toContain('No pudimos cargar los productos en este momento.');
expect(element.querySelectorAll('app-product-column-with-image')).toHaveLength(0);
expect(catalogServiceStub.getProductos).not.toHaveBeenCalled();
expect((fixture.nativeElement as HTMLElement).textContent).toContain(
'No pudimos cargar los productos en este momento.',
);
});
});

View File

@@ -3,43 +3,29 @@ import {
Component,
OnDestroy,
OnInit,
computed,
inject,
signal,
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { ApiPaginatedResponse } from '../../../../core/services/api-paginated-response.interface';
import { CatalogFeaturedGroup } from '../../../../core/services/catalog/catalog.interface';
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
import { Product } from '../../../../core/services/catalog/catalog.interface';
import { ProductColumnWithImageComponent } from '../../../../shared/components/product-column-with-image/product-column-with-image.component';
import { PaginatorComponent } from '../../../../shared/components/paginator/paginator.component';
import { StoreSectionComponent } from '../../../../shared/components/store-section/store-section.component';
import { HeroBannerComponent } from '../../../../shared/components/hero-banner/hero-banner.component';
import { TenantService } from '../../../../core/services/tenant.service';
import {
ProductListComponent,
ProductListItem,
} from '../../../../shared/components/product-list/product-list.component';
import { HeroBannerComponent } from '../../../../shared/components/hero-banner/hero-banner.component';
import { StoreSectionComponent } from '../../../../shared/components/store-section/store-section.component';
import {
STORE_HOME_PRODUCTS_ERROR_MESSAGE,
StoreHomeProductsResolvedData,
StoreHomeCatalogResolvedData,
} from './store-home-page.resolver';
interface StoreHomeProductCardViewModel {
id: number;
title: string;
originalPrice: number;
discount: null;
transferPrice: null;
imageUrl: string | null;
}
@Component({
selector: 'app-store-home-page',
imports: [
StoreSectionComponent,
ProductColumnWithImageComponent,
PaginatorComponent,
HeroBannerComponent,
],
imports: [StoreSectionComponent, ProductListComponent, HeroBannerComponent],
templateUrl: './store-home-page.component.html',
styleUrl: './store-home-page.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -51,120 +37,101 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
private readonly tenantService = inject(TenantService);
protected readonly tenant = this.tenantService.tenant;
private activeRequestId = 0;
private productsRequestSubscription: Subscription | null = null;
protected readonly priorityImageCount = 4;
protected readonly currentPage = signal(1);
protected readonly products = signal<Product[]>([]);
protected readonly totalPages = signal(0);
protected readonly catalog = signal<CatalogFeaturedGroup[]>([]);
protected readonly loading = signal(false);
protected readonly loadingGroupIds = signal<ReadonlySet<number>>(new Set());
protected readonly error = signal<string | null>(null);
protected readonly productCards = computed<StoreHomeProductCardViewModel[]>(() =>
this.products().map((product) => ({
id: product.id,
title: product.nombre,
originalPrice: this.parseProductPrice(product.precio),
discount: null,
transferPrice: null,
imageUrl: product.images?.[0] ?? null,
})),
);
private catalogRequestSubscription: Subscription | null = null;
private readonly groupRequestSubscriptions = new Map<number, Subscription>();
ngOnInit(): void {
const resolvedData = this.route.snapshot.data['productsData'] as
| StoreHomeProductsResolvedData
const resolvedData = this.route.snapshot.data['catalogData'] as
| StoreHomeCatalogResolvedData
| undefined;
if (resolvedData) {
this.applyResolvedData(resolvedData);
return;
}
this.loadProducts(1);
this.loadCatalog();
}
ngOnDestroy(): void {
this.productsRequestSubscription?.unsubscribe();
this.catalogRequestSubscription?.unsubscribe();
this.groupRequestSubscriptions.forEach((subscription) => subscription.unsubscribe());
}
protected onPageChange(page: number): void {
if (page === this.currentPage() || page < 1) {
protected isGroupLoading(groupId: number): boolean {
return this.loadingGroupIds().has(groupId);
}
protected onPageChange(groupId: number, page: number): void {
const group = this.catalog().find((candidate) => candidate.id === groupId);
if (
!group ||
page < 1 ||
page === group.items.meta.current_page ||
this.isGroupLoading(groupId)
) {
return;
}
this.loadProducts(page);
}
this.setGroupLoading(groupId, true);
this.groupRequestSubscriptions.get(groupId)?.unsubscribe();
protected onBuyProduct(productId: number): void {
this.router.navigate(['/producto', productId]);
}
private loadProducts(page: number): void {
this.activeRequestId += 1;
const requestId = this.activeRequestId;
this.productsRequestSubscription?.unsubscribe();
this.loading.set(true);
this.error.set(null);
this.productsRequestSubscription = this.catalogService.getProductos({ page }).subscribe({
next: (response) => {
if (requestId !== this.activeRequestId) {
return;
}
this.applyProductsResponse(response);
const subscription = this.catalogService.getFeaturedGroupItems(groupId, { page }).subscribe({
next: (items) => {
this.catalog.update((groups) =>
groups.map((candidate) =>
candidate.id === groupId ? { ...candidate, items } : candidate,
),
);
this.error.set(null);
},
error: () => {
if (requestId !== this.activeRequestId) {
return;
}
this.error.set(STORE_HOME_PRODUCTS_ERROR_MESSAGE);
this.setGroupLoading(groupId, false);
},
complete: () => this.setGroupLoading(groupId, false),
});
this.products.set([]);
this.totalPages.set(0);
this.groupRequestSubscriptions.set(groupId, subscription);
}
protected onBuyProduct(product: ProductListItem): void {
this.router.navigate(['/producto', product.id]);
}
private loadCatalog(): void {
this.loading.set(true);
this.error.set(null);
this.catalogRequestSubscription?.unsubscribe();
this.catalogRequestSubscription = this.catalogService.getCatalog().subscribe({
next: (catalog) => this.catalog.set(catalog),
error: () => {
this.catalog.set([]);
this.loading.set(false);
this.error.set(STORE_HOME_PRODUCTS_ERROR_MESSAGE);
},
complete: () => {
if (requestId !== this.activeRequestId) {
return;
}
this.loading.set(false);
},
complete: () => this.loading.set(false),
});
}
private applyResolvedData(resolvedData: StoreHomeProductsResolvedData): void {
if (resolvedData.error) {
this.products.set([]);
this.totalPages.set(0);
this.loading.set(false);
this.error.set(resolvedData.error);
return;
}
if (resolvedData.response) {
this.applyProductsResponse(resolvedData.response);
this.loading.set(false);
this.error.set(null);
}
private applyResolvedData(resolvedData: StoreHomeCatalogResolvedData): void {
this.catalog.set(resolvedData.response ?? []);
this.loading.set(false);
this.error.set(resolvedData.error);
}
private applyProductsResponse(response: ApiPaginatedResponse<Product[]>): void {
this.products.set(response.data ?? []);
this.currentPage.set(response.meta.current_page);
this.totalPages.set(response.meta.last_page);
}
private parseProductPrice(price: string): number {
const parsedPrice = Number(price);
return Number.isFinite(parsedPrice) ? parsedPrice : 0;
private setGroupLoading(groupId: number, loading: boolean): void {
this.loadingGroupIds.update((current) => {
const updated = new Set(current);
loading ? updated.add(groupId) : updated.delete(groupId);
return updated;
});
}
}

View File

@@ -2,23 +2,22 @@ import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { catchError, map, of } from 'rxjs';
import { ApiPaginatedResponse } from '../../../../core/services/api-paginated-response.interface';
import { Product } from '../../../../core/services/catalog/catalog.interface';
import { CatalogFeaturedGroup } from '../../../../core/services/catalog/catalog.interface';
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
export const STORE_HOME_PRODUCTS_ERROR_MESSAGE = 'No pudimos cargar los productos en este momento.';
export interface StoreHomeProductsResolvedData {
response: ApiPaginatedResponse<Product[]> | null;
export interface StoreHomeCatalogResolvedData {
response: CatalogFeaturedGroup[] | null;
error: string | null;
}
export const storeHomeProductsResolver: ResolveFn<StoreHomeProductsResolvedData> = () => {
export const storeHomeCatalogResolver: ResolveFn<StoreHomeCatalogResolvedData> = () => {
return inject(CatalogService)
.getProductos({ page: 1 })
.getCatalog()
.pipe(
map(
(response): StoreHomeProductsResolvedData => ({
(response): StoreHomeCatalogResolvedData => ({
response,
error: null,
}),

View File

@@ -8,7 +8,7 @@ import { hasMenuGuard } from '../../core/guards/menu.guard';
import { productDetailResolver } from './pages/product-detail-page/product-detail-page.resolver';
import { RegisterPageComponent } from './pages/register-page/register-page.component';
import { StoreHomePageComponent } from './pages/store-home-page/store-home-page.component';
import { storeHomeProductsResolver } from './pages/store-home-page/store-home-page.resolver';
import { storeHomeCatalogResolver } from './pages/store-home-page/store-home-page.resolver';
export const routes: Routes = [
{
@@ -20,7 +20,7 @@ export const routes: Routes = [
component: StoreHomePageComponent,
canActivate: [hasMenuGuard('index')],
resolve: {
productsData: storeHomeProductsResolver,
catalogData: storeHomeCatalogResolver,
},
},
{

View File

@@ -1,6 +1,11 @@
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
import { ApiPaginatedResponse } from '../../../core/services/api-paginated-response.interface';
import {
CatalogFeaturedItem,
CatalogFeaturedItemVariant,
CatalogProductLayout,
} from '../../../core/services/catalog/catalog.interface';
import { PaginatorComponent } from '../paginator/paginator.component';
import { ProductColumnWithImageComponent } from '../product-column-with-image/product-column-with-image.component';
import {
@@ -9,23 +14,9 @@ import {
} from '../product-row-card/product-row-card.component';
import { ProductVerticalWithCartCardComponent } from '../product-vertical-with-cart-card/product-vertical-with-cart-card.component';
export type ProductListLayout = 'row' | 'column_with_image' | 'column_with_cart';
export interface ProductListVariant {
id: number;
stock_tecnico: number | null;
values: Record<string, string>;
}
export interface ProductListItem {
id: number;
nombre: string;
descripcion?: string | null;
precio: number | string;
image?: string | null;
stock_tecnico?: number | null;
variants?: ProductListVariant[];
}
export type ProductListLayout = CatalogProductLayout;
export type ProductListVariant = CatalogFeaturedItemVariant;
export type ProductListItem = CatalogFeaturedItem;
export interface ProductListCartEvent {
product: ProductListItem;