Un site d’annonces – créer une annonce

Dans cet article nous allons maintenant mettre en place l’intendance pour la création d’une annonce. La principale difficulté va résider dans la gestion des photos. Heureusement pour nous il existe Dropzone, une librairie Javascript qui facilite la mise en place d’un système de glisser-déposer des fichiers.

Pour vous simplifier la vie vous pouvez télécharger le dossier complet pour le code de cet article.

Dropzone et Intervention image

Pour l’intégration de Dropzone dans Laravel je me suis largement inspiré de cet article et de celui-ci.

On commence par charger Dropzone avec npm :

npm i dropzone --save-dev

Pour le fonctionnement de Dropzone il faut prévoir la librairie dans le CSS. Voici le fichier resources\sass\app.scss complété en conséquence (je le remets complet parce que j’ai ajouté quelques règles spécifiques pour dropzone) :

// Fonts
@import url('https://fonts.googleapis.com/css?family=Sniglet');

// Dropzone
@import "~dropzone/dist/dropzone.css";

// Bootstrap
@import '~bootstrap/scss/bootstrap';

html {
    font-size: 0.8rem;
}

@include media-breakpoint-up(sm) {
    html {
        font-size: 1rem;
    }
}

@include media-breakpoint-up(lg) {
    html {
        font-size: 1.4rem;
    }
}

body {
    background-color: #e47517;
    font-family: 'Sniglet', cursive;
}

.navbar {
    background-color: #e4751788;
}

.navbar-expand-md {
    @include font-size(1.4rem);
}

.navbar-expand {
    padding: 0;
    font-size: 0.8rem;
}

.base {
    fill:#003dd4;
    cursor: pointer;
    -webkit-transition: fill .5s ease-out;
    -moz-transition: fill .5s ease-out;
    -o-transition: fill .5s ease-out;
    transition: fill .5s ease-out;
}

.base:hover {
    fill: #bd4;
}

#my-dropzone {
    border: 2px dashed #0087F7;
    border-radius: 5px;
    .message {
        font-family: "Segoe UI Light", "Arial", serif;
        font-weight: 600;
        color: #0087F7;
        @include font-size(1.5em);
        letter-spacing: 0.05em;
    }
}

a.dz-remove {
    text-decoration: none !important;
    &:hover {
        color: red;
        font-size: 1rem !important;
    }
}

a.blockAd {
    text-decoration: none;
    color: inherit;
    &:hover h4 {
        color: #e47517;
    }
    &:hover .card {
        box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
    }
}

#accordion .btn-link a {
    text-decoration: none;
}

Il faut aussi ajouté la partie Javascript dans resources\js\app.js :

window.Popper = require('popper.js').default;
window.$ = window.jQuery = require('jquery');
window.Dropzone = require('dropzone');
require('bootstrap');

Et on recompile !

Pour la gestion côté serveur on va ajouter Intervention Image qui est un incontournable sur le sujet :

composer require intervention/image

Il n’y a rien de plus à faire parce que la librairie est automatiquement reconnue par Laravel.

Routes et contrôleur

Pour les routes on en a déjà puisqu’on a prévu un contrôleur de ressource (AdController) avec ce code :

Route::resource('annonces', 'AdController')
    ->parameters([
        'annonces' => 'ad'
    ])->except([
        'index', 'show', 'destroy'
]);

On dispose donc du create et du store.

Mais on va avoir besoin de routes pour les photos. En effet chaque fois qu’une photo est déposée dans la zone de téléchargement elle est envoyée au serveur qui doit mémoriser à la fois la photo et la session correspondante. En tenant compte du fait que l’utilisateur peut aussi supprimer une photo on comprend que l’intendance n’est pas des plus faciles. se rajoute une autre difficulté : si l’utilisateur recharge la page il faut actualiser la zone des photos téléchargées !

On a donc besoin de ces 3 routes :

Route::middleware('ajax')->group(function () {
    Route::post('images-save', 'UploadImagesController@store')->name('save-images');
    Route::delete('images-delete', 'UploadImagesController@destroy')->name('destroy-images');
    Route::get('images-server','UploadImagesController@getServerImages')->name('server-images');
    ...
});
  • save-image : pour télécharger une photo
  • destroy-image : pour supprimer une photo
  • server-images : pour récupérer toutes les images téléchargées en cas de rechargement de la page

