feat(search): add category search support and sync with chips

This commit is contained in:
2026-03-20 18:14:02 +04:00
parent 4008a8c883
commit b083c1d001
8 changed files with 67 additions and 17 deletions

View File

@@ -11,7 +11,11 @@ import CategoryChips from '#components/category-chips';
export default class AppHeaderComponent extends Component { export default class AppHeaderComponent extends Component {
@service storage; @service storage;
@tracked isUserMenuOpen = false; @tracked isUserMenuOpen = false;
@tracked hasQuery = false; @tracked searchQuery = '';
get hasQuery() {
return !!this.searchQuery;
}
@action @action
toggleUserMenu() { toggleUserMenu() {
@@ -25,22 +29,21 @@ export default class AppHeaderComponent extends Component {
@action @action
handleQueryChange(query) { handleQueryChange(query) {
this.hasQuery = !!query; this.searchQuery = query;
} }
@action @action
handleChipSelect() { handleChipSelect(category) {
// When a chip is selected, we might want to ensure the search box is cleared visually, this.searchQuery = category.label;
// although the route transition will happen. // The existing logic in CategoryChips triggers the route transition.
// The SearchBox component manages its own state, so we rely on the route transition. // This update simply fills the search box.
// However, if we want to clear the search box input from here, we'd need to control it.
// For now, let's just let the route change happen.
} }
<template> <template>
<header class="app-header"> <header class="app-header">
<div class="header-left"> <div class="header-left">
<SearchBox <SearchBox
@query={{this.searchQuery}}
@onToggleMenu={{@onToggleMenu}} @onToggleMenu={{@onToggleMenu}}
@onQueryChange={{this.handleQueryChange}} @onQueryChange={{this.handleQueryChange}}
/> />

View File

@@ -7,6 +7,7 @@ import { fn } from '@ember/helper';
import { task, timeout } from 'ember-concurrency'; import { task, timeout } from 'ember-concurrency';
import Icon from '#components/icon'; import Icon from '#components/icon';
import humanizeOsmTag from '../helpers/humanize-osm-tag'; import humanizeOsmTag from '../helpers/humanize-osm-tag';
import { POI_CATEGORIES } from '../utils/poi-categories';
import eq from 'ember-truth-helpers/helpers/eq'; import eq from 'ember-truth-helpers/helpers/eq';
export default class SearchBoxComponent extends Component { export default class SearchBoxComponent extends Component {
@@ -15,11 +16,19 @@ export default class SearchBoxComponent extends Component {
@service mapUi; @service mapUi;
@service map; // Assuming we might need map context, but mostly we use router @service map; // Assuming we might need map context, but mostly we use router
@tracked query = ''; @tracked _internalQuery = '';
@tracked results = []; @tracked results = [];
@tracked isFocused = false; @tracked isFocused = false;
@tracked isLoading = false; @tracked isLoading = false;
get query() {
return this.args.query ?? this._internalQuery;
}
set query(value) {
this._internalQuery = value;
}
get showPopover() { get showPopover() {
return this.isFocused && this.results.length > 0; return this.isFocused && this.results.length > 0;
} }
@@ -51,8 +60,20 @@ export default class SearchBoxComponent extends Component {
if (this.mapUi.currentCenter) { if (this.mapUi.currentCenter) {
({ lat, lon } = this.mapUi.currentCenter); ({ lat, lon } = this.mapUi.currentCenter);
} }
// Filter categories
const q = this.query.toLowerCase();
const categoryMatches = POI_CATEGORIES.filter((c) =>
c.label.toLowerCase().includes(q)
).map((c) => ({
source: 'category',
title: c.label,
id: c.id,
icon: 'search',
}));
const results = await this.photon.search(this.query, lat, lon); const results = await this.photon.search(this.query, lat, lon);
this.results = results; this.results = [...categoryMatches, ...results];
} catch (e) { } catch (e) {
console.error('Search failed', e); console.error('Search failed', e);
this.results = []; this.results = [];
@@ -98,7 +119,29 @@ export default class SearchBoxComponent extends Component {
@action @action
selectResult(place) { selectResult(place) {
if (place.source === 'category') {
this.query = place.title;
if (this.args.onQueryChange) {
this.args.onQueryChange(place.title);
}
this.results = [];
this.router.transitionTo('search', {
queryParams: {
q: place.title,
category: place.id,
selected: null,
lat: null,
lon: null,
},
});
return;
}
this.query = place.title; this.query = place.title;
if (this.args.onQueryChange) {
this.args.onQueryChange(place.title);
}
this.results = []; // Hide popover this.results = []; // Hide popover
// If it has an OSM ID, go to place details // If it has an OSM ID, go to place details
@@ -183,7 +226,11 @@ export default class SearchBoxComponent extends Component {
{{on "click" (fn this.selectResult result)}} {{on "click" (fn this.selectResult result)}}
> >
<div class="result-icon"> <div class="result-icon">
<Icon @name="map-pin" @size={{16}} @color="#666" /> <Icon
@name={{if result.icon result.icon "map-pin"}}
@size={{16}}
@color="#666"
/>
</div> </div>
<div class="result-info"> <div class="result-info">
<span class="result-title">{{result.title}}</span> <span class="result-title">{{result.title}}</span>

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-gEUnNw-L.js"></script> <script type="module" crossorigin src="/assets/main-C4F17h3W.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-BOfcjRke.css"> <link rel="stylesheet" crossorigin href="/assets/main-CKp1bFPU.css">
</head> </head>
<body> <body>
</body> </body>