Laravel 7

Shopping : les pages

Nous avons des pages d’information dans notre boutique : conditions de vente, mentions légales… Ces pages seront régulièrement mises à jour. On va dans cet article gérer ces pages, les modifier, les créer.

Vous pouvez télécharger un ZIP du projet ici.

Les données

Les données des états sont dans la table pages :

On doit pouvoir les modifier, les supprimer…

Contrôleur et routes

On crée un contrôleur :

php artisan make:controller Back\PageController --resource --model=Models\Page

On utilisera toutes les méthodes sauf show.

On ajoute les routes :

Route::prefix('admin')->middleware('admin')->namespace('Back')->group(function () {
    ...
    Route::resource('pages', 'PageController')->except('show');
});

DataTable

On va utiliser un dataTable pour la gestion des pages :

php artisan datatables:make PagesDataTable

<?php

namespace App\DataTables;

use App\Models\Page;
use Yajra\DataTables\Html\Column;
use Yajra\DataTables\Services\DataTable;

class PagesDataTable extends DataTable
{
    /**
     * Build DataTable class.
     *
     * @param mixed $query Results from query() method.
     * @return \Yajra\DataTables\DataTableAbstract
     */
    public function dataTable($query)
    {
        return datatables()
            ->eloquent($query)
            ->addColumn('show', function ($page) {
                return '<a href="' . route('page', $page->slug) . '" class="btn btn-xs btn-info btn-block" target="_blank">Voir</a>';
            })
            ->addColumn('edit', function ($page) {
                return '<a href="' . route('pages.edit', $page->id) . '" class="btn btn-xs btn-warning btn-block">Modifier</a>';
            })
            ->addColumn('destroy', function ($page) {
                return '<a href="#" class="btn btn-xs btn-danger btn-block">Supprimer</a>';
            })
            ->rawColumns(['show', 'edit', 'destroy']);
    }

    /**
     * Get query source of dataTable.
     *
     * @param \App\Page $model
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function query(Page $model)
    {
        return $model->newQuery();
    }

    /**
     * Optional method if you want to use html builder.
     *
     * @return \Yajra\DataTables\Html\Builder
     */
    public function html()
    {
        return $this->builder()
                    ->setTableId('pages-table')
                    ->columns($this->getColumns())
                    ->minifiedAjax()
                    ->dom('Blfrtip')
                    ->orderBy(1)
                    ->lengthMenu()
                    ->language('//cdn.datatables.net/plug-ins/1.10.20/i18n/French.json');
    }

    /**
     * Get columns.
     *
     * @return array
     */
    protected function getColumns()
    {
        return [
            Column::make('id'),
            Column::make('title')->title('Titre'),
            Column::make('slug')->title('Slug'),
            Column::computed('show')
              ->title('')
              ->width(60)
              ->addClass('text-center'),
            Column::computed('edit')
              ->title('')
              ->width(60)
              ->addClass('text-center'),
            Column::computed('destroy')
              ->title('')
              ->width(60)
              ->addClass('text-center'),
        ];
    }

    /**
     * Get filename for export.
     *
     * @return string
     */
    protected function filename()
    {
        return 'Pages_' . date('YmdHis');
    }
}

Le tableau

Pour afficher la liste des pages on code la méthode index du contrôleur PageController :

use App\DataTables\PagesDataTable;

...

public function index(PagesDataTable $dataTable)
{
    return $dataTable->render('back.shared.index');
}

On renseigne le titre dans config.titles :

return [

    ...

    'pages' => [
        'index' => 'Gestion des pages',
        'edit' => 'Modification d\'une page',
        'create' => 'Création d\'une page',
    ],
];

On ajoute le code pour faire apparaître le bouton de création d’un état dans la vue back.shared.index :

@section('main') 
  {{ $dataTable->table(['class' => 'table table-bordered table-hover table-sm'], true) }}
  @if(Route::currentRouteName() === 'etats.index')
    <a class="btn btn-primary" href="{{ route('etats.create') }}" role="button">Créer un nouvel état</a>
  @elseif(Route::currentRouteName() === 'pays.index')
    <a class="btn btn-primary" href="{{ route('pays.create') }}" role="button">Créer un nouveau pays</a>
  @elseif(Route::currentRouteName() === 'pages.index')
    <a class="btn btn-primary" href="{{ route('pages.create') }}" role="button">Créer une nouvelle page</a>
  @endif
@endsection

Avec l’url …/admin/pages on a le tableau :

On peut trier par colonne.

Le bouton Voir se content d’ouvrir la page dans un autre onglet.

