سننشئ في هذا المقال الأخير من هذه السلسلة جزء الواجهة الأمامية من التطبيق، ولكن لنضع خطة أولًا، حيث سيكون لدينا الصفحة الرئيسية لتطبيق المدونة التي تعرض قائمة بجميع المنشورات الحديثة، وصفحة فئة Category المنشور التي تعرض قائمة المنشورات ضمن فئة معينة، وصفحة الوسم Tag التي تعرض قائمة المنشورات التي لها وسم مُحدَّد، وصفحة المنشور التي تعرض محتوى منشور معين، وصفحة بحث تعرض قائمة المنشورات بناءً على استعلام بحث معين.
كما ستحتوي جميع هذه الصفحات على شريط جانبي مع مربع بحث وقائمة الفئات والوسوم، وستحتوي صفحة المنشور أيضًا على قسم المنشورات ذات الصلة في نهايتها.
تحدّثنا عن قاعدة البيانات والنماذج Models في المقال السابق، وسنبدأ الآن بالوِجهات Routes.
إنشاء الوِجهات
ننشئ الوِجهات في لارافيل ضمن الملف routes/web.php
كما يلي:
. . . // الصفحة الرئيسية Route::get('/', [PostController::class, 'home'])->name('home'); // قائمة المنشورات ضمن هذه الفئة Route::get('/category/{category}', [CategoryController::class, 'category'])->name('category'); // قائمة المنشورات التي لها هذا الوسم Route::get('/tag/{tag}', [TagController::class, 'tag'])->name('tag'); // عرض منشور واحد Route::get('/post/{post}', [PostController::class, 'post'])->name('post'); // قائمة المنشورات بناء على استعلام البحث Route::post('/search', [PostController::class, 'search'])->name('search'); . . .
الصفحة الرئيسية
يكون لكل من هذه الوِجهات تابع تحكم مقابل، حيث سنبدأ أولًا بالمتحكم home()
إذًا لنضع ما يلي في الملف app/Http/Controllers/PostController.php
:
<?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Post; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Builder; class PostController extends Controller { /** * عرض الصفحة الرئيسية */ public function home(): View { $posts = Post::where('is_published', true)->paginate(env('PAGINATE_NUM')); $categories = Category::all(); $tags = Tag::all(); return view('home', [ 'posts' => $posts, 'categories' => $categories, 'tags' => $tags ]); } . . . }
هناك شيئان يجب عليك ملاحظتهما في السطر 20 من الشيفرة البرمجية السابقة وهما:
-
أولًا، تتأكد التعليمة
where('is_published', true)
من استرداد المقالات المنشورة فقط. -
ثانيًا، يُعَد التابع
paginate()
أحد توابع لارافيل Laravel المُضمَّنة، مما يسمح لك بإنشاء ترقيم للصفحات Pagination في تطبيقك بسهولة. يأخذ التابعpaginate()
عددًا صحيحًا كدخل، فمثلًا يمثّلpaginate(10)
عرض 10 عناصر في كل صفحة، وسيُستخدَم متغير الدخل في العديد من الصفحات، لذا يمكنك إنشاء متغير بيئة في ملف.env
كما يلي، ثم يمكنك استرداده في أيّ مكان باستخدام التابعenv()
:
. . . PAGINATE_NUM=12
لننشئ الآن عرض View الصفحة الرئيسية المقابل. اطّلع على بنية القوالب التي أنشأئاها، وإليك بنية العروض التالية:
resources/views ├── category.blade.php ├── home.blade.php ├── layout.blade.php ├── post.blade.php ├── search.blade.php ├── tag.blade.php ├── vendor │ ├── list.blade.php │ └── sidebar.blade.php └── welcome.blade.php
سنبدأ أولًا بالعرض layout.blade.php
الذي يمثل تخطيط الصفحة كما يلي:
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> @vite(['resources/css/app.css', 'resources/js/app.js']) @yield('title') </head> <body class="container mx-auto font-serif"> <nav class="flex flex-row justify-between h-16 items-center border-b-2"> <div class="px-5 text-2xl"> <a href="/"> My Blog </a> </div> <div class="hidden lg:flex content-between space-x-10 px-10 text-lg"> <a href="https://github.com/ericnanhu" class="hover:underline hover:underline-offset-1" >GitHub</a > <a href="{{ route('dashboard') }}" class="hover:underline hover:underline-offset-1" >Dashboard</a > <a href="#" class="hover:underline hover:underline-offset-1">Link</a> </div> </nav> @yield('content') <footer class="bg-gray-700 text-white"> <div class="flex justify-center items-center sm:justify-between flex-wrap lg:max-w-screen-2xl mx-auto px-4 sm:px-8 py-10" > <p class="font-serif text-center mb-3 sm:mb-0"> Copyright © <a href="https://www.ericsdevblog.com/" class="hover:underline" >Eric Hu</a > </p> <div class="flex justify-center space-x-4">. . .</div> </div> </footer> </body> </html>
يستخدم لارافيل الأداة Vite افتراضيًا لتجميع أصول المشروع كما في السطر 8، وثبّتنا الحزمة Breeze من لارافيل في المقال السابق، وتستخدم هذه الحزمة إطار عمل Tailwind CSS، حيث سيستورد السطر 8 من الشيفرة البرمجية السابقة تلقائيًا الملفين app.css
و app.js
المقابلَين. يمكنك استخدام إطار عمل مختلف أيضًا، ولكن يجب أن تعود إلى توثيقه للحصول على تفاصيل حول كيفية استخدامه مع لارافيل أو Vite.
لننشئ الآن عرض الصفحة الرئيسية resources/views/home.blade.php
كما يلي:
@extends('layout') @section('title') <title>Home</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div> @include('vendor.sidebar') </div> @endsection
ويكون عرض قائمة المنشورات resources/views/vendor/list.blade.php
كما يلي:
<!-- قائمة المنشورات --> <div class="grid grid-cols-3 gap-4"> @foreach ($posts as $post) <!-- المنشور --> <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md"> <a href="{{ route('post', ['post' => $post->id]) }}" ><img class="rounded-t-md object-cover h-60 w-full" src="{{ Storage::url($post->cover) }}" alt="..." /></a> <div class="m-4 grid gap-2"> <div class="text-sm text-gray-500"> {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }} </div> <h2 class="text-lg font-bold">{{ $post->title }}</h2> <p class="text-base"> {{ Str::limit(strip_tags($post->content), 150, '...') }} </p> <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{{ route('post', ['post' => $post->id]) }}" >Read more →</a > </div> </div> @endforeach </div> {{ $posts->links() }}
يولّد السطر 9 Storage::url($post->cover)
عنوان URL الذي يؤشّر إلى صورة الغلاف. لا تُعَد الطريقة التي يخزن بها لارافيل العلامات الزمنية Timestamps سهلة الاستخدام، لذلك استخدمنا حزمة Carbon في السطر 14 لإعادة تنسيق العلامات الزمنية.
استخدمنا أيضًا الدالة strip_tags()
في السطر 18 لإزالة وسوم HTML، ثم تحدِّد الدالة limit()
الحد الأقصى لطول السلسلة، وستوضَع نقاط ...
مكان الجزء الزائد منها. استخدمنا التابع paginate()
لإنشاء مرقّم الصفحات في المتحكم، ولكن يمكننا عرض مرقّم الصفحات في العرض باستخدام التعليمة {{ $posts->links() }}
الموجودة في السطر 30.
لننشئ الآن عرض الشريط الجانبي resources/views/vendor/sidebar.blade.php
كما يلي:
<div class="col-span-1"> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Search</div> <div class="p-4"> <form action="{{ route('search') }}" method="POST" class="grid grid-cols-4 gap-2" > {{ csrf_field() }} <input type="text" name="q" id="search" class="border rounded-md w-full focus:ring p-2 col-span-3" placeholder="Search something..." /> <button type="submit" class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase font-semibold font-sans w-full focus:ring col-span-1" > Search </button> </form> </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Categories</div> <div class="p-4"> <ul class="list-none list-inside"> @foreach ($categories as $category) <li> <a href="{{ route('category', ['category' => $category->id]) }}" class="text-blue-500 hover:underline" >{{ $category->name }}</a > </li> @endforeach </ul> </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Tags</div> <div class="p-4"> @foreach ($tags as $tag) <span class="mr-2" ><a href="{{ route('tag', ['tag' => $tag->id]) }}" class="text-blue-500 hover:underline" >{{ $tag->name }}</a ></span > @endforeach </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">More Card</div> <div class="p-4"> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit. Placeat, voluptatem ab tempore recusandae sequi libero sapiente autem! Sit hic reprehenderit pariatur autem totam, voluptates non officia accusantium rerum unde provident! </p> </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">...</div> <div class="p-4"> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit. Placeat, voluptatem ab tempore recusandae sequi libero sapiente autem! Sit hic reprehenderit pariatur autem totam, voluptates non officia accusantium rerum unde provident! </p> </div> </div> </div>
صفحة الفئة Category
ضع ما يلي في ملف المتحكم app/Http/Controllers/CategoryController.php
:
<?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class CategoryController extends Controller { /** * عرض قائمة المنشورات التي تنتمي إلى الفئة */ public function category(string $id): View { $category = Category::find($id); $posts = $category->posts()->where('is_published', true)->paginate(env('PAGINATE_NUM')); $categories = Category::all(); $tags = Tag::all(); return view('category', [ 'category' => $category, 'posts' => $posts, 'categories' => $categories, 'tags' => $tags ]); } . . . }
ويكون عرض الفئة المقابل resources/views/category.blade.php
كما يلي:
@extends('layout') @section('title') <title>Category - {{ $category->name }}</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div> @include('vendor.sidebar') </div> @endsection
صفحة الوسم Tag
ضع ما يلي في ملف المتحكم app/Http/Controllers/TagController.php
:
<?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class TagController extends Controller { /** * عرض قائمة المنشورات التي تنتمي إلى الوسم */ public function tag(string $id): View { $tag = Tag::find($id); $posts = $tag->posts()->where('is_published', true)->paginate(env('PAGINATE_NUM')); $categories = Category::all(); $tags = Tag::all(); return view('tag', [ 'tag' => $tag, 'posts' => $posts, 'categories' => $categories, 'tags' => $tags ]); } . . . }
ويكون عرض الوسم المقابل resources/views/tag.blade.php
كما يلي:
@extends('layout') @section('title') <title>Tag - {{ $tag->name }}</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div> @include('vendor.sidebar') </div> @endsection
صفحة المنشور
ضع ما يلي في ملف المتحكم app/Http/Controllers/PostController.php
:
<?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Post; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Builder; class PostController extends Controller { . . . /** * عرض المنشور المطلوب */ public function post(string $id): View { $post = Post::find($id); $categories = Category::all(); $tags = Tag::all(); $related_posts = Post::where('is_published', true)->whereHas('tags', function (Builder $query) use ($post) { return $query->whereIn('name', $post->tags->pluck('name')); })->where('id', '!=', $post->id)->take(3)->get(); return view('post', [ 'post' => $post, 'categories' => $categories, 'tags' => $tags, 'related_posts' => $related_posts ]); } . . . }
لاحظ في السطور من 27 إلى 29 أن هذه هي الطريقة التي يمكننا من خلالها استرداد المنشورات ذات الصلة، حيث نريد الحصول على المنشورات التي يكون لها الوسوم نفسها. قد تبدو سلسلة هذه التوابع مخيفة نوعًا ما، لكن لا تقلق بشأن ذلك، حيث سنوضّحها واحدًا تلو الآخر كما يلي:
-
يعيد التابع الأول
where('is_published', true)
جميع المنشورات التي نشرناها. -
تتعقد الأمور بعض الشيء عند التابع
whereHas()
، حيث إذا أردنا فهم هذا التابع، فيجب أن نتحدث أولًا عن التابعhas()
، وهو تابع Laravel Eloquent الذي يسمح لنا بالتحقق من وجود علاقة كما في المثال التالي:
$posts = Post::has('comments', '>', 3)->get();
ستسترد الشيفرة البرمجية السابقة جميع المنشورات التي تحتوي على أكثر من 3 تعليقات، ولاحظ أنه لا يمكنك استخدام where()
لإنجاز ذلك، لأن comments
ليس عمودًا في الجدول posts
، بل هو جدول آخر له علاقة بالجدول posts
. يعمل التابع whereHas()
مثل التابع has()
تمامًا، ولكنه يوفر مزيدًا من القوة، فالمعامل الثاني هو دالة تسمح بفحص محتوى جدول آخر، وهو الجدول tags
في حالتنا، ويمكننا الوصول إلى الجدول tags
من خلال المتغير $q
.
-
يأخذ التابع
whereIn()
في السطر 28 معاملَين هما: الأول هو العمود المحدَّد، والثاني هو مصفوفة من القيم المقبولة. يعيد هذا التابع السجلات ذات القيم المقبولة فقط ويستبعد الباقي. -
يجب أن تكون بقية التوابع سهلة الفهم، حيث يستبعد التابع
where('id', '!=', $post->id)
المنشور الحالي، ويأخذ التابعtake(3)
السجلات الثلاثة الأولى.
يكون عرض المنشور المقابل resources/views/post.blade.php
كما يلي:
@extends('layout') @section('title') <title>Page Title</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3"> <img class="rounded-md object-cover h-96 w-full" src="{{ Storage::url($post->cover) }}" alt="..." /> <h1 class="mt-5 mb-2 text-center text-2xl font-bold">{{ $post->title }}</h1> <p class="mb-5 text-center text-sm text-slate-500 italic">By {{ $post->user->name }} | {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }}</p> <div>{!! $post->content !!}</div> <div class="my-5"> @foreach ($post->tags as $tag) <a href="{{ route('tag', ['tag' => $tag->id]) }}" class="text-blue-500 hover:underline" mr-3">#{{ $tag->name }}</a> @endforeach </div> <hr> <!-- المنشورات ذات الصلة --> <div class="grid grid-cols-3 gap-4 my-5"> @foreach ($related_posts as $post) <!-- المنشور --> <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md"> <a href="{{ route('post', ['post' => $post->id]) }}"><img class="rounded-t-md object-cover h-60 w-full" src="{{ Storage::url($post->cover) }}" alt="..." /></a> <div class="m-4 grid gap-2"> <div class="text-sm text-gray-500"> {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }} </div> <h2 class="text-lg font-bold">{{ $post->title }}</h2> <p class="text-base"> {{ Str::limit(strip_tags($post->content), 150, '...') }} </p> <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{{ route('post', ['post' => $post->id]) }}">Read more →</a> </div> </div> @endforeach </div> </div> @include('vendor.sidebar') </div> @endsection
يُعَد {!! $post->content !!}
في السطر 15 هو الوضع الآمن في لارافيل، حيث يخبر لارافيل بعرض وسوم HTML بدلًا من عرضها كنص عادي.
صفحة البحث
ضع ما يلي في ملف المتحكم app/Http/Controllers/PostController.php
:
<?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Post; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Builder; class PostController extends Controller { . . . /** * عرض نتيجة البحث */ public function search(Request $request): View { $key = $request->input('q'); $posts = Post::where('title', 'like', "%{$key}%")->orderBy('id', 'desc')->paginate(env('PAGINATE_NUM')); $categories = Category::all(); $tags = Tag::all(); return view('search', [ 'key' => $key, 'posts' => $posts, 'categories' => $categories, 'tags' => $tags, ]); } . . . }
يكون عرض البحث المقابل resources/views/search.blade.php
كما يلي:
@extends('layout') @section('title') <title>Search - {{ $key }}</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div> @include('vendor.sidebar') </div> @endsection
ترجمة -وبتصرُّف- للمقال Laravel for Beginners #5 - Create the Frontend لصاحبه Eric Hu.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.