Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
64ccc694d3
|
|||
|
87e2380ef6
|
|||
| 66c31b19f1 | |||
|
55aecbd699
|
|||
|
ccaa56b78f
|
|||
|
d30375707a
|
|||
|
53300b92f5
|
|||
|
c37f794eea
|
|||
|
4bc92bb7cc
|
|||
|
9f48d7b264
|
|||
| bbd3bf47c6 | |||
|
59e3d91071
|
|||
|
348b721876
|
|||
|
3d982a6a7c
|
|||
|
0af9d9f16d
|
|||
|
a0f132ec64
|
|||
|
925f26ae5d
|
|||
|
58bb8831f3
|
+17
-6
@@ -92,6 +92,17 @@ We are building **Marco**, a decentralized maps application using **Ember.js** (
|
||||
- **Smart Linking:** The `showPlaces` action intercepts search results and automatically resolves them to existing **Bookmarks** if a match is found (via `storage.findPlaceById`). This ensures the app navigates to the persistent Bookmark URL (ULID) and correctly reflects the "Saved" status in the UI instead of treating it as a new generic OSM place.
|
||||
- **Data Normalization:** Refactored `OsmService` to return normalized objects (`osmTags`, `osmType`) for all queries. This ensures consistent data structures between fresh Overpass results and saved bookmarks throughout the app.
|
||||
|
||||
### 5. Creation & Editing Workflow
|
||||
|
||||
- **Create Place:**
|
||||
- Implemented `/place/new` route for creating new private places.
|
||||
- **UX:** Map displays a central crosshair for precise location selection.
|
||||
- **Mobile Optimization:**
|
||||
- Disabled map inertia (`kinetic: false`) to ensure the map stops exactly where the finger releases.
|
||||
- `PlaceEditForm` conditionally disables autofocus on mobile screens (`<= 768px`) to prevent the onscreen keyboard from obscuring the map view immediately.
|
||||
- Responsive crosshair sizing (48px desktop / 24px mobile).
|
||||
- **Persistence:** Form data (Title, Description) and Map coordinates are securely saved to RemoteStorage via `storage.storePlace`.
|
||||
|
||||
## Current State
|
||||
|
||||
- **Repo:** The app runs via `pnpm start`.
|
||||
@@ -102,20 +113,20 @@ We are building **Marco**, a decentralized maps application using **Ember.js** (
|
||||
- If direct match: Redirect to `/place/:id`.
|
||||
- If multiple results: Show `/search` list view.
|
||||
4. Sidebar displays details via `<PlaceDetails>` component (Bottom sheet on mobile).
|
||||
5. User clicks "Save Bookmark" -> Stores JSON in RemoteStorage.
|
||||
6. RemoteStorage change event -> Debounced reload updates the map reactive-ly.
|
||||
5. **Creation:** User clicks "Create Place" -> Enters creation mode (crosshair) -> Positions map -> Enters details -> Save.
|
||||
6. **Persistence:** RemoteStorage change event -> Debounced reload updates the map reactive-ly.
|
||||
7. **Editing:** User can edit the Title and Description of saved bookmarks via an "Edit" button in the details view.
|
||||
8. **Settings:** User can change the Overpass API provider via the new Settings menu.
|
||||
|
||||
## Files Currently in Focus
|
||||
|
||||
- `app/services/osm.js`: Caching logic.
|
||||
- `app/routes/search.js`: Search heuristics.
|
||||
- `app/components/place-details.gjs`: Formatting logic.
|
||||
- `app/components/map.gjs`
|
||||
- `app/components/place-edit-form.gjs`
|
||||
- `app/templates/place/new.gjs`
|
||||
|
||||
## Next Steps & Pending Tasks
|
||||
|
||||
1. **Linting & Code Quality:** Fix remaining CSS errors, remove inline styles in `map.gjs`, and address unused variables/runloop usage.
|
||||
1. **Linting & Code Quality:** Fix remaining CSS errors and address unused variables/runloop usage.
|
||||
2. **Testing:** Add automated tests for the geohash coverage, retry logic, and new editing features.
|
||||
3. **Performance:** Monitor performance with large datasets (thousands of bookmarks).
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export default class AppHeaderComponent extends Component {
|
||||
<header class="app-header">
|
||||
<div class="header-left">
|
||||
<button
|
||||
class="icon-btn"
|
||||
class="menu-btn btn-press"
|
||||
type="button"
|
||||
aria-label="Menu"
|
||||
{{on "click" @onToggleMenu}}
|
||||
@@ -36,7 +36,7 @@ export default class AppHeaderComponent extends Component {
|
||||
<div class="header-right">
|
||||
<div class="user-menu-container">
|
||||
<button
|
||||
class="user-btn"
|
||||
class="user-btn btn-press"
|
||||
type="button"
|
||||
aria-label="User Menu"
|
||||
{{on "click" this.toggleUserMenu}}
|
||||
|
||||
+60
-3
@@ -4,6 +4,8 @@ import { modifier } from 'ember-modifier';
|
||||
import 'ol/ol.css';
|
||||
import Map from 'ol/Map.js';
|
||||
import { defaults as defaultControls, Control } from 'ol/control.js';
|
||||
import { defaults as defaultInteractions, DragPan } from 'ol/interaction.js';
|
||||
import Kinetic from 'ol/Kinetic.js';
|
||||
import View from 'ol/View.js';
|
||||
import { fromLonLat, toLonLat, getPointResolution } from 'ol/proj.js';
|
||||
import Overlay from 'ol/Overlay.js';
|
||||
@@ -21,6 +23,7 @@ export default class MapComponent extends Component {
|
||||
@service storage;
|
||||
@service mapUi;
|
||||
@service router;
|
||||
@service settings;
|
||||
|
||||
mapInstance;
|
||||
bookmarkSource;
|
||||
@@ -65,6 +68,7 @@ export default class MapComponent extends Component {
|
||||
// Default view settings
|
||||
let center = [14.21683569, 27.060114248];
|
||||
let zoom = 2.661;
|
||||
let restoredFromStorage = false;
|
||||
|
||||
// Try to restore from localStorage
|
||||
try {
|
||||
@@ -79,6 +83,7 @@ export default class MapComponent extends Component {
|
||||
) {
|
||||
center = parsed.center;
|
||||
zoom = parsed.zoom;
|
||||
restoredFromStorage = true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -96,10 +101,13 @@ export default class MapComponent extends Component {
|
||||
layers: [openfreemap, bookmarkLayer],
|
||||
view: view,
|
||||
controls: defaultControls({
|
||||
zoom: false,
|
||||
zoom: true,
|
||||
rotate: true,
|
||||
attribution: true,
|
||||
}),
|
||||
interactions: defaultInteractions({
|
||||
dragPan: false, // Disable default DragPan to add a custom one
|
||||
}),
|
||||
});
|
||||
|
||||
apply(this.mapInstance, 'https://tiles.openfreemap.org/styles/liberty');
|
||||
@@ -237,6 +245,7 @@ export default class MapComponent extends Component {
|
||||
const coordinates = geolocation.getPosition();
|
||||
const accuracyGeometry = geolocation.getAccuracyGeometry();
|
||||
const accuracy = geolocation.getAccuracy();
|
||||
console.debug('Geolocation change:', { coordinates, accuracy });
|
||||
|
||||
if (!coordinates) return;
|
||||
|
||||
@@ -301,7 +310,8 @@ export default class MapComponent extends Component {
|
||||
this.mapInstance.getView().animate(viewOptions);
|
||||
};
|
||||
|
||||
locateBtn.addEventListener('click', () => {
|
||||
const startLocating = () => {
|
||||
console.debug('Getting current geolocation...')
|
||||
// 1. Clear any previous session
|
||||
stopLocating();
|
||||
|
||||
@@ -325,7 +335,9 @@ export default class MapComponent extends Component {
|
||||
locateTimeout = setTimeout(() => {
|
||||
stopLocating();
|
||||
}, 10000);
|
||||
});
|
||||
};
|
||||
|
||||
locateBtn.addEventListener('click', startLocating);
|
||||
|
||||
const locateControl = new Control({
|
||||
element: locateElement,
|
||||
@@ -334,6 +346,11 @@ export default class MapComponent extends Component {
|
||||
this.mapInstance.addLayer(geolocationLayer);
|
||||
this.mapInstance.addControl(locateControl);
|
||||
|
||||
// Auto-locate on first visit (if not restored from storage and on home page)
|
||||
if (!restoredFromStorage && this.router.currentRouteName === 'index') {
|
||||
startLocating();
|
||||
}
|
||||
|
||||
this.mapInstance.on('singleclick', this.handleMapClick);
|
||||
|
||||
// Load places when map moves
|
||||
@@ -353,6 +370,38 @@ export default class MapComponent extends Component {
|
||||
});
|
||||
});
|
||||
|
||||
updateInteractions = modifier(() => {
|
||||
if (!this.mapInstance) return;
|
||||
|
||||
// Remove existing DragPan interactions
|
||||
this.mapInstance.getInteractions().getArray().slice().forEach((interaction) => {
|
||||
if (interaction instanceof DragPan) {
|
||||
this.mapInstance.removeInteraction(interaction);
|
||||
}
|
||||
});
|
||||
|
||||
// Add new DragPan with current setting
|
||||
const kinetic = this.settings.mapKinetic
|
||||
? new Kinetic(-0.005, 0.05, 100)
|
||||
: false;
|
||||
|
||||
// Fix for "sticky" touches on mobile:
|
||||
// If we're on mobile (width <= 768) AND using kinetic,
|
||||
// we increase the minimum velocity required to trigger kinetic panning.
|
||||
// This prevents slow drags from being interpreted as a "throw"
|
||||
if (this.settings.mapKinetic && window.innerWidth <= 768) {
|
||||
// Default minVelocity is 0.05. We bump it up significantly.
|
||||
// This means the user has to really "flick" the map to get inertia.
|
||||
kinetic.minVelocity_ = 0.25;
|
||||
}
|
||||
|
||||
this.mapInstance.addInteraction(
|
||||
new DragPan({
|
||||
kinetic: kinetic,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Track the selected place from the UI Service (Router -> Map)
|
||||
updateSelectedPin = modifier(() => {
|
||||
const selected = this.mapUi.selectedPlace;
|
||||
@@ -695,6 +744,13 @@ export default class MapComponent extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
// Require Zoom >= 17 for generic map searches
|
||||
// This prevents accidental searches when interacting with the map at a high level
|
||||
const currentZoom = this.mapInstance.getView().getZoom();
|
||||
if (currentZoom < 16) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coords = toLonLat(event.coordinate);
|
||||
const [lon, lat] = coords;
|
||||
|
||||
@@ -736,6 +792,7 @@ export default class MapComponent extends Component {
|
||||
<div
|
||||
class="map-container {{if @isSidebarOpen 'sidebar-open'}}"
|
||||
{{this.setupMap}}
|
||||
{{this.updateInteractions}}
|
||||
{{this.updateBookmarks}}
|
||||
{{this.updateSelectedPin}}
|
||||
{{this.syncPulse}}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Component from '@glimmer/component';
|
||||
import { fn } from '@ember/helper';
|
||||
import { on } from '@ember/modifier';
|
||||
import { humanizeOsmTag } from '../utils/format-text';
|
||||
import { getLocalizedName } from '../utils/osm';
|
||||
import Icon from '../components/icon';
|
||||
import PlaceEditForm from './place-edit-form';
|
||||
|
||||
@@ -22,8 +23,7 @@ export default class PlaceDetails extends Component {
|
||||
get name() {
|
||||
return (
|
||||
this.place.title ||
|
||||
this.tags.name ||
|
||||
this.tags['name:en'] ||
|
||||
getLocalizedName(this.tags) ||
|
||||
'Unnamed Place'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,13 @@ export default class PlaceEditForm extends Component {
|
||||
this.description = this.args.place?.description || '';
|
||||
}
|
||||
|
||||
get shouldAutofocus() {
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.innerWidth > 768;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@action
|
||||
handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
@@ -45,7 +52,7 @@ export default class PlaceEditForm extends Component {
|
||||
{{on "input" this.updateTitle}}
|
||||
class="form-control"
|
||||
placeholder="Name of the place"
|
||||
autofocus
|
||||
autofocus={{this.shouldAutofocus}}
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -7,6 +7,7 @@ import or from 'ember-truth-helpers/helpers/or';
|
||||
import PlaceDetails from './place-details';
|
||||
import Icon from './icon';
|
||||
import humanizeOsmTag from '../helpers/humanize-osm-tag';
|
||||
import { getLocalizedName } from '../utils/osm';
|
||||
|
||||
export default class PlacesSidebar extends Component {
|
||||
@service storage;
|
||||
@@ -85,8 +86,7 @@ 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: getLocalizedName(place.osmTags, 'Untitled Place'),
|
||||
lat: place.lat,
|
||||
lon: place.lon,
|
||||
tags: [],
|
||||
|
||||
@@ -4,6 +4,7 @@ import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import Icon from '#components/icon';
|
||||
import eq from 'ember-truth-helpers/helpers/eq';
|
||||
import not from 'ember-truth-helpers/helpers/not';
|
||||
|
||||
export default class SettingsPane extends Component {
|
||||
@service settings;
|
||||
@@ -13,6 +14,11 @@ export default class SettingsPane extends Component {
|
||||
this.settings.updateOverpassApi(event.target.value);
|
||||
}
|
||||
|
||||
@action
|
||||
toggleKinetic(event) {
|
||||
this.settings.updateMapKinetic(event.target.value === 'true');
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="sidebar settings-pane">
|
||||
<div class="sidebar-header">
|
||||
@@ -25,6 +31,27 @@ export default class SettingsPane extends Component {
|
||||
<div class="sidebar-content">
|
||||
<section class="settings-section">
|
||||
<h3>Settings</h3>
|
||||
<div class="form-group">
|
||||
<label for="map-kinetic">Map Inertia (Kinetic Panning)</label>
|
||||
<select
|
||||
id="map-kinetic"
|
||||
class="form-control"
|
||||
{{on "change" this.toggleKinetic}}
|
||||
>
|
||||
<option
|
||||
value="true"
|
||||
selected={{if this.settings.mapKinetic "selected"}}
|
||||
>
|
||||
On
|
||||
</option>
|
||||
<option
|
||||
value="false"
|
||||
selected={{if (not this.settings.mapKinetic) "selected"}}
|
||||
>
|
||||
Off
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="overpass-api">Overpass API Provider</label>
|
||||
<select
|
||||
|
||||
@@ -56,6 +56,8 @@ export default class PlaceRoute extends Route {
|
||||
deactivate() {
|
||||
// Clear the pin when leaving the route
|
||||
this.mapUi.clearSelection();
|
||||
// Reset the "return to search" flag so it doesn't persist to subsequent navigations
|
||||
this.mapUi.returnToSearch = false;
|
||||
}
|
||||
|
||||
async loadOsmPlace(id, type = null) {
|
||||
|
||||
@@ -6,6 +6,7 @@ export default class MapUiService extends Service {
|
||||
@tracked isSearching = false;
|
||||
@tracked isCreating = false;
|
||||
@tracked creationCoordinates = null;
|
||||
@tracked returnToSearch = false;
|
||||
|
||||
selectPlace(place) {
|
||||
this.selectedPlace = place;
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
import Service, { service } from '@ember/service';
|
||||
import { getLocalizedName } from '../utils/osm';
|
||||
|
||||
export default class OsmService extends Service {
|
||||
@service settings;
|
||||
@@ -61,7 +62,7 @@ out center;
|
||||
|
||||
normalizePoi(poi) {
|
||||
return {
|
||||
title: poi.tags?.name || poi.tags?.['name:en'] || 'Untitled Place',
|
||||
title: getLocalizedName(poi.tags),
|
||||
lat: poi.lat || poi.center?.lat,
|
||||
lon: poi.lon || poi.center?.lon,
|
||||
url: poi.tags?.website,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class SettingsService extends Service {
|
||||
@tracked overpassApi = 'https://overpass.bke.ro/api/interpreter';
|
||||
@tracked mapKinetic = true;
|
||||
|
||||
overpassApis = [
|
||||
{ name: 'bke.ro', url: 'https://overpass.bke.ro/api/interpreter' },
|
||||
@@ -19,14 +20,25 @@ export default class SettingsService extends Service {
|
||||
}
|
||||
|
||||
loadSettings() {
|
||||
const savedApi = localStorage.getItem('marco-overpass-api');
|
||||
const savedApi = localStorage.getItem('marco:overpass-api');
|
||||
if (savedApi) {
|
||||
this.overpassApi = savedApi;
|
||||
}
|
||||
|
||||
const savedKinetic = localStorage.getItem('marco:map-kinetic');
|
||||
if (savedKinetic !== null) {
|
||||
this.mapKinetic = savedKinetic === 'true';
|
||||
}
|
||||
// Default is true (initialized in class field)
|
||||
}
|
||||
|
||||
updateOverpassApi(url) {
|
||||
this.overpassApi = url;
|
||||
localStorage.setItem('marco-overpass-api', url);
|
||||
localStorage.setItem('marco:overpass-api', url);
|
||||
}
|
||||
|
||||
updateMapKinetic(enabled) {
|
||||
this.mapKinetic = enabled;
|
||||
localStorage.setItem('marco:map-kinetic', String(enabled));
|
||||
}
|
||||
}
|
||||
|
||||
+38
-12
@@ -5,6 +5,11 @@ body {
|
||||
height: 100%;
|
||||
overscroll-behavior: none; /* Prevent pull-to-refresh on mobile */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
button {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -69,7 +74,15 @@ body {
|
||||
pointer-events: auto; /* Re-enable clicks for buttons */
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
.btn-press {
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
.btn-press:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.menu-btn {
|
||||
background: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
@@ -80,11 +93,6 @@ body {
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 5px rgb(0 0 0 / 20%);
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
.icon-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.user-btn {
|
||||
@@ -539,22 +547,40 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* Locate Control */
|
||||
/* Zoom Control - Moved to bottom right above attribution */
|
||||
.ol-zoom {
|
||||
top: auto !important;
|
||||
left: auto !important;
|
||||
bottom: 2.5em;
|
||||
right: 0.5em;
|
||||
}
|
||||
|
||||
.ol-touch .ol-zoom {
|
||||
bottom: 3.5em;
|
||||
}
|
||||
|
||||
/* Locate Control - Above Zoom */
|
||||
.ol-control.ol-locate {
|
||||
inset: auto 0.5em 2.5em auto;
|
||||
top: auto !important;
|
||||
left: auto !important;
|
||||
bottom: 6.5em;
|
||||
right: 0.5em;
|
||||
}
|
||||
|
||||
.ol-touch .ol-control.ol-locate {
|
||||
inset: auto 0.5em 3.5em auto;
|
||||
bottom: 8.5em;
|
||||
}
|
||||
|
||||
/* Rotate Control */
|
||||
/* Rotate Control - Above Locate */
|
||||
.ol-rotate {
|
||||
inset: auto 0.5em 5em auto;
|
||||
top: auto !important;
|
||||
left: auto !important;
|
||||
bottom: 9em;
|
||||
right: 0.5em;
|
||||
}
|
||||
|
||||
.ol-touch .ol-rotate {
|
||||
inset: auto 0.5em 6em auto;
|
||||
bottom: 11.5em;
|
||||
}
|
||||
|
||||
span.icon {
|
||||
|
||||
@@ -77,11 +77,11 @@ export default class PlaceTemplate extends Component {
|
||||
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) {
|
||||
// If we came from search results, go back in history
|
||||
if (this.mapUi.returnToSearch) {
|
||||
window.history.back();
|
||||
} else {
|
||||
// Fallback if opened directly
|
||||
// Otherwise just close the sidebar (return to map index)
|
||||
this.router.transitionTo('index');
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -5,10 +5,12 @@ import { action } from '@ember/object';
|
||||
|
||||
export default class SearchTemplate extends Component {
|
||||
@service router;
|
||||
@service mapUi;
|
||||
|
||||
@action
|
||||
selectPlace(place) {
|
||||
if (place) {
|
||||
this.mapUi.returnToSearch = true;
|
||||
this.router.transitionTo('place', place);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
export function getLocalizedName(tags, defaultName = 'Untitled Place') {
|
||||
if (!tags) return defaultName;
|
||||
|
||||
// 1. Get user's preferred languages
|
||||
const languages = navigator.languages || [navigator.language || 'en'];
|
||||
|
||||
// 2. Try to find a match for each preferred language
|
||||
for (const lang of languages) {
|
||||
if (!lang) continue;
|
||||
|
||||
// Handle "en-US", "de-DE", etc. -> look for "name:en", "name:de"
|
||||
const shortLang = lang.split('-')[0];
|
||||
const tagKey = `name:${shortLang}`;
|
||||
|
||||
if (tags[tagKey]) {
|
||||
return tags[tagKey];
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback to standard "name"
|
||||
if (tags.name) {
|
||||
return tags.name;
|
||||
}
|
||||
|
||||
// 4. Fallback to "name:en" (common in international places without local name)
|
||||
if (tags['name:en']) {
|
||||
return tags['name:en'];
|
||||
}
|
||||
|
||||
// 5. Final fallback
|
||||
return defaultName;
|
||||
}
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "marco",
|
||||
"version": "1.10.1",
|
||||
"version": "1.11.3",
|
||||
"private": true,
|
||||
"description": "Unhosted maps app",
|
||||
"repository": {
|
||||
@@ -34,6 +34,7 @@
|
||||
"lint:js:fix": "eslint . --fix",
|
||||
"start": "vite",
|
||||
"test": "vite build --mode development && testem ci --port 0",
|
||||
"preversion": "pnpm test",
|
||||
"version": "pnpm build && git add release/"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
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
+2
-2
@@ -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-Dep3TjPE.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-D53xPL_H.css">
|
||||
<script type="module" crossorigin src="/assets/main-DD9l9xzQ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-G8wPYi_P.css">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL, click, settled } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'marco/tests/helpers';
|
||||
import Service from '@ember/service';
|
||||
import sinon from 'sinon';
|
||||
|
||||
class MockOsmService extends Service {
|
||||
async getNearbyPois() {
|
||||
return [
|
||||
{
|
||||
osmId: '123',
|
||||
lat: 1,
|
||||
lon: 1,
|
||||
osmTags: { name: 'Test Place', amenity: 'cafe' },
|
||||
osmType: 'node',
|
||||
},
|
||||
];
|
||||
}
|
||||
async getPoiById() {
|
||||
return {
|
||||
osmId: '123',
|
||||
lat: 1,
|
||||
lon: 1,
|
||||
osmTags: { name: 'Test Place', amenity: 'cafe' },
|
||||
osmType: 'node',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MockStorageService extends Service {
|
||||
savedPlaces = [];
|
||||
findPlaceById() {
|
||||
return null;
|
||||
}
|
||||
loadPlacesInBounds() {
|
||||
return [];
|
||||
}
|
||||
get placesInView() {
|
||||
return [];
|
||||
}
|
||||
rs = {
|
||||
on: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
module('Acceptance | navigation', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.owner.register('service:osm', MockOsmService);
|
||||
this.owner.register('service:storage', MockStorageService);
|
||||
});
|
||||
|
||||
test('navigating from search results to place and back uses history', async function (assert) {
|
||||
const mapUi = this.owner.lookup('service:map-ui');
|
||||
const backStub = sinon.stub(window.history, 'back');
|
||||
|
||||
try {
|
||||
await visit('/search?lat=1&lon=1');
|
||||
assert.strictEqual(currentURL(), '/search?lat=1&lon=1');
|
||||
|
||||
await click('.place-item');
|
||||
assert.ok(currentURL().includes('/place/'), 'Navigated to place');
|
||||
assert.true(mapUi.returnToSearch, 'Flag returnToSearch is set');
|
||||
|
||||
// Click the back button in the sidebar
|
||||
await click('.back-btn');
|
||||
|
||||
assert.true(backStub.calledOnce, 'window.history.back() was called');
|
||||
} finally {
|
||||
backStub.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('closing the sidebar resets the returnToSearch flag', async function (assert) {
|
||||
const mapUi = this.owner.lookup('service:map-ui');
|
||||
|
||||
await visit('/search?lat=1&lon=1');
|
||||
await click('.place-item'); // Sets returnToSearch = true
|
||||
|
||||
assert.true(mapUi.returnToSearch, 'Flag is set upon entering place');
|
||||
|
||||
// Click the Close (X) button
|
||||
await click('.close-btn');
|
||||
await settled();
|
||||
|
||||
assert.strictEqual(currentURL(), '/', 'Returned to index');
|
||||
assert.false(mapUi.returnToSearch, 'Flag is reset after closing sidebar');
|
||||
});
|
||||
|
||||
test('navigating directly to place and back closes sidebar', async function (assert) {
|
||||
const backStub = sinon.stub(window.history, 'back');
|
||||
try {
|
||||
await visit('/place/osm:node:123');
|
||||
assert.ok(currentURL().includes('/place/'), 'Visited place directly');
|
||||
|
||||
await click('.back-btn');
|
||||
await settled();
|
||||
|
||||
assert.strictEqual(currentURL(), '/', 'Returned to index/map');
|
||||
assert.true(backStub.notCalled, 'window.history.back() was NOT called');
|
||||
} finally {
|
||||
backStub.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user