Développer un blog avec Laravel : Partie 3/5

La page d'accueil

Développer un blog avec Laravel : Partie 3/5

Nous avons dans la partie 2 mis en place les migrations ainsi que la population. Maintenant que nous avons du contenu nous allons pouvoir passer au contenant. Nous allons commencer par installer un package pour la gestion des medias. Ensuite j'ai suivi d'utiliser le thème Spurgeon en l’adaptant à Laravel pour créer le design de nos vues.

Retrouvez les articles precedents:

  1. Stack et installation des outils
  2. Base des données et population

Les medias

Pour la gestion des medias on va utiliser le package Laravel File Manager :

composer require unisharp/laravel-filemanager

Ensuite on publie la configuration est les assets :

php artisan vendor:publish --tag=lfm_config

php artisan vendor:publish --tag=lfm_public

On crée un lien symbolique :

php artisan storage:link

Apres cette commande, le dossier public\storage est relié au dossier storage\app\public.

Pour terminer on ajoute les routes dans routes/web.php :

use UniSharp\LaravelFilemanager\Lfm;

Route::group(['prefix' => 'laravel-filemanager', 'middleware' => 'auth'], function () {
    Lfm::routes();
});

Remarquer que nous avons ici utilisé le middleware auth sur cette route, nous avons donc besoin d’être connecter pour accéder a cette route. Mettons en place l'authentification.

Authentification

Nous allons utiliser le package breeze, on l'install:

composer require laravel/breeze --dev

Puis:

php artisan breeze:install
npm install

il ne reste plus qu’à générer :

npm run dev

a ce stage si vous accedez a votre application, dans mon cas http://localhost:8000/login vous devriez tomber sur une page:

Capture-4.png

Vous pouvez maintenant accéder à la démo de lfm en vous connectant. Comme on a créé des utilisateurs dans le précédent article vous pouvez aller trouver un email dans la base de données, tous les mots de passe sont $2y$10$92IX. La démo est accessible à l’adresse http://localhost:8000/laravel-filemanager/demo.

Par défaut vous avez ces dossiers de créés :

Capture-28.png

Si vous vous connectez avec l’utilisateur qui a l’id 2 et que vous chargez une image vous aller voir la création de ces dossiers :

Capture-29.png

Le principe : les images sont toutes dans le dossier photos. Chaque utilisateur a son dossier dont le nom est son identifiant. On a deux versions de l’image : une réduite dans thumbs et une normale. On a les réglages pour l’image réduite dans la configuration (config/lfm.php), on va conserver ces réglages.

Pour notre page d’accueil il nous faut 9 image que vous pouvez aller télécharger sur Unsplash par exemple . Il vous suffit de les copier dans le dossier photos et le renommer de img01.ext a img09.ext (.ext étant l’extension de l'image).

Capture-30.png

Vous aurez ainsi les images pour les 9 articles qu’on a déjà créés.

Spurgeon

Vous avez normalement téléchargé le thème Spurgeon et vous devez disposer de tout ça :

spurgeon-folder.png

On ne va évidemment pas tout prendre. On va copier le dossier css et js dans le dossier public de notre app.

Pour terminer on va créer un dossier front dans le dossier views, et ensuite récupérer le fichier index.html du thème Spurgeon, le copier dans le dossier views/front et le renommer layout.blade.php.

Capture-36.png

Nous allons évidemment lui apporter des modifications par la suite…

Le contrôleur et repository

Nous allons créer un PostController pour gérer les articles:

php artisan make:controller Font/PostController --model=Post

Pour bien séparer nous l'avons directement placer dans un dossier Front qui est creer automatiquement via la commande artisan :

Capture-38.png

Comme on aura pas mal de manipulations au niveau de la base de données on va aussi prévoir un repository :

Capture-39.png

On code le repository

on va avoir besoin :

  • des articles actifs classés par date paginés
  • des informations des derniers articles pour le diaporama(heros dans e langage de notre theme)

Les articles paginés

Commençons par les articles classés et paginés :

<?php

namespace App\Repositories;

use App\Models\Post;

class PostRepository
{
    protected function queryActive()
    {
        return Post::select(
            'id',
            'slug',
            'image',
            'title',
            'overview',
            'user_id'
        )->with('user:id,name')
        ->whereActive(true);
    }

    protected function queryActiveOrderByDate()
    {
        return $this->queryActive()->latest();
    }

    public function getActiveOrderByDate($nbrPages)
    {
        return $this->queryActiveOrderByDate()->paginate($nbrPages);
    }
}

J’ai séparé en 3 fonctions parce qu’on aura besoin de ce découpage pour les autres fonctionnalités. La fonction d’entrée est getActiveOrderByDate. On lui transmet le nombre de pages et elle renvoie les articles concernés. On utilise un SELECT pour éviter de charger le contenu des articles qui peut être volumineux et qui est inutile pour la page d’accueil. On charge aussi pour chaque article le nom de son auteur pour l’afficher.

Les heros(diapoa)

Le diaporama est constitué des 3 derniers articles créés ou modifiés, on ajoute donc cette fonction :

public function getDiapo()
{
    return $this->queryActive()->with('categories')->latest('updated_at')->take(3)->get();
}

On charge aussi les catégories parce qu’on doit les afficher.

On code le contrôleur et la route

Le contrôleur va utiliser le repository :

<?php

namespace App\Http\Controllers\Font;

use App\Models\Post;
use App\Http\Controllers\Controller;
use App\Repositories\PostRepository;

class PostController extends Controller
{
    protected $postRepository;
    protected $nbrPages;

    public function __construct(PostRepository $postRepository)
    {
        $this->postRepository = $postRepository;
        $this->nbrPages = 5;
    }

    public function index()
    {
        $posts = $this->postRepository->getActiveOrderByDate($this->nbrPages);
        $heros = $this->postRepository->getDiapo();

        return view('front.index', compact('posts', 'heros'));
    }
}

Pour la route on supprime celle de Breeze, Et on crée la nôtre pour pointer sur le contrôleur :

use App\Http\Controllers\Front\PostController as FrontPostController;

Route::name('home')->get('/', [FrontPostController::class, 'index']);

Le layout

On va traiter le fichier layout.blade.php par zones.

Le head

On a ce code :

<!DOCTYPE html>
<html lang="en" class="no-js" >
<head>

    <!--- basic page needs
    ================================================== -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Spurgeon</title>

    <script>
        document.documentElement.classList.remove('no-js');
        document.documentElement.classList.add('js');
    </script>

    <!-- CSS
    ================================================== -->
    <link rel="stylesheet" href="css/vendor.css">
    <link rel="stylesheet" href="css/styles.css">

    <!-- favicons
    ================================================== -->
    <link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
    <link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
    <link rel="manifest" href="site.webmanifest">

</head>

On va renseigner la langue selon la locale :

<html class="no-js" lang="{{ str_replace('_', '-', app()->getLocale()) }}">

On va chercher le titre dans la configuration :

<title>{{ config('app.name') }}</title>

Pour le CSS on utilise un helper pour générer l’url complète et on prévoit un emplacement pour que les vues qui vont utiliser ce layout puissent ajouter du style :

<link rel="stylesheet" href="{{ asset('css/vendor.css') }}">
<link rel="stylesheet" href="{{ asset('css/styles.css') }}">
@yield('style')

Le header

Le header contient la barre de navigation :

<header id="masthead" class="s-header">

            <div class="s-header__branding">
                <p class="site-title">
                    <a href="index.html" rel="home">Spurgeon.</a>
                </p>
            </div>

            <div class="row s-header__navigation">

                <nav class="s-header__nav-wrap">

                    <h3 class="s-header__nav-heading">Navigate to</h3>

                    <ul class="s-header__nav">
                        <li class="current-menu-item"><a href="index.html" title="">Home</a></li>
                        <li class="has-children">
                            <a href="#0" title="" class="">Categories</a>
                            <ul class="sub-menu">
                                <li><a href="category.html">Design</a></li>
                                <li><a href="category.html">Lifestyle</a></li>
                                <li><a href="category.html">Inspiration</a></li>
                                <li><a href="category.html">Work</a></li>
                                <li><a href="category.html">Health</a></li>
                                <li><a href="category.html">Photography</a></li>
                            </ul>
                        </li>
                        <li class="has-children">
                            <a href="#0" title="" class="">Blog</a>
                            <ul class="sub-menu">
                                <li><a href="single-standard.html">Standard Post</a></li>
                                <li><a href="single-video.html">Video Post</a></li>
                                <li><a href="single-audio.html">Audio Post</a></li>
                            </ul>
                        </li>
                        <li><a href="styles.html" title="">Styles</a></li>
                        <li><a href="about.html" title="">About</a></li>
                        <li><a href="contact.html" title="">Contact</a></li>
                    </ul> <!-- end s-header__nav -->

                </nav> <!-- end s-header__nav-wrap -->

            </div> <!-- end s-header__navigation -->

            <div class="s-header__search">

                <div class="s-header__search-inner">
                    <div class="row">

                        <form role="search" method="get" class="s-header__search-form" action="#">
                            <label>
                                <span class="u-screen-reader-text">Search for:</span>
                                <input type="search" class="s-header__search-field" placeholder="Search for..." value="" name="s" title="Search for:" autocomplete="off">
                            </label>
                            <input type="submit" class="s-header__search-submit" value="Search"> 
                        </form>

                        <a href="#0" title="Close Search" class="s-header__search-close">Close</a>

                    </div> <!-- end row -->
                </div> <!-- s-header__search-inner -->

            </div> <!-- end s-header__search -->

            <a class="s-header__menu-toggle" href="#0"><span>Menu</span></a>
            <a class="s-header__search-trigger" href="#">
                <svg width="24" height="24" fill="none" viewBox="0 0 24 24">
                    <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.25 19.25L15.5 15.5M4.75 11C4.75 7.54822 7.54822 4.75 11 4.75C14.4518 4.75 17.25 7.54822 17.25 11C17.25 14.4518 14.4518 17.25 11 17.25C7.54822 17.25 4.75 14.4518 4.75 11Z"></path>
                </svg>
            </a>

        </header>

Qui correspond à cette zone :

Capture d’écran du 2021-10-28 05-45-38.png

Il va déjà falloir changer le nom qui sert de logo, dans le code on renseigne l’url de la page d’accueil et le nouveau nom:

<div class="s-header__branding">
    <p class="site-title">
        <a href="{{ route('home') }}" rel="home">Mon blog.</a>
    </p>
</div>

On s’occupera du menu plus loin, on va donc conserver le reste du code inchangé.

La zone diaporama

Capture d’écran du 2021-10-29 06-31-44.png

On va ici se contenter de prévoir un emplacement qu’on remplira avec la vue index :

<!-- diapo
================================================== -->
@yield('diapo')

Le content

C’est là qu’on a le résumé des articles :

Capture d’écran du 2021-10-28 05-56-30.png

On va aussi juste prévoir un emplacement :

<!--  masonry -->
 <div id="bricks" class="bricks">
      @yield('main')
 </div> <!-- end bricks -->

Pour le moment on ne va rien faire dans le footer et se contenter de le conserver tel quel.

Capture d’écran du 2021-10-28 06-01-49.png

Le Javascript

Pour le Javascript en bas de page on va utiliser le meme helper que le css pour les urls des fichiers. On va aussi ajouter un emplacement pour pouvoir ajouter du code dans les vues enfants :

<!-- Java Script
================================================== -->
<script src="{{ asset('js/plugins.js') }}"></script>
<script src="{{ asset('js/main.js') }}"></script>
@yield('scripts')

Un composeur de vue

Comme on aura besoin d’informations systématiques dans le layout, mais aussi dans la vue index on va créer un composeur de vue : Http/ViewComposers/HomeComposer.php

Capture-51.png

Avec ce code :

<?php

namespace App\Http\ViewComposers;

use Illuminate\View\View;
use App\Models\Category;

class HomeComposer
{
    /**
     * Bind data to the view.
     *
     * @param  View  $view
     * @return void
     */
    public function compose(View $view)
    {
        $view->with([
            'categories' => Category::has('posts')->get(),
        ]);
    }
}

On envoie dans la vue les catégories qui possèdent des articles.

Il faut ensuite utiliser ce composeur de vue dans AppServiceProvider :

use App\Http\ViewComposers\HomeComposer;
use Illuminate\Support\Facades\View;

...

public function boot()
{
    View::composer(['front.layout', 'front.index'], HomeComposer::class);
}

Maintenant on est sûr d’avoir les catégories dans ces deux vues.

La vue index

Maintenant qu’on a un layout présentable on va créer la vue index :

Capture-48.png

On va avoir cette structure :

@extends('front.layout')


@section('diapo')

@endsection


@section('main')

@endsection

Les diapos

On va s’occuper du diaporama. On a vu qu’on envoie les données à partir du contrôleur et qu’on dispose ainsi d’une variable $diapos.

Un composant

Comme on va avoir du code répétitif on crée un composant anonyme :

Capture-50.png

Avec ce code :

@props(['post'])
<article class="hero__slide swiper-slide">
    <div class="hero__entry-image" style="background-image: url('storage/photos/{{ $post->user->id }}/{{ $post->image }}')"></div>
    <div class="hero__entry-text">
        <div class="hero__entry-text-inner">
            <div class="hero__entry-meta">
                <span class="cat-links">
                    @foreach($post->categories as $category)
                        <a href="#">{{ $category->title }}</a>
                    @endforeach
                </span>
            </div>
            <h2 class="hero__entry-title">
                <a href="#">
                    {{ $post->title }}
                </a>
            </h2>
            <p class="hero__entry-desc">
                {{ $post->overview }}
            </p>
            <a class="hero__more-link" href="#">Continuer</a>
        </div>
    </div>
</article>

Pour l’instant on ne peut pas renseigner les liens mais ça viendra…

Les diapos dans la vue

On peut maintenant intégrer les diapos dans la vue index :

@section('diapo')
    @isset($diapos)
        <div class="hero">

            <div class="hero__slider swiper-container">
                <div class="swiper-wrapper">

                    @foreach($diapos as $diapo)
                        <x-front.hero :post="$diapo" />
                    @endforeach

                </div> <!-- swiper-wrapper -->
                <div class="swiper-pagination"></div>

            </div> <!-- end hero slider -->

            <a href="#bricks" class="hero__scroll-down smoothscroll">
                <svg width="24" height="24" fill="none" viewBox="0 0 24 24">
                    <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.25 6.75L4.75 12L10.25 17.25"></path>
                    <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.25 12H5"></path>
                </svg>
                <span>Scroll</span>
            </a>

        </div> <!-- end hero -->
    @endisset
@endsection

Tous les diapos sont envoyés dans le composant qu’on a créé avec ce code :

@foreach($diapos as $diapo)
     <x-front.hero :post="$diapo" />
@endforeach

A ce stade nous avons ce diaporama :

Capture d’écran du 2021-10-29 07-47-41.png

Les articles

Un helper

Pour chaque article de la page d’accueil on va devoir aller récupérer l’image réduite (thumb). On crée un petit helper pour ca : app/helpers.php

Capture-54.png

Pour que les fonction qu’on de ce fichier soient connues il faut informer l’autoloader dans le fichier composer.json :

"autoload": {
    "psr-4": {
        ...
    },
    "files": [
        "app/helpers.php"
    ]
},

On crée la fonction pour aller chercher une image :

<?php

if (!function_exists('getImage')) {
    function getImage($post, $thumb = false)
    {   
        $url = "storage/photos/{$post->user->id}";
        if($thumb) 
            $url .= '/thumbs';
        return asset("{$url}/{$post->image}");
    }
}

Pour que ce soit vraiment prise en compte il faut rafraîchir l’autoload : composer dumpautoload

Les bricks

On va avoir des pavés (des bricks dans le theme). On crée un composant parce que le code est répétitif : ressources/views/components/front/brick.blade.php

Capture-53.png

Avec ce code:

@props(['post'])
<article class="brick entry" data-animate-el>

    <div class="entry__thumb">
        <a href="#" class="thumb-link">
            <img src="{{ getImage($post, true) }}" alt="">
        </a>
    </div> <!-- end entry__thumb -->

    <div class="entry__text">
        <div class="entry__header">
            <div class="entry__meta">
                <span class="byline">
                    By:
                    <a href="#0">{{ $post->user->name }}</a>
                </span>
            </div>
            <h1 class="entry__title"><a href="single-standard.html">{{ $post->title }}</a></h1>
         </div>
        <div class="entry__excerpt">
            <p>
                {{ $post->overview }}
            </p>
        </div>
        <a class="entry__more-link" href="#">Lire plus</a>
    </div> <!-- end entry__text -->

</article>

Les bricks dans la vue

On intégrer les bricks dans la vue index:

@section('main')

    @isset($title)
    <div class="row">
        <div class="column">
            <h1>{!! $title !!}</h1>
        </div>
    </div>
    @endisset


    <div class="masonry">
        <div class="bricks-wrapper" data-animate-block>

            <div class="grid-sizer"></div>

            @foreach($posts as $post)
                <x-front.brick :post="$post" />
            @endforeach

        </div> 

    </div> 

    <!-- pagination -->
    <div class="row pagination">
        <div class="column lg-12">
            <!-- la pagination viendra ici -->
        </div>
    </div>

@endsection

Vous devriez avoir :

Capture d’écran du 2021-10-29 08-26-43.png

La pagination

Créez une nouvelle vue pour cette pagination :

views/front/pagination.blade.php

Avec ce code :

@if ($paginator->hasPages())
    <nav class="pgn">
        <ul>
            {{-- Previous Page Link --}}
            <li>
                @if ($paginator->onFirstPage())
                    <span class="pgn__prev inactive">
                        <svg width="24" height="24" fill="none" viewBox="0 0 24 24">
                            <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.25 6.75L4.75 12L10.25 17.25"></path>
                            <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.25 12H5"></path>
                        </svg>
                    </span>
                @else
                    <a class="pgn__prev" href="{{ $paginator->previousPageUrl() }}">
                        <svg width="24" height="24" fill="none" viewBox="0 0 24 24">
                            <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.25 6.75L4.75 12L10.25 17.25"></path>
                            <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.25 12H5"></path>
                        </svg>
                    </a>
                @endif
            </li>

            {{-- Pagination Elements --}}
            @foreach ($elements as $element)

                {{-- "Three Dots" Separator --}}
                @if (is_string($element))
                    <li><span class="pgn__num current">{{ $element }}</span></li>
                @endif

                {{-- Array Of Links --}}
                @if (is_array($element))
                    @foreach ($element as $page => $url)
                        <li>
                            @if ($page == $paginator->currentPage())
                                <span class="pgn__num current">{{ $page }}</span>
                            @else
                                <a href="{{ $url }}" class="pgn__num">{{ $page }}</a>
                            @endif
                        </li>
                    @endforeach
                @endif

            @endforeach

            {{-- Next Page Link --}}
            <li>
                @if ($paginator->hasMorePages())
                    <a class="pgn__next" href="{{ $paginator->nextPageUrl() }}">
                        <svg width="24" height="24" fill="none" viewBox="0 0 24 24">
                            <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.75 6.75L19.25 12L13.75 17.25"></path>
                            <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 12H4.75"></path>
                        </svg>
                    </a>
                @else
                    <span class="pgn__next inactive">
                        <svg width="24" height="24" fill="none" viewBox="0 0 24 24">
                            <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.75 6.75L19.25 12L13.75 17.25"></path>
                            <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 12H4.75"></path>
                        </svg>
                    </span>
                @endif
            </li>
        </ul>
    </nav>
@endif

Dans la vue index, modifiez la zone prévu pour la pagination :

<!-- pagination -->
    <div class="row pagination">
        <div class="column lg-12">
            {{ $posts->links('front.pagination') }}
        </div>
    </div>

Vous devriez obtenir ce résultat :

Capture d’écran du 2021-10-29 08-39-12.png

Le menu

On va ajouter un helper dans le fichier helpers pour repérer la route active et placer la bonne classe :

use Illuminate\Support\Facades\Route;

if (!function_exists('currentRoute')) {
    function currentRoute($route)
    {
        return Route::currentRouteNamed($route) ? ' class=current' : '';
    }
}

On va se contenter pour le moment du lien pour la page d’accueil et celui des catégories. Apres modification dans la fichier layout le header ressemble a ceci:

<!-- # site header 
        ================================================== -->
        <header id="masthead" class="s-header">

            <div class="s-header__branding">
                <p class="site-title">
                    <a href="{{ route('home') }}" rel="home">Mon blog.</a>
                </p>
            </div>

            <div class="row s-header__navigation">

                <nav class="s-header__nav-wrap">

                    <h3 class="s-header__nav-heading">Navigate to</h3>

                    <ul class="s-header__nav">
                        <li {{ currentRoute('home') }}><a href="#" title="">Accueil</a></li>
                        <li class="has-children">
                            <a href="#0" title="">Categories</a>
                            <ul class="sub-menu">
                                @foreach($categories as $category)
                                    <li><a href="#">{{ $category->title }}</a></li>
                                @endforeach
                            </ul>
                        </li>
                    </ul> <!-- end s-header__nav -->

                </nav> <!-- end s-header__nav-wrap -->

            </div> <!-- end s-header__navigation -->

            <div class="s-header__search">

                <div class="s-header__search-inner">
                    <div class="row">

                        <form role="search" method="get" class="s-header__search-form" action="#">
                            <label>
                                <span class="u-screen-reader-text">Search for:</span>
                                <input type="search" class="s-header__search-field" placeholder="Search for..." value="" name="s" title="Search for:" autocomplete="off">
                            </label>
                            <input type="submit" class="s-header__search-submit" value="Search"> 
                        </form>

                        <a href="#0" title="Close Search" class="s-header__search-close">Close</a>

                    </div> <!-- end row -->
                </div> <!-- s-header__search-inner -->

            </div> <!-- end s-header__search -->

            <a class="s-header__menu-toggle" href="#0"><span>Menu</span></a>
            <a class="s-header__search-trigger" href="#">
                <svg width="24" height="24" fill="none" viewBox="0 0 24 24">
                    <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.25 19.25L15.5 15.5M4.75 11C4.75 7.54822 7.54822 4.75 11 4.75C14.4518 4.75 17.25 7.54822 17.25 11C17.25 14.4518 14.4518 17.25 11 17.25C7.54822 17.25 4.75 14.4518 4.75 11Z"></path>
                </svg>
            </a>

        </header>

C'est la fin de cet article, a ce stade nous avons une page fonctionnelle. nous revisiterons cette page pour y ajouter les liens et autres details. Dans le prochain article nous nous occuperons de l'affichage des articles.