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",
|
"buefy": "^0.8.2",
|
||||||
"bulma-divider": "^0.2.0",
|
"bulma-divider": "^0.2.0",
|
||||||
"core-js": "^3.6.4",
|
"core-js": "^3.6.4",
|
||||||
|
"date-fns": "^2.15.0",
|
||||||
"eslint-plugin-cypress": "^2.10.3",
|
"eslint-plugin-cypress": "^2.10.3",
|
||||||
"graphql": "^15.0.0",
|
"graphql": "^15.0.0",
|
||||||
"graphql-tag": "^2.10.3",
|
"graphql-tag": "^2.10.3",
|
||||||
|
|
|
@ -26,7 +26,7 @@ input.input {
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
padding: 1rem 2rem 4rem;
|
padding: 1rem 1% 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
figure img.is-rounded {
|
figure img.is-rounded {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<b-autocomplete
|
<b-autocomplete
|
||||||
:data="addressData"
|
:data="addressData"
|
||||||
v-model="queryText"
|
v-model="queryText"
|
||||||
:placeholder="$t('e.g. 10 Rue Jangot')"
|
:placeholder="placeholder || $t('e.g. 10 Rue Jangot')"
|
||||||
field="fullName"
|
field="fullName"
|
||||||
:loading="isFetching"
|
:loading="isFetching"
|
||||||
@typing="fetchAsyncData"
|
@typing="fetchAsyncData"
|
||||||
|
@ -45,6 +45,7 @@ import { IConfig } from "../../types/config.model";
|
||||||
})
|
})
|
||||||
export default class AddressAutoComplete extends Vue {
|
export default class AddressAutoComplete extends Vue {
|
||||||
@Prop({ required: true }) value!: IAddress;
|
@Prop({ required: true }) value!: IAddress;
|
||||||
|
@Prop({ required: false }) placeholder!: string;
|
||||||
|
|
||||||
addressData: IAddress[] = [];
|
addressData: IAddress[] = [];
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,22 @@
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
|
|
||||||
export const SEARCH_EVENTS = gql`
|
export const SEARCH_EVENTS = gql`
|
||||||
query SearchEvents($location: String, $radius: Float, $tags: String, $term: String) {
|
query SearchEvents(
|
||||||
searchEvents(location: $location, radius: $radius, tags: $tags, term: $term) {
|
$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
|
total
|
||||||
elements {
|
elements {
|
||||||
title
|
title
|
||||||
|
|
|
@ -730,5 +730,18 @@
|
||||||
"Delete post": "Delete post",
|
"Delete post": "Delete post",
|
||||||
"Update post": "Update post",
|
"Update post": "Update post",
|
||||||
"Posts": "Posts",
|
"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",
|
"Delete post": "Supprimer le billet",
|
||||||
"Update post": "Mettre à jour le billet",
|
"Update post": "Mettre à jour le billet",
|
||||||
"Posts": "Billets",
|
"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 App from "./App.vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
import { NotifierPlugin } from "./plugins/notifier";
|
import { NotifierPlugin } from "./plugins/notifier";
|
||||||
|
import { DateFnsPlugin } from "./plugins/dateFns";
|
||||||
import filters from "./filters";
|
import filters from "./filters";
|
||||||
import { i18n } from "./utils/i18n";
|
import { i18n } from "./utils/i18n";
|
||||||
import messages from "./i18n";
|
import messages from "./i18n";
|
||||||
|
@ -31,6 +32,7 @@ import(`javascript-time-ago/locale/${locale}`).then((localeFile) => {
|
||||||
|
|
||||||
Vue.use(Buefy);
|
Vue.use(Buefy);
|
||||||
Vue.use(NotifierPlugin);
|
Vue.use(NotifierPlugin);
|
||||||
|
Vue.use(DateFnsPlugin, { locale });
|
||||||
Vue.use(filters);
|
Vue.use(filters);
|
||||||
Vue.use(VueMeta);
|
Vue.use(VueMeta);
|
||||||
Vue.use(VueScrollTo);
|
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 editEvent = () => import(/* webpackChunkName: "edit-event" */ "@/views/Event/Edit.vue");
|
||||||
const event = () => import(/* webpackChunkName: "event" */ "@/views/Event/Event.vue");
|
const event = () => import(/* webpackChunkName: "event" */ "@/views/Event/Event.vue");
|
||||||
const myEvents = () => import(/* webpackChunkName: "my-events" */ "@/views/Event/MyEvents.vue");
|
const myEvents = () => import(/* webpackChunkName: "my-events" */ "@/views/Event/MyEvents.vue");
|
||||||
const explore = () => import(/* webpackChunkName: "explore" */ "@/views/Event/Explore.vue");
|
|
||||||
|
|
||||||
export enum EventRouteName {
|
export enum EventRouteName {
|
||||||
EVENT_LIST = "EventList",
|
EVENT_LIST = "EventList",
|
||||||
|
@ -43,7 +42,7 @@ export const eventRoutes: RouteConfig[] = [
|
||||||
{
|
{
|
||||||
path: "/events/explore",
|
path: "/events/explore",
|
||||||
name: EventRouteName.EXPLORE,
|
name: EventRouteName.EXPLORE,
|
||||||
component: explore,
|
redirect: { name: "Search" },
|
||||||
meta: { requiredAuth: false },
|
meta: { requiredAuth: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -49,7 +49,7 @@ const router = new Router({
|
||||||
...discussionRoutes,
|
...discussionRoutes,
|
||||||
...errorRoutes,
|
...errorRoutes,
|
||||||
{
|
{
|
||||||
path: "/search/:searchTerm/:searchType?",
|
path: "/search/:searchTerm?/:searchType?",
|
||||||
name: RouteName.SEARCH,
|
name: RouteName.SEARCH,
|
||||||
component: Search,
|
component: Search,
|
||||||
props: true,
|
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>
|
<template>
|
||||||
<section class="container">
|
<div class="section container">
|
||||||
<form @submit.prevent="processSearch" v-if="!actualTag">
|
<h1 class="title">{{ $t("Explore") }}</h1>
|
||||||
<b-field :label="$t('Event')">
|
<section class="hero is-light">
|
||||||
<b-input size="is-large" v-model="search" />
|
<div class="hero-body">
|
||||||
</b-field>
|
<form @submit.prevent="submit()">
|
||||||
<b-field :label="$t('Location')">
|
<b-field :label="$t('Key words')" label-for="search" expanded>
|
||||||
<address-auto-complete v-model="location" />
|
<b-input
|
||||||
</b-field>
|
icon="magnify"
|
||||||
<b-button native-type="submit">{{ $t("Go") }}</b-button>
|
type="search"
|
||||||
</form>
|
id="search"
|
||||||
<b-loading :active.sync="$apollo.loading" />
|
size="is-large"
|
||||||
<b-tabs v-model="activeTab" type="is-boxed" class="searchTabs" @change="changeTab">
|
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>
|
<b-tab-item>
|
||||||
<template slot="header">
|
<template slot="header">
|
||||||
<b-icon icon="calendar"></b-icon>
|
<b-icon icon="calendar"></b-icon>
|
||||||
|
@ -32,43 +78,45 @@
|
||||||
$t("No events found")
|
$t("No events found")
|
||||||
}}</b-message>
|
}}</b-message>
|
||||||
</b-tab-item>
|
</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>
|
</b-tabs>
|
||||||
</section>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
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 EventCard from "../components/Event/EventCard.vue";
|
||||||
import GroupCard from "../components/Group/GroupCard.vue";
|
import { FETCH_EVENTS } from "../graphql/event";
|
||||||
import AddressAutoComplete from "../components/Event/AddressAutoComplete.vue";
|
import { IEvent } from "../types/event.model";
|
||||||
import { Group, IGroup } from "../types/actor";
|
import RouteName from "../router/name";
|
||||||
import { IAddress, Address } from "../types/address.model";
|
import { IAddress, Address } from "../types/address.model";
|
||||||
import { SearchEvent, SearchGroup } from "../types/search.model";
|
import { SearchEvent, SearchGroup } from "../types/search.model";
|
||||||
|
import AddressAutoComplete from "../components/Event/AddressAutoComplete.vue";
|
||||||
import ngeohash from "ngeohash";
|
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 {
|
enum SearchTabs {
|
||||||
EVENTS = 0,
|
EVENTS = 0,
|
||||||
GROUPS = 1,
|
GROUPS = 1,
|
||||||
PERSONS = 2, // not used right now
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabsName: { events: number; groups: number } = {
|
const tabsName: { events: number; groups: number } = {
|
||||||
|
@ -77,7 +125,12 @@ const tabsName: { events: number; groups: number } = {
|
||||||
};
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
components: {
|
||||||
|
EventCard,
|
||||||
|
AddressAutoComplete,
|
||||||
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
|
events: FETCH_EVENTS,
|
||||||
searchEvents: {
|
searchEvents: {
|
||||||
query: SEARCH_EVENTS,
|
query: SEARCH_EVENTS,
|
||||||
variables() {
|
variables() {
|
||||||
|
@ -85,105 +138,117 @@ const tabsName: { events: number; groups: number } = {
|
||||||
term: this.search,
|
term: this.search,
|
||||||
tags: this.actualTag,
|
tags: this.actualTag,
|
||||||
location: this.geohash,
|
location: this.geohash,
|
||||||
|
beginsOn: this.start,
|
||||||
|
endsOn: this.end,
|
||||||
|
radius: this.radius,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
debounce: 300,
|
||||||
skip() {
|
skip() {
|
||||||
return !this.search && !this.actualTag;
|
return !this.search && !this.actualTag && !this.geohash && this.end === null;
|
||||||
},
|
|
||||||
},
|
|
||||||
searchGroups: {
|
|
||||||
query: SEARCH_GROUPS,
|
|
||||||
variables() {
|
|
||||||
return {
|
|
||||||
searchText: this.search,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
skip() {
|
|
||||||
return !this.search || this.isURL(this.search);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
metaInfo() {
|
||||||
GroupCard,
|
return {
|
||||||
EventCard,
|
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||||
AddressAutoComplete,
|
title: this.$t("Explore events") as string,
|
||||||
|
// all titles will be injected into this template
|
||||||
|
titleTemplate: "%s | Mobilizon",
|
||||||
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class Search extends Vue {
|
export default class Search extends Vue {
|
||||||
@Prop({ type: String, required: false }) searchTerm!: string;
|
@Prop({ type: String, required: false, default: "" }) searchTerm!: string;
|
||||||
|
|
||||||
@Prop({ type: String, required: false }) tag!: string;
|
|
||||||
|
|
||||||
@Prop({ type: String, required: false, default: "events" }) searchType!: "events" | "groups";
|
@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];
|
activeTab: SearchTabs = tabsName[this.searchType];
|
||||||
|
|
||||||
search: string = this.searchTerm;
|
|
||||||
actualTag: string = this.tag;
|
|
||||||
location: IAddress = new Address();
|
location: IAddress = new Address();
|
||||||
|
|
||||||
@Watch("searchEvents")
|
options: ISearchTimeOption[] = [
|
||||||
async redirectURLToEvent() {
|
{
|
||||||
if (this.searchEvents.total === 1 && this.isURL(this.searchTerm)) {
|
label: this.$t("Today") as string,
|
||||||
return await this.$router.replace({
|
start: new Date(),
|
||||||
name: RouteName.EVENT,
|
end: endOfToday(),
|
||||||
params: { uuid: this.searchEvents.elements[0].uuid },
|
},
|
||||||
});
|
{
|
||||||
|
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) {
|
radiusOptions: (number | null)[] = [1, 5, 10, 25, 50, 100, 150, null];
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("search")
|
radius: number | undefined = undefined;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("search")
|
submit() {
|
||||||
@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() {
|
|
||||||
this.$apollo.queries.searchEvents.refetch();
|
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() {
|
get geohash() {
|
||||||
if (this.location && this.location.geom) {
|
if (this.location && this.location.geom) {
|
||||||
const [lon, lat] = this.location.geom.split(";");
|
const [lon, lat] = this.location.geom.split(";");
|
||||||
|
@ -191,16 +256,47 @@ export default class Search extends Vue {
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get start(): Date | undefined {
|
||||||
|
return this.when.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
get end(): Date | undefined | null {
|
||||||
|
return this.when.end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</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 {
|
<style scoped lang="scss">
|
||||||
background: #fff;
|
@import "@/variables.scss";
|
||||||
min-height: 10em;
|
|
||||||
|
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>
|
</style>
|
||||||
|
|
|
@ -4406,6 +4406,11 @@ date-fns@^1.27.2:
|
||||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
|
||||||
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
|
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:
|
de-indent@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
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(:radius, :float, default_value: 50)
|
||||||
arg(:page, :integer, default_value: 1)
|
arg(:page, :integer, default_value: 1)
|
||||||
arg(:limit, :integer, default_value: 10)
|
arg(:limit, :integer, default_value: 10)
|
||||||
|
arg(:begins_on, :datetime)
|
||||||
|
arg(:ends_on, :datetime)
|
||||||
|
|
||||||
resolve(&Search.search_events/3)
|
resolve(&Search.search_events/3)
|
||||||
end
|
end
|
||||||
|
|
|
@ -463,9 +463,10 @@ defmodule Mobilizon.Events do
|
||||||
term
|
term
|
||||||
|> normalize_search_string()
|
|> normalize_search_string()
|
||||||
|> events_for_search_query()
|
|> events_for_search_query()
|
||||||
|
|> events_for_begins_on(args)
|
||||||
|
|> events_for_ends_on(args)
|
||||||
|> events_for_tags(args)
|
|> events_for_tags(args)
|
||||||
|> events_for_location(args)
|
|> events_for_location(args)
|
||||||
|> filter_future_events(true)
|
|
||||||
|> filter_local_or_from_followed_instances_events()
|
|> filter_local_or_from_followed_instances_events()
|
||||||
|> order_by([q], asc: q.id)
|
|> order_by([q], asc: q.id)
|
||||||
|> Page.build_page(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
|
@ -1296,6 +1297,29 @@ defmodule Mobilizon.Events do
|
||||||
)
|
)
|
||||||
end
|
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()
|
@spec events_for_tags(Ecto.Query.t(), map()) :: Ecto.Query.t()
|
||||||
defp events_for_tags(query, %{tags: tags}) when is_valid_string?(tags) do
|
defp events_for_tags(query, %{tags: tags}) when is_valid_string?(tags) do
|
||||||
query
|
query
|
||||||
|
@ -1307,6 +1331,9 @@ defmodule Mobilizon.Events do
|
||||||
defp events_for_tags(query, _args), do: query
|
defp events_for_tags(query, _args), do: query
|
||||||
|
|
||||||
@spec events_for_location(Ecto.Query.t(), map()) :: Ecto.Query.t()
|
@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})
|
defp events_for_location(query, %{location: location, radius: radius})
|
||||||
when is_valid_string?(location) and not is_nil(radius) do
|
when is_valid_string?(location) and not is_nil(radius) do
|
||||||
with {lon, lat} <- Geohax.decode(location),
|
with {lon, lat} <- Geohax.decode(location),
|
||||||
|
|
|
@ -15,8 +15,8 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
|
||||||
|
|
||||||
describe "search events/3" do
|
describe "search events/3" do
|
||||||
@search_events_query """
|
@search_events_query """
|
||||||
query SearchEvents($location: String, $radius: Float, $tags: String, $term: String) {
|
query SearchEvents($location: String, $radius: Float, $tags: String, $term: String, $beginsOn: DateTime, $endsOn: DateTime) {
|
||||||
searchEvents(location: $location, radius: $radius, tags: $tags, term: $term) {
|
searchEvents(location: $location, radius: $radius, tags: $tags, term: $term, beginsOn: $beginsOn, endsOn: $endsOn) {
|
||||||
total,
|
total,
|
||||||
elements {
|
elements {
|
||||||
id
|
id
|
||||||
|
@ -145,6 +145,41 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
|
||||||
event.uuid
|
event.uuid
|
||||||
end
|
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
|
test "finds events with multiple criteria", %{conn: conn} do
|
||||||
{lon, lat} = {45.75, 4.85}
|
{lon, lat} = {45.75, 4.85}
|
||||||
point = %Geo.Point{coordinates: {lon, lat}, srid: 4326}
|
point = %Geo.Point{coordinates: {lon, lat}, srid: 4326}
|
||||||
|
|
Loading…
Reference in a new issue