diff --git a/app/components/place-edit-form.gjs b/app/components/place-edit-form.gjs new file mode 100644 index 0000000..38e3465 --- /dev/null +++ b/app/components/place-edit-form.gjs @@ -0,0 +1,72 @@ +import Component from '@glimmer/component'; +import { on } from '@ember/modifier'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; + +export default class PlaceEditForm extends Component { + @tracked title = ''; + @tracked description = ''; + + constructor() { + super(...arguments); + this.title = this.args.place?.title || ''; + this.description = this.args.place?.description || ''; + } + + @action + handleSubmit(event) { + event.preventDefault(); + if (this.args.onSave) { + this.args.onSave({ + title: this.title, + description: this.description, + }); + } + } + + @action + updateTitle(e) { + this.title = e.target.value; + } + + @action + updateDescription(e) { + this.description = e.target.value; + } + + + + +} diff --git a/app/components/places-sidebar.gjs b/app/components/places-sidebar.gjs index 2be58bb..c9e95a9 100644 --- a/app/components/places-sidebar.gjs +++ b/app/components/places-sidebar.gjs @@ -10,6 +10,22 @@ import humanizeOsmTag from '../helpers/humanize-osm-tag'; export default class PlacesSidebar extends Component { @service storage; + @service router; + @service mapUi; + + @action + createNewPlace() { + const qp = this.router.currentRoute.queryParams; + const lat = qp.lat; + const lon = qp.lon; + + if (lat && lon) { + this.router.transitionTo('place.new', { queryParams: { lat, lon } }); + } else { + // Fallback (shouldn't happen in search context) + this.router.transitionTo('place.new', { queryParams: { lat: 0, lon: 0 } }); + } + } @action selectPlace(place) { @@ -180,6 +196,7 @@ export default class PlacesSidebar extends Component { place.osmTags.tourism place.osmTags.leisure place.osmTags.historic + "Point of Interest" )}}
No places found nearby.
{{/if}} + + {{/if}} diff --git a/app/router.js b/app/router.js index 89c9419..170d769 100644 --- a/app/router.js +++ b/app/router.js @@ -8,5 +8,6 @@ export default class Router extends EmberRouter { Router.map(function () { this.route('place', { path: '/place/:place_id' }); + this.route('place.new', { path: '/place/new' }); this.route('search'); }); diff --git a/app/routes/place/new.js b/app/routes/place/new.js new file mode 100644 index 0000000..33cfb6a --- /dev/null +++ b/app/routes/place/new.js @@ -0,0 +1,30 @@ +import Route from '@ember/routing/route'; +import { service } from '@ember/service'; + +export default class PlaceNewRoute extends Route { + @service mapUi; + + queryParams = { + lat: { refreshModel: true }, + lon: { refreshModel: true }, + }; + + model(params) { + return { + lat: parseFloat(params.lat), + lon: parseFloat(params.lon), + }; + } + + setupController(controller, model) { + super.setupController(controller, model); + if (model.lat && model.lon) { + this.mapUi.updateCreationCoordinates(model.lat, model.lon); + } + this.mapUi.startCreating(); + } + + deactivate() { + this.mapUi.stopCreating(); + } +} diff --git a/app/routes/search.js b/app/routes/search.js index cbb37c4..6ec1323 100644 --- a/app/routes/search.js +++ b/app/routes/search.js @@ -28,6 +28,26 @@ export default class SearchRoute extends Route { // Fetch POIs let pois = await this.osm.getNearbyPois(lat, lon, searchRadius); + // Get cached/saved places in search radius + const localMatches = this.storage.savedPlaces.filter((p) => { + const dist = getDistance(lat, lon, p.lat, p.lon); + return dist <= searchRadius; + }); + + // Add local matches to the list if they aren't already there + // We use osmId to deduplicate if possible + localMatches.forEach((local) => { + const exists = pois.find( + (poi) => + (local.osmId && poi.osmId === local.osmId) || + (poi.id && poi.id === local.id) + ); + + if (!exists) { + pois.push(local); + } + }); + // Sort by distance from click pois = pois .map((p) => { diff --git a/app/services/map-ui.js b/app/services/map-ui.js index d3d9892..c125cbd 100644 --- a/app/services/map-ui.js +++ b/app/services/map-ui.js @@ -4,6 +4,8 @@ import { tracked } from '@glimmer/tracking'; export default class MapUiService extends Service { @tracked selectedPlace = null; @tracked isSearching = false; + @tracked isCreating = false; + @tracked creationCoordinates = null; selectPlace(place) { this.selectedPlace = place; @@ -15,9 +17,24 @@ export default class MapUiService extends Service { startSearch() { this.isSearching = true; + this.isCreating = false; } stopSearch() { this.isSearching = false; } + + startCreating() { + this.isCreating = true; + this.isSearching = false; + } + + stopCreating() { + this.isCreating = false; + this.creationCoordinates = null; + } + + updateCreationCoordinates(lat, lon) { + this.creationCoordinates = { lat, lon }; + } } diff --git a/app/services/storage.js b/app/services/storage.js index c2455b0..24e2bce 100644 --- a/app/services/storage.js +++ b/app/services/storage.js @@ -216,29 +216,54 @@ export default class StorageService extends Service { async storePlace(placeData) { const savedPlace = await this.places.store(placeData); - // Only append if not already there (handlePlaceChange might also fire) + + // Optimistic Update: Global List if (!this.savedPlaces.some((p) => p.id === savedPlace.id)) { this.savedPlaces = [...this.savedPlaces, savedPlace]; + } else { + // Update if exists + this.savedPlaces = this.savedPlaces.map((p) => + p.id === savedPlace.id ? savedPlace : p + ); } + + // Optimistic Update: Map View (same logic as Global List) + if (!this.placesInView.some((p) => p.id === savedPlace.id)) { + this.placesInView = [...this.placesInView, savedPlace]; + } else { + this.placesInView = this.placesInView.map((p) => + p.id === savedPlace.id ? savedPlace : p + ); + } + return savedPlace; } async updatePlace(placeData) { const savedPlace = await this.places.store(placeData); - // Update local list + // Optimistic Update: Global List const index = this.savedPlaces.findIndex((p) => p.id === savedPlace.id); if (index !== -1) { const newPlaces = [...this.savedPlaces]; newPlaces[index] = savedPlace; this.savedPlaces = newPlaces; } + + // Update Map View + this.placesInView = this.placesInView.map((p) => + p.id === savedPlace.id ? savedPlace : p + ); + return savedPlace; } async removePlace(place) { await this.places.remove(place.id, place.geohash); + + // Update both lists this.savedPlaces = this.savedPlaces.filter((p) => p.id !== place.id); + this.placesInView = this.placesInView.filter((p) => p.id !== place.id); } @action diff --git a/app/styles/app.css b/app/styles/app.css index 32e0353..57b5ecb 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -649,6 +649,82 @@ span.icon { } } +/* Map Crosshair for "Create Place" mode */ +.map-crosshair { + position: absolute; + /* Default Center */ + top: 50%; + left: 50%; + width: 24px; + height: 24px; + transform: translate(-50%, -50%); + color: #333; + pointer-events: none; + z-index: 2000; + display: none; + transition: + top 0.3s ease, + left 0.3s ease; +} + +.map-crosshair.visible { + display: block; +} + +/* Sidebar is open (Desktop: Left 300px) */ +/* We want to center in the remaining space (width - 300px) */ +/* Center X = 300 + (width - 300) / 2 = 300 + width/2 - 150 = width/2 + 150 */ +/* So shift left by 150px from center */ +.map-container.sidebar-open .map-crosshair { + left: calc(50% + 150px); +} + +@media (width <= 768px) { + /* Sidebar/Bottom Sheet is open (Mobile: Bottom 50%) */ + /* Center Y = (height/2) / 2 = height/4 = 25% */ + .map-container.sidebar-open .map-crosshair { + left: 50%; /* Reset desktop shift */ + top: 25%; + } +} + +/* Helper Text */ +.helper-text { + background: #eef4fc; + color: #1a5c9b; + padding: 0.75rem; + border-radius: 4px; + font-size: 0.9rem; + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* Create Place Button */ +.create-place-btn { + width: 100%; + padding: 1rem; + margin-top: 1rem; + background: white; + border: 1px dashed #ccc; + border-radius: 8px; + color: #666; + font-weight: 500; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + transition: all 0.2s; +} + +.create-place-btn:hover { + background: #f8f9fa; + border-color: #999; + color: #333; +} + @media (width <= 768px) { .sidebar { width: 100%; diff --git a/app/templates/application.gjs b/app/templates/application.gjs index 4ec36d4..fc013b7 100644 --- a/app/templates/application.gjs +++ b/app/templates/application.gjs @@ -21,6 +21,7 @@ export default class ApplicationComponent extends Component { // This helps the map know if it should shift the center or adjust view. return ( this.router.currentRouteName === 'place' || + this.router.currentRouteName === 'place.new' || this.router.currentRouteName === 'search' ); } diff --git a/app/templates/place/new.gjs b/app/templates/place/new.gjs new file mode 100644 index 0000000..0605beb --- /dev/null +++ b/app/templates/place/new.gjs @@ -0,0 +1,83 @@ +import Component from '@glimmer/component'; +import { service } from '@ember/service'; +import { action } from '@ember/object'; +import { on } from '@ember/modifier'; +import PlaceEditForm from '#components/place-edit-form'; +import Icon from '#components/icon'; + +export default class PlaceNewTemplate extends Component { + @service router; + @service storage; + @service mapUi; + + get initialPlace() { + return { + title: '', + description: '', + }; + } + + @action + close() { + this.router.transitionTo('index'); + } + + @action + async savePlace(changes) { + try { + // Use coordinates from Map UI (which tracks the crosshair center) + // Fallback to URL params if map state isn't ready + const center = this.mapUi.creationCoordinates || { + lat: this.args.model.lat, + lon: this.args.model.lon, + }; + + const lat = parseFloat(center.lat.toFixed(6)); + const lon = parseFloat(center.lon.toFixed(6)); + + const placeData = { + title: changes.title || 'Untitled Place', + description: changes.description, + lat: lat, + lon: lon, + tags: [], + osmTags: {}, + }; + + const savedPlace = await this.storage.storePlace(placeData); + console.log('Created private place:', savedPlace.title); + + // Transition to the new place + this.router.replaceWith('place', savedPlace); + } catch (e) { + console.error('Failed to create place:', e); + alert('Failed to create place: ' + e.message); + } + } + + + + +}