# AJAX Search Implementation Guide

**Goal:** Refresh only the search results section when filters/search are applied, without full page reload.

---

## 🏆 Option 1: Laravel Livewire (RECOMMENDED)

### Why Livewire?
- ✅ **Zero JavaScript** - Everything in PHP
- ✅ **Automatic AJAX** - Livewire handles it
- ✅ **Loading states** - Built-in spinners
- ✅ **Reactive** - Updates in real-time
- ✅ **Perfect for Laravel** - Native integration
- ✅ **Easy to maintain** - No frontend/backend split

### Installation (5 minutes)

```bash
# Install Livewire
composer require livewire/livewire

# Publish config (optional)
php artisan livewire:publish --config

# Add to layout (already in your app)
# In resources/views/layouts/app.blade.php:
@livewireStyles  <!-- Before </head> -->
@livewireScripts <!-- Before </body> -->
```

---

### Implementation for Job Search

#### Step 1: Create Livewire Component

```bash
php artisan make:livewire JobSearchResults
```

This creates:
- `app/Livewire/JobSearchResults.php` (logic)
- `resources/views/livewire/job-search-results.blade.php` (view)

#### Step 2: Component Logic

**File:** `app/Livewire/JobSearchResults.php`

```php
<?php

namespace App\Livewire;

use App\Filters\JobFilters;
use App\Services\JobSearchService;
use Livewire\Component;
use Livewire\WithPagination;
use Illuminate\Http\Request;

class JobSearchResults extends Component
{
    use WithPagination;

    // Public properties (bound to inputs)
    public $keyword = '';
    public $country = '';
    public $state = '';
    public $specialty = '';
    public $employment_type = '';
    public $work_mode = '';
    public $salary_min = '';
    public $salary_max = '';
    public $visa_sponsorship = false;
    public $relocation_support = false;
    public $housing = false;
    public $verified_only = false;
    
    // For filter dropdowns
    public $availableCountries = [];
    public $availableSpecialties = [];
    public $availableStates = [];
    
    protected $queryString = [
        'keyword' => ['except' => ''],
        'country' => ['except' => ''],
        'specialty' => ['except' => ''],
        'page' => ['except' => 1],
    ];

    public function mount()
    {
        // Load filter options
        $this->loadFilterOptions();
        
        // Initialize from query string
        $this->keyword = request('keyword', '');
        $this->country = request('country', '');
        $this->specialty = request('specialty', '');
    }

    public function updatedCountry()
    {
        // When country changes, reset state and reload states
        $this->state = '';
        $this->loadStates();
        $this->resetPage(); // Reset to page 1
    }
    
    public function updatedKeyword()
    {
        $this->resetPage();
    }
    
    public function clearFilters()
    {
        $this->reset([
            'keyword', 'country', 'state', 'specialty', 
            'employment_type', 'work_mode', 'salary_min', 
            'salary_max', 'visa_sponsorship', 'relocation_support',
            'housing', 'verified_only'
        ]);
        $this->resetPage();
    }

    public function render()
    {
        // Build request from component properties
        $request = Request::create('/', 'GET', [
            'keyword' => $this->keyword,
            'country' => $this->country,
            'state' => $this->state,
            'specialty' => $this->specialty,
            'employment_type' => $this->employment_type,
            'work_mode' => $this->work_mode,
            'salary_min' => $this->salary_min,
            'salary_max' => $this->salary_max,
            'visa_sponsorship' => $this->visa_sponsorship,
            'relocation_support' => $this->relocation_support,
            'housing' => $this->housing,
            'verified_only' => $this->verified_only,
        ]);
        
        // Use existing search service
        $filters = new JobFilters($request);
        $searchService = new JobSearchService($filters);
        
        $jobs = $searchService->perPage(20)->search();
        $activeFilters = $filters->getActiveFilterLabels();
        
        return view('livewire.job-search-results', [
            'jobs' => $jobs,
            'activeFilters' => $activeFilters,
        ]);
    }
    
    private function loadFilterOptions()
    {
        // Get unique values for dropdowns
        $this->availableCountries = \DB::table('job_postings')
            ->whereIn('status', ['open', 'published'])
            ->whereNotNull('location_country')
            ->distinct()
            ->pluck('location_country')
            ->sort()
            ->values()
            ->toArray();
            
        $this->availableSpecialties = \DB::table('job_postings')
            ->whereIn('status', ['open', 'published'])
            ->whereNotNull('required_specialty')
            ->distinct()
            ->pluck('required_specialty')
            ->sort()
            ->values()
            ->toArray();
    }
    
    private function loadStates()
    {
        if ($this->country) {
            $this->availableStates = \DB::table('job_postings')
                ->where('location_country', $this->country)
                ->whereNotNull('location_state')
                ->distinct()
                ->pluck('location_state')
                ->sort()
                ->values()
                ->toArray();
        } else {
            $this->availableStates = [];
        }
    }
}
```

