# Story 5.4: Public Posts Display

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

## User Story
As a **website visitor**,
I want **to view published blog posts**,
So that **I can read legal updates and articles from the firm**.

## Story Context

### Existing System Integration
- **Integrates with:** posts table (published only)
- **Technology:** Livewire Volt (public routes)
- **Follows pattern:** Public content display
- **Touch points:** Navigation, homepage

### Assumptions
- Post model exists with `published()` scope (from Story 5.1)
- Post model has locale-aware `title`, `body`, and `excerpt` accessors (from Story 5.1)
- Bilingual infrastructure is in place (from Epic 1)
- Base navigation includes link to posts section (from Epic 1)

## Acceptance Criteria

### Posts Listing Page
- [ ] Public access (no login required)
- [ ] Display in reverse chronological order
- [ ] Each post card shows:
  - Title
  - Publication date
  - Excerpt (first ~150 characters)
  - Read more link
- [ ] Pagination for many posts

### Individual Post Page
- [ ] Full post content displayed
- [ ] Clean, readable typography
- [ ] Publication date shown
- [ ] Back to posts list link

### Language Support
- [ ] Content displayed based on current site language
- [ ] Title and body in selected language
- [ ] Date formatting per locale

### Design Requirements
- [ ] Responsive design (mobile-friendly)
- [ ] Professional typography
- [ ] Consistent with brand colors

### Quality Requirements
- [ ] Only published posts visible
- [ ] Fast loading
- [ ] SEO-friendly URLs (optional per PRD)
- [ ] Tests for display logic

## Technical Notes

### Routes
```php
// routes/web.php (public)
Route::get('/posts', \App\Livewire\Posts\Index::class)->name('posts.index');
Route::get('/posts/{post}', \App\Livewire\Posts\Show::class)->name('posts.show');
```

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

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

new class extends Component {
    use WithPagination;

    public function with(): array
    {
        return [
            'posts' => Post::published()
                ->latest()
                ->paginate(10),
        ];
    }
}; ?>

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

    <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">
                        {{ $post->title }}
                    </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">
                    {{ $post->excerpt }}
                </p>

                <a href="{{ route('posts.show', $post) }}" class="text-gold hover:underline mt-4 inline-block">
                    {{ __('posts.read_more') }} &rarr;
                </a>
            </article>
        @empty
            <p class="text-center text-charcoal/70 py-12">
                {{ __('posts.no_posts') }}
            </p>
        @endforelse
    </div>

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

### Post Show Component
```php
<?php

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

new class extends Component {
    public Post $post;

    public function mount(Post $post): void
    {
        // Only show published posts
        abort_unless($post->status === 'published', 404);

        $this->post = $post;
    }
}; ?>

<article class="max-w-3xl mx-auto">
    <header class="mb-8">
        <flux:heading>{{ $post->title }}</flux:heading>

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

    <div class="prose prose-lg prose-navy max-w-none">
        {!! $post->body !!}
    </div>

    <footer class="mt-12 pt-6 border-t border-charcoal/20">
        <a href="{{ route('posts.index') }}" class="text-gold hover:underline">
            &larr; {{ __('posts.back_to_posts') }}
        </a>
    </footer>
</article>
```

### Prose Styling
```css
/* In app.css */
.prose-navy {
    --tw-prose-headings: theme('colors.navy');
    --tw-prose-links: theme('colors.gold');
    --tw-prose-bold: theme('colors.navy');
}

.prose-navy a {
    text-decoration: underline;
}

.prose-navy a:hover {
    color: theme('colors.gold-light');
}
```

## Testing Requirements

### Feature Tests
```php
use App\Models\Post;
use Livewire\Volt\Volt;

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

    $this->get(route('posts.index'))
        ->assertOk()
        ->assertSee('Published Post')
        ->assertDontSee('Draft Post');
});

test('posts displayed in reverse chronological order', function () {
    $older = Post::factory()->create(['status' => 'published', 'created_at' => now()->subDays(5)]);
    $newer = Post::factory()->create(['status' => 'published', 'created_at' => now()]);

    $this->get(route('posts.index'))
        ->assertOk()
        ->assertSeeInOrder([$newer->title, $older->title]);
});

test('posts listing page paginates results', function () {
    Post::factory()->count(15)->create(['status' => 'published']);

    $this->get(route('posts.index'))
        ->assertOk()
        ->assertSee('Next'); // pagination link
});

test('individual post page shows full content', function () {
    $post = Post::factory()->create(['status' => 'published']);

    $this->get(route('posts.show', $post))
        ->assertOk()
        ->assertSee($post->title)
        ->assertSee($post->body);
});

test('individual post page returns 404 for unpublished posts', function () {
    $post = Post::factory()->create(['status' => 'draft']);

    $this->get(route('posts.show', $post))
        ->assertNotFound();
});

test('individual post page returns 404 for non-existent posts', function () {
    $this->get(route('posts.show', 999))
        ->assertNotFound();
});
```

### Language/RTL Tests
- [ ] Arabic content displays correctly with RTL direction
- [ ] English content displays correctly with LTR direction
- [ ] Date formatting respects locale (`translatedFormat`)
- [ ] Back link text displays in current language

### Responsive Tests
- [ ] Mobile layout renders correctly (single column, touch-friendly)
- [ ] Tablet layout renders correctly
- [ ] Typography remains readable at all breakpoints

### Key Test Scenarios
| Scenario | Expected Result |
|----------|-----------------|
| Visit /posts as guest | Shows published posts list |
| Visit /posts with no posts | Shows "no posts" message |
| Visit /posts/{id} for published post | Shows full post content |
| Visit /posts/{id} for draft post | Returns 404 |
| Switch language on posts page | Content displays in selected language |

## Definition of Done

