From 2c068277a21537a0654ba74f262f88bf6cc681ef Mon Sep 17 00:00:00 2001 From: Naser Mansour Date: Sat, 31 Oct 2026 18:54:18 +0200 Subject: [PATCH] =?UTF-8?q?24-8:=20articles=20and=20legal=20pages=20?= =?UTF-8?q?=E2=80=94=20implemented,=20QA=20Review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- docs/tasks/24-8.md | 2 +- docs/tasks/manual-tests/24-8.md | 15 +++ docs/tasks/statuses.md | 2 +- resources/css/public.css | 22 +++- .../components/site/section-head.blade.php | 4 +- .../views/livewire/pages/booking.blade.php | 2 +- .../livewire/pages/posts/index.blade.php | 117 ++++++------------ .../views/livewire/pages/posts/show.blade.php | 31 ++--- resources/views/pages/legal.blade.php | 26 ++-- resources/views/pages/privacy.blade.php | 12 +- resources/views/pages/terms.blade.php | 12 +- .../Design/ArticlesLegalRedesignTest.php | 86 +++++++++++++ .../Design/BookingPageRedesignTest.php | 1 + 13 files changed, 203 insertions(+), 129 deletions(-) create mode 100644 docs/tasks/manual-tests/24-8.md create mode 100644 tests/Feature/Design/ArticlesLegalRedesignTest.php diff --git a/docs/tasks/24-8.md b/docs/tasks/24-8.md index 09fb4e2..f901215 100644 --- a/docs/tasks/24-8.md +++ b/docs/tasks/24-8.md @@ -1,7 +1,7 @@ --- id: 24-8 title: 24-8 Articles and Legal Pages -status: Ready for Dev +status: QA Review date: 2026-10-31 parent: 24 --- diff --git a/docs/tasks/manual-tests/24-8.md b/docs/tasks/manual-tests/24-8.md new file mode 100644 index 0000000..7ed913c --- /dev/null +++ b/docs/tasks/manual-tests/24-8.md @@ -0,0 +1,15 @@ +# Manual tests — 24-8 Articles and Legal Pages + +Automated (passed 2026-10-31, `tests/Feature/Design/ArticlesLegalRedesignTest.php`, 9 tests; Design dir 106 incl. old `PostsTest`): `/posts` section head, +one h1, debounced search input, text-only post cards; search filters, shows the count line and clear action; empty-state text; +pagination wrapper present with 12 posts; article page meta/h1/prose/back link, unpublished post is 404; `/page/terms` shows title, +sanitised content (script stripped) and last-updated line; terms/privacy views one h1; `/admin/posts` loads no public assets. +UI loop (`/posts`, `/page/terms`): no horizontal scroll at 390/768/1440. + +Ruling logged: `x-site.section-head` gained an optional `tag` prop (default `h2`); `/posts` and `/booking` pass `tag="h1"` so each page has exactly one h1 (the spec's own acceptance criterion). + +- [ ] `/posts`: type in the search box; after a short pause the list filters and "N results" appears; Clear resets. +- [ ] With more than 10 published posts: pagination pills at the bottom, current page forest green. +- [ ] Open an article: date, large display heading, readable body, Back button. +- [ ] `/page/terms` and `/page/privacy`: white card with the CMS text and "last updated" line; check Arabic and English. +- [ ] 390px on each page: no sideways scroll. diff --git a/docs/tasks/statuses.md b/docs/tasks/statuses.md index ef40e68..4b67a1e 100644 --- a/docs/tasks/statuses.md +++ b/docs/tasks/statuses.md @@ -144,6 +144,6 @@ | 24-5 | 24-5 Home Booking Card (Compact Booking and Day Strip) | QA Review | | 24-6 | 24-6 Booking Page and Booking Success | QA Review | | 24-7 | 24-7 About Page | QA Review | -| 24-8 | 24-8 Articles and Legal Pages | Ready for Dev | +| 24-8 | 24-8 Articles and Legal Pages | QA Review | | 24-9 | 24-9 Auth Pages Type and Controls | Ready for Dev | | 24-10 | 24-10 Redesign Integration Verification | Ready for Dev | diff --git a/resources/css/public.css b/resources/css/public.css index ae4c0eb..22d6ba8 100644 --- a/resources/css/public.css +++ b/resources/css/public.css @@ -62,7 +62,7 @@ html[dir=ltr] body.site .site-progress{transform-origin:left} body.site section{position:relative} body.site .sec{padding:120px 0} body.site .sec-head{display:flex;align-items:end;justify-content:space-between;gap:32px;margin-bottom:54px;flex-wrap:wrap} -body.site .sec-head h2{font-size:clamp(2rem,3.4vw,3rem);margin-top:12px} +body.site .sec-head h2,body.site .sec-head h1{font-size:clamp(2rem,3.4vw,3rem);margin-top:12px} body.site .sec-head p{max-width:30em;color:var(--muted);margin:0} @@ -338,6 +338,26 @@ body.site .row-static:hover{background:none;color:inherit;transform:none;box-sha body.site .row-static::before{display:none} @media (max-width:760px){body.site .row-static{grid-template-columns:52px 1fr}} +/* ---------- Articles and legal pages (24-8) ---------- */ +body.site .search{min-height:48px;border-radius:999px;padding:12px 20px;border:1.5px solid rgba(45,54,36,.18);background:#fff;width:min(360px,100%);font:400 1rem var(--body);color:var(--ink);transition:border-color .2s,box-shadow .2s} +body.site .search:focus{outline:none;border-color:var(--gold);box-shadow:0 0 0 4px rgba(166,137,102,.2)} +body.site .search-info{color:var(--muted);margin:0 0 24px;display:flex;gap:14px;align-items:center;flex-wrap:wrap} +body.site .search-info .btn{min-height:44px;padding:8px 18px} +body.site .posts-index .post:last-child{grid-column:auto} +body.site .pagination-wrap{margin-top:40px} +body.site .pagination-wrap nav>div:first-child{display:none} +body.site .pagination-wrap nav a,body.site .pagination-wrap nav span[aria-disabled] span,body.site .pagination-wrap nav span[aria-current] span{display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:44px;padding:0 14px;border-radius:999px;border:1.5px solid var(--line);background:#fff;color:var(--ink);text-decoration:none;font-weight:500} +body.site .pagination-wrap nav a:hover{border-color:var(--gold)} +body.site .pagination-wrap nav span[aria-current] span{background:var(--forest);border-color:var(--forest);color:var(--cream)} +body.site .pagination-wrap nav span[aria-disabled] span{opacity:.5} +body.site .site-prose :is(h2,h3,h4){font-family:var(--display)} +body.site .site-prose a{color:var(--olive)} +body.site .site-prose{font-size:1.06rem;line-height:1.85} +body.site .post-meta,body.site article>.meta{display:block;font-size:.85rem;font-weight:600;color:var(--gold-ink)} +body.site .legal-card{background:#fff;border-radius:16px;box-shadow:var(--shadow-1);padding:40px} +body.site .legal-updated{font-size:.85rem;color:var(--muted);margin:16px 0 0} +@media (max-width:760px){body.site .legal-card{padding:24px}} + /* ---------- Reveal (hidden only when JavaScript runs) ---------- */ body.site.js [data-reveal]{opacity:0;transform:translateY(28px);transition:opacity .9s var(--ease),transform .9s var(--ease)} body.site.js [data-reveal].in{opacity:1;transform:none} diff --git a/resources/views/components/site/section-head.blade.php b/resources/views/components/site/section-head.blade.php index b58e9f0..d3503ab 100644 --- a/resources/views/components/site/section-head.blade.php +++ b/resources/views/components/site/section-head.blade.php @@ -1,9 +1,9 @@ -@props(['label', 'title', 'lede' => null]) +@props(['label', 'title', 'lede' => null, 'tag' => 'h2'])
{{ $label }} -

