lastwebnovel-app/app/components/NovelSearch.vue
2026-04-11 22:55:16 +02:00

85 lines
1.9 KiB
Vue

<script setup lang="ts">
import type { WebNovel } from '~/types'
const route = useRoute()
const router = useRouter()
const open = ref(false)
const query = ref('')
const results = ref<WebNovel[]>([])
const isLoading = ref(false)
const searchNovels = async () => {
if (!query.value.trim()) {
results.value = []
return
}
isLoading.value = true
try {
const response = await $fetch<WebNovel[]>('/api/novels/search', {
query: { q: query.value }
})
results.value = response
} catch (error) {
console.error('Search error:', error)
} finally {
isLoading.value = false
}
}
const selectResult = (novel: WebNovel) => {
router.push(`/novels/${novel.id}`)
open.value = false
query.value = ''
}
// Watch for Ctrl+K
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault()
open.value = !open.value
}
}
onMounted(() => {
window.addEventListener('keydown', handleKeyDown)
})
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyDown)
})
</script>
<template>
<UCommandPalette
v-model="open"
:groups="[
{
id: 'novels',
label: 'Novels',
commands: results.map(novel => ({
id: novel.id,
label: `${novel.title}`,
description: `by ${typeof novel.author === 'string' ? novel.author : novel.author?.name} · ${(novel.rating || 0).toFixed(1)}⭐`,
icon: 'i-lucide-book',
click: () => selectResult(novel)
}))
}
]"
:loading="isLoading"
icon="i-lucide-search"
placeholder="Search novels..."
:ui="{ base: 'h-64 sm:h-96' }"
@update:model-value="val => open = val"
>
<template #default="{ query: q }">
<input
v-model="query"
placeholder="Search novels, authors, tags..."
class="w-full bg-transparent px-2 py-1 outline-none"
@input="searchNovels"
>
</template>
</UCommandPalette>
</template>