complete story 14.5
This commit is contained in:
parent
88961e11b4
commit
49aeceb25c
|
|
@ -220,19 +220,19 @@ $post->created_at->translatedFormat('j F Y')
|
||||||
|
|
||||||
## Dev Checklist
|
## Dev Checklist
|
||||||
|
|
||||||
- [ ] Determine data passing method (Volt component or route)
|
- [x] Determine data passing method (Volt component or route)
|
||||||
- [ ] Create posts section HTML structure
|
- [x] Create posts section HTML structure
|
||||||
- [ ] Add section heading with translations
|
- [x] Add section heading with translations
|
||||||
- [ ] Implement post card design
|
- [x] Implement post card design
|
||||||
- [ ] Query and display 3 latest published posts
|
- [x] Query and display 3 latest published posts
|
||||||
- [ ] Add date formatting with translation support
|
- [x] Add date formatting with translation support
|
||||||
- [ ] Implement excerpt truncation
|
- [x] Implement excerpt truncation
|
||||||
- [ ] Add "Read More" links to individual posts
|
- [x] Add "Read More" links to individual posts
|
||||||
- [ ] Add "View All" link to posts index
|
- [x] Add "View All" link to posts index
|
||||||
- [ ] Handle empty state (hide section or show message)
|
- [x] Handle empty state (hide section or show message)
|
||||||
- [ ] Implement responsive grid layout
|
- [x] Implement responsive grid layout
|
||||||
- [ ] Test RTL layout
|
- [x] Test RTL layout
|
||||||
- [ ] Verify links work correctly
|
- [x] Verify links work correctly
|
||||||
|
|
||||||
## Estimation
|
## Estimation
|
||||||
|
|
||||||
|
|
@ -244,3 +244,45 @@ $post->created_at->translatedFormat('j F Y')
|
||||||
- Previous stories for page structure
|
- Previous stories for page structure
|
||||||
- Existing Post model
|
- Existing Post model
|
||||||
- Posts routes (`posts.index`, `posts.show`)
|
- Posts routes (`posts.index`, `posts.show`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dev Agent Record
|
||||||
|
|
||||||
|
### Status
|
||||||
|
Ready for Review
|
||||||
|
|
||||||
|
### Agent Model Used
|
||||||
|
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||||
|
|
||||||
|
### File List
|
||||||
|
| File | Action |
|
||||||
|
|------|--------|
|
||||||
|
| `resources/views/livewire/pages/home.blade.php` | Created (new Volt component) |
|
||||||
|
| `resources/views/pages/home.blade.php` | Deleted (replaced by Volt component) |
|
||||||
|
| `routes/web.php` | Modified (changed to Volt::route) |
|
||||||
|
| `lang/en/home.php` | Modified (added posts translations) |
|
||||||
|
| `lang/ar/home.php` | Modified (added posts translations) |
|
||||||
|
| `tests/Feature/Public/HomePageTest.php` | Modified (added 17 new tests) |
|
||||||
|
|
||||||
|
### Change Log
|
||||||
|
- Converted home page from plain Blade view to Volt component with `with()` method for data fetching
|
||||||
|
- Added latest posts section that displays 3 most recent published posts
|
||||||
|
- Implemented responsive grid layout (1 col mobile, 2 col tablet, 3 col desktop)
|
||||||
|
- Added post cards with title, date, excerpt, and "Read More" links
|
||||||
|
- Added "View All Articles" button linking to posts index
|
||||||
|
- Section hidden when no published posts exist (empty state)
|
||||||
|
- Used Post model's `getTitle()` and `getExcerpt()` methods for bilingual support
|
||||||
|
- Uses `published_at` for ordering and display, with fallback to `created_at`
|
||||||
|
- Added wire:navigate for SPA-like navigation
|
||||||
|
- Added hover effects on cards and title links
|
||||||
|
|
||||||
|
### Debug Log References
|
||||||
|
None - implementation completed without issues
|
||||||
|
|
||||||
|
### Completion Notes
|
||||||
|
- All acceptance criteria met
|
||||||
|
- 17 new tests added covering posts section functionality
|
||||||
|
- All 91 home page and posts tests pass
|
||||||
|
- RTL support inherited from existing layout structure
|
||||||
|
- Uses existing Post model scope `published()` for filtering
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,13 @@ return [
|
||||||
// Values Section
|
// Values Section
|
||||||
'values_title' => 'قيمنا',
|
'values_title' => 'قيمنا',
|
||||||
'values_subtitle' => 'تبدأ من الميدان وتعود إلى الناس',
|
'values_subtitle' => 'تبدأ من الميدان وتعود إلى الناس',
|
||||||
|
|
||||||
|
// Latest Posts Section
|
||||||
|
'posts_title' => 'أحدث المقالات',
|
||||||
|
'read_more' => 'اقرأ المزيد',
|
||||||
|
'view_all_posts' => 'عرض جميع المقالات',
|
||||||
|
'posts_empty' => 'المقالات قريباً',
|
||||||
|
|
||||||
'values' => [
|
'values' => [
|
||||||
'integrity' => [
|
'integrity' => [
|
||||||
'icon' => 'shield-check',
|
'icon' => 'shield-check',
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,13 @@ return [
|
||||||
// Values Section
|
// Values Section
|
||||||
'values_title' => 'Our Values',
|
'values_title' => 'Our Values',
|
||||||
'values_subtitle' => 'These start in the field and return to the people',
|
'values_subtitle' => 'These start in the field and return to the people',
|
||||||
|
|
||||||
|
// Latest Posts Section
|
||||||
|
'posts_title' => 'Latest Articles',
|
||||||
|
'read_more' => 'Read More',
|
||||||
|
'view_all_posts' => 'View All Articles',
|
||||||
|
'posts_empty' => 'Articles coming soon',
|
||||||
|
|
||||||
'values' => [
|
'values' => [
|
||||||
'integrity' => [
|
'integrity' => [
|
||||||
'icon' => 'shield-check',
|
'icon' => 'shield-check',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,23 @@
|
||||||
<x-layouts.public>
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Post;
|
||||||
|
use Livewire\Attributes\Layout;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
|
||||||
|
new #[Layout('components.layouts.public')] class extends Component
|
||||||
|
{
|
||||||
|
public function with(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'latestPosts' => Post::published()
|
||||||
|
->latest('published_at')
|
||||||
|
->take(3)
|
||||||
|
->get(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<div>
|
||||||
{{-- Hero Section --}}
|
{{-- Hero Section --}}
|
||||||
<section class="bg-background py-8 sm:py-12 lg:py-16">
|
<section class="bg-background py-8 sm:py-12 lg:py-16">
|
||||||
<div class="container mx-auto px-4 text-center">
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
|
@ -113,4 +132,45 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</x-layouts.public>
|
|
||||||
|
{{-- Latest Posts Section --}}
|
||||||
|
@if($latestPosts->count() > 0)
|
||||||
|
<section id="posts" class="py-16 lg:py-20 bg-background">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="text-center mb-12">
|
||||||
|
<h2 class="text-2xl lg:text-3xl font-semibold text-text mb-4">
|
||||||
|
{{ __('home.posts_title') }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||||
|
@foreach($latestPosts as $post)
|
||||||
|
<article class="bg-card p-6 rounded-lg shadow-card hover:shadow-card-hover transition-shadow">
|
||||||
|
<time class="text-xs text-body/70 mb-2 block">
|
||||||
|
{{ $post->published_at?->translatedFormat('j F Y') ?? $post->created_at->translatedFormat('j F Y') }}
|
||||||
|
</time>
|
||||||
|
<h3 class="text-lg font-bold text-text mb-3">
|
||||||
|
<a href="{{ route('posts.show', $post) }}" class="hover:text-cta transition-colors" wire:navigate>
|
||||||
|
{{ $post->getTitle() }}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<p class="text-body text-sm mb-4 line-clamp-3">
|
||||||
|
{{ $post->getExcerpt() }}
|
||||||
|
</p>
|
||||||
|
<a href="{{ route('posts.show', $post) }}" class="text-cta font-medium text-sm hover:text-cta-hover" wire:navigate>
|
||||||
|
{{ __('home.read_more') }} →
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="{{ route('posts.index') }}" class="btn-secondary inline-flex items-center gap-2" wire:navigate>
|
||||||
|
{{ __('home.view_all_posts') }}
|
||||||
|
<span aria-hidden="true">→</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
@ -7,9 +7,7 @@ use Illuminate\Support\Facades\Route;
|
||||||
use Laravel\Fortify\Features;
|
use Laravel\Fortify\Features;
|
||||||
use Livewire\Volt\Volt;
|
use Livewire\Volt\Volt;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Volt::route('/', 'pages.home')->name('home');
|
||||||
return view('pages.home');
|
|
||||||
})->name('home');
|
|
||||||
|
|
||||||
Volt::route('/booking', 'pages.booking')->name('booking');
|
Volt::route('/booking', 'pages.booking')->name('booking');
|
||||||
Volt::route('/booking/success', 'pages.booking-success')->name('booking.success');
|
Volt::route('/booking/success', 'pages.booking-success')->name('booking.success');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Post;
|
||||||
|
|
||||||
test('home page is accessible', function () {
|
test('home page is accessible', function () {
|
||||||
$this->get('/')
|
$this->get('/')
|
||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
@ -369,3 +371,188 @@ test('home page displays social innovation value in Arabic', function () {
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('الابتكار الاجتماعي');
|
->assertSee('الابتكار الاجتماعي');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Latest Posts Section Tests
|
||||||
|
|
||||||
|
test('home page does not display posts section when no published posts exist', function () {
|
||||||
|
// No posts created
|
||||||
|
$this->get('/')
|
||||||
|
->assertOk()
|
||||||
|
->assertDontSee('id="posts"', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('home page displays posts section when published posts exist', function () {
|
||||||
|
Post::factory()->published()->count(3)->create();
|
||||||
|
|
||||||
|
$this->get('/')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('id="posts"', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('home page displays posts section title in English', function () {
|
||||||
|
Post::factory()->published()->create();
|
||||||
|
|
||||||
|
$this->withSession(['locale' => 'en'])
|
||||||
|
->get('/')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Latest Articles');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('home page displays posts section title in Arabic', function () {
|
||||||
|
Post::factory()->published()->create();
|
||||||
|
|
||||||
|
$this->withSession(['locale' => 'ar'])
|
||||||
|
->get('/')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('أحدث المقالات');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('home page displays maximum 3 latest posts', function () {
|
||||||
|
// Create 5 published posts
|
||||||
|
Post::factory()->published()->count(5)->create();
|
||||||
|
|
||||||
|
$response = $this->get('/');
|
||||||
|
$response->assertOk();
|
||||||
|
|
||||||
|
// The section should exist
|
||||||
|
$response->assertSee('id="posts"', false);
|
||||||
|
|
||||||
|
// Count the number of post cards (articles with specific class)
|
||||||
|
$content = $response->getContent();
|
||||||
|
$postCardCount = substr_count($content, 'class="bg-card p-6 rounded-lg shadow-card hover:shadow-card-hover transition-shadow"');
|
||||||
|
expect($postCardCount)->toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('home page displays post titles', function () {
|
||||||
|
$post = Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'Test English Title', 'ar' => 'عنوان اختبار'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withSession(['locale' => 'en'])
|
||||||
|
->get('/')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Test English Title');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('home page displays post titles in Arabic', function () {
|
||||||
|
$post = Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'Test English Title', 'ar' => 'عنوان اختبار عربي'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withSession(['locale' => 'ar'])
|
||||||
|
->get('/')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('عنوان اختبار عربي');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('home page displays post excerpt', function () {
|
||||||
|
$post = Post::factory()->published()->create([
|
||||||
|
'body' => ['en' => 'This is a test body content for the post that should be truncated.', 'ar' => 'هذا محتوى اختبار'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withSession(['locale' => 'en'])
|
||||||
|
->get('/')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('This is a test body content');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('home page displays read more link in English', function () {
|
||||||
|
Post::factory()->published()->create();
|
||||||
|
|
||||||
|
$this->withSession(['locale' => 'en'])
|
||||||
|
->get('/')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Read More');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('home page displays read more link in Arabic', function () {
|
||||||
|
Post::factory()->published()->create();
|
||||||
|
|
||||||
|
$this->withSession(['locale' => 'ar'])
|
||||||
|
->get('/')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('اقرأ المزيد');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('home page displays view all posts link in English', function () {
|
||||||
|
Post::factory()->published()->create();
|
||||||
|
|
||||||
|
$this->withSession(['locale' => 'en'])
|
||||||
|
->get('/')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('View All Articles');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('home page displays view all posts link in Arabic', function () {
|
||||||
|
Post::factory()->published()->create();
|
||||||
|
|
||||||
|
$this->withSession(['locale' => 'ar'])
|
||||||
|
->get('/')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('عرض جميع المقالات');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('home page view all posts links to posts index', function () {
|
||||||
|
Post::factory()->published()->create();
|
||||||
|
|
||||||
|
$this->get('/')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('href="'.route('posts.index').'"', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('home page post title links to post show page', function () {
|
||||||
|
$post = Post::factory()->published()->create();
|
||||||
|
|
||||||
|
$this->get('/')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('href="'.route('posts.show', $post).'"', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('home page only shows published posts not drafts', function () {
|
||||||
|
$publishedPost = Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'Published Post Title', 'ar' => 'منشور'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$draftPost = Post::factory()->draft()->create([
|
||||||
|
'title' => ['en' => 'Draft Post Title', 'ar' => 'مسودة'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withSession(['locale' => 'en'])
|
||||||
|
->get('/')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Published Post Title')
|
||||||
|
->assertDontSee('Draft Post Title');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('home page displays posts in order of latest published first', function () {
|
||||||
|
$oldPost = Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'Old Post', 'ar' => 'قديم'],
|
||||||
|
'published_at' => now()->subDays(10),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$newPost = Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'New Post', 'ar' => 'جديد'],
|
||||||
|
'published_at' => now()->subDay(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withSession(['locale' => 'en'])->get('/');
|
||||||
|
$response->assertOk();
|
||||||
|
|
||||||
|
$content = $response->getContent();
|
||||||
|
$newPostPosition = strpos($content, 'New Post');
|
||||||
|
$oldPostPosition = strpos($content, 'Old Post');
|
||||||
|
|
||||||
|
// New post should appear before old post
|
||||||
|
expect($newPostPosition)->toBeLessThan($oldPostPosition);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('home page displays post publication date', function () {
|
||||||
|
$post = Post::factory()->published()->create([
|
||||||
|
'published_at' => now()->setDate(2026, 1, 15),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withSession(['locale' => 'en'])
|
||||||
|
->get('/')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('15 January 2026');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue