# Story 5.5: Post Search

## Epic Reference
**Epic 5:** Posts/Blog System

## User Story
As a **website visitor**,
I want **to search through blog posts**,
So that **I can find relevant legal articles and information**.

## Story Context

### Existing System Integration
- **Integrates with:** posts table, public posts listing
- **Technology:** Livewire Volt with real-time search
- **Follows pattern:** Search component pattern
- **Touch points:** Posts listing page

## Acceptance Criteria

### Search Functionality
- [x] Search input on posts listing page
- [x] Search by title (both languages)
- [x] Search by body content (both languages)
- [x] Real-time search results (debounced)

### User Experience
- [x] "No results found" message when empty
- [x] Clear search button
- [x] Search works in both Arabic and English
- [x] Only searches published posts

### Optional Enhancements
- [x] Search highlights in results
- [ ] Search suggestions

### Quality Requirements
- [x] Debounce to reduce server load (300ms)
- [x] Case-insensitive search
- [x] Tests for search functionality

## Technical Notes

### Updated Posts Index Component
```php
<?php

use App\Models\Post;
use Livewire\Volt\Component;
use Livewire\WithPagination;

new class extends Component {
    use WithPagination;

    public string $search = '';

    public function updatedSearch()
    {
        $this->resetPage();
    }

    public function clearSearch(): void
    {
        $this->search = '';
        $this->resetPage();
    }

    public function with(): array
    {
        return [
            'posts' => Post::published()
                ->when($this->search, function ($query) {
                    $search = $this->search;
                    $query->where(function ($q) use ($search) {
                        $q->where('title_ar', 'like', "%{$search}%")
                          ->orWhere('title_en', 'like', "%{$search}%")
                          ->orWhere('body_ar', 'like', "%{$search}%")
                          ->orWhere('body_en', 'like', "%{$search}%");
                    });
                })
                ->latest()
                ->paginate(10),
        ];
    }
}; ?>

<div class="max-w-4xl mx-auto">
    <flux:heading>{{ __('posts.title') }}</flux:heading>

    <!-- Search Bar -->
    <div class="mt-6 relative">
        <flux:input
            wire:model.live.debounce.300ms="search"
            placeholder="{{ __('posts.search_placeholder') }}"
            class="w-full"
        >
            <x-slot:leading>
                <flux:icon name="magnifying-glass" class="w-5 h-5 text-charcoal/50" />
            </x-slot:leading>
        </flux:input>

        @if($search)
            <button
                wire:click="clearSearch"
                class="absolute {{ app()->getLocale() === 'ar' ? 'left-3' : 'right-3' }} top-1/2 -translate-y-1/2 text-charcoal/50 hover:text-charcoal"
            >
                <flux:icon name="x-mark" class="w-5 h-5" />
            </button>
        @endif
    </div>

    <!-- Search Results Info -->
    @if($search)
        <p class="mt-4 text-sm text-charcoal/70">
            @if($posts->total() > 0)
                {{ __('posts.search_results', ['count' => $posts->total(), 'query' => $search]) }}
            @else
                {{ __('posts.no_results', ['query' => $search]) }}
            @endif
        </p>
    @endif

    <!-- Posts List -->
    <div class="mt-8 space-y-6">
        @forelse($posts as $post)
            <article class="bg-cream p-6 rounded-lg shadow-sm hover:shadow-md transition-shadow">
                <h2 class="text-xl font-semibold text-navy">
                    <a href="{{ route('posts.show', $post) }}" class="hover:text-gold">
                        @if($search)
                            {!! $this->highlightSearch($post->title, $search) !!}
                        @else
                            {{ $post->title }}
                        @endif
                    </a>
                </h2>

                <time class="text-sm text-charcoal/70 mt-2 block">
                    {{ $post->created_at->translatedFormat('d F Y') }}
                </time>

                <p class="mt-3 text-charcoal">
                    @if($search)
                        {!! $this->highlightSearch($post->excerpt, $search) !!}
                    @else
                        {{ $post->excerpt }}
                    @endif
                </p>

                <a href="{{ route('posts.show', $post) }}" class="text-gold hover:underline mt-4 inline-block">
                    {{ __('posts.read_more') }} &rarr;
                </a>
            </article>
        @empty
            <div class="text-center py-12">
                @if($search)
                    <flux:icon name="magnifying-glass" class="w-12 h-12 text-charcoal/30 mx-auto mb-4" />
                    <p class="text-charcoal/70">{{ __('posts.no_results', ['query' => $search]) }}</p>
                    <flux:button wire:click="clearSearch" class="mt-4">
                        {{ __('posts.clear_search') }}
                    </flux:button>
                @else
                    <p class="text-charcoal/70">{{ __('posts.no_posts') }}</p>
                @endif
            </div>
        @endforelse
    </div>

    {{ $posts->links() }}
</div>
```

