Fix #1476 by updating the search interface

- Removed "Everything" choice for search, default to "Events"
- Add radio button for search type selection (events, activities, groups)
- Label now have a cursor pointer
- Remove the confusing concept of EVENTS, SHORTEVENTS and LONGEVENTS. There is only EVENTS and LONGEVENTS.
This commit is contained in:
Massedil 2024-10-28 19:41:30 +01:00
parent ce2d4f44cb
commit 042b0f097f
3 changed files with 133 additions and 313 deletions

View file

@ -186,18 +186,7 @@
v-model:location="location" v-model:location="location"
/> />
<li class="m-auto" v-if="islongEvents"> <li class="m-auto">
<router-link
:to="{
...$route,
name: RouteName.SEARCH,
query: { ...$route.query, contentType: 'SHORTEVENTS' },
}"
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
>{{ t("Events") }}</router-link
>
</li>
<li class="m-auto" v-else>
<router-link <router-link
:to="{ :to="{
...$route, ...$route,

View file

@ -132,9 +132,7 @@ export enum SearchTabs {
} }
export enum ContentType { export enum ContentType {
ALL = "ALL",
EVENTS = "EVENTS", EVENTS = "EVENTS",
SHORTEVENTS = "SHORTEVENTS",
LONGEVENTS = "LONGEVENTS", LONGEVENTS = "LONGEVENTS",
GROUPS = "GROUPS", GROUPS = "GROUPS",
} }

View file

