feat: implement post search using Pagefind (#12)

This commit is contained in:
saica.go
2024-01-24 12:28:16 +08:00
committed by GitHub
parent 31bb0d576e
commit 79ec8030cc
9 changed files with 297 additions and 61 deletions

View File

@@ -195,5 +195,15 @@ color_set({
.float-panel.closed {
@apply top-[4.75rem] opacity-0 pointer-events-none
}
.search-panel mark {
@apply bg-transparent text-[var(--primary)]
}
.text-deep {
@apply text-black/90 dark:text-white/90
}
.text-sub {
@apply text-black/50 dark:text-white/50
}
}
</style>

View File

@@ -7,6 +7,7 @@ import {i18n} from "../i18n/translation";
import {LinkPreset, NavBarLink} from "../types/config";
import {navBarConfig, siteConfig} from "../config";
import NavMenuPanel from "./widget/NavMenuPanel.astro";
import SearchPanel from "./SearchPanel.vue"
const className = Astro.props.class;
function isI18nKey(key: string): key is I18nKey {
@@ -63,19 +64,18 @@ function getLinkPresetInfo(p: LinkPreset): NavBarLink {
})}
</div>
<div class="flex">
<div>
<Button name="Display Settings" class="rounded-lg active:scale-90" id="display-settings-switch" iconName="material-symbols:palette-outline" iconSize={"1.25rem"} isIcon light></Button>
</div>
<div>
<Button name="Light/Dark Mode" class="rounded-lg flex items-center justify-center active:scale-90" id="scheme-switch" light height="2.75rem" width="2.75rem">
<Icon name="material-symbols:wb-sunny-outline-rounded" size={"1.25rem"} class="absolute opacity-[var(--display-light-icon)]"></Icon>
<Icon name="material-symbols:dark-mode-outline-rounded" size={"1.25rem"} class="absolute opacity-[var(--display-dark-icon)]"></Icon>
</Button>
</div>
<div>
<Button name="Nav Menu" class="rounded-lg active:scale-90 block md:hidden" id="nav-menu-switch" iconName="material-symbols:menu-rounded" iconSize={"1.25rem"} isIcon light></Button>
</div>
<SearchPanel client:load>
<Icon slot="search-icon" name="material-symbols:search" size={"1.25rem"} class="ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon>
<!--<Icon slot="arrow-icon" name="material-symbols:chevron-right-rounded" size={"1.25rem"} class="transition my-auto text-[var(&#45;&#45;primary)]"></Icon>-->
<Icon slot="arrow-icon" name="fa6-solid:chevron-right" size={"0.75rem"} class="transition translate-x-0.5 my-auto text-[var(--primary)]"></Icon>
<Button slot="search-switch" name="Search Panel" class="block lg:hidden rounded-lg active:scale-90" id="search-switch" iconName="material-symbols:search" iconSize={"1.25rem"} isIcon light></Button>
</SearchPanel>
<Button name="Display Settings" class="rounded-lg active:scale-90" id="display-settings-switch" iconName="material-symbols:palette-outline" iconSize={"1.25rem"} isIcon light></Button>
<Button name="Light/Dark Mode" class="rounded-lg flex items-center justify-center active:scale-90" id="scheme-switch" light height="2.75rem" width="2.75rem">
<Icon name="material-symbols:wb-sunny-outline-rounded" size={"1.25rem"} class="absolute opacity-[var(--display-light-icon)]"></Icon>
<Icon name="material-symbols:dark-mode-outline-rounded" size={"1.25rem"} class="absolute opacity-[var(--display-dark-icon)]"></Icon>
</Button>
<Button name="Nav Menu" class="rounded-lg active:scale-90 block md:hidden" id="nav-menu-switch" iconName="material-symbols:menu-rounded" iconSize={"1.25rem"} isIcon light></Button>
</div>
<DisplaySetting></DisplaySetting>
<NavMenuPanel links={links}></NavMenuPanel>
@@ -121,7 +121,16 @@ loadButtonScript();
document.addEventListener('astro:after-swap', () => {
loadButtonScript();
}, { once: false });
</script>
<script is:raw>
async function loadPagefind() {
console.log('load')
const pagefind = await import("/pagefind/pagefind.js");
await pagefind.options({
"excerptLength": 20
})
pagefind.init();
window.pagefind = pagefind
}
loadPagefind()
</script>

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
const keywordDesktop = ref('')
const keywordMobile = ref('')
const result = ref([])
const search = async (keyword: string, isDesktop: boolean) => {
let panel = document.getElementById('search-panel');
// for desktop hide panel immediately if keyword is empty
if (!keyword && isDesktop) {
panel.classList.add("closed");
return
}
// for prod
const ret = await pagefind.search(keyword)
let arr = []
for (const item of ret.results) {
arr.push(await item.data())
}
// for dev
// const arr = JSON.parse('[{"url":"/posts/guide/","content":"Simple Guides for Fuwari. Cover image source: Source. This blog template is built with Astro. For the things that are not mentioned in this guide, you may find the answers in the Astro Docs. Front-matter of Posts. --- title: My First Blog Post published: 2023-09-09 description: This is the first post of my new Astro blog. image: ./cover.jpg tags: [Foo, Bar] category: Front-end draft: false ---AttributeDescription title. The title of the post. published. The date the post was published. description. A short description of the post. Displayed on index page. image. The cover image path of the post. 1. Start with http:// or https://: Use web image 2. Start with /: For image in public dir 3. With none of the prefixes: Relative to the markdown file. tags. The tags of the post. category. The category of the post. draft. If this post is still a draft, which wont be displayed. Where to Place the Post Files. Your post files should be placed in src/content/posts/ directory. You can also create sub-directories to better organize your posts and assets. src/content/posts/ ├── post-1.md └── post-2/ ├── cover.png └── index.md.","word_count":187,"filters":{},"meta":{"title":"Simple Guides for Fuwari - Fuwari"},"anchors":[{"element":"h2","id":"front-matter-of-posts","text":"Front-matter of Posts","location":34},{"element":"h2","id":"where-to-place-the-post-files","text":"Where to Place the Post Files","location":151}],"weighted_locations":[{"weight":10,"balanced_score":57600,"location":3}],"locations":[3],"raw_content":"Simple Guides for Fuwari. Cover image source: Source. This blog template is built with Astro. For the things that are not mentioned in this guide, you may find the answers in the Astro Docs. Front-matter of Posts. --- title: My First Blog Post published: 2023-09-09 description: This is the first post of my new Astro blog. image: ./cover.jpg tags: [Foo, Bar] category: Front-end draft: false ---AttributeDescription title. The title of the post. published. The date the post was published. description. A short description of the post. Displayed on index page. image. The cover image path of the post. 1. Start with http:// or https://: Use web image 2. Start with /: For image in public dir 3. With none of the prefixes: Relative to the markdown file. tags. The tags of the post. category. The category of the post. draft. If this post is still a draft, which wont be displayed. Where to Place the Post Files. Your post files should be placed in src/content/posts/ directory. You can also create sub-directories to better organize your posts and assets. src/content/posts/ ├── post-1.md └── post-2/ ├── cover.png └── index.md.","raw_url":"/posts/guide/","excerpt":"Simple Guides for <mark>Fuwari.</mark> Cover image source: Source. This blog template is built with Astro. For the things that are not mentioned in this guide, you may find the answers","sub_results":[{"title":"Simple Guides for Fuwari - Fuwari","url":"/posts/guide/","weighted_locations":[{"weight":10,"balanced_score":57600,"location":3}],"locations":[3],"excerpt":"Simple Guides for <mark>Fuwari.</mark> Cover image source: Source. This blog template is built with Astro. For the things that are not mentioned in this guide, you may find the answers"}]},{"url":"/about/","content":"About. This is the demo site for Fuwari. Sources of images used in this site. Unsplash. 星と少女 by Stella. Rabbit - v1.4 Showcase by Rabbit_YourMajesty.","word_count":25,"filters":{},"meta":{"title":"About"},"anchors":[{"element":"h1","id":"about","text":"About","location":0},{"element":"h3","id":"sources-of-images-used-in-this-site","text":"Sources of images used in this site","location":8}],"weighted_locations":[{"weight":1,"balanced_score":576,"location":7}],"locations":[7],"raw_content":"About. This is the demo site for Fuwari. Sources of images used in this site. Unsplash. 星と少女 by Stella. Rabbit - v1.4 Showcase by Rabbit_YourMajesty.","raw_url":"/about/","excerpt":"About. This is the demo site for <mark>Fuwari.</mark> Sources of images used in this site. Unsplash. 星と少女 by Stella. Rabbit - v1.4 Showcase by Rabbit_YourMajesty.","sub_results":[{"title":"About","url":"/about/#about","anchor":{"element":"h1","id":"about","text":"About","location":0},"weighted_locations":[{"weight":1,"balanced_score":576,"location":7}],"locations":[7],"excerpt":"About. This is the demo site for <mark>Fuwari.</mark>"}]}]')
if (!arr.length && isDesktop) {
// for desktop hide panel immediately if results is empty
panel.classList.add("closed");
return
}
panel.classList.remove("closed");
result.value = arr
}
const togglePanel = async () => {
console.log('togglePanel')
let panel = document.getElementById('search-panel');
panel.classList.toggle("closed");
}
watch(keywordDesktop, async (newVal, oldVal) => {
search(newVal, true)
})
watch(keywordMobile, async (newVal, oldVal) => {
search(newVal, false)
})
</script>
<template>
<!-- search bar for desktop view -->
<div id="search-bar" class="hidden lg:flex transition-all items-center h-11 mr-2 rounded-lg
bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06]
dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10
">
<slot name="search-icon"></slot>
<input placeholder="Search" v-model="keywordDesktop" @focusin="search(keywordDesktop, true)"
class="transition-all text-sm ml-2 bg-transparent outline-0
h-full w-40 active:w-60 focus:w-60 text-black/50 dark:text-white/50"
>
</div>
<!-- toggle btn for phone/tablet view -->
<div @click="togglePanel">
<slot name="search-switch"></slot>
</div>
<!-- search panel -->
<div id="search-panel" class="float-panel closed search-panel absolute md:w-[30rem] top-20 left-4 md:left-[unset] right-4 shadow-2xl rounded-2xl p-2">
<!-- search bar inside panel for phone/tablet -->
<div id="search-bar-inside" class="flex lg:hidden transition-all items-center h-11 rounded-xl
bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06]
dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10
">
<slot name="search-icon"></slot>
<input placeholder="Search" v-model="keywordMobile"
class="transition-all text-sm ml-2 bg-transparent outline-0
h-full w-full focus:w-60 text-black/50 dark:text-white/50"
>
</div>
<!-- search results -->
<a v-for="item of result" :href="item.url"
class="transition first-of-type:mt-2 lg:first-of-type:mt-0 group block
rounded-xl text-lg px-3 py-2 hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)]">
<div class="transition text-deep inline-flex font-bold group-hover:text-[var(--primary)]">
{{ item.meta.title }}<slot name="arrow-icon"></slot>
</div>
<!-- list all sub results looks bad -->
<!-- <div v-for="sub of item.sub_results" v-html="sub.excerpt" class="transition text-sm text-sub">-->
<!-- </div>-->
<div v-html="item.excerpt" class="transition text-sm text-sub">
</div>
</a>
</div>
</template>
<style scoped>
</style>

