Compare commits

...

7 Commits

Author SHA1 Message Date
2b219fe0cf 1.17.0
All checks were successful
CI / Lint (push) Successful in 28s
CI / Test (push) Successful in 43s
2026-03-27 15:17:16 +04:00
9fd6c4d64d Merge pull request 'When search requests fail, show error in toast notifications instead of empty search results' (#38) from feature/failed_requests into master
Some checks failed
CI / Lint (push) Successful in 28s
CI / Test (push) Failing after 44s
Reviewed-on: #38
2026-03-27 11:12:58 +00:00
8e5b2c7439 Fix lint error
All checks were successful
CI / Lint (pull_request) Successful in 28s
CI / Test (pull_request) Successful in 44s
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2026-03-27 15:05:56 +04:00
0f29430e1a When request retries exhaust, show error in toast notification
Some checks failed
CI / Lint (pull_request) Failing after 29s
CI / Test (pull_request) Failing after 44s
2026-03-27 15:01:04 +04:00
0059d89cc3 Add toast notifications 2026-03-27 15:00:36 +04:00
54e2766dc4 Merge pull request 'Add setting for hiding quick search buttons' (#36) from feature/settings_quick-search into master
All checks were successful
CI / Lint (push) Successful in 28s
CI / Test (push) Successful in 43s
Reviewed-on: #36
2026-03-27 10:09:18 +00:00
5978f67d48 Add setting for hiding quick search buttons
All checks were successful
CI / Lint (pull_request) Successful in 28s
CI / Test (pull_request) Successful in 44s
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2026-03-27 13:59:36 +04:00
18 changed files with 194 additions and 13 deletions

View File

@@ -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">

View File

@@ -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
View 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>
}

View File

@@ -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
} }
} }

View File

@@ -155,7 +155,7 @@ out center;
return []; return [];
} }
console.error('Category search failed', e); console.error('Category search failed', e);
return []; throw e;
} }
} }

View File

@@ -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
View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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>
} }

View File

@@ -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

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

View File

@@ -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>

View File

@@ -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');
});
}); });

View File

@@ -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>