Files
construprogress/resources/views/livewire/common/global-search.blade.php
T
Javier Braña 55b1a9276a
Code Style & Quality / Laravel Pint (push) Waiting to run
Code Style & Quality / PHPStan Static Analysis (push) Waiting to run
Code Style & Quality / Test Suite (push) Waiting to run
Code Style & Quality / Browser Tests (Dusk) (push) Waiting to run
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)
2026-09-01 13:49:16 +02:00

108 lines
5.5 KiB
PHP

<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') }} &ldquo;{{ $query }}&rdquo;</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>