feat(store-home): implement resolver for product data and enhance component logic for improved error handling and loading states
This commit is contained in:
@@ -1,42 +1,44 @@
|
||||
|
||||
<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) {
|
||||
<div class="col">
|
||||
<app-product-card
|
||||
[title]="product.title"
|
||||
[originalPrice]="product.originalPrice"
|
||||
[discount]="product.discount"
|
||||
[transferPrice]="product.transferPrice"
|
||||
[imageUrl]="product.imageUrl"
|
||||
(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)"
|
||||
<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-card
|
||||
[title]="product.title"
|
||||
[originalPrice]="product.originalPrice"
|
||||
[discount]="product.discount"
|
||||
[transferPrice]="product.transferPrice"
|
||||
[imageUrl]="product.imageUrl"
|
||||
[imagePriority]="index < priorityImageCount"
|
||||
(buy)="onBuyProduct(product.id)"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</app-store-section>
|
||||
}
|
||||
|
||||
@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>
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Subject, of, throwError } from 'rxjs';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
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 { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
||||
import { StoreHomePageComponent } from './store-home-page.component';
|
||||
import {
|
||||
STORE_HOME_PRODUCTS_ERROR_MESSAGE,
|
||||
StoreHomeProductsResolvedData,
|
||||
} from './store-home-page.resolver';
|
||||
|
||||
function createPaginatedResponse(
|
||||
products: Product[],
|
||||
currentPage: number,
|
||||
lastPage: number
|
||||
lastPage: number,
|
||||
): ApiPaginatedResponse<Product[]> {
|
||||
return {
|
||||
data: products,
|
||||
@@ -22,14 +27,34 @@ function createPaginatedResponse(
|
||||
path: '/productos',
|
||||
per_page: 12,
|
||||
to: products.length || null,
|
||||
total: products.length
|
||||
total: products.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
|
||||
}
|
||||
next: currentPage < lastPage ? `/productos?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 },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,9 +68,9 @@ describe('StoreHomePageComponent', () => {
|
||||
nombre: 'Auriculares Bluetooth',
|
||||
descripcion: 'Auriculares bluetooth de prueba',
|
||||
precio: '24999',
|
||||
category: 'Tecnología',
|
||||
category: 'Tecnologia',
|
||||
brand: null,
|
||||
images: []
|
||||
images: ['/catalog/auriculares.jpg'],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -53,31 +78,32 @@ describe('StoreHomePageComponent', () => {
|
||||
brand_id: null,
|
||||
slug: 'teclado-mecanico',
|
||||
nombre: 'Teclado Mecanico',
|
||||
descripcion: 'Teclado mecánico de prueba',
|
||||
descripcion: 'Teclado mecanico de prueba',
|
||||
precio: '18999',
|
||||
category: 'Tecnología',
|
||||
category: 'Tecnologia',
|
||||
brand: null,
|
||||
images: []
|
||||
}
|
||||
images: [],
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('loads page 1 on init, renders the products title and the fetched product cards', async () => {
|
||||
it('renders the products resolved by the route before component init', async () => {
|
||||
const catalogServiceStub = {
|
||||
getProductos: vi.fn().mockReturnValue(of(createPaginatedResponse(pageOneProducts, 1, 3)))
|
||||
getProductos: vi.fn(),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [StoreHomePageComponent],
|
||||
providers: [
|
||||
provideActivatedRoute(createResolvedData(createPaginatedResponse(pageOneProducts, 1, 3))),
|
||||
{
|
||||
provide: CatalogService,
|
||||
useValue: catalogServiceStub
|
||||
}
|
||||
]
|
||||
useValue: catalogServiceStub,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(StoreHomePageComponent);
|
||||
@@ -85,51 +111,52 @@ describe('StoreHomePageComponent', () => {
|
||||
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(catalogServiceStub.getProductos).toHaveBeenCalledWith({ page: 1 });
|
||||
expect(catalogServiceStub.getProductos).not.toHaveBeenCalled();
|
||||
expect(element.querySelector('.store-section__title')?.textContent?.trim()).toBe('Productos');
|
||||
expect(element.querySelectorAll('app-product-card')).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');
|
||||
expect(element.querySelector('[data-testid="paginator-status"]')?.textContent?.trim()).toBe(
|
||||
'1/3',
|
||||
);
|
||||
expect(element.querySelector('img')?.getAttribute('fetchpriority')).toBe('high');
|
||||
});
|
||||
|
||||
it('requests the next page when the paginator emits a page change', async () => {
|
||||
const catalogServiceStub = {
|
||||
getProductos: vi
|
||||
.fn()
|
||||
.mockReturnValueOnce(of(createPaginatedResponse(pageOneProducts, 1, 3)))
|
||||
.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: 'Tecnología',
|
||||
brand: null,
|
||||
images: []
|
||||
}
|
||||
],
|
||||
2,
|
||||
3
|
||||
)
|
||||
)
|
||||
)
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [StoreHomePageComponent],
|
||||
providers: [
|
||||
provideActivatedRoute(createResolvedData(createPaginatedResponse(pageOneProducts, 1, 3))),
|
||||
{
|
||||
provide: CatalogService,
|
||||
useValue: catalogServiceStub
|
||||
}
|
||||
]
|
||||
useValue: catalogServiceStub,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(StoreHomePageComponent);
|
||||
@@ -139,29 +166,29 @@ describe('StoreHomePageComponent', () => {
|
||||
(element.querySelector('[data-testid="paginator-next"]') as HTMLButtonElement).click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(catalogServiceStub.getProductos).toHaveBeenNthCalledWith(1, { page: 1 });
|
||||
expect(catalogServiceStub.getProductos).toHaveBeenNthCalledWith(2, { page: 2 });
|
||||
expect(element.querySelector('[data-testid="paginator-status"]')?.textContent?.trim()).toBe('2/3');
|
||||
expect(catalogServiceStub.getProductos).toHaveBeenCalledTimes(1);
|
||||
expect(catalogServiceStub.getProductos).toHaveBeenCalledWith({ page: 2 });
|
||||
expect(element.querySelector('[data-testid="paginator-status"]')?.textContent?.trim()).toBe(
|
||||
'2/3',
|
||||
);
|
||||
expect(element.textContent).toContain('Mouse Gamer');
|
||||
});
|
||||
|
||||
it('disables the paginator while a new page request is in flight', async () => {
|
||||
const nextPageSubject = new Subject<ApiPaginatedResponse<Product[]>>();
|
||||
const catalogServiceStub = {
|
||||
getProductos: vi
|
||||
.fn()
|
||||
.mockReturnValueOnce(of(createPaginatedResponse(pageOneProducts, 1, 3)))
|
||||
.mockReturnValueOnce(nextPageSubject.asObservable())
|
||||
getProductos: vi.fn().mockReturnValueOnce(nextPageSubject.asObservable()),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [StoreHomePageComponent],
|
||||
providers: [
|
||||
provideActivatedRoute(createResolvedData(createPaginatedResponse(pageOneProducts, 1, 3))),
|
||||
{
|
||||
provide: CatalogService,
|
||||
useValue: catalogServiceStub
|
||||
}
|
||||
]
|
||||
useValue: catalogServiceStub,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(StoreHomePageComponent);
|
||||
@@ -173,8 +200,8 @@ describe('StoreHomePageComponent', () => {
|
||||
|
||||
expect(
|
||||
Array.from(element.querySelectorAll('app-paginator button')).every(
|
||||
(button) => (button as HTMLButtonElement).disabled
|
||||
)
|
||||
(button) => (button as HTMLButtonElement).disabled,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(element.textContent).toContain('Actualizando productos...');
|
||||
|
||||
@@ -184,24 +211,25 @@ describe('StoreHomePageComponent', () => {
|
||||
|
||||
expect(
|
||||
Array.from(element.querySelectorAll('app-paginator button')).some(
|
||||
(button) => !(button as HTMLButtonElement).disabled
|
||||
)
|
||||
(button) => !(button as HTMLButtonElement).disabled,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('shows an empty-state message and hides the paginator when there are no products', async () => {
|
||||
const catalogServiceStub = {
|
||||
getProductos: vi.fn().mockReturnValue(of(createPaginatedResponse([], 1, 1)))
|
||||
getProductos: vi.fn(),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [StoreHomePageComponent],
|
||||
providers: [
|
||||
provideActivatedRoute(createResolvedData(createPaginatedResponse([], 1, 1))),
|
||||
{
|
||||
provide: CatalogService,
|
||||
useValue: catalogServiceStub
|
||||
}
|
||||
]
|
||||
useValue: catalogServiceStub,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(StoreHomePageComponent);
|
||||
@@ -211,21 +239,26 @@ describe('StoreHomePageComponent', () => {
|
||||
|
||||
expect(element.textContent).toContain('No hay productos disponibles en este momento.');
|
||||
expect(element.querySelector('app-paginator')).toBeNull();
|
||||
expect(catalogServiceStub.getProductos).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows an error message when the catalog request fails', async () => {
|
||||
it('shows an error message when the route resolver cannot load the catalog', async () => {
|
||||
const catalogServiceStub = {
|
||||
getProductos: vi.fn().mockReturnValue(throwError(() => new Error('boom')))
|
||||
getProductos: vi.fn(),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [StoreHomePageComponent],
|
||||
providers: [
|
||||
provideActivatedRoute({
|
||||
response: null,
|
||||
error: STORE_HOME_PRODUCTS_ERROR_MESSAGE,
|
||||
}),
|
||||
{
|
||||
provide: CatalogService,
|
||||
useValue: catalogServiceStub
|
||||
}
|
||||
]
|
||||
useValue: catalogServiceStub,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(StoreHomePageComponent);
|
||||
@@ -235,5 +268,6 @@ describe('StoreHomePageComponent', () => {
|
||||
|
||||
expect(element.textContent).toContain('No pudimos cargar los productos en este momento.');
|
||||
expect(element.querySelectorAll('app-product-card')).toHaveLength(0);
|
||||
expect(catalogServiceStub.getProductos).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, computed, inject, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
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 { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
||||
import { Product } from '../../../../core/services/catalog/catalog.interface';
|
||||
import { ProductCardComponent } from '../../../../shared/components/product-card/product-card.component';
|
||||
import { PaginatorComponent } from '../../../../shared/components/paginator/paginator.component';
|
||||
import { StoreSectionComponent } from '../../../../shared/components/store-section/store-section.component';
|
||||
import {
|
||||
STORE_HOME_PRODUCTS_ERROR_MESSAGE,
|
||||
StoreHomeProductsResolvedData,
|
||||
} from './store-home-page.resolver';
|
||||
|
||||
interface StoreHomeProductCardViewModel {
|
||||
id: number;
|
||||
@@ -22,14 +35,16 @@ interface StoreHomeProductCardViewModel {
|
||||
imports: [StoreSectionComponent, ProductCardComponent, PaginatorComponent],
|
||||
templateUrl: './store-home-page.component.html',
|
||||
styleUrl: './store-home-page.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class StoreHomePageComponent implements OnInit, OnDestroy {
|
||||
private readonly catalogService = inject(CatalogService);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
private activeRequestId = 0;
|
||||
private productsRequestSubscription: Subscription | null = null;
|
||||
protected readonly priorityImageCount = 4;
|
||||
|
||||
protected readonly currentPage = signal(1);
|
||||
protected readonly products = signal<Product[]>([]);
|
||||
@@ -44,11 +59,21 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
|
||||
originalPrice: this.parseProductPrice(product.precio),
|
||||
discount: null,
|
||||
transferPrice: null,
|
||||
imageUrl: product.images?.[0] ?? null
|
||||
}))
|
||||
imageUrl: product.images?.[0] ?? null,
|
||||
})),
|
||||
);
|
||||
|
||||
ngOnInit(): void {
|
||||
const resolvedData = this.route.snapshot.data['productsData'] as
|
||||
| StoreHomeProductsResolvedData
|
||||
| undefined;
|
||||
|
||||
if (resolvedData) {
|
||||
this.applyResolvedData(resolvedData);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadProducts(1);
|
||||
}
|
||||
|
||||
@@ -82,9 +107,7 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
this.products.set(response.data ?? []);
|
||||
this.currentPage.set(response.meta.current_page);
|
||||
this.totalPages.set(response.meta.last_page);
|
||||
this.applyProductsResponse(response);
|
||||
},
|
||||
error: () => {
|
||||
if (requestId !== this.activeRequestId) {
|
||||
@@ -94,7 +117,7 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
|
||||
this.products.set([]);
|
||||
this.totalPages.set(0);
|
||||
this.loading.set(false);
|
||||
this.error.set('No pudimos cargar los productos en este momento.');
|
||||
this.error.set(STORE_HOME_PRODUCTS_ERROR_MESSAGE);
|
||||
},
|
||||
complete: () => {
|
||||
if (requestId !== this.activeRequestId) {
|
||||
@@ -102,10 +125,33 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
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 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);
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
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 { 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;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export const storeHomeProductsResolver: ResolveFn<StoreHomeProductsResolvedData> = () => {
|
||||
return inject(CatalogService)
|
||||
.getProductos({ page: 1 })
|
||||
.pipe(
|
||||
map(
|
||||
(response): StoreHomeProductsResolvedData => ({
|
||||
response,
|
||||
error: null,
|
||||
}),
|
||||
),
|
||||
catchError(() =>
|
||||
of({
|
||||
response: null,
|
||||
error: STORE_HOME_PRODUCTS_ERROR_MESSAGE,
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { authGuard, guestOnlyGuard } from '../../core/services/auth/auth.guards'
|
||||
import { LoginPageComponent } from './pages/login-page/login-page.component';
|
||||
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';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -14,7 +15,10 @@ export const routes: Routes = [
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: StoreHomePageComponent
|
||||
component: StoreHomePageComponent,
|
||||
resolve: {
|
||||
productsData: storeHomeProductsResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
|
||||
Reference in New Issue
Block a user