Pour ces 3 actions on va créer le contrôleur UploadImagesController :

php artisan make:controller UploadImagesController

On vérifie nos routes :

On va commencer par coder la méthode create de AdController :

use Illuminate\Support\Str;

...


public function create(Request $request)
{
    if(!$request->session()->has('index')) {
        $request->session()->put('index', Str::random(30));
    }

    $categories = Category::select('name', 'id')->oldest('name')->get();
    $regions = Region::oldest('name')->get();

    return view('create', compact('categories', 'regions'));
}

Si on n’a pas de référence en session on en crée une (index).

Pour le formulaire de création des annonces on a besoin de la liste des catégories et de celle des régions.

On va aussi ajouter l’adresse du lien pour le bouton de création d’une annonce dans la vue home :

<a class="btn btn-primary" href="{{ route('annonces.create') }}" role="button">Déposer une annonce</a>

La vue create

On crée la vue create ici :

Avec ce code :

@extends('layouts.app')

@section('content')

<div class="container">

    @guest
        <div class="alert alert-warning text-center alert-dismissible fade show" role="alert">
            Vous n'êtes pas connecté, vous ne pourrez pas modifier votre annonce ultérieurement<br>
            La création d'un compte est gratuite et vous offre de nombreuses fonctionnalités
            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span>
            </button>
        </div>
    @endguest

    <div class="card bg-light">
        <h5 class="card-header">Votre annonce</h5>
        <div class="card-body">

            <div class="form-group">
                <label>Vos photos (3 images au maximum et 3Mo pour chacune)</label>
                <form method="post" action="{{ route('save-images') }}" enctype="multipart/form-data" class="dropzone" id="my-dropzone">
                    @csrf
                    <div class="dz-message">
                        <div class="col-xs-8">
                            <div class="message">
                                <p>Déposez vos photos ici ou cliquez</p>
                            </div>
                        </div>
                    </div>
                </form>
            </div>

            <form method="POST" action="{{ route('annonces.store') }}">
                @csrf

                <div class="form-group">
                    <label for="category">Catégorie</label>
                    <select class="custom-select" name="category" id="category">
                        @foreach ($categories as $category)
                            <option value="{{ $category->id }}" @if($loop->first) selected @endif>{{ $category->name }}</option>
                        @endforeach
                    </select>
                </div>

                @include('partials.form-group', [
                    'title' => 'Titre',
                    'type' => 'text',
                    'name' => 'title',
                    'required' => true,
                ])

                <div class="form-group">
                    <label for="texte">Texte</label>
                    <textarea class="form-control{{ $errors->has('texte') ? ' is-invalid' : '' }}" id="texte" name="texte" rows="3" required>{{ old('texte', isset($value) ? $value : '') }}</textarea>
                    @if ($errors->has('texte'))
                        <div class="invalid-feedback">
                            {{ $errors->first('texte') }}
                        </div>
                    @endif
                </div>

                <div class="form-group">
                    <label for="limit">Nombre de semaines de parution</label>
                    <select class="custom-select" id="limit" name="limit">
                        <option value="1">1</option>
                        <option value="2">2</option>
                        <option value="3">3</option>
                        <option value="4">4</option>
                    </select>
                </div>

                <br>

                <div class="card">
                    <h5 class="card-header">Votre localisation</h5>
                    <div class="card-body">
                        <div class="form-group">
                            <label for="region">Région</label>
                            <select class="custom-select" name="region" id="region">
                                @foreach ($regions as $region)
                                    <option data-code="{{ $region->code }}" value="{{ $region->id }}" @if($loop->first) selected @endif>{{ $region->name }}</option>
                                @endforeach
                            </select>
                        </div>

                        <div class="form-group">
                            <label for="departement">Département</label>
                            <select class="custom-select" name="departement" id="departement"></select>
                        </div>

                        <div class="form-group">
                            <label for="commune">Commune</label>
                            <select class="custom-select" name="commune" id="commune"></select>
                        </div>
                    </div>
                </div>

                <br>

                @guest
                    <div class="card">
                        <h5 class="card-header">Votre identité</h5>
                        <div class="card-body">

                            @include('partials.form-group', [
                                'title' => 'Pseudo',
                                'type' => 'text',
                                'name' => 'pseudo',
                                'required' => true,
                            ])

                            @include('partials.form-group', [
                                'title' => 'Email',
                                'type' => 'email',
                                'name' => 'email',
                                'required' => true,
                            ])

                        </div>
                    </div>
                @endguest

                <br>

                <button type="submit" class="btn btn-primary">Valider</button>
            </form>

            <div id="preview" style="display: none;">
                <div class="dz-preview dz-file-preview">
                    <div class="dz-image"><img data-dz-thumbnail /></div>
                    <div class="dz-details">
                        <div class="dz-size"><span data-dz-size></span></div>
                        <div class="dz-filename"><span data-dz-name></span></div>
                    </div>
                    <div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
                    <div class="dz-error-message"><span data-dz-errormessage></span></div>
                    <div class="dz-success-mark">
                        <svg width="54px" height="54px" viewBox="0 0 54 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
                            <title>Check</title>
                            <defs></defs>
                            <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
                                <path d="M23.5,31.8431458 L17.5852419,25.9283877 C16.0248253,24.3679711 13.4910294,24.366835 11.9289322,25.9289322 C10.3700136,27.4878508 10.3665912,30.0234455 11.9283877,31.5852419 L20.4147581,40.0716123 C20.5133999,40.1702541 20.6159315,40.2626649 20.7218615,40.3488435 C22.2835669,41.8725651 24.794234,41.8626202 26.3461564,40.3106978 L43.3106978,23.3461564 C44.8771021,21.7797521 44.8758057,19.2483887 43.3137085,17.6862915 C41.7547899,16.1273729 39.2176035,16.1255422 37.6538436,17.6893022 L23.5,31.8431458 Z M27,53 C41.3594035,53 53,41.3594035 53,27 C53,12.6405965 41.3594035,1 27,1 C12.6405965,1 1,12.6405965 1,27 C1,41.3594035 12.6405965,53 27,53 Z" id="Oval-2" stroke-opacity="0.198794158" stroke="#747474" fill-opacity="0.816519475" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
                            </g>
                        </svg>
                    </div>
                    <div class="dz-error-mark">
                        <svg width="54px" height="54px" viewBox="0 0 54 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
                            <title>Erreur</title>
                            <defs></defs>
                            <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
                                <g id="Check-+-Oval-2" sketch:type="MSLayerGroup" stroke="#747474" stroke-opacity="0.198794158" fill="#FFFFFF" fill-opacity="0.816519475">
                                    <path d="M32.6568542,29 L38.3106978,23.3461564 C39.8771021,21.7797521 39.8758057,19.2483887 38.3137085,17.6862915 C36.7547899,16.1273729 34.2176035,16.1255422 32.6538436,17.6893022 L27,23.3431458 L21.3461564,17.6893022 C19.7823965,16.1255422 17.2452101,16.1273729 15.6862915,17.6862915 C14.1241943,19.2483887 14.1228979,21.7797521 15.6893022,23.3461564 L21.3431458,29 L15.6893022,34.6538436 C14.1228979,36.2202479 14.1241943,38.7516113 15.6862915,40.3137085 C17.2452101,41.8726271 19.7823965,41.8744578 21.3461564,40.3106978 L27,34.6568542 L32.6538436,40.3106978 C34.2176035,41.8744578 36.7547899,41.8726271 38.3137085,40.3137085 C39.8758057,38.7516113 39.8771021,36.2202479 38.3106978,34.6538436 L32.6568542,29 Z M27,53 C41.3594035,53 53,41.3594035 53,27 C53,12.6405965 41.3594035,1 27,1 C12.6405965,1 1,12.6405965 1,27 C1,41.3594035 12.6405965,53 27,53 Z" id="Oval-2" sketch:type="MSShapeGroup"></path>
                                </g>
                            </g>
                        </svg>
                    </div>
                </div>
            </div>

        </div>
    </div>
