Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
026d1c4712
|
|||
|
6bd55843bb
|
|||
|
33a6469a19
|
|||
|
6d7bea411a
|
|||
|
7b01bb1118
|
@@ -67,6 +67,11 @@ export default class PlaceDetails extends Component {
|
||||
return this.place.url || this.tags.website || this.tags['contact:website'];
|
||||
}
|
||||
|
||||
get websiteDomain() {
|
||||
const url = new URL(this.website);
|
||||
return url.hostname;
|
||||
}
|
||||
|
||||
get openingHours() {
|
||||
return this.tags.opening_hours;
|
||||
}
|
||||
@@ -106,6 +111,10 @@ export default class PlaceDetails extends Component {
|
||||
return `https://www.openstreetmap.org/${type}/${id}`;
|
||||
}
|
||||
|
||||
get gmapsUrl() {
|
||||
return `https://www.google.com/maps/search/?api=1&query=${this.name}&query=${this.place.lat},${this.place.lon}`;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="place-details">
|
||||
<h3>{{this.name}}</h3>
|
||||
@@ -129,6 +138,7 @@ export default class PlaceDetails extends Component {
|
||||
</div>
|
||||
|
||||
<div class="meta-info">
|
||||
|
||||
{{#if this.cuisine}}
|
||||
<p>
|
||||
<strong>Cuisine:</strong>
|
||||
@@ -153,7 +163,7 @@ 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">Website</a></span>
|
||||
<span><a href={{this.website}} target="_blank" rel="noopener noreferrer">{{this.websiteDomain}}</a></span>
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
@@ -164,7 +174,8 @@ export default class PlaceDetails extends Component {
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
<hr class="meta-divider">
|
||||
</div>
|
||||
<div class="meta-info">
|
||||
|
||||
{{#if this.address}}
|
||||
<p class="content-with-icon">
|
||||
@@ -192,6 +203,16 @@ export default class PlaceDetails extends Component {
|
||||
</span>
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
<p class="content-with-icon">
|
||||
<Icon @name="map" @title="OSM ID" />
|
||||
<span>
|
||||
<a href={{this.gmapsUrl}} target="_blank" rel="noopener noreferrer">
|
||||
Google Maps
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -31,70 +31,46 @@ export default class PlacesSidebar extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
get geoLink() {
|
||||
if (!this.args.selectedPlace) return '#';
|
||||
const p = this.args.selectedPlace;
|
||||
// geo:lat,lon?q=lat,lon(Label)
|
||||
const label = encodeURIComponent(
|
||||
p.title ||
|
||||
p.tags?.name ||
|
||||
p.tags?.['name:en'] ||
|
||||
'Location'
|
||||
);
|
||||
return `geo:${p.lat},${p.lon}?q=${p.lat},${p.lon}(${label})`;
|
||||
}
|
||||
|
||||
get visibleGeoLink() {
|
||||
if (!this.args.selectedPlace) return '';
|
||||
const p = this.args.selectedPlace;
|
||||
return `geo:${p.lat},${p.lon}`;
|
||||
}
|
||||
|
||||
@action
|
||||
async toggleSave(place) {
|
||||
if (!place) return;
|
||||
|
||||
if (place.createdAt) {
|
||||
// It's a saved bookmark -> Delete it
|
||||
if (confirm(`Delete "${place.title}"?`)) {
|
||||
try {
|
||||
if (place.id && place.geohash) {
|
||||
await this.storage.places.remove(place.id, place.geohash);
|
||||
console.log('Place deleted:', place.title);
|
||||
await this.storage.removePlace(place);
|
||||
console.log('Place deleted:', place.title);
|
||||
|
||||
// Notify parent to refresh map bookmarks
|
||||
if (this.args.onBookmarkChange) {
|
||||
this.args.onBookmarkChange();
|
||||
}
|
||||
// Notify parent to refresh map bookmarks
|
||||
if (this.args.onBookmarkChange) {
|
||||
this.args.onBookmarkChange();
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Close sidebar after delete
|
||||
if (this.args.onClose) {
|
||||
this.args.onClose();
|
||||
}
|
||||
} else {
|
||||
alert('Cannot delete: Missing ID or Geohash');
|
||||
// Close sidebar after delete
|
||||
if (this.args.onClose) {
|
||||
this.args.onClose();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to delete:', e);
|
||||
@@ -115,7 +91,7 @@ export default class PlacesSidebar extends Component {
|
||||
};
|
||||
|
||||
try {
|
||||
const savedPlace = await this.storage.places.store(placeData);
|
||||
const savedPlace = await this.storage.storePlace(placeData);
|
||||
console.log('Place saved:', placeData.title);
|
||||
|
||||
// Notify parent to refresh map bookmarks
|
||||
@@ -148,7 +124,6 @@ export default class PlacesSidebar extends Component {
|
||||
class="back-btn"
|
||||
{{on "click" this.clearSelection}}
|
||||
>←</button>
|
||||
<h2>Details</h2>
|
||||
{{else}}
|
||||
<h2>Nearby Places</h2>
|
||||
{{/if}}
|
||||
@@ -161,9 +136,9 @@ export default class PlacesSidebar extends Component {
|
||||
|
||||
<div class="sidebar-content">
|
||||
{{#if @selectedPlace}}
|
||||
<PlaceDetails
|
||||
@place={{@selectedPlace}}
|
||||
@onToggleSave={{this.toggleSave}}
|
||||
<PlaceDetails
|
||||
@place={{@selectedPlace}}
|
||||
@onToggleSave={{this.toggleSave}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#if @places}}
|
||||
|
||||
@@ -9,17 +9,14 @@ export default class PlaceRoute extends Route {
|
||||
async model(params) {
|
||||
const id = params.place_id;
|
||||
|
||||
// Check for explicit OSM prefixes
|
||||
if (id.startsWith('osm:node:') || id.startsWith('osm:way:')) {
|
||||
const [, type, osmId] = id.split(':');
|
||||
console.log(`Fetching explicit OSM ${type}:`, osmId);
|
||||
return this.loadOsmPlace(osmId, type);
|
||||
}
|
||||
|
||||
// Wait for storage sync before checking bookmarks
|
||||
await this.waitForSync();
|
||||
|
||||
// 1. Try to find in local bookmarks
|
||||
let bookmark = this.storage.findPlaceById(id);
|
||||
|
||||
if (bookmark) {
|
||||
@@ -27,9 +24,8 @@ export default class PlaceRoute extends Route {
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
// 2. Fallback: Fetch from OSM (assuming generic ID or old format)
|
||||
console.log('Not in bookmarks, fetching from OSM:', id);
|
||||
return this.loadOsmPlace(id);
|
||||
console.warn('Not in bookmarks:', id);
|
||||
return null;
|
||||
}
|
||||
|
||||
async waitForSync() {
|
||||
|
||||
@@ -23,7 +23,8 @@ export default class OsmService extends Service {
|
||||
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
|
||||
)}`;
|
||||
|
||||
|
||||
@@ -178,12 +178,26 @@ export default class StorageService extends Service {
|
||||
}
|
||||
|
||||
findPlaceById(id) {
|
||||
// Search by internal ID first
|
||||
let place = this.savedPlaces.find((p) => p.id === id);
|
||||
if (!id) return undefined;
|
||||
const strId = String(id);
|
||||
|
||||
// Search by internal ID first (loose comparison via string cast)
|
||||
let place = this.savedPlaces.find((p) => p.id && String(p.id) === strId);
|
||||
if (place) return place;
|
||||
|
||||
// Then search by OSM ID
|
||||
place = this.savedPlaces.find((p) => p.osmId === id);
|
||||
place = this.savedPlaces.find((p) => p.osmId && String(p.osmId) === strId);
|
||||
return place;
|
||||
}
|
||||
|
||||
async storePlace(placeData) {
|
||||
const savedPlace = await this.places.store(placeData);
|
||||
this.savedPlaces = [...this.savedPlaces, savedPlace];
|
||||
return savedPlace;
|
||||
}
|
||||
|
||||
async removePlace(place) {
|
||||
await this.places.remove(place.id, place.geohash);
|
||||
this.savedPlaces = this.savedPlaces.filter(p => p.id !== place.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,10 @@ body {
|
||||
.place-details .place-description {
|
||||
}
|
||||
|
||||
.place-details .actions {
|
||||
padding-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
@@ -149,13 +153,16 @@ body {
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #eee;
|
||||
font-size: 0.9rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.meta-info p:first-child {
|
||||
margin-top: 1.2rem;
|
||||
padding-top: 1.2rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.meta-info p {
|
||||
margin: 0.75rem 0;
|
||||
line-height: 1.4;
|
||||
@@ -169,18 +176,13 @@ body {
|
||||
.meta-info a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
.meta-info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.meta-divider {
|
||||
border: 0;
|
||||
border-top: 1px dashed #ddd;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
/* Map Search Pulse Animation */
|
||||
.search-pulse {
|
||||
border-radius: 50%;
|
||||
|
||||
@@ -23,20 +23,37 @@ export default class PlaceTemplate extends Component {
|
||||
// Let's use a modifier or just sync it.
|
||||
|
||||
get place() {
|
||||
// If we have a manually updated place (from save), use it.
|
||||
// Otherwise use the route model.
|
||||
// We need to ensure we reset `localPlace` when navigating to a NEW place.
|
||||
// Comparing IDs is a safe bet.
|
||||
|
||||
// 1. Resolve the ID from the model (OSM ID or internal ID)
|
||||
const model = this.args.model;
|
||||
if (
|
||||
this.localPlace &&
|
||||
(this.localPlace.id === model.id || this.localPlace.osmId === model.osmId)
|
||||
) {
|
||||
// If the local place is "richer" (has createdAt), prefer it.
|
||||
if (this.localPlace.createdAt && !model.createdAt) return this.localPlace;
|
||||
// If we deleted it (local has no createdAt, model might?) - wait, if we delete, we close sidebar.
|
||||
const id = model.osmId || model.id;
|
||||
|
||||
// 2. Check the storage service for a LIVE version of this bookmark
|
||||
// This is the most critical fix: Storage is the source of truth.
|
||||
// Since `this.storage.savedPlaces` is @tracked, this getter will re-compute
|
||||
// whenever a bookmark is added or removed.
|
||||
const saved = this.storage.findPlaceById(id);
|
||||
if (saved) {
|
||||
return saved;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 4. Fallback to the route model (which might be the stale "saved" object from when the route loaded)
|
||||
// If the model *has* a createdAt but we didn't find it in step 2 (storage),
|
||||
// it means it was deleted. We must return a sanitized version.
|
||||
if (model.createdAt) {
|
||||
return {
|
||||
...model,
|
||||
id: undefined,
|
||||
createdAt: undefined,
|
||||
geohash: undefined
|
||||
};
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "marco",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.2",
|
||||
"private": true,
|
||||
"description": "Small description for marco goes here",
|
||||
"repository": "",
|
||||
|
||||
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-_X0dk-zm.css
Normal file
1
release/assets/main-_X0dk-zm.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-IB7GaxzK.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-B-vHK2y6.css">
|
||||
<script type="module" crossorigin src="/assets/main-C6x36ClG.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-_X0dk-zm.css">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user