Compare commits

...

5 Commits

Author SHA1 Message Date
raucao 2b219fe0cf 1.17.0
CI / Lint (push) Successful in 28s
CI / Test (push) Successful in 43s
2026-03-27 15:17:16 +04:00
raucao 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
CI / Lint (push) Successful in 28s
CI / Test (push) Failing after 44s
Reviewed-on: #38
2026-03-27 11:12:58 +00:00
raucao 8e5b2c7439 Fix lint error
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
raucao 0f29430e1a When request retries exhaust, show error in toast notification
CI / Lint (pull_request) Failing after 29s
CI / Test (pull_request) Failing after 44s
2026-03-27 15:01:04 +04:00
raucao 0059d89cc3 Add toast notifications 2026-03-27 15:00:36 +04:00
14 changed files with 138 additions and 10 deletions
+14
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>
}
+7 -2
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
} }
} }
+1 -1
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;
} }
} }
+21
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);
}
}
+33
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);
}
}
+3
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>
} }
+1 -1
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
+2 -2
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>
+52
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');
});
}); });