اذهب إلى المحتوى

لارافيل للمبتدئين-الجزء الخامس: إنشاء الواجهة الأمامية لمدونة بسيطة


Ola Abbas

سننشئ في هذا المقال الأخير من هذه السلسلة جزء الواجهة الأمامية من التطبيق، ولكن لنضع خطة أولًا، حيث سيكون لدينا الصفحة الرئيسية لتطبيق المدونة التي تعرض قائمة بجميع المنشورات الحديثة، وصفحة فئة 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.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...