Commit 87c5e24

HPCesia <me@hpcesia.com>
2025-04-12 19:30:48
refactor: Pagefind Search
close #11
1 parent 2781f66
src/components/search/Pagefind.astro
@@ -1,89 +0,0 @@
----
-import { Icon } from 'astro-icon/components';
-import SearchBaseUI from './SearchBaseUI.astro';
-
-const bundlePath = `${import.meta.env.BASE_URL}pagefind/`;
----
-
-<SearchBaseUI
-  data-pagefind-ui
-  data-bundle-path={bundlePath}
-  data-base-url={import.meta.env.BASE_URL}
-/>
-
-<template id="pagefind-result-template">
-  <a class="group hover:bg-primary/30 w-full rounded-md p-2 duration-150" href="#">
-    <div class="flex flex-row items-center gap-1 text-center">
-      <span class="group-hover:text-primary text-lg duration-150">Fake Result</span>
-      <Icon
-        name="material-symbols:chevron-right"
-        class="text-primary"
-        height="1.125rem"
-        width="1.125rem"
-      />
-    </div>
-    <div id="pagefind-result-template-excerpt" class="text-sm opacity-60">
-      This is a fake result.
-    </div>
-  </a>
-</template>
-
-<script is:inline>
-  async function setup() {
-    for (const el of document.querySelectorAll('[data-pagefind-ui]')) {
-      const bundlePath = el.getAttribute('data-bundle-path');
-      const baseUrl = el.getAttribute('data-base-url');
-      const pagefind = await import(`${bundlePath}pagefind.js`);
-      await pagefind.options({
-        baseUrl: baseUrl,
-        basePath: bundlePath,
-      });
-      pagefind.init();
-
-      const searchInput = el.querySelector('input');
-      const searchResultsWrapper = el.querySelector('.search-result');
-      const searchResultTemplate = document.getElementById('pagefind-result-template');
-      if (!searchInput || !searchResultsWrapper || !searchResultTemplate) {
-        console.error('Pagefind: Required elements not found');
-        return;
-      }
-
-      const search = async (text) => {
-        const results = (await pagefind.debouncedSearch(text, 300)).results;
-        searchResultsWrapper.innerHTML = '';
-        if (results.length === 0) {
-          searchResultsWrapper.textContent = 'No results found';
-          return;
-        }
-        results.forEach(async (result) => {
-          const data = await result.data();
-          const resultNode = searchResultTemplate.content.cloneNode(true);
-          const resultLink = resultNode.querySelector('a');
-          const resultTitle = resultNode.querySelector('span');
-          const resultExcerpt = resultNode.querySelector('#pagefind-result-template-excerpt');
-
-          resultLink.setAttribute('href', data.url);
-          resultTitle.textContent = data.meta.title;
-          resultExcerpt.innerHTML = data.excerpt;
-
-          resultExcerpt.removeAttribute('id');
-          searchResultsWrapper.appendChild(resultNode);
-        });
-      };
-
-      searchInput.addEventListener('input', async () => {
-        await search(searchInput.value);
-      });
-    }
-  }
-
-  document.addEventListener('astro:page-load', setup);
-  setup();
-</script>
-
-<style is:global>
-  [data-pagefind-ui] mark {
-    background-color: transparent;
-    color: var(--color-secondary);
-  }
-</style>
src/components/search/Pagefind.vue
@@ -0,0 +1,106 @@
+<script setup>
+import { onMounted, ref } from 'vue';
+
+const props = defineProps({
+  inputId: {
+    type: String,
+    required: true,
+  },
+});
+
+const bundlePath = `${import.meta.env.BASE_URL}pagefind/`;
+const baseUrl = import.meta.env.BASE_URL;
+
+const searchResults = ref([]);
+const noResults = ref(false);
+let pagefind = null;
+
+const search = async (text) => {
+  if (!pagefind) return;
+
+  const searchResponse = await pagefind.debouncedSearch(text, 300);
+  if (!searchResponse) return;
+  const results = searchResponse.results;
+  if (results.length === 0) {
+    searchResults.value = [];
+    noResults.value = true;
+    return;
+  }
+
+  noResults.value = false;
+  const processedResults = await Promise.all(
+    results.map(async (result) => {
+      const data = await result.data();
+      return {
+        url: data.url,
+        title: data.meta.title,
+        excerpt: data.excerpt,
+      };
+    })
+  );
+  searchResults.value = processedResults;
+};
+
+const setupSearch = () => {
+  const searchInput = document.getElementById(props.inputId);
+  if (!searchInput) {
+    console.error(`Pagefind: Input element with id "${props.inputId}" not found`);
+    return;
+  }
+
+  searchInput.addEventListener('input', (e) => search(e.target.value));
+};
+
+const setup = async () => {
+  pagefind = await import(/* @vite-ignore */ `${bundlePath}pagefind.js`);
+  await pagefind.options({
+    baseUrl: baseUrl,
+    basePath: bundlePath,
+  });
+  pagefind.init();
+  setupSearch();
+};
+
+onMounted(() => {
+  setup();
+  document.addEventListener('astro:page-load', setup);
+});
+
+defineExpose({
+  searchResults,
+  noResults,
+});
+</script>
+
+<template>
+  <div class="w-full" data-pagefind-ui>
+    <slot></slot>
+    <div
+      class="search-result mt-4 flex h-fit max-h-[calc(60vh-8rem)] flex-col items-center gap-2 overflow-y-auto text-center"
+    >
+      <template v-if="noResults"> No results found </template>
+      <template v-else>
+        <a
+          v-for="result in searchResults"
+          :key="result.url"
+          :href="result.url"
+          class="group hover:bg-primary/30 w-full rounded-md p-2 duration-150"
+        >
+          <div class="flex flex-row items-center gap-1 text-center">
+            <span class="group-hover:text-primary text-lg duration-150"
+              >{{ result.title }}<slot name="icon"></slot
+            ></span>
+          </div>
+          <div class="text-sm opacity-60" v-html="result.excerpt"></div>
+        </a>
+      </template>
+    </div>
+  </div>
+</template>
+
+<style>
+[data-pagefind-ui] mark {
+  background-color: transparent;
+  color: var(--color-secondary);
+}
+</style>
src/components/search/SearchBaseUI.astro
@@ -1,21 +0,0 @@
----
-import I18nKey from '@i18n/I18nKey';
-import { i18n } from '@i18n/translation';
-import { Icon } from 'astro-icon/components';
-import type { HTMLAttributes } from 'astro/types';
-
-type Props = HTMLAttributes<'div'>;
-
-const { class: className, ...rest } = Astro.props;
----
-
-<div class:list={['w-full', className]} {...rest}>
-  <label class="input input-bordered flex w-full items-center gap-2">
-    <input type="text" class="grow" placeholder={i18n(I18nKey.search)} />
-    <Icon name="material-symbols:search-rounded" height="1.875rem" width="1.875rem" />
-  </label>
-  <div
-    class="search-result mt-4 flex h-fit max-h-[calc(60vh-8rem)] flex-col items-center gap-2 overflow-y-auto text-center"
-  >
-  </div>
-</div>
src/components/Search.astro
@@ -1,6 +1,9 @@
 ---
 import { searchConfig } from '@/config';
