Compare commits

...

14 Commits

Author SHA1 Message Date
913d5c915c 1.17.2
All checks were successful
CI / Lint (push) Successful in 28s
CI / Test (push) Successful in 43s
2026-03-28 16:49:03 +04:00
89f667b17e Add more icons 2026-03-28 16:48:14 +04:00
22d4ef8d96 Update Pinhead 2026-03-28 16:47:53 +04:00
b17793af9d 1.17.1
Some checks failed
CI / Lint (push) Successful in 29s
CI / Test (push) Failing after 44s
2026-03-28 15:32:21 +04:00
dc9e0f210a Hide search result markers when result is selected 2026-03-28 15:30:27 +04:00
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
d72e5f3de2 Merge pull request 'Add category search, and search result markers with icons' (#35) from feature/poi_type_search into master
All checks were successful
CI / Lint (push) Successful in 31s
CI / Test (push) Successful in 47s
Reviewed-on: #35
2026-03-27 09:12:28 +00:00
582ab4f8b3 Fix lint errors
All checks were successful
CI / Lint (pull_request) Successful in 53s
CI / Test (pull_request) Successful in 47s
Release Drafter / Update release notes draft (pull_request) Successful in 38s
2026-03-23 18:32:18 +04:00
26 changed files with 246 additions and 30 deletions

View File

@@ -10,6 +10,7 @@ import CategoryChips from '#components/category-chips';
export default class AppHeaderComponent extends Component {
@service storage;
@service settings;
@tracked isUserMenuOpen = false;
@tracked searchQuery = '';
@@ -49,9 +50,11 @@ export default class AppHeaderComponent extends Component {
/>
</div>
<div class="header-center {{if this.hasQuery 'searching'}}">
<CategoryChips @onSelect={{this.handleChipSelect}} />
</div>
{{#if this.settings.showQuickSearchButtons}}
<div class="header-center {{if this.hasQuery 'searching'}}">
<CategoryChips @onSelect={{this.handleChipSelect}} />
</div>
{{/if}}
<div class="header-right">
<div class="user-menu-container">

View File

@@ -18,6 +18,11 @@ export default class AppMenuSettings extends Component {
this.settings.updateMapKinetic(event.target.value === 'true');
}
@action
toggleQuickSearchButtons(event) {
this.settings.updateShowQuickSearchButtons(event.target.value === 'true');
}
@action
updatePhotonApi(event) {
this.settings.updatePhotonApi(event.target.value);
@@ -36,6 +41,30 @@ export default class AppMenuSettings extends Component {
<div class="sidebar-content">
<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">
<label for="map-kinetic">Map Inertia (Kinetic Panning)</label>
<select

View File

@@ -42,7 +42,10 @@ export default class CategoryChipsComponent extends Component {
class="category-chip"
{{on "click" (fn this.searchCategory category)}}
aria-label={{category.label}}
disabled={{and (eq this.mapUi.loadingState.type "category") (eq this.mapUi.loadingState.value category.id)}}
disabled={{and
(eq this.mapUi.loadingState.type "category")
(eq this.mapUi.loadingState.value category.id)
}}
>
<Icon @name={{category.icon}} @size={{16}} />
<span>{{category.label}}</span>

View File

@@ -119,6 +119,28 @@ export default class MapComponent extends Component {
const searchResultStyle = (feature) => {
const originalPlace = feature.get('originalPlace');
// If this place is currently selected, hide the search result marker
// because the main red drop pin will be shown instead.
const selectedPlace = this.mapUi.selectedPlace;
if (selectedPlace) {
const isSameOsmId =
originalPlace.osmId &&
selectedPlace.osmId &&
originalPlace.osmId === selectedPlace.osmId;
const isSameId =
originalPlace.id &&
selectedPlace.id &&
originalPlace.id === selectedPlace.id;
const isSameCoords =
originalPlace.lat === selectedPlace.lat &&
originalPlace.lon === selectedPlace.lon;
if (isSameOsmId || isSameId || isSameCoords) {
return new Style({}); // Empty style makes it invisible
}
}
// Some search results might be just the place object without separate tags
// If it's a raw place object, it might have osmTags property.
// Or it might be the tags object itself.
@@ -599,6 +621,11 @@ export default class MapComponent extends Component {
const selected = this.mapUi.selectedPlace;
const options = this.mapUi.selectionOptions || {};
// Force a redraw of the search results layer so it can hide/show the selected pin
if (this.searchResultsSource) {
this.searchResultsSource.changed();
}
if (!this.selectedPinOverlay || !this.selectedPinElement) return;
// Clear any previous shape

View File

@@ -217,7 +217,12 @@ export default class SearchBoxComponent extends Component {
/>
<button type="submit" class="search-submit-btn" aria-label="Search">
{{#if (or (eq this.mapUi.loadingState.type "text") (eq this.mapUi.loadingState.type "category"))}}
{{#if
(or
(eq this.mapUi.loadingState.type "text")
(eq this.mapUi.loadingState.type "category")
)
}}
<Icon @name="loading-ring" @size={{20}} />
{{else}}
<Icon @name="search" @size={{20}} @color="#5f6368" />

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

View File

@@ -155,7 +155,7 @@ out center;
return [];
}
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 mapKinetic = true;
@tracked photonApi = 'https://photon.komoot.io/api/';
@tracked showQuickSearchButtons = true;
overpassApis = [
{
@@ -56,6 +57,13 @@ export default class SettingsService extends Service {
this.mapKinetic = savedKinetic === 'true';
}
// Default is true (initialized in class field)
const savedShowQuickSearch = localStorage.getItem(
'marco:show-quick-search'
);
if (savedShowQuickSearch !== null) {
this.showQuickSearchButtons = savedShowQuickSearch === 'true';
}
}
updateOverpassApi(url) {
@@ -68,6 +76,11 @@ export default class SettingsService extends Service {
localStorage.setItem('marco:map-kinetic', String(enabled));
}
updateShowQuickSearchButtons(enabled) {
this.showQuickSearchButtons = enabled;
localStorage.setItem('marco:show-quick-search', String(enabled));
}
updatePhotonApi(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;
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 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 {
<AppMenu @onClose={{this.closeAppMenu}} />
{{/if}}
<Toast />
{{outlet}}
</template>
}

View File

@@ -79,8 +79,8 @@ export default class PlaceTemplate extends Component {
if (place === null) {
// If we have an active search context, return to it (UP navigation)
if (this.mapUi.returnToSearch && this.mapUi.currentSearch) {
this.router.transitionTo('search', {
queryParams: this.mapUi.currentSearch
this.router.transitionTo('search', {
queryParams: this.mapUi.currentSearch,
});
} else {
// Otherwise just close the sidebar (return to map index)

View File

@@ -38,6 +38,7 @@ import loadingRing from '../icons/270-ring.svg?raw';
import angelfish from '@waysidemapping/pinhead/dist/icons/angelfish.svg?raw';
import barbell from '@waysidemapping/pinhead/dist/icons/barbell.svg?raw';
import climbingWall from '@waysidemapping/pinhead/dist/icons/climbing_wall.svg?raw';
import banknote from '@waysidemapping/pinhead/dist/icons/banknote.svg?raw';
import badgeShieldWithFire from '@waysidemapping/pinhead/dist/icons/badge_shield_with_fire.svg?raw';
import beachUmbrellaInGround from '@waysidemapping/pinhead/dist/icons/beach_umbrella_in_ground.svg?raw';
@@ -106,6 +107,7 @@ import womensAndMensRestroomSymbol from '@waysidemapping/pinhead/dist/icons/wome
import wikipedia from '../icons/wikipedia.svg?raw';
import parkingP from '@waysidemapping/pinhead/dist/icons/parking_p.svg?raw';
import car from '@waysidemapping/pinhead/dist/icons/car.svg?raw';
const ICONS = {
activity,
@@ -123,6 +125,7 @@ const ICONS = {
camera,
'check-square': checkSquare,
'cigarette-with-smoke-curl': cigaretteWithSmokeCurl,
climbing_wall: climbingWall,
'classical-building': classicalBuilding,
'classical-building-with-dome-and-flag': classicalBuildingWithDomeAndFlag,
'classical-building-with-flag': classicalBuildingWithFlag,
@@ -212,6 +215,7 @@ const ICONS = {
'womens-and-mens-restroom-symbol': womensAndMensRestroomSymbol,
wikipedia,
parking_p: parkingP,
car,
x,
zap,
'loading-ring': loadingRing,

View File

@@ -31,6 +31,7 @@ export const POI_ICON_RULES = [
{ tags: { amenity: 'police' }, icon: 'police-officer-with-stop-arm' },
{ tags: { amenity: 'toilets' }, icon: 'womens-and-mens-restroom-symbol' },
{ tags: { amenity: 'school' }, icon: 'open-book' },
{ tags: { amenity: 'driving_school' }, icon: 'car' },
{ tags: { shop: 'coffee' }, icon: 'coffee-bean' },
{ tags: { shop: 'tea' }, icon: 'coffee-bean' },
@@ -144,6 +145,7 @@ export const POI_ICON_RULES = [
{ tags: { sport: 'squash' }, icon: 'person-playing-tennis' },
{ tags: { sport: 'padel' }, icon: 'person-playing-tennis' },
{ tags: { sport: 'table_tennis' }, icon: 'table-tennis-paddle' },
{ tags: { sport: 'climbing' }, icon: 'climbing_wall' },
{ tags: { leisure: 'water_park' }, icon: 'person-swimming-in-water' },
{ tags: { sport: 'swimming' }, icon: 'person-swimming-in-water' },
{ tags: { sport: 'golf' }, icon: 'person-swinging-golf-club' },

View File

@@ -1,6 +1,6 @@
{
"name": "marco",
"version": "1.16.0",
"version": "1.17.2",
"private": true,
"description": "Unhosted maps app",
"repository": {
@@ -102,7 +102,7 @@
"edition": "octane"
},
"dependencies": {
"@waysidemapping/pinhead": "^15.17.0",
"@waysidemapping/pinhead": "^15.20.0",
"ember-concurrency": "^5.2.0",
"ember-lifeline": "^7.0.0"
}

10
pnpm-lock.yaml generated
View File

@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@waysidemapping/pinhead':
specifier: ^15.17.0
version: 15.17.0
specifier: ^15.20.0
version: 15.20.0
ember-concurrency:
specifier: ^5.2.0
version: 5.2.0(@babel/core@7.28.6)
@@ -1654,8 +1654,8 @@ packages:
peerDependencies:
'@warp-drive/core': 5.8.1
'@waysidemapping/pinhead@15.17.0':
resolution: {integrity: sha512-XcL/0Ll+gkRIpXlO+skwd6USynA+mX3DNwqrWDMhgRmLP4DNRPTeaecK64BBxk1bB/F9Xi/9kgN6JA5zbdgejQ==}
'@waysidemapping/pinhead@15.20.0':
resolution: {integrity: sha512-JD9XINaMhtEy3VEjvc+l4r1sLwbyOKoYdD2IYY2QNKP3FeeNwE/2gcUly631JH9jPymoFeOix0f3o9L/n9YDSQ==}
'@xmldom/xmldom@0.8.11':
resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
@@ -7245,7 +7245,7 @@ snapshots:
- '@glint/template'
- supports-color
'@waysidemapping/pinhead@15.17.0': {}
'@waysidemapping/pinhead@15.20.0': {}
'@xmldom/xmldom@0.8.11': {}

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-TileImage" content="/icons/icon-144.png">
<script type="module" crossorigin src="/assets/main-C4F17h3W.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-CKp1bFPU.css">
<script type="module" crossorigin src="/assets/main-B8Ckz4Ru.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-OLSOzTKA.css">
</head>
<body>
</body>

View File

@@ -1,11 +1,5 @@
import { module, test } from 'qunit';
import {
visit,
click,
fillIn,
triggerEvent,
currentURL,
} from '@ember/test-helpers';
import { visit, click, fillIn, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'marco/tests/helpers';
import Service from '@ember/service';
import { Promise } from 'rsvp';

View File

@@ -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 {
// 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 MockMapUiService extends Service {}
class MockMapService extends Service {}
class MockSettingsService extends Service {
showQuickSearchButtons = true;
}
this.owner.register('service:photon', MockPhotonService);
this.owner.register('service:router', MockRouterService);
this.owner.register('service:map-ui', MockMapUiService);
this.owner.register('service:map', MockMapService);
this.owner.register('service:settings', MockSettingsService);
await render(
<template><AppHeader @onToggleMenu={{this.noop}} /></template>
@@ -43,11 +47,15 @@ module('Integration | Component | app-header', function (hooks) {
currentCenter = null;
}
class MockMapService extends Service {}
class MockSettingsService extends Service {
showQuickSearchButtons = true;
}
this.owner.register('service:photon', MockPhotonService);
this.owner.register('service:router', MockRouterService);
this.owner.register('service:map-ui', MockMapUiService);
this.owner.register('service:map', MockMapService);
this.owner.register('service:settings', MockSettingsService);
await render(
<template><AppHeader @onToggleMenu={{this.noop}} /></template>