Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
f60dacac80
|
|||
|
9a02363515
|
|||
|
f28be0c994
|
|||
|
721fe5f01d
|
|||
|
518685b7dc
|
|||
|
262e5b61a8
|
|||
|
f87d8bdda9
|
|||
|
f17f8ca17b
|
59
app/components/app-header.gjs
Normal file
59
app/components/app-header.gjs
Normal file
@@ -0,0 +1,59 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { on } from '@ember/modifier';
|
||||
import Icon from '#components/icon';
|
||||
import UserMenu from '#components/user-menu';
|
||||
|
||||
export default class AppHeaderComponent extends Component {
|
||||
@service storage;
|
||||
@tracked isUserMenuOpen = false;
|
||||
|
||||
@action
|
||||
toggleUserMenu() {
|
||||
this.isUserMenuOpen = !this.isUserMenuOpen;
|
||||
}
|
||||
|
||||
@action
|
||||
closeUserMenu() {
|
||||
this.isUserMenuOpen = false;
|
||||
}
|
||||
|
||||
<template>
|
||||
<header class="app-header">
|
||||
<div class="header-left">
|
||||
<button class="icon-btn" type="button" aria-label="Menu">
|
||||
<Icon @name="menu" @size={{24}} @color="#333" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="user-menu-container">
|
||||
<button
|
||||
class="user-btn"
|
||||
type="button"
|
||||
aria-label="User Menu"
|
||||
{{on "click" this.toggleUserMenu}}
|
||||
>
|
||||
<div class="user-avatar-placeholder">
|
||||
<Icon @name="user" @size={{20}} @color="white" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{{#if this.isUserMenuOpen}}
|
||||
<UserMenu
|
||||
@storage={{this.storage}}
|
||||
@onClose={{this.closeUserMenu}}
|
||||
/>
|
||||
<div
|
||||
class="menu-backdrop"
|
||||
{{on "click" this.closeUserMenu}}
|
||||
role="button"
|
||||
></div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
}
|
||||
@@ -1,26 +1,44 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { htmlSafe } from '@ember/template';
|
||||
|
||||
import arrowLeft from 'feather-icons/dist/icons/arrow-left.svg?raw';
|
||||
import activity from 'feather-icons/dist/icons/activity.svg?raw';
|
||||
import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw';
|
||||
import clock from 'feather-icons/dist/icons/clock.svg?raw';
|
||||
import globe from 'feather-icons/dist/icons/globe.svg?raw';
|
||||
import home from 'feather-icons/dist/icons/home.svg?raw';
|
||||
import logIn from 'feather-icons/dist/icons/log-in.svg?raw';
|
||||
import logOut from 'feather-icons/dist/icons/log-out.svg?raw';
|
||||
import map from 'feather-icons/dist/icons/map.svg?raw';
|
||||
import mapPin from 'feather-icons/dist/icons/map-pin.svg?raw';
|
||||
import menu from 'feather-icons/dist/icons/menu.svg?raw';
|
||||
import navigation from 'feather-icons/dist/icons/navigation.svg?raw';
|
||||
import phone from 'feather-icons/dist/icons/phone.svg?raw';
|
||||
import user from 'feather-icons/dist/icons/user.svg?raw';
|
||||
import server from 'feather-icons/dist/icons/server.svg?raw';
|
||||
import settings from 'feather-icons/dist/icons/settings.svg?raw';
|
||||
import user from 'feather-icons/dist/icons/user.svg?raw';
|
||||
import x from 'feather-icons/dist/icons/x.svg?raw';
|
||||
import zap from 'feather-icons/dist/icons/zap.svg?raw';
|
||||
|
||||
const ICONS = {
|
||||
'arrow-left': arrowLeft,
|
||||
activity,
|
||||
bookmark,
|
||||
clock,
|
||||
globe,
|
||||
home,
|
||||
'log-in': logIn,
|
||||
'log-out': logOut,
|
||||
map,
|
||||
mapPin,
|
||||
'map-pin': mapPin,
|
||||
menu,
|
||||
navigation,
|
||||
phone,
|
||||
server,
|
||||
settings,
|
||||
user,
|
||||
settings
|
||||
x,
|
||||
zap,
|
||||
};
|
||||
|
||||
export default class IconComponent extends Component {
|
||||
|
||||
@@ -61,7 +61,6 @@ export default class MapComponent extends Component {
|
||||
zIndex: 10, // Ensure it sits above the map tiles
|
||||
});
|
||||
|
||||
|
||||
// Default view settings
|
||||
let center = [99.05738, 7.55087];
|
||||
let zoom = 13.0;
|
||||
@@ -95,7 +94,11 @@ export default class MapComponent extends Component {
|
||||
target: element,
|
||||
layers: [openfreemap, bookmarkLayer],
|
||||
view: view,
|
||||
controls: defaultControls({ zoom: false, rotate: true, attribution: true }),
|
||||
controls: defaultControls({
|
||||
zoom: false,
|
||||
rotate: true,
|
||||
attribution: true,
|
||||
}),
|
||||
});
|
||||
|
||||
apply(this.mapInstance, 'https://tiles.openfreemap.org/styles/liberty');
|
||||
@@ -210,7 +213,9 @@ export default class MapComponent extends Component {
|
||||
geolocation.un('change:position', zoomToLocation);
|
||||
locateListenerKey = null;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
// Hide pulse
|
||||
if (this.locationOverlayElement) {
|
||||
@@ -261,7 +266,8 @@ export default class MapComponent extends Component {
|
||||
else if (accuracy) {
|
||||
const viewportWidthMeters = 6.325 * accuracy;
|
||||
const minDimensionPixels = Math.min(size[0], size[1]);
|
||||
const requiredResolutionMeters = viewportWidthMeters / minDimensionPixels;
|
||||
const requiredResolutionMeters =
|
||||
viewportWidthMeters / minDimensionPixels;
|
||||
const metersPerMapUnit = getPointResolution(
|
||||
view.getProjection(),
|
||||
1,
|
||||
@@ -443,23 +449,23 @@ export default class MapComponent extends Component {
|
||||
|
||||
// If the pin is in the bottom half (y > splitPoint), it is obscured
|
||||
if (pixel[1] > splitPoint) {
|
||||
// Target position: Center of top half = height * 0.25
|
||||
const targetY = height * 0.25;
|
||||
const deltaY = pixel[1] - targetY;
|
||||
// Target position: Center of top half = height * 0.25
|
||||
const targetY = height * 0.25;
|
||||
const deltaY = pixel[1] - targetY;
|
||||
|
||||
const view = this.mapInstance.getView();
|
||||
const center = view.getCenter();
|
||||
const resolution = view.getResolution();
|
||||
const view = this.mapInstance.getView();
|
||||
const center = view.getCenter();
|
||||
const resolution = view.getResolution();
|
||||
|
||||
// Move the map center SOUTH (decrease Y) to move the pin UP (decrease pixel Y)
|
||||
const deltaMapUnits = deltaY * resolution;
|
||||
const newCenter = [center[0], center[1] - deltaMapUnits];
|
||||
// Move the map center SOUTH (decrease Y) to move the pin UP (decrease pixel Y)
|
||||
const deltaMapUnits = deltaY * resolution;
|
||||
const newCenter = [center[0], center[1] - deltaMapUnits];
|
||||
|
||||
view.animate({
|
||||
center: newCenter,
|
||||
duration: 500,
|
||||
easing: (t) => t * (2 - t) // Ease-out
|
||||
});
|
||||
view.animate({
|
||||
center: newCenter,
|
||||
duration: 500,
|
||||
easing: (t) => t * (2 - t), // Ease-out
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,7 +531,7 @@ export default class MapComponent extends Component {
|
||||
|
||||
const viewState = {
|
||||
center: currentCenter,
|
||||
zoom: currentZoom
|
||||
zoom: currentZoom,
|
||||
};
|
||||
|
||||
localStorage.setItem('marco:map-view', JSON.stringify(viewState));
|
||||
@@ -545,7 +551,9 @@ export default class MapComponent extends Component {
|
||||
|
||||
if (features && features.length > 0) {
|
||||
console.debug(`Found ${features.length} features in map layer:`);
|
||||
for (const f of features) { console.debug(f) }
|
||||
for (const f of features) {
|
||||
console.debug(f);
|
||||
}
|
||||
const bookmarkFeature = features.find((f) => f.get('isBookmark'));
|
||||
if (bookmarkFeature) {
|
||||
clickedBookmark = bookmarkFeature.get('originalPlace');
|
||||
|
||||
@@ -79,10 +79,10 @@ export default class PlaceDetails extends Component {
|
||||
get cuisine() {
|
||||
if (!this.tags.cuisine) return null;
|
||||
return this.tags.cuisine
|
||||
.split(';')
|
||||
.map(c => capitalize.compute([c]))
|
||||
.map(c => c.replace('_', ' '))
|
||||
.join(', ');
|
||||
.split(';')
|
||||
.map((c) => capitalize.compute([c]))
|
||||
.map((c) => c.replace('_', ' '))
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
get wikipedia() {
|
||||
@@ -123,17 +123,25 @@ export default class PlaceDetails extends Component {
|
||||
</p>
|
||||
{{#if this.place.description}}
|
||||
<p class="place-description">
|
||||
{{this.place.description}}
|
||||
{{this.place.description}}
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
<div class="actions">
|
||||
<button
|
||||
type="button"
|
||||
class={{if this.place.createdAt "btn-secondary" "btn-primary"}}
|
||||
class={{if
|
||||
this.place.createdAt
|
||||
"btn btn-secondary"
|
||||
"btn btn-outline"
|
||||
}}
|
||||
{{on "click" (fn @onToggleSave this.place)}}
|
||||
>
|
||||
{{if this.place.createdAt "Saved ✓" "Save"}}
|
||||
<Icon
|
||||
@name="bookmark"
|
||||
@color={{if this.place.createdAt "currentColor" "#007bff"}}
|
||||
/>
|
||||
{{if this.place.createdAt "Saved" "Save"}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -163,14 +171,22 @@ export default class PlaceDetails extends Component {
|
||||
{{#if this.website}}
|
||||
<p class="content-with-icon">
|
||||
<Icon @name="globe" @title="Website" />
|
||||
<span><a href={{this.website}} target="_blank" rel="noopener noreferrer">{{this.websiteDomain}}</a></span>
|
||||
<span><a
|
||||
href={{this.website}}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{this.websiteDomain}}</a></span>
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.wikipedia}}
|
||||
<p>
|
||||
<strong>Wikipedia:</strong>
|
||||
<a href="https://wikipedia.org/wiki/{{this.wikipedia}}" target="_blank" rel="noopener noreferrer">Article</a>
|
||||
<a
|
||||
href="https://wikipedia.org/wiki/{{this.wikipedia}}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Article</a>
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
@@ -185,7 +201,7 @@ export default class PlaceDetails extends Component {
|
||||
{{/if}}
|
||||
|
||||
<p class="content-with-icon">
|
||||
<Icon @name="mapPin" @title="Geo link" />
|
||||
<Icon @name="map-pin" @title="Geo link" />
|
||||
<span>
|
||||
<a href={{this.geoLink}} target="_blank" rel="noopener noreferrer">
|
||||
{{this.visibleGeoLink}}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { on } from '@ember/modifier';
|
||||
import { fn } from '@ember/helper';
|
||||
import or from 'ember-truth-helpers/helpers/or';
|
||||
import PlaceDetails from './place-details';
|
||||
import Icon from './icon';
|
||||
|
||||
export default class PlacesSidebar extends Component {
|
||||
@service storage;
|
||||
@@ -49,23 +50,23 @@ export default class PlacesSidebar extends Component {
|
||||
// Update selection to the new saved place object
|
||||
// This updates the local UI state immediately without a route refresh
|
||||
if (this.args.onUpdate) {
|
||||
// When deleting, we revert to a "fresh" object or just close.
|
||||
// Since we close the sidebar below, we might not strictly need to update local state,
|
||||
// but it's good practice.
|
||||
// Reconstruct the "original" place without ID/Geohash/CreatedAt
|
||||
const freshPlace = {
|
||||
...place,
|
||||
id: undefined,
|
||||
geohash: undefined,
|
||||
createdAt: undefined
|
||||
};
|
||||
this.args.onUpdate(freshPlace);
|
||||
// When deleting, we revert to a "fresh" object or just close.
|
||||
// Since we close the sidebar below, we might not strictly need to update local state,
|
||||
// but it's good practice.
|
||||
// Reconstruct the "original" place without ID/Geohash/CreatedAt
|
||||
const freshPlace = {
|
||||
...place,
|
||||
id: undefined,
|
||||
geohash: undefined,
|
||||
createdAt: undefined,
|
||||
};
|
||||
this.args.onUpdate(freshPlace);
|
||||
}
|
||||
|
||||
// Also fire onSelect if it exists (for list view)
|
||||
// Also fire onSelect if it exists (for list view)
|
||||
if (this.args.onSelect) {
|
||||
// Similar logic for select if needed, but we usually close.
|
||||
this.args.onSelect(null);
|
||||
// Similar logic for select if needed, but we usually close.
|
||||
this.args.onSelect(null);
|
||||
}
|
||||
|
||||
// Close sidebar after delete
|
||||
@@ -80,7 +81,8 @@ export default class PlacesSidebar extends Component {
|
||||
} else {
|
||||
// It's a fresh POI -> Save it
|
||||
const placeData = {
|
||||
title: place.osmTags.name || place.osmTags['name:en'] || 'Untitled Place',
|
||||
title:
|
||||
place.osmTags.name || place.osmTags['name:en'] || 'Untitled Place',
|
||||
lat: place.lat,
|
||||
lon: place.lon,
|
||||
tags: [],
|
||||
@@ -123,15 +125,15 @@ export default class PlacesSidebar extends Component {
|
||||
type="button"
|
||||
class="back-btn"
|
||||
{{on "click" this.clearSelection}}
|
||||
>←</button>
|
||||
><Icon @name="arrow-left" @size={{20}} @color="#333" /></button>
|
||||
{{else}}
|
||||
<h2>Nearby Places</h2>
|
||||
{{/if}}
|
||||
<button
|
||||
type="button"
|
||||
class="close-btn"
|
||||
{{on "click" @onClose}}
|
||||
>×</button>
|
||||
<button type="button" class="close-btn" {{on "click" @onClose}}><Icon
|
||||
@name="x"
|
||||
@size={{20}}
|
||||
@color="#333"
|
||||
/></button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-content">
|
||||
|
||||
66
app/components/user-menu.gjs
Normal file
66
app/components/user-menu.gjs
Normal file
@@ -0,0 +1,66 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import Icon from '#components/icon';
|
||||
import { on } from '@ember/modifier';
|
||||
|
||||
export default class UserMenuComponent extends Component {
|
||||
@action
|
||||
connectRS() {
|
||||
this.args.onClose();
|
||||
this.args.storage.connect();
|
||||
}
|
||||
|
||||
@action
|
||||
disconnectRS() {
|
||||
this.args.storage.disconnect();
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="user-menu-popover">
|
||||
<div class="user-status">
|
||||
{{#if @storage.connected}}
|
||||
Connected as
|
||||
<strong>{{@storage.userAddress}}</strong>
|
||||
{{else}}
|
||||
Not connected
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<ul class="account-list">
|
||||
<li class="account-item">
|
||||
<div class="account-info">
|
||||
<Icon @name="server" @size={{18}} />
|
||||
<span>RemoteStorage</span>
|
||||
</div>
|
||||
{{#if @storage.connected}}
|
||||
<button
|
||||
class="btn-text text-danger"
|
||||
type="button"
|
||||
{{on "click" this.disconnectRS}}
|
||||
>Disconnect</button>
|
||||
{{else}}
|
||||
<button
|
||||
class="btn-text text-primary"
|
||||
type="button"
|
||||
{{on "click" this.connectRS}}
|
||||
>Connect</button>
|
||||
{{/if}}
|
||||
</li>
|
||||
|
||||
<li class="account-item disabled">
|
||||
<div class="account-info">
|
||||
<Icon @name="globe" @size={{18}} />
|
||||
<span>OpenStreetMap</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="account-item disabled">
|
||||
<div class="account-info">
|
||||
<Icon @name="zap" @size={{18}} />
|
||||
<span>Nostr</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -24,7 +24,7 @@ out center;
|
||||
`.trim();
|
||||
|
||||
const url = `https://overpass.bke.ro/api/interpreter?data=${encodeURIComponent(
|
||||
// const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(
|
||||
// const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(
|
||||
query
|
||||
)}`;
|
||||
|
||||
@@ -100,7 +100,8 @@ out center;
|
||||
`.trim();
|
||||
}
|
||||
|
||||
const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(
|
||||
const url = `https://overpass.bke.ro/api/interpreter?data=${encodeURIComponent(
|
||||
// const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(
|
||||
query
|
||||
)}`;
|
||||
const res = await this.fetchWithRetry(url);
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import Service from '@ember/service';
|
||||
import RemoteStorage from 'remotestoragejs';
|
||||
import Places from '@remotestorage/module-places';
|
||||
import Widget from 'remotestorage-widget';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
|
||||
import { action } from '@ember/object';
|
||||
import { debounce } from '@ember/runloop';
|
||||
import Geohash from 'latlon-geohash';
|
||||
|
||||
export default class StorageService extends Service {
|
||||
rs;
|
||||
widget;
|
||||
@tracked placesInView = [];
|
||||
@tracked savedPlaces = [];
|
||||
@tracked loadedPrefixes = [];
|
||||
@tracked currentBbox = null;
|
||||
@tracked version = 0; // Shared version tracker for bookmarks
|
||||
@tracked initialSyncDone = false;
|
||||
@tracked connected = false;
|
||||
@tracked userAddress = null;
|
||||
@tracked isWidgetOpen = false;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
@@ -29,14 +35,38 @@ export default class StorageService extends Service {
|
||||
|
||||
window.remoteStorage = this.rs;
|
||||
|
||||
// const widget = new Widget(this.rs);
|
||||
// widget.attach();
|
||||
this.widget = new Widget(this.rs, {
|
||||
leaveOpen: true,
|
||||
skipInitial: true,
|
||||
});
|
||||
// We don't attach immediately; we'll attach when the user clicks Connect
|
||||
|
||||
this.rs.on('ready', () => {
|
||||
// console.debug('[rs] client ready');
|
||||
});
|
||||
|
||||
this.rs.on('sync-done', (result) => {
|
||||
this.rs.on('connected', () => {
|
||||
console.debug('Remote storage connected');
|
||||
this.connected = true;
|
||||
this.userAddress = this.rs.remote.userAddress;
|
||||
|
||||
// Close widget after successful connection (respecting autoCloseAfter)
|
||||
setTimeout(() => {
|
||||
this.isWidgetOpen = false;
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
this.rs.on('disconnected', () => {
|
||||
console.debug('Remote storage disconnected');
|
||||
this.connected = false;
|
||||
this.userAddress = null;
|
||||
this.placesInView = [];
|
||||
this.savedPlaces = [];
|
||||
this.loadedPrefixes = [];
|
||||
this.initialSyncDone = false;
|
||||
});
|
||||
|
||||
this.rs.on('sync-done', () => {
|
||||
// console.debug('[rs] sync done:', result);
|
||||
if (!this.initialSyncDone) {
|
||||
this.initialSyncDone = true;
|
||||
@@ -157,7 +187,7 @@ export default class StorageService extends Service {
|
||||
// If the hash is in the set of reloaded prefixes, we discard the old version
|
||||
// (because the 'places' array contains the authoritative new state for this prefix)
|
||||
return !prefixSet.has(hash);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return true; // Keep malformed/unknown places safe
|
||||
}
|
||||
});
|
||||
@@ -198,6 +228,28 @@ export default class StorageService extends Service {
|
||||
|
||||
async removePlace(place) {
|
||||
await this.places.remove(place.id, place.geohash);
|
||||
this.savedPlaces = this.savedPlaces.filter(p => p.id !== place.id);
|
||||
this.savedPlaces = this.savedPlaces.filter((p) => p.id !== place.id);
|
||||
}
|
||||
|
||||
@action
|
||||
connect() {
|
||||
this.isWidgetOpen = true;
|
||||
|
||||
// Check if widget is already attached
|
||||
if (!document.querySelector('.rs-widget')) {
|
||||
// Attach to our specific container
|
||||
this.widget.attach('rs-widget-container');
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
closeWidget() {
|
||||
this.isWidgetOpen = false;
|
||||
}
|
||||
|
||||
@action
|
||||
disconnect() {
|
||||
this.rs.disconnect();
|
||||
this.isWidgetOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,161 @@ body {
|
||||
outline: none; /* Prevent focus outline on click */
|
||||
}
|
||||
|
||||
/* Ensure RS widget is above the map */
|
||||
#remotestorage-widget {
|
||||
/* Ensure RS widget is above the map but potentially hidden initially if needed */
|
||||
#rs-widget-container {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
top: 60px; /* Below header */
|
||||
right: 10px;
|
||||
z-index: 1000;
|
||||
z-index: 4000;
|
||||
display: none; /* Hidden by default */
|
||||
}
|
||||
|
||||
#rs-widget-container.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.rs-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 3999; /* Below widget container but above everything else */
|
||||
/* background: rgba(0,0,0,0.2); Optional: dim background */
|
||||
}
|
||||
|
||||
/* App Header */
|
||||
.app-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60px;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 3000; /* Above sidebar (2000) and map */
|
||||
pointer-events: none; /* Let clicks pass through to map where transparent */
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right {
|
||||
pointer-events: auto; /* Re-enable clicks for buttons */
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
.icon-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.user-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.user-avatar-placeholder {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #333;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* User Menu Popover */
|
||||
.user-menu-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-menu-popover {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 10px;
|
||||
width: 280px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
padding: 1rem;
|
||||
z-index: 3001;
|
||||
}
|
||||
|
||||
.menu-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 3000; /* Below popover but above everything else */
|
||||
/* background: rgba(0,0,0,0.1); Optional dimming */
|
||||
}
|
||||
|
||||
.user-status {
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.account-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.account-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
|
||||
.account-item.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.account-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #007bff;
|
||||
}
|
||||
.text-danger {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.btn-text:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Sidebar Styles */
|
||||
@@ -62,20 +211,26 @@ body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
margin-right: 0.5rem;
|
||||
padding: 0 0.5rem;
|
||||
margin-left: -0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0 0.5rem;
|
||||
margin-right: -0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.place-details {
|
||||
@@ -99,23 +254,63 @@ body {
|
||||
|
||||
.place-details .actions {
|
||||
padding-bottom: 0.3rem;
|
||||
/* display: flex; */
|
||||
/* flex-direction: row; */
|
||||
/* gap: 1rem; */
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
/* width: 50%; */
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: #333;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
border: 1px solid #898989;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
color: #333;
|
||||
border: 1px solid rgba(255, 204, 51, 0.2);
|
||||
background: rgba(255, 204, 51, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(255, 204, 51, 0.4);
|
||||
}
|
||||
|
||||
.btn-blue {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-blue:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.btn-green {
|
||||
background: #198754;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-green:hover {
|
||||
background: #157347;
|
||||
}
|
||||
|
||||
.places-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
@@ -218,13 +413,28 @@ body {
|
||||
|
||||
/* Locate Control */
|
||||
.ol-control.ol-locate {
|
||||
top: 5em; /* Position below zoom controls (usually at .5em or similar) */
|
||||
top: auto;
|
||||
bottom: 2.5em;
|
||||
right: 0.5em;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.ol-touch .ol-control.ol-locate {
|
||||
top: 5.5em; /* Adjust for touch devices where controls might be larger */
|
||||
top: auto;
|
||||
bottom: 3.5em;
|
||||
}
|
||||
|
||||
/* Rotate Control */
|
||||
.ol-rotate {
|
||||
top: auto;
|
||||
bottom: 5em;
|
||||
right: 0.5em;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.ol-touch .ol-rotate {
|
||||
top: auto;
|
||||
bottom: 6em;
|
||||
}
|
||||
|
||||
span.icon {
|
||||
|
||||
@@ -2,11 +2,13 @@ import Component from '@glimmer/component';
|
||||
import { pageTitle } from 'ember-page-title';
|
||||
import Map from '#components/map';
|
||||
import PlacesSidebar from '#components/places-sidebar';
|
||||
import AppHeader from '#components/app-header';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { eq } from 'ember-truth-helpers';
|
||||
import { and } from 'ember-truth-helpers';
|
||||
import { on } from '@ember/modifier';
|
||||
|
||||
export default class ApplicationComponent extends Component {
|
||||
@service storage;
|
||||
@@ -74,6 +76,21 @@ export default class ApplicationComponent extends Component {
|
||||
<template>
|
||||
{{pageTitle "M/\RCO"}}
|
||||
|
||||
<AppHeader />
|
||||
|
||||
<div
|
||||
id="rs-widget-container"
|
||||
class={{if this.storage.isWidgetOpen "visible"}}
|
||||
></div>
|
||||
|
||||
{{#if this.storage.isWidgetOpen}}
|
||||
<div
|
||||
class="rs-backdrop"
|
||||
role="button"
|
||||
{{on "click" this.storage.closeWidget}}
|
||||
></div>
|
||||
{{/if}}
|
||||
|
||||
<Map
|
||||
@onPlacesFound={{this.showPlaces}}
|
||||
@isSidebarOpen={{this.isSidebarOpen}}
|
||||
|
||||
@@ -38,8 +38,11 @@ export default class PlaceTemplate extends Component {
|
||||
|
||||
// 3. If not saved, check our local "optimistic" state (from handleUpdate)
|
||||
// This handles the "unsaved" state immediately after deletion before any other sync
|
||||
if (this.localPlace && (this.localPlace.osmId === id || this.localPlace.id === id)) {
|
||||
return this.localPlace;
|
||||
if (
|
||||
this.localPlace &&
|
||||
(this.localPlace.osmId === id || this.localPlace.id === id)
|
||||
) {
|
||||
return this.localPlace;
|
||||
}
|
||||
|
||||
// 4. Fallback to the route model (which might be the stale "saved" object from when the route loaded)
|
||||
@@ -50,7 +53,7 @@ export default class PlaceTemplate extends Component {
|
||||
...model,
|
||||
id: undefined,
|
||||
createdAt: undefined,
|
||||
geohash: undefined
|
||||
geohash: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "marco",
|
||||
"version": "1.4.2",
|
||||
"version": "1.5.0",
|
||||
"private": true,
|
||||
"description": "Small description for marco goes here",
|
||||
"repository": "",
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
release/assets/main-DPNrocGB.js
Normal file
2
release/assets/main-DPNrocGB.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
release/assets/main-h3_9Qqi3.css
Normal file
1
release/assets/main-h3_9Qqi3.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -6,8 +6,8 @@
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<script type="module" crossorigin src="/assets/main-C6x36ClG.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-_X0dk-zm.css">
|
||||
<script type="module" crossorigin src="/assets/main-DPNrocGB.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-h3_9Qqi3.css">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user