diff --git a/js/package.json b/js/package.json index 97609c267..6aa5d3a05 100644 --- a/js/package.json +++ b/js/package.json @@ -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", diff --git a/js/src/common.scss b/js/src/common.scss index 373e1d810..db448b2f5 100644 --- a/js/src/common.scss +++ b/js/src/common.scss @@ -26,7 +26,7 @@ input.input { } .section { - padding: 1rem 2rem 4rem; + padding: 1rem 1% 4rem; } figure img.is-rounded { diff --git a/js/src/components/Event/AddressAutoComplete.vue b/js/src/components/Event/AddressAutoComplete.vue index 16e6c96df..83292ce5c 100644 --- a/js/src/components/Event/AddressAutoComplete.vue +++ b/js/src/components/Event/AddressAutoComplete.vue @@ -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[] = []; diff --git a/js/src/graphql/search.ts b/js/src/graphql/search.ts index c517879ae..6f8b516b0 100644 --- a/js/src/graphql/search.ts +++ b/js/src/graphql/search.ts @@ -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 diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index a6e8bf5fd..398e0761a 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -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" } diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index e464c0e20..7eb4b669b 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -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" } diff --git a/js/src/main.ts b/js/src/main.ts index 784e26f8f..b425b56ee 100644 --- a/js/src/main.ts +++ b/js/src/main.ts @@ -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); diff --git a/js/src/plugins/dateFns.ts b/js/src/plugins/dateFns.ts new file mode 100644 index 000000000..c989092f6 --- /dev/null +++ b/js/src/plugins/dateFns.ts @@ -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; + }); +} diff --git a/js/src/router/event.ts b/js/src/router/event.ts index 0431c9e30..e287e893a 100644 --- a/js/src/router/event.ts +++ b/js/src/router/event.ts @@ -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 }, }, { diff --git a/js/src/router/index.ts b/js/src/router/index.ts index 19091f961..1084ede7b 100644 --- a/js/src/router/index.ts +++ b/js/src/router/index.ts @@ -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, diff --git a/js/src/views/Event/Explore.vue b/js/src/views/Event/Explore.vue deleted file mode 100644 index 2c8981e17..000000000 --- a/js/src/views/Event/Explore.vue +++ /dev/null @@ -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> diff --git a/js/src/views/Search.vue b/js/src/views/Search.vue index 8b5af747d..3a10508c4 100644 --- a/js/src/views/Search.vue +++ b/js/src/views/Search.vue @@ -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" /> - </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 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> + </form> + </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; - }, - }, - searchGroups: { - query: SEARCH_GROUPS, - variables() { - return { - searchText: this.search, - }; - }, - skip() { - return !this.search || this.isURL(this.search); + return !this.search && !this.actualTag && !this.geohash && this.end === null; }, }, }, - components: { - GroupCard, - EventCard, - AddressAutoComplete, + metaInfo() { + return { + // 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", + }; }, }) 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, + }, + ]; + + when: ISearchTimeOption = { + label: this.$t("Any day") as string, + start: undefined, + end: null, + }; + + radiusString = (radius: number | null) => { + if (radius) { + return this.$tc("{nb} km", radius, { nb: radius }); } - } + return this.$t("any distance"); + }; - 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; - } - } + radiusOptions: (number | null)[] = [1, 5, 10, 25, 50, 100, 150, 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; - } - } + radius: number | undefined = undefined; - @Watch("search") - @Watch("$route") - async loadSearch() { - (await this.$apollo.queries.searchEvents.refetch()) && - this.$apollo.queries.searchGroups.refetch(); - } - - get groups(): IGroup[] { - return this.searchGroups.elements.map((group) => Object.assign(new Group(), group)); - } - - 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> diff --git a/js/yarn.lock b/js/yarn.lock index ce0a49a75..d67b50900 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -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" diff --git a/lib/graphql/schema/search.ex b/lib/graphql/schema/search.ex index d0d71247d..02859d686 100644 --- a/lib/graphql/schema/search.ex +++ b/lib/graphql/schema/search.ex @@ -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 diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index dbe2407e9..af877fb3f 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -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), diff --git a/test/graphql/resolvers/search_test.exs b/test/graphql/resolvers/search_test.exs index 9a4103a68..cb5f634a9 100644 --- a/test/graphql/resolvers/search_test.exs +++ b/test/graphql/resolvers/search_test.exs @@ -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}