diff --git a/src/app/features/store/pages/store-home-page/store-home-page.component.html b/src/app/features/store/pages/store-home-page/store-home-page.component.html index 625108a..d62e95a 100644 --- a/src/app/features/store/pages/store-home-page/store-home-page.component.html +++ b/src/app/features/store/pages/store-home-page/store-home-page.component.html @@ -4,6 +4,7 @@ class="store-home__main-carousel" [images]="mainCarouselImages" ariaLabel="Imágenes destacadas de la tienda" + (firstImageReady)="mainCarouselReady.set(true)" /> } } @@ -31,6 +32,7 @@ [groupLayout]="group.group_layout" [items]="group.items" [loading]="isGroupLoading(group.id)" + [loadImages]="!hasMainCarouselImages() || mainCarouselReady()" (buy)="onBuyProduct($event)" (addToCart)="onAddToCart($event)" (pageChange)="onPageChange(group.id, $event)" diff --git a/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts b/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts index 7dd6171..b5218ad 100644 --- a/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts +++ b/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts @@ -176,7 +176,14 @@ describe('StoreHomePageComponent', () => { expect(element.firstElementChild).toBe(carousel); expect(images).toHaveLength(2); expect(images?.[0].getAttribute('src')).toBe(mainCarouselImages[0]); + expect(images?.[1].getAttribute('src')).toBeNull(); + expect(element.querySelectorAll('app-product-column-with-image img')).toHaveLength(0); + + images?.[0].dispatchEvent(new Event('load')); + fixture.detectChanges(); + expect(images?.[1].getAttribute('src')).toBe(mainCarouselImages[1]); + expect(element.querySelectorAll('app-product-column-with-image img')).toHaveLength(1); fixture.destroy(); }); diff --git a/src/app/features/store/pages/store-home-page/store-home-page.component.ts b/src/app/features/store/pages/store-home-page/store-home-page.component.ts index cdfce5f..02cc320 100644 --- a/src/app/features/store/pages/store-home-page/store-home-page.component.ts +++ b/src/app/features/store/pages/store-home-page/store-home-page.component.ts @@ -3,6 +3,7 @@ import { Component, OnDestroy, OnInit, + computed, inject, signal, } from '@angular/core'; @@ -53,6 +54,10 @@ export class StoreHomePageComponent implements OnInit, OnDestroy { protected readonly loading = signal(false); protected readonly loadingGroupIds = signal>(new Set()); protected readonly error = signal(null); + protected readonly mainCarouselReady = signal(false); + protected readonly hasMainCarouselImages = computed( + () => (this.tenant()?.main_carousel_images?.length ?? 0) > 0, + ); private catalogRequestSubscription: Subscription | null = null; private readonly groupRequestSubscriptions = new Map(); diff --git a/src/app/shared/components/main-carousel/main-carousel.component.html b/src/app/shared/components/main-carousel/main-carousel.component.html index 1998a4d..98b315c 100644 --- a/src/app/shared/components/main-carousel/main-carousel.component.html +++ b/src/app/shared/components/main-carousel/main-carousel.component.html @@ -15,11 +15,14 @@ } diff --git a/src/app/shared/components/main-carousel/main-carousel.component.spec.ts b/src/app/shared/components/main-carousel/main-carousel.component.spec.ts index 667a851..af77618 100644 --- a/src/app/shared/components/main-carousel/main-carousel.component.spec.ts +++ b/src/app/shared/components/main-carousel/main-carousel.component.spec.ts @@ -41,6 +41,29 @@ describe('MainCarouselComponent', () => { expect(images[0].classList).toContain('main-carousel__image--active'); }); + it('carga las imágenes secuencialmente y prioriza la imagen visible', () => { + const images = fixture.nativeElement.querySelectorAll( + '.main-carousel__image', + ) as NodeListOf; + + expect(images[0].getAttribute('src')).toBe('/images/one.webp'); + expect(images[0].getAttribute('fetchpriority')).toBe('high'); + expect(images[1].getAttribute('src')).toBeNull(); + expect(images[2].getAttribute('src')).toBeNull(); + + images[0].dispatchEvent(new Event('load')); + fixture.detectChanges(); + + expect(images[1].getAttribute('src')).toBe('/images/two.webp'); + expect(images[1].getAttribute('fetchpriority')).toBe('low'); + expect(images[2].getAttribute('src')).toBeNull(); + + images[1].dispatchEvent(new Event('load')); + fixture.detectChanges(); + + expect(images[2].getAttribute('src')).toBe('/images/three.webp'); + }); + it('avanza automáticamente entre las imágenes', () => { fixture.destroy(); vi.useFakeTimers(); @@ -48,10 +71,18 @@ describe('MainCarouselComponent', () => { try { fixture = TestBed.createComponent(TestHostComponent); fixture.detectChanges(); + const images = fixture.nativeElement.querySelectorAll( + '.main-carousel__image', + ) as NodeListOf; + + images[0].dispatchEvent(new Event('load')); + fixture.detectChanges(); + images[1].dispatchEvent(new Event('load')); + fixture.detectChanges(); + vi.advanceTimersByTime(1000); fixture.detectChanges(); - const images = fixture.nativeElement.querySelectorAll('.main-carousel__image'); expect(images[1].classList).toContain('main-carousel__image--active'); } finally { fixture.destroy(); diff --git a/src/app/shared/components/main-carousel/main-carousel.component.ts b/src/app/shared/components/main-carousel/main-carousel.component.ts index 4c10deb..581b827 100644 --- a/src/app/shared/components/main-carousel/main-carousel.component.ts +++ b/src/app/shared/components/main-carousel/main-carousel.component.ts @@ -2,10 +2,12 @@ import { ChangeDetectionStrategy, Component, PLATFORM_ID, + WritableSignal, computed, effect, inject, input, + output, signal, } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @@ -20,11 +22,15 @@ export class MainCarouselComponent { private readonly platformId = inject(PLATFORM_ID); private readonly requestedIndex = signal(0); private readonly paused = signal(false); + private readonly loadedIndexes = signal>(new Set()); + private readonly settledIndexes = signal>(new Set()); + private firstImageReadyEmitted = false; readonly images = input.required(); readonly imageAlts = input([]); readonly autoSlideInterval = input(5000); readonly ariaLabel = input('Imágenes destacadas'); + readonly firstImageReady = output(); protected readonly activeIndex = computed(() => { const imageCount = this.images().length; @@ -32,8 +38,27 @@ export class MainCarouselComponent { return imageCount ? this.requestedIndex() % imageCount : 0; }); protected readonly hasMultipleImages = computed(() => this.images().length > 1); + protected readonly sequentialLoadIndex = computed(() => { + const settledIndexes = this.settledIndexes(); + + for (let index = 0; index < this.images().length; index += 1) { + if (!settledIndexes.has(index)) { + return index; + } + } + + return -1; + }); constructor() { + effect(() => { + this.images(); + this.requestedIndex.set(0); + this.loadedIndexes.set(new Set()); + this.settledIndexes.set(new Set()); + this.firstImageReadyEmitted = false; + }); + effect(() => { const imageCount = this.images().length; const interval = Math.max(this.autoSlideInterval(), 1000); @@ -44,7 +69,11 @@ export class MainCarouselComponent { const timer = window.setInterval(() => { if (!this.paused()) { - this.requestedIndex.update((index) => (index + 1) % imageCount); + this.requestedIndex.update((index) => { + const nextIndex = (index + 1) % imageCount; + + return this.loadedIndexes().has(nextIndex) ? nextIndex : index; + }); } }, interval); @@ -62,6 +91,27 @@ export class MainCarouselComponent { this.paused.set(paused); } + protected imageSource(image: string, index: number): string | null { + const shouldLoad = + this.loadedIndexes().has(index) || + index === this.sequentialLoadIndex() || + index === this.activeIndex(); + + return shouldLoad ? image : null; + } + + protected handleImageLoad(index: number): void { + this.updateIndexSet(this.loadedIndexes, index, true); + this.updateIndexSet(this.settledIndexes, index, true); + this.notifyFirstImageReady(index); + } + + protected handleImageError(index: number): void { + this.updateIndexSet(this.loadedIndexes, index, false); + this.updateIndexSet(this.settledIndexes, index, true); + this.notifyFirstImageReady(index); + } + protected handleKeydown(event: KeyboardEvent): void { if (!this.hasMultipleImages()) { return; @@ -83,4 +133,26 @@ export class MainCarouselComponent { protected imageAlt(index: number): string { return this.imageAlts()[index] || `Imagen destacada ${index + 1}`; } + + private updateIndexSet( + target: WritableSignal>, + index: number, + present: boolean, + ): void { + target.update((current) => { + const updated = new Set(current); + present ? updated.add(index) : updated.delete(index); + + return updated; + }); + } + + private notifyFirstImageReady(index: number): void { + if (index !== 0 || this.firstImageReadyEmitted) { + return; + } + + this.firstImageReadyEmitted = true; + this.firstImageReady.emit(); + } } diff --git a/src/app/shared/components/product-list/product-list.component.html b/src/app/shared/components/product-list/product-list.component.html index e4ad662..1c09a32 100644 --- a/src/app/shared/components/product-list/product-list.component.html +++ b/src/app/shared/components/product-list/product-list.component.html @@ -28,10 +28,10 @@ } @default { } diff --git a/src/app/shared/components/product-list/product-list.component.ts b/src/app/shared/components/product-list/product-list.component.ts index 4f97f77..1a79478 100644 --- a/src/app/shared/components/product-list/product-list.component.ts +++ b/src/app/shared/components/product-list/product-list.component.ts @@ -59,6 +59,7 @@ export class ProductListComponent { readonly groupLayout = input.required(); readonly items = input.required(); readonly loading = input(false); + readonly loadImages = input(true); readonly buy = output(); readonly addToCart = output();