- [ ] Posts listing page works
- [ ] Individual post page works
- [ ] Only published posts visible
- [ ] Reverse chronological order
- [ ] Excerpts display correctly
- [ ] Full content renders properly
- [ ] Language-appropriate content shown
- [ ] Mobile responsive
- [ ] RTL support
- [ ] Tests pass
- [ ] Code formatted with Pint

## Dependencies

- **Story 5.1:** Post creation (provides Post model with `published()` scope, `title`, `body`, `excerpt` accessors - see Database Schema and Post Model sections)
- **Epic 1:** Base UI, navigation, bilingual infrastructure

## Estimation

**Complexity:** Medium
**Estimated Effort:** 3-4 hours

---

## Dev Agent Record

### Status
**Ready for Review**

### Agent Model Used
Claude Opus 4.5

### Tasks Completed
- [x] Add public posts routes to web.php (posts.index, posts.show)
- [x] Create Posts Index Volt component with pagination
- [x] Create Posts Show Volt component with 404 for unpublished
- [x] Add prose-navy styling to app.css
- [x] Add translation strings (read_more) to en/ar
- [x] Write feature tests for public posts (13 tests, 32 assertions)
- [x] Run Pint and verify all tests pass

### File List
| File | Action |
|------|--------|
| `routes/web.php` | Modified - Added Volt routes for posts.index and posts.show |
| `resources/views/livewire/pages/posts/index.blade.php` | Created - Posts listing Volt component |
| `resources/views/livewire/pages/posts/show.blade.php` | Created - Individual post Volt component |
| `resources/css/app.css` | Modified - Added prose-navy styling |
| `lang/en/posts.php` | Modified - Added read_more translation |
| `lang/ar/posts.php` | Modified - Added read_more translation |
| `tests/Feature/Public/PostsTest.php` | Created - 13 feature tests |
| `resources/views/pages/posts/index.blade.php` | Deleted - Replaced by Volt component |

### Change Log
- Used `#[Layout('components.layouts.public')]` attribute for full-page Volt components
- Used `Post::published()->latest()` for reverse chronological order
- Used `$post->getTitle()`, `$post->getBody()`, `$post->getExcerpt()` methods for locale-aware content
- Used `translatedFormat()` for locale-aware date formatting
- Added `wire:navigate` for SPA-like navigation between pages

### Debug Log References
None - implementation completed without issues.

### Completion Notes
- All 13 public posts tests passing
- Full test suite (362 tests, 883 assertions) passing
- Code formatted with Pint

---

## QA Results

### Review Date: 2025-12-27

### Reviewed By: Quinn (Test Architect)

### Code Quality Assessment

**Overall: Excellent** - Implementation is clean, well-structured, and follows all project conventions. The class-based Volt components are properly implemented with appropriate use of Flux UI components, wire:navigate for SPA navigation, and wire:key for list items. The Post model's locale-aware methods (getTitle, getBody, getExcerpt) are used correctly throughout.

**Strengths:**
- Clean separation using published() scope
- Proper 404 handling for unpublished posts using PostStatus enum
- Locale-aware content rendering with fallback to Arabic
- Professional prose-navy typography styling
- Comprehensive test coverage (13 tests, 32 assertions)

### Refactoring Performed

None required - implementation is solid and follows all coding standards.

### Compliance Check

- Coding Standards: ✓ Class-based Volt pattern, Flux UI components, proper naming
- Project Structure: ✓ Files in correct locations per source-tree.md
- Testing Strategy: ✓ Pest tests with comprehensive coverage
- All ACs Met: ✓ All 15 acceptance criteria verified

### Requirements Traceability

| Acceptance Criteria | Test Coverage |
|---------------------|---------------|
| Public access (no login required) | ✓ `posts listing page is accessible without authentication` |
| Reverse chronological order | ✓ `posts listing page displays in reverse chronological order` |
| Post cards show title, date, excerpt, read more | ✓ `posts listing page shows post excerpt` |
| Pagination | ✓ `posts listing page paginates results` |
| Full post content displayed | ✓ `individual post page shows full content` |
| Publication date shown | ✓ Verified in template |
| Back to posts link | ✓ `individual post page shows back to posts link` |
| Content in current locale | ✓ `posts listing page displays content in current locale` |
| Individual post locale support | ✓ `individual post page displays content in current locale` |
| Only published posts visible | ✓ `posts listing page shows only published posts` |
| 404 for unpublished posts | ✓ `individual post page returns 404 for unpublished posts` |
| 404 for non-existent posts | ✓ `individual post page returns 404 for non-existent posts` |
| No posts message | ✓ `posts listing page shows no posts message when empty` |

### Improvements Checklist

- [x] All acceptance criteria implemented and tested
- [x] Bilingual support with locale-aware methods
- [x] RTL support via public layout
- [x] Professional typography with prose-navy
- [x] Responsive design with max-width constraints
- [x] SPA-like navigation with wire:navigate
- [x] Proper pagination
- [x] Code formatted with Pint

### Security Review

**Status: PASS** - No security concerns identified.
- Public read-only feature with no sensitive data exposure
- Proper status check prevents accessing unpublished posts
- No user input processed beyond route parameters
- Route model binding handles non-existent posts safely

### Performance Considerations

**Status: PASS** - No performance issues identified.
- Simple queries with pagination (10 per page)
- No N+1 query issues
- Uses eager loading pattern via published() scope
- CSS optimized with Tailwind

### Files Modified During Review

None - no modifications required.

### Gate Status

Gate: **PASS** → docs/qa/gates/5.4-public-posts-display.yml

### Recommended Status

✓ **Ready for Done** - All acceptance criteria met, comprehensive test coverage, no issues identified. Implementation follows all project conventions and coding standards.
