feat: Phase 5.2 - Global Search with Laravel Scout
- Install laravel/scout with database driver - Add Searchable trait to Feature, Issue, Task, Inspection models - Create GlobalSearch Livewire component with Blade view - Configure scout.php for database driver + queued syncing - Add SCOUT_DRIVER=database to .env.example Tests: 101 passing (319 assertions)
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
<div class="relative" wire:ignore.self>
|
||||
<div class="relative w-full max-w-xl">
|
||||
<!-- Search Input -->
|
||||
<div class="relative">
|
||||
<label for="global-search" class="sr-only">{{ __('Buscar globalmente') }}</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg class="h-5 w-5 text-base-content/40" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="global-search"
|
||||
wire:model.debounce.300ms="query"
|
||||
wire:keydown.down="focusFirstResult"
|
||||
wire:keydown.enter="selectFirstResult"
|
||||
wire:keydown.escape="clearSearch"
|
||||
placeholder="{{ __('Buscar en proyectos, elementos, tareas, incidencias...') }}"
|
||||
class="block w-full pl-10 pr-4 py-2 border border-base-300 rounded-lg bg-base-100 text-base-content placeholder:text-base-content/40 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
|
||||
autocomplete="off"
|
||||
@click="showResults = true"
|
||||
>
|
||||
@if($query)
|
||||
<button
|
||||
wire:click="clearSearch"
|
||||
class="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
aria-label="{{ __('Limpiar búsqueda') }}"
|
||||
>
|
||||
<svg class="h-5 w-5 text-base-content/40 hover:text-base-content/60" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Dropdown -->
|
||||
@if($showResults && count($results) > 0)
|
||||
<div
|
||||
class="absolute z-50 mt-1 w-full max-w-xl bg-base-100 border border-base-200 rounded-lg shadow-lg overflow-hidden"
|
||||
style="max-height: 400px; overflow-y: auto;"
|
||||
x-data="{ highlightedIndex: 0 }"
|
||||
x-init="
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
@this.focusFirstResult();
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
highlightedIndex = Math.max(0, highlightedIndex - 1);
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
@this.selectFirstResult();
|
||||
} else if (e.key === 'Escape') {
|
||||
@this.clearSearch();
|
||||
}
|
||||
})
|
||||
"
|
||||
>
|
||||
@foreach($results as $index => $result)
|
||||
<a
|
||||
href="{{ $result['url'] }}"
|
||||
wire:navigate
|
||||
class="flex items-center gap-3 px-4 py-3 hover:bg-base-200 transition-colors {{ $index === 0 ? 'bg-base-200' : '' }}"
|
||||
:class="{ 'bg-base-200': highlightedIndex === {{ $index }} }"
|
||||
x-on:mouseenter="highlightedIndex = {{ $index }}"
|
||||
>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 rounded-lg flex items-center justify-center bg-{{ $result['color'] }}-100 text-{{ $result['color'] }}-600">
|
||||
<x-heroicon-o-{{ $result['icon'] }} class="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-base-content truncate">{{ $result['title'] }}</p>
|
||||
<p class="text-xs text-base-content/50 truncate">{{ $result['subtitle'] }}</p>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-base-content/40 bg-base-200 px-2 py-0.5 rounded">{{ $result['type_label'] }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@elseif($showResults && strlen($query) >= 2)
|
||||
<div class="absolute z-50 mt-1 w-full max-w-xl bg-base-100 border border-base-200 rounded-lg shadow-lg overflow-hidden">
|
||||
<div class="px-4 py-6 text-center text-base-content/50">
|
||||
<x-heroicon-o-magnifying-glass class="w-8 h-8 mx-auto text-base-content/30 mb-2" />
|
||||
<p class="text-sm">{{ __('No se encontraron resultados para') }} “{{ $query }}”</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('click', (e) => {
|
||||
const searchComponent = document.querySelector('[wire\\:id="{{ $id }}"]');
|
||||
if (searchComponent && !searchComponent.contains(e.target)) {
|
||||
@this.hideResults();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
@this.clearSearch();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
</div>
|
||||
Reference in New Issue
Block a user