-import Pagefind from './search/Pagefind.astro';
+import I18nKey from '@i18n/I18nKey';
+import { i18n } from '@i18n/translation';
+import { Icon } from 'astro-icon/components';
+import Pagefind from './search/Pagefind.vue';
 ---
 
 <dialog id="search_modal" class="modal">
@@ -13,22 +16,46 @@ import Pagefind from './search/Pagefind.astro';
         (() => {
           switch (searchConfig.provider) {
             case 'pagefind':
-              return <Pagefind />;
+              return (
+                <Pagefind client:visible inputId="search-input">
+                  <label class="input input-bordered flex w-full items-center gap-2">
+                    <input
+                      id="search-input"
+                      type="text"
+                      class="grow"
+                      placeholder={i18n(I18nKey.search)}
+                    />
+                    <Icon
+                      name="material-symbols:search-rounded"
+                      height="1.875rem"
+                      width="1.875rem"
+                    />
+                  </label>
+                  <Icon
+                    slot="icon"
+                    name="material-symbols:chevron-right"
+                    class="text-primary inline-block"
+                    height="1.125rem"
+                    width="1.125rem"
+                  />
+                </Pagefind>
+              );
           }
         })()
       }
     </div>
     <div class="relative mt-auto w-full shrink-0 pt-4 text-center">
       Powered by {
-        searchConfig.provider === 'pagefind' ? (
-          <a href="https://pagefind.app" target="_blank" class="text-primary">
-            Pagefind
-          </a>
-        ) : (
-          <a href="https://www.algolia.com" target="_blank" class="text-primary">
-            Algolia
-          </a>
-        )
+        (() => {
+          switch (searchConfig.provider) {
+            case 'pagefind':
+              return (
+                <a href="https://pagefind.app" target="_blank" class="text-primary">
+                  Pagefind
+                </a>
+              );
+          }
+        })()
       }
     </div>
   </div>