From be33c3b2138db8d1c388a054f32d95fcf8b37821 Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
Date: Mon, 31 Oct 2022 18:07:14 +0100
Subject: [PATCH] Search improvements

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
---
 .../Event/SkeletonEventResultList.vue         |  20 +++
 .../Group/SkeletonGroupResultList.vue         |  16 +++
 js/src/i18n/en_US.json                        |  12 +-
 js/src/i18n/fr_FR.json                        |  12 +-
 js/src/views/SearchView.vue                   | 127 +++++++++++++++---
 5 files changed, 163 insertions(+), 24 deletions(-)
 create mode 100644 js/src/components/Event/SkeletonEventResultList.vue
 create mode 100644 js/src/components/Group/SkeletonGroupResultList.vue

diff --git a/js/src/components/Event/SkeletonEventResultList.vue b/js/src/components/Event/SkeletonEventResultList.vue
new file mode 100644
index 000000000..ce795a14a
--- /dev/null
+++ b/js/src/components/Event/SkeletonEventResultList.vue
@@ -0,0 +1,20 @@
+<template>
+  <div class="bg-white dark:bg-slate-800 shadow rounded-md w-full mx-auto">
+    <div class="animate-pulse flex flex-col sm:flex-row space-3-4 items-center">
+      <div class="object-cover h-40 w-72 bg-slate-700 m-2 md:m-4 shrink-0" />
+
+      <div
+        class="flex gap-3 flex self-start flex-col justify-between m-2 md:m-4 w-full px-2 md:px-4"
+      >
+        <div class="h-3 bg-slate-700 w-52 hidden sm:block"></div>
+        <div class="h-5 bg-slate-700 w-72 lg:w-96"></div>
+        <div class="flex items-center">
+          <div
+            class="rounded-full object-cover h-6 w-6 bg-slate-700 mx-2 shrink-0"
+          />
+          <div class="h-3 bg-slate-700 w-52"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
diff --git a/js/src/components/Group/SkeletonGroupResultList.vue b/js/src/components/Group/SkeletonGroupResultList.vue
new file mode 100644
index 000000000..4749ff3aa
--- /dev/null
+++ b/js/src/components/Group/SkeletonGroupResultList.vue
@@ -0,0 +1,16 @@
+<template>
+  <div class="bg-white dark:bg-slate-800 shadow rounded-md w-full mx-auto">
+    <div class="animate-pulse flex flex-col sm:flex-row space-3-4 items-center">
+      <div
+        class="object-cover h-40 w-40 rounded-full bg-slate-700 m-2 md:m-4 shrink-0"
+      />
+
+      <div
+        class="flex gap-3 flex self-start flex-col justify-between m-2 md:m-4 self-center w-full px-2 md:px-4"
+      >
+        <div class="h-5 bg-slate-700 w-64"></div>
+        <div class="h-3 bg-slate-700 w-52"></div>
+      </div>
+    </div>
+  </div>
+</template>
diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json
index 5bdd4c5ba..e8671ce43 100644
--- a/js/src/i18n/en_US.json
+++ b/js/src/i18n/en_US.json
@@ -1425,5 +1425,15 @@
   "Message body": "Message body",
   "Describe your event": "Describe your event",
   "A few lines about your group": "A few lines about your group",
-  "Write your post": "Write your post"
+  "Write your post": "Write your post",
+  "Suggestions:": "Suggestions:",
+  "Make sure that all words are spelled correctly.": "Make sure that all words are spelled correctly.",
+  "Try different keywords.": "Try different keywords.",
+  "Try more general keywords.": "Try more general keywords.",
+  "Try fewer keywords.": "Try fewer keywords.",
+  "Change the filters.": "Change the filters.",
+  "No results found for {search}": "No results found for {search}",
+  "No events found for {search}": "No events found for {search}",
+  "No groups found for {search}": "No groups found for {search}",
+  "No event found at this address": "No event found at this address"
 }
\ No newline at end of file
diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json
index e0baf2e3e..02f50b558 100644
--- a/js/src/i18n/fr_FR.json
+++ b/js/src/i18n/fr_FR.json
@@ -1423,5 +1423,15 @@
   "Message body": "Corps du message",
   "Describe your event": "Décrivez votre événement",
   "A few lines about your group": "Quelques lignes à propos de votre groupe",
-  "Write your post": "Écrivez votre billet"
+  "Write your post": "Écrivez votre billet",
+  "Suggestions:": "Suggestions :",
+  "Make sure that all words are spelled correctly.": "Vérifiez l’orthographe des termes de recherche.",
+  "Try different keywords.": "Essayez d'autres mots.",
+  "Try more general keywords.": "Utilisez des mots clés plus généraux.",
+  "Try fewer keywords.": "Spécifiez un moins grand nombre de mots-clés.",
+  "Change the filters.": "Changez les filtres.",
+  "No results found for {search}": "Aucun résultat trouvé pour {search}",
+  "No events found for {search}": "Aucun événement trouvé pour {search}",
+  "No groups found for {search}": "Aucun groupe trouvé pour {search}",
+  "No event found at this address": "Aucun événement trouvé à cette addresse"
 }
