5 Commits

Author SHA1 Message Date
026d1c4712 1.4.2 2026-01-23 12:59:37 +07:00
6bd55843bb Switch to bkero's API (for now) 2026-01-23 12:59:11 +07:00
33a6469a19 Various layout and style improvements for place details 2026-01-23 12:41:27 +07:00
6d7bea411a 1.4.1 2026-01-23 10:21:25 +07:00
7b01bb1118 Fix place store/remove behavior 2026-01-23 10:21:02 +07:00
13 changed files with 124 additions and 98 deletions

View File

@@ -67,6 +67,11 @@ export default class PlaceDetails extends Component {
return this.place.url || this.tags.website || this.tags['contact:website']; return this.place.url || this.tags.website || this.tags['contact:website'];
} }
get websiteDomain() {
const url = new URL(this.website);
return url.hostname;
}
get openingHours() { get openingHours() {
return this.tags.opening_hours; return this.tags.opening_hours;
} }
@@ -106,6 +111,10 @@ export default class PlaceDetails extends Component {
return `https://www.openstreetmap.org/${type}/${id}`; 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> <template>
<div class="place-details"> <div class="place-details">
<h3>{{this.name}}</h3> <h3>{{this.name}}</h3>
@@ -129,6 +138,7 @@ export default class PlaceDetails extends Component {
</div> </div>
<div class="meta-info"> <div class="meta-info">
{{#if this.cuisine}} {{#if this.cuisine}}
<p> <p>
<strong>Cuisine:</strong> <strong>Cuisine:</strong>
@@ -153,7 +163,7 @@ export default class PlaceDetails extends Component {
{{#if this.website}} {{#if this.website}}
<p class="content-with-icon"> <p class="content-with-icon">
<Icon @name="globe" @title="Website" /> <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> </p>
{{/if}} {{/if}}
@@ -164,7 +174,8 @@ export default class PlaceDetails extends Component {
</p> </p>
{{/if}} {{/if}}
<hr class="meta-divider"> </div>
<div class="meta-info">
{{#if this.address}} {{#if this.address}}
<p class="content-with-icon"> <p class="content-with-icon">
@@ -192,6 +203,16 @@ export default class PlaceDetails extends Component {
</span> </span>
</p> </p>
{{/if}} {{/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>
</div> </div>
</template> </template>

View File

@@ -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 @action
async toggleSave(place) { async toggleSave(place) {
if (!place) return; if (!place) return;
if (place.createdAt) { if (place.createdAt) {
// It's a saved bookmark -> Delete it
if (confirm(`Delete "${place.title}"?`)) { if (confirm(`Delete "${place.title}"?`)) {
try { try {
if (place.id && place.geohash) { await this.storage.removePlace(place);
await this.storage.places.remove(place.id, place.geohash); console.log('Place deleted:', place.title);
console.log('Place deleted:', place.title);
// Notify parent to refresh map bookmarks // Notify parent to refresh map bookmarks
if (this.args.onBookmarkChange) { if (this.args.onBookmarkChange) {
this.args.onBookmarkChange(); this.args.onBookmarkChange();
} }
// Update selection to the new saved place object // Update selection to the new saved place object
// This updates the local UI state immediately without a route refresh // This updates the local UI state immediately without a route refresh
if (this.args.onUpdate) { if (this.args.onUpdate) {
// When deleting, we revert to a "fresh" object or just close. // 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, // Since we close the sidebar below, we might not strictly need to update local state,
// but it's good practice. // but it's good practice.
// Reconstruct the "original" place without ID/Geohash/CreatedAt // Reconstruct the "original" place without ID/Geohash/CreatedAt
const freshPlace = { const freshPlace = {
...place, ...place,
id: undefined, id: undefined,
geohash: undefined, geohash: undefined,
createdAt: undefined createdAt: undefined
}; };
this.args.onUpdate(freshPlace); 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) { if (this.args.onSelect) {
// Similar logic for select if needed, but we usually close. // Similar logic for select if needed, but we usually close.
this.args.onSelect(null); this.args.onSelect(null);
} }
// Close sidebar after delete // Close sidebar after delete
if (this.args.onClose) { if (this.args.onClose) {
this.args.onClose(); this.args.onClose();
}
} else {
alert('Cannot delete: Missing ID or Geohash');
} }
} catch (e) { } catch (e) {
console.error('Failed to delete:', e); console.error('Failed to delete:', e);
@@ -115,7 +91,7 @@ export default class PlacesSidebar extends Component {
}; };
try { try {
const savedPlace = await this.storage.places.store(placeData); const savedPlace = await this.storage.storePlace(placeData);
console.log('Place saved:', placeData.title); console.log('Place saved:', placeData.title);
// Notify parent to refresh map bookmarks // Notify parent to refresh map bookmarks
@@ -148,7 +124,6 @@ export default class PlacesSidebar extends Component {
class="back-btn" class="back-btn"
{{on "click" this.clearSelection}} {{on "click" this.clearSelection}}
>←</button> >←</button>
<h2>Details</h2>
{{else}} {{else}}
<h2>Nearby Places</h2> <h2>Nearby Places</h2>
{{/if}} {{/if}}
@@ -161,9 +136,9 @@ export default class PlacesSidebar extends Component {
<div class="sidebar-content"> <div class="sidebar-content">
{{#if @selectedPlace}} {{#if @selectedPlace}}
<PlaceDetails <PlaceDetails
@place={{@selectedPlace}} @place={{@selectedPlace}}
@onToggleSave={{this.toggleSave}} @onToggleSave={{this.toggleSave}}
/> />
{{else}} {{else}}
{{#if @places}} {{#if @places}}

View File

@@ -9,17 +9,14 @@ export default class PlaceRoute extends Route {
async model(params) { async model(params) {
const id = params.place_id; const id = params.place_id;
// Check for explicit OSM prefixes
if (id.startsWith('osm:node:') || id.startsWith('osm:way:')) { if (id.startsWith('osm:node:') || id.startsWith('osm:way:')) {
const [, type, osmId] = id.split(':'); const [, type, osmId] = id.split(':');
console.log(`Fetching explicit OSM ${type}:`, osmId); console.log(`Fetching explicit OSM ${type}:`, osmId);
return this.loadOsmPlace(osmId, type); return this.loadOsmPlace(osmId, type);
} }
// Wait for storage sync before checking bookmarks
await this.waitForSync(); await this.waitForSync();
// 1. Try to find in local bookmarks
let bookmark = this.storage.findPlaceById(id); let bookmark = this.storage.findPlaceById(id);
if (bookmark) { if (bookmark) {
@@ -27,9 +24,8 @@ export default class PlaceRoute extends Route {
return bookmark; return bookmark;
} }
// 2. Fallback: Fetch from OSM (assuming generic ID or old format) console.warn('Not in bookmarks:', id);
console.log('Not in bookmarks, fetching from OSM:', id); return null;
return this.loadOsmPlace(id);
} }
async waitForSync() { async waitForSync() {

View File

@@ -23,7 +23,8 @@ export default class OsmService extends Service {
out center; out center;
`.trim(); `.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 query
)}`; )}`;

View File

@@ -178,12 +178,26 @@ export default class StorageService extends Service {
} }
findPlaceById(id) { findPlaceById(id) {
// Search by internal ID first if (!id) return undefined;
let place = this.savedPlaces.find((p) => p.id === id); 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; if (place) return place;
// Then search by OSM ID // 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; 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);
}
} }

View File

@@ -97,6 +97,10 @@ body {
.place-details .place-description { .place-details .place-description {
} }
.place-details .actions {
padding-bottom: 0.3rem;
}
.btn-primary { .btn-primary {
background: #007bff; background: #007bff;
color: white; color: white;
@@ -149,13 +153,16 @@ body {
} }
.meta-info { .meta-info {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid #eee;
font-size: 0.9rem; font-size: 0.9rem;
text-align: left; text-align: left;
} }
.meta-info p:first-child {
margin-top: 1.2rem;
padding-top: 1.2rem;
border-top: 1px solid #eee;
}
.meta-info p { .meta-info p {
margin: 0.75rem 0; margin: 0.75rem 0;
line-height: 1.4; line-height: 1.4;
@@ -169,18 +176,13 @@ body {
.meta-info a { .meta-info a {
color: #007bff; color: #007bff;
text-decoration: none; text-decoration: none;
padding-bottom: 4rem;
} }
.meta-info a:hover { .meta-info a:hover {
text-decoration: underline; text-decoration: underline;
} }
.meta-divider {
border: 0;
border-top: 1px dashed #ddd;
margin: 1rem 0;
}
/* Map Search Pulse Animation */ /* Map Search Pulse Animation */
.search-pulse { .search-pulse {
border-radius: 50%; border-radius: 50%;

View File

@@ -23,20 +23,37 @@ export default class PlaceTemplate extends Component {
// Let's use a modifier or just sync it. // Let's use a modifier or just sync it.
get place() { get place() {
// If we have a manually updated place (from save), use it. // 1. Resolve the ID from the model (OSM ID or internal ID)
// Otherwise use the route model.
// We need to ensure we reset `localPlace` when navigating to a NEW place.
// Comparing IDs is a safe bet.
const model = this.args.model; const model = this.args.model;
if ( const id = model.osmId || model.id;
this.localPlace &&
(this.localPlace.id === model.id || this.localPlace.osmId === model.osmId) // 2. Check the storage service for a LIVE version of this bookmark
) { // This is the most critical fix: Storage is the source of truth.
// If the local place is "richer" (has createdAt), prefer it. // Since `this.storage.savedPlaces` is @tracked, this getter will re-compute
if (this.localPlace.createdAt && !model.createdAt) return this.localPlace; // whenever a bookmark is added or removed.
// If we deleted it (local has no createdAt, model might?) - wait, if we delete, we close sidebar. 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; return model;
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "marco", "name": "marco",
"version": "1.4.0", "version": "1.4.2",
"private": true, "private": true,
"description": "Small description for marco goes here", "description": "Small description for marco goes here",
"repository": "", "repository": "",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,8 +6,8 @@
<meta name="description" content=""> <meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<script type="module" crossorigin src="/assets/main-IB7GaxzK.js"></script> <script type="module" crossorigin src="/assets/main-C6x36ClG.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-B-vHK2y6.css"> <link rel="stylesheet" crossorigin href="/assets/main-_X0dk-zm.css">
</head> </head>
<body> <body>
</body> </body>