From 0059d89cc3adaeb8eea202f20481394536b8ac7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Fri, 27 Mar 2026 15:00:36 +0400 Subject: [PATCH 1/3] Add toast notifications --- app/components/toast.gjs | 14 ++++++++++++++ app/services/toast.js | 21 +++++++++++++++++++++ app/styles/app.css | 33 +++++++++++++++++++++++++++++++++ app/templates/application.gjs | 3 +++ 4 files changed, 71 insertions(+) create mode 100644 app/components/toast.gjs create mode 100644 app/services/toast.js diff --git a/app/components/toast.gjs b/app/components/toast.gjs new file mode 100644 index 0000000..e7f1a58 --- /dev/null +++ b/app/components/toast.gjs @@ -0,0 +1,14 @@ +import Component from '@glimmer/component'; +import { service } from '@ember/service'; + +export default class ToastComponent extends Component { + @service toast; + + +} diff --git a/app/services/toast.js b/app/services/toast.js new file mode 100644 index 0000000..4116944 --- /dev/null +++ b/app/services/toast.js @@ -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); + } +} diff --git a/app/styles/app.css b/app/styles/app.css index 8c53979..f27c8e5 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -1321,3 +1321,36 @@ button.create-place { cursor: not-allowed; 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); + } +} diff --git a/app/templates/application.gjs b/app/templates/application.gjs index 8dae51b..8dfd51b 100644 --- a/app/templates/application.gjs +++ b/app/templates/application.gjs @@ -3,6 +3,7 @@ import { pageTitle } from 'ember-page-title'; import Map from '#components/map'; import AppHeader from '#components/app-header'; import AppMenu from '#components/app-menu/index'; +import Toast from '#components/toast'; import { service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; @@ -89,6 +90,8 @@ export default class ApplicationComponent extends Component { {{/if}} + + {{outlet}} } From 0f29430e1ab7184c758aa313c3cf716353ec42f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Fri, 27 Mar 2026 15:01:04 +0400 Subject: [PATCH 2/3] When request retries exhaust, show error in toast notification --- app/routes/search.js | 9 ++++-- app/services/osm.js | 2 +- tests/acceptance/search-test.js | 52 +++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/app/routes/search.js b/app/routes/search.js index cf876a4..e47eb08 100644 --- a/app/routes/search.js +++ b/app/routes/search.js @@ -9,6 +9,7 @@ export default class SearchRoute extends Route { @service mapUi; @service storage; @service router; + @service toast; queryParams = { lat: { refreshModel: true }, @@ -199,8 +200,12 @@ export default class SearchRoute extends Route { } @action - error() { + error(error, transition) { 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 } } diff --git a/app/services/osm.js b/app/services/osm.js index 9132a2c..e265178 100644 --- a/app/services/osm.js +++ b/app/services/osm.js @@ -155,7 +155,7 @@ out center; return []; } console.error('Category search failed', e); - return []; + throw e; } } diff --git a/tests/acceptance/search-test.js b/tests/acceptance/search-test.js index c344631..59b97c0 100644 --- a/tests/acceptance/search-test.js +++ b/tests/acceptance/search-test.js @@ -218,4 +218,56 @@ module('Acceptance | search', function (hooks) { // Ensure it shows "Results" not "Nearby" 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 (e) { + // 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'); + }); }); From 8e5b2c74391e1ecba087b43f72c2276e4f54db9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Fri, 27 Mar 2026 15:05:56 +0400 Subject: [PATCH 3/3] Fix lint error --- tests/acceptance/search-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/acceptance/search-test.js b/tests/acceptance/search-test.js index 59b97c0..42ecedb 100644 --- a/tests/acceptance/search-test.js +++ b/tests/acceptance/search-test.js @@ -260,7 +260,7 @@ module('Acceptance | search', function (hooks) { try { await visit('/search?category=coffee&lat=52.52&lon=13.405'); - } catch (e) { + } catch { // Aborted transition throws, which is expected }