Compare commits
13 Commits
2087cfc4f7
...
v1.21.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
4c4a53ae42
|
|||
|
a240a5d199
|
|||
|
0332cf4c3c
|
|||
|
59c447fe1f
|
|||
|
1140ecfe41
|
|||
|
60936ed2f5
|
|||
|
ca82a029bc
|
|||
|
0630aed73d
|
|||
|
f27a636529
|
|||
|
995ae95b09
|
|||
|
0fb320d996
|
|||
|
4f4ca827b1
|
|||
|
c1d3f25d50
|
@@ -15,6 +15,7 @@ export default class AppHeaderComponent extends Component {
|
||||
@service settings;
|
||||
@service nostrAuth;
|
||||
@service nostrData;
|
||||
@service mapUi;
|
||||
@tracked isUserMenuOpen = false;
|
||||
@tracked searchQuery = '';
|
||||
|
||||
@@ -22,6 +23,11 @@ export default class AppHeaderComponent extends Component {
|
||||
return !!this.searchQuery;
|
||||
}
|
||||
|
||||
get showQuickSearch() {
|
||||
const zoom = this.mapUi.currentZoom ?? 13;
|
||||
return this.settings.showQuickSearchButtons && zoom >= 12;
|
||||
}
|
||||
|
||||
@action
|
||||
toggleUserMenu() {
|
||||
this.isUserMenuOpen = !this.isUserMenuOpen;
|
||||
@@ -54,7 +60,7 @@ export default class AppHeaderComponent extends Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{#if this.settings.showQuickSearchButtons}}
|
||||
{{#if this.showQuickSearch}}
|
||||
<div class="header-center {{if this.hasQuery 'searching'}}">
|
||||
<CategoryChips @onSelect={{this.handleChipSelect}} />
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ export default class AppMenuSettingsApis extends Component {
|
||||
<Icon @name="server" @size={{20}} />
|
||||
<span>API Providers</span>
|
||||
</summary>
|
||||
<div class="details-content">
|
||||
<div class="details-content form-layout">
|
||||
<div class="form-group">
|
||||
<label for="overpass-api">Overpass API Provider</label>
|
||||
<select
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class AppMenuSettingsMapUi extends Component {
|
||||
<Icon @name="map" @size={{20}} />
|
||||
<span>Map & UI</span>
|
||||
</summary>
|
||||
<div class="details-content">
|
||||
<div class="details-content form-layout">
|
||||
<div class="form-group">
|
||||
<label for="show-quick-search">Quick search buttons visible</label>
|
||||
<select
|
||||
|
||||
@@ -108,7 +108,7 @@ export default class AppMenuSettingsNostr extends Component {
|
||||
<Icon @name="zap" @size={{20}} />
|
||||
<span>Nostr</span>
|
||||
</summary>
|
||||
<div class="details-content">
|
||||
<div class="details-content form-layout">
|
||||
<div class="form-group">
|
||||
<label for="nostr-photo-fallback-uploads">Upload photos to fallback
|
||||
servers</label>
|
||||
|
||||
@@ -284,6 +284,7 @@ export default class MapComponent extends Component {
|
||||
// Initialize the UI service with the map center
|
||||
const initialCenter = toLonLat(view.getCenter());
|
||||
this.mapUi.updateCenter(initialCenter[1], initialCenter[0]);
|
||||
this.mapUi.updateZoom(view.getZoom());
|
||||
|
||||
apply(this.mapInstance, 'https://tiles.openfreemap.org/styles/liberty', {
|
||||
webfonts: 'data:text/css,',
|
||||
@@ -1046,6 +1047,7 @@ export default class MapComponent extends Component {
|
||||
const view = this.mapInstance.getView();
|
||||
const center = toLonLat(view.getCenter());
|
||||
this.mapUi.updateCenter(center[1], center[0]);
|
||||
this.mapUi.updateZoom(view.getZoom());
|
||||
|
||||
// If in creation mode, update the coordinates in the service AND the URL
|
||||
if (this.mapUi.isCreating) {
|
||||
|
||||
@@ -41,6 +41,40 @@ export default class NostrConnectComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async copyConnectUri() {
|
||||
const text = this.nostrAuth.connectUri;
|
||||
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.top = '0';
|
||||
textArea.style.left = '0';
|
||||
textArea.style.opacity = '0';
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
const successful = document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
if (!successful) {
|
||||
throw new Error('Fallback copy failed');
|
||||
}
|
||||
}
|
||||
this.toast.show('Connection link copied to clipboard');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
alert('Failed to copy link');
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="nostr-connect-modal">
|
||||
<h2>Connect with Nostr</h2>
|
||||
@@ -59,7 +93,7 @@ export default class NostrConnectComponent extends Component {
|
||||
class="btn btn-outline"
|
||||
type="button"
|
||||
disabled
|
||||
title="No Nostr extension found in your browser."
|
||||
title="No Nostr extension found in your browser"
|
||||
>
|
||||
Browser Extension (Not Found)
|
||||
</button>
|
||||
@@ -79,9 +113,20 @@ export default class NostrConnectComponent extends Component {
|
||||
{{#if this.nostrAuth.isMobile}}
|
||||
<p>Waiting for you to approve the connection in your mobile signer
|
||||
app...</p>
|
||||
<div class="mobile-connect-actions">
|
||||
<a href={{this.nostrAuth.connectUri}} class="btn btn-primary">
|
||||
Open Signer App
|
||||
</a>
|
||||
<button
|
||||
class="btn btn-outline"
|
||||
type="button"
|
||||
{{on "click" this.copyConnectUri}}
|
||||
>
|
||||
Copy Connection Link
|
||||
</button>
|
||||
</div>
|
||||
{{else}}
|
||||
<p>Scan this QR code with a compatible Nostr signer app (like
|
||||
Amber):</p>
|
||||
<p>Scan this QR code with a Nostr signer app (like Amber):</p>
|
||||
<div class="qr-code-container">
|
||||
<canvas {{qrCode this.nostrAuth.connectUri}}></canvas>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.photos.length}}
|
||||
<div class="place-photos-carousel-wrapper">
|
||||
<div class="photo-carousel {{this.variantClass}}">
|
||||
<div
|
||||
class="place-photos-carousel-track"
|
||||
class="photo-carousel-track"
|
||||
{{this.setupCarousel}}
|
||||
{{this.resetScrollPosition @resetKey}}
|
||||
{{this.scrollToNewPhoto @scrollToEventId}}
|
||||
{{on "scroll" this.updateScrollState}}
|
||||
>
|
||||
{{#each this.photos as |photo|}}
|
||||
{{! template-lint-disable no-inline-styles }}
|
||||
{{! template-lint-disable no-inline-styles no-invalid-interactive }}
|
||||
<div
|
||||
class="carousel-slide
|
||||
{{if photo.isLandscape 'landscape' 'portrait'}}"
|
||||
{{if photo.isLandscape 'landscape' 'portrait'}}
|
||||
{{if
|
||||
(and @scrollToEventId (eq photo.eventId @scrollToEventId))
|
||||
'active'
|
||||
}}"
|
||||
style={{photo.style}}
|
||||
data-event-id={{photo.eventId}}
|
||||
{{on "click" (fn this.handlePhotoClick photo)}}
|
||||
>
|
||||
{{#if photo.blurhash}}
|
||||
<Blurhash
|
||||
@@ -169,7 +244,11 @@ export default class PlacePhotosCarousel extends Component {
|
||||
disabled={{this.cannotScrollLeft}}
|
||||
aria-label="Previous photo"
|
||||
>
|
||||
<Icon @name="chevron-left" @color="currentColor" />
|
||||
<Icon
|
||||
@name="chevron-left"
|
||||
@color="currentColor"
|
||||
@size={{if (eq @variant "gallery-main") 24 16}}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -180,7 +259,11 @@ export default class PlacePhotosCarousel extends Component {
|
||||
disabled={{this.cannotScrollRight}}
|
||||
aria-label="Next photo"
|
||||
>
|
||||
<Icon @name="chevron-right" @color="currentColor" />
|
||||
<Icon
|
||||
@name="chevron-right"
|
||||
@color="currentColor"
|
||||
@size={{if (eq @variant "gallery-main") 24 16}}
|
||||
/>
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
85
app/components/photo-gallery.gjs
Normal file
85
app/components/photo-gallery.gjs
Normal file
@@ -0,0 +1,85 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { on } from '@ember/modifier';
|
||||
import Icon from './icon';
|
||||
import PhotoCarousel from './photo-carousel';
|
||||
|
||||
export default class PhotoGallery extends Component {
|
||||
@tracked currentPhoto = this.args.selectedPhoto || this.args.photos?.[0];
|
||||
|
||||
@action
|
||||
handleClose() {
|
||||
if (this.args.onClose) {
|
||||
this.args.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
handleBackgroundClick(e) {
|
||||
// Don't close if clicking on thumbnails, nav buttons, or the close button itself
|
||||
if (
|
||||
e.target.closest('.thumbnail-strip-container') ||
|
||||
e.target.closest('.carousel-nav-btn') ||
|
||||
e.target.closest('.close-btn')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleClose();
|
||||
}
|
||||
|
||||
@action
|
||||
selectPhoto(photo) {
|
||||
this.currentPhoto = photo;
|
||||
}
|
||||
|
||||
@action
|
||||
handleVisiblePhotoChange(photo) {
|
||||
if (this.currentPhoto !== photo) {
|
||||
this.currentPhoto = photo;
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="photo-gallery-overlay"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
{{on "click" this.handleBackgroundClick}}
|
||||
>
|
||||
{{! template-lint-disable no-invalid-interactive }}
|
||||
<div class="photo-gallery-content">
|
||||
<button
|
||||
type="button"
|
||||
class="close-btn btn-text"
|
||||
{{on "click" this.handleClose}}
|
||||
aria-label="Close gallery"
|
||||
title="Close"
|
||||
>
|
||||
<Icon @name="x" @size={{24}} @color="white" />
|
||||
</button>
|
||||
|
||||
<div class="main-photo-container">
|
||||
<PhotoCarousel
|
||||
@variant="gallery-main"
|
||||
@photos={{@photos}}
|
||||
@scrollToEventId={{this.currentPhoto.eventId}}
|
||||
@onVisiblePhotoChange={{this.handleVisiblePhotoChange}}
|
||||
@name={{@placeName}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="thumbnail-strip-container">
|
||||
<PhotoCarousel
|
||||
@variant="gallery-thumbnails"
|
||||
@photos={{@photos}}
|
||||
@scrollToEventId={{this.currentPhoto.eventId}}
|
||||
@onPhotoClick={{this.selectPhoto}}
|
||||
@name={{@placeName}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -13,7 +13,8 @@ import PlaceListsManager from './place-lists-manager';
|
||||
import PlacePhotoUpload from './place-photo-upload';
|
||||
import NostrConnect from './nostr-connect';
|
||||
import Modal from './modal';
|
||||
import PlacePhotosCarousel from './place-photos-carousel';
|
||||
import PhotoCarousel from './photo-carousel';
|
||||
import PhotoGallery from './photo-gallery';
|
||||
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
@@ -24,10 +25,13 @@ export default class PlaceDetails extends Component {
|
||||
@service nostrData;
|
||||
@tracked isEditing = false;
|
||||
@tracked showLists = false;
|
||||
@tracked isPhotoUploadActive = false;
|
||||
@tracked isConnectingNostr = false;
|
||||
@tracked isGalleryOpen = false;
|
||||
@tracked selectedGalleryPhoto = null;
|
||||
@tracked isPhotoUploadModalOpen = false;
|
||||
@tracked isNostrConnectModalOpen = false;
|
||||
@tracked newlyUploadedPhotoId = null;
|
||||
@tracked isPhotoUploadActive = false;
|
||||
|
||||
@action
|
||||
handleUploadStateChange(isActive) {
|
||||
@@ -362,6 +366,18 @@ export default class PlaceDetails extends Component {
|
||||
return !!this.place.description;
|
||||
}
|
||||
|
||||
@action
|
||||
openGallery(photo) {
|
||||
this.selectedGalleryPhoto = photo;
|
||||
this.isGalleryOpen = true;
|
||||
}
|
||||
|
||||
@action
|
||||
closeGallery() {
|
||||
this.isGalleryOpen = false;
|
||||
this.selectedGalleryPhoto = null;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="place-details">
|
||||
{{#if this.isEditing}}
|
||||
@@ -371,11 +387,13 @@ export default class PlaceDetails extends Component {
|
||||
@onCancel={{this.cancelEditing}}
|
||||
/>
|
||||
{{else}}
|
||||
<PlacePhotosCarousel
|
||||
<PhotoCarousel
|
||||
@variant="inline"
|
||||
@photos={{this.photos}}
|
||||
@name={{this.name}}
|
||||
@resetKey={{this.place.osmId}}
|
||||
@scrollToEventId={{this.newlyUploadedPhotoId}}
|
||||
@onPhotoClick={{this.openGallery}}
|
||||
/>
|
||||
<h3>{{this.name}}</h3>
|
||||
<p class="place-type">
|
||||
@@ -609,5 +627,14 @@ export default class PlaceDetails extends Component {
|
||||
<NostrConnect @onConnect={{this.onNostrConnected}} />
|
||||
</Modal>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.isGalleryOpen}}
|
||||
<PhotoGallery
|
||||
@photos={{this.photos}}
|
||||
@selectedPhoto={{this.selectedGalleryPhoto}}
|
||||
@placeName={{this.name}}
|
||||
@onClose={{this.closeGallery}}
|
||||
/>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
|
||||
@@ -9,11 +9,28 @@ export default modifier((element, [url]) => {
|
||||
|
||||
let observer;
|
||||
|
||||
const hideBlurhash = () => {
|
||||
const parent = element.parentElement;
|
||||
const slide =
|
||||
parent && parent.tagName === 'PICTURE' ? parent.parentElement : parent;
|
||||
|
||||
// Only hide the blurhash if we're in the gallery-main view.
|
||||
// In the inline view, we want to keep the blurhash visible behind portrait photos
|
||||
// to fill the 16:9 container gracefully.
|
||||
if (slide && slide.closest('.photo-carousel.gallery-main')) {
|
||||
const blur = slide.querySelector('.place-header-photo-blur');
|
||||
if (blur) {
|
||||
blur.style.opacity = '0';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoad = () => {
|
||||
// Only apply the fade-in animation if it wasn't already loaded instantly
|
||||
if (!element.classList.contains('loaded-instant')) {
|
||||
element.classList.add('loaded');
|
||||
}
|
||||
hideBlurhash();
|
||||
};
|
||||
|
||||
element.addEventListener('load', handleLoad);
|
||||
@@ -33,6 +50,7 @@ export default modifier((element, [url]) => {
|
||||
if (img.complete) {
|
||||
// Already in browser cache, skip the animation
|
||||
element.classList.add('loaded-instant');
|
||||
hideBlurhash();
|
||||
}
|
||||
|
||||
// If this image is inside a <picture> tag, we also need to swap <source> tags
|
||||
|
||||
@@ -11,6 +11,7 @@ export default class MapUiService extends Service {
|
||||
@tracked returnToSearch = false;
|
||||
@tracked currentCenter = null;
|
||||
@tracked currentBounds = null;
|
||||
@tracked currentZoom = null;
|
||||
@tracked searchBoxHasFocus = false;
|
||||
@tracked selectionOptions = {};
|
||||
@tracked preventNextZoom = false;
|
||||
@@ -81,6 +82,10 @@ export default class MapUiService extends Service {
|
||||
this.currentCenter = { lat, lon };
|
||||
}
|
||||
|
||||
updateZoom(zoom) {
|
||||
this.currentZoom = zoom;
|
||||
}
|
||||
|
||||
updateBounds(bounds) {
|
||||
this.currentBounds = bounds;
|
||||
}
|
||||
|
||||
@@ -594,6 +594,9 @@ body {
|
||||
padding: 0 1.4rem 1rem;
|
||||
animation: details-slide-down 0.2s ease-out;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.sidebar-content details .details-content.form-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
@@ -890,12 +893,15 @@ abbr[title] {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.place-photos-carousel-wrapper {
|
||||
.photo-carousel {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.photo-carousel.inline {
|
||||
margin: -1rem -1rem 1rem;
|
||||
}
|
||||
|
||||
.place-photos-carousel-track {
|
||||
.photo-carousel-track {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
scroll-behavior: smooth;
|
||||
@@ -904,11 +910,12 @@ abbr[title] {
|
||||
background-color: var(--hover-bg);
|
||||
}
|
||||
|
||||
.place-photos-carousel-track::-webkit-scrollbar {
|
||||
.photo-carousel-track::-webkit-scrollbar {
|
||||
display: none; /* Safari and Chrome */
|
||||
}
|
||||
|
||||
.carousel-slide {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
flex: 0 0 100%;
|
||||
scroll-snap-align: start;
|
||||
@@ -984,7 +991,7 @@ abbr[title] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.place-photos-carousel-wrapper:hover .carousel-nav-btn:not(.disabled) {
|
||||
.photo-carousel:hover .carousel-nav-btn:not(.disabled) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -1006,40 +1013,40 @@ abbr[title] {
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.place-photos-carousel-track {
|
||||
.photo-carousel.inline .photo-carousel-track {
|
||||
scroll-snap-type: none;
|
||||
gap: 2px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.carousel-slide {
|
||||
.photo-carousel.inline .carousel-slide {
|
||||
flex: 0 0 auto;
|
||||
height: 100px;
|
||||
width: auto;
|
||||
scroll-snap-align: none;
|
||||
}
|
||||
|
||||
.carousel-slide.landscape {
|
||||
.photo-carousel.inline .carousel-slide.landscape {
|
||||
aspect-ratio: var(--slide-ratio, 16 / 9);
|
||||
}
|
||||
|
||||
.carousel-slide.portrait {
|
||||
.photo-carousel.inline .carousel-slide.portrait {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.carousel-placeholder {
|
||||
.photo-carousel.inline .carousel-placeholder {
|
||||
display: block;
|
||||
background-color: var(--hover-bg);
|
||||
flex: 1 1 0%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.place-header-photo.landscape,
|
||||
.place-header-photo.portrait {
|
||||
.photo-carousel.inline .place-header-photo.landscape,
|
||||
.photo-carousel.inline .place-header-photo.portrait {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.carousel-nav-btn {
|
||||
.photo-carousel.inline .carousel-nav-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1077,6 +1084,7 @@ abbr[title] {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
@@ -1773,6 +1781,13 @@ button.create-place {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.mobile-connect-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.nostr-connect-status {
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
@@ -1866,3 +1881,149 @@ button.create-place {
|
||||
.btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Photo Gallery */
|
||||
.photo-gallery-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgb(0 0 0 / 90%);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.photo-gallery-overlay .photo-gallery-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.photo-gallery-overlay .close-btn {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
z-index: 10;
|
||||
color: white;
|
||||
background: transparent;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.photo-gallery-overlay .close-btn {
|
||||
right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.photo-gallery-overlay .main-photo-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.photo-gallery-overlay .main-photo-container {
|
||||
padding: 2rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.photo-gallery-overlay .main-photo-container img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Photo Carousel: Gallery Main Variant */
|
||||
.photo-carousel.gallery-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.photo-carousel.gallery-main .photo-carousel-track {
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.photo-carousel.gallery-main .carousel-slide {
|
||||
height: 100%;
|
||||
flex: 0 0 100%;
|
||||
aspect-ratio: auto;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.photo-carousel.gallery-main .carousel-nav-btn {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.photo-carousel.gallery-main .place-header-photo-blur,
|
||||
.photo-carousel.gallery-main .place-header-photo.landscape,
|
||||
.photo-carousel.gallery-main .place-header-photo.portrait {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.photo-carousel.gallery-main .carousel-nav-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Photo Carousel: Gallery Thumbnails Variant */
|
||||
.photo-carousel.gallery-thumbnails {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background: rgb(0 0 0 / 50%);
|
||||
padding-bottom: env(safe-area-inset-bottom, 0); /* Support mobile safe area */
|
||||
}
|
||||
|
||||
.photo-carousel.gallery-thumbnails .photo-carousel-track {
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
gap: 4px;
|
||||
scroll-snap-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.photo-carousel.gallery-thumbnails .carousel-slide {
|
||||
flex: 0 0 auto;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
scroll-snap-align: none;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.photo-carousel.gallery-thumbnails .carousel-slide.landscape {
|
||||
aspect-ratio: var(--slide-ratio, 16 / 9);
|
||||
}
|
||||
|
||||
.photo-carousel.gallery-thumbnails .carousel-slide.portrait {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.photo-carousel.gallery-thumbnails .carousel-slide:hover,
|
||||
.photo-carousel.gallery-thumbnails .carousel-slide.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.photo-carousel.gallery-thumbnails .place-header-photo.landscape,
|
||||
.photo-carousel.gallery-thumbnails .place-header-photo.portrait {
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.photo-carousel.gallery-thumbnails .carousel-nav-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "marco",
|
||||
"version": "1.20.5",
|
||||
"version": "1.21.2",
|
||||
"private": true,
|
||||
"description": "Unhosted maps app",
|
||||
"repository": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
14
release/assets/main-CjxGWim8.js
Normal file
14
release/assets/main-CjxGWim8.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
release/assets/main-M5C-HUrg.css
Normal file
1
release/assets/main-M5C-HUrg.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -39,8 +39,8 @@
|
||||
<meta name="msapplication-TileColor" content="#F6E9A6">
|
||||
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
|
||||
|
||||
<script type="module" crossorigin src="/assets/main-Dhq0XoTm.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-CHuW_yI-.css">
|
||||
<script type="module" crossorigin src="/assets/main-CjxGWim8.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-M5C-HUrg.css">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'marco/tests/helpers';
|
||||
import { render, click } from '@ember/test-helpers';
|
||||
import PlacePhotosCarousel from 'marco/components/place-photos-carousel';
|
||||
import PhotoCarousel from 'marco/components/photo-carousel';
|
||||
|
||||
module('Integration | Component | place-photos-carousel', function (hooks) {
|
||||
module('Integration | Component | photo-carousel', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders gracefully with no photos', async function (assert) {
|
||||
this.photos = [];
|
||||
|
||||
await render(
|
||||
<template><PlacePhotosCarousel @photos={{this.photos}} /></template>
|
||||
<template><PhotoCarousel @photos={{this.photos}} /></template>
|
||||
);
|
||||
|
||||
assert
|
||||
.dom('.place-photos-carousel-wrapper')
|
||||
.dom('.photo-carousel')
|
||||
.doesNotExist('it does not render the wrapper when there are no photos');
|
||||
});
|
||||
|
||||
@@ -32,14 +32,12 @@ module('Integration | Component | place-photos-carousel', function (hooks) {
|
||||
await render(
|
||||
<template>
|
||||
<div class="test-container">
|
||||
<PlacePhotosCarousel @photos={{this.photos}} />
|
||||
<PhotoCarousel @photos={{this.photos}} />
|
||||
</div>
|
||||
</template>
|
||||
);
|
||||
|
||||
assert
|
||||
.dom('.place-photos-carousel-wrapper')
|
||||
.exists('it renders the wrapper');
|
||||
assert.dom('.photo-carousel').exists('it renders the wrapper');
|
||||
assert
|
||||
.dom('.carousel-slide:not(.carousel-placeholder)')
|
||||
.exists({ count: 1 }, 'it renders one real photo slide');
|
||||
@@ -84,7 +82,7 @@ module('Integration | Component | place-photos-carousel', function (hooks) {
|
||||
await render(
|
||||
<template>
|
||||
<div class="test-container">
|
||||
<PlacePhotosCarousel @photos={{this.photos}} />
|
||||
<PhotoCarousel @photos={{this.photos}} />
|
||||
</div>
|
||||
</template>
|
||||
);
|
||||
@@ -4,7 +4,7 @@ import { babel } from '@rollup/plugin-babel';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
host: '127.0.0.1',
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
plugins: [
|
||||
ember(),
|
||||
|
||||
Reference in New Issue
Block a user