{{ $title }}

+ <{{ $tag }} data-reveal data-d="1">{{ $title }}
@if ($lede)

{{ $lede }}

diff --git a/resources/views/livewire/pages/booking.blade.php b/resources/views/livewire/pages/booking.blade.php index ccb1c17..fe6a7c5 100644 --- a/resources/views/livewire/pages/booking.blade.php +++ b/resources/views/livewire/pages/booking.blade.php @@ -278,7 +278,7 @@ new #[Layout('components.layouts.public')] class extends Component @else
- +
diff --git a/resources/views/livewire/pages/posts/index.blade.php b/resources/views/livewire/pages/posts/index.blade.php index 62d7e14..5895bf0 100644 --- a/resources/views/livewire/pages/posts/index.blade.php +++ b/resources/views/livewire/pages/posts/index.blade.php @@ -55,91 +55,44 @@ new #[Layout('components.layouts.public')] class extends Component } }; ?> -
-
- {{ __('posts.posts') }} - - -
- - - - - +
+
+ +
+ + +
+
@if($search) - - @endif -
- - - @if($search) -

- @if($posts->total() > 0) - {{ __('posts.search_results', ['count' => $posts->total(), 'query' => $search]) }} - @else - {{ __('posts.no_results', ['query' => $search]) }} - @endif -

- @endif - - -
- @forelse($posts as $post) - - @empty -
- @if($search) - -

{{ __('posts.no_results', ['query' => $search]) }}

- - {{ __('posts.clear_search') }} - +

+ @if($posts->total() > 0) + {{ __('posts.search_results', ['count' => $posts->total(), 'query' => $search]) }} @else - -

{{ __('posts.no_posts') }}

+ {{ __('posts.no_results', ['query' => $search]) }} @endif -
- @endforelse -
+ +

+ @endif -
- {{ $posts->links() }} + @if($posts->count() > 0) +
+ @foreach($posts as $post) + + @endforeach +
+ @elseif(! $search) +

{{ __('posts.no_posts') }}