diff --git a/js/src/views/SearchView.vue b/js/src/views/SearchView.vue
index 52808e904..61354e8df 100644
--- a/js/src/views/SearchView.vue
+++ b/js/src/views/SearchView.vue
@@ -500,11 +500,16 @@
       </div>
       <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"
@@ -513,9 +518,6 @@
               mode="row"
             />
           </div>
-          <o-notification v-else-if="searchLoading === false" variant="danger">
-            {{ t("No groups found") }}
-          </o-notification>
           <div v-if="searchEvents && searchEvents.total > 0">
             <event-card
               mode="row"
@@ -529,16 +531,40 @@
               class="my-4"
             />
           </div>
-          <o-notification v-else-if="searchLoading === false" variant="info">
-            <p>{{ t("No events found") }}</p>
-            <p v-if="searchIsUrl && !currentUser?.id">
+          <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."
                 )
               }}
-            </p>
-          </o-notification>
+            </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) ||
@@ -556,6 +582,9 @@
           />
         </template>
         <template v-else-if="contentType === ContentType.EVENTS">
+          <template v-if="searchLoading">
+            <SkeletonEventResultList v-for="i in 8" :key="i" />
+          </template>
           <template v-if="searchEvents && searchEvents.total > 0">
             <event-card
               mode="row"
@@ -580,24 +609,51 @@
             >
             </o-pagination>
           </template>
-          <o-notification v-else-if="searchLoading === false" variant="info">
-            <p>{{ t("No events found") }}</p>
-            <p v-if="searchIsUrl && !currentUser?.id">
+          <EmptyContent v-else-if="searchLoading === false" icon="calendar">
+            <span v-if="searchIsUrl">
+              {{ t("No event found at this address") }}
+            </span>
+            <span v-else-if="!search">
+              {{ t("No events found") }}
+            </span>
+            <i18n-t keypath="No events found for {search}" tag="span" v-else>
+              <template #search>
+                <b>{{ search }}</b>
+              </template>
+            </i18n-t>
+            <template #desc v-if="searchIsUrl && !currentUser?.id">
               {{
                 t(
                   "Only registered users may fetch remote events from their URL."
                 )
               }}
-            </p>
-          </o-notification>
+            </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>
         </template>
         <template v-else-if="contentType === ContentType.GROUPS">
           <o-notification v-if="features && !features.groups" variant="danger">
             {{ t("Groups are not enabled on this instance.") }}
           </o-notification>
-
+          <template v-else-if="searchLoading">
+            <SkeletonGroupResultList v-for="i in 6" :key="i" />
+          </template>
           <template v-else-if="searchGroups && searchGroups?.total > 0">
             <GroupCard
+              class="my-2"
               v-for="group in searchGroups?.elements"
               :group="group"
               :key="group.id"
@@ -617,9 +673,33 @@
             >
             </o-pagination>
           </template>
-          <o-notification v-else-if="searchLoading === false" variant="danger">
-            {{ t("No groups found") }}
-          </o-notification>
+          <EmptyContent
+            v-else-if="searchLoading === false"
+            icon="account-multiple"
+          >
+            <span v-if="!search">
+              {{ t("No events found") }}
+            </span>
+            <i18n-t keypath="No groups found for {search}" tag="span" v-else>
+              <template #search>
+                <b>{{ search }}</b>
+              </template>
+            </i18n-t>
+            <template #desc>
+              <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>
         </template>
       </div>
       <event-marker-map
@@ -693,6 +773,9 @@ import { IConfig } from "@/types/config.model";
 import { TypeNamed } from "@/types/apollo";
 import { LatLngBounds } from "leaflet";
 import lodashSortBy from "lodash/sortBy";
+import EmptyContent from "@/components/Utils/EmptyContent.vue";
+import SkeletonGroupResultList from "@/components/Group/SkeletonGroupResultList.vue";
+import SkeletonEventResultList from "@/components/Event/SkeletonEventResultList.vue";
 
 const EventMarkerMap = defineAsyncComponent(
   () => import("@/components/Search/EventMarkerMap.vue")
@@ -764,6 +847,10 @@ const arrayTransformer: RouteQueryTransformer<string[]> = {
   },
 };
 
+const props = defineProps<{
+  tag?: string;
+}>();
+
 const page = useRouteQuery("page", 1, integerTransformer);
 const eventPage = useRouteQuery("eventPage", 1, integerTransformer);
 const groupPage = useRouteQuery("groupPage", 1, integerTransformer);
@@ -775,7 +862,7 @@ const distance = useRouteQuery("distance", "10_km");
 const when = useRouteQuery("when", "any");
 const contentType = useRouteQuery(
   "contentType",
-  ContentType.ALL,
+  props.tag ? ContentType.EVENTS : ContentType.ALL,
   enumTransformer(ContentType)
 );
 
@@ -819,10 +906,6 @@ const EVENT_PAGE_LIMIT = 16;
 
 const GROUP_PAGE_LIMIT = 16;
 
-const props = defineProps<{
-  tag?: string;
-}>();
-
 const { features } = useFeatures();
 const { eventCategories } = useEventCategories();