@ -34,23 +34,26 @@
<li <li
v-for="content in contentTypeMapping" v-for="content in contentTypeMapping"
:key="content.contentType" :key="content.contentType"
class="flex gap-1" class="flex gap-1 items-center"
> >
<Magnify <input
v-if="content.contentType === ContentType.ALL" :id="'contentType' + content.contentType"
:size="24" v-model="contentType"
type="radio"
name="contentType"
:value="content.contentType"
class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600"
/> />
<label
:for="'contentType' + content.contentType"
class="cursor-pointer w-full font-medium text-gray-900 dark:text-gray-300 flex gap-1"
>
<Calendar <Calendar
v-if="content.contentType === ContentType.EVENTS" v-if="content.contentType === ContentType.EVENTS"
:size="24" :size="24"
/> />
<Calendar
v-if="content.contentType === ContentType.SHORTEVENTS"
:size="24"
/>
<CalendarStar <CalendarStar
v-if="content.contentType === ContentType.LONGEVENTS" v-if="content.contentType === ContentType.LONGEVENTS"
:size="24" :size="24"
@ -59,16 +62,8 @@
<AccountMultiple <AccountMultiple
v-if="content.contentType === ContentType.GROUPS" v-if="content.contentType === ContentType.GROUPS"
:size="24" :size="24"
/> /><span>{{ content.label }}</span></label
<router-link
:to="{
...$route,
query: { ...$route.query, contentType: content.contentType },
}"
> >
{{ content.label }}
</router-link>
</li> </li>
</ul> </ul>
@ -90,7 +85,7 @@
/> />
<label <label
for="selfTarget" for="selfTarget"
class="ml-3 font-medium text-gray-900 dark:text-gray-300" class="cursor-pointer ml-3 font-medium text-gray-900 dark:text-gray-300"
>{{ t("From this instance only") }}</label >{{ t("From this instance only") }}</label
> >
</div> </div>
@ -106,7 +101,7 @@
/> />
<label <label
for="internalTarget" for="internalTarget"
class="ml-3 font-medium text-gray-900 dark:text-gray-300" class="cursor-pointer ml-3 font-medium text-gray-900 dark:text-gray-300"
>{{ t("In this instance's network") }}</label >{{ t("In this instance's network") }}</label
> >
</div> </div>
@ -142,7 +137,7 @@
/> />
<label <label
:for="key" :for="key"
class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300" class="cursor-pointer ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
>{{ eventStartDateRangeOption.label }}</label >{{ eventStartDateRangeOption.label }}</label
> >
</div> </div>
@ -182,7 +177,7 @@
/> />
<label <label
:for="distanceOption.id" :for="distanceOption.id"
class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300" class="cursor-pointer ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
>{{ distanceOption.label }}</label >{{ distanceOption.label }}</label
> >
</div> </div>
@ -216,7 +211,7 @@
/> />
<label <label
:for="category.id" :for="category.id"
class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300" class="cursor-pointer ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
>{{ category.label }}</label >{{ category.label }}</label
> >
</div> </div>
@ -277,7 +272,7 @@
/> />
<label <label
:for="eventStatusOption.id" :for="eventStatusOption.id"
class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300" class="cursor-pointer ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
>{{ eventStatusOption.label }}</label >{{ eventStatusOption.label }}</label
> >
</div> </div>
@ -324,7 +319,7 @@
/> />
<label <label
:for="key" :for="key"
class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300" class="cursor-pointer ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
>{{ language }}</label >{{ language }}</label
> >
</div> </div>
@ -360,61 +355,6 @@
</template> </template>
</filter-section> </filter-section>
<!--
<div class="">
<label v-translate class="font-bold" for="host">Mobilizon instance</label>
<input
id="host"
v-model="formHost"
type="text"
name="host"
placeholder="mobilizon.fr"
class="dark:text-black md:max-w-fit w-full"
/>
</div>
<div class="">
<label v-translate class="inline font-bold" for="tagsAllOf">All of these tags</label>
<button
v-if="formTagsAllOf.length !== 0"
v-translate
class="text-sm ml-2"
@click="resetField('tagsAllOf')"
>
Reset
</button>
<vue-tags-input
v-model="formTagAllOf"
:placeholder="tagsPlaceholder"
:tags="formTagsAllOf"
@tags-changed="(newTags) => (formTagsAllOf = newTags)"
/>
</div>
<div>
<div>
<label v-translate class="inline font-bold" for="tagsOneOf">One of these tags</label>
<button
v-if="formTagsOneOf.length !== 0"
v-translate
class="text-sm ml-2"
@click="resetField('tagsOneOf')"
>
Reset
</button>
</div>
<vue-tags-input
v-model="formTagOneOf"
:placeholder="tagsPlaceholder"
:tags="formTagsOneOf"
@tags-changed="(newTags) => (formTagsOneOf = newTags)"
/>
</div>-->
<div class="sr-only"> <div class="sr-only">
<button <button
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
@ -440,13 +380,9 @@
> >
<p v-if="searchLoading">{{ t("Loading search results...") }}</p> <p v-if="searchLoading">{{ t("Loading search results...") }}</p>
<p v-else-if="totalCount === 0"> <p v-else-if="totalCount === 0">
<span <span v-if="contentType === ContentType.EVENTS">{{
v-if=" t("No events found")
contentType === ContentType.EVENTS || }}</span>
contentType === ContentType.SHORTEVENTS
"
>{{ t("No events found") }}</span
>
<span v-else-if="contentType === ContentType.LONGEVENTS">{{ <span v-else-if="contentType === ContentType.LONGEVENTS">{{
t("No activities found") t("No activities found")
}}</span> }}</span>
@ -456,12 +392,7 @@
<span v-else>{{ t("No results found") }}</span> <span v-else>{{ t("No results found") }}</span>
</p> </p>
<p v-else> <p v-else>
<span <span v-if="contentType === ContentType.EVENTS">
v-if="
contentType === ContentType.EVENTS ||
contentType === ContentType.SHORTEVENTS
"
>
{{ {{
t( t(
"{eventsCount} events found", "{eventsCount} events found",
@ -503,12 +434,27 @@
t("Sort by") t("Sort by")
}}</label> }}</label>
<o-select <o-select
:placeholder="t('Sort by')" v-if="contentType !== ContentType.GROUPS"
v-model="sortBy" :placeholder="t('Sort by events')"
id="sortOptionSelect" v-model="sortByEvents"
id="sortOptionSelectEvents"
> >
<option <option
v-for="sortOption in sortOptions" v-for="sortOption in sortOptionsEvents"
:key="sortOption.key"
:value="sortOption.key"
>
{{ sortOption.label }}
</option>
</o-select>
<o-select
v-if="contentType === ContentType.GROUPS"
:placeholder="t('Sort by groups')"
v-model="sortByGroups"
id="sortOptionSelectGroups"
>
<option
v-for="sortOption in sortOptionsGroups"
:key="sortOption.key" :key="sortOption.key"
:value="sortOption.key" :value="sortOption.key"
> >
@ -533,92 +479,9 @@
</div> </div>
</div> </div>
<div v-if="mode === ViewMode.LIST"> <div v-if="mode === ViewMode.LIST">
<template v-if="contentType === ContentType.ALL">
<template v-if="searchLoading">
<SkeletonGroupResultList v-for="i in 2" :key="i" />
<SkeletonEventResultList v-for="i in 4" :key="i" />
</template>
<o-notification v-if="features && !features.groups" variant="danger">
{{ t("Groups are not enabled on this instance.") }}
</o-notification>
<div v-else-if="searchGroups && searchGroups?.total > 0">
<GroupCard
class="my-2"
v-for="group in searchGroups?.elements"
:group="group"
:key="group.id"
:isRemoteGroup="group.__typename === 'GroupResult'"
:isLoggedIn="currentUser?.isLoggedIn"
mode="row"
/>
</div>
<div v-if="searchEvents && searchEvents.total > 0">
<event-card
mode="row"
v-for="event in searchEvents?.elements"
:event="event"
:key="event.uuid"
:options="{
isRemoteEvent: event.__typename === 'EventResult',
isLoggedIn: currentUser?.isLoggedIn,
}"
class="my-4"
/>
</div>
<EmptyContent v-else-if="searchLoading === false" icon="magnify">
<span v-if="searchIsUrl">
{{ t("No event found at this address") }}
</span>
<span v-else-if="!search">
{{ t("No results found") }}
</span>
<i18n-t keypath="No results found for {search}" tag="span" v-else>
<template #search>
<b class="">{{ search }}</b>
</template>
</i18n-t>
<template #desc v-if="searchIsUrl && !currentUser?.id">
{{
t(
"Only registered users may fetch remote events from their URL."
)
}}
</template>
<template #desc v-else>
<p class="my-2 text-start">
{{ t("Suggestions:") }}
</p>
<ul class="list-disc list-inside text-start">
<li>
{{ t("Make sure that all words are spelled correctly.") }}
</li>
<li>{{ t("Try different keywords.") }}</li>
<li>{{ t("Try more general keywords.") }}</li>
<li>{{ t("Try fewer keywords.") }}</li>
<li>{{ t("Change the filters.") }}</li>
</ul>
</template>
</EmptyContent>
<o-pagination
v-if="
(searchEvents && searchEvents?.total > EVENT_PAGE_LIMIT) ||
(searchGroups && searchGroups?.total > GROUP_PAGE_LIMIT)
"
:total="
Math.max(searchEvents?.total ?? 0, searchGroups?.total ?? 0)
"
v-model:current="page"
:per-page="EVENT_PAGE_LIMIT"
:aria-next-label="t('Next page')"
:aria-previous-label="t('Previous page')"
:aria-page-label="t('Page')"
:aria-current-label="t('Current page')"
/>
</template>
<template <template
v-else-if=" v-if="
contentType === ContentType.EVENTS || contentType === ContentType.EVENTS ||
contentType === ContentType.SHORTEVENTS ||
contentType === ContentType.LONGEVENTS contentType === ContentType.LONGEVENTS
" "
> >
@ -803,7 +666,6 @@ import {
import Calendar from "vue-material-design-icons/Calendar.vue"; import Calendar from "vue-material-design-icons/Calendar.vue";
import CalendarStar from "vue-material-design-icons/CalendarStar.vue"; import CalendarStar from "vue-material-design-icons/CalendarStar.vue";
import AccountMultiple from "vue-material-design-icons/AccountMultiple.vue"; import AccountMultiple from "vue-material-design-icons/AccountMultiple.vue";
import Magnify from "vue-material-design-icons/Magnify.vue";
import { useHead } from "@/utils/head"; import { useHead } from "@/utils/head";
import type { Locale } from "date-fns"; import type { Locale } from "date-fns";
@ -886,27 +748,11 @@ enum GroupSortValues {
LAST_EVENT_ACTIVITY = "LAST_EVENT_ACTIVITY", LAST_EVENT_ACTIVITY = "LAST_EVENT_ACTIVITY",
} }
enum SortValues {
//Common
MATCH_DESC = "MATCH_DESC",
CREATED_AT_DESC = "CREATED_AT_DESC",
CREATED_AT_ASC = "CREATED_AT_ASC",
// EventSortValues
START_TIME_ASC = "START_TIME_ASC",
START_TIME_DESC = "START_TIME_DESC",
PARTICIPANT_COUNT_DESC = "PARTICIPANT_COUNT_DESC",
// GroupSortValues
MEMBER_COUNT_ASC = "MEMBER_COUNT_ASC",
MEMBER_COUNT_DESC = "MEMBER_COUNT_DESC",
LAST_EVENT_ACTIVITY = "LAST_EVENT_ACTIVITY",
}
const props = defineProps<{ const props = defineProps<{
tag?: string; tag?: string;
}>(); }>();
const tag = computed(() => props.tag); const tag = computed(() => props.tag);
const page = useRouteQuery("page", 1, integerTransformer);
const eventPage = useRouteQuery("eventPage", 1, integerTransformer); const eventPage = useRouteQuery("eventPage", 1, integerTransformer);
const groupPage = useRouteQuery("groupPage", 1, integerTransformer); const groupPage = useRouteQuery("groupPage", 1, integerTransformer);
@ -920,10 +766,9 @@ const distance = useRouteQuery("distance", "10_km");
const when = useRouteQuery("when", "any"); const when = useRouteQuery("when", "any");
const contentType = useRouteQuery( const contentType = useRouteQuery(
"contentType", "contentType",
tag.value ? ContentType.EVENTS : ContentType.ALL, ContentType.EVENTS,
enumTransformer(ContentType) enumTransformer(ContentType)
); );
const isOnline = useRouteQuery("isOnline", false, booleanTransformer); const isOnline = useRouteQuery("isOnline", false, booleanTransformer);
const categoryOneOf = useRouteQuery("categoryOneOf", [], arrayTransformer); const categoryOneOf = useRouteQuery("categoryOneOf", [], arrayTransformer);
const statusOneOf = useRouteQuery( const statusOneOf = useRouteQuery(
@ -932,16 +777,22 @@ const statusOneOf = useRouteQuery(
arrayTransformer arrayTransformer
); );
const languageOneOf = useRouteQuery("languageOneOf", [], arrayTransformer); const languageOneOf = useRouteQuery("languageOneOf", [], arrayTransformer);
const searchTarget = useRouteQuery( const searchTarget = useRouteQuery(
"target", "target",
SearchTargets.INTERNAL, SearchTargets.INTERNAL,
enumTransformer(SearchTargets) enumTransformer(SearchTargets)
); );
const mode = useRouteQuery("mode", ViewMode.LIST, enumTransformer(ViewMode)); const mode = useRouteQuery("mode", ViewMode.LIST, enumTransformer(ViewMode));
const sortBy = useRouteQuery( const sortByEvents = useRouteQuery(
"sortBy", "sortByEvents",
SortValues.START_TIME_ASC, EventSortValues.MATCH_DESC,
enumTransformer(SortValues) enumTransformer(EventSortValues)
);
const sortByGroups = useRouteQuery(
"sortByGroups",
GroupSortValues.MATCH_DESC,
enumTransformer(GroupSortValues)
); );
const bbox = useRouteQuery("bbox", undefined); const bbox = useRouteQuery("bbox", undefined);
const zoom = useRouteQuery("zoom", undefined, integerTransformer); const zoom = useRouteQuery("zoom", undefined, integerTransformer);
@ -1069,34 +920,26 @@ const contentTypeMapping = computed(() => {
if (islongEvents.value) { if (islongEvents.value) {
return [ return [
{ {
contentType: "ALL", contentType: ContentType.EVENTS,
label: t("Everything"),
},
{
contentType: "SHORTEVENTS",
label: t("Events"), label: t("Events"),
}, },
{ {
contentType: "LONGEVENTS", contentType: ContentType.LONGEVENTS,
label: t("Activities"), label: t("Activities"),
}, },
{ {
contentType: "GROUPS", contentType: ContentType.GROUPS,
label: t("Groups"), label: t("Groups"),
}, },
]; ];
} else { } else {
return [ return [
{ {
contentType: "ALL", contentType: ContentType.EVENTS,
label: t("Everything"),
},
{
contentType: "EVENTS",
label: t("Events"), label: t("Events"),
}, },
{ {
contentType: "GROUPS", contentType: ContentType.GROUPS,
label: t("Groups"), label: t("Groups"),
}, },
]; ];
@ -1209,7 +1052,7 @@ const geoHashLocation = computed(() =>
const radius = computed(() => Number.parseInt(distance.value.slice(0, -3))); const radius = computed(() => Number.parseInt(distance.value.slice(0, -3)));
const longEvents = computed(() => { const longEvents = computed(() => {
if (contentType.value === ContentType.SHORTEVENTS) { if (contentType.value === ContentType.EVENTS) {
return false; return false;
} else if (contentType.value === ContentType.LONGEVENTS) { } else if (contentType.value === ContentType.LONGEVENTS) {
return true; return true;
@ -1222,63 +1065,60 @@ const totalCount = computed(() => {
return (searchEvents.value?.total ?? 0) + (searchGroups.value?.total ?? 0); return (searchEvents.value?.total ?? 0) + (searchGroups.value?.total ?? 0);
}); });
const sortOptions = computed(() => { const sortOptionsGroups = computed(() => {
const options = [ const options = [
{ {
key: SortValues.MATCH_DESC, key: GroupSortValues.MATCH_DESC,
label: t("Best match"), label: t("Best match"),
}, },
];
if (
contentType.value === ContentType.EVENTS ||
contentType.value === ContentType.SHORTEVENTS ||
contentType.value === ContentType.LONGEVENTS
) {
options.push(
{ {
key: SortValues.START_TIME_ASC, key: GroupSortValues.MEMBER_COUNT_ASC,
label: t("Event date"),
},
{
key: SortValues.CREATED_AT_DESC,
label: t("Most recently published"),
},
{
key: SortValues.CREATED_AT_ASC,
label: t("Least recently published"),
},
{
key: SortValues.PARTICIPANT_COUNT_DESC,
label: t("With the most participants"),
}
);
}
if (contentType.value === ContentType.GROUPS) {
options.push(
{
key: SortValues.MEMBER_COUNT_ASC,
label: t("Increasing number of members"), label: t("Increasing number of members"),
}, },
{ {
key: SortValues.MEMBER_COUNT_DESC, key: GroupSortValues.MEMBER_COUNT_DESC,
label: t("Decreasing number of members"), label: t("Decreasing number of members"),
}, },
{ {
key: SortValues.CREATED_AT_ASC, key: GroupSortValues.CREATED_AT_ASC,
label: t("Increasing creation date"), label: t("Increasing creation date"),
}, },
{ {
key: SortValues.CREATED_AT_DESC, key: GroupSortValues.CREATED_AT_DESC,
label: t("Decreasing creation date"), label: t("Decreasing creation date"),
}, },
{ {
key: SortValues.LAST_EVENT_ACTIVITY, key: GroupSortValues.LAST_EVENT_ACTIVITY,
label: t("Last event activity"), label: t("Last event activity"),
} },
); ];
}
return options;
});
const sortOptionsEvents = computed(() => {
const options = [
{
key: EventSortValues.MATCH_DESC,
label: t("Best match"),
},
{
key: EventSortValues.START_TIME_ASC,
label: t("Event date"),
},
{
key: EventSortValues.CREATED_AT_DESC,
label: t("Most recently published"),
},
{
key: EventSortValues.CREATED_AT_ASC,
label: t("Least recently published"),
},
{
key: EventSortValues.PARTICIPANT_COUNT_DESC,
label: t("With the most participants"),
},
];
return options; return options;
}); });
@ -1334,11 +1174,11 @@ watch(isOnline, (newIsOnline) => {
}); });
const sortByForType = ( const sortByForType = (
value: SortValues, value: EventSortValues,
allowed: typeof EventSortValues | typeof GroupSortValues allowed: typeof EventSortValues
): SortValues | undefined => { ): EventSortValues | undefined => {
if (value === SortValues.START_TIME_ASC && when.value === "past") { if (value === EventSortValues.START_TIME_ASC && when.value === "past") {
value = SortValues.START_TIME_DESC; value = EventSortValues.START_TIME_DESC;
} }
return Object.values(allowed).includes(value) ? value : undefined; return Object.values(allowed).includes(value) ? value : undefined;
}; };
@ -1373,20 +1213,15 @@ watch(
searchTarget, searchTarget,
bbox, bbox,
zoom, zoom,
sortBy, sortByEvents,
sortByGroups,
boostLanguagesQuery, boostLanguagesQuery,
], ],
([newContentType]) => { ([newContentType]) => {
switch (newContentType) { switch (newContentType) {
case ContentType.ALL:
page.value = 1;
break;
case ContentType.EVENTS: case ContentType.EVENTS:
eventPage.value = 1; eventPage.value = 1;
break; break;
case ContentType.SHORTEVENTS:
eventPage.value = 1;
break;
case ContentType.LONGEVENTS: case ContentType.LONGEVENTS:
eventPage.value = 1; eventPage.value = 1;
break; break;
@ -1408,10 +1243,8 @@ const { result: searchElementsResult, loading: searchLoading } = useQuery<{
endsOn: end.value, endsOn: end.value,
longevents: longEvents.value, longevents: longEvents.value,
radius: geoHashLocation.value ? radius.value : undefined, radius: geoHashLocation.value ? radius.value : undefined,
eventPage: eventPage: eventPage.value,
contentType.value === ContentType.ALL ? page.value : eventPage.value, groupPage: groupPage.value,
groupPage:
contentType.value === ContentType.ALL ? page.value : groupPage.value,
limit: EVENT_PAGE_LIMIT, limit: EVENT_PAGE_LIMIT,
type: isOnline.value ? "ONLINE" : undefined, type: isOnline.value ? "ONLINE" : undefined,
categoryOneOf: categoryOneOf.value, categoryOneOf: categoryOneOf.value,
@ -1420,8 +1253,8 @@ const { result: searchElementsResult, loading: searchLoading } = useQuery<{
searchTarget: searchTarget.value, searchTarget: searchTarget.value,
bbox: mode.value === ViewMode.MAP ? bbox.value : undefined, bbox: mode.value === ViewMode.MAP ? bbox.value : undefined,
zoom: zoom.value, zoom: zoom.value,
sortByEvents: sortByForType(sortBy.value, EventSortValues), sortByEvents: sortByForType(sortByEvents.value, EventSortValues),
sortByGroups: sortByForType(sortBy.value, GroupSortValues), sortByGroups: sortByGroups.value,
boostLanguages: boostLanguagesQuery.value, boostLanguages: boostLanguagesQuery.value,
})); }));
</script> </script>