Skip to content
Snippets Groups Projects
SearchContainer.vue 5.74 KiB
Newer Older
Emmanuel Salomon's avatar
Emmanuel Salomon committed
<template>
  <div class="container">
    <slot name="header">
      <PageHeader v-if="title" :document="{ title }" />
    </slot>

Emmanuel Salomon's avatar
Emmanuel Salomon committed
    <div class="md:flex justify-between mb-8">
Emmanuel Salomon's avatar
Emmanuel Salomon committed
      <div class="w-full">
Emmanuel Salomon's avatar
Emmanuel Salomon committed
        <div class="md:flex">
Emmanuel Salomon's avatar
Emmanuel Salomon committed
            v-model="query"
            type="search"
            :placeholder="searchPlaceholder || $t('search')"
            input-class="rounded-full"
            icon="search"
            icon-class="text-2xl text-blue-100 dark:text-gray-600"
            icon-wrapper-class="top-0.5"
Emmanuel Salomon's avatar
Emmanuel Salomon committed
            class="flex-1"
            has-focus
Emmanuel Salomon's avatar
Emmanuel Salomon committed
          />
          <slot name="search" />
        </div>

        <slot name="items" :items="computedResults">
          <section v-if="computedResults.length" class="mt-8">
            <transition-group name="list">
              <nuxt-link
                v-for="(item, index) of computedResults"
                :id="
                  (index === 0 ||
                    computedResults[index - 1].firstLetter !==
                      item.firstLetter) &&
                  item.firstLetter
                "
                :key="item.path"
                :to="item.path.replace(/^\/pages\//, '/')"
                class="
                  block
                  cursor-pointer
                  dark-hover:bg-gray-700
                  hover:bg-gray-100
                  mb-2
                  p-2
                  rounded-lg
                  transition-colors
                "
Emmanuel Salomon's avatar
Emmanuel Salomon committed
              >
                <slot name="item" :item="item">
                  <h1 class="text-xl" v-html="item.title" />
                  <div
                    class="font-light text-gray-600 dark:text-gray-400"
                    v-html="item.description"
                  />
                </slot>
              </nuxt-link>
            </transition-group>
          </section>
        </slot>

        <SearchNoResult v-if="query.length && hasNoResult">
          <slot name="noResult" :query="query">
            <ul class="list-disc list-inside mt-3">
              <li>
                <nuxt-link
                  :to="`/recherche?q=${query}`"
                  class="hover:underline"
                >
                  {{ $t('noResult.searchWholeSite', { query }) }}
                </nuxt-link>
              </li>
              <li>
                <a
                  :href="`${$config.forum_url}/search?q=${query}`"
Emmanuel Salomon's avatar
Emmanuel Salomon committed
                  class="hover:underline"
                  target="_blank"
                  rel="noopener noreferrer"
                >
                  {{ $t('noResult.searchOnForum', { query }) }}
                </a>
              </li>
            </ul>
          </slot>
        </SearchNoResult>
      </div>

      <aside class="sticky h-full top-24 ml-12 bottom-12">
Emmanuel Salomon's avatar
Emmanuel Salomon committed
        <slot
          name="sidebar"
          :query="query"
          :computedResults="computedResults"
        />
      </aside>
    </div>

    <slot name="footer" :query="query" :computedResults="computedResults" />
  </div>
</template>

<script>
import { debounce, highlight } from '~/libs/helpers'

export default {
  name: 'SearchContainer',
  props: {
    results: {
      type: Array,
      required: true,
    },
    title: {
      type: String,
      default: null,
    },
    searchPlaceholder: {
      type: String,
      default: null,
    },
    searchFunction: {
      type: Function,
      required: true,
    },
    getQueryUrl: {
      type: Function,
      default(q) {
        return new URLSearchParams({
          q,
        })
      },
    },
Emmanuel Salomon's avatar
Emmanuel Salomon committed
  },
  data() {
    return {
      query: '',
      searchResults: [],
      hasNoResult: false,
    }
  },
  computed: {
    computedResults() {
      return this.searchResults.map((item) => ({
        ...item,
        firstLetter: item.title.replace(/^<mark>/, '')[0].toUpperCase(), // ! remove <mark> if title start by highlighten query
      }))
    },
  },
  watch: {
    query: debounce(function () {
      this.search()
    }, 500),
  },
  mounted() {
    this.searchResults = this.results
    this.query = this.$route.query.q || ''
  },
  methods: {
    async search(force) {
      const query = this.query.trim()

      if (query.length || force) {
        const results = await this.searchFunction(this.query)

        if (results.length) {
          this.hasNoResult = false
          this.searchResults = highlight(this.query, results)
        } else {
          this.hasNoResult = true
          this.searchResults = []
        }
      } else {
        this.searchResults = this.results
        this.hasNoResult = false
      }

      // Replace url with query string
      const queryUrl = this.getQueryUrl(query).toString()
Emmanuel Salomon's avatar
Emmanuel Salomon committed
      history.replaceState(
        {},
        null,
        this.$route.path + (queryUrl.length ? `?${queryUrl}` : '')
Emmanuel Salomon's avatar
Emmanuel Salomon committed
      )
    },
  },
}

// @ManUtopiK: Test avec composition-api
// import {
//   defineComponent,
//   ref,
//   useContext,
//   useAsync,
//   useFetch,
//   onBeforeMount,
//   watch,
// } from '@nuxtjs/composition-api'

// export default defineComponent({
//   name: 'FaqPage',
//   setup() {
//     const { route, query, $content } = useContext()
//     const query = ref(query.value.q)
//     const results = useAsync(() => $content('faq').fetch())
//     const documentFAQ = useAsync(() => $content('ui/faq').fetch())

//     onBeforeMount(async () => {
//       results.value = await $content('faq').search(query.value).fetch()
//     })

//     watch(query, async (val) => {
//       val = val.trim()
//       results.value = await $content('faq').search(val).fetch()

//       const queryPath = val.length ? `?s=${encodeURIComponent(val)}` : ''
//       history.pushState({}, null, route.value.path + queryPath)
//     })

//     console.log(documentFAQ.value)
//     return { query, results, documentFAQ }
//   },
// })
</script>