#### Step 3: Component View

**File:** `resources/views/livewire/job-search-results.blade.php`

```blade
<div class="space-y-6">
    {{-- Search & Filters Section --}}
    <div class="bg-white rounded-lg shadow-sm p-6">
        {{-- Search Bar --}}
        <div class="mb-4">
            <input 
                type="text" 
                wire:model.live.debounce.300ms="keyword"
                placeholder="Search jobs by title, specialty, or keyword..."
                class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
            >
        </div>
        
        {{-- Filters Grid --}}
        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
            {{-- Country --}}
            <div>
                <label class="block text-sm font-medium text-gray-700 mb-1">Country</label>
                <select wire:model.live="country" class="w-full px-3 py-2 border border-gray-300 rounded-lg">
                    <option value="">All Countries</option>
                    @foreach($availableCountries as $c)
                        <option value="{{ $c }}">{{ $c }}</option>
                    @endforeach
                </select>
            </div>
            
            {{-- State (conditional) --}}
            @if($country && count($availableStates) > 0)
            <div>
                <label class="block text-sm font-medium text-gray-700 mb-1">State</label>
                <select wire:model.live="state" class="w-full px-3 py-2 border border-gray-300 rounded-lg">
                    <option value="">All States</option>
                    @foreach($availableStates as $s)
                        <option value="{{ $s }}">{{ $s }}</option>
                    @endforeach
                </select>
            </div>
            @endif
            
            {{-- Specialty --}}
            <div>
                <label class="block text-sm font-medium text-gray-700 mb-1">Specialty</label>
                <select wire:model.live="specialty" class="w-full px-3 py-2 border border-gray-300 rounded-lg">
                    <option value="">All Specialties</option>
                    @foreach($availableSpecialties as $spec)
                        <option value="{{ $spec }}">{{ $spec }}</option>
                    @endforeach
                </select>
            </div>
            
            {{-- Work Mode --}}
            <div>
                <label class="block text-sm font-medium text-gray-700 mb-1">Work Mode</label>
                <select wire:model.live="work_mode" class="w-full px-3 py-2 border border-gray-300 rounded-lg">
                    <option value="">All Modes</option>
                    <option value="remote">Remote</option>
                    <option value="onsite">On-site</option>
                    <option value="hybrid">Hybrid</option>
                </select>
            </div>
        </div>
        
        {{-- Boolean Filters --}}
        <div class="flex flex-wrap gap-4 mb-4">
            <label class="flex items-center space-x-2 cursor-pointer">
                <input type="checkbox" wire:model.live="visa_sponsorship" class="rounded">
                <span class="text-sm">Visa Sponsorship</span>
            </label>
            
            <label class="flex items-center space-x-2 cursor-pointer">
                <input type="checkbox" wire:model.live="relocation_support" class="rounded">
                <span class="text-sm">Relocation Support</span>
            </label>
            
            <label class="flex items-center space-x-2 cursor-pointer">
                <input type="checkbox" wire:model.live="housing" class="rounded">
                <span class="text-sm">Housing Provided</span>
            </label>
            
            <label class="flex items-center space-x-2 cursor-pointer">
                <input type="checkbox" wire:model.live="verified_only" class="rounded">
                <span class="text-sm">Verified Employers Only</span>
            </label>
        </div>
        
        {{-- Active Filters & Clear Button --}}
        @if(count($activeFilters) > 0)
        <div class="flex items-center justify-between">
            <div class="flex flex-wrap gap-2">
                @foreach($activeFilters as $filter)
                    <span class="px-3 py-1 bg-blue-100 text-blue-700 text-sm rounded-full">
                        {{ $filter }}
                    </span>
                @endforeach
            </div>
            <button 
                wire:click="clearFilters" 
                class="text-sm text-red-600 hover:text-red-800"
            >
                Clear all filters
            </button>
        </div>
        @endif
    </div>
    
    {{-- Loading Indicator --}}
    <div wire:loading class="text-center py-4">
        <div class="inline-flex items-center space-x-2">
            <svg class="animate-spin h-5 w-5 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
            </svg>
            <span class="text-gray-600">Searching...</span>
        </div>
    </div>
    
    {{-- Results Section (This is what refreshes) --}}
    <div wire:loading.remove>
        @if($jobs->count() > 0)
            {{-- Results Count --}}
            <div class="mb-4 text-sm text-gray-600">
                Showing {{ $jobs->firstItem() }} to {{ $jobs->lastItem() }} of {{ $jobs->total() }} jobs
            </div>
            
            {{-- Job Cards --}}
            <div class="space-y-4">
                @foreach($jobs as $job)
                    <div class="bg-white rounded-lg shadow-sm p-6 hover:shadow-md transition-shadow">
                        <div class="flex items-start justify-between">
                            <div class="flex-1">
                                <h3 class="text-lg font-semibold text-gray-900 mb-2">
                                    <a href="{{ route('jobs.show', $job->id) }}" class="hover:text-blue-600">
                                        {{ $job->title }}
                                    </a>
                                </h3>
                                
                                <div class="flex items-center space-x-4 text-sm text-gray-600 mb-3">
                                    <span class="flex items-center">
                                        <svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
                                            <path d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"/>
                                        </svg>
                                        {{ $job->employer->company_name ?? 'Unknown' }}
                                    </span>
                                    <span class="flex items-center">
                                        <svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
                                            <path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"/>
                                        </svg>
                                        {{ $job->location_city }}, {{ $job->location_country }}
                                    </span>
                                </div>
                                
                                <p class="text-gray-600 text-sm mb-3">
                                    {{ Str::limit($job->summary, 150) }}
                                </p>
                                
                                <div class="flex flex-wrap gap-2">
                                    @if($job->employment_type)
                                        <span class="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded">
                                            {{ $job->employment_type }}
                                        </span>
                                    @endif
                                    @if($job->work_mode)
                                        <span class="px-2 py-1 bg-blue-100 text-blue-700 text-xs rounded">
                                            {{ $job->work_mode }}
                                        </span>
                                    @endif
                                    @if($job->visa_sponsorship)
                                        <span class="px-2 py-1 bg-green-100 text-green-700 text-xs rounded">
                                            Visa Sponsorship
                                        </span>
                                    @endif
                                </div>
                            </div>
                            
                            <div class="ml-4 text-right">
                                @if($job->salary_min || $job->salary_max)
                                    <div class="text-lg font-semibold text-gray-900">
                                        ${{ number_format($job->salary_min) }} - ${{ number_format($job->salary_max) }}
                                    </div>
                                @endif
                                <div class="text-sm text-gray-500 mt-1">
                                    {{ $job->published_at?->diffForHumans() }}
                                </div>
                            </div>
                        </div>
                    </div>
                @endforeach
            </div>
            
            {{-- Pagination --}}
            <div class="mt-6">
                {{ $jobs->links() }}
            </div>
        @else
            {{-- No Results --}}
            <div class="bg-white rounded-lg shadow-sm p-12 text-center">
                <svg class="w-16 h-16 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
                </svg>
                <h3 class="text-xl font-semibold text-gray-900 mb-2">No jobs found</h3>
                <p class="text-gray-600 mb-4">Try adjusting your filters or search terms</p>
                <button wire:click="clearFilters" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
                    Clear all filters
                </button>
            </div>
        @endif
    </div>
</div>
```