@endsection

@section('script')
    <script>

        const fillSelect = (element, data) => {
            element.html('');
            data.forEach((e) => {
                element.append($('<option>').val(e.code).text(e.nom));
            });
        }

        const fillDepartements = () => {
            $.get('https://geo.api.gouv.fr/regions/' + $('#region option:selected').attr('data-code') + '/departements', function(data) {
                fillSelect($('#departement'), data);
                fillCommunes();
            });
        }

        const fillCommunes = () => {
            $.get('https://geo.api.gouv.fr/departements/' + $('#departement').val() + '/communes', function(data) {
                fillSelect($('#commune'), data);
            });
        }

        Dropzone.options.myDropzone = {
            uploadMultiple: true,
            parallelUploads: 3,
            maxFilesize: 3,
            maxFiles: 3,
            dictMaxFilesExceeded : 'Vous ne pouvez charger que 3 photos',
            previewTemplate: document.querySelector('#preview').innerHTML,
            addRemoveLinks: true,
            acceptedFiles: 'image/*',
            dictInvalidFileType : 'Type de fichier interdit',
            dictRemoveFile: 'Supprimer',
            dictFileTooBig: 'L\'image fait plus de 3 Mo',
            timeout: 10000,

            init () {

                const myDropzone = this;

                $.get('{{ route('server-images') }}', data => {
                    $.each(data.images, (key, value) => {
                        const mockFile = {
                            name: value.original,
                            size: value.size,
                            dataURL: '{{ url('images') }}' + '/' + value.server
                        };
                        myDropzone.files.push(mockFile);
                        myDropzone.emit("addedfile", mockFile);
                        myDropzone.createThumbnailFromUrl(mockFile,
                        myDropzone.options.thumbnailWidth,
                        myDropzone.options.thumbnailHeight,
                        myDropzone.options.thumbnailMethod, true, (thumbnail) => {
                            myDropzone.emit('thumbnail', mockFile, thumbnail);
                        });
                        myDropzone.emit('complete', mockFile);
                    });
                });

                this.on("removedfile", file => {
                    $.ajax({
                        method: 'delete',
                        url: '{{ route('destroy-images') }}',
                        data: { name: file.name, _token: $('[name="_token"]').val() }
                    });
                });
            }
        };

        $(() => {
            fillDepartements();
            $('#region').change(() => { fillDepartements(); });
            $('#departement').change(() => { fillCommunes(); });
        })
    </script>
