SearchBox.svelte 3.01 KB
<script>
  import { onMount } from 'svelte';
  import { goto } from '$app/navigation';
  import SearchResults from './SearchResults.svelte';
  import {
    query,
    results,
    isLoading,
    indexLoaded,
    error,
    selectedIndex,
    showResults,
    filterType,
    initIndex,
    performSearch,
    clearSearch,
    navigateResults,
    setFilter
  } from '$lib/stores/searchStore';

  const filterOptions = [
    { value: 'todos', label: 'Todos' },
    { value: 'entidad', label: 'Entidades' },
    { value: 'objeto_gasto', label: 'Objetos' },
    { value: 'rubro', label: 'Rubros' }
  ];

  let inputRef;
  let debounceTimer;
  let isFocused = $state(false);

  onMount(() => {
    initIndex();
  });

  function handleInput(e) {
    const value = e.target.value;
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => {
      performSearch(value);
    }, 250);
  }

  function handleKeydown(e) {
    if (!$showResults) return;

    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        navigateResults('down', $results.length);
        break;
      case 'ArrowUp':
        e.preventDefault();
        navigateResults('up', $results.length);
        break;
      case 'Enter':
        e.preventDefault();
        if ($selectedIndex >= 0 && $results[$selectedIndex]) {
          const item = $results[$selectedIndex];
          if (item.tipo === 'entidad') {
            goto(`/entidad/${item.codigo}`);
          } else if (item.tipo === 'objeto_gasto') {
            goto(`/objeto/${item.codigo}`);
          } else if (item.tipo === 'rubro') {
            goto(`/rubro/${item.codigo}`);
          }
          clearSearch();
          inputRef.value = '';
        }
        break;
      case 'Escape':
        clearSearch();
        inputRef.value = '';
        inputRef.blur();
        break;
    }
  }

  function handleClickOutside(e) {
    if (!e.target.closest('.search-container')) {
      isFocused = false;
    }
  }
</script>

<svelte:window onclick={handleClickOutside} />

<div class="search-container relative">
  <div class="flex gap-2">
    <input
      bind:this={inputRef}
      type="text"
      placeholder={$indexLoaded ? "Buscar..." : "Cargando..."}
      disabled={!$indexLoaded}
      class="flex-1 p-2 border rounded"
      oninput={handleInput}
      onkeydown={handleKeydown}
      onfocus={() => isFocused = true}
    />
    <select
      class="px-3 py-2 border rounded bg-white text-sm text-slate-600 cursor-pointer"
      value={$filterType}
      onchange={(e) => setFilter(e.target.value)}
      disabled={!$indexLoaded}
    >
      {#each filterOptions as option}
        <option value={option.value}>{option.label}</option>
      {/each}
    </select>
  </div>

  {#if $isLoading}
    <span class="absolute right-2 top-2 text-sm text-gray-400">...</span>
  {/if}

  {#if isFocused && $showResults}
    <SearchResults
      results={$results}
      query={$query}
      selectedIndex={$selectedIndex}
    />
  {/if}

  {#if $error}
    <p class="text-red-500 text-sm mt-1">{$error}</p>
  {/if}
</div>