From 13f33b8393d57c4978b64c45b0c271bf7da17e0f Mon Sep 17 00:00:00 2001 From: setop <setop@zoocoop.com> Date: Fri, 5 Mar 2021 16:19:42 +0000 Subject: [PATCH] make search with location bookmarkable (fix #482) using geohash set a default radius when a location is set, so it does not trigger a worldwide search reduce pressure on server and view with debonce --- .../components/Event/AddressAutoComplete.vue | 15 ++- js/src/views/Admin/Settings.vue | 2 +- js/src/views/Search.vue | 119 +++++++++++++++--- 3 files changed, 115 insertions(+), 21 deletions(-) diff --git a/js/src/components/Event/AddressAutoComplete.vue b/js/src/components/Event/AddressAutoComplete.vue index 407c991f7..9be4b29aa 100644 --- a/js/src/components/Event/AddressAutoComplete.vue +++ b/js/src/components/Event/AddressAutoComplete.vue @@ -81,7 +81,7 @@ export default class AddressAutoComplete extends Vue { isFetching = false; - queryText: string = (this.value && new Address(this.value).fullName) || ""; + initialQueryText = ""; addressModalActive = false; @@ -149,12 +149,21 @@ export default class AddressAutoComplete extends Vue { } } + get queryText(): string { + if (this.value) { + return new Address(this.value).fullName; + } + return this.initialQueryText; + } + + set queryText(queryText: string) { + this.initialQueryText = queryText; + } + @Watch("value") updateEditing(): void { if (!this.value?.id) return; this.selected = this.value; - const address = new Address(this.selected); - this.queryText = `${address.poiInfos.name} ${address.poiInfos.alternativeName}`; } updateSelected(option: IAddress): void { diff --git a/js/src/views/Admin/Settings.vue b/js/src/views/Admin/Settings.vue index 2c4a05444..10214a49c 100644 --- a/js/src/views/Admin/Settings.vue +++ b/js/src/views/Admin/Settings.vue @@ -332,7 +332,7 @@ </div> </template> <script lang="ts"> -import { Component, Vue, Watch } from "vue-property-decorator"; +import { Component, Vue } from "vue-property-decorator"; import { ADMIN_SETTINGS, SAVE_ADMIN_SETTINGS, diff --git a/js/src/views/Search.vue b/js/src/views/Search.vue index 6a3c2b044..f5132e4df 100644 --- a/js/src/views/Search.vue +++ b/js/src/views/Search.vue @@ -27,7 +27,9 @@ <address-auto-complete v-model="location" id="location" + ref="aac" :placeholder="$t('For instance: London')" + @input="locchange" /> </b-field> <b-field :label="$t('Radius')" label-for="radius"> @@ -63,7 +65,7 @@ </section> <section class="events-featured" - v-if="!tag && !(search || location.geom || when !== 'any')" + v-if="!canSearchEvents && !canSearchGroups" > <b-loading :active.sync="$apollo.loading"></b-loading> <h2 class="title">{{ $t("Featured events") }}</h2> @@ -173,7 +175,7 @@ <script lang="ts"> import { Component, Prop, Vue } from "vue-property-decorator"; -import ngeohash from "ngeohash"; +import ngeohash, { GeographicPoint } from "ngeohash"; import { endOfToday, addDays, @@ -188,7 +190,6 @@ import { eachWeekendOfInterval, } from "date-fns"; import { SearchTabs } from "@/types/enums"; -import { RawLocation } from "vue-router"; import EventCard from "../components/Event/EventCard.vue"; import { FETCH_EVENTS } from "../graphql/event"; import { IEvent } from "../types/event.model"; @@ -200,6 +201,7 @@ import { Paginate } from "../types/paginate"; import { IGroup } from "../types/actor"; import GroupCard from "../components/Group/GroupCard.vue"; import { CONFIG } from "../graphql/config"; +import { REVERSE_GEOCODE } from "../graphql/address"; interface ISearchTimeOption { label: string; @@ -211,6 +213,14 @@ const EVENT_PAGE_LIMIT = 10; const GROUP_PAGE_LIMIT = 10; +const DEFAULT_RADIUS = 25; // value to set if radius is null but location set + +const DEFAULT_ZOOM = 11; // zoom on a city + +const GEOHASH_DEPTH = 9; // put enough accuracy, radius will be used anyway + +const THROTTLE = 2000; // minimum interval in ms between two requests + @Component({ components: { EventCard, @@ -235,9 +245,9 @@ const GROUP_PAGE_LIMIT = 10; limit: EVENT_PAGE_LIMIT, }; }, - debounce: 300, + throttle: THROTTLE, skip() { - return !this.tag && !this.geohash && this.end === null; + return !this.canSearchEvents; }, }, searchGroups: { @@ -252,8 +262,9 @@ const GROUP_PAGE_LIMIT = 10; limit: GROUP_PAGE_LIMIT, }; }, + throttle: THROTTLE, skip() { - return !this.search && !this.geohash; + return !this.canSearchGroups; }, }, }, @@ -334,6 +345,14 @@ export default class Search extends Vue { GROUP_PAGE_LIMIT = GROUP_PAGE_LIMIT; + $refs!: { + aac: AddressAutoComplete; + }; + + mounted(): void { + this.prepareLocation(this.$route.query.geohash as string); + } + radiusString = (radius: number | null): string => { if (radius) { return this.$tc("{nb} km", radius, { nb: radius }) as string; @@ -352,13 +371,10 @@ export default class Search extends Vue { } set search(term: string | undefined) { - const route: RawLocation = { + this.$router.replace({ name: RouteName.SEARCH, - }; - if (term !== "") { - route.query = { ...this.$route.query, term }; - } - this.$router.replace(route); + query: { ...this.$route.query, term }, + }); } get activeTab(): SearchTabs { @@ -374,6 +390,21 @@ export default class Search extends Vue { }); } + get geohash(): string | undefined { + if (this.location?.geom) { + const [lon, lat] = this.location.geom.split(";"); + return ngeohash.encode(lat, lon, GEOHASH_DEPTH); + } + return undefined; + } + + set geohash(value: string | undefined) { + this.$router.replace({ + name: RouteName.SEARCH, + query: { ...this.$route.query, geohash: value }, + }); + } + get radius(): number | null { if (this.$route.query.radius === "any") { return null; @@ -411,14 +442,43 @@ export default class Search extends Vue { return { start: startOfDay(start), end: endOfDay(end) }; } - get geohash(): string | undefined { - if (this.location?.geom) { - const [lon, lat] = this.location.geom.split(";"); - return ngeohash.encode(lat, lon, 6); + private prepareLocation(value: string | undefined): void { + if (value !== undefined) { + // decode + const latlon = ngeohash.decode(value); + // set location + this.reverseGeoCode(latlon, DEFAULT_ZOOM); } - return undefined; } + async reverseGeoCode(e: GeographicPoint, zoom: number): Promise<void> { + const result = await this.$apollo.query({ + query: REVERSE_GEOCODE, + variables: { + latitude: e.latitude, + longitude: e.longitude, + zoom, + locale: this.$i18n.locale, + }, + }); + const addressData = result.data.reverseGeocode.map( + (address: IAddress) => new Address(address) + ); + if (addressData.length > 0) { + this.location = addressData[0]; + } + } + + locchange = (e: IAddress): void => { + if (this.radius === undefined || this.radius === null) { + this.radius = DEFAULT_RADIUS; + } + if (e.geom) { + const [lon, lat] = e.geom.split(";"); + this.geohash = ngeohash.encode(lat, lon, GEOHASH_DEPTH); + } + }; + get start(): Date | undefined { if (this.options[this.when]) { return this.options[this.when].start; @@ -432,6 +492,31 @@ export default class Search extends Vue { } return undefined; } + + get canSearchGroups(): boolean { + return ( + this.stringExists(this.search) || + (this.stringExists(this.geohash) && this.valueExists(this.radius)) + ); + } + + get canSearchEvents(): boolean { + return ( + this.stringExists(this.search) || + this.stringExists(this.tag) || + (this.stringExists(this.geohash) && this.valueExists(this.radius)) || + this.valueExists(this.end) + ); + } + + // helper functions for skip + private valueExists(value: any): boolean { + return value !== undefined && value !== null; + } + + private stringExists(value: string | undefined): boolean { + return this.valueExists(value) && (value as string).length > 0; + } } </script>