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:
parent
5be8872edc
commit
13f33b8393
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue