import { ChangeDetectionStrategy, Component, PLATFORM_ID, WritableSignal, computed, effect, inject, input, output, signal, } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Component({ selector: 'app-main-carousel', templateUrl: './main-carousel.component.html', styleUrl: './main-carousel.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) 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; 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); if (!isPlatformBrowser(this.platformId) || imageCount < 2) { return; } const timer = window.setInterval(() => { if (!this.paused()) { this.requestedIndex.update((index) => { const nextIndex = (index + 1) % imageCount; return this.loadedIndexes().has(nextIndex) ? nextIndex : index; }); } }, interval); return () => window.clearInterval(timer); }); } protected selectImage(index: number): void { if (index >= 0 && index < this.images().length) { this.requestedIndex.set(index); } } protected setPaused(paused: boolean): void { 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; } if (event.key === 'ArrowLeft') { event.preventDefault(); this.requestedIndex.update( (index) => (index - 1 + this.images().length) % this.images().length, ); } if (event.key === 'ArrowRight') { event.preventDefault(); this.requestedIndex.update((index) => (index + 1) % this.images().length); } } 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(); } }