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
This commit is contained in:
setop 2021-03-05 16:19:42 +00:00 committed by Thomas Citharel
parent 5be8872edc
commit 13f33b8393
3 changed files with 115 additions and 21 deletions

View file

@ -81,7 +81,7 @@ export default class AddressAutoComplete extends Vue {
isFetching = false; isFetching = false;
queryText: string = (this.value && new Address(this.value).fullName) || ""; initialQueryText = "";
addressModalActive = false; 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") @Watch("value")
updateEditing(): void { updateEditing(): void {
if (!this.value?.id) return; if (!this.value?.id) return;
this.selected = this.value; this.selected = this.value;
const address = new Address(this.selected);
this.queryText = `${address.poiInfos.name} ${address.poiInfos.alternativeName}`;
} }
updateSelected(option: IAddress): void { updateSelected(option: IAddress): void {

View file

@ -332,7 +332,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue, Watch } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { import {
ADMIN_SETTINGS, ADMIN_SETTINGS,
SAVE_ADMIN_SETTINGS, SAVE_ADMIN_SETTINGS,

View file

@ -27,7 +27,9 @@
<address-auto-complete <address-auto-complete
v-model="location" v-model="location"
id="location" id="location"
ref="aac"
:placeholder="$t('For instance: London')" :placeholder="$t('For instance: London')"
@input="locchange"
/> />
</b-field> </b-field>
<b-field :label="$t('Radius')" label-for="radius"> <b-field :label="$t('Radius')" label-for="radius">
@ -63,7 +65,7 @@
</section> </section>
<section <section
class="events-featured" class="events-featured"
v-if="!tag && !(search || location.geom || when !== 'any')" v-if="!canSearchEvents && !canSearchGroups"
> >
<b-loading :active.sync="$apollo.loading"></b-loading> <b-loading :active.sync="$apollo.loading"></b-loading>
<h2 class="title">{{ $t("Featured events") }}</h2> <h2 class="title">{{ $t("Featured events") }}</h2>
@ -173,7 +175,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import ngeohash from "ngeohash"; import ngeohash, { GeographicPoint } from "ngeohash";
import { import {
endOfToday, endOfToday,
addDays, addDays,
@ -188,7 +190,6 @@ import {
eachWeekendOfInterval, eachWeekendOfInterval,
} from "date-fns"; } from "date-fns";
import { SearchTabs } from "@/types/enums"; import { SearchTabs } from "@/types/enums";
import { RawLocation } from "vue-router";
import EventCard from "../components/Event/EventCard.vue"; import EventCard from "../components/Event/EventCard.vue";
import { FETCH_EVENTS } from "../graphql/event"; import { FETCH_EVENTS } from "../graphql/event";
import { IEvent } from "../types/event.model"; import { IEvent } from "../types/event.model";
@ -200,6 +201,7 @@ import { Paginate } from "../types/paginate";
import { IGroup } from "../types/actor"; import { IGroup } from "../types/actor";
import GroupCard from "../components/Group/GroupCard.vue"; import GroupCard from "../components/Group/GroupCard.vue";
import { CONFIG } from "../graphql/config"; import { CONFIG } from "../graphql/config";
import { REVERSE_GEOCODE } from "../graphql/address";
interface ISearchTimeOption { interface ISearchTimeOption {
label: string; label: string;
@ -211,6 +213,14 @@ const EVENT_PAGE_LIMIT = 10;
const GROUP_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({ @Component({
components: { components: {
EventCard, EventCard,
@ -235,9 +245,9 @@ const GROUP_PAGE_LIMIT = 10;
limit: EVENT_PAGE_LIMIT, limit: EVENT_PAGE_LIMIT,
}; };
}, },
debounce: 300, throttle: THROTTLE,
skip() { skip() {
return !this.tag && !this.geohash && this.end === null; return !this.canSearchEvents;
}, },
}, },
searchGroups: { searchGroups: {
@ -252,8 +262,9 @@ const GROUP_PAGE_LIMIT = 10;
limit: GROUP_PAGE_LIMIT, limit: GROUP_PAGE_LIMIT,
}; };
}, },
throttle: THROTTLE,
skip() { 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; GROUP_PAGE_LIMIT = GROUP_PAGE_LIMIT;
$refs!: {
aac: AddressAutoComplete;
};
mounted(): void {
this.prepareLocation(this.$route.query.geohash as string);
}
radiusString = (radius: number | null): string => { radiusString = (radius: number | null): string => {
if (radius) { if (radius) {
return this.$tc("{nb} km", radius, { nb: radius }) as string; 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) { set search(term: string | undefined) {
const route: RawLocation = { this.$router.replace({
name: RouteName.SEARCH, name: RouteName.SEARCH,
}; query: { ...this.$route.query, term },
if (term !== "") { });
route.query = { ...this.$route.query, term };
}
this.$router.replace(route);
} }
get activeTab(): SearchTabs { 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 { get radius(): number | null {
if (this.$route.query.radius === "any") { if (this.$route.query.radius === "any") {
return null; return null;
@ -411,14 +442,43 @@ export default class Search extends Vue {
return { start: startOfDay(start), end: endOfDay(end) }; return { start: startOfDay(start), end: endOfDay(end) };
} }
get geohash(): string | undefined { private prepareLocation(value: string | undefined): void {
if (this.location?.geom) { if (value !== undefined) {
const [lon, lat] = this.location.geom.split(";"); // decode
return ngeohash.encode(lat, lon, 6); 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 { get start(): Date | undefined {
if (this.options[this.when]) { if (this.options[this.when]) {
return this.options[this.when].start; return this.options[this.when].start;
@ -432,6 +492,31 @@ export default class Search extends Vue {
} }
return undefined; 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> </script>