### Highlight Helper Method
```php
public function highlightSearch(string $text, string $search): string
{
    if (empty($search)) {
        return e($text);
    }

    $escapedText = e($text);
    $escapedSearch = e($search);

    return preg_replace(
        '/(' . preg_quote($escapedSearch, '/') . ')/iu',
        '<mark class="bg-gold/30 rounded px-1">$1</mark>',
        $escapedText
    );
}
```

### Translation Strings
```php
// resources/lang/en/posts.php
return [
    'search_placeholder' => 'Search articles...',
    'search_results' => 'Found :count results for ":query"',
    'no_results' => 'No results found for ":query"',
    'clear_search' => 'Clear search',
];

// resources/lang/ar/posts.php
return [
    'search_placeholder' => 'البحث في المقالات...',
    'search_results' => 'تم العثور على :count نتيجة لـ ":query"',
    'no_results' => 'لم يتم العثور على نتائج لـ ":query"',
    'clear_search' => 'مسح البحث',
];
```

### Testing
```php
it('searches posts by title', function () {
    Post::factory()->published()->create(['title_en' => 'Legal Rights Guide']);
    Post::factory()->published()->create(['title_en' => 'Tax Information']);

    Volt::test('posts.index')
        ->set('search', 'Legal')
        ->assertSee('Legal Rights Guide')
        ->assertDontSee('Tax Information');
});

it('searches posts by body content', function () {
    Post::factory()->published()->create([
        'title_en' => 'Post 1',
        'body_en' => 'This discusses property law topics.',
    ]);
    Post::factory()->published()->create([
        'title_en' => 'Post 2',
        'body_en' => 'This is about family matters.',
    ]);

    Volt::test('posts.index')
        ->set('search', 'property')
        ->assertSee('Post 1')
        ->assertDontSee('Post 2');
});

it('shows no results message', function () {
    Volt::test('posts.index')
        ->set('search', 'nonexistent')
        ->assertSee('No results found');
});

it('only searches published posts', function () {
    Post::factory()->create(['status' => 'draft', 'title_en' => 'Draft Post']);
    Post::factory()->published()->create(['title_en' => 'Published Post']);

    Volt::test('posts.index')
        ->set('search', 'Post')
        ->assertSee('Published Post')
        ->assertDontSee('Draft Post');
});
```

## Definition of Done

- [x] Search input on posts page
- [x] Searches title in both languages
- [x] Searches body in both languages
- [x] Real-time results with debounce
- [x] Clear search button works
- [x] "No results" message shows
- [x] Only published posts searched
- [x] Search highlighting works
- [x] Tests pass
- [x] Code formatted with Pint

## Dev Agent Record

### Agent Model Used
Claude Opus 4.5

### Completion Notes
- Implemented search functionality on the posts index page with real-time debounced search (300ms)
- Search queries both title and body fields as JSON columns (works with SQLite for tests and MySQL/MariaDB for production)
- Added search result highlighting with gold background on matched terms
- Clear search button with RTL/LTR position awareness
- Added bilingual translation strings for search UI (English and Arabic)
- 9 new search-specific tests added to PostsTest.php covering title search, body search, Arabic search, case-insensitivity, pagination reset, and clear functionality
- All 22 posts tests pass, full test suite passes

### File List
- `resources/views/livewire/pages/posts/index.blade.php` (modified) - Added search functionality and highlight method
- `lang/en/posts.php` (modified) - Added search translation strings
- `lang/ar/posts.php` (modified) - Added Arabic search translation strings
- `tests/Feature/Public/PostsTest.php` (modified) - Added 9 search tests

### Change Log
- Added `$search` property with debounced live binding
- Added `updatedSearch()` method to reset pagination on search change
- Added `clearSearch()` method to clear search and reset pagination
- Added `highlightSearch()` method for search term highlighting with HTML escaping
- Updated `with()` query to filter posts by title and body columns using LIKE
- Added search input with magnifying glass icon
- Added clear search button (X) with RTL-aware positioning
- Added search results info message showing count or "no results" message
- Added empty state with search icon and clear button when search yields no results
- Search highlighting applied to both title and excerpt in search results

## Dependencies

- **Story 5.4:** Public posts display

## Estimation

**Complexity:** Low-Medium
**Estimated Effort:** 2-3 hours

---

## QA Results

### Review Date: 2025-12-27