Création d’une page

Le contrôleur

Pour créer un état on code la méthode create du contrôleur :

public function create()
{
    return view('back.pages.form');
}

La vue

On crée la vue du formulaire :

@extends('back.layout')

@section('css')
  <link href="https://cdn.jsdelivr.net/npm/summernote@0.8.16/dist/summernote-bs4.min.css" rel="stylesheet">
@endsection

@section('main') 
  <div class="container-fluid"> 
    @if(session()->has('alert'))
      <div class="alert alert-success alert-dismissible fade show" role="alert">
        {{ session('alert') }}
        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
    @endif
    <div class="row">
      <div class="col-sm-12">
        <div class="card">
        
          <form method="POST" action="@isset($page) {{ route('pages.update', $page->id) }} @else {{ route('pages.store') }} @endisset">
            <div class="card-body">
              @isset($page) @method('PUT') @endisset
              @csrf
          
                <x-inputbs4
                  name="title"
                  type="text"
                  label="Titre"
                  :value="isset($page) ? $page->title : ''"
                ></x-inputbs4>

                <x-inputbs4
                  name="slug"
                  type="text"
                  label="Slug"
                  :value="isset($page) ? $page->slug : ''"
                ></x-inputbs4>

                <div class="form-group">
                  <label for="text">Texte</label>
                  <textarea id="text" name="text"></textarea>
                </div>

              </div>
            </div>      

            <div class="form-group row mb-0">
              <div class="col-md-12">
                <a class="btn btn-primary" href="{{ route('pages.index') }}" role="button"><i class="fas fa-arrow-left"></i> Retour à la liste des pages</a>
                <button type="submit" class="btn btn-primary">Enregistrer</button>          
              </div>
            </div>
              
          </form>

        </div>
      </div>
    </div>
  </div>
@endsection