#### Step 4: Update Main Jobs Page

**File:** `resources/views/jobs/index.blade.php`

Replace the entire content with:

```blade
<x-app-layout>
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
        <div class="mb-8">
            <h1 class="text-3xl font-bold text-gray-900">Find Your Next Nursing Job</h1>
            <p class="text-gray-600 mt-2">Explore thousands of nursing opportunities worldwide</p>
        </div>
        
        {{-- Livewire Component (handles everything) --}}
        <livewire:job-search-results />
    </div>
</x-app-layout>
```

#### Step 5: Update Routes (Optional)

Your existing route still works, but you can add a Livewire-specific route:

```php
// routes/web.php
Route::get('/jobs', [JobController::class, 'index'])->name('jobs.index');
// OR use Livewire directly
Route::get('/jobs', function () {
    return view('jobs.index');
})->name('jobs.index');
```

---

### Benefits of This Approach

✅ **No page refresh** - Only results section updates
✅ **Real-time** - Updates as you type (with debounce)
✅ **Loading states** - Automatic spinners
✅ **URL updates** - Filters stay in URL for sharing
✅ **Back button works** - Browser history maintained
✅ **Reuses existing code** - JobSearchService, JobFilters
✅ **Easy to maintain** - All logic in PHP
✅ **Mobile-friendly** - Works everywhere
✅ **SEO-friendly** - Server-side rendering

