Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
87e2380ef6
|
|||
| 66c31b19f1 | |||
|
55aecbd699
|
|||
|
ccaa56b78f
|
|||
|
d30375707a
|
|||
|
53300b92f5
|
|||
|
c37f794eea
|
|||
|
4bc92bb7cc
|
|||
|
9f48d7b264
|
|||
| bbd3bf47c6 | |||
|
59e3d91071
|
@@ -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}}
|
||||
|
||||
+21
-3
@@ -68,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 {
|
||||
@@ -82,6 +83,7 @@ export default class MapComponent extends Component {
|
||||
) {
|
||||
center = parsed.center;
|
||||
zoom = parsed.zoom;
|
||||
restoredFromStorage = true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -99,7 +101,7 @@ export default class MapComponent extends Component {
|
||||
layers: [openfreemap, bookmarkLayer],
|
||||
view: view,
|
||||
controls: defaultControls({
|
||||
zoom: false,
|
||||
zoom: true,
|
||||
rotate: true,
|
||||
attribution: true,
|
||||
}),
|
||||
@@ -243,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;
|
||||
|
||||
@@ -307,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();
|
||||
|
||||
@@ -331,7 +335,9 @@ export default class MapComponent extends Component {
|
||||
locateTimeout = setTimeout(() => {
|
||||
stopLocating();
|
||||
}, 10000);
|
||||
});
|
||||
};
|
||||
|
||||
locateBtn.addEventListener('click', startLocating);
|
||||
|
||||
const locateControl = new Control({
|
||||
element: locateElement,
|
||||
@@ -340,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
|
||||
@@ -733,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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "marco",
|
||||
"version": "1.11.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-DlYgnqpR.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