libra/docs/stories/story-5.5-post-search.md

15 KiB

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

  • Search input on posts listing page
  • Search by title (both languages)
  • Search by body content (both languages)
  • Real-time search results (debounced)

User Experience

  • "No results found" message when empty
  • Clear search button
  • Search works in both Arabic and English
  • Only searches published posts

Optional Enhancements

  • Search highlights in results
  • Search suggestions

Quality Requirements

  • Debounce to reduce server load (300ms)
  • Case-insensitive search
  • Tests for search functionality

Technical Notes

Updated Posts Index Component

<?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

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

// 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

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

  • Search input on posts page
  • Searches title in both languages
  • Searches body in both languages
  • Real-time results with debounce
  • Clear search button works
  • "No results" message shows
  • Only published posts searched
  • Search highlighting works
  • Tests pass
  • 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

  • Search functionality implemented with proper debouncing (300ms)
  • Bilingual search working for both title and body
  • XSS protection in highlight function
  • Case-insensitive search implemented
  • Clear search resets pagination
  • "No results" message displays correctly
  • 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

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


Story DoD Checklist

1. Requirements Met:

  • 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 ✓
  • 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:

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

3. Testing:

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

4. Functionality & Verification:

  • Functionality verified through comprehensive test suite.
  • Edge cases handled (empty search, no results, case-insensitivity, RTL support).

5. Story Administration:

  • All tasks within the story file are marked as complete.
  • Decisions documented (search on raw JSON columns vs JSON path, highlight implementation).
  • Story wrap up section completed with change log and file list.

6. Dependencies, Build & Configuration:

  • Project builds successfully without errors.
  • Project linting passes (Pint --dirty).
  • [N/A] No new dependencies added.
  • [N/A] No new environment variables or configurations.

7. Documentation:

  • 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

  • I, the Developer Agent, confirm that all applicable items above have been addressed.

Status: Ready for Review