---

## 🎨 Option 2: Alpine.js + Fetch API

### Why Alpine.js?
- ✅ Lightweight (15KB)
- ✅ Works with existing controllers
- ✅ More control over requests
- ✅ No build process

### Installation

```blade
{{-- In your layout head --}}
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
```

### Implementation

**File:** `resources/views/jobs/index.blade.php`

```blade
<x-app-layout>
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"
         x-data="jobSearch()"
         x-init="init()">
         
        {{-- Search & Filters --}}
        <div class="bg-white rounded-lg shadow-sm p-6 mb-6">
            <input 
                type="text" 
                x-model="filters.keyword"
                @input.debounce.300ms="search()"
                placeholder="Search jobs..."
                class="w-full px-4 py-3 border rounded-lg"
            >
            
            <div class="grid grid-cols-4 gap-4 mt-4">
                <select x-model="filters.country" @change="search()" class="border rounded-lg px-3 py-2">
                    <option value="">All Countries</option>
                    <template x-for="country in availableCountries" :key="country">
                        <option :value="country" x-text="country"></option>
                    </template>
                </select>
                
                {{-- Add other filters --}}
            </div>
        </div>
        
        {{-- Loading Indicator --}}
        <div x-show="loading" class="text-center py-8">
            <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
        </div>
        
        {{-- Results --}}
        <div x-show="!loading" id="results-container">
            <template x-for="job in jobs" :key="job.id">
                <div class="bg-white rounded-lg shadow-sm p-6 mb-4">
                    <h3 class="text-lg font-semibold" x-text="job.title"></h3>
                    <p class="text-gray-600" x-text="job.summary"></p>
                </div>
            </template>
            
            <div x-show="jobs.length === 0" class="text-center py-12">
                <p class="text-gray-600">No jobs found</p>
            </div>
        </div>
        
        {{-- Pagination --}}
        <div class="mt-6 flex justify-center gap-2">
            <template x-for="page in pagination.lastPage" :key="page">
                <button 
                    @click="goToPage(page)"
                    :class="page === pagination.currentPage ? 'bg-blue-600 text-white' : 'bg-white text-gray-700'"
                    class="px-4 py-2 rounded-lg border"
                    x-text="page"
                ></button>
            </template>
        </div>
    </div>
    
    <script>
        function jobSearch() {
            return {
                jobs: [],
                loading: false,
                availableCountries: [],
                availableSpecialties: [],
                filters: {
                    keyword: '',
                    country: '',
                    specialty: '',
                    page: 1
                },
                pagination: {
                    currentPage: 1,
                    lastPage: 1,
                    total: 0
                },
                
                init() {
                    // Load initial data
                    this.loadFilterOptions();
                    this.search();
                },
                
                async loadFilterOptions() {
                    const response = await fetch('/api/jobs/filter-options');
                    const data = await response.json();
                    this.availableCountries = data.countries;
                    this.availableSpecialties = data.specialties;
                },
                
                async search() {
                    this.loading = true;
                    
                    // Build query string
                    const params = new URLSearchParams(this.filters);
                    
                    try {
                        const response = await fetch(`/api/jobs/search?${params}`);
                        const data = await response.json();
                        
                        this.jobs = data.jobs.data;
                        this.pagination = {
                            currentPage: data.jobs.current_page,
                            lastPage: data.jobs.last_page,
                            total: data.jobs.total
                        };
                        
                        // Update URL without reload
                        history.pushState({}, '', `/jobs?${params}`);
                    } catch (error) {
                        console.error('Search failed:', error);
                    } finally {
                        this.loading = false;
                    }
                },
                
                goToPage(page) {
                    this.filters.page = page;
                    this.search();
                }
            }
        }
    </script>
</x-app-layout>
```

