Compare commits
9 Commits
v1.11.1
...
feature/lo
| Author | SHA1 | Date | |
|---|---|---|---|
|
55aecbd699
|
|||
|
ccaa56b78f
|
|||
|
d30375707a
|
|||
|
53300b92f5
|
|||
|
c37f794eea
|
|||
|
4bc92bb7cc
|
|||
|
9f48d7b264
|
|||
| bbd3bf47c6 | |||
|
59e3d91071
|
@@ -24,7 +24,7 @@ export default class AppHeaderComponent extends Component {
|
|||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<button
|
<button
|
||||||
class="icon-btn"
|
class="menu-btn btn-press"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Menu"
|
aria-label="Menu"
|
||||||
{{on "click" @onToggleMenu}}
|
{{on "click" @onToggleMenu}}
|
||||||
@@ -36,7 +36,7 @@ export default class AppHeaderComponent extends Component {
|
|||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="user-menu-container">
|
<div class="user-menu-container">
|
||||||
<button
|
<button
|
||||||
class="user-btn"
|
class="user-btn btn-press"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="User Menu"
|
aria-label="User Menu"
|
||||||
{{on "click" this.toggleUserMenu}}
|
{{on "click" this.toggleUserMenu}}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export default class MapComponent extends Component {
|
|||||||
// Default view settings
|
// Default view settings
|
||||||
let center = [14.21683569, 27.060114248];
|
let center = [14.21683569, 27.060114248];
|
||||||
let zoom = 2.661;
|
let zoom = 2.661;
|
||||||
|
let restoredFromStorage = false;
|
||||||
|
|
||||||
// Try to restore from localStorage
|
// Try to restore from localStorage
|
||||||
try {
|
try {
|
||||||
@@ -82,6 +83,7 @@ export default class MapComponent extends Component {
|
|||||||
) {
|
) {
|
||||||
center = parsed.center;
|
center = parsed.center;
|
||||||
zoom = parsed.zoom;
|
zoom = parsed.zoom;
|
||||||
|
restoredFromStorage = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -99,7 +101,7 @@ export default class MapComponent extends Component {
|
|||||||
layers: [openfreemap, bookmarkLayer],
|
layers: [openfreemap, bookmarkLayer],
|
||||||
view: view,
|
view: view,
|
||||||
controls: defaultControls({
|
controls: defaultControls({
|
||||||
zoom: false,
|
zoom: true,
|
||||||
rotate: true,
|
rotate: true,
|
||||||
attribution: true,
|
attribution: true,
|
||||||
}),
|
}),
|
||||||
@@ -243,6 +245,7 @@ export default class MapComponent extends Component {
|
|||||||
const coordinates = geolocation.getPosition();
|
const coordinates = geolocation.getPosition();
|
||||||
const accuracyGeometry = geolocation.getAccuracyGeometry();
|
const accuracyGeometry = geolocation.getAccuracyGeometry();
|
||||||
const accuracy = geolocation.getAccuracy();
|
const accuracy = geolocation.getAccuracy();
|
||||||
|
console.debug('Geolocation change:', { coordinates, accuracy });
|
||||||
|
|
||||||
if (!coordinates) return;
|
if (!coordinates) return;
|
||||||
|
|
||||||
@@ -307,7 +310,8 @@ export default class MapComponent extends Component {
|
|||||||
this.mapInstance.getView().animate(viewOptions);
|
this.mapInstance.getView().animate(viewOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
locateBtn.addEventListener('click', () => {
|
const startLocating = () => {
|
||||||
|
console.debug('Getting current geolocation...')
|
||||||
// 1. Clear any previous session
|
// 1. Clear any previous session
|
||||||
stopLocating();
|
stopLocating();
|
||||||
|
|
||||||
@@ -331,7 +335,9 @@ export default class MapComponent extends Component {
|
|||||||
locateTimeout = setTimeout(() => {
|
locateTimeout = setTimeout(() => {
|
||||||
stopLocating();
|
stopLocating();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
});
|
};
|
||||||
|
|
||||||
|
locateBtn.addEventListener('click', startLocating);
|
||||||
|
|
||||||
const locateControl = new Control({
|
const locateControl = new Control({
|
||||||
element: locateElement,
|
element: locateElement,
|
||||||
@@ -340,6 +346,11 @@ export default class MapComponent extends Component {
|
|||||||
this.mapInstance.addLayer(geolocationLayer);
|
this.mapInstance.addLayer(geolocationLayer);
|
||||||
this.mapInstance.addControl(locateControl);
|
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);
|
this.mapInstance.on('singleclick', this.handleMapClick);
|
||||||
|
|
||||||
// Load places when map moves
|
// Load places when map moves
|
||||||
@@ -733,6 +744,13 @@ export default class MapComponent extends Component {
|
|||||||
return;
|
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 coords = toLonLat(event.coordinate);
|
||||||
const [lon, lat] = coords;
|
const [lon, lat] = coords;
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ export default class PlaceRoute extends Route {
|
|||||||
deactivate() {
|
deactivate() {
|
||||||
// Clear the pin when leaving the route
|
// Clear the pin when leaving the route
|
||||||
this.mapUi.clearSelection();
|
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) {
|
async loadOsmPlace(id, type = null) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export default class MapUiService extends Service {
|
|||||||
@tracked isSearching = false;
|
@tracked isSearching = false;
|
||||||
@tracked isCreating = false;
|
@tracked isCreating = false;
|
||||||
@tracked creationCoordinates = null;
|
@tracked creationCoordinates = null;
|
||||||
|
@tracked returnToSearch = false;
|
||||||
|
|
||||||
selectPlace(place) {
|
selectPlace(place) {
|
||||||
this.selectedPlace = place;
|
this.selectedPlace = place;
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overscroll-behavior: none; /* Prevent pull-to-refresh on mobile */
|
overscroll-behavior: none; /* Prevent pull-to-refresh on mobile */
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -69,7 +74,15 @@ body {
|
|||||||
pointer-events: auto; /* Re-enable clicks for buttons */
|
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;
|
background: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -80,11 +93,6 @@ body {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 2px 5px rgb(0 0 0 / 20%);
|
box-shadow: 0 2px 5px rgb(0 0 0 / 20%);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-btn {
|
.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 {
|
.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 {
|
.ol-touch .ol-control.ol-locate {
|
||||||
inset: auto 0.5em 3.5em auto;
|
bottom: 8.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rotate Control */
|
/* Rotate Control - Above Locate */
|
||||||
.ol-rotate {
|
.ol-rotate {
|
||||||
inset: auto 0.5em 5em auto;
|
top: auto !important;
|
||||||
|
left: auto !important;
|
||||||
|
bottom: 9em;
|
||||||
|
right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-touch .ol-rotate {
|
.ol-touch .ol-rotate {
|
||||||
inset: auto 0.5em 6em auto;
|
bottom: 11.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.icon {
|
span.icon {
|
||||||
|
|||||||
@@ -77,11 +77,11 @@ export default class PlaceTemplate extends Component {
|
|||||||
navigateBack(place) {
|
navigateBack(place) {
|
||||||
// The sidebar calls this with null when "Back" is clicked.
|
// The sidebar calls this with null when "Back" is clicked.
|
||||||
if (place === null) {
|
if (place === null) {
|
||||||
// If we have history, go back (preserves search state)
|
// If we came from search results, go back in history
|
||||||
if (window.history.length > 1) {
|
if (this.mapUi.returnToSearch) {
|
||||||
window.history.back();
|
window.history.back();
|
||||||
} else {
|
} else {
|
||||||
// Fallback if opened directly
|
// Otherwise just close the sidebar (return to map index)
|
||||||
this.router.transitionTo('index');
|
this.router.transitionTo('index');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { action } from '@ember/object';
|
|||||||
|
|
||||||
export default class SearchTemplate extends Component {
|
export default class SearchTemplate extends Component {
|
||||||
@service router;
|
@service router;
|
||||||
|
@service mapUi;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
selectPlace(place) {
|
selectPlace(place) {
|
||||||
if (place) {
|
if (place) {
|
||||||
|
this.mapUi.returnToSearch = true;
|
||||||
this.router.transitionTo('place', place);
|
this.router.transitionTo('place', place);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "marco",
|
"name": "marco",
|
||||||
"version": "1.11.1",
|
"version": "1.11.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Unhosted maps app",
|
"description": "Unhosted maps app",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
"lint:js:fix": "eslint . --fix",
|
"lint:js:fix": "eslint . --fix",
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"test": "vite build --mode development && testem ci --port 0",
|
"test": "vite build --mode development && testem ci --port 0",
|
||||||
|
"preversion": "pnpm test",
|
||||||
"version": "pnpm build && git add release/"
|
"version": "pnpm build && git add release/"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -26,7 +26,7 @@
|
|||||||
<meta name="msapplication-TileColor" content="#F6E9A6">
|
<meta name="msapplication-TileColor" content="#F6E9A6">
|
||||||
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
|
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
|
||||||
|
|
||||||
<script type="module" crossorigin src="/assets/main-DlYgnqpR.js"></script>
|
<script type="module" crossorigin src="/assets/main-CYFdUlXN.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-D53xPL_H.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-D53xPL_H.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
106
tests/acceptance/navigation-test.js
Normal file
106
tests/acceptance/navigation-test.js
Normal file
@@ -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