feat: search feature with debounce

This commit is contained in:
Ruidy 2024-12-14 09:46:57 +01:00
parent a5d328a133
commit 2d9fdd07c2
No known key found for this signature in database
GPG key ID: E00F51288CB857CC
7 changed files with 76 additions and 3 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -48,7 +48,8 @@ const handleRandomClick = async () => {
<li><a>Categories</a></li> <li><a>Categories</a></li>
</ul> </ul>
</div> </div>
<div class="navbar-end"> <div class="navbar-end gap-2">
<recipe-search />
<button class="btn btn-primary" @click="handleRandomClick">Random</button> <button class="btn btn-primary" @click="handleRandomClick">Random</button>
</div> </div>
</nav> </nav>

View file

@ -0,0 +1,48 @@
<template>
<label
class="input input-bordered input-primary flex items-center gap-2 container mx-auto px-4 lg:px-8 my-4"
>
<input
type="text"
class="grow"
placeholder="Search"
v-model="searchQuery"
@focus="isFocused = true"
@blur="isFocused = false"
/>
<kbd class="kbd kbd-sm" :class="{ 'opacity-50': !isFocused }"></kbd>
<kbd class="kbd kbd-sm" :class="{ 'opacity-50': !isFocused }">K</kbd>
</label>
</template>
<script setup lang="ts">
const searchQuery = ref("");
const isFocused = ref(false);
// Debounced search function
const debouncedSearch = useDebounceFn(async (query: string) => {
const { data, status, error } = await useRecipeSearch(query);
console.log("result", data.value, status.value, error.value);
}, 500);
// Watch for changes in searchQuery
watch(searchQuery, (newQuery) => {
debouncedSearch(newQuery);
});
// Optional: Handle keyboard shortcut
onMounted(() => {
const handleKeydown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
const inputEl = document.querySelector("input");
inputEl?.focus();
}
};
window.addEventListener("keydown", handleKeydown);
onUnmounted(() => {
window.removeEventListener("keydown", handleKeydown);
});
});
</script>

View file

@ -0,0 +1,4 @@
export default function useRecipeSearch(query: string) {
const { $client } = useNuxtApp();
return $client.recipeSearch.useQuery(query);
}

View file

@ -8,6 +8,7 @@ export default defineNuxtConfig({
"nuxt-icon", "nuxt-icon",
"nuxt-delay-hydration", "nuxt-delay-hydration",
"@nuxtjs/robots", "@nuxtjs/robots",
"@vueuse/nuxt",
], ],
app: { app: {
@ -59,5 +60,4 @@ export default defineNuxtConfig({
ssr: true, ssr: true,
compatibilityDate: "2024-12-13", compatibilityDate: "2024-12-13",
}); });

View file

@ -18,6 +18,7 @@
"@nuxtjs/robots": "5.0.1", "@nuxtjs/robots": "5.0.1",
"@trpc/client": "^10.45.2", "@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2", "@trpc/server": "^10.45.2",
"@vueuse/nuxt": "12.0.0",
"nuxt": "^3.14.1592", "nuxt": "^3.14.1592",
"nuxt-icon": "^0.6.10", "nuxt-icon": "^0.6.10",
"trpc-nuxt": "^0.10.22", "trpc-nuxt": "^0.10.22",

View file

@ -42,4 +42,23 @@ export const recipeRouter = router({
const recipes = parseRecipeData(data); const recipes = parseRecipeData(data);
return recipes[0]; return recipes[0];
}), }),
recipeSearch: publicProcedure
.input(
z.string({
required_error: "search query is required",
}),
)
.query(async ({ input }) => {
const data = await $fetch<{ meals: Meal[] }>(
new URL(`search.php?s=${input}`, apiUrl).href,
);
if (!data?.meals) {
throw createError({
statusCode: 404,
statusMessage: "Recipe not found",
});
}
const recipes = parseRecipeData(data);
return recipes;
}),
}); });