diff --git a/src/app/core/services/catalog/catalog.interface.ts b/src/app/core/services/catalog/catalog.interface.ts index ab5d17d..ed700eb 100644 --- a/src/app/core/services/catalog/catalog.interface.ts +++ b/src/app/core/services/catalog/catalog.interface.ts @@ -11,3 +11,33 @@ export interface Product { brand: string | null; images: string[]; } + +export interface ProductAttributeOption { + id: number; + value: string; + label: string; + sort_order: number; + metadata: Record | null; +} + +export interface ProductAttribute { + id: number; + codigo: string; + nombre: string; + is_required: boolean; + metadata_schema: Record | null; + type: string; + options: ProductAttributeOption[]; +} + +export interface ProductVariant { + variant_id: number; + images: string[]; + attributes: Record; +} + +export interface ProductDetail extends Product { + attributes: ProductAttribute[]; + default_variant: ProductVariant | null; +} + diff --git a/src/app/core/services/catalog/catalog.service.ts b/src/app/core/services/catalog/catalog.service.ts index 89d5769..3a40197 100644 --- a/src/app/core/services/catalog/catalog.service.ts +++ b/src/app/core/services/catalog/catalog.service.ts @@ -6,7 +6,7 @@ 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 } from './catalog.interface'; +import { Product, ProductDetail } from './catalog.interface'; type HttpParamValue = | string @@ -35,9 +35,9 @@ export class CatalogService { ); } - getProducto(id: number): Observable { + getProducto(id: number): Observable { return this.http - .get>(`${this.tenantApiUrl}/productos/${id}`) + .get>(`${this.tenantApiUrl}/productos/${id}`) .pipe(map((response) => response.data)); } diff --git a/src/app/features/store/components/product-carousel/product-carousel.component.html b/src/app/features/store/components/product-carousel/product-carousel.component.html index 8b187b7..25cb4d3 100644 --- a/src/app/features/store/components/product-carousel/product-carousel.component.html +++ b/src/app/features/store/components/product-carousel/product-carousel.component.html @@ -6,7 +6,7 @@ Product active image } @else { @@ -55,7 +55,7 @@ (click)="selectImage(idx)" [attr.aria-label]="'Select image ' + (idx + 1)" > - Product thumbnail + Product thumbnail } diff --git a/src/app/features/store/components/product-carousel/product-carousel.component.scss b/src/app/features/store/components/product-carousel/product-carousel.component.scss index 9d06cea..24bd878 100644 --- a/src/app/features/store/components/product-carousel/product-carousel.component.scss +++ b/src/app/features/store/components/product-carousel/product-carousel.component.scss @@ -12,7 +12,7 @@ } &__main-image { - padding: 1rem; + padding: 0; transition: transform 0.3s ease; } @@ -65,7 +65,7 @@ aspect-ratio: 1 / 1; background-color: #e0e0e0 !important; /* Matches mock layout */ border-radius: 4px; - padding: 0.25rem; + padding: 0; transition: opacity 0.2s ease, border-color 0.2s ease; border: 3px solid transparent !important; cursor: pointer; @@ -73,7 +73,7 @@ img { width: 100%; height: 100%; - object-fit: contain; + object-fit: cover; } &:hover { diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.component.html b/src/app/features/store/pages/product-detail-page/product-detail-page.component.html index 1e87558..fb96610 100644 --- a/src/app/features/store/pages/product-detail-page/product-detail-page.component.html +++ b/src/app/features/store/pages/product-detail-page/product-detail-page.component.html @@ -14,7 +14,7 @@
- +
} @else { @@ -24,3 +24,5 @@ } + + diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.component.scss b/src/app/features/store/pages/product-detail-page/product-detail-page.component.scss index 40a8c17..ef37f26 100644 --- a/src/app/features/store/pages/product-detail-page/product-detail-page.component.scss +++ b/src/app/features/store/pages/product-detail-page/product-detail-page.component.scss @@ -1 +1,2 @@ /* empty */ + diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts b/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts index 4a2e571..0e05a83 100644 --- a/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts +++ b/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts @@ -1,15 +1,16 @@ import { TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; +import { By } from '@angular/platform-browser'; import { BehaviorSubject, of, throwError } from 'rxjs'; import { convertToParamMap } from '@angular/router'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { Product } from '../../../../core/services/catalog/catalog.interface'; +import { ProductDetail } from '../../../../core/services/catalog/catalog.interface'; import { CatalogService } from '../../../../core/services/catalog/catalog.service'; import { ProductDetailPageComponent } from './product-detail-page.component'; describe('ProductDetailPageComponent', () => { - const mockProduct: Product = { + const mockProduct: ProductDetail = { id: 1, tenant_codigo: 'test', category_id: 10, @@ -20,7 +21,9 @@ describe('ProductDetailPageComponent', () => { precio: '24999', category: 'TecnologĂ­a', brand: 'Sony', - images: ['https://example.com/image.png'] + images: ['https://example.com/image.png'], + attributes: [], + default_variant: null }; let paramMapSubject: BehaviorSubject; @@ -81,4 +84,57 @@ describe('ProductDetailPageComponent', () => { expect(element.textContent).toContain('No pudimos cargar los detalles del producto.'); }); + + it('should use default variant images if present', async () => { + const detailProduct: ProductDetail = { + ...mockProduct, + images: ['https://example.com/product.png'], + default_variant: { + variant_id: 123, + images: ['https://example.com/variant1.png', 'https://example.com/variant2.png'], + attributes: {} + } + }; + catalogServiceStub.getProducto.mockReturnValue(of(detailProduct)); + + await configureTestingModule(); + const fixture = TestBed.createComponent(ProductDetailPageComponent); + fixture.detectChanges(); + + const carousel = fixture.debugElement.query(By.css('app-product-carousel')).componentInstance; + expect(carousel.images()).toEqual(['https://example.com/variant1.png', 'https://example.com/variant2.png']); + }); + + it('should fallback to product images if default variant images are not present', async () => { + const detailProduct: ProductDetail = { + ...mockProduct, + images: ['https://example.com/product.png'], + default_variant: null + }; + catalogServiceStub.getProducto.mockReturnValue(of(detailProduct)); + + await configureTestingModule(); + const fixture = TestBed.createComponent(ProductDetailPageComponent); + fixture.detectChanges(); + + const carousel = fixture.debugElement.query(By.css('app-product-carousel')).componentInstance; + expect(carousel.images()).toEqual(['https://example.com/product.png']); + }); + + it('should fallback to empty array if no images are present at all', async () => { + const detailProduct: ProductDetail = { + ...mockProduct, + images: [], + default_variant: null + }; + catalogServiceStub.getProducto.mockReturnValue(of(detailProduct)); + + await configureTestingModule(); + const fixture = TestBed.createComponent(ProductDetailPageComponent); + fixture.detectChanges(); + + const carousel = fixture.debugElement.query(By.css('app-product-carousel')).componentInstance; + expect(carousel.images()).toEqual([]); + }); }); + diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts b/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts index 6652124..ae51147 100644 --- a/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts +++ b/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts @@ -1,10 +1,10 @@ -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, computed, inject, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { Subscription } from 'rxjs'; import { CatalogService } from '../../../../core/services/catalog/catalog.service'; -import { Product } from '../../../../core/services/catalog/catalog.interface'; +import { ProductDetail } from '../../../../core/services/catalog/catalog.interface'; import { ProductCarouselComponent } from '../../components/product-carousel/product-carousel.component'; @Component({ @@ -23,7 +23,18 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy { private routeSub: Subscription | null = null; private productSub: Subscription | null = null; - protected readonly product = signal(null); + protected readonly product = signal(null); + protected readonly carouselImages = computed(() => { + const prod = this.product(); + if (!prod) return []; + if (prod.default_variant?.images && prod.default_variant.images.length > 0) { + return prod.default_variant.images; + } + if (prod.images && prod.images.length > 0) { + return prod.images; + } + return []; + }); protected readonly loading = signal(false); protected readonly error = signal(null);