@section('js')
  <script src="https://cdn.jsdelivr.net/npm/summernote@0.8.16/dist/summernote-bs4.min.js"></script>
  <script>
    $(document).ready(function() {
      $.extend($.summernote.lang, {
        'fr-FR': {
          font: {
            bold: 'Gras',
            italic: 'Italique',
            underline: 'Souligné',
            clear: 'Effacer la mise en forme',
            height: 'Interligne',
            name: 'Famille de police',
            strikethrough: 'Barré',
            superscript: 'Exposant',
            subscript: 'Indice',
            size: 'Taille de police',
          },
          image: {
            image: 'Image',
            insert: 'Insérer une image',
            resizeFull: 'Taille originale',
            resizeHalf: 'Redimensionner à 50 %',
            resizeQuarter: 'Redimensionner à 25 %',
            floatLeft: 'Aligné à gauche',
            floatRight: 'Aligné à droite',
            floatNone: 'Pas d\'alignement',
            shapeRounded: 'Forme: Rectangle arrondi',
            shapeCircle: 'Forme: Cercle',
            shapeThumbnail: 'Forme: Vignette',
            shapeNone: 'Forme: Aucune',
            dragImageHere: 'Faites glisser une image ou un texte dans ce cadre',
            dropImage: 'Lachez l\'image ou le texte',
            selectFromFiles: 'Choisir un fichier',
            maximumFileSize: 'Taille de fichier maximale',
            maximumFileSizeError: 'Taille maximale du fichier dépassée',
            url: 'URL de l\'image',
            remove: 'Supprimer l\'image',
            original: 'Original',
          },
          video: {
            video: 'Vidéo',
            videoLink: 'Lien vidéo',
            insert: 'Insérer une vidéo',
            url: 'URL de la vidéo',
            providers: '(YouTube, Vimeo, Vine, Instagram, DailyMotion ou Youku)',
          },
          link: {
            link: 'Lien',
            insert: 'Insérer un lien',
            unlink: 'Supprimer un lien',
            edit: 'Modifier',
            textToDisplay: 'Texte à afficher',
            url: 'URL du lien',
            openInNewWindow: 'Ouvrir dans une nouvelle fenêtre',
          },
          table: {
            table: 'Tableau',
            addRowAbove: 'Ajouter une ligne au-dessus',
            addRowBelow: 'Ajouter une ligne en dessous',
            addColLeft: 'Ajouter une colonne à gauche',
            addColRight: 'Ajouter une colonne à droite',
            delRow: 'Supprimer la ligne',
            delCol: 'Supprimer la colonne',
            delTable: 'Supprimer le tableau',
          },
          hr: {
            insert: 'Insérer une ligne horizontale',
          },
          style: {
            style: 'Style',
            p: 'Normal',
            blockquote: 'Citation',
            pre: 'Code source',
            h1: 'Titre 1',
            h2: 'Titre 2',
            h3: 'Titre 3',
            h4: 'Titre 4',
            h5: 'Titre 5',
            h6: 'Titre 6',
          },
          lists: {
            unordered: 'Liste à puces',
            ordered: 'Liste numérotée',
          },
          options: {
            help: 'Aide',
            fullscreen: 'Plein écran',
            codeview: 'Afficher le code HTML',
          },
          paragraph: {
            paragraph: 'Paragraphe',
            outdent: 'Diminuer le retrait',
            indent: 'Augmenter le retrait',
            left: 'Aligner à gauche',
            center: 'Centrer',
            right: 'Aligner à droite',
            justify: 'Justifier',
          },
          color: {
            recent: 'Dernière couleur sélectionnée',
            more: 'Plus de couleurs',
            background: 'Couleur de fond',
            foreground: 'Couleur de police',
            transparent: 'Transparent',
            setTransparent: 'Définir la transparence',
            reset: 'Restaurer',
            resetToDefault: 'Restaurer la couleur par défaut',
          },
          shortcut: {
            shortcuts: 'Raccourcis',
            close: 'Fermer',
            textFormatting: 'Mise en forme du texte',
            action: 'Action',
            paragraphFormatting: 'Mise en forme des paragraphes',
            documentStyle: 'Style du document',
            extraKeys: 'Touches supplémentaires',
          },
          help: {
            'insertParagraph': 'Insérer paragraphe',
            'undo': 'Défaire la dernière commande',
            'redo': 'Refaire la dernière commande',
            'tab': 'Tabulation',
            'untab': 'Tabulation arrière',
            'bold': 'Mettre en caractère gras',
            'italic': 'Mettre en italique',
            'underline': 'Mettre en souligné',
            'strikethrough': 'Mettre en texte barré',
            'removeFormat': 'Nettoyer les styles',
            'justifyLeft': 'Aligner à gauche',
            'justifyCenter': 'Centrer',
            'justifyRight': 'Aligner à droite',
            'justifyFull': 'Justifier à gauche et à droite',
            'insertUnorderedList': 'Basculer liste à puces',
            'insertOrderedList': 'Basculer liste ordonnée',
            'outdent': 'Diminuer le retrait du paragraphe',
            'indent': 'Augmenter le retrait du paragraphe',
            'formatPara': 'Changer le paragraphe en cours en normal (P)',
            'formatH1': 'Changer le paragraphe en cours en entête H1',
            'formatH2': 'Changer le paragraphe en cours en entête H2',
            'formatH3': 'Changer le paragraphe en cours en entête H3',
            'formatH4': 'Changer le paragraphe en cours en entête H4',
            'formatH5': 'Changer le paragraphe en cours en entête H5',
            'formatH6': 'Changer le paragraphe en cours en entête H6',
            'insertHorizontalRule': 'Insérer séparation horizontale',
            'linkDialog.show': 'Afficher fenêtre d\'hyperlien',
          },
          history: {
            undo: 'Annuler la dernière action',
            redo: 'Restaurer la dernière action annulée',
          },
          specialChar: {
            specialChar: 'Caractères spéciaux',
            select: 'Choisir des caractères spéciaux',
          },
        },
      });
      $('#text').summernote({
        tabsize: 2,
        height: 400,
        lang: 'fr-FR',
        toolbar: [
          ['style', ['style']],
          ['font', ['bold', 'underline', 'clear']],
          ['fontname', ['fontname']],
          ['color', ['color']],
          ['para', ['ul', 'ol', 'paragraph']],
          ['table', ['table']],
          ['insert', ['link']],
          ['view', ['fullscreen', 'codeview', 'help']],
        ],
      });
      @isset($page)
        const content = {!! json_encode($page->text) !!};
        $('#text').summernote('code', content);
      @endisset
    });
  </script>
@endsection

On utilise Summernote comme éditeur. Dans le Javascript j’ai intégré la traduction en français et l’initialisation du menu.

On obtient ce formulaire :

La validation

Pour la validation on crée une requête de formulaire :

php artisan make:request PageRequest

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class PageRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'slug' => 'required|string|max:255',
            'title' => 'required|string|max:255',
            'text' => 'required|string',
        ];
    }
}

Le contrôleur

On code la méthode store du contrôleur :

use App\Http\Requests\PageRequest;

...

public function store(PageRequest $request)
{
    Page::create($request->all());

    return back()->with('alert', config('messages.pagecreated'));
}

