diff --git a/app/components/place-photos-carousel.gjs b/app/components/photo-carousel.gjs similarity index 60% rename from app/components/place-photos-carousel.gjs rename to app/components/photo-carousel.gjs index 4c115bc..c4ffa07 100644 --- a/app/components/place-photos-carousel.gjs +++ b/app/components/photo-carousel.gjs @@ -1,16 +1,19 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; +import { fn } from '@ember/helper'; +import { and, eq } from 'ember-truth-helpers'; import Blurhash from './blurhash'; import Icon from './icon'; import fadeInImage from '../modifiers/fade-in-image'; import { on } from '@ember/modifier'; import { modifier } from 'ember-modifier'; -export default class PlacePhotosCarousel extends Component { +export default class PhotoCarousel extends Component { @tracked canScrollLeft = false; @tracked canScrollRight = false; + internalEventId = null; carouselElement = null; get photos() { @@ -18,6 +21,7 @@ export default class PlacePhotosCarousel extends Component { } get showChevrons() { + // Only show chevrons if there's more than one photo return this.photos.length > 1; } @@ -29,6 +33,10 @@ export default class PlacePhotosCarousel extends Component { return !this.canScrollRight; } + get variantClass() { + return this.args.variant || 'inline'; + } + lastResetKey = null; resetScrollPosition = modifier((element, [resetKey]) => { @@ -41,16 +49,42 @@ export default class PlacePhotosCarousel extends Component { scrollToNewPhoto = modifier((element, [eventId]) => { if (eventId && eventId !== this.lastEventId) { + const isInitial = !this.lastEventId; this.lastEventId = eventId; - // Allow DOM to update first since the photo was *just* added to the store - setTimeout(() => { + + // Prevent feedback loop if this carousel initiated the change + if (this.internalEventId === eventId) { + return; + } + + const scrollAction = () => { const targetSlide = element.querySelector( `[data-event-id="${eventId}"]` ); if (targetSlide) { - element.scrollLeft = targetSlide.offsetLeft; + if (isInitial) { + const originalScrollBehavior = element.style.scrollBehavior; + element.style.scrollBehavior = 'auto'; + element.scrollLeft = targetSlide.offsetLeft; + + // Restore smooth scroll after the jump + setTimeout(() => { + element.style.scrollBehavior = originalScrollBehavior; + }, 50); + } else { + // Use native CSS smooth scrolling for subsequent clicks + element.scrollLeft = targetSlide.offsetLeft; + } } - }, 100); + }; + + if (isInitial) { + // Execute immediately for the first render to prevent flash + scrollAction(); + } else { + // Allow DOM to update first for subsequent clicks + setTimeout(scrollAction, 100); + } } }); @@ -68,10 +102,39 @@ export default class PlacePhotosCarousel extends Component { resizeObserver.observe(element); } + let intersectionObserver; + if (this.args.onVisiblePhotoChange && window.IntersectionObserver) { + // Set up intersection observer to track which photo is currently "most" visible + intersectionObserver = new IntersectionObserver( + (entries) => { + for (let entry of entries) { + if (entry.isIntersecting && entry.intersectionRatio >= 0.5) { + const eventId = entry.target.dataset.eventId; + this.internalEventId = eventId; + const photo = this.photos.find((p) => p.eventId === eventId); + if (photo) { + this.args.onVisiblePhotoChange(photo); + } + } + } + }, + { + root: element, + threshold: 0.5, + } + ); + + const slides = element.querySelectorAll('.carousel-slide'); + slides.forEach((slide) => intersectionObserver.observe(slide)); + } + return () => { if (resizeObserver) { resizeObserver.unobserve(element); } + if (intersectionObserver) { + intersectionObserver.disconnect(); + } }; }); @@ -103,23 +166,35 @@ export default class PlacePhotosCarousel extends Component { }); } + @action + handlePhotoClick(photo) { + if (this.args.onPhotoClick) { + this.args.onPhotoClick(photo); + } + } +