### Reviewed By: Quinn (Test Architect)

### Code Quality Assessment

The implementation is solid and follows project conventions well. The search functionality is implemented correctly using Livewire Volt's class-based pattern with proper debouncing, pagination reset, and bilingual support. The `highlightSearch()` method is well-designed with proper XSS protection using `e()` for both input text and search term before applying regex highlighting.

**Notable positives:**
- Clean, readable code following coding standards
- Proper use of Flux UI components
- RTL-aware positioning for the clear search button
- Good separation between display logic (getTitle/getExcerpt) and search logic

### Refactoring Performed

None required - the implementation is clean and follows best practices.

### Compliance Check

- Coding Standards: ✓ Uses class-based Volt pattern, Flux UI components, proper testing
- Project Structure: ✓ Files in correct locations, naming follows conventions
- Testing Strategy: ✓ 9 comprehensive tests with 22 assertions covering all scenarios
- All ACs Met: ✓ All acceptance criteria verified with corresponding tests

### Improvements Checklist

- [x] Search functionality implemented with proper debouncing (300ms)
- [x] Bilingual search working for both title and body
- [x] XSS protection in highlight function
- [x] Case-insensitive search implemented
- [x] Clear search resets pagination
- [x] "No results" message displays correctly
- [x] Only published posts are searched
- [ ] Optional: Consider JSON path operators (`->`) for more precise JSON column searches in production MySQL (current LIKE on JSON works but is less precise)
- [ ] Optional enhancement: Search suggestions (marked as optional in story, not implemented)

### Security Review

✓ **XSS Protection**: The `highlightSearch()` method properly escapes HTML using `e()` before applying regex replacement, preventing XSS attacks.
✓ **Regex Injection**: `preg_quote()` is used to escape special regex characters in the search term.
✓ **Authorization**: Search is scoped to `published()` posts only - draft content is not accessible.

### Performance Considerations

✓ **Debouncing**: 300ms debounce reduces unnecessary server requests
✓ **Pagination**: 10 items per page limits data transfer
- Note: LIKE queries on JSON columns may become slow with very large datasets. For a blog with hundreds/thousands of posts, this is acceptable. For larger scale, consider full-text search indexes.

### Files Modified During Review

None - no refactoring was required.

### Gate Status

Gate: PASS → docs/qa/gates/5.5-post-search.yml

### Recommended Status

✓ Ready for Done - All acceptance criteria met, tests pass, code follows standards.

---

## Story DoD Checklist

### 1. Requirements Met:
- [x] All functional requirements specified in the story are implemented.
  - Search input on posts listing page ✓
  - Search by title (both languages) ✓
  - Search by body content (both languages) ✓
  - Real-time search with 300ms debounce ✓
- [x] All acceptance criteria defined in the story are met.
  - "No results found" message ✓
  - Clear search button ✓
  - Works in Arabic and English ✓
  - Only searches published posts ✓
  - Search highlights in results ✓

### 2. Coding Standards & Project Structure:
- [x] All new/modified code strictly adheres to `Operational Guidelines`.
- [x] All new/modified code aligns with `Project Structure` (file locations, naming, etc.).
- [x] Adherence to `Tech Stack` for technologies/versions used.
- [N/A] Adherence to `Api Reference` and `Data Models` - No API or data model changes.
- [x] Basic security best practices applied (HTML escaping in highlight function).
- [x] No new linter errors or warnings introduced (Pint passes).
- [x] Code is well-commented where necessary.

### 3. Testing:
- [x] All required unit tests implemented (9 new search tests).
- [N/A] Integration tests - Component tests cover functionality adequately.
- [x] All tests pass successfully (22 posts tests, full suite passes).
- [x] Test coverage meets project standards.

### 4. Functionality & Verification:
- [x] Functionality verified through comprehensive test suite.
- [x] Edge cases handled (empty search, no results, case-insensitivity, RTL support).

### 5. Story Administration:
- [x] All tasks within the story file are marked as complete.
- [x] Decisions documented (search on raw JSON columns vs JSON path, highlight implementation).
- [x] Story wrap up section completed with change log and file list.

### 6. Dependencies, Build & Configuration:
- [x] Project builds successfully without errors.
- [x] Project linting passes (Pint --dirty).
- [N/A] No new dependencies added.
- [N/A] No new environment variables or configurations.

### 7. Documentation:
- [x] Inline code documentation adequate (method names are self-documenting).
- [N/A] No user-facing documentation changes needed.
- [N/A] No architectural changes made.

### Final Confirmation
- [x] I, the Developer Agent, confirm that all applicable items above have been addressed.

**Status: Ready for Review**
