Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
f0a19e30b8
|
@@ -9,7 +9,6 @@ import SearchBox from '#components/search-box';
|
|||||||
import CategoryChips from '#components/category-chips';
|
import CategoryChips from '#components/category-chips';
|
||||||
import { and } from 'ember-truth-helpers';
|
import { and } from 'ember-truth-helpers';
|
||||||
import cachedImage from '../modifiers/cached-image';
|
import cachedImage from '../modifiers/cached-image';
|
||||||
import { POI_CATEGORIES } from '../utils/poi-categories';
|
|
||||||
|
|
||||||
export default class AppHeaderComponent extends Component {
|
export default class AppHeaderComponent extends Component {
|
||||||
@service storage;
|
@service storage;
|
||||||
@@ -17,59 +16,11 @@ export default class AppHeaderComponent extends Component {
|
|||||||
@service nostrAuth;
|
@service nostrAuth;
|
||||||
@service nostrData;
|
@service nostrData;
|
||||||
@service mapUi;
|
@service mapUi;
|
||||||
@service router;
|
|
||||||
@tracked isUserMenuOpen = false;
|
@tracked isUserMenuOpen = false;
|
||||||
@tracked searchQuery = '';
|
@tracked searchQuery = '';
|
||||||
|
|
||||||
constructor() {
|
get hasQuery() {
|
||||||
super(...arguments);
|
return !!this.searchQuery;
|
||||||
if (this.router && typeof this.router.on === 'function') {
|
|
||||||
this.router.on('routeDidChange', this.syncSearchQuery);
|
|
||||||
}
|
|
||||||
this.syncSearchQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
willDestroy() {
|
|
||||||
if (this.router && typeof this.router.off === 'function') {
|
|
||||||
this.router.off('routeDidChange', this.syncSearchQuery);
|
|
||||||
}
|
|
||||||
super.willDestroy(...arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
syncSearchQuery() {
|
|
||||||
const qp =
|
|
||||||
this.mapUi.currentSearch || this.router?.currentRoute?.queryParams;
|
|
||||||
if (qp?.q) {
|
|
||||||
this.searchQuery = qp.q;
|
|
||||||
} else if (qp?.category) {
|
|
||||||
const category = POI_CATEGORIES.find((c) => c.id === qp.category);
|
|
||||||
this.searchQuery = category ? category.label : qp.category;
|
|
||||||
} else {
|
|
||||||
this.searchQuery = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get isSearching() {
|
|
||||||
// 1. If we are actively focusing/typing in the search box with a query, hide pills
|
|
||||||
if (this.mapUi.searchBoxHasFocus && this.searchQuery) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. If we are on the search route, check loading and results status
|
|
||||||
if (this.router?.currentRouteName === 'search') {
|
|
||||||
if (this.mapUi.loadingState) {
|
|
||||||
return false; // Keep pills visible while loading
|
|
||||||
}
|
|
||||||
return this.mapUi.searchResults && this.mapUi.searchResults.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Fallback for integration tests (non-search route with a query)
|
|
||||||
if (this.router?.currentRouteName !== 'search' && this.searchQuery) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get showQuickSearch() {
|
get showQuickSearch() {
|
||||||
@@ -110,7 +61,7 @@ export default class AppHeaderComponent extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if this.showQuickSearch}}
|
{{#if this.showQuickSearch}}
|
||||||
<div class="header-center {{if this.isSearching 'searching'}}">
|
<div class="header-center {{if this.hasQuery 'searching'}}">
|
||||||
<CategoryChips @onSelect={{this.handleChipSelect}} />
|
<CategoryChips @onSelect={{this.handleChipSelect}} />
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import Icon from '#components/icon';
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{! template-lint-disable no-nested-interactive }}
|
{{! template-lint-disable no-nested-interactive }}
|
||||||
<div class="sidebar-header has-back-btn">
|
<div class="sidebar-header">
|
||||||
<button type="button" class="back-btn" {{on "click" @onBack}}>
|
<button type="button" class="back-btn" {{on "click" @onBack}}>
|
||||||
<Icon @name="arrow-left" @size={{20}} @color="#333" />
|
<Icon @name="arrow-left" @size={{20}} @color="#333" />
|
||||||
</button>
|
</button>
|
||||||
<h2 class="sidebar-header-text-centered">About</h2>
|
<h2>About</h2>
|
||||||
<button type="button" class="close-btn" {{on "click" @onClose}}>
|
<button type="button" class="close-btn" {{on "click" @onClose}}>
|
||||||
<Icon @name="x" @size={{20}} @color="#333" />
|
<Icon @name="x" @size={{20}} @color="#333" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -19,12 +19,6 @@ import iconRounded from '../../icons/icon-rounded.svg?raw';
|
|||||||
|
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<ul class="app-menu">
|
<ul class="app-menu">
|
||||||
<li>
|
|
||||||
<button type="button" {{on "click" @onSavedPlaces}}>
|
|
||||||
<Icon @name="bookmark" @size={{20}} />
|
|
||||||
<span>Collections</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<button type="button" {{on "click" (fn @onNavigate "settings")}}>
|
<button type="button" {{on "click" (fn @onNavigate "settings")}}>
|
||||||
<Icon @name="settings" @size={{20}} />
|
<Icon @name="settings" @size={{20}} />
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import Component from '@glimmer/component';
|
|||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { fn } from '@ember/helper';
|
import { fn } from '@ember/helper';
|
||||||
import { service } from '@ember/service';
|
|
||||||
import eq from 'ember-truth-helpers/helpers/eq';
|
import eq from 'ember-truth-helpers/helpers/eq';
|
||||||
|
|
||||||
import AppMenuHome from './home';
|
import AppMenuHome from './home';
|
||||||
@@ -10,7 +9,6 @@ import AppMenuSettings from './settings';
|
|||||||
import AppMenuAbout from './about';
|
import AppMenuAbout from './about';
|
||||||
|
|
||||||
export default class AppMenu extends Component {
|
export default class AppMenu extends Component {
|
||||||
@service router;
|
|
||||||
@tracked currentView = 'menu'; // 'menu', 'settings', 'about'
|
@tracked currentView = 'menu'; // 'menu', 'settings', 'about'
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@@ -18,19 +16,10 @@ export default class AppMenu extends Component {
|
|||||||
this.currentView = view;
|
this.currentView = view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
goToSavedPlaces() {
|
|
||||||
this.router.transitionTo('lists.index');
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sidebar app-menu-pane">
|
<div class="sidebar app-menu-pane">
|
||||||
{{#if (eq this.currentView "menu")}}
|
{{#if (eq this.currentView "menu")}}
|
||||||
<AppMenuHome
|
<AppMenuHome @onNavigate={{this.setView}} @onClose={{@onClose}} />
|
||||||
@onNavigate={{this.setView}}
|
|
||||||
@onClose={{@onClose}}
|
|
||||||
@onSavedPlaces={{this.goToSavedPlaces}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{{else if (eq this.currentView "settings")}}
|
{{else if (eq this.currentView "settings")}}
|
||||||
<AppMenuSettings
|
<AppMenuSettings
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ export default class AppMenuSettings extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sidebar-header has-back-btn">
|
<div class="sidebar-header">
|
||||||
<button type="button" class="back-btn" {{on "click" @onBack}}>
|
<button type="button" class="back-btn" {{on "click" @onBack}}>
|
||||||
<Icon @name="arrow-left" @size={{20}} @color="#333" />
|
<Icon @name="arrow-left" @size={{20}} @color="#333" />
|
||||||
</button>
|
</button>
|
||||||
<h2 class="sidebar-header-text-centered">Settings</h2>
|
<h2>Settings</h2>
|
||||||
<button type="button" class="close-btn" {{on "click" @onClose}}>
|
<button type="button" class="close-btn" {{on "click" @onClose}}>
|
||||||
<Icon @name="x" @size={{20}} @color="#333" />
|
<Icon @name="x" @size={{20}} @color="#333" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ import { tracked } from '@glimmer/tracking';
|
|||||||
import { service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
import { fn } from '@ember/helper';
|
import { fn } from '@ember/helper';
|
||||||
import Icon from '#components/icon';
|
import Icon from '#components/icon';
|
||||||
import {
|
import { normalizeRelayUrl } from '../../../utils/nostr';
|
||||||
excludeRequiredRelays,
|
|
||||||
mergeRequiredRelays,
|
|
||||||
normalizeRelayUrl,
|
|
||||||
} from '../../../utils/nostr';
|
|
||||||
|
|
||||||
const stripProtocol = (url) => (url ? url.replace(/^wss?:\/\//, '') : '');
|
const stripProtocol = (url) => (url ? url.replace(/^wss?:\/\//, '') : '');
|
||||||
|
|
||||||
@@ -21,74 +17,6 @@ export default class AppMenuSettingsNostr extends Component {
|
|||||||
@tracked newReadRelay = '';
|
@tracked newReadRelay = '';
|
||||||
@tracked newWriteRelay = '';
|
@tracked newWriteRelay = '';
|
||||||
|
|
||||||
get customReadRelays() {
|
|
||||||
return excludeRequiredRelays(
|
|
||||||
this.settings.nostrReadRelays || [],
|
|
||||||
this.nostrData.requiredReadRelays
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get customWriteRelays() {
|
|
||||||
return excludeRequiredRelays(
|
|
||||||
this.settings.nostrWriteRelays || [],
|
|
||||||
this.nostrData.requiredWriteRelays
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get readRelayExclusions() {
|
|
||||||
return this.settings.nostrReadRelayExclusions || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
get writeRelayExclusions() {
|
|
||||||
return this.settings.nostrWriteRelayExclusions || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
get requiredReadRelaySet() {
|
|
||||||
return new Set(this.nostrData.requiredReadRelays.filter(Boolean));
|
|
||||||
}
|
|
||||||
|
|
||||||
get requiredWriteRelaySet() {
|
|
||||||
return new Set(this.nostrData.requiredWriteRelays.filter(Boolean));
|
|
||||||
}
|
|
||||||
|
|
||||||
get mailboxReadRelaySet() {
|
|
||||||
return new Set(this.nostrData.mailboxReadRelays);
|
|
||||||
}
|
|
||||||
|
|
||||||
get mailboxWriteRelaySet() {
|
|
||||||
return new Set(this.nostrData.mailboxWriteRelays);
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasReadOverrides() {
|
|
||||||
return (
|
|
||||||
this.customReadRelays.length > 0 || this.readRelayExclusions.length > 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasWriteOverrides() {
|
|
||||||
return (
|
|
||||||
this.customWriteRelays.length > 0 || this.writeRelayExclusions.length > 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get readRelaysForDisplay() {
|
|
||||||
return this.nostrData.activeReadRelays.map((url) => {
|
|
||||||
return {
|
|
||||||
url,
|
|
||||||
isRequired: this.requiredReadRelaySet.has(url),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get writeRelaysForDisplay() {
|
|
||||||
return this.nostrData.activeWriteRelays.map((url) => {
|
|
||||||
return {
|
|
||||||
url,
|
|
||||||
isRequired: this.requiredWriteRelaySet.has(url),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateNewReadRelay(event) {
|
updateNewReadRelay(event) {
|
||||||
this.newReadRelay = event.target.value;
|
this.newReadRelay = event.target.value;
|
||||||
@@ -104,51 +32,19 @@ export default class AppMenuSettingsNostr extends Component {
|
|||||||
const url = normalizeRelayUrl(this.newReadRelay);
|
const url = normalizeRelayUrl(this.newReadRelay);
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
|
|
||||||
const merged = mergeRequiredRelays(this.nostrData.requiredReadRelays, [
|
const current =
|
||||||
...this.customReadRelays,
|
this.settings.nostrReadRelays || this.nostrData.defaultReadRelays;
|
||||||
url,
|
const set = new Set([...current, url]);
|
||||||
]);
|
this.settings.update('nostrReadRelays', Array.from(set));
|
||||||
const custom = excludeRequiredRelays(
|
|
||||||
merged,
|
|
||||||
this.nostrData.requiredReadRelays
|
|
||||||
);
|
|
||||||
|
|
||||||
const readExclusions = this.readRelayExclusions.filter((relay) => {
|
|
||||||
return normalizeRelayUrl(relay) !== url;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.settings.update('nostrReadRelays', custom.length > 0 ? custom : null);
|
|
||||||
this.settings.update(
|
|
||||||
'nostrReadRelayExclusions',
|
|
||||||
readExclusions.length > 0 ? readExclusions : null
|
|
||||||
);
|
|
||||||
this.newReadRelay = '';
|
this.newReadRelay = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
removeReadRelay(url) {
|
removeReadRelay(url) {
|
||||||
if (this.requiredReadRelaySet.has(url)) {
|
const current =
|
||||||
return;
|
this.settings.nostrReadRelays || this.nostrData.defaultReadRelays;
|
||||||
}
|
const filtered = current.filter((r) => r !== url);
|
||||||
|
this.settings.update('nostrReadRelays', filtered);
|
||||||
const normalizedUrl = normalizeRelayUrl(url);
|
|
||||||
|
|
||||||
const remainingCustom = this.customReadRelays.filter((relay) => {
|
|
||||||
return normalizeRelayUrl(relay) !== normalizedUrl;
|
|
||||||
});
|
|
||||||
|
|
||||||
const nextExclusions = this.mailboxReadRelaySet.has(normalizedUrl)
|
|
||||||
? Array.from(new Set([...this.readRelayExclusions, normalizedUrl]))
|
|
||||||
: this.readRelayExclusions;
|
|
||||||
|
|
||||||
this.settings.update(
|
|
||||||
'nostrReadRelays',
|
|
||||||
remainingCustom.length > 0 ? remainingCustom : null
|
|
||||||
);
|
|
||||||
this.settings.update(
|
|
||||||
'nostrReadRelayExclusions',
|
|
||||||
nextExclusions.length > 0 ? nextExclusions : null
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@@ -168,7 +64,6 @@ export default class AppMenuSettingsNostr extends Component {
|
|||||||
@action
|
@action
|
||||||
resetReadRelays() {
|
resetReadRelays() {
|
||||||
this.settings.update('nostrReadRelays', null);
|
this.settings.update('nostrReadRelays', null);
|
||||||
this.settings.update('nostrReadRelayExclusions', null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@@ -176,57 +71,24 @@ export default class AppMenuSettingsNostr extends Component {
|
|||||||
const url = normalizeRelayUrl(this.newWriteRelay);
|
const url = normalizeRelayUrl(this.newWriteRelay);
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
|
|
||||||
const merged = mergeRequiredRelays(this.nostrData.requiredWriteRelays, [
|
const current =
|
||||||
...this.customWriteRelays,
|
this.settings.nostrWriteRelays || this.nostrData.defaultWriteRelays;
|
||||||
url,
|
const set = new Set([...current, url]);
|
||||||
]);
|
this.settings.update('nostrWriteRelays', Array.from(set));
|
||||||
const custom = excludeRequiredRelays(
|
|
||||||
merged,
|
|
||||||
this.nostrData.requiredWriteRelays
|
|
||||||
);
|
|
||||||
|
|
||||||
const writeExclusions = this.writeRelayExclusions.filter((relay) => {
|
|
||||||
return normalizeRelayUrl(relay) !== url;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.settings.update('nostrWriteRelays', custom.length > 0 ? custom : null);
|
|
||||||
this.settings.update(
|
|
||||||
'nostrWriteRelayExclusions',
|
|
||||||
writeExclusions.length > 0 ? writeExclusions : null
|
|
||||||
);
|
|
||||||
this.newWriteRelay = '';
|
this.newWriteRelay = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
removeWriteRelay(url) {
|
removeWriteRelay(url) {
|
||||||
if (this.requiredWriteRelaySet.has(url)) {
|
const current =
|
||||||
return;
|
this.settings.nostrWriteRelays || this.nostrData.defaultWriteRelays;
|
||||||
}
|
const filtered = current.filter((r) => r !== url);
|
||||||
|
this.settings.update('nostrWriteRelays', filtered);
|
||||||
const normalizedUrl = normalizeRelayUrl(url);
|
|
||||||
|
|
||||||
const remainingCustom = this.customWriteRelays.filter((relay) => {
|
|
||||||
return normalizeRelayUrl(relay) !== normalizedUrl;
|
|
||||||
});
|
|
||||||
|
|
||||||
const nextExclusions = this.mailboxWriteRelaySet.has(normalizedUrl)
|
|
||||||
? Array.from(new Set([...this.writeRelayExclusions, normalizedUrl]))
|
|
||||||
: this.writeRelayExclusions;
|
|
||||||
|
|
||||||
this.settings.update(
|
|
||||||
'nostrWriteRelays',
|
|
||||||
remainingCustom.length > 0 ? remainingCustom : null
|
|
||||||
);
|
|
||||||
this.settings.update(
|
|
||||||
'nostrWriteRelayExclusions',
|
|
||||||
nextExclusions.length > 0 ? nextExclusions : null
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
resetWriteRelays() {
|
resetWriteRelays() {
|
||||||
this.settings.update('nostrWriteRelays', null);
|
this.settings.update('nostrWriteRelays', null);
|
||||||
this.settings.update('nostrWriteRelayExclusions', null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@@ -250,20 +112,18 @@ export default class AppMenuSettingsNostr extends Component {
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="new-read-relay">Read Relays</label>
|
<label for="new-read-relay">Read Relays</label>
|
||||||
<ul class="relay-list">
|
<ul class="relay-list">
|
||||||
{{#each this.readRelaysForDisplay as |relay|}}
|
{{#each this.nostrData.activeReadRelays as |relay|}}
|
||||||
<li>
|
<li>
|
||||||
<span>{{stripProtocol relay.url}}</span>
|
<span>{{stripProtocol relay}}</span>
|
||||||
{{#unless relay.isRequired}}
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
class="btn-remove-relay"
|
||||||
class="btn-remove-relay"
|
title="Remove relay"
|
||||||
title="Remove relay"
|
aria-label="Remove"
|
||||||
aria-label="Remove"
|
{{on "click" (fn this.removeReadRelay relay)}}
|
||||||
{{on "click" (fn this.removeReadRelay relay.url)}}
|
>
|
||||||
>
|
<Icon @name="x" @size={{14}} @color="currentColor" />
|
||||||
<Icon @name="x" @size={{14}} @color="currentColor" />
|
</button>
|
||||||
</button>
|
|
||||||
{{/unless}}
|
|
||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -283,7 +143,7 @@ export default class AppMenuSettingsNostr extends Component {
|
|||||||
{{on "click" this.addReadRelay}}
|
{{on "click" this.addReadRelay}}
|
||||||
>Add</button>
|
>Add</button>
|
||||||
</div>
|
</div>
|
||||||
{{#if this.hasReadOverrides}}
|
{{#if this.settings.nostrReadRelays}}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-link reset-relays"
|
class="btn-link reset-relays"
|
||||||
@@ -297,20 +157,18 @@ export default class AppMenuSettingsNostr extends Component {
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="new-write-relay">Write Relays</label>
|
<label for="new-write-relay">Write Relays</label>
|
||||||
<ul class="relay-list">
|
<ul class="relay-list">
|
||||||
{{#each this.writeRelaysForDisplay as |relay|}}
|
{{#each this.nostrData.activeWriteRelays as |relay|}}
|
||||||
<li>
|
<li>
|
||||||
<span>{{stripProtocol relay.url}}</span>
|
<span>{{stripProtocol relay}}</span>
|
||||||
{{#unless relay.isRequired}}
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
class="btn-remove-relay"
|
||||||
class="btn-remove-relay"
|
title="Remove relay"
|
||||||
title="Remove relay"
|
aria-label="Remove"
|
||||||
aria-label="Remove"
|
{{on "click" (fn this.removeWriteRelay relay)}}
|
||||||
{{on "click" (fn this.removeWriteRelay relay.url)}}
|
>
|
||||||
>
|
<Icon @name="x" @size={{14}} @color="currentColor" />
|
||||||
<Icon @name="x" @size={{14}} @color="currentColor" />
|
</button>
|
||||||
</button>
|
|
||||||
{{/unless}}
|
|
||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -330,7 +188,7 @@ export default class AppMenuSettingsNostr extends Component {
|
|||||||
{{on "click" this.addWriteRelay}}
|
{{on "click" this.addWriteRelay}}
|
||||||
>Add</button>
|
>Add</button>
|
||||||
</div>
|
</div>
|
||||||
{{#if this.hasWriteOverrides}}
|
{{#if this.settings.nostrWriteRelays}}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-link reset-relays"
|
class="btn-link reset-relays"
|
||||||
|
|||||||
+21
-9
@@ -20,6 +20,7 @@ import { Style, Circle, Fill, Stroke, Icon } from 'ol/style.js';
|
|||||||
import { apply } from 'ol-mapbox-style';
|
import { apply } from 'ol-mapbox-style';
|
||||||
import { getIcon } from '../utils/icons';
|
import { getIcon } from '../utils/icons';
|
||||||
import { getIconNameForTags } from '../utils/osm-icons';
|
import { getIconNameForTags } from '../utils/osm-icons';
|
||||||
|
import config from 'marco/config/environment';
|
||||||
|
|
||||||
export default class MapComponent extends Component {
|
export default class MapComponent extends Component {
|
||||||
@service osm;
|
@service osm;
|
||||||
@@ -286,9 +287,26 @@ export default class MapComponent extends Component {
|
|||||||
this.mapUi.updateCenter(initialCenter[1], initialCenter[0]);
|
this.mapUi.updateCenter(initialCenter[1], initialCenter[0]);
|
||||||
this.mapUi.updateZoom(view.getZoom());
|
this.mapUi.updateZoom(view.getZoom());
|
||||||
|
|
||||||
apply(this.mapInstance, 'https://tiles.openfreemap.org/styles/liberty', {
|
if (config.environment === 'test') {
|
||||||
webfonts: 'data:text/css,',
|
apply(this.mapInstance, {
|
||||||
});
|
version: 8,
|
||||||
|
name: 'Test Style',
|
||||||
|
sources: {},
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: 'background',
|
||||||
|
type: 'background',
|
||||||
|
paint: {
|
||||||
|
'background-color': '#f8f4f0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
apply(this.mapInstance, 'https://tiles.openfreemap.org/styles/liberty', {
|
||||||
|
webfonts: 'data:text/css,',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.searchOverlayElement = document.createElement('div');
|
this.searchOverlayElement = document.createElement('div');
|
||||||
this.searchOverlayElement.className = 'search-pulse';
|
this.searchOverlayElement.className = 'search-pulse';
|
||||||
@@ -1191,12 +1209,6 @@ export default class MapComponent extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.mapUi.searchResults && this.mapUi.searchResults.length > 0) {
|
|
||||||
console.debug('Clearing active search and markers on map click');
|
|
||||||
this.router.transitionTo('index');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Require Zoom >= 17 for generic map searches
|
// Require Zoom >= 17 for generic map searches
|
||||||
// This prevents accidental searches when interacting with the map at a high level
|
// This prevents accidental searches when interacting with the map at a high level
|
||||||
const currentZoom = this.mapInstance.getView().getZoom();
|
const currentZoom = this.mapInstance.getView().getZoom();
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export default class PlaceDetails extends Component {
|
|||||||
@service storage;
|
@service storage;
|
||||||
@service nostrAuth;
|
@service nostrAuth;
|
||||||
@service nostrData;
|
@service nostrData;
|
||||||
@service mapUi;
|
|
||||||
@tracked isEditing = false;
|
@tracked isEditing = false;
|
||||||
@tracked showLists = false;
|
@tracked showLists = false;
|
||||||
@tracked isPhotoUploadActive = false;
|
@tracked isPhotoUploadActive = false;
|
||||||
@@ -346,21 +345,9 @@ export default class PlaceDetails extends Component {
|
|||||||
|
|
||||||
get osmUrl() {
|
get osmUrl() {
|
||||||
const id = this.place.osmId;
|
const id = this.place.osmId;
|
||||||
if (id) {
|
if (!id) return null;
|
||||||
const type = this.place.osmType || 'node';
|
const type = this.place.osmType || 'node';
|
||||||
return `https://www.openstreetmap.org/${type}/${id}`;
|
return `https://www.openstreetmap.org/${type}/${id}`;
|
||||||
}
|
|
||||||
|
|
||||||
const lat = this.place.lat;
|
|
||||||
const lon = this.place.lon;
|
|
||||||
if (!lat || !lon) return null;
|
|
||||||
|
|
||||||
const viewLat = this.mapUi.currentCenter?.lat ?? lat;
|
|
||||||
const viewLon = this.mapUi.currentCenter?.lon ?? lon;
|
|
||||||
const zoom = this.mapUi.currentZoom ?? 17;
|
|
||||||
const roundedZoom = Math.round(zoom);
|
|
||||||
|
|
||||||
return `https://www.openstreetmap.org/search?lat=${lat}&lon=${lon}&zoom=${roundedZoom}#map=${roundedZoom}/${Number(viewLat).toFixed(5)}/${Number(viewLon).toFixed(5)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get gmapsUrl() {
|
get gmapsUrl() {
|
||||||
@@ -604,7 +591,7 @@ export default class PlaceDetails extends Component {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if this.place.osmId}}
|
{{#if this.osmUrl}}
|
||||||
<div class="meta-info">
|
<div class="meta-info">
|
||||||
<p class="content-with-icon">
|
<p class="content-with-icon">
|
||||||
<Icon @name="feather-camera" />
|
<Icon @name="feather-camera" />
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export default class PlaceListsManager extends Component {
|
|||||||
checked={{this.isSaved}}
|
checked={{this.isSaved}}
|
||||||
{{on "change" this.toggleSaved}}
|
{{on "change" this.toggleSaved}}
|
||||||
/>
|
/>
|
||||||
<span class="list-color-dot"></span>
|
<span class="list-color"></span>
|
||||||
<span class="list-name">Saved places</span>
|
<span class="list-name">Saved places</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,7 +122,7 @@ export default class PlaceListsManager extends Component {
|
|||||||
/>
|
/>
|
||||||
{{! template-lint-disable no-inline-styles }}
|
{{! template-lint-disable no-inline-styles }}
|
||||||
<span
|
<span
|
||||||
class="list-color-dot"
|
class="list-color"
|
||||||
style={{this.styleFor list.color}}
|
style={{this.styleFor list.color}}
|
||||||
></span>
|
></span>
|
||||||
<span class="list-name">{{list.title}}</span>
|
<span class="list-name">{{list.title}}</span>
|
||||||
|
|||||||
@@ -5,13 +5,10 @@ import { on } from '@ember/modifier';
|
|||||||
import { fn } from '@ember/helper';
|
import { fn } from '@ember/helper';
|
||||||
import or from 'ember-truth-helpers/helpers/or';
|
import or from 'ember-truth-helpers/helpers/or';
|
||||||
import eq from 'ember-truth-helpers/helpers/eq';
|
import eq from 'ember-truth-helpers/helpers/eq';
|
||||||
import and from 'ember-truth-helpers/helpers/and';
|
|
||||||
import not from 'ember-truth-helpers/helpers/not';
|
|
||||||
import PlaceDetails from './place-details';
|
import PlaceDetails from './place-details';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import humanizeOsmTag from '../helpers/humanize-osm-tag';
|
import humanizeOsmTag from '../helpers/humanize-osm-tag';
|
||||||
import { getLocalizedName, getPlaceType } from '../utils/osm';
|
import { getLocalizedName, getPlaceType } from '../utils/osm';
|
||||||
import restoreScroll from '../modifiers/restore-scroll';
|
|
||||||
|
|
||||||
export default class PlacesSidebar extends Component {
|
export default class PlacesSidebar extends Component {
|
||||||
@service storage;
|
@service storage;
|
||||||
@@ -163,11 +160,7 @@ export default class PlacesSidebar extends Component {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div
|
<div class="sidebar-header {{if this.hasHeaderPhoto 'no-border'}}">
|
||||||
class="sidebar-header
|
|
||||||
{{if this.hasHeaderPhoto 'no-border'}}
|
|
||||||
{{if (and (not @selectedPlace) @onBack) 'has-back-btn'}}"
|
|
||||||
>
|
|
||||||
{{#if @selectedPlace}}
|
{{#if @selectedPlace}}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -175,32 +168,11 @@ export default class PlacesSidebar extends Component {
|
|||||||
{{on "click" this.clearSelection}}
|
{{on "click" this.clearSelection}}
|
||||||
><Icon @name="arrow-left" @size={{20}} @color="#333" /></button>
|
><Icon @name="arrow-left" @size={{20}} @color="#333" /></button>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if @onBack}}
|
{{#if this.isNearbySearch}}
|
||||||
<button type="button" class="back-btn" {{on "click" @onBack}}><Icon
|
<h2><Icon @name="target" @size={{20}} @color="#ea4335" />
|
||||||
@name="arrow-left"
|
Nearby</h2>
|
||||||
@size={{20}}
|
|
||||||
@color="#333"
|
|
||||||
/></button>
|
|
||||||
{{/if}}
|
|
||||||
{{#if @onBack}}
|
|
||||||
<h2 class="sidebar-header-text-centered">
|
|
||||||
<span class="sidebar-header-icon-wrapper">
|
|
||||||
<Icon
|
|
||||||
@name="bookmark"
|
|
||||||
@size={{20}}
|
|
||||||
@color={{or @color "#898989"}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
{{@title}}
|
|
||||||
</h2>
|
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if this.isNearbySearch}}
|
<h2><Icon @name="search" @size={{20}} @color="#333" /> Results</h2>
|
||||||
<h2><Icon @name="target" @size={{20}} @color="#ea4335" />
|
|
||||||
Nearby</h2>
|
|
||||||
{{else}}
|
|
||||||
<h2><Icon @name="search" @size={{20}} @color="#333" />
|
|
||||||
Results</h2>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<button type="button" class="close-btn" {{on "click" @onClose}}><Icon
|
<button type="button" class="close-btn" {{on "click" @onClose}}><Icon
|
||||||
@@ -210,7 +182,7 @@ export default class PlacesSidebar extends Component {
|
|||||||
/></button>
|
/></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-content" {{restoreScroll @scrollTop}}>
|
<div class="sidebar-content">
|
||||||
{{#if @selectedPlace}}
|
{{#if @selectedPlace}}
|
||||||
<PlaceDetails
|
<PlaceDetails
|
||||||
@place={{@selectedPlace}}
|
@place={{@selectedPlace}}
|
||||||
@@ -218,58 +190,54 @@ export default class PlacesSidebar extends Component {
|
|||||||
@onSave={{this.updateBookmark}}
|
@onSave={{this.updateBookmark}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if @isLoading}}
|
{{#if @places}}
|
||||||
<div class="sidebar-loading">
|
<ul class="places-list">
|
||||||
<Icon @name="loading-ring" @size={{24}} @color="#898989" />
|
{{#each @places as |place|}}
|
||||||
</div>
|
<li>
|
||||||
{{else}}
|
<button
|
||||||
{{#if @places}}
|
type="button"
|
||||||
<ul class="places-list">
|
class="place-item"
|
||||||
{{#each @places as |place|}}
|
{{on "click" (fn this.selectPlace place)}}
|
||||||
<li>
|
>
|
||||||
<button
|
<div class="place-name">{{or
|
||||||
type="button"
|
place.title
|
||||||
class="place-item"
|
place.osmTags.name
|
||||||
{{on "click" (fn this.selectPlace place)}}
|
place.osmTags.name:en
|
||||||
>
|
"Unnamed Place"
|
||||||
<div class="place-name">{{or
|
}}</div>
|
||||||
place.title
|
<div class="place-type">
|
||||||
place.osmTags.name
|
{{#if (eq place.source "osm")}}
|
||||||
place.osmTags.name:en
|
{{humanizeOsmTag place.type}}
|
||||||
"Unnamed Place"
|
{{else if (eq place.source "photon")}}
|
||||||
}}</div>
|
{{place.description}}
|
||||||
<div class="place-type">
|
{{else}}
|
||||||
{{#if (eq place.source "osm")}}
|
{{#if place.osmTags}}
|
||||||
{{humanizeOsmTag place.type}}
|
{{humanizeOsmTag (getPlaceType place.osmTags)}}
|
||||||
{{else if (eq place.source "photon")}}
|
{{else if place.description}}
|
||||||
{{place.description}}
|
{{place.description}}
|
||||||
{{else if (getPlaceType place.osmTags)}}
|
|
||||||
{{getPlaceType place.osmTags}}
|
|
||||||
{{else}}
|
|
||||||
Saved place
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
{{/if}}
|
||||||
</button>
|
</div>
|
||||||
</li>
|
</button>
|
||||||
{{/each}}
|
</li>
|
||||||
</ul>
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
{{else}}
|
||||||
|
{{#if this.isNearbySearch}}
|
||||||
|
<p class="empty-state">No places found nearby.</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if this.isNearbySearch}}
|
<p class="empty-state">No results found.</p>
|
||||||
<p class="empty-state">No places found nearby.</p>
|
|
||||||
{{else}}
|
|
||||||
<p class="empty-state">No results found.</p>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline create-place"
|
|
||||||
{{on "click" this.createNewPlace}}
|
|
||||||
>
|
|
||||||
<Icon @name="plus" @size={{18}} @color="var(--link-color)" />
|
|
||||||
Create new place
|
|
||||||
</button>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline create-place"
|
||||||
|
{{on "click" this.createNewPlace}}
|
||||||
|
>
|
||||||
|
<Icon @name="plus" @size={{18}} @color="var(--link-color)" />
|
||||||
|
Create new place
|
||||||
|
</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
import Controller from '@ember/controller';
|
|
||||||
import { service } from '@ember/service';
|
|
||||||
import { action } from '@ember/object';
|
|
||||||
import { tracked } from '@glimmer/tracking';
|
|
||||||
import { task } from 'ember-concurrency';
|
|
||||||
|
|
||||||
export default class ListsListController extends Controller {
|
|
||||||
@service router;
|
|
||||||
@service mapUi;
|
|
||||||
@service storage;
|
|
||||||
|
|
||||||
@tracked model;
|
|
||||||
@tracked loadedPlaces = [];
|
|
||||||
|
|
||||||
get listId() {
|
|
||||||
return this.model?.list_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPlacesTask = task({ restartable: true }, async (listId) => {
|
|
||||||
this.loadedPlaces = []; // Clear previous elements immediately to show fresh loader
|
|
||||||
try {
|
|
||||||
this.loadedPlaces = await this.storage.getPlacesInList(listId);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load places in list', listId, e);
|
|
||||||
this.loadedPlaces = [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
get scrollTop() {
|
|
||||||
return this.mapUi.getScrollPosition(`list-${this.listId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
get listColor() {
|
|
||||||
const list = this.storage.lists.find((l) => l.id === this.listId);
|
|
||||||
if (list && list.color) {
|
|
||||||
return list.color;
|
|
||||||
}
|
|
||||||
return getComputedStyle(document.documentElement)
|
|
||||||
.getPropertyValue('--default-list-color')
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
get listTitle() {
|
|
||||||
const list = this.storage.lists.find((l) => l.id === this.listId);
|
|
||||||
return list ? list.title : 'Collections';
|
|
||||||
}
|
|
||||||
|
|
||||||
get places() {
|
|
||||||
const currentList = this.storage.lists.find((l) => l.id === this.listId);
|
|
||||||
const placeRefsIds = new Set(
|
|
||||||
currentList?.placeRefs?.map((ref) => ref.id) || []
|
|
||||||
);
|
|
||||||
|
|
||||||
// Filter live tracked savedPlaces that are in this list
|
|
||||||
const livePlaces = this.storage.savedPlaces.filter((p) =>
|
|
||||||
placeRefsIds.has(p.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
const merged = [];
|
|
||||||
const seen = new Set();
|
|
||||||
|
|
||||||
// Process live state first to reflect deletions/edits immediately
|
|
||||||
livePlaces.forEach((p) => {
|
|
||||||
merged.push(p);
|
|
||||||
seen.add(p.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Supplement with any background-fetched places that are still valid but not in live state yet
|
|
||||||
this.loadedPlaces.forEach((p) => {
|
|
||||||
if (placeRefsIds.has(p.id) && !seen.has(p.id)) {
|
|
||||||
merged.push(p);
|
|
||||||
seen.add(p.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
selectPlace(place) {
|
|
||||||
if (place) {
|
|
||||||
const sidebarContent = document.querySelector('.sidebar-content');
|
|
||||||
if (sidebarContent) {
|
|
||||||
this.mapUi.saveScrollPosition(
|
|
||||||
`list-${this.listId}`,
|
|
||||||
sidebarContent.scrollTop
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.mapUi.returnToRoute = {
|
|
||||||
name: 'lists.list',
|
|
||||||
model: this.listId,
|
|
||||||
};
|
|
||||||
this.mapUi.showSidebar();
|
|
||||||
this.mapUi.preventNextZoom = true;
|
|
||||||
this.router.transitionTo('place', place);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
close() {
|
|
||||||
this.router.transitionTo('index');
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
backToLists() {
|
|
||||||
this.router.transitionTo('lists.index');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,26 +20,7 @@ export default class SearchController extends Controller {
|
|||||||
category = null;
|
category = null;
|
||||||
|
|
||||||
fetchResultsTask = task({ restartable: true }, async (params) => {
|
fetchResultsTask = task({ restartable: true }, async (params) => {
|
||||||
// 1. Check if the incoming parameters match our currently loaded search
|
// Hide sidebar and clear previous results immediately to signal a new search
|
||||||
const isSameSearch =
|
|
||||||
this.mapUi.currentSearch &&
|
|
||||||
params.q === this.mapUi.currentSearch.q &&
|
|
||||||
params.category === this.mapUi.currentSearch.category &&
|
|
||||||
params.lat === this.mapUi.currentSearch.lat &&
|
|
||||||
params.lon === this.mapUi.currentSearch.lon;
|
|
||||||
|
|
||||||
const hasResults =
|
|
||||||
this.mapUi.searchResults && this.mapUi.searchResults.length > 0;
|
|
||||||
|
|
||||||
// 2. If it's a back navigation to the exact same search, resolve instantly with no animation
|
|
||||||
if (isSameSearch && hasResults) {
|
|
||||||
if (this.mapUi.isSidebarVisible) {
|
|
||||||
this.mapUi.showSidebar();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Otherwise, this is a brand new search: hide the sidebar and clear previous results immediately to signal a new search
|
|
||||||
this.mapUi.hideSidebar();
|
this.mapUi.hideSidebar();
|
||||||
this.mapUi.clearSearchResults();
|
this.mapUi.clearSearchResults();
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import { modifier } from 'ember-modifier';
|
|
||||||
|
|
||||||
export default modifier((element, [scrollTop]) => {
|
|
||||||
if (element && typeof scrollTop === 'number' && scrollTop > 0) {
|
|
||||||
// Restore inside requestAnimationFrame to guarantee layout rendering is ready
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
element.scrollTop = scrollTop;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -10,10 +10,6 @@ Router.map(function () {
|
|||||||
this.route('place', { path: '/place/:place_id' });
|
this.route('place', { path: '/place/:place_id' });
|
||||||
this.route('place.new', { path: '/place/new' });
|
this.route('place.new', { path: '/place/new' });
|
||||||
this.route('search');
|
this.route('search');
|
||||||
this.route('menu');
|
|
||||||
this.route('lists', function () {
|
|
||||||
this.route('list', { path: '/:list_id' });
|
|
||||||
});
|
|
||||||
this.route('oauth', function () {
|
this.route('oauth', function () {
|
||||||
this.route('osm-callback', { path: '/osm/callback' });
|
this.route('osm-callback', { path: '/osm/callback' });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,5 @@ export default class IndexRoute extends Route {
|
|||||||
|
|
||||||
activate() {
|
activate() {
|
||||||
this.mapUi.clearSearchResults();
|
this.mapUi.clearSearchResults();
|
||||||
this.mapUi.hideSidebar();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import Route from '@ember/routing/route';
|
|
||||||
import { service } from '@ember/service';
|
|
||||||
|
|
||||||
export default class ListsRoute extends Route {
|
|
||||||
@service mapUi;
|
|
||||||
|
|
||||||
activate() {
|
|
||||||
this.mapUi.showSidebar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import Route from '@ember/routing/route';
|
|
||||||
|
|
||||||
export default class ListsIndexRoute extends Route {}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import Route from '@ember/routing/route';
|
|
||||||
import { service } from '@ember/service';
|
|
||||||
|
|
||||||
export default class ListsListRoute extends Route {
|
|
||||||
@service storage;
|
|
||||||
|
|
||||||
model(params) {
|
|
||||||
// Resolve instantly so transition happens in 0ms!
|
|
||||||
return { list_id: params.list_id };
|
|
||||||
}
|
|
||||||
|
|
||||||
setupController(controller, model) {
|
|
||||||
console.debug('DEBUG: setupController controller is:', controller);
|
|
||||||
console.debug(
|
|
||||||
'DEBUG: controller.loadPlacesTask is:',
|
|
||||||
controller?.loadPlacesTask
|
|
||||||
);
|
|
||||||
controller.model = model;
|
|
||||||
super.setupController(controller, model);
|
|
||||||
if (controller && controller.loadPlacesTask) {
|
|
||||||
controller.loadPlacesTask.perform(model.list_id);
|
|
||||||
} else {
|
|
||||||
console.error('DEBUG: ERROR! controller.loadPlacesTask is undefined!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import Route from '@ember/routing/route';
|
|
||||||
import { service } from '@ember/service';
|
|
||||||
|
|
||||||
export default class MenuRoute extends Route {
|
|
||||||
@service mapUi;
|
|
||||||
|
|
||||||
activate() {
|
|
||||||
this.mapUi.showSidebar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -108,7 +108,6 @@ export default class PlaceRoute extends Route {
|
|||||||
this.mapUi.clearSelection();
|
this.mapUi.clearSelection();
|
||||||
// Reset the "return to search" flag so it doesn't persist to subsequent navigations
|
// Reset the "return to search" flag so it doesn't persist to subsequent navigations
|
||||||
this.mapUi.returnToSearch = false;
|
this.mapUi.returnToSearch = false;
|
||||||
this.mapUi.returnToRoute = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadOsmPlace(id, type = null) {
|
async loadOsmPlace(id, type = null) {
|
||||||
|
|||||||
+1
-23
@@ -1,6 +1,5 @@
|
|||||||
import Service, { service } from '@ember/service';
|
import Service, { service } from '@ember/service';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { action } from '@ember/object';
|
|
||||||
|
|
||||||
export default class MapUiService extends Service {
|
export default class MapUiService extends Service {
|
||||||
@service nostrData;
|
@service nostrData;
|
||||||
@@ -10,7 +9,6 @@ export default class MapUiService extends Service {
|
|||||||
@tracked isCreating = false;
|
@tracked isCreating = false;
|
||||||
@tracked creationCoordinates = null;
|
@tracked creationCoordinates = null;
|
||||||
@tracked returnToSearch = false;
|
@tracked returnToSearch = false;
|
||||||
@tracked returnToRoute = null;
|
|
||||||
@tracked currentCenter = null;
|
@tracked currentCenter = null;
|
||||||
@tracked currentBounds = null;
|
@tracked currentBounds = null;
|
||||||
@tracked currentZoom = null;
|
@tracked currentZoom = null;
|
||||||
@@ -21,33 +19,13 @@ export default class MapUiService extends Service {
|
|||||||
@tracked currentSearch = null;
|
@tracked currentSearch = null;
|
||||||
@tracked loadingState = null;
|
@tracked loadingState = null;
|
||||||
@tracked isSidebarVisible = false;
|
@tracked isSidebarVisible = false;
|
||||||
@tracked isSidebarOpening = false;
|
|
||||||
|
|
||||||
scrollPositions = {};
|
|
||||||
|
|
||||||
@action
|
|
||||||
saveScrollPosition(key, value) {
|
|
||||||
this.scrollPositions[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
getScrollPosition(key) {
|
|
||||||
return this.scrollPositions[key] || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
showSidebar() {
|
showSidebar() {
|
||||||
if (!this.isSidebarVisible) {
|
this.isSidebarVisible = true;
|
||||||
this.isSidebarVisible = true;
|
|
||||||
this.isSidebarOpening = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.isSidebarOpening = false;
|
|
||||||
}, 250);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hideSidebar() {
|
hideSidebar() {
|
||||||
this.isSidebarVisible = false;
|
this.isSidebarVisible = false;
|
||||||
this.isSidebarOpening = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selectPlace(place, options = {}) {
|
selectPlace(place, options = {}) {
|
||||||
|
|||||||
+26
-50
@@ -6,12 +6,7 @@ import { MailboxesModel } from 'applesauce-core/models/mailboxes';
|
|||||||
import { npubEncode } from 'applesauce-core/helpers/pointers';
|
import { npubEncode } from 'applesauce-core/helpers/pointers';
|
||||||
import { persistEventsToCache } from 'applesauce-core/helpers/event-cache';
|
import { persistEventsToCache } from 'applesauce-core/helpers/event-cache';
|
||||||
import { NostrIDB, openDB } from 'nostr-idb';
|
import { NostrIDB, openDB } from 'nostr-idb';
|
||||||
import {
|
import { normalizeRelayUrl } from '../utils/nostr';
|
||||||
excludeRequiredRelays,
|
|
||||||
mergeRequiredRelays,
|
|
||||||
normalizeRelayUrl,
|
|
||||||
uniqNormalizedRelays,
|
|
||||||
} from '../utils/nostr';
|
|
||||||
import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
|
import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
|
||||||
|
|
||||||
const DIRECTORY_RELAYS = [
|
const DIRECTORY_RELAYS = [
|
||||||
@@ -88,62 +83,43 @@ export default class NostrDataService extends Service {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get requiredReadRelays() {
|
get defaultReadRelays() {
|
||||||
return DEFAULT_READ_RELAYS;
|
const mailboxes = (this.mailboxes?.inboxes || [])
|
||||||
}
|
|
||||||
|
|
||||||
get requiredWriteRelays() {
|
|
||||||
return DEFAULT_WRITE_RELAYS;
|
|
||||||
}
|
|
||||||
|
|
||||||
get mailboxReadRelays() {
|
|
||||||
return (this.mailboxes?.inboxes || [])
|
|
||||||
.map(normalizeRelayUrl)
|
.map(normalizeRelayUrl)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
const defaults = DEFAULT_READ_RELAYS.map(normalizeRelayUrl).filter(Boolean);
|
||||||
|
return Array.from(new Set([...defaults, ...mailboxes]));
|
||||||
}
|
}
|
||||||
|
|
||||||
get mailboxWriteRelays() {
|
get defaultWriteRelays() {
|
||||||
return (this.mailboxes?.outboxes || [])
|
const mailboxes = (this.mailboxes?.outboxes || [])
|
||||||
.map(normalizeRelayUrl)
|
.map(normalizeRelayUrl)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
const defaults =
|
||||||
|
DEFAULT_WRITE_RELAYS.map(normalizeRelayUrl).filter(Boolean);
|
||||||
get configuredReadRelays() {
|
return Array.from(new Set([...defaults, ...mailboxes]));
|
||||||
const configured = uniqNormalizedRelays([
|
|
||||||
...this.mailboxReadRelays,
|
|
||||||
...(this.settings.nostrReadRelays || []),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return excludeRequiredRelays(
|
|
||||||
configured,
|
|
||||||
this.settings.nostrReadRelayExclusions || []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get configuredWriteRelays() {
|
|
||||||
const configured = uniqNormalizedRelays([
|
|
||||||
...this.mailboxWriteRelays,
|
|
||||||
...(this.settings.nostrWriteRelays || []),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return excludeRequiredRelays(
|
|
||||||
configured,
|
|
||||||
this.settings.nostrWriteRelayExclusions || []
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeReadRelays() {
|
get activeReadRelays() {
|
||||||
return mergeRequiredRelays(
|
if (this.settings.nostrReadRelays) {
|
||||||
this.requiredReadRelays,
|
return Array.from(
|
||||||
this.configuredReadRelays
|
new Set(
|
||||||
);
|
this.settings.nostrReadRelays.map(normalizeRelayUrl).filter(Boolean)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.defaultReadRelays;
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeWriteRelays() {
|
get activeWriteRelays() {
|
||||||
return mergeRequiredRelays(
|
if (this.settings.nostrWriteRelays) {
|
||||||
this.requiredWriteRelays,
|
return Array.from(
|
||||||
this.configuredWriteRelays
|
new Set(
|
||||||
);
|
this.settings.nostrWriteRelays.map(normalizeRelayUrl).filter(Boolean)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.defaultWriteRelays;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPlacesInBounds(bbox) {
|
async loadPlacesInBounds(bbox) {
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ const DEFAULT_SETTINGS = {
|
|||||||
nostrPhotoFallbackUploads: false,
|
nostrPhotoFallbackUploads: false,
|
||||||
nostrReadRelays: null,
|
nostrReadRelays: null,
|
||||||
nostrWriteRelays: null,
|
nostrWriteRelays: null,
|
||||||
nostrReadRelayExclusions: null,
|
|
||||||
nostrWriteRelayExclusions: null,
|
|
||||||
experimentalEnablePhotoDeletion: false,
|
experimentalEnablePhotoDeletion: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,9 +21,6 @@ export default class SettingsService extends Service {
|
|||||||
DEFAULT_SETTINGS.nostrPhotoFallbackUploads;
|
DEFAULT_SETTINGS.nostrPhotoFallbackUploads;
|
||||||
@tracked nostrReadRelays = DEFAULT_SETTINGS.nostrReadRelays;
|
@tracked nostrReadRelays = DEFAULT_SETTINGS.nostrReadRelays;
|
||||||
@tracked nostrWriteRelays = DEFAULT_SETTINGS.nostrWriteRelays;
|
@tracked nostrWriteRelays = DEFAULT_SETTINGS.nostrWriteRelays;
|
||||||
@tracked nostrReadRelayExclusions = DEFAULT_SETTINGS.nostrReadRelayExclusions;
|
|
||||||
@tracked nostrWriteRelayExclusions =
|
|
||||||
DEFAULT_SETTINGS.nostrWriteRelayExclusions;
|
|
||||||
@tracked experimentalEnablePhotoDeletion =
|
@tracked experimentalEnablePhotoDeletion =
|
||||||
DEFAULT_SETTINGS.experimentalEnablePhotoDeletion;
|
DEFAULT_SETTINGS.experimentalEnablePhotoDeletion;
|
||||||
|
|
||||||
@@ -116,8 +111,6 @@ export default class SettingsService extends Service {
|
|||||||
this.nostrPhotoFallbackUploads = finalSettings.nostrPhotoFallbackUploads;
|
this.nostrPhotoFallbackUploads = finalSettings.nostrPhotoFallbackUploads;
|
||||||
this.nostrReadRelays = finalSettings.nostrReadRelays;
|
this.nostrReadRelays = finalSettings.nostrReadRelays;
|
||||||
this.nostrWriteRelays = finalSettings.nostrWriteRelays;
|
this.nostrWriteRelays = finalSettings.nostrWriteRelays;
|
||||||
this.nostrReadRelayExclusions = finalSettings.nostrReadRelayExclusions;
|
|
||||||
this.nostrWriteRelayExclusions = finalSettings.nostrWriteRelayExclusions;
|
|
||||||
this.experimentalEnablePhotoDeletion =
|
this.experimentalEnablePhotoDeletion =
|
||||||
finalSettings.experimentalEnablePhotoDeletion;
|
finalSettings.experimentalEnablePhotoDeletion;
|
||||||
|
|
||||||
@@ -134,8 +127,6 @@ export default class SettingsService extends Service {
|
|||||||
nostrPhotoFallbackUploads: this.nostrPhotoFallbackUploads,
|
nostrPhotoFallbackUploads: this.nostrPhotoFallbackUploads,
|
||||||
nostrReadRelays: this.nostrReadRelays,
|
nostrReadRelays: this.nostrReadRelays,
|
||||||
nostrWriteRelays: this.nostrWriteRelays,
|
nostrWriteRelays: this.nostrWriteRelays,
|
||||||
nostrReadRelayExclusions: this.nostrReadRelayExclusions,
|
|
||||||
nostrWriteRelayExclusions: this.nostrWriteRelayExclusions,
|
|
||||||
experimentalEnablePhotoDeletion: this.experimentalEnablePhotoDeletion,
|
experimentalEnablePhotoDeletion: this.experimentalEnablePhotoDeletion,
|
||||||
};
|
};
|
||||||
localStorage.setItem('marco:settings', JSON.stringify(settings));
|
localStorage.setItem('marco:settings', JSON.stringify(settings));
|
||||||
|
|||||||
@@ -270,11 +270,6 @@ export default class StorageService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPlacesInList(listId) {
|
|
||||||
if (!this.places || !this.places.lists) return [];
|
|
||||||
return this.places.lists.getPlaces(listId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadPlacesInBounds(bbox) {
|
async loadPlacesInBounds(bbox) {
|
||||||
// 1. Calculate required prefixes
|
// 1. Calculate required prefixes
|
||||||
const requiredPrefixes = getGeohashPrefixesInBbox(bbox);
|
const requiredPrefixes = getGeohashPrefixesInBbox(bbox);
|
||||||
|
|||||||
+40
-178
@@ -1,10 +1,8 @@
|
|||||||
/* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */
|
/* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--body-text-color: #333;
|
--default-list-color: #fc3;
|
||||||
--primary-background-color: #fff;
|
|
||||||
--hover-bg: #f8f9fa;
|
--hover-bg: #f8f9fa;
|
||||||
--divider-color: #eee;
|
|
||||||
--sidebar-width: 350px;
|
--sidebar-width: 350px;
|
||||||
--link-color: #2a7fff;
|
--link-color: #2a7fff;
|
||||||
--link-color-visited: #6a4fbf;
|
--link-color-visited: #6a4fbf;
|
||||||
@@ -12,7 +10,6 @@
|
|||||||
--marker-color-dark: #b31412;
|
--marker-color-dark: #b31412;
|
||||||
--danger-color: var(--marker-color-primary);
|
--danger-color: var(--marker-color-primary);
|
||||||
--danger-color-dark: var(--marker-color-dark);
|
--danger-color-dark: var(--marker-color-dark);
|
||||||
--default-list-color: #fc3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
@@ -34,7 +31,7 @@ body {
|
|||||||
font-family: 'Noto Sans', sans-serif;
|
font-family: 'Noto Sans', sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: var(--body-text-color);
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root,
|
#root,
|
||||||
@@ -115,6 +112,8 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
grid-area: search;
|
grid-area: search;
|
||||||
|
|
||||||
|
/* Ensure it sits at the start of its grid area */
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -128,6 +127,7 @@ body {
|
|||||||
|
|
||||||
@media (width > 768px) {
|
@media (width > 768px) {
|
||||||
.header-left {
|
.header-left {
|
||||||
|
/* Desktop: Ensure minimum width for search box so it's not squeezed */
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
max-width: 350px;
|
max-width: 350px;
|
||||||
}
|
}
|
||||||
@@ -140,6 +140,8 @@ body {
|
|||||||
|
|
||||||
.header-center {
|
.header-center {
|
||||||
grid-area: chips;
|
grid-area: chips;
|
||||||
|
|
||||||
|
/* Desktop: Center the chips block in the available space */
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 0; /* Allow shrinking */
|
min-width: 0; /* Allow shrinking */
|
||||||
@@ -154,6 +156,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (width <= 768px) {
|
@media (width <= 768px) {
|
||||||
|
/* No need to reset min-width/max-width since they are only set in media query above */
|
||||||
.header-center {
|
.header-center {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -380,7 +383,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
border-bottom: 1px solid var(--divider-color);
|
border-bottom: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-item:last-child {
|
.account-item:last-child {
|
||||||
@@ -457,11 +460,6 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden; /* Ensure flex children are contained */
|
overflow: hidden; /* Ensure flex children are contained */
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-opening .sidebar {
|
|
||||||
animation: sidebar-slide-in-left 0.18s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.app-menu-pane {
|
.sidebar.app-menu-pane {
|
||||||
@@ -480,13 +478,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-header {
|
.sidebar-header {
|
||||||
height: 56px; /* Strictly enforce identical vertical height */
|
padding: 1rem;
|
||||||
padding: 0 1rem; /* Keep horizontal padding, remove vertical padding */
|
border-bottom: 1px solid #eee;
|
||||||
border-bottom: 1px solid var(--divider-color);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box; /* Guarantee strict height boundaries */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-header.no-border {
|
.sidebar-header.no-border {
|
||||||
@@ -495,7 +491,7 @@ body {
|
|||||||
|
|
||||||
.sidebar-header h2 {
|
.sidebar-header h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.1rem;
|
font-size: 1.2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -530,7 +526,7 @@ body {
|
|||||||
padding-left: 1.4rem;
|
padding-left: 1.4rem;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--body-text-color);
|
color: #333;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
@@ -561,7 +557,7 @@ body {
|
|||||||
padding-left: 1.4rem;
|
padding-left: 1.4rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
color: var(--body-text-color);
|
color: #333;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,7 +628,7 @@ body {
|
|||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: var(--primary-background-color);
|
background-color: #fff;
|
||||||
border: 1px solid var(--danger-color);
|
border: 1px solid var(--danger-color);
|
||||||
color: var(--danger-color);
|
color: var(--danger-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -648,7 +644,7 @@ body {
|
|||||||
.btn-remove-relay:hover,
|
.btn-remove-relay:hover,
|
||||||
.btn-remove-relay:active {
|
.btn-remove-relay:active {
|
||||||
background-color: var(--danger-color);
|
background-color: var(--danger-color);
|
||||||
color: var(--primary-background-color);
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-relay-input {
|
.add-relay-input {
|
||||||
@@ -678,7 +674,7 @@ body {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
background: var(--hover-bg);
|
background: var(--hover-bg);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-bottom: 1px solid var(--divider-color);
|
border-bottom: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
@@ -700,8 +696,8 @@ body {
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
box-sizing: border-box; /* Ensure padding doesn't overflow width */
|
box-sizing: border-box; /* Ensure padding doesn't overflow width */
|
||||||
color: var(--body-text-color);
|
color: #333;
|
||||||
background-color: var(--primary-background-color);
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
@@ -712,7 +708,7 @@ body {
|
|||||||
|
|
||||||
select.form-control {
|
select.form-control {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background-color: var(--primary-background-color);
|
background-color: #fff;
|
||||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: right 0.75rem center;
|
background-position: right 0.75rem center;
|
||||||
@@ -788,7 +784,7 @@ select.form-control {
|
|||||||
.meta-info p:first-child {
|
.meta-info p:first-child {
|
||||||
margin-top: 1.2rem;
|
margin-top: 1.2rem;
|
||||||
padding-top: 1.2rem;
|
padding-top: 1.2rem;
|
||||||
border-top: 1px solid var(--divider-color);
|
border-top: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-info a,
|
.meta-info a,
|
||||||
@@ -847,9 +843,9 @@ abbr[title] {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid var(--divider-color);
|
border-bottom: 1px solid #eee;
|
||||||
background: var(--primary-background-color);
|
background: #fff;
|
||||||
color: var(--body-text-color);
|
color: #333;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
@@ -1020,7 +1016,7 @@ abbr[title] {
|
|||||||
.photo-carousel.inline .photo-carousel-track {
|
.photo-carousel.inline .photo-carousel-track {
|
||||||
scroll-snap-type: none;
|
scroll-snap-type: none;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
background-color: var(--primary-background-color);
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo-carousel.inline .carousel-slide {
|
.photo-carousel.inline .carousel-slide {
|
||||||
@@ -1099,7 +1095,7 @@ abbr[title] {
|
|||||||
|
|
||||||
.btn-outline {
|
.btn-outline {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--body-text-color);
|
color: #333;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1108,7 +1104,7 @@ abbr[title] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
color: var(--body-text-color);
|
color: #333;
|
||||||
border: 1px solid rgb(255 204 51 / 20%);
|
border: 1px solid rgb(255 204 51 / 20%);
|
||||||
background: rgb(255 204 51 / 30%);
|
background: rgb(255 204 51 / 30%);
|
||||||
}
|
}
|
||||||
@@ -1270,20 +1266,6 @@ span.icon {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-with-icon > span:not(.icon) {
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-with-icon > span:not(.icon) a {
|
|
||||||
display: inline-block;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-with-icon .icon {
|
.content-with-icon .icon {
|
||||||
margin-top: 0.15rem;
|
margin-top: 0.15rem;
|
||||||
}
|
}
|
||||||
@@ -1365,7 +1347,7 @@ span.icon {
|
|||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
color: var(--body-text-color);
|
color: #333;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
display: none;
|
display: none;
|
||||||
@@ -1426,11 +1408,6 @@ button.create-place {
|
|||||||
inset: auto 0 0;
|
inset: auto 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-opening .sidebar {
|
|
||||||
animation: sidebar-slide-up-bottom 0.18s cubic-bezier(0.16, 1, 0.3, 1)
|
|
||||||
forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-content {
|
.sidebar-content {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
@@ -1504,7 +1481,7 @@ button.create-place {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: var(--body-text-color);
|
color: #333;
|
||||||
outline: none;
|
outline: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
@@ -1535,7 +1512,7 @@ button.create-place {
|
|||||||
|
|
||||||
.search-submit-btn:hover {
|
.search-submit-btn:hover {
|
||||||
background: rgb(0 0 0 / 5%);
|
background: rgb(0 0 0 / 5%);
|
||||||
color: var(--body-text-color);
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-clear-btn {
|
.search-clear-btn {
|
||||||
@@ -1553,7 +1530,7 @@ button.create-place {
|
|||||||
|
|
||||||
.search-clear-btn:hover {
|
.search-clear-btn:hover {
|
||||||
background: rgb(0 0 0 / 5%);
|
background: rgb(0 0 0 / 5%);
|
||||||
color: var(--body-text-color);
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search Results Popover */
|
/* Search Results Popover */
|
||||||
@@ -1622,7 +1599,7 @@ button.create-place {
|
|||||||
|
|
||||||
.result-title {
|
.result-title {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--body-text-color);
|
color: #333;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -1674,7 +1651,7 @@ button.create-place {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
color: var(--body-text-color);
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.place-lists-manager input[type='checkbox'] {
|
.place-lists-manager input[type='checkbox'] {
|
||||||
@@ -1684,8 +1661,7 @@ button.create-place {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shared List Color Dot */
|
.place-lists-manager .list-color {
|
||||||
.list-color-dot {
|
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
background-color: var(--default-list-color);
|
background-color: var(--default-list-color);
|
||||||
@@ -1696,7 +1672,7 @@ button.create-place {
|
|||||||
|
|
||||||
.place-lists-manager .divider {
|
.place-lists-manager .divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: var(--divider-color);
|
background: #eee;
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1739,7 +1715,7 @@ button.create-place {
|
|||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 16px; /* Pill shape */
|
border-radius: 16px; /* Pill shape */
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: var(--body-text-color);
|
color: #333;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
box-shadow: 0 1px 3px rgb(0 0 0 / 10%);
|
box-shadow: 0 1px 3px rgb(0 0 0 / 10%);
|
||||||
@@ -1751,7 +1727,7 @@ button.create-place {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.category-chip:active {
|
.category-chip:active {
|
||||||
background: var(--divider-color);
|
background: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-chip:disabled {
|
.category-chip:disabled {
|
||||||
@@ -1886,7 +1862,7 @@ button.create-place {
|
|||||||
|
|
||||||
.photo-tag-chip {
|
.photo-tag-chip {
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
color: var(--body-text-color);
|
color: #333;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
@@ -1896,7 +1872,7 @@ button.create-place {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.photo-tag-chip:hover {
|
.photo-tag-chip:hover {
|
||||||
background: var(--divider-color);
|
background: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo-tag-chip.is-selected {
|
.photo-tag-chip.is-selected {
|
||||||
@@ -2127,7 +2103,7 @@ button.create-place {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
color: var(--body-text-color);
|
color: #333;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2147,117 +2123,3 @@ button.create-place {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Snappy slide-in from left (Desktop) */
|
|
||||||
@keyframes sidebar-slide-in-left {
|
|
||||||
from {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Snappy slide-up from bottom (Mobile) */
|
|
||||||
@keyframes sidebar-slide-up-bottom {
|
|
||||||
from {
|
|
||||||
transform: translateY(100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Lists Index Sidebar Menu */
|
|
||||||
.lists-index-item {
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid var(--divider-color);
|
|
||||||
background: var(--primary-background-color);
|
|
||||||
color: var(--body-text-color);
|
|
||||||
padding: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
font-family: inherit;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lists-index-item:hover {
|
|
||||||
background: var(--hover-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lists-index-item-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lists-index-name {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-weight: normal;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lists-index-count {
|
|
||||||
color: #666;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
margin: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Centered layout when back button is present */
|
|
||||||
.sidebar-header.has-back-btn {
|
|
||||||
position: relative;
|
|
||||||
justify-content: center; /* Center horizontally */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Absolute positioning for buttons in centered header */
|
|
||||||
.sidebar-header.has-back-btn .back-btn {
|
|
||||||
position: absolute;
|
|
||||||
left: 1rem;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header.has-back-btn .close-btn {
|
|
||||||
position: absolute;
|
|
||||||
right: 1rem;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Centralized Title text */
|
|
||||||
.sidebar-header-text-centered {
|
|
||||||
position: relative;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
max-width: 60%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header-icon-wrapper {
|
|
||||||
position: absolute;
|
|
||||||
right: 100%;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar Loading State */
|
|
||||||
.sidebar-loading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 4rem 1rem;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import Component from '@glimmer/component';
|
|||||||
import { pageTitle } from 'ember-page-title';
|
import { pageTitle } from 'ember-page-title';
|
||||||
import Map from '#components/map';
|
import Map from '#components/map';
|
||||||
import AppHeader from '#components/app-header';
|
import AppHeader from '#components/app-header';
|
||||||
|
import AppMenu from '#components/app-menu/index';
|
||||||
import Toast from '#components/toast';
|
import Toast from '#components/toast';
|
||||||
import { service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
|
import { or } from 'ember-truth-helpers';
|
||||||
import { on } from '@ember/modifier';
|
import { on } from '@ember/modifier';
|
||||||
|
|
||||||
export default class ApplicationComponent extends Component {
|
export default class ApplicationComponent extends Component {
|
||||||
@@ -12,17 +15,16 @@ export default class ApplicationComponent extends Component {
|
|||||||
@service mapUi;
|
@service mapUi;
|
||||||
@service router;
|
@service router;
|
||||||
|
|
||||||
|
@tracked isAppMenuOpen = false;
|
||||||
|
|
||||||
get isSidebarOpen() {
|
get isSidebarOpen() {
|
||||||
// We consider the sidebar "open" if we are in search, menu, lists or place routes AND it's visible.
|
// We consider the sidebar "open" if we are in search or place routes AND it's visible.
|
||||||
// This helps the map know if it should shift the center or adjust view.
|
// This helps the map know if it should shift the center or adjust view.
|
||||||
const name = this.router.currentRouteName;
|
|
||||||
return (
|
return (
|
||||||
this.mapUi.isSidebarVisible &&
|
this.mapUi.isSidebarVisible &&
|
||||||
(name === 'place' ||
|
(this.router.currentRouteName === 'place' ||
|
||||||
name === 'place.new' ||
|
this.router.currentRouteName === 'place.new' ||
|
||||||
name === 'search' ||
|
this.router.currentRouteName === 'search')
|
||||||
name === 'menu' ||
|
|
||||||
name.startsWith('lists'))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,35 +37,24 @@ export default class ApplicationComponent extends Component {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
toggleAppMenu() {
|
toggleAppMenu() {
|
||||||
if (this.router.currentRouteName === 'menu') {
|
this.isAppMenuOpen = !this.isAppMenuOpen;
|
||||||
this.router.transitionTo('index');
|
}
|
||||||
} else {
|
|
||||||
this.router.transitionTo('menu');
|
@action
|
||||||
}
|
closeAppMenu() {
|
||||||
|
this.isAppMenuOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleOutsideClick() {
|
handleOutsideClick() {
|
||||||
const name = this.router.currentRouteName;
|
if (this.isAppMenuOpen) {
|
||||||
if (
|
this.closeAppMenu();
|
||||||
name === 'search' ||
|
} else if (
|
||||||
name === 'place' ||
|
this.router.currentRouteName === 'search' ||
|
||||||
name === 'menu' ||
|
this.router.currentRouteName === 'place'
|
||||||
name.startsWith('lists')
|
|
||||||
) {
|
) {
|
||||||
this.mapUi.clearSelection();
|
this.mapUi.clearSelection();
|
||||||
this.mapUi.hideSidebar();
|
this.mapUi.hideSidebar();
|
||||||
if (name === 'menu' || name.startsWith('lists')) {
|
|
||||||
this.router.transitionTo('index');
|
|
||||||
} else if (name === 'place') {
|
|
||||||
if (this.mapUi.returnToSearch && this.mapUi.currentSearch) {
|
|
||||||
this.router.transitionTo('search', {
|
|
||||||
queryParams: this.mapUi.currentSearch,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.router.transitionTo('index');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,30 +66,32 @@ export default class ApplicationComponent extends Component {
|
|||||||
<template>
|
<template>
|
||||||
{{pageTitle "Marco"}}
|
{{pageTitle "Marco"}}
|
||||||
|
|
||||||
<div class={{if this.mapUi.isSidebarOpening "sidebar-opening"}}>
|
<AppHeader @onToggleMenu={{this.toggleAppMenu}} />
|
||||||
<AppHeader @onToggleMenu={{this.toggleAppMenu}} />
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="rs-widget-container"
|
||||||
|
class={{if this.storage.isWidgetOpen "visible"}}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{{#if this.storage.isWidgetOpen}}
|
||||||
<div
|
<div
|
||||||
id="rs-widget-container"
|
class="rs-backdrop"
|
||||||
class={{if this.storage.isWidgetOpen "visible"}}
|
role="button"
|
||||||
|
{{on "click" this.storage.closeWidget}}
|
||||||
></div>
|
></div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if this.storage.isWidgetOpen}}
|
<Map
|
||||||
<div
|
@isSidebarOpen={{or this.isSidebarOpen this.isAppMenuOpen}}
|
||||||
class="rs-backdrop"
|
@onOutsideClick={{this.handleOutsideClick}}
|
||||||
role="button"
|
/>
|
||||||
{{on "click" this.storage.closeWidget}}
|
|
||||||
></div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<Map
|
{{#if this.isAppMenuOpen}}
|
||||||
@isSidebarOpen={{this.isSidebarOpen}}
|
<AppMenu @onClose={{this.closeAppMenu}} />
|
||||||
@onOutsideClick={{this.handleOutsideClick}}
|
{{/if}}
|
||||||
/>
|
|
||||||
|
|
||||||
<Toast />
|
<Toast />
|
||||||
|
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<template>{{outlet}}</template>
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import Component from '@glimmer/component';
|
|
||||||
import { service } from '@ember/service';
|
|
||||||
import { action } from '@ember/object';
|
|
||||||
import { fn } from '@ember/helper';
|
|
||||||
import { on } from '@ember/modifier';
|
|
||||||
import Icon from '#components/icon';
|
|
||||||
import { htmlSafe } from '@ember/template';
|
|
||||||
|
|
||||||
export default class ListsIndexTemplate extends Component {
|
|
||||||
@service storage;
|
|
||||||
@service router;
|
|
||||||
@service mapUi;
|
|
||||||
|
|
||||||
styleFor(color) {
|
|
||||||
const finalColor =
|
|
||||||
color ||
|
|
||||||
getComputedStyle(document.documentElement)
|
|
||||||
.getPropertyValue('--default-list-color')
|
|
||||||
.trim();
|
|
||||||
return htmlSafe(`background-color: ${finalColor}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
selectList(listId) {
|
|
||||||
this.router.transitionTo('lists.list', listId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
close() {
|
|
||||||
this.router.transitionTo('index');
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
backToMenu() {
|
|
||||||
this.router.transitionTo('menu');
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
{{#if this.mapUi.isSidebarVisible}}
|
|
||||||
<div class="sidebar">
|
|
||||||
<div class="sidebar-header has-back-btn">
|
|
||||||
<button type="button" class="back-btn" {{on "click" this.backToMenu}}>
|
|
||||||
<Icon @name="arrow-left" @size={{20}} @color="#333" />
|
|
||||||
</button>
|
|
||||||
<h2 class="sidebar-header-text-centered">
|
|
||||||
<span class="sidebar-header-icon-wrapper">
|
|
||||||
<Icon @name="bookmark" @size={{20}} @color="#898989" />
|
|
||||||
</span>
|
|
||||||
Collections
|
|
||||||
</h2>
|
|
||||||
<button type="button" class="close-btn" {{on "click" this.close}}>
|
|
||||||
<Icon @name="x" @size={{20}} @color="#333" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-content">
|
|
||||||
<ul class="places-list">
|
|
||||||
{{#each this.storage.lists as |list|}}
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="lists-index-item"
|
|
||||||
{{on "click" (fn this.selectList list.id)}}
|
|
||||||
>
|
|
||||||
<div class="lists-index-item-left">
|
|
||||||
{{! template-lint-disable no-inline-styles }}
|
|
||||||
<span
|
|
||||||
class="list-color-dot"
|
|
||||||
style={{this.styleFor list.color}}
|
|
||||||
></span>
|
|
||||||
<div class="lists-index-name">{{list.title}}</div>
|
|
||||||
</div>
|
|
||||||
<div class="lists-index-count">
|
|
||||||
{{#if list.placeRefs.length}}
|
|
||||||
{{list.placeRefs.length}}
|
|
||||||
places
|
|
||||||
{{else}}
|
|
||||||
empty
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</template>
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import PlacesSidebar from '#components/places-sidebar';
|
|
||||||
|
|
||||||
<template>
|
|
||||||
{{#if @controller.mapUi.isSidebarVisible}}
|
|
||||||
<PlacesSidebar
|
|
||||||
@places={{@controller.places}}
|
|
||||||
@title={{@controller.listTitle}}
|
|
||||||
@color={{@controller.listColor}}
|
|
||||||
@scrollTop={{@controller.scrollTop}}
|
|
||||||
@isLoading={{@controller.loadPlacesTask.isRunning}}
|
|
||||||
@onSelect={{@controller.selectPlace}}
|
|
||||||
@onClose={{@controller.close}}
|
|
||||||
@onBack={{@controller.backToLists}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</template>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import Component from '@glimmer/component';
|
|
||||||
import AppMenu from '#components/app-menu/index';
|
|
||||||
import { service } from '@ember/service';
|
|
||||||
import { action } from '@ember/object';
|
|
||||||
|
|
||||||
export default class MenuTemplate extends Component {
|
|
||||||
@service router;
|
|
||||||
|
|
||||||
@action
|
|
||||||
close() {
|
|
||||||
this.router.transitionTo('index');
|
|
||||||
}
|
|
||||||
|
|
||||||
<template><AppMenu @onClose={{this.close}} /></template>
|
|
||||||
}
|
|
||||||
+1
-14
@@ -77,14 +77,8 @@ export default class PlaceTemplate extends Component {
|
|||||||
navigateBack(place) {
|
navigateBack(place) {
|
||||||
// The sidebar calls this with null when "Back" is clicked.
|
// The sidebar calls this with null when "Back" is clicked.
|
||||||
if (place === null) {
|
if (place === null) {
|
||||||
// If we have an active route context (e.g. lists), return to it
|
|
||||||
if (this.mapUi.returnToRoute) {
|
|
||||||
this.mapUi.showSidebar();
|
|
||||||
const { name, model } = this.mapUi.returnToRoute;
|
|
||||||
this.router.transitionTo(name, model);
|
|
||||||
}
|
|
||||||
// If we have an active search context, return to it (UP navigation)
|
// If we have an active search context, return to it (UP navigation)
|
||||||
else if (this.mapUi.returnToSearch && this.mapUi.currentSearch) {
|
if (this.mapUi.returnToSearch && this.mapUi.currentSearch) {
|
||||||
this.mapUi.showSidebar();
|
this.mapUi.showSidebar();
|
||||||
this.router.transitionTo('search', {
|
this.router.transitionTo('search', {
|
||||||
queryParams: this.mapUi.currentSearch,
|
queryParams: this.mapUi.currentSearch,
|
||||||
@@ -104,13 +98,6 @@ export default class PlaceTemplate extends Component {
|
|||||||
close() {
|
close() {
|
||||||
this.mapUi.clearSelection();
|
this.mapUi.clearSelection();
|
||||||
this.mapUi.hideSidebar();
|
this.mapUi.hideSidebar();
|
||||||
if (this.mapUi.returnToSearch && this.mapUi.currentSearch) {
|
|
||||||
this.router.transitionTo('search', {
|
|
||||||
queryParams: this.mapUi.currentSearch,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.router.transitionTo('index');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ export default class SearchTemplate extends Component {
|
|||||||
@action
|
@action
|
||||||
selectPlace(place) {
|
selectPlace(place) {
|
||||||
if (place) {
|
if (place) {
|
||||||
const sidebarContent = document.querySelector('.sidebar-content');
|
|
||||||
if (sidebarContent) {
|
|
||||||
this.mapUi.saveScrollPosition('search', sidebarContent.scrollTop);
|
|
||||||
}
|
|
||||||
this.mapUi.returnToSearch = true;
|
this.mapUi.returnToSearch = true;
|
||||||
this.mapUi.showSidebar();
|
this.mapUi.showSidebar();
|
||||||
this.mapUi.preventNextZoom = true;
|
this.mapUi.preventNextZoom = true;
|
||||||
@@ -32,7 +28,6 @@ export default class SearchTemplate extends Component {
|
|||||||
{{#if this.mapUi.isSidebarVisible}}
|
{{#if this.mapUi.isSidebarVisible}}
|
||||||
<PlacesSidebar
|
<PlacesSidebar
|
||||||
@places={{this.mapUi.searchResults}}
|
@places={{this.mapUi.searchResults}}
|
||||||
@scrollTop={{this.mapUi.getScrollPosition "search"}}
|
|
||||||
@onSelect={{this.selectPlace}}
|
@onSelect={{this.selectPlace}}
|
||||||
@onClose={{this.close}}
|
@onClose={{this.close}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
// AGENT: Keep imports sorted alphabetically, grouped by feather-icons → pinhead → custom/local
|
// AGENT: Keep imports sorted alphabetically, grouped by feather-icons → pinhead → custom/local
|
||||||
/*
|
|
||||||
* Feather icons
|
|
||||||
*/
|
|
||||||
import activity from 'feather-icons/dist/icons/activity.svg?raw';
|
import activity from 'feather-icons/dist/icons/activity.svg?raw';
|
||||||
import arrowLeft from 'feather-icons/dist/icons/arrow-left.svg?raw';
|
import arrowLeft from 'feather-icons/dist/icons/arrow-left.svg?raw';
|
||||||
import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw';
|
import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw';
|
||||||
@@ -43,9 +40,6 @@ import check from 'feather-icons/dist/icons/check.svg?raw';
|
|||||||
import alertCircle from 'feather-icons/dist/icons/alert-circle.svg?raw';
|
import alertCircle from 'feather-icons/dist/icons/alert-circle.svg?raw';
|
||||||
import zap from 'feather-icons/dist/icons/zap.svg?raw';
|
import zap from 'feather-icons/dist/icons/zap.svg?raw';
|
||||||
|
|
||||||
/*
|
|
||||||
* Pinhead icons
|
|
||||||
*/
|
|
||||||
import angelfish from '@waysidemapping/pinhead/dist/icons/angelfish.svg?raw';
|
import angelfish from '@waysidemapping/pinhead/dist/icons/angelfish.svg?raw';
|
||||||
import barbell from '@waysidemapping/pinhead/dist/icons/barbell.svg?raw';
|
import barbell from '@waysidemapping/pinhead/dist/icons/barbell.svg?raw';
|
||||||
import climbingWall from '@waysidemapping/pinhead/dist/icons/climbing_wall.svg?raw';
|
import climbingWall from '@waysidemapping/pinhead/dist/icons/climbing_wall.svg?raw';
|
||||||
@@ -59,12 +53,8 @@ import bus from '@waysidemapping/pinhead/dist/icons/bus.svg?raw';
|
|||||||
import camera from '@waysidemapping/pinhead/dist/icons/camera.svg?raw';
|
import camera from '@waysidemapping/pinhead/dist/icons/camera.svg?raw';
|
||||||
import boxingGloveUp from '@waysidemapping/pinhead/dist/icons/boxing_glove_up.svg?raw';
|
import boxingGloveUp from '@waysidemapping/pinhead/dist/icons/boxing_glove_up.svg?raw';
|
||||||
import car from '@waysidemapping/pinhead/dist/icons/car.svg?raw';
|
import car from '@waysidemapping/pinhead/dist/icons/car.svg?raw';
|
||||||
import carAndWrench from '@waysidemapping/pinhead/dist/icons/car_and_wrench.svg?raw';
|
|
||||||
import castleKeep from '@waysidemapping/pinhead/dist/icons/castle_keep.svg?raw';
|
|
||||||
import cigaretteWithSmokeCurl from '@waysidemapping/pinhead/dist/icons/cigarette_with_smoke_curl.svg?raw';
|
import cigaretteWithSmokeCurl from '@waysidemapping/pinhead/dist/icons/cigarette_with_smoke_curl.svg?raw';
|
||||||
import cityGate from '@waysidemapping/pinhead/dist/icons/city_gate.svg?raw';
|
|
||||||
import classicalBuilding from '@waysidemapping/pinhead/dist/icons/classical_building.svg?raw';
|
import classicalBuilding from '@waysidemapping/pinhead/dist/icons/classical_building.svg?raw';
|
||||||
import classicalBuildingWithClock from '@waysidemapping/pinhead/dist/icons/classical_building_with_clock.svg?raw';
|
|
||||||
import classicalBuildingWithDomeAndFlag from '@waysidemapping/pinhead/dist/icons/classical_building_with_dome_and_flag.svg?raw';
|
import classicalBuildingWithDomeAndFlag from '@waysidemapping/pinhead/dist/icons/classical_building_with_dome_and_flag.svg?raw';
|
||||||
import classicalBuildingWithFlag from '@waysidemapping/pinhead/dist/icons/classical_building_with_flag.svg?raw';
|
import classicalBuildingWithFlag from '@waysidemapping/pinhead/dist/icons/classical_building_with_flag.svg?raw';
|
||||||
import commercialBuilding from '@waysidemapping/pinhead/dist/icons/commercial_building.svg?raw';
|
import commercialBuilding from '@waysidemapping/pinhead/dist/icons/commercial_building.svg?raw';
|
||||||
@@ -92,13 +82,8 @@ import grecianVase from '@waysidemapping/pinhead/dist/icons/grecian_vase.svg?raw
|
|||||||
import greekCross from '@waysidemapping/pinhead/dist/icons/greek_cross.svg?raw';
|
import greekCross from '@waysidemapping/pinhead/dist/icons/greek_cross.svg?raw';
|
||||||
import iceCreamOnCone from '@waysidemapping/pinhead/dist/icons/ice_cream_on_cone.svg?raw';
|
import iceCreamOnCone from '@waysidemapping/pinhead/dist/icons/ice_cream_on_cone.svg?raw';
|
||||||
import industrialBuilding from '@waysidemapping/pinhead/dist/icons/industrial_building.svg?raw';
|
import industrialBuilding from '@waysidemapping/pinhead/dist/icons/industrial_building.svg?raw';
|
||||||
import infoI from '@waysidemapping/pinhead/dist/icons/info_i.svg?raw';
|
|
||||||
import jewel from '@waysidemapping/pinhead/dist/icons/jewel.svg?raw';
|
import jewel from '@waysidemapping/pinhead/dist/icons/jewel.svg?raw';
|
||||||
import lowriseBuilding from '@waysidemapping/pinhead/dist/icons/lowrise_building.svg?raw';
|
import lowriseBuilding from '@waysidemapping/pinhead/dist/icons/lowrise_building.svg?raw';
|
||||||
import marketStall from '@waysidemapping/pinhead/dist/icons/market_stall.svg?raw';
|
|
||||||
import memorialStoneWithInscription from '@waysidemapping/pinhead/dist/icons/memorial_stone_with_inscription.svg?raw';
|
|
||||||
import mobilePhoneWithKeypadAndAntenna from '@waysidemapping/pinhead/dist/icons/mobile_phone_with_keypad_and_antenna.svg?raw';
|
|
||||||
import molarTooth from '@waysidemapping/pinhead/dist/icons/molar_tooth.svg?raw';
|
|
||||||
import needleAndSpoolOfThread from '@waysidemapping/pinhead/dist/icons/needle_and_spool_of_thread.svg?raw';
|
import needleAndSpoolOfThread from '@waysidemapping/pinhead/dist/icons/needle_and_spool_of_thread.svg?raw';
|
||||||
import openBook from '@waysidemapping/pinhead/dist/icons/open_book.svg?raw';
|
import openBook from '@waysidemapping/pinhead/dist/icons/open_book.svg?raw';
|
||||||
import palace from '@waysidemapping/pinhead/dist/icons/palace.svg?raw';
|
import palace from '@waysidemapping/pinhead/dist/icons/palace.svg?raw';
|
||||||
@@ -133,9 +118,6 @@ import wallHangingWithMountainsAndSun from '@waysidemapping/pinhead/dist/icons/w
|
|||||||
import windingWayWide from '@waysidemapping/pinhead/dist/icons/winding_way_wide.svg?raw';
|
import windingWayWide from '@waysidemapping/pinhead/dist/icons/winding_way_wide.svg?raw';
|
||||||
import womensAndMensRestroomSymbol from '@waysidemapping/pinhead/dist/icons/womens_and_mens_restroom_symbol.svg?raw';
|
import womensAndMensRestroomSymbol from '@waysidemapping/pinhead/dist/icons/womens_and_mens_restroom_symbol.svg?raw';
|
||||||
|
|
||||||
/*
|
|
||||||
* Custom/local icons
|
|
||||||
*/
|
|
||||||
import loadingRing from '../icons/270-ring.svg?raw';
|
import loadingRing from '../icons/270-ring.svg?raw';
|
||||||
import nostrich from '../icons/nostrich-2.svg?raw';
|
import nostrich from '../icons/nostrich-2.svg?raw';
|
||||||
import remotestorage from '../icons/remotestorage.svg?raw';
|
import remotestorage from '../icons/remotestorage.svg?raw';
|
||||||
@@ -162,13 +144,11 @@ const ICONS = {
|
|||||||
'chevron-left': chevronLeft,
|
'chevron-left': chevronLeft,
|
||||||
'chevron-right': chevronRight,
|
'chevron-right': chevronRight,
|
||||||
'cigarette-with-smoke-curl': cigaretteWithSmokeCurl,
|
'cigarette-with-smoke-curl': cigaretteWithSmokeCurl,
|
||||||
'city-gate': cityGate,
|
|
||||||
climbing_wall: climbingWall,
|
climbing_wall: climbingWall,
|
||||||
check,
|
check,
|
||||||
'alert-circle': alertCircle,
|
'alert-circle': alertCircle,
|
||||||
'alert-triangle': alertTriangle,
|
'alert-triangle': alertTriangle,
|
||||||
'classical-building': classicalBuilding,
|
'classical-building': classicalBuilding,
|
||||||
'classical-building-with-clock': classicalBuildingWithClock,
|
|
||||||
'classical-building-with-dome-and-flag': classicalBuildingWithDomeAndFlag,
|
'classical-building-with-dome-and-flag': classicalBuildingWithDomeAndFlag,
|
||||||
'classical-building-with-flag': classicalBuildingWithFlag,
|
'classical-building-with-flag': classicalBuildingWithFlag,
|
||||||
'commercial-building': commercialBuilding,
|
'commercial-building': commercialBuilding,
|
||||||
@@ -205,7 +185,6 @@ const ICONS = {
|
|||||||
'ice-cream-on-cone': iceCreamOnCone,
|
'ice-cream-on-cone': iceCreamOnCone,
|
||||||
'industrial-building': industrialBuilding,
|
'industrial-building': industrialBuilding,
|
||||||
info,
|
info,
|
||||||
'info-i': infoI,
|
|
||||||
instagram,
|
instagram,
|
||||||
jewel,
|
jewel,
|
||||||
'log-in': logIn,
|
'log-in': logIn,
|
||||||
@@ -214,11 +193,7 @@ const ICONS = {
|
|||||||
mail,
|
mail,
|
||||||
map,
|
map,
|
||||||
'map-pin': mapPin,
|
'map-pin': mapPin,
|
||||||
'market-stall': marketStall,
|
|
||||||
'memorial-stone-with-inscription': memorialStoneWithInscription,
|
|
||||||
menu,
|
menu,
|
||||||
'mobile-phone-with-keypad-and-antenna': mobilePhoneWithKeypadAndAntenna,
|
|
||||||
'molar-tooth': molarTooth,
|
|
||||||
'more-horizontal': moreHorizontal,
|
'more-horizontal': moreHorizontal,
|
||||||
'more-vertical': moreVertical,
|
'more-vertical': moreVertical,
|
||||||
navigation,
|
navigation,
|
||||||
@@ -270,8 +245,6 @@ const ICONS = {
|
|||||||
winding_way_wide: windingWayWide,
|
winding_way_wide: windingWayWide,
|
||||||
parking_p: parkingP,
|
parking_p: parkingP,
|
||||||
car,
|
car,
|
||||||
'car-and-wrench': carAndWrench,
|
|
||||||
'castle-keep': castleKeep,
|
|
||||||
x,
|
x,
|
||||||
zap,
|
zap,
|
||||||
'loading-ring': loadingRing,
|
'loading-ring': loadingRing,
|
||||||
|
|||||||
@@ -14,30 +14,6 @@ export function normalizeRelayUrl(url) {
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uniqNormalizedRelays(relays = []) {
|
|
||||||
return Array.from(new Set(relays.map(normalizeRelayUrl).filter(Boolean)));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mergeRequiredRelays(requiredRelays = [], customRelays = []) {
|
|
||||||
const requiredSet = new Set(requiredRelays.filter(Boolean));
|
|
||||||
const merged = [...requiredRelays.filter(Boolean)];
|
|
||||||
|
|
||||||
for (const relay of uniqNormalizedRelays(customRelays)) {
|
|
||||||
if (!requiredSet.has(relay)) {
|
|
||||||
merged.push(relay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function excludeRequiredRelays(customRelays = [], requiredRelays = []) {
|
|
||||||
const requiredSet = new Set(requiredRelays.filter(Boolean));
|
|
||||||
return uniqNormalizedRelays(customRelays).filter((relay) => {
|
|
||||||
return !requiredSet.has(relay);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts and normalizes photo data from NIP-360 (Place Photos) events.
|
* Extracts and normalizes photo data from NIP-360 (Place Photos) events.
|
||||||
* Sorts chronologically and guarantees the first landscape photo (or first portrait) is at index 0.
|
* Sorts chronologically and guarantees the first landscape photo (or first portrait) is at index 0.
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ export const POI_ICON_RULES = [
|
|||||||
|
|
||||||
{ tags: { amenity: 'bank' }, icon: 'banknote' },
|
{ tags: { amenity: 'bank' }, icon: 'banknote' },
|
||||||
{ tags: { amenity: 'place_of_worship' }, icon: 'place-of-worship-building' },
|
{ tags: { amenity: 'place_of_worship' }, icon: 'place-of-worship-building' },
|
||||||
{ tags: { amenity: 'townhall' }, icon: 'classical-building-with-clock' },
|
|
||||||
{ tags: { building: 'townhall' }, icon: 'classical-building-with-clock' },
|
|
||||||
{ tags: { amenity: 'fire_station' }, icon: 'badge-shield-with-fire' },
|
{ tags: { amenity: 'fire_station' }, icon: 'badge-shield-with-fire' },
|
||||||
{ tags: { amenity: 'police' }, icon: 'police-officer-with-stop-arm' },
|
{ tags: { amenity: 'police' }, icon: 'police-officer-with-stop-arm' },
|
||||||
{ tags: { amenity: 'toilets' }, icon: 'womens-and-mens-restroom-symbol' },
|
{ tags: { amenity: 'toilets' }, icon: 'womens-and-mens-restroom-symbol' },
|
||||||
@@ -74,7 +72,6 @@ export const POI_ICON_RULES = [
|
|||||||
tags: { shop: 'beauty' },
|
tags: { shop: 'beauty' },
|
||||||
icon: 'fancy-mirror-with-reflection-and-stars',
|
icon: 'fancy-mirror-with-reflection-and-stars',
|
||||||
},
|
},
|
||||||
{ tags: { shop: 'car_repair' }, icon: 'car-and-wrench' },
|
|
||||||
{ tags: { craft: 'tailor' }, icon: 'needle-and-spool-of-thread' },
|
{ tags: { craft: 'tailor' }, icon: 'needle-and-spool-of-thread' },
|
||||||
{ tags: { office: 'estate_agent' }, icon: 'village-buildings' },
|
{ tags: { office: 'estate_agent' }, icon: 'village-buildings' },
|
||||||
{ tags: { office: true }, icon: 'commercial-building' },
|
{ tags: { office: true }, icon: 'commercial-building' },
|
||||||
@@ -106,7 +103,6 @@ export const POI_ICON_RULES = [
|
|||||||
{ tags: { tourism: 'viewpoint' }, icon: 'camera' },
|
{ tags: { tourism: 'viewpoint' }, icon: 'camera' },
|
||||||
{ tags: { tourism: 'zoo' }, icon: 'camera' },
|
{ tags: { tourism: 'zoo' }, icon: 'camera' },
|
||||||
{ tags: { tourism: 'artwork' }, icon: 'camera' },
|
{ tags: { tourism: 'artwork' }, icon: 'camera' },
|
||||||
{ tags: { tourism: 'information' }, icon: 'info-i' },
|
|
||||||
{ tags: { amenity: 'cinema' }, icon: 'film' },
|
{ tags: { amenity: 'cinema' }, icon: 'film' },
|
||||||
{ tags: { amenity: 'theatre' }, icon: 'camera' },
|
{ tags: { amenity: 'theatre' }, icon: 'camera' },
|
||||||
{ tags: { amenity: 'arts_centre' }, icon: 'comedy-mask-and-tragedy-mask' },
|
{ tags: { amenity: 'arts_centre' }, icon: 'comedy-mask-and-tragedy-mask' },
|
||||||
@@ -117,9 +113,7 @@ export const POI_ICON_RULES = [
|
|||||||
{ tags: { historic: 'bridge' }, icon: 'bridge' },
|
{ tags: { historic: 'bridge' }, icon: 'bridge' },
|
||||||
{ tags: { historic: 'bridge_site' }, icon: 'bridge' },
|
{ tags: { historic: 'bridge_site' }, icon: 'bridge' },
|
||||||
{ tags: { historic: 'fort' }, icon: 'fort' },
|
{ tags: { historic: 'fort' }, icon: 'fort' },
|
||||||
{ tags: { historic: 'city_gate' }, icon: 'city-gate' },
|
|
||||||
{ tags: { historic: 'castle' }, icon: 'palace' },
|
{ tags: { historic: 'castle' }, icon: 'palace' },
|
||||||
{ tags: { building: 'tower', historic: 'yes' }, icon: 'castle-keep' },
|
|
||||||
{ tags: { historic: 'building' }, icon: 'classical-building-with-flag' },
|
{ tags: { historic: 'building' }, icon: 'classical-building-with-flag' },
|
||||||
{ tags: { historic: 'archaeological_site' }, icon: 'grecian-vase' },
|
{ tags: { historic: 'archaeological_site' }, icon: 'grecian-vase' },
|
||||||
{ tags: { historic: 'memorial' }, icon: 'memorial-stone-with-inscription' },
|
{ tags: { historic: 'memorial' }, icon: 'memorial-stone-with-inscription' },
|
||||||
|
|||||||
+1
-10
@@ -56,24 +56,15 @@ const PLACE_TYPE_KEYS = [
|
|||||||
export function getPlaceType(tags) {
|
export function getPlaceType(tags) {
|
||||||
if (!tags) return null;
|
if (!tags) return null;
|
||||||
|
|
||||||
let fallbackKey = null;
|
|
||||||
|
|
||||||
for (const key of PLACE_TYPE_KEYS) {
|
for (const key of PLACE_TYPE_KEYS) {
|
||||||
const value = tags[key];
|
const value = tags[key];
|
||||||
if (value) {
|
if (value) {
|
||||||
if (value === 'yes') {
|
if (value === 'yes') {
|
||||||
if (!fallbackKey) {
|
return humanizeOsmTag(key);
|
||||||
fallbackKey = key;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
return humanizeOsmTag(value);
|
return humanizeOsmTag(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fallbackKey) {
|
|
||||||
return humanizeOsmTag(fallbackKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const POI_CATEGORIES = [
|
|||||||
label: 'Things to do',
|
label: 'Things to do',
|
||||||
icon: 'feather-camera',
|
icon: 'feather-camera',
|
||||||
filter: [
|
filter: [
|
||||||
'["tourism"~"^(museum|gallery|attraction|viewpoint|zoo|theme_park|aquarium|artwork|information)$"]',
|
'["tourism"~"^(museum|gallery|attraction|viewpoint|zoo|theme_park|aquarium|artwork)$"]',
|
||||||
'["amenity"~"^(cinema|theatre|arts_centre|planetarium)$"]',
|
'["amenity"~"^(cinema|theatre|arts_centre|planetarium)$"]',
|
||||||
'["leisure"~"^(sports_centre|stadium|water_park)$"]',
|
'["leisure"~"^(sports_centre|stadium|water_park)$"]',
|
||||||
'["historic"]',
|
'["historic"]',
|
||||||
@@ -55,7 +55,7 @@ export const POI_CATEGORIES = [
|
|||||||
id: 'accommodation',
|
id: 'accommodation',
|
||||||
label: 'Hotels',
|
label: 'Hotels',
|
||||||
icon: 'person-sleeping-in-bed',
|
icon: 'person-sleeping-in-bed',
|
||||||
filter: ['["tourism"~"^(hotel|hostel|motel|chalet|guest_house)$"]'],
|
filter: ['["tourism"~"^(hotel|hostel|motel|chalet)$"]'],
|
||||||
types: ['node', 'way', 'relation'],
|
types: ['node', 'way', 'relation'],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
+1
-1
@@ -52,7 +52,7 @@
|
|||||||
"@embroider/vite": "^1.5.0",
|
"@embroider/vite": "^1.5.0",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@glimmer/component": "^2.0.0",
|
"@glimmer/component": "^2.0.0",
|
||||||
"@remotestorage/module-places": "~1.3.0",
|
"@remotestorage/module-places": "~1.2.1",
|
||||||
"@rollup/plugin-babel": "^6.1.0",
|
"@rollup/plugin-babel": "^6.1.0",
|
||||||
"@warp-drive/core": "~5.8.0",
|
"@warp-drive/core": "~5.8.0",
|
||||||
"@warp-drive/ember": "~5.8.0",
|
"@warp-drive/ember": "~5.8.0",
|
||||||
|
|||||||
Generated
+5
-5
@@ -88,8 +88,8 @@ importers:
|
|||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
'@remotestorage/module-places':
|
'@remotestorage/module-places':
|
||||||
specifier: ~1.3.0
|
specifier: ~1.2.1
|
||||||
version: 1.3.0
|
version: 1.2.1
|
||||||
'@rollup/plugin-babel':
|
'@rollup/plugin-babel':
|
||||||
specifier: ^6.1.0
|
specifier: ^6.1.0
|
||||||
version: 6.1.0(@babel/core@7.28.6)(rollup@4.55.1)
|
version: 6.1.0(@babel/core@7.28.6)(rollup@4.55.1)
|
||||||
@@ -1468,8 +1468,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-4rdu8GPY9TeQwsYp5D2My74dC3dSVS3tghAvisG80ybK4lqa0gvlrglaSTBxogJbxqHRw/NjI/liEtb3+SD+Bw==}
|
resolution: {integrity: sha512-4rdu8GPY9TeQwsYp5D2My74dC3dSVS3tghAvisG80ybK4lqa0gvlrglaSTBxogJbxqHRw/NjI/liEtb3+SD+Bw==}
|
||||||
engines: {node: '>=18.12'}
|
engines: {node: '>=18.12'}
|
||||||
|
|
||||||
'@remotestorage/module-places@1.3.0':
|
'@remotestorage/module-places@1.2.1':
|
||||||
resolution: {integrity: sha512-VM0CqkIP6IBEpjqJ2DyTrGDOQXc73aXoAFDLIoME5Lo033uTitgn+qKgTGPK/lD4H92mk1+D3W88ECtf9v3mWw==}
|
resolution: {integrity: sha512-hNRuhGoG8RS+cieVvDVzXWBEuNPfyeFirhgNH3z1WoKw9ngHdPY6V0sT0vKbsxB8xaODReZfo2ZKHLTmdFunlw==}
|
||||||
|
|
||||||
'@rollup/plugin-babel@6.1.0':
|
'@rollup/plugin-babel@6.1.0':
|
||||||
resolution: {integrity: sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==}
|
resolution: {integrity: sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==}
|
||||||
@@ -7439,7 +7439,7 @@ snapshots:
|
|||||||
'@pnpm/error': 1000.0.5
|
'@pnpm/error': 1000.0.5
|
||||||
find-up: 5.0.0
|
find-up: 5.0.0
|
||||||
|
|
||||||
'@remotestorage/module-places@1.3.0':
|
'@remotestorage/module-places@1.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
latlon-geohash: 2.0.0
|
latlon-geohash: 2.0.0
|
||||||
ulid: 3.0.2
|
ulid: 3.0.2
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
import { module, test } from 'qunit';
|
|
||||||
import { visit, currentURL, click, waitFor } from '@ember/test-helpers';
|
|
||||||
import { setupApplicationTest } from 'marco/tests/helpers';
|
|
||||||
import Service from '@ember/service';
|
|
||||||
|
|
||||||
class MockOsmService extends Service {
|
|
||||||
async fetchOsmObject() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MockStorageService extends Service {
|
|
||||||
initialSyncDone = true;
|
|
||||||
savedPlaces = [
|
|
||||||
{
|
|
||||||
id: 'place-123',
|
|
||||||
title: 'Mountain Trail',
|
|
||||||
geohash: 'u33dc0',
|
|
||||||
osmTags: { name: 'Mountain Trail' },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
lists = [
|
|
||||||
{
|
|
||||||
id: 'to-go',
|
|
||||||
title: 'Want to go',
|
|
||||||
color: '#2e9e4f',
|
|
||||||
placeRefs: [{ id: 'place-123', geohash: 'u33dc0' }],
|
|
||||||
},
|
|
||||||
{ id: 'to-do', title: 'To do', color: '#2a7fff', placeRefs: [] },
|
|
||||||
];
|
|
||||||
|
|
||||||
findPlaceById(id) {
|
|
||||||
if (id === 'place-123') {
|
|
||||||
return this.savedPlaces[0];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
isPlaceSaved() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPlacesInBounds() {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
getPlacesInList(listId) {
|
|
||||||
if (listId === 'to-go') {
|
|
||||||
return Promise.resolve([this.savedPlaces[0]]);
|
|
||||||
}
|
|
||||||
return Promise.resolve([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
rs = {
|
|
||||||
on: () => {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module('Acceptance | collections navigation', function (hooks) {
|
|
||||||
setupApplicationTest(hooks);
|
|
||||||
|
|
||||||
hooks.beforeEach(function () {
|
|
||||||
this.owner.register('service:osm', MockOsmService);
|
|
||||||
this.owner.register('service:storage', MockStorageService);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('navigating through the collections menu hierarchy, viewing list places, and going back', async function (assert) {
|
|
||||||
// 1. Visit Home Map
|
|
||||||
await visit('/');
|
|
||||||
assert.strictEqual(currentURL(), '/');
|
|
||||||
|
|
||||||
// 2. Open the App Menu overlay
|
|
||||||
await click('.menu-btn-integrated');
|
|
||||||
assert.dom('.sidebar.app-menu-pane').exists('App menu sidebar is open');
|
|
||||||
assert
|
|
||||||
.dom('.app-menu')
|
|
||||||
.includesText('Collections', 'Menu contains Collections link');
|
|
||||||
|
|
||||||
// 3. Transition to Collections Index (List of lists)
|
|
||||||
await click(document.querySelectorAll('.app-menu button')[0]); // Click "Collections"
|
|
||||||
assert.strictEqual(currentURL(), '/lists', 'Transitions to /lists index');
|
|
||||||
assert
|
|
||||||
.dom('.sidebar-header-text-centered')
|
|
||||||
.includesText('Collections', 'Header is centered and titled Collections');
|
|
||||||
assert
|
|
||||||
.dom('.lists-index-item')
|
|
||||||
.exists({ count: 2 }, 'Renders our 2 mocked list items');
|
|
||||||
|
|
||||||
// 4. Transition to a specific list (Want to go)
|
|
||||||
await click(document.querySelectorAll('.lists-index-item')[0]); // Click "Want to go"
|
|
||||||
assert.strictEqual(
|
|
||||||
currentURL(),
|
|
||||||
'/lists/to-go',
|
|
||||||
'Transitions instantly to /lists/to-go'
|
|
||||||
);
|
|
||||||
|
|
||||||
// 5. Verify background loading spinner shows up, then results populate
|
|
||||||
await waitFor('.places-list');
|
|
||||||
assert
|
|
||||||
.dom('.places-list .place-name')
|
|
||||||
.hasText('Mountain Trail', 'Renders the saved place from the list');
|
|
||||||
assert
|
|
||||||
.dom('.places-list .place-type')
|
|
||||||
.hasText('Saved place', 'Place type displays Saved place correctly');
|
|
||||||
|
|
||||||
// 6. Click back button in collection list header
|
|
||||||
await click('.sidebar-header .back-btn');
|
|
||||||
assert.strictEqual(currentURL(), '/lists', 'Goes back to /lists index');
|
|
||||||
|
|
||||||
// 7. Click back button in collections index header
|
|
||||||
await click('.sidebar-header .back-btn');
|
|
||||||
assert.strictEqual(currentURL(), '/menu', 'Goes back to main menu route');
|
|
||||||
|
|
||||||
// 8. Close sidebar
|
|
||||||
await click('.sidebar-header .close-btn');
|
|
||||||
assert.strictEqual(currentURL(), '/', 'Sidebar closed and returned home');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('clicking a place inside a collection sets returnToRoute and returns gracefully on back click', async function (assert) {
|
|
||||||
await visit('/lists/to-go');
|
|
||||||
await waitFor('.places-list');
|
|
||||||
|
|
||||||
// Click on the place item to view details
|
|
||||||
await click('.place-item');
|
|
||||||
assert.ok(
|
|
||||||
currentURL().includes('/place/place-123'),
|
|
||||||
'Transitions to place details route'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Click back from place details
|
|
||||||
await click('.back-btn');
|
|
||||||
assert.strictEqual(
|
|
||||||
currentURL(),
|
|
||||||
'/lists/to-go',
|
|
||||||
'Returns gracefully back to lists/to-go list view'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -101,7 +101,7 @@ module('Acceptance | map search reset', function (hooks) {
|
|||||||
'Should have stayed on the search route with markers intact'
|
'Should have stayed on the search route with markers intact'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Second Click (Clear search and markers)
|
// Second Click (Start new search)
|
||||||
// Click slightly differently to ensure fresh event
|
// Click slightly differently to ensure fresh event
|
||||||
await triggerEvent(canvas, 'pointerdown', {
|
await triggerEvent(canvas, 'pointerdown', {
|
||||||
clientX: 250,
|
clientX: 250,
|
||||||
@@ -125,6 +125,11 @@ module('Acceptance | map search reset', function (hooks) {
|
|||||||
// 3. Wait for transition
|
// 3. Wait for transition
|
||||||
await new Promise((r) => setTimeout(r, 1000));
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
|
|
||||||
assert.strictEqual(currentURL(), '/', 'Should have transitioned to index');
|
const newUrl = currentURL();
|
||||||
|
assert.notOk(
|
||||||
|
newUrl.includes('category=coffee'),
|
||||||
|
`New URL ${newUrl} should not contain category param`
|
||||||
|
);
|
||||||
|
assert.ok(newUrl.includes('/search'), 'Should be on search route');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ module('Acceptance | navigation', function (hooks) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('closing the sidebar transitions back to search route when opened from search results', async function (assert) {
|
test('closing the sidebar resets the returnToSearch flag', async function (assert) {
|
||||||
const mapUi = this.owner.lookup('service:map-ui');
|
const mapUi = this.owner.lookup('service:map-ui');
|
||||||
|
|
||||||
await visit('/search?lat=1&lon=1');
|
await visit('/search?lat=1&lon=1');
|
||||||
@@ -97,22 +97,7 @@ module('Acceptance | navigation', function (hooks) {
|
|||||||
await click('.close-btn');
|
await click('.close-btn');
|
||||||
|
|
||||||
assert.dom('.sidebar').doesNotExist('Sidebar should be closed');
|
assert.dom('.sidebar').doesNotExist('Sidebar should be closed');
|
||||||
assert.strictEqual(
|
assert.ok(currentURL().includes('/place/'), 'Remains on place route');
|
||||||
currentURL(),
|
|
||||||
'/search?lat=1&lon=1',
|
|
||||||
'Should transition back to search route'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('closing the sidebar when visiting a place directly transitions to index', async function (assert) {
|
|
||||||
await visit('/place/osm:node:123');
|
|
||||||
assert.ok(currentURL().includes('/place/'), 'Visited place directly');
|
|
||||||
|
|
||||||
// Click the Close (X) button
|
|
||||||
await click('.close-btn');
|
|
||||||
|
|
||||||
assert.dom('.sidebar').doesNotExist('Sidebar should be closed');
|
|
||||||
assert.strictEqual(currentURL(), '/', 'Should transition back to index');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('navigating directly to place and back closes sidebar', async function (assert) {
|
test('navigating directly to place and back closes sidebar', async function (assert) {
|
||||||
|
|||||||
@@ -169,77 +169,4 @@ module('Acceptance | search loading', function (hooks) {
|
|||||||
// Verify we are back on index (or at least query is gone)
|
// Verify we are back on index (or at least query is gone)
|
||||||
assert.strictEqual(currentURL(), '/', 'Navigated to index');
|
assert.strictEqual(currentURL(), '/', 'Navigated to index');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('quick search pills visibility during category search transition', async function (assert) {
|
|
||||||
const mapUi = this.owner.lookup('service:map-ui');
|
|
||||||
mapUi.currentZoom = 15;
|
|
||||||
|
|
||||||
// Seed localStorage with a high zoom level to ensure quick search buttons show
|
|
||||||
const highZoomState = {
|
|
||||||
center: [13.4, 52.5],
|
|
||||||
zoom: 18,
|
|
||||||
};
|
|
||||||
window.localStorage.setItem(
|
|
||||||
'marco:map-view',
|
|
||||||
JSON.stringify(highZoomState)
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Make sure quick search buttons setting is enabled
|
|
||||||
const settings = this.owner.lookup('service:settings');
|
|
||||||
settings.showQuickSearchButtons = true;
|
|
||||||
|
|
||||||
// 1. Visit slowly loading category search
|
|
||||||
const catPromise = visit('/search?category=slow_category&lat=1&lon=1');
|
|
||||||
await new Promise((r) => setTimeout(r, 10));
|
|
||||||
|
|
||||||
// Verify loading state is set and pills are visible (i.e. header-center does NOT have .searching)
|
|
||||||
assert.ok(mapUi.loadingState, 'Search is loading');
|
|
||||||
assert
|
|
||||||
.dom('.header-center')
|
|
||||||
.doesNotHaveClass(
|
|
||||||
'searching',
|
|
||||||
'Pills remain visible while search is loading'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Resolve the promise with empty results
|
|
||||||
osmResolve();
|
|
||||||
await catPromise;
|
|
||||||
await settled();
|
|
||||||
|
|
||||||
// Verify search completed and since results are empty, pills are still visible
|
|
||||||
assert.strictEqual(mapUi.searchResults.length, 0, 'No results found');
|
|
||||||
assert
|
|
||||||
.dom('.header-center')
|
|
||||||
.doesNotHaveClass(
|
|
||||||
'searching',
|
|
||||||
'Pills remain visible after search completes with no results'
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. Now simulate a fast category search that returns results
|
|
||||||
const osmService = this.owner.lookup('service:osm');
|
|
||||||
osmService.getCategoryPois = async () => [
|
|
||||||
{
|
|
||||||
title: 'Latte Art Cafe',
|
|
||||||
lat: 1,
|
|
||||||
lon: 1,
|
|
||||||
osmId: '101',
|
|
||||||
osmType: 'N',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
await visit('/search?category=coffee&lat=1&lon=1');
|
|
||||||
|
|
||||||
// Verify search completed with results, so pills are hidden
|
|
||||||
assert.ok(mapUi.searchResults.length > 0, 'Results found');
|
|
||||||
assert
|
|
||||||
.dom('.header-center')
|
|
||||||
.hasClass(
|
|
||||||
'searching',
|
|
||||||
'Pills are hidden after search completes with results'
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
window.localStorage.removeItem('marco:map-view');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -270,76 +270,4 @@ module('Acceptance | search', function (hooks) {
|
|||||||
.hasText('Search request failed. Please try again.');
|
.hasText('Search request failed. Please try again.');
|
||||||
assert.dom('.places-sidebar').doesNotExist('Results panel should not open');
|
assert.dom('.places-sidebar').doesNotExist('Results panel should not open');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('search box query synchronized with active route query parameters', async function (assert) {
|
|
||||||
// Mock Osm Service
|
|
||||||
class MockOsmService extends Service {
|
|
||||||
async getCategoryPois() {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.owner.register('service:osm', MockOsmService);
|
|
||||||
|
|
||||||
// Mock Photon Service
|
|
||||||
class MockPhotonService extends Service {
|
|
||||||
async search() {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.owner.register('service:photon', MockPhotonService);
|
|
||||||
|
|
||||||
// Mock Storage Service
|
|
||||||
class MockStorageService extends Service {
|
|
||||||
savedPlaces = [];
|
|
||||||
findPlaceById() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
isPlaceSaved() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
rs = { on: () => {} };
|
|
||||||
placesInView = [];
|
|
||||||
loadPlacesInBounds() {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.owner.register('service:storage', MockStorageService);
|
|
||||||
|
|
||||||
// Mock Map Service
|
|
||||||
class MockMapService extends Service {
|
|
||||||
getBounds() {
|
|
||||||
return {
|
|
||||||
minLat: 52.5,
|
|
||||||
minLon: 13.4,
|
|
||||||
maxLat: 52.6,
|
|
||||||
maxLon: 13.5,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.owner.register('service:map', MockMapService);
|
|
||||||
|
|
||||||
// 1. Visit a search URL directly
|
|
||||||
await visit('/search?q=Berlin');
|
|
||||||
assert
|
|
||||||
.dom('.search-input')
|
|
||||||
.hasValue(
|
|
||||||
'Berlin',
|
|
||||||
'Search input is populated with search term on direct load'
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. Visit a category search URL
|
|
||||||
await visit('/search?category=coffee&lat=52.52&lon=13.405');
|
|
||||||
assert
|
|
||||||
.dom('.search-input')
|
|
||||||
.hasValue(
|
|
||||||
'Coffee',
|
|
||||||
'Search input is populated with mapped category label'
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. Go back to index
|
|
||||||
await visit('/');
|
|
||||||
assert
|
|
||||||
.dom('.search-input')
|
|
||||||
.hasValue('', 'Search input is cleared on transitioning to index');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,92 +4,6 @@ import {
|
|||||||
setupTest as upstreamSetupTest,
|
setupTest as upstreamSetupTest,
|
||||||
} from 'ember-qunit';
|
} from 'ember-qunit';
|
||||||
import { setupNostrMocks } from './mock-nostr';
|
import { setupNostrMocks } from './mock-nostr';
|
||||||
import sinon from 'sinon';
|
|
||||||
|
|
||||||
function setupMapStyleMocks(hooks) {
|
|
||||||
hooks.beforeEach(function () {
|
|
||||||
// Stub window.fetch to capture map-style assets before they hit the network
|
|
||||||
this.fetchStub = sinon.stub(window, 'fetch');
|
|
||||||
|
|
||||||
this.fetchStub.callsFake(async (input, init) => {
|
|
||||||
let url = input;
|
|
||||||
if (typeof input === 'object' && input !== null && 'url' in input) {
|
|
||||||
url = input.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof url === 'string' && url.includes('tiles.openfreemap.org')) {
|
|
||||||
// A. Mock Style Sheet
|
|
||||||
if (url.includes('/styles/liberty')) {
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
status: 200,
|
|
||||||
json: async () => ({
|
|
||||||
version: 8,
|
|
||||||
name: 'Mock Style',
|
|
||||||
sources: {
|
|
||||||
openmaptiles: {
|
|
||||||
type: 'vector',
|
|
||||||
tiles: [], // Empty tiles list prevents any map tile fetching completely!
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sprite: 'https://tiles.openfreemap.org/sprites/liberty',
|
|
||||||
glyphs:
|
|
||||||
'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf',
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: 'background',
|
|
||||||
type: 'background',
|
|
||||||
paint: { 'background-color': '#f8f9fa' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// B. Mock Sprite Atlas JSON
|
|
||||||
if (url.endsWith('.json') && url.includes('/sprites/')) {
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
status: 200,
|
|
||||||
json: async () => ({}), // Empty sprite dictionary
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// C. Mock Sprite Atlas PNG (Returns a valid 1x1 transparent PNG)
|
|
||||||
if (url.endsWith('.png') && url.includes('/sprites/')) {
|
|
||||||
const bytes = new Uint8Array([
|
|
||||||
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0,
|
|
||||||
0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73,
|
|
||||||
68, 65, 84, 120, 156, 99, 96, 0, 0, 0, 2, 0, 1, 226, 33, 188, 51, 0,
|
|
||||||
0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
|
|
||||||
]);
|
|
||||||
const blob = new Blob([bytes], { type: 'image/png' });
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
status: 200,
|
|
||||||
blob: async () => blob,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Catch-all mock for other openfreemap endpoints
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
status: 200,
|
|
||||||
json: async () => ({}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass through to original fetch (e.g. Photon results, local mock APIs)
|
|
||||||
return this.fetchStub.wrappedMethod(input, init);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
hooks.afterEach(function () {
|
|
||||||
if (this.fetchStub && typeof this.fetchStub.restore === 'function') {
|
|
||||||
this.fetchStub.restore();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// This file exists to provide wrappers around ember-qunit's
|
// This file exists to provide wrappers around ember-qunit's
|
||||||
// test setup functions. This way, you can easily extend the setup that is
|
// test setup functions. This way, you can easily extend the setup that is
|
||||||
@@ -98,7 +12,6 @@ function setupMapStyleMocks(hooks) {
|
|||||||
function setupApplicationTest(hooks, options) {
|
function setupApplicationTest(hooks, options) {
|
||||||
upstreamSetupApplicationTest(hooks, options);
|
upstreamSetupApplicationTest(hooks, options);
|
||||||
setupNostrMocks(hooks);
|
setupNostrMocks(hooks);
|
||||||
setupMapStyleMocks(hooks);
|
|
||||||
|
|
||||||
// Additional setup for application tests can be done here.
|
// Additional setup for application tests can be done here.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -49,19 +49,11 @@ export class MockNostrDataService extends Service {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
get requiredReadRelays() {
|
get defaultReadRelays() {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
get requiredWriteRelays() {
|
get defaultWriteRelays() {
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
get mailboxReadRelays() {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
get mailboxWriteRelays() {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
import { module, test } from 'qunit';
|
|
||||||
import { setupRenderingTest } from 'marco/tests/helpers';
|
|
||||||
import { click, fillIn, render } from '@ember/test-helpers';
|
|
||||||
import Service, { service } from '@ember/service';
|
|
||||||
import AppMenuSettingsNostr from 'marco/components/app-menu/settings/nostr';
|
|
||||||
import {
|
|
||||||
excludeRequiredRelays,
|
|
||||||
mergeRequiredRelays,
|
|
||||||
uniqNormalizedRelays,
|
|
||||||
} from 'marco/utils/nostr';
|
|
||||||
|
|
||||||
class MockNostrDataService extends Service {
|
|
||||||
@service settings;
|
|
||||||
|
|
||||||
requiredReadRelays = ['wss://nostr.kosmos.org'];
|
|
||||||
requiredWriteRelays = [];
|
|
||||||
|
|
||||||
mailboxReadRelays = ['wss://mailbox.example.com'];
|
|
||||||
mailboxWriteRelays = ['wss://mailbox-write.example.com'];
|
|
||||||
|
|
||||||
get configuredReadRelays() {
|
|
||||||
const configured = uniqNormalizedRelays([
|
|
||||||
...this.mailboxReadRelays,
|
|
||||||
...(this.settings.nostrReadRelays || []),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return excludeRequiredRelays(
|
|
||||||
configured,
|
|
||||||
this.settings.nostrReadRelayExclusions || []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get configuredWriteRelays() {
|
|
||||||
const configured = uniqNormalizedRelays([
|
|
||||||
...this.mailboxWriteRelays,
|
|
||||||
...(this.settings.nostrWriteRelays || []),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return excludeRequiredRelays(
|
|
||||||
configured,
|
|
||||||
this.settings.nostrWriteRelayExclusions || []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get activeReadRelays() {
|
|
||||||
return mergeRequiredRelays(
|
|
||||||
this.requiredReadRelays,
|
|
||||||
this.configuredReadRelays
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get activeWriteRelays() {
|
|
||||||
return mergeRequiredRelays(
|
|
||||||
this.requiredWriteRelays,
|
|
||||||
this.configuredWriteRelays
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async clearCache() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function readRows(element) {
|
|
||||||
const list = element.querySelectorAll('.relay-list')[0];
|
|
||||||
return [...list.querySelectorAll('li')];
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeRows(element) {
|
|
||||||
const list = element.querySelectorAll('.relay-list')[1];
|
|
||||||
return [...list.querySelectorAll('li')];
|
|
||||||
}
|
|
||||||
|
|
||||||
function rowByText(rows, text) {
|
|
||||||
return rows.find((row) => row.textContent.includes(text));
|
|
||||||
}
|
|
||||||
|
|
||||||
module('Integration | Component | app-menu/settings/nostr', function (hooks) {
|
|
||||||
setupRenderingTest(hooks);
|
|
||||||
|
|
||||||
hooks.beforeEach(function () {
|
|
||||||
localStorage.removeItem('marco:settings');
|
|
||||||
|
|
||||||
this.owner.register('service:nostrData', MockNostrDataService);
|
|
||||||
this.settings = this.owner.lookup('service:settings');
|
|
||||||
this.onChange = () => {};
|
|
||||||
});
|
|
||||||
|
|
||||||
hooks.afterEach(function () {
|
|
||||||
localStorage.removeItem('marco:settings');
|
|
||||||
});
|
|
||||||
|
|
||||||
async function renderAndOpenDetails(context) {
|
|
||||||
await render(
|
|
||||||
<template><AppMenuSettingsNostr @onChange={{this.onChange}} /></template>
|
|
||||||
);
|
|
||||||
await click('summary');
|
|
||||||
return context.element;
|
|
||||||
}
|
|
||||||
|
|
||||||
test('required read relay is first and non-removable', async function (assert) {
|
|
||||||
const element = await renderAndOpenDetails(this);
|
|
||||||
const rows = readRows(element);
|
|
||||||
|
|
||||||
assert.dom(rows[0]).includesText('nostr.kosmos.org');
|
|
||||||
|
|
||||||
const requiredRow = rowByText(rows, 'nostr.kosmos.org');
|
|
||||||
const mailboxRow = rowByText(rows, 'mailbox.example.com');
|
|
||||||
|
|
||||||
assert.dom(requiredRow.querySelector('.btn-remove-relay')).doesNotExist();
|
|
||||||
assert.dom(mailboxRow.querySelector('.btn-remove-relay')).exists();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('removing mailbox read relay stores exclusion override', async function (assert) {
|
|
||||||
const element = await renderAndOpenDetails(this);
|
|
||||||
const mailboxRow = rowByText(readRows(element), 'mailbox.example.com');
|
|
||||||
|
|
||||||
await click(mailboxRow.querySelector('.btn-remove-relay'));
|
|
||||||
|
|
||||||
assert.deepEqual(this.settings.nostrReadRelayExclusions, [
|
|
||||||
'wss://mailbox.example.com',
|
|
||||||
]);
|
|
||||||
assert.strictEqual(this.settings.nostrReadRelays, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('removing custom read relay updates custom list without exclusions', async function (assert) {
|
|
||||||
this.settings.update('nostrReadRelays', ['wss://custom.example.com']);
|
|
||||||
|
|
||||||
const element = await renderAndOpenDetails(this);
|
|
||||||
const customRow = rowByText(readRows(element), 'custom.example.com');
|
|
||||||
|
|
||||||
await click(customRow.querySelector('.btn-remove-relay'));
|
|
||||||
|
|
||||||
assert.strictEqual(this.settings.nostrReadRelays, null);
|
|
||||||
assert.strictEqual(this.settings.nostrReadRelayExclusions, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('adding read relay clears existing exclusion for same relay', async function (assert) {
|
|
||||||
this.settings.update('nostrReadRelayExclusions', [
|
|
||||||
'wss://mailbox.example.com',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const element = await renderAndOpenDetails(this);
|
|
||||||
|
|
||||||
await fillIn('#new-read-relay', 'Mailbox.EXAMPLE.com/');
|
|
||||||
await click(element.querySelector('#new-read-relay').nextElementSibling);
|
|
||||||
|
|
||||||
assert.deepEqual(this.settings.nostrReadRelays, [
|
|
||||||
'wss://mailbox.example.com',
|
|
||||||
]);
|
|
||||||
assert.strictEqual(this.settings.nostrReadRelayExclusions, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('reset read relays clears additions and exclusions', async function (assert) {
|
|
||||||
this.settings.update('nostrReadRelays', ['wss://custom.example.com']);
|
|
||||||
this.settings.update('nostrReadRelayExclusions', [
|
|
||||||
'wss://mailbox.example.com',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const element = await renderAndOpenDetails(this);
|
|
||||||
await click(element.querySelectorAll('.reset-relays')[0]);
|
|
||||||
|
|
||||||
assert.strictEqual(this.settings.nostrReadRelays, null);
|
|
||||||
assert.strictEqual(this.settings.nostrReadRelayExclusions, null);
|
|
||||||
|
|
||||||
const requiredRow = rowByText(readRows(element), 'nostr.kosmos.org');
|
|
||||||
assert.dom(requiredRow).exists();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('write relays are removable and mailbox delete stores exclusion', async function (assert) {
|
|
||||||
this.settings.update('nostrWriteRelays', [
|
|
||||||
'wss://custom-write.example.com',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const element = await renderAndOpenDetails(this);
|
|
||||||
const rows = writeRows(element);
|
|
||||||
|
|
||||||
assert.true(
|
|
||||||
rows.every((row) => row.querySelector('.btn-remove-relay')),
|
|
||||||
'all write relays can be removed'
|
|
||||||
);
|
|
||||||
|
|
||||||
const mailboxRow = rowByText(rows, 'mailbox-write.example.com');
|
|
||||||
await click(mailboxRow.querySelector('.btn-remove-relay'));
|
|
||||||
|
|
||||||
assert.deepEqual(this.settings.nostrWriteRelayExclusions, [
|
|
||||||
'wss://mailbox-write.example.com',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -334,54 +334,4 @@ module('Integration | Component | place-details', function (hooks) {
|
|||||||
assert.dom(links[0]).hasText('+44 987 654 321');
|
assert.dom(links[0]).hasText('+44 987 654 321');
|
||||||
assert.dom(links[1]).hasText('+1 234-567 8900');
|
assert.dom(links[1]).hasText('+1 234-567 8900');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it renders correct OpenStreetMap link for an OSM place', async function (assert) {
|
|
||||||
const place = {
|
|
||||||
title: 'OSM Place',
|
|
||||||
osmId: '12345',
|
|
||||||
osmType: 'node',
|
|
||||||
lat: 52.520008,
|
|
||||||
lon: 13.404954,
|
|
||||||
};
|
|
||||||
|
|
||||||
await render(<template><PlaceDetails @place={{place}} /></template>);
|
|
||||||
|
|
||||||
const osmLink = this.element.querySelector(
|
|
||||||
'.meta-info a[href^="https://www.openstreetmap.org/node/12345"]'
|
|
||||||
);
|
|
||||||
assert.ok(osmLink, 'OpenStreetMap link is rendered');
|
|
||||||
assert.strictEqual(
|
|
||||||
osmLink.getAttribute('href'),
|
|
||||||
'https://www.openstreetmap.org/node/12345'
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.dom('button.btn-link').hasText('Add a photo');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it renders correct search-based OpenStreetMap link for a custom saved place', async function (assert) {
|
|
||||||
class MockMapUi extends Service {
|
|
||||||
currentCenter = { lat: 52.5, lon: 13.4 };
|
|
||||||
currentZoom = 15.6;
|
|
||||||
}
|
|
||||||
this.owner.register('service:map-ui', MockMapUi);
|
|
||||||
|
|
||||||
const place = {
|
|
||||||
title: 'Custom Place',
|
|
||||||
lat: 52.520008,
|
|
||||||
lon: 13.404954,
|
|
||||||
};
|
|
||||||
|
|
||||||
await render(<template><PlaceDetails @place={{place}} /></template>);
|
|
||||||
|
|
||||||
const osmLink = this.element.querySelector(
|
|
||||||
'.meta-info a[href^="https://www.openstreetmap.org/search"]'
|
|
||||||
);
|
|
||||||
assert.ok(osmLink, 'OpenStreetMap search link is rendered');
|
|
||||||
assert.strictEqual(
|
|
||||||
osmLink.getAttribute('href'),
|
|
||||||
'https://www.openstreetmap.org/search?lat=52.520008&lon=13.404954&zoom=16#map=16/52.50000/13.40000'
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.dom('button.btn-link').doesNotExist();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import {
|
import { normalizeRelayUrl, parsePlacePhotos } from 'marco/utils/nostr';
|
||||||
excludeRequiredRelays,
|
|
||||||
mergeRequiredRelays,
|
|
||||||
normalizeRelayUrl,
|
|
||||||
parsePlacePhotos,
|
|
||||||
uniqNormalizedRelays,
|
|
||||||
} from 'marco/utils/nostr';
|
|
||||||
|
|
||||||
module('Unit | Utility | nostr', function () {
|
module('Unit | Utility | nostr', function () {
|
||||||
test('normalizeRelayUrl normalizes protocol, case, and slashes', function (assert) {
|
test('normalizeRelayUrl normalizes protocol, case, and slashes', function (assert) {
|
||||||
@@ -147,59 +141,4 @@ module('Unit | Utility | nostr', function () {
|
|||||||
assert.strictEqual(photos[0].placeIdentifier, 'osm:node:123');
|
assert.strictEqual(photos[0].placeIdentifier, 'osm:node:123');
|
||||||
assert.strictEqual(photos[1].placeIdentifier, 'osm:node:456');
|
assert.strictEqual(photos[1].placeIdentifier, 'osm:node:456');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('uniqNormalizedRelays returns normalized unique relays', function (assert) {
|
|
||||||
const relays = uniqNormalizedRelays([
|
|
||||||
'Relay.example.com',
|
|
||||||
'wss://relay.example.com/',
|
|
||||||
'wss://other.example.com',
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert.deepEqual(relays, [
|
|
||||||
'wss://relay.example.com',
|
|
||||||
'wss://other.example.com',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('mergeRequiredRelays keeps required relays as-is and merges normalized custom relays', function (assert) {
|
|
||||||
const relays = mergeRequiredRelays(
|
|
||||||
['wss://required.example.com', 'required-2.example.com'],
|
|
||||||
['required-2.example.com/', 'wss://custom.example.com']
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.deepEqual(relays, [
|
|
||||||
'wss://required.example.com',
|
|
||||||
'required-2.example.com',
|
|
||||||
'wss://required-2.example.com',
|
|
||||||
'wss://custom.example.com',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('excludeRequiredRelays removes required relays from normalized custom list', function (assert) {
|
|
||||||
const relays = excludeRequiredRelays(
|
|
||||||
[
|
|
||||||
'wss://required.example.com',
|
|
||||||
'custom.example.com',
|
|
||||||
'ws://custom2.example.com',
|
|
||||||
],
|
|
||||||
['wss://required.example.com']
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.deepEqual(relays, [
|
|
||||||
'wss://custom.example.com',
|
|
||||||
'ws://custom2.example.com',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('excludeRequiredRelays trusts required list without normalizing it', function (assert) {
|
|
||||||
const relays = excludeRequiredRelays(
|
|
||||||
['Required.example.com', 'custom.example.com'],
|
|
||||||
['required.example.com']
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.deepEqual(relays, [
|
|
||||||
'wss://required.example.com',
|
|
||||||
'wss://custom.example.com',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { getIconNameForTags, POI_ICON_RULES } from 'marco/utils/osm-icons';
|
import { getIconNameForTags } from 'marco/utils/osm-icons';
|
||||||
import { getIcon } from 'marco/utils/icons';
|
|
||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
|
|
||||||
module('Unit | Utility | osm-icons', function () {
|
module('Unit | Utility | osm-icons', function () {
|
||||||
@@ -37,14 +36,4 @@ module('Unit | Utility | osm-icons', function () {
|
|||||||
let result = getIconNameForTags({ foo: 'bar' });
|
let result = getIconNameForTags({ foo: 'bar' });
|
||||||
assert.strictEqual(result, null);
|
assert.strictEqual(result, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('all icons used in POI_ICON_RULES exist in the icons utility', function (assert) {
|
|
||||||
for (let rule of POI_ICON_RULES) {
|
|
||||||
let icon = getIcon(rule.icon);
|
|
||||||
assert.ok(
|
|
||||||
icon,
|
|
||||||
`Icon "${rule.icon}" specified in POI_ICON_RULES should be imported and available in getIcon`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -89,12 +89,6 @@ module('Unit | Utility | osm', function (hooks) {
|
|||||||
assert.strictEqual(result, 'Building');
|
assert.strictEqual(result, 'Building');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getPlaceType ignores generic "yes" values if a more specific tag is present', function (assert) {
|
|
||||||
const tags = { historic: 'yes', building: 'tower' };
|
|
||||||
const result = getPlaceType(tags);
|
|
||||||
assert.strictEqual(result, 'Tower');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getPlaceType prioritizes order (amenity > shop > building)', function (assert) {
|
test('getPlaceType prioritizes order (amenity > shop > building)', function (assert) {
|
||||||
// If something is both a shop and a building, it should be a shop
|
// If something is both a shop and a building, it should be a shop
|
||||||
const tags = { building: 'yes', shop: 'supermarket' };
|
const tags = { building: 'yes', shop: 'supermarket' };
|
||||||
|
|||||||
Reference in New Issue
Block a user