forked from potsda.mn/mobilizon
Allow to filter by begins_on and ends_on. Redirect explore to search
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
d725393fd4
commit
b4f500532f
|
@ -25,6 +25,7 @@
|
|||
"buefy": "^0.8.2",
|
||||
"bulma-divider": "^0.2.0",
|
||||
"core-js": "^3.6.4",
|
||||
"date-fns": "^2.15.0",
|
||||
"eslint-plugin-cypress": "^2.10.3",
|
||||
"graphql": "^15.0.0",
|
||||
"graphql-tag": "^2.10.3",
|
||||
|
|
|
@ -26,7 +26,7 @@ input.input {
|
|||
}
|
||||
|
||||
.section {
|
||||
padding: 1rem 2rem 4rem;
|
||||
padding: 1rem 1% 4rem;
|
||||
}
|
||||
|
||||
figure img.is-rounded {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<b-autocomplete
|
||||
:data="addressData"
|
||||
v-model="queryText"
|
||||
:placeholder="$t('e.g. 10 Rue Jangot')"
|
||||
:placeholder="placeholder || $t('e.g. 10 Rue Jangot')"
|
||||
field="fullName"
|
||||
:loading="isFetching"
|
||||
@typing="fetchAsyncData"
|
||||
|
@ -45,6 +45,7 @@ import { IConfig } from "../../types/config.model";
|
|||
})
|
||||
export default class AddressAutoComplete extends Vue {
|
||||
@Prop({ required: true }) value!: IAddress;
|
||||
@Prop({ required: false }) placeholder!: string;
|
||||
|
||||
addressData: IAddress[] = [];
|
||||
|
||||
|
|
|
@ -1,8 +1,22 @@
|
|||
import gql from "graphql-tag";
|
||||
|
||||
export const SEARCH_EVENTS = gql`
|
||||
query SearchEvents($location: String, $radius: Float, $tags: String, $term: String) {
|
||||
searchEvents(location: $location, radius: $radius, tags: $tags, term: $term) {
|
||||
query SearchEvents(
|
||||
$location: String
|
||||
$radius: Float
|
||||
$tags: String
|
||||
$term: String
|
||||
$beginsOn: DateTime
|
||||
$endsOn: DateTime
|
||||
) {
|
||||
searchEvents(
|
||||
location: $location
|
||||
radius: $radius
|
||||
tags: $tags
|
||||
term: $term
|
||||
beginsOn: $beginsOn
|
||||
endsOn: $endsOn
|
||||
) {
|
||||
total
|
||||
elements {
|
||||
title
|
||||
|
|
|
@ -730,5 +730,18 @@
|
|||
"Delete post": "Delete post",
|
||||
"Update post": "Update post",
|
||||
"Posts": "Posts",
|
||||
"Register an account on {instanceName}!": "Register an account on {instanceName}!"
|
||||
"Register an account on {instanceName}!": "Register an account on {instanceName}!",
|
||||
"Key words": "Key words",
|
||||
"For instance: London": "For instance: London",
|
||||
"Radius": "Radius",
|
||||
"Today": "Today",
|
||||
"Tomorrow": "Tomorrow",
|
||||
"This weekend": "This weekend",
|
||||
"This week": "This week",
|
||||
"Next week": "Next week",
|
||||
"This month": "This month",
|
||||
"Next month": "Next month",
|
||||
"Any day": "Any day",
|
||||
"{nb} km": "{nb} km",
|
||||
"any distance": "any distance"
|
||||
}
|
||||
|
|
|
@ -730,5 +730,18 @@
|
|||
"Delete post": "Supprimer le billet",
|
||||
"Update post": "Mettre à jour le billet",
|
||||
"Posts": "Billets",
|
||||
"Register an account on {instanceName}!": "S'inscrire sur {instanceName} !"
|
||||
"Register an account on {instanceName}!": "S'inscrire sur {instanceName} !",
|
||||
"Key words": "Mots clés",
|
||||
"For instance: London": "Par exemple : Lyon",
|
||||
"Radius": "Rayon",
|
||||
"Today": "Aujourd'hui",
|
||||
"Tomorrow": "Demain",
|
||||
"This weekend": "Ce weekend",
|
||||
"This week": "Cette semaine",
|
||||
"Next week": "La semaine prochaine",
|
||||
"This month": "Ce mois-ci",
|
||||
"Next month": "Le mois-prochain",
|
||||
"Any day": "N'importe quand",
|
||||
"{nb} km": "{nb} km",
|
||||
"any distance": "peu importe"
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import TimeAgo from "javascript-time-ago";
|
|||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import { NotifierPlugin } from "./plugins/notifier";
|
||||
import { DateFnsPlugin } from "./plugins/dateFns";
|
||||
import filters from "./filters";
|
||||
import { i18n } from "./utils/i18n";
|
||||
import messages from "./i18n";
|
||||
|
@ -31,6 +32,7 @@ import(`javascript-time-ago/locale/${locale}`).then((localeFile) => {
|
|||
|
||||
Vue.use(Buefy);
|
||||
Vue.use(NotifierPlugin);
|
||||
Vue.use(DateFnsPlugin, { locale });
|
||||
Vue.use(filters);
|
||||
Vue.use(VueMeta);
|
||||
Vue.use(VueScrollTo);
|
||||
|
|
14
js/src/plugins/dateFns.ts
Normal file
14
js/src/plugins/dateFns.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import Vue from "vue";
|
||||
import Locale from "date-fns";
|
||||
|
||||
declare module "vue/types/vue" {
|
||||
interface Vue {
|
||||
$dateFnsLocale: Locale;
|
||||
}
|
||||
}
|
||||
|
||||
export function DateFnsPlugin(vue: typeof Vue, { locale }: { locale: string }): void {
|
||||
import(`date-fns/locale/${locale}/index.js`).then((localeEntity) => {
|
||||
Vue.prototype.$dateFnsLocale = localeEntity;
|
||||
});
|
||||
}
|
|
@ -8,7 +8,6 @@ const participations = () =>
|
|||
const editEvent = () => import(/* webpackChunkName: "edit-event" */ "@/views/Event/Edit.vue");
|
||||
const event = () => import(/* webpackChunkName: "event" */ "@/views/Event/Event.vue");
|
||||
const myEvents = () => import(/* webpackChunkName: "my-events" */ "@/views/Event/MyEvents.vue");
|
||||
const explore = () => import(/* webpackChunkName: "explore" */ "@/views/Event/Explore.vue");
|
||||
|
||||
export enum EventRouteName {
|
||||
EVENT_LIST = "EventList",
|
||||
|
@ -43,7 +42,7 @@ export const eventRoutes: RouteConfig[] = [
|
|||
{
|
||||
path: "/events/explore",
|
||||
name: EventRouteName.EXPLORE,
|
||||
component: explore,
|
||||
redirect: { name: "Search" },
|
||||
meta: { requiredAuth: false },
|
||||
},
|
||||
{
|
||||
|
|
|
@ -49,7 +49,7 @@ const router = new Router({
|
|||
...discussionRoutes,
|
||||
...errorRoutes,
|
||||
{
|
||||
path: "/search/:searchTerm/:searchType?",
|
||||
path: "/search/:searchTerm?/:searchType?",
|
||||
name: RouteName.SEARCH,
|
||||
component: Search,
|
||||
props: true,
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
<template>
|
||||
<div class="section container">
|
||||
<h1 class="title">{{ $t("Explore") }}</h1>
|
||||
<section class="hero">
|
||||
<div class="hero-body">
|
||||
<form @submit.prevent="submit()">
|
||||
<b-field
|
||||
:label="$t('Event')"
|
||||
grouped
|
||||
group-multiline
|
||||
label-position="on-border"
|
||||
label-for="search"
|
||||
>
|
||||
<b-input
|
||||
icon="magnify"
|
||||
type="search"
|
||||
id="search"
|
||||
size="is-large"
|
||||
expanded
|
||||
v-model="searchTerm"
|
||||
:placeholder="$t('For instance: London, Taekwondo, Architecture…')"
|
||||
/>
|
||||
<p class="control">
|
||||
<b-button
|
||||
@click="submit"
|
||||
type="is-info"
|
||||
size="is-large"
|
||||
v-bind:disabled="searchTerm.trim().length === 0"
|
||||
>{{ $t("Search") }}</b-button
|
||||
>
|
||||
</p>
|
||||
</b-field>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<section class="events-featured">
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<h2 class="title">{{ $t("Featured events") }}</h2>
|
||||
<div v-if="events.length > 0" class="columns is-multiline">
|
||||
<div class="column is-one-third-desktop" v-for="event in events" :key="event.uuid">
|
||||
<EventCard :event="event" />
|
||||
</div>
|
||||
</div>
|
||||
<b-message v-else-if="events.length === 0 && $apollo.loading === false" type="is-danger">{{
|
||||
$t("No events found")
|
||||
}}</b-message>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import EventCard from "@/components/Event/EventCard.vue";
|
||||
import { FETCH_EVENTS } from "@/graphql/event";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
EventCard,
|
||||
},
|
||||
apollo: {
|
||||
events: {
|
||||
query: FETCH_EVENTS,
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
title: this.$t("Explore") as string,
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Explore extends Vue {
|
||||
events: IEvent[] = [];
|
||||
|
||||
searchTerm = "";
|
||||
|
||||
submit() {
|
||||
this.$router.push({
|
||||
name: RouteName.SEARCH,
|
||||
params: { searchTerm: this.searchTerm },
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/variables.scss";
|
||||
|
||||
main > .container {
|
||||
background: $white;
|
||||
|
||||
.hero-body {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h1.title {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
h3.title {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.events-featured {
|
||||
margin: 25px auto;
|
||||
|
||||
.columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,16 +1,62 @@
|
|||
<template>
|
||||
<section class="container">
|
||||
<form @submit.prevent="processSearch" v-if="!actualTag">
|
||||
<b-field :label="$t('Event')">
|
||||
<b-input size="is-large" v-model="search" />
|
||||
<div class="section container">
|
||||
<h1 class="title">{{ $t("Explore") }}</h1>
|
||||
<section class="hero is-light">
|
||||
<div class="hero-body">
|
||||
<form @submit.prevent="submit()">
|
||||
<b-field :label="$t('Key words')" label-for="search" expanded>
|
||||
<b-input
|
||||
icon="magnify"
|
||||
type="search"
|
||||
id="search"
|
||||
size="is-large"
|
||||
expanded
|
||||
v-model="search"
|
||||
:placeholder="$t('For instance: London, Taekwondo, Architecture…')"
|
||||
/>
|
||||
</b-field>
|
||||
<b-field grouped group-multiline position="is-right" expanded>
|
||||
<b-field :label="$t('Location')" label-for="location">
|
||||
<address-auto-complete
|
||||
v-model="location"
|
||||
id="location"
|
||||
:placeholder="$t('For instance: London')"
|
||||
/>
|
||||
</b-field>
|
||||
<b-field :label="$t('Radius')" label-for="radius">
|
||||
<b-select v-model="radius" id="radius">
|
||||
<option
|
||||
v-for="(radiusOption, index) in radiusOptions"
|
||||
:key="index"
|
||||
:value="radiusOption"
|
||||
>{{ radiusString(radiusOption) }}</option
|
||||
>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="$t('Date')" label-for="date">
|
||||
<b-select v-model="when" id="date">
|
||||
<option v-for="(option, index) in options" :key="index" :value="option">{{
|
||||
option.label
|
||||
}}</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="$t('Location')">
|
||||
<address-auto-complete v-model="location" />
|
||||
</b-field>
|
||||
<b-button native-type="submit">{{ $t("Go") }}</b-button>
|
||||
</form>
|
||||
<b-loading :active.sync="$apollo.loading" />
|
||||
<b-tabs v-model="activeTab" type="is-boxed" class="searchTabs" @change="changeTab">
|
||||
</div>
|
||||
</section>
|
||||
<section class="events-featured" v-if="searchEvents.initial">
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<h2 class="title">{{ $t("Featured events") }}</h2>
|
||||
<div v-if="events.length > 0" class="columns is-multiline">
|
||||
<div class="column is-one-third-desktop" v-for="event in events" :key="event.uuid">
|
||||
<EventCard :event="event" />
|
||||
</div>
|
||||
</div>
|
||||
<b-message v-else-if="events.length === 0 && $apollo.loading === false" type="is-danger">{{
|
||||
$t("No events found")
|
||||
}}</b-message>
|
||||
</section>
|
||||
<b-tabs v-else v-model="activeTab" type="is-boxed" class="searchTabs">
|
||||
<b-tab-item>
|
||||
<template slot="header">
|
||||
<b-icon icon="calendar"></b-icon>
|
||||
|
@ -32,43 +78,45 @@
|
|||
$t("No events found")
|
||||
}}</b-message>
|
||||
</b-tab-item>
|
||||
<!-- <b-tab-item>-->
|
||||
<!-- <template slot="header">-->
|
||||
<!-- <b-icon icon="account-multiple"></b-icon>-->
|
||||
<!-- <span>-->
|
||||
<!-- {{ $t('Groups') }} <b-tag rounded>{{ searchGroups.total }}</b-tag>-->
|
||||
<!-- </span>-->
|
||||
<!-- </template>-->
|
||||
<!-- <div v-if="searchGroups.total > 0" class="columns is-multiline">-->
|
||||
<!-- <div class="column is-one-quarter-desktop is-half-mobile"-->
|
||||
<!-- v-for="group in groups"-->
|
||||
<!-- :key="group.uuid">-->
|
||||
<!-- <group-card :group="group" />-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <b-message v-else-if="$apollo.loading === false" type="is-danger">-->
|
||||
<!-- {{ $t('No groups found') }}-->
|
||||
<!-- </b-message>-->
|
||||
<!-- </b-tab-item>-->
|
||||
</b-tabs>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import { SEARCH_EVENTS, SEARCH_GROUPS } from "../graphql/search";
|
||||
import RouteName from "../router/name";
|
||||
import EventCard from "../components/Event/EventCard.vue";
|
||||
import GroupCard from "../components/Group/GroupCard.vue";
|
||||
import AddressAutoComplete from "../components/Event/AddressAutoComplete.vue";
|
||||
import { Group, IGroup } from "../types/actor";
|
||||
import { FETCH_EVENTS } from "../graphql/event";
|
||||
import { IEvent } from "../types/event.model";
|
||||
import RouteName from "../router/name";
|
||||
import { IAddress, Address } from "../types/address.model";
|
||||
import { SearchEvent, SearchGroup } from "../types/search.model";
|
||||
import AddressAutoComplete from "../components/Event/AddressAutoComplete.vue";
|
||||
import ngeohash from "ngeohash";
|
||||
import { SEARCH_EVENTS, SEARCH_GROUPS } from "../graphql/search";
|
||||
import { Paginate } from "../types/paginate";
|
||||
import {
|
||||
endOfToday,
|
||||
addDays,
|
||||
startOfDay,
|
||||
endOfDay,
|
||||
endOfWeek,
|
||||
addWeeks,
|
||||
startOfWeek,
|
||||
endOfMonth,
|
||||
addMonths,
|
||||
startOfMonth,
|
||||
eachWeekendOfInterval,
|
||||
} from "date-fns";
|
||||
|
||||
interface ISearchTimeOption {
|
||||
label: string;
|
||||
start?: Date;
|
||||
end?: Date | null;
|
||||
}
|
||||
|
||||
enum SearchTabs {
|
||||
EVENTS = 0,
|
||||
GROUPS = 1,
|
||||
PERSONS = 2, // not used right now
|
||||
}
|
||||
|
||||
const tabsName: { events: number; groups: number } = {
|
||||
|
@ -77,7 +125,12 @@ const tabsName: { events: number; groups: number } = {
|
|||
};
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
EventCard,
|
||||
AddressAutoComplete,
|
||||
},
|
||||
apollo: {
|
||||
events: FETCH_EVENTS,
|
||||
searchEvents: {
|
||||
query: SEARCH_EVENTS,
|
||||
variables() {
|
||||
|
@ -85,105 +138,117 @@ const tabsName: { events: number; groups: number } = {
|
|||
term: this.search,
|
||||
tags: this.actualTag,
|
||||
location: this.geohash,
|
||||
beginsOn: this.start,
|
||||
endsOn: this.end,
|
||||
radius: this.radius,
|
||||
};
|
||||
},
|
||||
debounce: 300,
|
||||
skip() {
|
||||
return !this.search && !this.actualTag;
|
||||
return !this.search && !this.actualTag && !this.geohash && this.end === null;
|
||||
},
|
||||
},
|
||||
searchGroups: {
|
||||
query: SEARCH_GROUPS,
|
||||
variables() {
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
searchText: this.search,
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
title: this.$t("Explore events") as string,
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.search || this.isURL(this.search);
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
GroupCard,
|
||||
EventCard,
|
||||
AddressAutoComplete,
|
||||
},
|
||||
})
|
||||
export default class Search extends Vue {
|
||||
@Prop({ type: String, required: false }) searchTerm!: string;
|
||||
|
||||
@Prop({ type: String, required: false }) tag!: string;
|
||||
|
||||
@Prop({ type: String, required: false, default: "" }) searchTerm!: string;
|
||||
@Prop({ type: String, required: false, default: "events" }) searchType!: "events" | "groups";
|
||||
|
||||
searchEvents: SearchEvent = { total: 0, elements: [] };
|
||||
events: IEvent[] = [];
|
||||
|
||||
searchGroups: SearchGroup = { total: 0, elements: [] };
|
||||
searchEvents: Paginate<IEvent> & { initial: boolean } = { total: 0, elements: [], initial: true };
|
||||
|
||||
search = this.searchTerm;
|
||||
|
||||
activeTab: SearchTabs = tabsName[this.searchType];
|
||||
|
||||
search: string = this.searchTerm;
|
||||
actualTag: string = this.tag;
|
||||
location: IAddress = new Address();
|
||||
|
||||
@Watch("searchEvents")
|
||||
async redirectURLToEvent() {
|
||||
if (this.searchEvents.total === 1 && this.isURL(this.searchTerm)) {
|
||||
return await this.$router.replace({
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: this.searchEvents.elements[0].uuid },
|
||||
});
|
||||
}
|
||||
}
|
||||
options: ISearchTimeOption[] = [
|
||||
{
|
||||
label: this.$t("Today") as string,
|
||||
start: new Date(),
|
||||
end: endOfToday(),
|
||||
},
|
||||
{
|
||||
label: this.$t("Tomorrow") as string,
|
||||
start: startOfDay(addDays(new Date(), 1)),
|
||||
end: endOfDay(addDays(new Date(), 1)),
|
||||
},
|
||||
{
|
||||
label: this.$t("This weekend") as string,
|
||||
start: this.weekend.start,
|
||||
end: this.weekend.end,
|
||||
},
|
||||
{
|
||||
label: this.$t("This week") as string,
|
||||
start: new Date(),
|
||||
end: endOfWeek(new Date(), { locale: this.$dateFnsLocale }),
|
||||
},
|
||||
{
|
||||
label: this.$t("Next week") as string,
|
||||
start: startOfWeek(addWeeks(new Date(), 1), { locale: this.$dateFnsLocale }),
|
||||
end: endOfWeek(addWeeks(new Date(), 1), { locale: this.$dateFnsLocale }),
|
||||
},
|
||||
{
|
||||
label: this.$t("This month") as string,
|
||||
start: new Date(),
|
||||
end: endOfMonth(new Date()),
|
||||
},
|
||||
{
|
||||
label: this.$t("Next month") as string,
|
||||
start: startOfMonth(addMonths(new Date(), 1)),
|
||||
end: endOfMonth(addMonths(new Date(), 1)),
|
||||
},
|
||||
{
|
||||
label: this.$t("Any day") as string,
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
changeTab(index: number) {
|
||||
switch (index) {
|
||||
case SearchTabs.EVENTS:
|
||||
this.$router.push({
|
||||
name: RouteName.SEARCH,
|
||||
params: { searchTerm: this.searchTerm, searchType: "events" },
|
||||
});
|
||||
break;
|
||||
case SearchTabs.GROUPS:
|
||||
this.$router.push({
|
||||
name: RouteName.SEARCH,
|
||||
params: { searchTerm: this.searchTerm, searchType: "groups" },
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
when: ISearchTimeOption = {
|
||||
label: this.$t("Any day") as string,
|
||||
start: undefined,
|
||||
end: null,
|
||||
};
|
||||
|
||||
@Watch("search")
|
||||
changeTabForResult() {
|
||||
if (this.searchEvents.total === 0 && this.searchGroups.total > 0) {
|
||||
this.activeTab = SearchTabs.GROUPS;
|
||||
}
|
||||
if (this.searchGroups.total === 0 && this.searchEvents.total > 0) {
|
||||
this.activeTab = SearchTabs.EVENTS;
|
||||
}
|
||||
radiusString = (radius: number | null) => {
|
||||
if (radius) {
|
||||
return this.$tc("{nb} km", radius, { nb: radius });
|
||||
}
|
||||
return this.$t("any distance");
|
||||
};
|
||||
|
||||
@Watch("search")
|
||||
@Watch("$route")
|
||||
async loadSearch() {
|
||||
(await this.$apollo.queries.searchEvents.refetch()) &&
|
||||
this.$apollo.queries.searchGroups.refetch();
|
||||
}
|
||||
radiusOptions: (number | null)[] = [1, 5, 10, 25, 50, 100, 150, null];
|
||||
|
||||
get groups(): IGroup[] {
|
||||
return this.searchGroups.elements.map((group) => Object.assign(new Group(), group));
|
||||
}
|
||||
radius: number | undefined = undefined;
|
||||
|
||||
isURL(url: string): boolean {
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
return (a.host && a.host !== window.location.host) as boolean;
|
||||
}
|
||||
|
||||
processSearch() {
|
||||
submit() {
|
||||
this.$apollo.queries.searchEvents.refetch();
|
||||
}
|
||||
|
||||
@Watch("searchTerm")
|
||||
updateSearchTerm() {
|
||||
this.search = this.searchTerm;
|
||||
}
|
||||
|
||||
get weekend(): { start: Date; end: Date } {
|
||||
const now = new Date();
|
||||
const endOfWeekDate = endOfWeek(now, { locale: this.$dateFnsLocale });
|
||||
const startOfWeekDate = startOfWeek(now, { locale: this.$dateFnsLocale });
|
||||
const [start, end] = eachWeekendOfInterval({ start: startOfWeekDate, end: endOfWeekDate });
|
||||
return { start: startOfDay(start), end: endOfDay(end) };
|
||||
}
|
||||
|
||||
get geohash() {
|
||||
if (this.location && this.location.geom) {
|
||||
const [lon, lat] = this.location.geom.split(";");
|
||||
|
@ -191,16 +256,47 @@ export default class Search extends Vue {
|
|||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get start(): Date | undefined {
|
||||
return this.when.start;
|
||||
}
|
||||
|
||||
get end(): Date | undefined | null {
|
||||
return this.when.end;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "~bulma/sass/utilities/_all";
|
||||
@import "~bulma/sass/components/tabs";
|
||||
@import "~buefy/src/scss/components/tabs";
|
||||
@import "~bulma/sass/elements/tag";
|
||||
|
||||
.searchTabs .tab-content {
|
||||
background: #fff;
|
||||
min-height: 10em;
|
||||
<style scoped lang="scss">
|
||||
@import "@/variables.scss";
|
||||
|
||||
main > .container {
|
||||
background: $white;
|
||||
|
||||
.hero-body {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h1.title {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
h3.title {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.events-featured {
|
||||
margin: 25px auto;
|
||||
|
||||
.columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
/deep/ .field label.label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4406,6 +4406,11 @@ date-fns@^1.27.2:
|
|||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
|
||||
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
|
||||
|
||||
date-fns@^2.15.0:
|
||||
version "2.15.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.15.0.tgz#424de6b3778e4e69d3ff27046ec136af58ae5d5f"
|
||||
integrity sha512-ZCPzAMJZn3rNUvvQIMlXhDr4A+Ar07eLeGsGREoWU19a3Pqf5oYa+ccd+B3F6XVtQY6HANMFdOQ8A+ipFnvJdQ==
|
||||
|
||||
de-indent@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
||||
|
|
|
@ -51,6 +51,8 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
|
|||
arg(:radius, :float, default_value: 50)
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
arg(:begins_on, :datetime)
|
||||
arg(:ends_on, :datetime)
|
||||
|
||||
resolve(&Search.search_events/3)
|
||||
end
|
||||
|
|
|
@ -463,9 +463,10 @@ defmodule Mobilizon.Events do
|
|||
term
|
||||
|> normalize_search_string()
|
||||
|> events_for_search_query()
|
||||
|> events_for_begins_on(args)
|
||||
|> events_for_ends_on(args)
|
||||
|> events_for_tags(args)
|
||||
|> events_for_location(args)
|
||||
|> filter_future_events(true)
|
||||
|> filter_local_or_from_followed_instances_events()
|
||||
|> order_by([q], asc: q.id)
|
||||
|> Page.build_page(page, limit)
|
||||
|
@ -1296,6 +1297,29 @@ defmodule Mobilizon.Events do
|
|||
)
|
||||
end
|
||||
|
||||
@spec events_for_begins_on(Ecto.Query.t(), map()) :: Ecto.Query.t()
|
||||
defp events_for_begins_on(query, args) do
|
||||
begins_on = Map.get(args, :begins_on, DateTime.utc_now())
|
||||
|
||||
query
|
||||
|> where([q], q.begins_on >= ^begins_on)
|
||||
end
|
||||
|
||||
@spec events_for_ends_on(Ecto.Query.t(), map()) :: Ecto.Query.t()
|
||||
defp events_for_ends_on(query, args) do
|
||||
ends_on = Map.get(args, :ends_on)
|
||||
|
||||
if is_nil(ends_on),
|
||||
do: query,
|
||||
else:
|
||||
where(
|
||||
query,
|
||||
[q],
|
||||
(is_nil(q.ends_on) and q.begins_on <= ^ends_on) or
|
||||
q.ends_on <= ^ends_on
|
||||
)
|
||||
end
|
||||
|
||||
@spec events_for_tags(Ecto.Query.t(), map()) :: Ecto.Query.t()
|
||||
defp events_for_tags(query, %{tags: tags}) when is_valid_string?(tags) do
|
||||
query
|
||||
|
@ -1307,6 +1331,9 @@ defmodule Mobilizon.Events do
|
|||
defp events_for_tags(query, _args), do: query
|
||||
|
||||
@spec events_for_location(Ecto.Query.t(), map()) :: Ecto.Query.t()
|
||||
defp events_for_location(query, %{radius: radius}) when is_nil(radius),
|
||||
do: query
|
||||
|
||||
defp events_for_location(query, %{location: location, radius: radius})
|
||||
when is_valid_string?(location) and not is_nil(radius) do
|
||||
with {lon, lat} <- Geohax.decode(location),
|
||||
|
|
|
@ -15,8 +15,8 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
|
|||
|
||||
describe "search events/3" do
|
||||
@search_events_query """
|
||||
query SearchEvents($location: String, $radius: Float, $tags: String, $term: String) {
|
||||
searchEvents(location: $location, radius: $radius, tags: $tags, term: $term) {
|
||||
query SearchEvents($location: String, $radius: Float, $tags: String, $term: String, $beginsOn: DateTime, $endsOn: DateTime) {
|
||||
searchEvents(location: $location, radius: $radius, tags: $tags, term: $term, beginsOn: $beginsOn, endsOn: $endsOn) {
|
||||
total,
|
||||
elements {
|
||||
id
|
||||
|
@ -145,6 +145,41 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
|
|||
event.uuid
|
||||
end
|
||||
|
||||
test "finds events by begins_on and ends_on", %{conn: conn} do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
# TODO
|
||||
event =
|
||||
insert(:event,
|
||||
title: "Tour du monde",
|
||||
begins_on: DateTime.add(now, 3600 * 24 * 3),
|
||||
ends_on: DateTime.add(now, 3600 * 24 * 10)
|
||||
)
|
||||
|
||||
insert(:event,
|
||||
title: "Autre événement",
|
||||
begins_on: DateTime.add(now, 3600 * 24 * 30),
|
||||
ends_on: nil
|
||||
)
|
||||
|
||||
Workers.BuildSearch.insert_search_event(event)
|
||||
|
||||
res =
|
||||
AbsintheHelpers.graphql_query(conn,
|
||||
query: @search_events_query,
|
||||
variables: %{
|
||||
beginsOn: now |> DateTime.add(86_400) |> DateTime.to_iso8601(),
|
||||
endsOn: now |> DateTime.add(1_728_000) |> DateTime.to_iso8601()
|
||||
}
|
||||
)
|
||||
|
||||
assert res["errors"] == nil
|
||||
assert res["data"]["searchEvents"]["total"] == 1
|
||||
|
||||
assert hd(res["data"]["searchEvents"]["elements"])["uuid"] ==
|
||||
event.uuid
|
||||
end
|
||||
|
||||
test "finds events with multiple criteria", %{conn: conn} do
|
||||
{lon, lat} = {45.75, 4.85}
|
||||
point = %Geo.Point{coordinates: {lon, lat}, srid: 4326}
|
||||
|
|
Loading…
Reference in a new issue