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:
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:
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 :
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 :
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).
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 :
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.
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 :
Comme on aura pas mal de manipulations au niveau de la base de données on va aussi prévoir un repository :
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 :
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
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 :
On va aussi juste prévoir un emplacement :
<!-- masonry -->
<div id="bricks" class="bricks">
@yield('main')
</div> <!-- end bricks -->
Le footer
Pour le moment on ne va rien faire dans le footer et se contenter de le conserver tel quel.
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
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 :
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 :
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 :
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
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
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 :
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 :
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.