@endsection

Pour simplifier le codage on crée une vue partielle pour les contrôles de formulaire :

Avec ce code :

<div class="form-group">
    <label for="{{ $name }}">{{ $title }}</label>
    <input id="{{ $name }}" type="{{ $type }}" class="form-control{{ $errors->has($name) ? ' is-invalid' : '' }}"
           name="{{ $name }}" value="{{ old($name, isset($value) ? $value : '') }}" {{ $required ? 'required' : ''}}>

    @if ($errors->has($name))
        <div class="invalid-feedback">
            {{ $errors->first($name) }}
        </div>
    @endif
</div>

Ca fait beaucoup de code mais on va détailler un peu tout ça pour les différentes étapes !

Normalement vous devriez obtenir le formulaire en cliquant sur le bouton. dans la partie supérieure :

Et dans la partie inférieure la localisation :

On avance !

C’est l’aspect pour un visiteur non connecté. Sinon on n’a plus l’alerte ni l’identité. Le reste restant inchangé.

Le téléchargement des images

Dropzone simplifie la gestion du téléchargement mais il y a quand même du travail ! Pour le paramétrage on se contente au départ de spécifier l’url dans l’action de la balise form :

<form method="post" action="{{ route('save-images') }}" enctype="multipart/form-data" class="dropzone" id="my-dropzone">

Du coup dès qu’on dépose une image elle est expédiée à cette adresse automatiquement. Une image miniature est créée en local pour afficher dans la zone avec un lien pour la suppression :

Mais évidemment pour le moment il ne se passe rien côté serveur !

Il nous faut coder la méthode store du contrôleur UploadImagesController :

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\ { Storage, File };
use Intervention\Image\Facades\Image;
use App\Models\Upload;

class UploadImagesController extends Controller
{
    private $photos_path;
    private $thumbs_path;

    public function __construct()
    {
        $this->photos_path = public_path('/images');
        $this->thumbs_path = public_path('/thumbs');
    }

