WIP Search places by category

This commit is contained in:
2026-03-20 15:40:03 +04:00
parent 6b37508f66
commit f2a2d910a0
10 changed files with 350 additions and 11 deletions

View File

@@ -6,10 +6,12 @@ import { on } from '@ember/modifier';
import Icon from '#components/icon';
import UserMenu from '#components/user-menu';
import SearchBox from '#components/search-box';
import CategoryChips from '#components/category-chips';
export default class AppHeaderComponent extends Component {
@service storage;
@tracked isUserMenuOpen = false;
@tracked hasQuery = false;
@action
toggleUserMenu() {
@@ -21,10 +23,31 @@ export default class AppHeaderComponent extends Component {
this.isUserMenuOpen = false;
}
@action
handleQueryChange(query) {
this.hasQuery = !!query;
}
@action
handleChipSelect() {
// When a chip is selected, we might want to ensure the search box is cleared visually,
// although the route transition will happen.
// The SearchBox component manages its own state, so we rely on the route transition.
// 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>
<header class="app-header">
<div class="header-left">
<SearchBox @onToggleMenu={{@onToggleMenu}} />
<SearchBox
@onToggleMenu={{@onToggleMenu}}
@onQueryChange={{this.handleQueryChange}}
/>
</div>
<div class="header-center {{if this.hasQuery 'searching'}}">
<CategoryChips @onSelect={{this.handleChipSelect}} />
</div>
<div class="header-right">

View File

@@ -0,0 +1,52 @@
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { action } from '@ember/object';
import { on } from '@ember/modifier';
import { fn } from '@ember/helper';
import Icon from '#components/icon';
import { POI_CATEGORIES } from '../utils/poi-categories';
export default class CategoryChipsComponent extends Component {
@service router;
@service mapUi;
get categories() {
return POI_CATEGORIES;
}
@action
searchCategory(category) {
// If passed an onSelect action, call it (e.g. to clear search box)
if (this.args.onSelect) {
this.args.onSelect(category);
}
let queryParams = { category: category.id, q: null };
if (this.mapUi.currentCenter) {
const { lat, lon } = this.mapUi.currentCenter;
queryParams.lat = parseFloat(lat).toFixed(4);
queryParams.lon = parseFloat(lon).toFixed(4);
}
this.router.transitionTo('search', { queryParams });
}
<template>
<div class="category-chips-scroll">
<div class="category-chips-container">
{{#each this.categories as |category|}}
<button
type="button"
class="category-chip"
{{on "click" (fn this.searchCategory category)}}
aria-label={{category.label}}
>
<Icon @name={{category.icon}} @size={{16}} />
<span>{{category.label}}</span>
</button>
{{/each}}
</div>
</div>
</template>
}

View File

@@ -914,6 +914,7 @@ export default class MapComponent extends Component {
const [maxLon, maxLat] = toLonLat([extent[2], extent[3]]);
const bbox = { minLat, minLon, maxLat, maxLon };
this.mapUi.updateBounds(bbox);
await this.storage.loadPlacesInBounds(bbox);
this.loadBookmarks(this.storage.placesInView);

View File

@@ -27,6 +27,10 @@ export default class SearchBoxComponent extends Component {
@action
handleInput(event) {
this.query = event.target.value;
if (this.args.onQueryChange) {
this.args.onQueryChange(this.query);
}
if (this.query.length < 2) {
this.results = [];
return;
@@ -35,6 +39,7 @@ export default class SearchBoxComponent extends Component {
this.searchTask.perform();
}
searchTask = task({ restartable: true }, async () => {
await timeout(300);
@@ -121,8 +126,10 @@ export default class SearchBoxComponent extends Component {
clear() {
this.query = '';
this.results = [];
this.router.transitionTo('index'); // Or stay on current page?
// Usually clear just clears the input.
if (this.args.onQueryChange) {
this.args.onQueryChange('');
}
this.router.transitionTo('index');
}
<template>