View File

@@ -7,7 +7,7 @@ interface Props {
}
const className = Astro.props.class;
---
<div class=`prose dark:prose-invert prose-base max-w-none custom-md ${className}`>
<div data-pagefind-body class=`prose dark:prose-invert prose-base max-w-none custom-md ${className}`>
<!--<div class="prose dark:prose-invert max-w-none custom-md">-->
<!--<div class="max-w-none custom-md">-->
<slot />

View File

@@ -234,19 +234,22 @@ function activateDisplaySettings() {
setHue(hue);
}
function setClickOutsideToClose(panel, switchBtn) {
function setClickOutsideToClose(panel: string, ignores: string[]) {
document.addEventListener("click", event => {
var cDom = document.getElementById(panel);
let settingBtn = document.getElementById(switchBtn);
var tDom = event.target;
if (cDom == tDom || cDom.contains(tDom) || settingBtn == tDom || settingBtn.contains(tDom)) {
return;
let panelDom = document.getElementById(panel);
let tDom = event.target;
for (let ig of ignores) {
let ie = document.getElementById(ig)
if (ie == tDom || ie.contains(tDom)) {
return;
}
}
cDom.classList.add("closed");
panelDom.classList.add("closed");
});
}
setClickOutsideToClose("display-setting", "display-settings-switch")
setClickOutsideToClose("nav-menu-panel", "nav-menu-switch")
setClickOutsideToClose("display-setting", ["display-setting", "display-settings-switch"])
setClickOutsideToClose("nav-menu-panel", ["nav-menu-panel", "nav-menu-switch"])
setClickOutsideToClose("search-panel", ["search-panel", "search-bar", "search-switch"])
}
function loadTheme() {

View File

@@ -52,6 +52,7 @@ const { remarkPluginFrontmatter } = await entry.render();
<!-- title -->
<div class="relative">
<div
data-pagefind-body data-pagefind-weight="10" data-pagefind-meta="title"
class="transition w-full block font-bold mb-3
text-3xl md:text-[2.5rem]/[2.75rem]
text-black/90 dark:text-white/90