On ajoute les messages dans config.messages :

return [
    ...
    'pageupdated' => 'La page a bien été mise à jour.',
    'pagecreated' => 'La page a bien été créée.',
];

Quand on crée un état on a bien le message :

Et on le retrouve dans la liste :

D’autre par un lien est créé automatiquement dans le footer de la boutique :

Modification d’un état

Le contrôleur

Pour modifier un état on code la méthode edit du contrôleur :

public function edit(Page $page)
{
    return view('back.pages.form', compact('page'));
}

On utilise la même vue mais cette fois on envoie les données.

Pour la modification dans la base on code la méthode update du contrôleur :

public function update(PageRequest $request, Page $page)
{
    $page->update($request->all());

    return back()->with('alert', config('messages.pageupdated'));
}

La validation est la même que pour la création.

On a un message lorsque la mise à jour a eu lieu :

Supprimer une page

Le contrôleur

Pour supprimer une page on code la méthode destroy et on crée une méthode alert dans le contrôleur comme on l’avait fait pour les pays et les états :

public function destroy(Page $page)
{
    $page->delete();

    return redirect(route('pages.index'));
}

public function alert(Page $page)
{
    return view('back.pages.destroy', compact('page'));
}

On ajoute la vue pour l’alerte :

@extends('back.layout')

@section('main') 
  <div class="container-fluid"> 
    <form id="deleteproduct" action="{{ route('pages.destroy', $page->id) }}" method="POST" style="display: none;">
      @csrf
      @method('DELETE')
    </form>
    <div class="row">
      <div class="col-sm-12 col-md-6 offset-md-3 col-lg-4 offset-lg-4">
        <div class="card text-white bg-dark mb-3">
          <div class="card-body">
            <h5 class="card-title text-center mb-3">Vous êtes sur le point de supprimer la page "<strong>{{ $page->title }}"</strong></h5>
            <p class="card-text">
              <a class="btn btn-danger btn-lg btn-block" href="#" role="button"
              onclick="event.preventDefault(); 
              $('#deleteproduct').submit();"
              >Je confirme la suppression</a>
            </p>
          </div>
        </div>
      </div>
    </div>
  </div>
@endsection

On crée la route :

Route::prefix('admin')->middleware('admin')->namespace('Back')->group(function () {
    ...
    Route::name('pages.destroy.alert')->get('pages/{page}', 'PageController@alert');
});

On ajoute le lien dans les boutons de suppression dans PagesDataTable :

->addColumn('destroy', function ($page) {
    return '<a href="' . route('pages.destroy.alert', $page->id) . '" class="btn btn-xs btn-danger btn-block">Supprimer</a>';
})

Quand on clique on a le message d’alerte :

Le menu

Il ne nous reste plus qu’à compléter le menu de l’administration (back.layout) :

<li class="nav-item has-treeview {{ menuOpen(
      'shop.edit',
      'shop.update',
      'pays.index',
      'pays.edit',
      'pays.create',
      'plages.edit',
      'colissimos.edit',
      'etats.index', 
      'etats.edit', 
      'etats.create', 
      'etats.destroy.alert',
      'pages.index',
      'pages.edit',
      'pages.create',
      'pages.destroy.alert'
  ) }}">
  <a href="#" class="nav-link {{ currentRouteActive(
      'shop.edit',
      'shop.update',
      'pays.index',
      'pays.edit',
      'pays.create',
      'plages.edit',
      'colissimos.edit',
      'etats.index', 
      'etats.edit', 
      'etats.create', 
      'etats.destroy.alert',
      'pages.index',
      'pages.edit',
      'pages.create',
      'pages.destroy.alert'
    ) }}">
    <i class="nav-icon fas fa-cogs"></i>
    <p>
      Administration
      <i class="right fas fa-angle-left"></i>
    </p>
  </a>
  <ul class="nav nav-treeview">

    ...

    <x-menu-item :href="route('pays.index')" :sub=true :active="currentRouteActive(
        'pays.index', 
        'pays.edit',
        'pays.create'
      )">
      Pays
    </x-menu-item>

    <x-menu-item :href="route('pages.index')" :sub=true :active="currentRouteActive(
        'pages.index',
        'pages.edit',
        'pages.create',
        'pages.destroy.alert'
      )">
      Pages
    </x-menu-item>

    ...

  </ul>
</li>

Conclusion

Dans le prochain article nous verrons la gestion des produits.

Print Friendly, PDF & Email

4 commentaires

Leave a Reply