Compare commits
5 Commits
41d61be42e
...
e7b3b72e2f
| Author | SHA1 | Date | |
|---|---|---|---|
| e7b3b72e2f | |||
| 399ad1822d | |||
| 104a742543 | |||
| a8dc4c81e4 | |||
| 156280950f |
@ -21,6 +21,7 @@ export default class MapComponent extends Component {
|
||||
@service osm;
|
||||
@service storage;
|
||||
@service mapUi;
|
||||
@service router;
|
||||
|
||||
mapInstance;
|
||||
bookmarkSource;
|
||||
@ -510,6 +511,17 @@ export default class MapComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Sync the pulse animation with the UI service state
|
||||
syncPulse = modifier(() => {
|
||||
if (!this.searchOverlayElement) return;
|
||||
|
||||
if (this.mapUi.isSearching) {
|
||||
this.searchOverlayElement.classList.add('active');
|
||||
} else {
|
||||
this.searchOverlayElement.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
handleMapMove = async () => {
|
||||
if (!this.mapInstance) return;
|
||||
|
||||
@ -546,7 +558,6 @@ export default class MapComponent extends Component {
|
||||
});
|
||||
let clickedBookmark = null;
|
||||
let selectedFeatureName = null;
|
||||
let selectedFeatureType = null;
|
||||
|
||||
if (features && features.length > 0) {
|
||||
console.debug(`Found ${features.length} features in map layer:`);
|
||||
@ -561,7 +572,6 @@ export default class MapComponent extends Component {
|
||||
const props = features[0].getProperties();
|
||||
if (props.name) {
|
||||
selectedFeatureName = props.name;
|
||||
selectedFeatureType = props.class || props.subclass;
|
||||
}
|
||||
}
|
||||
|
||||
@ -573,9 +583,7 @@ export default class MapComponent extends Component {
|
||||
'Clicked bookmark while sidebar open (switching):',
|
||||
clickedBookmark
|
||||
);
|
||||
if (this.args.onPlacesFound) {
|
||||
this.args.onPlacesFound([], clickedBookmark);
|
||||
}
|
||||
this.router.transitionTo('place', clickedBookmark);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -589,9 +597,7 @@ export default class MapComponent extends Component {
|
||||
// Normal behavior (sidebar is closed)
|
||||
if (clickedBookmark) {
|
||||
console.log('Clicked bookmark:', clickedBookmark);
|
||||
if (this.args.onPlacesFound) {
|
||||
this.args.onPlacesFound([], clickedBookmark);
|
||||
}
|
||||
this.router.transitionTo('place', clickedBookmark);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -615,76 +621,21 @@ export default class MapComponent extends Component {
|
||||
this.searchOverlayElement.style.width = `${diameterInPixels}px`;
|
||||
this.searchOverlayElement.style.height = `${diameterInPixels}px`;
|
||||
this.searchOverlay.setPosition(event.coordinate);
|
||||
this.searchOverlayElement.classList.add('active');
|
||||
}
|
||||
|
||||
// 2. Fetch authoritative data via Overpass
|
||||
try {
|
||||
let pois = await this.osm.getNearbyPois(lat, lon, searchRadius);
|
||||
// Start Search State
|
||||
this.mapUi.startSearch();
|
||||
|
||||
// Sort by distance from click
|
||||
pois = pois
|
||||
.map((p) => {
|
||||
// p is already normalized by service, so lat/lon are at top level
|
||||
return {
|
||||
...p,
|
||||
_distance: getDistance(lat, lon, p.lat, p.lon),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a._distance - b._distance);
|
||||
|
||||
let matchedPlace = null;
|
||||
|
||||
if (selectedFeatureName && pois.length > 0) {
|
||||
// Heuristic:
|
||||
// 1. Exact Name Match
|
||||
matchedPlace = pois.find(
|
||||
(p) =>
|
||||
p.osmTags &&
|
||||
(p.osmTags.name === selectedFeatureName ||
|
||||
p.osmTags['name:en'] === selectedFeatureName)
|
||||
);
|
||||
|
||||
// 2. If no exact match, look for VERY close (<=20m) and matching type
|
||||
if (!matchedPlace) {
|
||||
const topCandidate = pois[0];
|
||||
if (topCandidate._distance <= 20) {
|
||||
// Check type compatibility if available
|
||||
// (visual tile 'class' is often 'cafe', osm tag is 'amenity'='cafe')
|
||||
const pType =
|
||||
topCandidate.osmTags.amenity ||
|
||||
topCandidate.osmTags.shop ||
|
||||
topCandidate.osmTags.tourism;
|
||||
if (
|
||||
selectedFeatureType &&
|
||||
pType &&
|
||||
(selectedFeatureType === pType ||
|
||||
pType.includes(selectedFeatureType))
|
||||
) {
|
||||
console.log(
|
||||
'Heuristic match found (distance + type):',
|
||||
topCandidate
|
||||
);
|
||||
matchedPlace = topCandidate;
|
||||
} else if (topCandidate._distance <= 10) {
|
||||
// Even without type match, if it's super close (<=10m), it's likely the one.
|
||||
console.log('Heuristic match found (proximity):', topCandidate);
|
||||
matchedPlace = topCandidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.args.onPlacesFound) {
|
||||
this.args.onPlacesFound(pois, matchedPlace);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch POIs:', error);
|
||||
} finally {
|
||||
if (this.searchOverlayElement) {
|
||||
this.searchOverlayElement.classList.remove('active');
|
||||
}
|
||||
// Transition to Search Route
|
||||
const queryParams = {
|
||||
lat: lat.toFixed(6),
|
||||
lon: lon.toFixed(6),
|
||||
};
|
||||
if (selectedFeatureName) {
|
||||
queryParams.q = selectedFeatureName;
|
||||
}
|
||||
|
||||
this.router.transitionTo('search', { queryParams });
|
||||
};
|
||||
|
||||
<template>
|
||||
@ -693,6 +644,7 @@ export default class MapComponent extends Component {
|
||||
{{this.setupMap}}
|
||||
{{this.updateBookmarks}}
|
||||
{{this.updateSelectedPin}}
|
||||
{{this.syncPulse}}
|
||||
></div>
|
||||
</template>
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { fn } from '@ember/helper';
|
||||
import { on } from '@ember/modifier';
|
||||
import capitalize from '../helpers/capitalize';
|
||||
import { humanizeOsmTag } from '../utils/format-text';
|
||||
import Icon from '../components/icon';
|
||||
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
@ -76,14 +76,15 @@ export default class PlaceDetails extends Component {
|
||||
}
|
||||
|
||||
get type() {
|
||||
return (
|
||||
const rawType =
|
||||
this.tags.amenity ||
|
||||
this.tags.shop ||
|
||||
this.tags.tourism ||
|
||||
this.tags.leisure ||
|
||||
this.tags.historic ||
|
||||
'Point of Interest'
|
||||
);
|
||||
'Point of Interest';
|
||||
|
||||
return humanizeOsmTag(rawType);
|
||||
}
|
||||
|
||||
get address() {
|
||||
@ -133,8 +134,7 @@ export default class PlaceDetails extends Component {
|
||||
if (!this.tags.cuisine) return null;
|
||||
return this.tags.cuisine
|
||||
.split(';')
|
||||
.map((c) => capitalize.compute([c]))
|
||||
.map((c) => c.replace('_', ' '))
|
||||
.map((c) => humanizeOsmTag(c))
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import { fn } from '@ember/helper';
|
||||
import or from 'ember-truth-helpers/helpers/or';
|
||||
import PlaceDetails from './place-details';
|
||||
import Icon from './icon';
|
||||
import humanizeOsmTag from '../helpers/humanize-osm-tag';
|
||||
|
||||
export default class PlacesSidebar extends Component {
|
||||
@service storage;
|
||||
@ -23,13 +24,6 @@ export default class PlacesSidebar extends Component {
|
||||
if (this.args.onSelect) {
|
||||
this.args.onSelect(null);
|
||||
}
|
||||
|
||||
// Fallback logic: if no list available, close sidebar
|
||||
if (!this.args.places || this.args.places.length === 0) {
|
||||
if (this.args.onClose) {
|
||||
this.args.onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@ -180,13 +174,13 @@ export default class PlacesSidebar extends Component {
|
||||
place.osmTags.name:en
|
||||
"Unnamed Place"
|
||||
}}</div>
|
||||
<div class="place-type">{{or
|
||||
<div class="place-type">{{humanizeOsmTag (or
|
||||
place.osmTags.amenity
|
||||
place.osmTags.shop
|
||||
place.osmTags.tourism
|
||||
place.osmTags.leisure
|
||||
place.osmTags.historic
|
||||
}}</div>
|
||||
)}}</div>
|
||||
</button>
|
||||
</li>
|
||||
{{/each}}
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
import { helper } from '@ember/component/helper';
|
||||
|
||||
export function capitalize([str]) {
|
||||
if (typeof str !== 'string') return '';
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
export default helper(capitalize);
|
||||
6
app/helpers/humanize-osm-tag.js
Normal file
6
app/helpers/humanize-osm-tag.js
Normal file
@ -0,0 +1,6 @@
|
||||
import { helper } from '@ember/component/helper';
|
||||
import { humanizeOsmTag as format } from '../utils/format-text';
|
||||
|
||||
export default helper(function humanizeOsmTag([text]) {
|
||||
return format(text);
|
||||
});
|
||||
@ -8,4 +8,5 @@ export default class Router extends EmberRouter {
|
||||
|
||||
Router.map(function () {
|
||||
this.route('place', { path: '/place/:place_id' });
|
||||
this.route('search');
|
||||
});
|
||||
|
||||
@ -49,6 +49,8 @@ export default class PlaceRoute extends Route {
|
||||
if (model) {
|
||||
this.mapUi.selectPlace(model);
|
||||
}
|
||||
// Stop the pulse animation if it was running (e.g. redirected from search)
|
||||
this.mapUi.stopSearch();
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
|
||||
96
app/routes/search.js
Normal file
96
app/routes/search.js
Normal file
@ -0,0 +1,96 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { getDistance } from '../utils/geo';
|
||||
|
||||
export default class SearchRoute extends Route {
|
||||
@service osm;
|
||||
@service mapUi;
|
||||
@service storage;
|
||||
@service router;
|
||||
|
||||
queryParams = {
|
||||
lat: { refreshModel: true },
|
||||
lon: { refreshModel: true },
|
||||
q: { refreshModel: true },
|
||||
};
|
||||
|
||||
async model(params) {
|
||||
// If no coordinates, we can't search
|
||||
if (!params.lat || !params.lon) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lat = parseFloat(params.lat);
|
||||
const lon = parseFloat(params.lon);
|
||||
const searchRadius = params.q ? 30 : 50;
|
||||
|
||||
// Fetch POIs
|
||||
let pois = await this.osm.getNearbyPois(lat, lon, searchRadius);
|
||||
|
||||
// Sort by distance from click
|
||||
pois = pois
|
||||
.map((p) => {
|
||||
return {
|
||||
...p,
|
||||
_distance: getDistance(lat, lon, p.lat, p.lon),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a._distance - b._distance);
|
||||
|
||||
// Check if any of these are already bookmarked
|
||||
// We resolve them to the bookmark version if they exist
|
||||
pois = pois.map((p) => {
|
||||
const saved = this.storage.findPlaceById(p.osmId);
|
||||
return saved || p;
|
||||
});
|
||||
|
||||
return pois;
|
||||
}
|
||||
|
||||
afterModel(model, transition) {
|
||||
const { q } = transition.to.queryParams;
|
||||
|
||||
// Heuristic Match Logic (ported from MapComponent)
|
||||
if (q && model.length > 0) {
|
||||
let matchedPlace = null;
|
||||
|
||||
// 1. Exact Name Match
|
||||
matchedPlace = model.find(
|
||||
(p) => p.osmTags && (p.osmTags.name === q || p.osmTags['name:en'] === q)
|
||||
);
|
||||
|
||||
// 2. High Proximity Match (<= 10m)
|
||||
// Note: MapComponent had logic for <=20m + type match.
|
||||
// We might want to pass the 'type' in queryParams if we want to be that precise.
|
||||
// For now, let's stick to name or very close proximity.
|
||||
if (!matchedPlace) {
|
||||
const topCandidate = model[0];
|
||||
if (topCandidate._distance <= 10) {
|
||||
matchedPlace = topCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedPlace) {
|
||||
// Direct transition!
|
||||
this.router.replaceWith('place', matchedPlace);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the pulse animation since search is done (and we are staying here)
|
||||
this.mapUi.stopSearch();
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
super.setupController(controller, model);
|
||||
// Ensure pulse is stopped if we reach here
|
||||
this.mapUi.stopSearch();
|
||||
}
|
||||
|
||||
@action
|
||||
error() {
|
||||
this.mapUi.stopSearch();
|
||||
return true; // Bubble error
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class MapUiService extends Service {
|
||||
@tracked selectedPlace = null;
|
||||
@tracked isSearching = false;
|
||||
|
||||
selectPlace(place) {
|
||||
this.selectedPlace = place;
|
||||
@ -11,4 +12,12 @@ export default class MapUiService extends Service {
|
||||
clearSelection() {
|
||||
this.selectedPlace = null;
|
||||
}
|
||||
|
||||
startSearch() {
|
||||
this.isSearching = true;
|
||||
}
|
||||
|
||||
stopSearch() {
|
||||
this.isSearching = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,8 +4,18 @@ export default class OsmService extends Service {
|
||||
@service settings;
|
||||
|
||||
controller = null;
|
||||
cachedResults = null;
|
||||
lastQueryKey = null;
|
||||
|
||||
async getNearbyPois(lat, lon, radius = 50) {
|
||||
const queryKey = `${lat},${lon},${radius}`;
|
||||
|
||||
// Return cached results if the query is identical to the last one
|
||||
if (this.lastQueryKey === queryKey && this.cachedResults) {
|
||||
console.log('Returning cached Overpass results for:', queryKey);
|
||||
return this.cachedResults;
|
||||
}
|
||||
|
||||
// Cancel previous request if it exists
|
||||
if (this.controller) {
|
||||
this.controller.abort();
|
||||
@ -33,7 +43,13 @@ out center;
|
||||
const data = await res.json();
|
||||
|
||||
// Normalize data
|
||||
return data.elements.map(this.normalizePoi);
|
||||
const results = data.elements.map(this.normalizePoi);
|
||||
|
||||
// Update cache
|
||||
this.lastQueryKey = queryKey;
|
||||
this.cachedResults = results;
|
||||
|
||||
return results;
|
||||
} catch (e) {
|
||||
if (e.name === 'AbortError') {
|
||||
console.log('Overpass request aborted');
|
||||
|
||||
@ -11,6 +11,7 @@ body {
|
||||
margin: 0;
|
||||
font-family: 'Noto Serif', sans-serif;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#root,
|
||||
@ -96,7 +97,7 @@ body {
|
||||
.user-avatar-placeholder {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #333;
|
||||
background: #2a3743;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -192,7 +193,6 @@ body {
|
||||
bottom: 0;
|
||||
width: 300px;
|
||||
background: white;
|
||||
color: #333;
|
||||
z-index: 3100; /* Higher than Header (3000) */
|
||||
box-shadow: 2px 0 5px rgb(0 0 0 / 10%);
|
||||
display: flex;
|
||||
@ -402,7 +402,6 @@ body {
|
||||
.place-type {
|
||||
color: #666;
|
||||
font-size: 0.85rem;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
@ -436,7 +435,6 @@ body {
|
||||
.place-details .place-type {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
text-transform: capitalize;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
|
||||
@ -1,26 +1,28 @@
|
||||
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 SettingsPane from '#components/settings-pane';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { eq } from 'ember-truth-helpers';
|
||||
import { and, or } from 'ember-truth-helpers';
|
||||
import { or } from 'ember-truth-helpers';
|
||||
import { on } from '@ember/modifier';
|
||||
|
||||
export default class ApplicationComponent extends Component {
|
||||
@service storage;
|
||||
@service mapUi;
|
||||
@service router;
|
||||
|
||||
@tracked nearbyPlaces = null;
|
||||
@tracked isSettingsOpen = false;
|
||||
// @tracked bookmarksVersion = 0; // Moved to storage service
|
||||
|
||||
get isSidebarOpen() {
|
||||
return !!this.nearbyPlaces || this.router.currentRouteName === 'place';
|
||||
// We consider the sidebar "open" if we are in search or place routes.
|
||||
// This helps the map know if it should shift the center or adjust view.
|
||||
return (
|
||||
this.router.currentRouteName === 'place' ||
|
||||
this.router.currentRouteName === 'search'
|
||||
);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
@ -30,32 +32,6 @@ export default class ApplicationComponent extends Component {
|
||||
this.storage;
|
||||
}
|
||||
|
||||
@action
|
||||
showPlaces(places, selectedPlace = null) {
|
||||
// Helper to resolve a place to its bookmark if it exists
|
||||
const resolvePlace = (p) => {
|
||||
if (!p) return null;
|
||||
// We use the OSM ID to check if we already have this place saved
|
||||
const saved = this.storage.findPlaceById(p.osmId);
|
||||
return saved || p;
|
||||
};
|
||||
|
||||
const resolvedSelected = resolvePlace(selectedPlace);
|
||||
const resolvedPlaces = places ? places.map(resolvePlace) : [];
|
||||
|
||||
// If we have a specific place, transition to the route
|
||||
if (resolvedSelected) {
|
||||
// Pass the FULL object model to avoid re-fetching!
|
||||
// The Route's serialize() hook handles URL generation.
|
||||
this.router.transitionTo('place', resolvedSelected);
|
||||
this.nearbyPlaces = null; // Clear list when selecting specific
|
||||
} else if (resolvedPlaces && resolvedPlaces.length > 0) {
|
||||
// Show list case
|
||||
this.nearbyPlaces = resolvedPlaces;
|
||||
this.router.transitionTo('index');
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleSettings() {
|
||||
this.isSettingsOpen = !this.isSettingsOpen;
|
||||
@ -66,29 +42,20 @@ export default class ApplicationComponent extends Component {
|
||||
this.isSettingsOpen = false;
|
||||
}
|
||||
|
||||
@action
|
||||
selectFromList(place) {
|
||||
if (place) {
|
||||
// Optimize: Pass full object to avoid fetch
|
||||
this.router.transitionTo('place', place);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
handleOutsideClick() {
|
||||
if (this.isSettingsOpen) {
|
||||
this.closeSettings();
|
||||
} else {
|
||||
this.closeSidebar();
|
||||
} else if (this.router.currentRouteName === 'search') {
|
||||
this.router.transitionTo('index');
|
||||
} else if (this.router.currentRouteName === 'place') {
|
||||
// If in place route, decide if we want to go back to search or index
|
||||
// For now, let's go to index or maybe back to search if search params exist?
|
||||
// Simplest behavior: clear selection
|
||||
this.router.transitionTo('index');
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
closeSidebar() {
|
||||
this.nearbyPlaces = null;
|
||||
this.router.transitionTo('index');
|
||||
}
|
||||
|
||||
@action
|
||||
refreshBookmarks() {
|
||||
this.storage.notifyChange();
|
||||
@ -113,19 +80,10 @@ export default class ApplicationComponent extends Component {
|
||||
{{/if}}
|
||||
|
||||
<Map
|
||||
@onPlacesFound={{this.showPlaces}}
|
||||
@isSidebarOpen={{or this.isSidebarOpen this.isSettingsOpen}}
|
||||
@onOutsideClick={{this.handleOutsideClick}}
|
||||
/>
|
||||
|
||||
{{#if (and (eq this.router.currentRouteName "index") this.nearbyPlaces)}}
|
||||
<PlacesSidebar
|
||||
@places={{this.nearbyPlaces}}
|
||||
@onSelect={{this.selectFromList}}
|
||||
@onClose={{this.closeSidebar}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.isSettingsOpen}}
|
||||
<SettingsPane @onClose={{this.closeSettings}} />
|
||||
{{/if}}
|
||||
|
||||
@ -7,6 +7,7 @@ import { tracked } from '@glimmer/tracking';
|
||||
export default class PlaceTemplate extends Component {
|
||||
@service router;
|
||||
@service storage;
|
||||
@service mapUi;
|
||||
|
||||
@tracked localPlace = null;
|
||||
|
||||
@ -72,8 +73,26 @@ export default class PlaceTemplate extends Component {
|
||||
this.storage.notifyChange();
|
||||
}
|
||||
|
||||
@action
|
||||
navigateBack(place) {
|
||||
// The sidebar calls this with null when "Back" is clicked.
|
||||
if (place === null) {
|
||||
// If we have history, go back (preserves search state)
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
} else {
|
||||
// Fallback if opened directly
|
||||
this.router.transitionTo('index');
|
||||
}
|
||||
} else {
|
||||
// If a place is selected (unlikely in this view, but possible if we add related links)
|
||||
this.router.transitionTo('place', place);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
close() {
|
||||
// Clear search results so we don't fall back to the list
|
||||
this.router.transitionTo('index');
|
||||
}
|
||||
|
||||
@ -81,6 +100,7 @@ export default class PlaceTemplate extends Component {
|
||||
<PlacesSidebar
|
||||
@selectedPlace={{this.place}}
|
||||
@onClose={{this.close}}
|
||||
@onSelect={{this.navigateBack}}
|
||||
@onBookmarkChange={{this.refreshMap}}
|
||||
@onUpdate={{this.handleUpdate}}
|
||||
/>
|
||||
|
||||
28
app/templates/search.gjs
Normal file
28
app/templates/search.gjs
Normal file
@ -0,0 +1,28 @@
|
||||
import Component from '@glimmer/component';
|
||||
import PlacesSidebar from '#components/places-sidebar';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class SearchTemplate extends Component {
|
||||
@service router;
|
||||
|
||||
@action
|
||||
selectPlace(place) {
|
||||
if (place) {
|
||||
this.router.transitionTo('place', place);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
close() {
|
||||
this.router.transitionTo('index');
|
||||
}
|
||||
|
||||
<template>
|
||||
<PlacesSidebar
|
||||
@places={{@model}}
|
||||
@onSelect={{this.selectPlace}}
|
||||
@onClose={{this.close}}
|
||||
/>
|
||||
</template>
|
||||
}
|
||||
9
app/utils/format-text.js
Normal file
9
app/utils/format-text.js
Normal file
@ -0,0 +1,9 @@
|
||||
export function humanizeOsmTag(text) {
|
||||
if (typeof text !== 'string' || !text) return '';
|
||||
// Replace underscores and dashes with spaces
|
||||
const spaced = text.replace(/[_-]/g, ' ');
|
||||
// Capitalize first letter of each word (Title Case)
|
||||
return spaced.replace(/\w\S*/g, (w) =>
|
||||
w.replace(/^\w/, (c) => c.toUpperCase())
|
||||
);
|
||||
}
|
||||
@ -9,7 +9,7 @@
|
||||
<!-- App identity -->
|
||||
<meta name="application-name" content="Marco">
|
||||
<meta name="apple-mobile-web-app-title" content="Marco">
|
||||
<meta name="theme-color" content="#333333">
|
||||
<meta name="theme-color" content="#2a3743">
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="/web-app-manifest.json">
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "marco",
|
||||
"version": "1.8.10",
|
||||
"version": "1.9.0",
|
||||
"private": true,
|
||||
"description": "Unhosted maps app",
|
||||
"repository": {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#f8f9fa",
|
||||
"theme_color": "#333333",
|
||||
"theme_color": "#2a3743",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192.png",
|
||||
|
||||
1
release/assets/main-BZSIy5va.css
Normal file
1
release/assets/main-BZSIy5va.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
2
release/assets/main-DwYp7tls.js
Normal file
2
release/assets/main-DwYp7tls.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
File diff suppressed because one or more lines are too long
@ -9,7 +9,7 @@
|
||||
<!-- App identity -->
|
||||
<meta name="application-name" content="Marco">
|
||||
<meta name="apple-mobile-web-app-title" content="Marco">
|
||||
<meta name="theme-color" content="#333333">
|
||||
<meta name="theme-color" content="#2a3743">
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="/web-app-manifest.json">
|
||||
@ -26,8 +26,8 @@
|
||||
<meta name="msapplication-TileColor" content="#F6E9A6">
|
||||
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
|
||||
|
||||
<script type="module" crossorigin src="/assets/main-Din37YgL.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-iBIZAPnF.css">
|
||||
<script type="module" crossorigin src="/assets/main-DwYp7tls.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-BZSIy5va.css">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#f8f9fa",
|
||||
"theme_color": "#333333",
|
||||
"theme_color": "#2a3743",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192.png",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user