feat(search): add category search support and sync with chips
This commit is contained in:
@@ -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}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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
2
release/assets/main-C4F17h3W.js
Normal file
2
release/assets/main-C4F17h3W.js
Normal file
File diff suppressed because one or more lines are too long
1
release/assets/main-CKp1bFPU.css
Normal file
1
release/assets/main-CKp1bFPU.css
Normal file
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
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user