Compare commits
7 Commits
d72e5f3de2
...
v1.17.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
2b219fe0cf
|
|||
|
9fd6c4d64d
|
|||
|
8e5b2c7439
|
|||
|
0f29430e1a
|
|||
|
0059d89cc3
|
|||
|
54e2766dc4
|
|||
|
5978f67d48
|
@@ -10,6 +10,7 @@ import CategoryChips from '#components/category-chips';
|
|||||||
|
|
||||||
export default class AppHeaderComponent extends Component {
|
export default class AppHeaderComponent extends Component {
|
||||||
@service storage;
|
@service storage;
|
||||||
|
@service settings;
|
||||||
@tracked isUserMenuOpen = false;
|
@tracked isUserMenuOpen = false;
|
||||||
@tracked searchQuery = '';
|
@tracked searchQuery = '';
|
||||||
|
|
||||||
@@ -49,9 +50,11 @@ export default class AppHeaderComponent extends Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-center {{if this.hasQuery 'searching'}}">
|
{{#if this.settings.showQuickSearchButtons}}
|
||||||
<CategoryChips @onSelect={{this.handleChipSelect}} />
|
<div class="header-center {{if this.hasQuery 'searching'}}">
|
||||||
</div>
|
<CategoryChips @onSelect={{this.handleChipSelect}} />
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="user-menu-container">
|
<div class="user-menu-container">
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export default class AppMenuSettings extends Component {
|
|||||||
this.settings.updateMapKinetic(event.target.value === 'true');
|
this.settings.updateMapKinetic(event.target.value === 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleQuickSearchButtons(event) {
|
||||||
|
this.settings.updateShowQuickSearchButtons(event.target.value === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updatePhotonApi(event) {
|
updatePhotonApi(event) {
|
||||||
this.settings.updatePhotonApi(event.target.value);
|
this.settings.updatePhotonApi(event.target.value);
|
||||||
@@ -36,6 +41,30 @@ export default class AppMenuSettings extends Component {
|
|||||||
|
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="show-quick-search">Quick search buttons visible</label>
|
||||||
|
<select
|
||||||
|
id="show-quick-search"
|
||||||
|
class="form-control"
|
||||||
|
{{on "change" this.toggleQuickSearchButtons}}
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="true"
|
||||||
|
selected={{if this.settings.showQuickSearchButtons "selected"}}
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="false"
|
||||||
|
selected={{unless
|
||||||
|
this.settings.showQuickSearchButtons
|
||||||
|
"selected"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
No
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="map-kinetic">Map Inertia (Kinetic Panning)</label>
|
<label for="map-kinetic">Map Inertia (Kinetic Panning)</label>
|
||||||
<select
|
<select
|
||||||
|
|||||||
14
app/components/toast.gjs
Normal file
14
app/components/toast.gjs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { service } from '@ember/service';
|
||||||
|
|
||||||
|
export default class ToastComponent extends Component {
|
||||||
|
@service toast;
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{#if this.toast.isVisible}}
|
||||||
|
<div class="toast-notification">
|
||||||
|
{{this.toast.message}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</template>
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ export default class SearchRoute extends Route {
|
|||||||
@service mapUi;
|
@service mapUi;
|
||||||
@service storage;
|
@service storage;
|
||||||
@service router;
|
@service router;
|
||||||
|
@service toast;
|
||||||
|
|
||||||
queryParams = {
|
queryParams = {
|
||||||
lat: { refreshModel: true },
|
lat: { refreshModel: true },
|
||||||
@@ -199,8 +200,12 @@ export default class SearchRoute extends Route {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
error() {
|
error(error, transition) {
|
||||||
this.mapUi.stopSearch();
|
this.mapUi.stopSearch();
|
||||||
return true; // Bubble error
|
this.toast.show('Search request failed. Please try again.');
|
||||||
|
if (transition) {
|
||||||
|
transition.abort();
|
||||||
|
}
|
||||||
|
return false; // Prevent bubble and stop transition
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ out center;
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
console.error('Category search failed', e);
|
console.error('Category search failed', e);
|
||||||
return [];
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export default class SettingsService extends Service {
|
|||||||
@tracked overpassApi = 'https://overpass-api.de/api/interpreter';
|
@tracked overpassApi = 'https://overpass-api.de/api/interpreter';
|
||||||
@tracked mapKinetic = true;
|
@tracked mapKinetic = true;
|
||||||
@tracked photonApi = 'https://photon.komoot.io/api/';
|
@tracked photonApi = 'https://photon.komoot.io/api/';
|
||||||
|
@tracked showQuickSearchButtons = true;
|
||||||
|
|
||||||
overpassApis = [
|
overpassApis = [
|
||||||
{
|
{
|
||||||
@@ -56,6 +57,13 @@ export default class SettingsService extends Service {
|
|||||||
this.mapKinetic = savedKinetic === 'true';
|
this.mapKinetic = savedKinetic === 'true';
|
||||||
}
|
}
|
||||||
// Default is true (initialized in class field)
|
// Default is true (initialized in class field)
|
||||||
|
|
||||||
|
const savedShowQuickSearch = localStorage.getItem(
|
||||||
|
'marco:show-quick-search'
|
||||||
|
);
|
||||||
|
if (savedShowQuickSearch !== null) {
|
||||||
|
this.showQuickSearchButtons = savedShowQuickSearch === 'true';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOverpassApi(url) {
|
updateOverpassApi(url) {
|
||||||
@@ -68,6 +76,11 @@ export default class SettingsService extends Service {
|
|||||||
localStorage.setItem('marco:map-kinetic', String(enabled));
|
localStorage.setItem('marco:map-kinetic', String(enabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateShowQuickSearchButtons(enabled) {
|
||||||
|
this.showQuickSearchButtons = enabled;
|
||||||
|
localStorage.setItem('marco:show-quick-search', String(enabled));
|
||||||
|
}
|
||||||
|
|
||||||
updatePhotonApi(url) {
|
updatePhotonApi(url) {
|
||||||
this.photonApi = url;
|
this.photonApi = url;
|
||||||
}
|
}
|
||||||
|
|||||||
21
app/services/toast.js
Normal file
21
app/services/toast.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import Service from '@ember/service';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
|
||||||
|
export default class ToastService extends Service {
|
||||||
|
@tracked message = null;
|
||||||
|
@tracked isVisible = false;
|
||||||
|
timeoutId = null;
|
||||||
|
|
||||||
|
show(message, duration = 3000) {
|
||||||
|
this.message = message;
|
||||||
|
this.isVisible = true;
|
||||||
|
|
||||||
|
if (this.timeoutId) {
|
||||||
|
clearTimeout(this.timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeoutId = setTimeout(() => {
|
||||||
|
this.isVisible = false;
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1321,3 +1321,36 @@ button.create-place {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Toast Notification */
|
||||||
|
.toast-notification {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 2rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: rgb(51 51 51 / 85%);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
color: white;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
z-index: 9999;
|
||||||
|
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
|
||||||
|
animation: fade-in-up 0.3s ease-out forwards;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 90%;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { pageTitle } from 'ember-page-title';
|
|||||||
import Map from '#components/map';
|
import Map from '#components/map';
|
||||||
import AppHeader from '#components/app-header';
|
import AppHeader from '#components/app-header';
|
||||||
import AppMenu from '#components/app-menu/index';
|
import AppMenu from '#components/app-menu/index';
|
||||||
|
import Toast from '#components/toast';
|
||||||
import { service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
@@ -89,6 +90,8 @@ export default class ApplicationComponent extends Component {
|
|||||||
<AppMenu @onClose={{this.closeAppMenu}} />
|
<AppMenu @onClose={{this.closeAppMenu}} />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
<Toast />
|
||||||
|
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "marco",
|
"name": "marco",
|
||||||
"version": "1.16.0",
|
"version": "1.17.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Unhosted maps app",
|
"description": "Unhosted maps app",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
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-DM7YMuyX.js
Normal file
2
release/assets/main-DM7YMuyX.js
Normal file
File diff suppressed because one or more lines are too long
1
release/assets/main-OLSOzTKA.css
Normal file
1
release/assets/main-OLSOzTKA.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
@@ -39,8 +39,8 @@
|
|||||||
<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-C4F17h3W.js"></script>
|
<script type="module" crossorigin src="/assets/main-DM7YMuyX.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-CKp1bFPU.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-OLSOzTKA.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -218,4 +218,56 @@ module('Acceptance | search', function (hooks) {
|
|||||||
// Ensure it shows "Results" not "Nearby"
|
// Ensure it shows "Results" not "Nearby"
|
||||||
assert.dom('.sidebar-header h2').includesText('Results');
|
assert.dom('.sidebar-header h2').includesText('Results');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('search error handling prevents opening empty panel and shows toast', async function (assert) {
|
||||||
|
// Mock Osm Service to throw an error
|
||||||
|
class MockOsmService extends Service {
|
||||||
|
async getCategoryPois() {
|
||||||
|
throw new Error('Overpass request failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.owner.register('service:osm', MockOsmService);
|
||||||
|
|
||||||
|
class MockStorageService extends Service {
|
||||||
|
savedPlaces = [];
|
||||||
|
findPlaceById() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
isPlaceSaved() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rs = { on: () => {} };
|
||||||
|
placesInView = [];
|
||||||
|
loadPlacesInBounds() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.owner.register('service:storage', MockStorageService);
|
||||||
|
|
||||||
|
class MockMapService extends Service {
|
||||||
|
getBounds() {
|
||||||
|
return {
|
||||||
|
minLat: 52.5,
|
||||||
|
minLon: 13.4,
|
||||||
|
maxLat: 52.6,
|
||||||
|
maxLon: 13.5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.owner.register('service:map', MockMapService);
|
||||||
|
|
||||||
|
await visit('/');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await visit('/search?category=coffee&lat=52.52&lon=13.405');
|
||||||
|
} catch {
|
||||||
|
// Aborted transition throws, which is expected
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.dom('.toast-notification').exists('Toast should be visible');
|
||||||
|
assert
|
||||||
|
.dom('.toast-notification')
|
||||||
|
.hasText('Search request failed. Please try again.');
|
||||||
|
assert.dom('.places-sidebar').doesNotExist('Results panel should not open');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,11 +14,15 @@ module('Integration | Component | app-header', function (hooks) {
|
|||||||
class MockRouterService extends Service {}
|
class MockRouterService extends Service {}
|
||||||
class MockMapUiService extends Service {}
|
class MockMapUiService extends Service {}
|
||||||
class MockMapService extends Service {}
|
class MockMapService extends Service {}
|
||||||
|
class MockSettingsService extends Service {
|
||||||
|
showQuickSearchButtons = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.owner.register('service:photon', MockPhotonService);
|
this.owner.register('service:photon', MockPhotonService);
|
||||||
this.owner.register('service:router', MockRouterService);
|
this.owner.register('service:router', MockRouterService);
|
||||||
this.owner.register('service:map-ui', MockMapUiService);
|
this.owner.register('service:map-ui', MockMapUiService);
|
||||||
this.owner.register('service:map', MockMapService);
|
this.owner.register('service:map', MockMapService);
|
||||||
|
this.owner.register('service:settings', MockSettingsService);
|
||||||
|
|
||||||
await render(
|
await render(
|
||||||
<template><AppHeader @onToggleMenu={{this.noop}} /></template>
|
<template><AppHeader @onToggleMenu={{this.noop}} /></template>
|
||||||
@@ -43,11 +47,15 @@ module('Integration | Component | app-header', function (hooks) {
|
|||||||
currentCenter = null;
|
currentCenter = null;
|
||||||
}
|
}
|
||||||
class MockMapService extends Service {}
|
class MockMapService extends Service {}
|
||||||
|
class MockSettingsService extends Service {
|
||||||
|
showQuickSearchButtons = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.owner.register('service:photon', MockPhotonService);
|
this.owner.register('service:photon', MockPhotonService);
|
||||||
this.owner.register('service:router', MockRouterService);
|
this.owner.register('service:router', MockRouterService);
|
||||||
this.owner.register('service:map-ui', MockMapUiService);
|
this.owner.register('service:map-ui', MockMapUiService);
|
||||||
this.owner.register('service:map', MockMapService);
|
this.owner.register('service:map', MockMapService);
|
||||||
|
this.owner.register('service:settings', MockSettingsService);
|
||||||
|
|
||||||
await render(
|
await render(
|
||||||
<template><AppHeader @onToggleMenu={{this.noop}} /></template>
|
<template><AppHeader @onToggleMenu={{this.noop}} /></template>
|
||||||
|
|||||||
Reference in New Issue
Block a user