### Required API Endpoint

**File:** `app/Http/Controllers/Api/JobSearchController.php`

```php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Filters\JobFilters;
use App\Services\JobSearchService;
use Illuminate\Http\Request;

class JobSearchController extends Controller
{
    public function search(Request $request)
    {
        $filters = new JobFilters($request);
        $searchService = new JobSearchService($filters);
        
        $jobs = $searchService->perPage(20)->search();
        $availableOptions = $searchService->getAvailableOptions();
        
        return response()->json([
            'jobs' => $jobs,
            'filters' => $filters->toArray(),
            'activeFilters' => $filters->getActiveFilterLabels(),
        ]);
    }
    
    public function filterOptions()
    {
        return response()->json([
            'countries' => \DB::table('job_postings')
                ->distinct()
                ->pluck('location_country')
                ->filter()
                ->sort()
                ->values(),
            'specialties' => \DB::table('job_postings')
                ->distinct()
                ->pluck('required_specialty')
                ->filter()
                ->sort()
                ->values(),
        ]);
    }
}
```

**Add routes:**
```php
// routes/api.php
Route::get('/jobs/search', [Api\JobSearchController::class, 'search']);
Route::get('/jobs/filter-options', [Api\JobSearchController::class, 'filterOptions']);
```

---

## 📊 Comparison

| Feature | Livewire | Alpine.js |
|---------|----------|-----------|
| **JavaScript** | None | Minimal |
| **Learning Curve** | Easy (PHP only) | Medium (JS knowledge) |
| **Server Load** | Higher (each change = request) | Lower (batch requests) |
| **Real-time Updates** | Excellent | Excellent |
| **SEO** | Perfect (server-rendered) | Good (initial render) |
| **Bundle Size** | ~60KB | ~15KB |
| **Maintenance** | Easier (PHP only) | More complex (PHP + JS) |
| **Best For** | Laravel devs, rapid development | Full control, performance-critical |

---

## 🎯 My Recommendation

### **Use Livewire** because:

1. ✅ **You're already using Laravel/Blade** - Perfect fit
2. ✅ **Zero JavaScript** - Stay in PHP comfort zone
3. ✅ **Faster development** - 2-3 hours vs 4-6 hours
4. ✅ **Easier maintenance** - One codebase
5. ✅ **Your team knows PHP** - No JS expertise needed
6. ✅ **Reuses existing services** - JobSearchService, JobFilters work as-is
7. ✅ **Built-in features** - Loading states, validation, security

---

## 🚀 Quick Start (Choose One)

### Want me to implement Livewire? Say:
**"Implement Livewire for job search"**

### Want me to implement Alpine.js? Say:
**"Implement Alpine.js for job search"**

### Want to see a demo first? Say:
**"Show me a working example"**

---

## 📋 Implementation Checklist

### Livewire Implementation (Recommended)
- [ ] Install Livewire package
- [ ] Add Livewire styles/scripts to layout
- [ ] Create JobSearchResults component
- [ ] Update jobs index view
- [ ] Test search and filters
- [ ] Test pagination
- [ ] Add loading indicators
- [ ] Deploy and monitor

**Time:** 2-3 hours

### Alpine.js Implementation
- [ ] Add Alpine.js CDN to layout
- [ ] Create API controller
- [ ] Add API routes
- [ ] Update jobs index view with Alpine
- [ ] Implement search function
- [ ] Add pagination
- [ ] Test and debug
- [ ] Deploy and monitor

**Time:** 4-6 hours

---

## 💡 Additional Features (After Basic Implementation)

Once you have AJAX working, you can easily add:

1. **Saved searches** - Let users save filter combinations
2. **Search history** - Show recent searches
3. **Instant suggestions** - Autocomplete as they type
4. **Filter badges** - Visual active filter pills
5. **Sort options** - Date, salary, relevance
6. **View toggle** - List vs grid view
7. **Bulk actions** - Save multiple jobs
8. **Export results** - Download job list

---

Would you like me to implement one of these approaches for you?
