chefbible/src/lib/components/SearchBar.svelte

165 lines
4.5 KiB
Svelte

<script lang="ts">
import { goto } from '$app/navigation';
import { invalidate } from '$app/navigation';
export let recipes: any[] = [];
export let placeholder: string = 'Search recipes...';
let searchTerm = '';
let filteredRecipes: any[] = [];
let showResults = false;
function handleSearch() {
if (!searchTerm.trim()) {
filteredRecipes = [];
showResults = false;
return;
}
const term = searchTerm.toLowerCase();
filteredRecipes = recipes.filter(
(recipe) =>
recipe.name.toLowerCase().includes(term) ||
recipe.description?.toLowerCase().includes(term) ||
recipe.station.toLowerCase().includes(term) ||
recipe.time.toLowerCase().includes(term)
);
showResults = true;
}
function handleRecipeClick(recipe: any) {
window.location.href = `/recipe/${recipe.id}`;
searchTerm = '';
showResults = false;
}
function handleInputFocus() {
if (searchTerm.trim()) {
showResults = true;
}
}
let searchContainer: HTMLElement;
// Handle clicks outside the search container
function handleClickOutside(event: MouseEvent) {
if (searchContainer && !searchContainer.contains(event.target as Node)) {
showResults = false;
}
}
import { onMount } from 'svelte';
onMount(() => {
document.addEventListener('click', handleClickOutside);
return () => {
document.removeEventListener('click', handleClickOutside);
};
});
</script>
<div class="relative w-full max-w-lg" bind:this={searchContainer}>
<div
class="relative flex items-center rounded-xl border-2 border-transparent bg-white shadow-sm transition-all duration-200 focus-within:border-primary focus-within:shadow-lg"
>
<svg
class="pointer-events-none absolute left-4 text-gray-400"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
<input
type="text"
bind:value={searchTerm}
on:input={handleSearch}
on:focus={handleInputFocus}
{placeholder}
class="w-full border-none bg-transparent px-4 py-3.5 pl-12 text-base text-gray-900 outline-none placeholder:text-gray-400"
/>
{#if searchTerm}
<button
class="absolute right-3 cursor-pointer rounded border-none bg-none p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
aria-label="Clear search"
on:click={() => {
searchTerm = '';
showResults = false;
}}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
{/if}
</div>
{#if showResults && filteredRecipes.length > 0}
<div class="absolute top-full left-1/2 z-50 mt-1 w-screen -translate-x-1/2">
<div class="mx-auto max-w-7xl px-5">
<div class="rounded-xl border border-gray-200 bg-white shadow-2xl">
{#each filteredRecipes as recipe}
<button
class="flex w-full cursor-pointer items-center gap-3 border-b border-gray-100 bg-none p-4 transition-colors last:border-b-0 hover:bg-gray-50"
on:click={() => handleRecipeClick(recipe)}
>
<div class="flex-1 text-left">
<h4 class="mb-1 text-base font-semibold text-gray-900">{recipe.name}</h4>
{#if recipe.description}
<p class="mb-2 line-clamp-2 text-sm text-gray-600">{recipe.description}</p>
{/if}
<div class="flex gap-3">
<span class="rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700"
>{recipe.station}</span
>
<span
class="rounded-md bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-700"
>{recipe.time}</span
>
</div>
</div>
<svg
class="flex-shrink-0 text-gray-400"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M9 18l6-6-6-6" />
</svg>
</button>
{/each}
</div>
</div>
</div>
{:else if showResults && searchTerm.trim()}
<div class="absolute top-full left-1/2 z-50 mt-1 w-screen -translate-x-1/2">
<div class="mx-auto max-w-7xl px-5">
<div class="rounded-xl border border-gray-200 bg-white shadow-2xl">
<div class="p-6 text-center text-gray-600">
<p>No recipes found for "{searchTerm}"</p>
</div>
</div>
</div>
</div>
{/if}
</div>
<style>
/* No component CSS needed - all styles use Tailwind utilities */
</style>