    /**
     * Saving images uploaded through XHR Request.
     *
     * @param  \Illuminate\Http\Request $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $photos = $request->file('file');

        if (!is_array($photos)) {
            $photos = [$photos];
        }

        if (!is_dir($this->photos_path)) {
            mkdir($this->photos_path);
        }

        if (!is_dir($this->thumbs_path)) {
            mkdir($this->thumbs_path);
        }

        for ($i = 0; $i < count($photos); $i++) {
            $photo = $photos[$i];
            $name = str_random(30);
            $save_name = $name . '.' . $photo->getClientOriginalExtension();

            Image::make($photo)
                ->resize(150, null, function ($constraints) {
                    $constraints->aspectRatio();
                })
                ->save($this->thumbs_path . '/' . $save_name);

            $photo->move($this->photos_path, $save_name);

            $upload = new Upload();
            $upload->filename = $save_name;
            $upload->original_name = basename($photo->getClientOriginalName());
            $upload->index = $request->session()->get('index');
            $upload->ad_id = 0;
            $upload->save();
        }
    }
}

Les images vont dans le dossier public/images et les miniatures dans le dossier public/thumbs. ces dossiers sont créés s’ils n’existent pas. le processus pour récupérer les images et les sauvegarder est simplifié grâce à Intervention Image. On crée un nom aléatoire de 30 caractères pour chaque image, le même nom pour l’image complète et sa miniature, puisqu’elles se trouvent dans des dossiers différents il n’y a pas de confusion possible.

On mémorise les informations dans la table uploads en se servant du modèle Upload :

  • le nom de 30 caractères
  • le nom original (pour le cas du rechargement de la page)
  • l’index au niveau de la session pour lier les images téléchargées avec la session
  • l’identifiant de l’annonce correspondante (au départ c’est 0 parce qu’elle n’existe pas encore)

Avec Dropzone le paramétrage se fait dans le Javascript au niveau des options :

Dropzone.options.myDropzone = {
    uploadMultiple: true,
    parallelUploads: 3,
    maxFilesize: 3,
    maxFiles: 3,
    dictMaxFilesExceeded : 'Vous ne pouvez charger que 3 photos',
    previewTemplate: document.querySelector('#preview').innerHTML,
    addRemoveLinks: true,
    acceptedFiles: 'image/*',
    dictInvalidFileType : 'Type de fichier interdit',
    dictRemoveFile: 'Supprimer',
    dictFileTooBig: 'L\'image fait plus de 3 Mo',
    timeout: 10000,

On voit ainsi qu’on peut envoyer en parallèle 3 images, qu’on limite aussi à 3 le nombre d’images, etc…

Le rechargement de la page

Comme je l’ai dit précédemment il faut gérer le rechargement éventuel de la page. Dans ce cas on charge le formulaire mais que faire des images déjà téléchargées ? On va mettre en place une procédure pour les récupérer sur le serveur et recréer la données pour Dropzone. C’est ce javascript qui s’en charge lors de l’initalisation de Dropzone :

$.get('{{ route('server-images') }}', data => {
    $.each(data.images, (key, value) => {
        const mockFile = {
            name: value.original,
            size: value.size,
            dataURL: '{{ url('images') }}' + '/' + value.server
        };
        myDropzone.files.push(mockFile);
        myDropzone.emit("addedfile", mockFile);
        myDropzone.createThumbnailFromUrl(mockFile,
        myDropzone.options.thumbnailWidth,
        myDropzone.options.thumbnailHeight,
        myDropzone.options.thumbnailMethod, true, (thumbnail) => {
            myDropzone.emit('thumbnail', mockFile, thumbnail);
        });
        myDropzone.emit('complete', mockFile);
    });
});

On va compléter le contrôleur UploadImageController avec cette méthode :

public function getServerImages(Request $request)
{
    if($request->session()->has('index')) {
        $imageAnswer = [];

        $index = $request->session()->get('index');
        $images = Upload::whereIndex($index)->get();

        foreach ($images as $image) {
            $imageAnswer[] = [
                'original' => $image->original_name,
                'server' => $image->filename,
                'size' => File::size($this->photos_path . '/' . $image->filename),
            ];
        }

        if(!empty($imageAnswer)) {
            return response()->json([
                'images' => $imageAnswer
            ]);
        }
    }
}

On vérifie qu’on a mémorisé quelque chose en session. Si c’est le cas on va aller récupérer toutes les images dans la table uploads. Ensuite on prépare la réponse pour le client.

Suppression d’une image

Avec Dropzone on peut aussi supprime rune image avec le lien dédié. IL est immédiatment supprimé côté client mais il faut aussi aller faire un nettoyage sur le serveur. Au niveau du Javascript ça se passe ici :

this.on("removedfile", file => {
    $.ajax({
        method: 'delete',
        url: '{{ route('destroy-images') }}',
        data: { name: file.name, _token: $('[name="_token"]').val() }
    });
});

On complète le contrôleur UploadImageController avec cette méthode :

public function destroy(Request $request)
{
    $uploaded_image = Upload::where('original_name', basename($request->name))->first();

    if (!empty($uploaded_image)) {

        $file_path = $this->photos_path . '/' . $uploaded_image->filename;
        $thumb_file = $this->thumbs_path . '/' . $uploaded_image->filename;

        if (file_exists($file_path)) {
            unlink($file_path);
        }

        if (file_exists($thumb_file)) {
            unlink($thumb_file);
        }

        $uploaded_image->delete();
    }
}

On recherche l’image dans la table uploads grâce à son nom original. Si on la trouve on supprime les deux versions de l’image sur le serveur. On finit par nettoyer la table.

Enregistrement de l’annonce

Maintenant que le problème des images est réglé voyons le reste des infrmattions du formulaire. Vous retrouverez les routines qu’on a déjà vues pour aller récupérer les départements et communes sur l’API geo.

Pour le reste on a des champs de formulaire classiques à prendre en compte à la soumission.

La validation

On va créer un Form Request pour la validation :

php artisan make:request AdStore

On ne va s’intéresser qu’aux champ de texte étant donné que pour les listes il est difficile de faire une erreur de saisie :

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class AdStore extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'title' => ['required', 'string', 'max:100'],
            'texte' => ['required', 'string', 'max:1000'],
            'pseudo' => ['sometimes', 'required', 'string', 'max:20'],
            'email' => ['sometimes', 'required', 'string', 'email', 'max:255'],
        ];
    }
}

On a un sometimes pour les champs pseudo et email qui ne sont pas présents pour un utilisateur connecté.

Le contrôleur

Dans le contrôleur AdController on complète la méthode store :

use App\Http\Requests\AdStore;
use Carbon\Carbon;
use App\Models\ { Category, Region, Ad, Upload };

...

public function store(AdStore $request)
{
    $commune = json_decode(file_get_contents('https://geo.api.gouv.fr/communes/' . $request->commune), true);

    $ad = $this->adRepository->create([
        'title' => $request->title,
        'texte' => $request->texte,
        'category_id' => $request->category,
        'region_id' => $request->region,
        'departement' => $request->departement,
        'commune' => $request->commune,
        'commune_name' => $commune['nom'],
        'commune_postal' => $commune['codesPostaux'][0],
        'user_id' => auth()->check() ? auth()->id() : 0,
        'pseudo' => auth()->check() ? auth()->user()->name :$request->pseudo,
        'email' => auth()->check() ? auth()->user()->email : $request->email,
        'limit' => Carbon::now()->addWeeks($request->limit),
    ]);

    if($request->session()->has('index')) {
        $index = $request->session()->get('index');
        Upload::whereIndex($index)->update(['ad_id' => $ad->id, 'index' => 0]);
    }

    return view('adconfirm');
}

Il faut aller chercher les renseignements de la commune avec l’API parce qu’on ne reçoit que le code.

On ajoute la méthode create dans le repository (AdRepository) :

public function create($data)
{
    return Ad::create($data);
}

On peut alors l’utiliser dans le contrôleur pour enregistrer toutes les données.

En ce qui concerne les photos on vérifie qu’on a quelque chose en session et si c’est le cas pour chaque photo mémorisée on actualise le champ ad_id dans la table uploads pour lier les photos à l’annonce.

On retroune à la fin une vue adconfirm :

Avec ce simple code :

@extends('layouts.app')

@section('content')

<div class="container">
    <div class="jumbotron">
        <div class="row">
            <div class="col-md-4">
                <img src="{{ asset('img/speaker.png') }}" alt="">
            </div>
            <div class="col-md-8">
                <h1 class="display-4">Confirmation d'annonce</h1>
                <p class="lead">Votre annonce a bien été prise en compte, elle sera publiée après modération.</p>
            </div>
        </div>
    </div>
</div>

@endsection

Si vous avez bien codé vous devriez obtenir ça (l’image est dans le dossier à télécharger) :

Conclusion

On a désormais achevé le traitement côté client avec la création des annonces. Je ne suis pas entré dans le détail de tout le code pour ne pas alourdir l’article. Dans le prochain article on commencera la mise en place de l’administration.

Print Friendly, PDF & Email

Laisser un commentaire