diff --git a/app/components/place-details.gjs b/app/components/place-details.gjs index 662fa30..daa889d 100644 --- a/app/components/place-details.gjs +++ b/app/components/place-details.gjs @@ -12,8 +12,7 @@ import PlaceListsManager from './place-lists-manager'; import PlacePhotoUpload from './place-photo-upload'; import NostrConnect from './nostr-connect'; import Modal from './modal'; -import Blurhash from './blurhash'; -import fadeInImage from '../modifiers/fade-in-image'; +import PlacePhotosCarousel from './place-photos-carousel'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; @@ -79,51 +78,71 @@ export default class PlaceDetails extends Component { return this.place.title || getLocalizedName(this.tags) || 'Unnamed Place'; } - get headerPhoto() { - const photos = this.nostrData.placePhotos; - if (!photos || photos.length === 0) return null; + get photos() { + const rawPhotos = this.nostrData.placePhotos; + if (!rawPhotos || rawPhotos.length === 0) return []; // Sort by created_at ascending (oldest first) - const sortedEvents = [...photos].sort( + const sortedEvents = [...rawPhotos].sort( (a, b) => a.created_at - b.created_at ); - let firstPortrait = null; + const allPhotos = []; for (const event of sortedEvents) { // Find all imeta tags const imetas = event.tags.filter((t) => t[0] === 'imeta'); for (const imeta of imetas) { let url = null; + let thumbUrl = null; let blurhash = null; let isLandscape = false; + let aspectRatio = 16 / 9; // default for (const tag of imeta.slice(1)) { if (tag.startsWith('url ')) { url = tag.substring(4); + } else if (tag.startsWith('thumb ')) { + thumbUrl = tag.substring(6); } else if (tag.startsWith('blurhash ')) { blurhash = tag.substring(9); } else if (tag.startsWith('dim ')) { const dimStr = tag.substring(4); const [width, height] = dimStr.split('x').map(Number); - if (width && height && width > height) { - isLandscape = true; + if (width && height) { + aspectRatio = width / height; + if (width > height) { + isLandscape = true; + } } } } if (url) { - const photoData = { url, blurhash }; - if (isLandscape) { - return photoData; // Return the first landscape photo found - } else if (!firstPortrait) { - firstPortrait = photoData; // Save the first portrait as fallback - } + allPhotos.push({ + url, + thumbUrl, + blurhash, + isLandscape, + aspectRatio, + style: htmlSafe(`--slide-ratio: ${aspectRatio};`), + }); } } } - return firstPortrait; + if (allPhotos.length === 0) return []; + + // Find the first landscape photo + const firstLandscapeIndex = allPhotos.findIndex((p) => p.isLandscape); + + if (firstLandscapeIndex > 0) { + // Move the first landscape photo to the front + const [firstLandscape] = allPhotos.splice(firstLandscapeIndex, 1); + allPhotos.unshift(firstLandscape); + } + + return allPhotos; } @action @@ -389,24 +408,7 @@ export default class PlaceDetails extends Component { @onCancel={{this.cancelEditing}} /> {{else}} - {{#if this.headerPhoto}} -
{{this.type}}
diff --git a/app/components/place-photos-carousel.gjs b/app/components/place-photos-carousel.gjs
new file mode 100644
index 0000000..942ee05
--- /dev/null
+++ b/app/components/place-photos-carousel.gjs
@@ -0,0 +1,155 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+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 {
+ @tracked canScrollLeft = false;
+ @tracked canScrollRight = false;
+
+ carouselElement = null;
+
+ get photos() {
+ return this.args.photos || [];
+ }
+
+ get showChevrons() {
+ return this.photos.length > 1;
+ }
+
+ get cannotScrollLeft() {
+ return !this.canScrollLeft;
+ }
+
+ get cannotScrollRight() {
+ return !this.canScrollRight;
+ }
+
+ setupCarousel = modifier((element) => {
+ this.carouselElement = element;
+
+ // Defer the initial calculation slightly to ensure CSS and images have applied
+ setTimeout(() => {
+ this.updateScrollState();
+ }, 50);
+
+ let resizeObserver;
+ if (window.ResizeObserver) {
+ resizeObserver = new ResizeObserver(() => this.updateScrollState());
+ resizeObserver.observe(element);
+ }
+
+ return () => {
+ if (resizeObserver) {
+ resizeObserver.unobserve(element);
+ }
+ };
+ });
+
+ @action
+ updateScrollState() {
+ if (!this.carouselElement) return;
+
+ const { scrollLeft, scrollWidth, clientWidth } = this.carouselElement;
+ // tolerance of 1px for floating point rounding issues
+ this.canScrollLeft = scrollLeft > 1;
+ this.canScrollRight = scrollLeft + clientWidth < scrollWidth - 1;
+ }
+
+ @action
+ scrollLeft() {
+ if (!this.carouselElement) return;
+ this.carouselElement.scrollBy({
+ left: -this.carouselElement.clientWidth,
+ behavior: 'smooth',
+ });
+ }
+
+ @action
+ scrollRight() {
+ if (!this.carouselElement) return;
+ this.carouselElement.scrollBy({
+ left: this.carouselElement.clientWidth,
+ behavior: 'smooth',
+ });
+ }
+
+
+ {{#if this.photos.length}}
+