+ @endif + +
+ {{ $posts->links() }} +
-
-
+
diff --git a/resources/views/livewire/pages/posts/show.blade.php b/resources/views/livewire/pages/posts/show.blade.php index 8b05373..2681617 100644 --- a/resources/views/livewire/pages/posts/show.blade.php +++ b/resources/views/livewire/pages/posts/show.blade.php @@ -18,24 +18,17 @@ new #[Layout('components.layouts.public')] class extends Component } }; ?> -
-
-
- {{ $post->getTitle() }} +
+
+ {{ $post->published_at?->translatedFormat('l, d F Y') ?? $post->created_at->translatedFormat('l, d F Y') }} +

{{ $post->getTitle() }}

- -
+
+ {!! $post->getBody() !!} +
-
- {!! $post->getBody() !!} -
- - -
-
+ + + diff --git a/resources/views/pages/legal.blade.php b/resources/views/pages/legal.blade.php index bf9ba1e..e0c0969 100644 --- a/resources/views/pages/legal.blade.php +++ b/resources/views/pages/legal.blade.php @@ -1,15 +1,17 @@ -
-

{{ $page->getTitle() }}

-
- @if ($page->getContent()) - {!! clean($page->getContent()) !!} - @else -

{{ __('pages.content_coming_soon') }}

- @endif +
+
+

{{ $page->getTitle() }}

+ +
-

- {{ __('pages.last_updated') }}: {{ $page->updated_at->format('M d, Y') }} -

-
+ diff --git a/resources/views/pages/privacy.blade.php b/resources/views/pages/privacy.blade.php index 11a0de5..948c458 100644 --- a/resources/views/pages/privacy.blade.php +++ b/resources/views/pages/privacy.blade.php @@ -1,8 +1,10 @@ -
-

{{ __('footer.privacy') }}

-
-

{{ __('Privacy policy content will be added here.') }}

+
+
+

{{ __('footer.privacy') }}

+
-
+ diff --git a/resources/views/pages/terms.blade.php b/resources/views/pages/terms.blade.php index 0634821..3d8573f 100644 --- a/resources/views/pages/terms.blade.php +++ b/resources/views/pages/terms.blade.php @@ -1,8 +1,10 @@ -
-

{{ __('footer.terms') }}

-
-

{{ __('Terms of service content will be added here.') }}

+
+
+

{{ __('footer.terms') }}

+
-
+ diff --git a/tests/Feature/Design/ArticlesLegalRedesignTest.php b/tests/Feature/Design/ArticlesLegalRedesignTest.php new file mode 100644 index 0000000..d194fe9 --- /dev/null +++ b/tests/Feature/Design/ArticlesLegalRedesignTest.php @@ -0,0 +1,86 @@ +published()->count(3)->create(); + + $html = $this->withSession(['locale' => 'en'])->get('/posts')->assertOk()->getContent(); + + expect(substr_count($html, 'toBe(1); + expect($html) + ->toContain('class="sec-head"') + ->toContain('wire:model.live.debounce.300ms="search"') + ->toContain('class="search"') + ->toContain('posts-index'); + expect(substr_count($html, 'class="post"'))->toBe(3); + + preg_match('~
(.*?)~s', $html, $block); + expect($block[1])->not->toContain('published()->create(['title' => ['en' => 'Alimony guide', 'ar' => 'دليل النفقة']]); + Post::factory()->published()->create(['title' => ['en' => 'Contracts', 'ar' => 'العقود']]); + + app()->setLocale('en'); + Volt::test('pages.posts.index')->set('search', 'Alimony') + ->assertSee('Alimony guide')->assertDontSee('Contracts') + ->assertSee(__('posts.search_results', ['count' => 1, 'query' => 'Alimony'])) + ->assertSee(__('posts.clear_search')) + ->set('search', 'zzzz')->assertSee(__('posts.no_results', ['query' => 'zzzz'])); +}); + +test('empty index shows the no posts message', function () { + Volt::test('pages.posts.index')->assertSee(__('posts.no_posts')); +}); + +test('pagination is wrapped and styled when there are more than ten posts', function () { + Post::factory()->published()->count(12)->create(); + + expect($this->get('/posts')->assertOk()->getContent())->toContain('pagination-wrap'); +}); + +test('a published post shows meta, h1, body and a back link; unpublished is 404', function () { + $post = Post::factory()->published()->create(); + $html = $this->get(route('posts.show', $post))->assertOk()->getContent(); + + expect($html)->toContain('class="meta"')->toContain('site-prose')->toContain(route('posts.index'))->toContain(__('posts.back_to_posts')); + expect(substr_count($html, 'toBe(1); + + $draft = Post::factory()->draft()->create(); + $this->get(route('posts.show', $draft))->assertNotFound(); +}); + +test('legal pages show title, sanitised content and the last updated line', function () { + Page::factory()->terms()->create(['content_en' => '

Hello

', 'content_ar' => '

مرحبا

']); + + $html = $this->withSession(['locale' => 'en'])->get('/page/terms')->assertOk()->getContent(); + + expect($html)->toContain('Terms of Service')->toContain('Hello')->not->toContain('