Un site d’annonces – les annonces

Dans ce nouvel article on va s’intéresser à l’affichage des annonces. Sur la page d’accueil qu’on a créée dans la précédente étape on a une sélection de la région. On va ajouter la sélection optionnelle du département et de la commune. Selon ces choix on va afficher une liste paginée des annonces correspondantes. On pourra ensuite cliquer sur l’une d’elle pour afficher tous ses détails.

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

Avertissement : de nombreuses personnes ont eu un souci dans l’affichage des annonces. Après analyse il apparaît qu’un nom de classe que j’ai choisi rentre en collision avec le module AdBlockPlus qui est très utilisé. Pour ne pas avoir le souci il suffit de désactiver ce module. Vous pouvez aussi changer le nom de la classe en cause (blockAd). Pour ceux qui ont galéré avec ce problème je rappelle que l’utilisation des outils de développement des navigateurs permettent de traquer facilement ce genre de bug. D’autre part ça met en évidence un problème assez fréquent qui incite à utiliser des solutions comme le shadow dom pour mieux isoler le style, et le reste !

Routes, contrôleur et middleware

On va commencer par définir les routes dont on va avoir besoin pour les annonces ainsi que le contrôleur. A priori notre contrôleur va avoir la plupart des méthodes d’un contrôleur de ressource. Alors on le crée :

php artisan make:controller AdController --resource

Pour les routes on va les définir ainsi :

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

Route::prefix('annonces')->group(function () {
    Route::get('voir/{ad}', 'AdController@show')->name('annonces.show');
    Route::get('{region?}/{departement?}/{commune?}', 'AdController@index')->name('annonces.index');
    Route::post('recherche', 'AdController@search')->name('annonces.search')->middleware('ajax');
});

On commence par définir partiellement les routes de la ressource en excluant (except) index, show et destroy qui seront définies ailleurs.

On redéfinit le paramètre pour avoir dans l’url annonce, plutôt que ad, ce qui est plus explicite.

On a ensuite un groupe de 3 routes :

  • pour voir une annonce (show) mais en ayant le mot voir dans l’url,
  • l’url de recherche avec 3 paramètres optionnels (region, departement et commune) étant entendu qu’il y a une hiérarchie et que la région par exemple ne peut pas être optionnelle si on choisit un département et ainsi de suite, sinon ça ne marcherait pas,
  • enfin une route de recherche qui sera en Ajax (on voit qu’il y a un middleware qu’on va devoir créer).

Vous avez compris que l’actualisation de la recherche va se faire en Ajax.

Voilà un petit point :

Le middleware Ajax

Puisqu’il nous faut un middleware pour Ajax créons le :

php artisan make:middleware Ajax

Avec ce code :

<?php

namespace App\Http\Middleware;

use Closure;

class Ajax
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if ($request->ajax()) {
            return $next($request);
        }

        abort(404);
    }
}

Et il ne faut pas oublier de le déclarer dans app\Http\Kernel :

protected $routeMiddleware = [
    ...
    'ajax' => \App\Http\Middleware\Ajax::class,
];

La page d’accueil

On avait pas mis les adresses dans les liens de la page d’accueil (home) pour les régions parce qu’on avait pas encore prévu les routes. On va pouvoir maintenant les compléter :

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-12 col-md-3 col-lg-3">
            <a class="btn btn-primary" href="" role="button">Déposer une annonce</a>
        </div>
        <div class="col-12 col-md-9 col-lg-9">
            <svg xmlns:svg="http://www.w3.org/2000/svg" viewBox="0 0 900 900">
                <a href="{{ route('annonces.index', 'occitanie') }}">
                    <path class="base" data-toggle="tooltip" title="Occitanie"
                        d="m 428.3,774.7 68.28824,1.41765 L 494.4,720.6 l 52.5,-38.8 14.8,7 25.1,-26 0.9,-38.1 -37.5,-8.3 -9.7,-29.4 -34.6,-24.3 -15.42353,24.04118 -15.61765,-21.68824 -21.22941,26.62353 -22.55294,-3.91765 -2.51176,-23.22941 -34.12942,-3.54706 L 351.5,638.6 293.45294,646.81765 302.82353,749.64706 356.4,744.2 Z"/>
                </a>
                <a href="{{ route('annonces.index', 'pays_de_la_loire') }}">
                    <path class="base" data-toggle="tooltip" title="Pays de la Loire"
                        d="M 268.2,432.3 229.2353,441.52941 182.9,401.6 162.2,343.4 231.37059,312.44118 247.29412,255.67647 356.5,281.5 l -20.8,46.1 -30.75882,38.22353 -56.11765,14.82353 z""/>
                </a>
                <a href="{{ route('annonces.index', 'bretagne') }}">
                    <path class="base" data-toggle="tooltip" title="Bretagne"
                        d="M 226.05882,309.17647 157.23529,338.29411 41.82353,307.05882 20.647059,288 48.176471,281.64706 16.941177,259.94117 25.8,247 123.35294,218.64706 143.47059,248.29411 207.5,243.2 242,252.5 Z""/>
                </a>
                <a href="{{ route('annonces.index', 'centre') }}">
                    <path class="base" data-toggle="tooltip" title="Centre-Val de Loire"
                        d="m 313.41177,364.7647 c 9,7.41177 71.31764,68.52942 71.31764,68.52942 L 441.04118,427.20588 490.2,398.1 487.95294,290.12941 388.1513,230.74351 362.62353,241.30588 362.1,283.3 342,329.82353 Z"/>
                </a>
                <a href="{{ route('annonces.index', 'normandie') }}">
                    <path class="base" data-toggle="tooltip" title="Normandie"
                        d="m 323.71176,174.21765 -88.65294,2.60588 -9.52941,-29.11765 -36,-1.58824 25.19191,92.05393 141.17868,35.3929 0.7,-36.86447 33.52419,-16.52616 22.53911,-31.1701 2.80632,-42.7111 L 392.6,117.1 312.35294,151.41176 Z"/>
                </a>
                <a href="{{ route('annonces.index', 'ile_de_france') }}">
                    <path class="base" data-toggle="tooltip" title="Île-de-France"
                        d="M 397.58824,227.11764 415,194 l 79,6.6 20.1,26 -1.1,37.7 -29.3,16.7 z"/>
                </a>
                <a href="{{ route('annonces.index', 'hauts_de_france') }}">
                    <path class="base" data-toggle="tooltip" title="Hauts-de-France"
                        d="m 459.5,28.1 -55.3,18.4 -3.4,39.5 -3.53336,26.02312 23.75689,29.84747 -3.0264,46.31423 76.47889,6.53412 20.30295,23.76216 46.87398,-86.33992 L 558.3,99 Z"/>
                </a>
                <a href="{{ route('annonces.index', 'grand_est') }}">
                    <path class="base" data-toggle="tooltip" title="Grand Est"
                        d="M 563.57853,103.93079 635.69174,156.40927 800,212.3 774.52942,246.70588 757.9,336.4 l -19.6,7.6 -12.21765,-32.78235 -64.04787,-12.25792 -36.27715,30.39069 -38.03513,-33.68543 -43.84789,1.1548 -25.04824,-32.27372 0.18819,-41.30026 48.92475,-91.74037 z/"">
                </a>
                <a href="{{ route('annonces.index', 'bourgogne') }}">
                    <path class="base" data-toggle="tooltip" title="Bourgogne-Franche-Comté"
                        d="m 665.06471,305.92941 56.52353,11.18823 10.88823,27.94118 -71.77058,99.11765 L 635.1,442.5 l -4.6043,-15.10589 -25.49168,-3.77814 -15.22208,26.95233 -46.4355,-3.2994 7.80392,-22.04274 -55.53596,-28.13453 -0.1772,-114.99224 20.02314,-11.68867 26.14333,32.71916 47.7414,0.78345 38.12551,35.23961 z"/>
                </a>
                <a href="{{ route('annonces.index', 'auvergne') }}">
                    <path class="base" data-toggle="tooltip" title="Auvergne-Rhône-Alpes"
                        d="M 535.45882,450.42353 593.6,455.2 608.82353,429.88235 625.7,431.6 l 5.35883,17.87059 36,1.05882 36,-21.17647 30.70588,101.11765 L 632.9,608.1 l 12.8,20.3 -13.5,2.3 -45.6,-16.1 -30.18823,-2.6 -11.11765,-29.64706 -39.13122,-29.42816 -16.90577,21.26103 -15.41509,-17.33867 -22.25617,29.48934 -17.33293,-4.9306 1.98236,-19.69999 30.04875,-93.6762 -19.98993,-33.38263 45.80543,-30.84615 49.88761,25.00432 z"/>
                </a>
                <a href="{{ route('annonces.index', 'provence') }}">
                    <path class="base" data-toggle="tooltip" title="Provence-Alpes-Côte d'Azur"
                        d="m 594,625.23529 c 9,4.76471 41.29412,12.70589 41.29412,12.70589 l 18,-3.17648 -12.08824,-25.51764 68.20589,-54.42353 24.35294,26.47059 L 726.4,621.1 769.76471,636.88235 696.8,716.5 676.3,726.7 567,690.35294 592.94118,664.41176 Z"/>
                </a>
                <a href="{{ route('annonces.index', 'corse') }}">
                    <path class="base" data-toggle="tooltip" title="Corse"
                        d="m 773.65294,695.94118 5.7,72.5 -18.2,63.7 -19.2,-6.7 -24.6,-65.20589 12.17648,-29.11764 L 765,715.23529 l 1.05883,-20.64706 z"/>
                </a>
                <a href="{{ route('annonces.index', 'aquitaine') }}">
                    <path class="base" data-toggle="tooltip" title="Nouvelle-Aquitaine"
                        d="m 224.35882,446.83529 c 9,4.76471 51.35295,-11.64705 51.35295,-11.64705 l -15.35294,-49.76472 48.79411,-13.87058 75.36142,69.83442 53.72682,-6.19912 20.62104,34.50899 -27.50339,87.78513 -40.71765,-4.66471 L 349.3,630.54117 285.86471,643.45294 293.62353,745.47059 205.3,694.6 214.82942,628.95294 235.1874,506.12438 Z"/>
                </a>
            </svg>
        </div>
    </div>
</div>
@endsection

@section('script')

<script>
    $(function(){
        $('[data-toggle="tooltip"]').tooltip();
    });
</script>
@endsection

Maintenant pour chaque région on a un lien de la forme annonces/{slug de la region} et on vise la méthode index de notre contrôleur. On va créer maintenant cette méthode, ou plutôt la compléter parce qu’elle a été créée avec le contrôleur.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\ { Category, Region };

class AdController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  String  $regionSlug
     * @param  Integer  $departementCode
     * @param  Integer  $communeCode
     * @return \Illuminate\Http\Response
     */
    public function index(
        Request $request,
        $regionSlug = null,
        $departementCode = null,
        $communeCode = null)
    {
        $categories = Category::select('name', 'id')->oldest('name')->get();
        $regions = Region::select('id','code','name','slug')->oldest('name')->get();
        $region = $regionSlug ? Region::whereSlug($regionSlug)->firstOrFail() : null;
        $page = $request->query('page', 0);

        return view('adsvue', compact('categories', 'regions', 'region', 'departementCode', 'communeCode', 'page'));
    }

On reçoit les 3 paramètres optionnels dans la méthode.

On va avoir besoin de toutes les catégories dans la vue alors on les récupère dans l’ordre alphabétique (oldest) :

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

Il nous faut aussi la liste complète des régions, la aussi dans l’ordre alphabétique :

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

Ensuite si un slug est présent pour la région (ça sera le cas quand on va cliquer sur une région de la carte) on la récupère, sinon on garde le null :

$region = $regionSlug ? Region::whereSlug($regionSlug)->firstOrFail() : null;

Ensuite on regarde s’il y a une pagination et on renvoie le numéro de la page :

$page = $request->query('page', 0);

En effet comme on va fonctionner en Ajax il va falloir tenir à jour l’url avec le numéro de page pour le cas ou l’utilisateur rechargerait cette page (il ont parfois de ces idées !).

Ensuite on envoie tout ça dans une vue :

return view('adsvue', compact('categories', 'regions', 'region', 'departementCode', 'communeCode', 'page'));

Evidemment cette vue n’existant pas encore pour le moment on obtient une jolie erreur :

Un composant

Pour la vue adsvue on va créer un composant Vue.js qu’on va appeler AdComponent. Mais pour commencer on va prévoir l’intendance de Vue.js en créant un fichier Javascript pour l’initialisation :

Avec ce simple code :

window.Vue = require('vue');

Vue.component('ad', require('./components/AdComponent.vue').default);

const app = new Vue({
    el: '#app'
});

On charge Vue.js, le composant qu’on va bientôt créer et on crée l’objet principal pour l’application.

On va le déclarer dans webpack.mix.js :

mix.js('resources/js/vue.js', 'public/js')

Si vous compiler maintenant évidemment on va vous dire que le composant n’existe pas !

On crée le fichier du composant :

En voici le code :

<template>
<div class="container">
    <div class="card bg-light">
        <h5 class="card-header">Votre recherche</h5>
        <div class="card-body">
            <form id="formAd" method="POST" :action="url">

                <div class="form-group">
                    <label for="category">Catégorie</label>
                    <select class="custom-select" name="category" id="category" @change="onCategoryChange()" v-model="categorySelected">
                        <option value="0">Toutes</option>
                        <option v-for="category in categories" :key="category.id" :value="category.id">
                            {{ category.name }}
                        </option>
                    </select>
                </div>

                <div class="form-group">
                    <label for="region">Région</label>
                    <select class="custom-select" name="region" id="region" @change="onRegionChange()" v-model="regionSelected">
                        <option data-code="0" value="0">Toute la France</option>
                        <option v-for="region in regions" :key="region.id" :value="region.id" :data-code="region.code">
                            {{ region.name }}
                        </option>
                    </select>
                </div>

                <div v-if="regionSelected != 0" class="form-group">
                    <label for="departement">Département</label>
                    <select class="custom-select" name="departement" id="departement" @change="onDepartementChange" v-model="departementSelected">
                        <option value="0">Tous</option>
                        <option v-for="departement in departements" :key="departement.code" :value="departement.code">
                            {{ departement.nom }}
                        </option>
                    </select>
                </div>

                <div v-if="departementSelected != 0" class="form-group" >
                    <label for="commune">Commune</label>
                    <select class="custom-select" name="commune" id="commune" @change="onCommuneChange" v-model="communeSelected">
                        <option value="0">Toutes</option>
                        <option v-for="commune in communes" :key="commune.code" :value="commune.code">
                            {{ commune.nom }}
                        </option>
                    </select>
                </div>
            </form>
        </div>
    </div>

    <br>

    <span v-html="ads"></span>

</div>
</template>

<script>

    export default {
        props: [
            'url',
            'categories',
            'regions'
        ],
        data () {
            return {
                categorySelected: 0,
                regionSelected: 0,
                regionIndex: 0,
                regionSlug: '',
                departements: [],
                departementSelected: 0,
                communes: [],
                communeSelected: 0,
                ads: ''
            }
        },
        methods: {
            onCategoryChange() {
                this.submit();
            },
            onRegionChange() {
                const index = event.target.selectedIndex;
                if (index) {
                    this.regionSlug = this.regions[index - 1]['slug'];
                    let code = event.target.options[index].attributes['data-code'].value;
                    this.fillDepartements(code);
                } else {
                    this.regionSelected = 0;
                }
                this.submit();
            },
            fillDepartements(code) {
                if(code) {
                    let that = this;
                    $.get('https://geo.api.gouv.fr/regions/' + code + '/departements', data => {
                        that.departements = data;
                    });
                }
                this.departementSelected = 0;
            },
            onDepartementChange() {
                const index = event.target.selectedIndex;
                if (index) {
                    this.fillCommunes(event.target.value);
                } else {
                    this.departementSelected = 0;
                }
                this.submit();
            },
            onCommuneChange() {
                this.communeId = event.target.selectedIndex;
                this.submit();
            },
            fillCommunes(code) {
                if(code) {
                    let that = this;
                    $.get('https://geo.api.gouv.fr/departements/' + code + '/communes', data  => {
                        that.communes = data;
                    });
                }
                this.communeSelected = 0;
            },
            submit(e, comp = '') {
                $.ajax({
                    method: 'post',
                    url: this.url + comp,
                    data: {
                        'category': this.categorySelected,
                        'region': this.regionSelected,
                        'departement': this.departementSelected,
                        'commune': this.communeSelected,
                        '_token': $('meta[name="csrf-token"]').attr('content')
                    }
                })
                .done(data => {
                    this.ads = data;
                    let ref = '/annonces';
                    if(this.regionSelected) {
                        ref += '/' + this.regionSlug
                    }
                    if(this.departementSelected) {
                        ref += '/' + this.departementSelected
                    }
                    if(this.communeSelected) {
                        ref += '/' + this.communeSelected
                    }
                    if(comp) {
                        ref += comp;
                    }
                    history.pushState({}, 'Annonces', ref);
                })
            }
        },
        mounted(e) {
            this.regionSelected = $('#start').attr('data-id');
            if(this.regionSelected != 0) {
                this.regionSlug = $('#start').attr('data-slug');
                this.fillDepartements($('#start').attr('data-code'));
                const dep = $('#start').attr('data-departement');
                if(dep) {
                    this.departementSelected = dep;
                    this.fillCommunes(dep);
                }
                const com = $('#start').attr('data-commune');
                if(com) {
                    this.communeSelected = com;
                }
            }
            if($('#start').attr('data-page')) {
                this.submit(e, '?page=' + $('#start').attr('data-page'));
            } else {
                this.submit();
            }
        }
    }

    $('body').on('click', 'a.page-link', e => {
        e.preventDefault();
        app.__vue__.$refs.adComp.submit(e, '?' + ($(e.currentTarget).attr('href')).split('?')[1]);
    });

</script>

Analysons un peu ce code…

On a d’abord un template qui a pour but d’afficher 4 liste de sélection pour :

  • les catégories
  • les régions
  • les départements
  • les communes

Au niveau des données on voit qu’on reçoit en props :

  • une url
  • les catégories
  • les régions

Il va donc falloir renseigner des données dans la vue.

Au niveau des données internes du composant on a essentiellement :

  • departements : un tableau des départements
  • communes : un tableau des communes
  • ads : la liste des annonces en cours

Quand on a un changement dans l’une des listes il faut agir !

Si c’est un changement de catégorie on se contente d’envoyer la requête de recherche :

onCategoryChange() {
    this.submit();
},

Si c’est un changement de région c’est plus complexe, en effet il faut alors actualiser la liste des départements :

onRegionChange() {
    ...

    this.fillDepartements(code);

    ...

    this.submit();
},

Pour remplir la liste des département on fait appel à l’API comme on l’a vue pour les factories :

fillDepartements(code) {
    if(code) {
        let that = this;
        $.get('https://geo.api.gouv.fr/regions/' + code + '/departements', function(data) {
            that.departements = data;
        });
    }
    this.departementSelected = 0;
},

C’est le même principe pour un changement de département avec la liste des communes.

Dans tous les cas on lance la requête au serveur :

submit(e, comp = '') {
    $.ajax({
        method: 'post',
        url: this.url + comp,
        data: {
            'category': this.categorySelected,
            'region': this.regionSelected,
            'departement': this.departementSelected,
            'commune': this.communeSelected,
            '_token': $('meta[name="csrf-token"]').attr('content')
        }
    })
    .done((data) => {
        this.ads = data;
        let ref = '/annonces';
        if(this.regionSelected) {
            ref += '/' + this.regionSlug
        }
        if(this.departementSelected) {
            ref += '/' + this.departementSelected
        }
        if(this.communeSelected) {
            ref += '/' + this.communeSelected
        }
        if(comp) {
            ref += comp;
        }
        history.pushState({}, 'Annonces', ref);
    })
}

C’est une requête POST avec l’url /annonces/recherche. On a prévu dans les routes que ça arrive sur la méthode search du contrôleur qui n’existe pas encore.

On transmet les 4 paramètres :

  • catégorie
  • région
  • département
  • commune

Et on ajoute le token pour la sécurité.

Au retour on actualise la variable ads qui affiche ainsi la liste des annonces dans le template :

<span v-html="ads"></span>

Le serveur renvoie aussi la pagination qu’on affiche telle quelle. Mais comment gérer cette pagination ? Il faut déclencher la méthode submit du composant. Quand on utilise Vue.js on a parfois des soucis de communication quand on se situe à l’extérieur du système. Ce souci est géré ici :

$('body').on('click', 'a.page-link', e => {
    e.preventDefault();
    app.__vue__.$refs.adComp.submit(e, '?' + ($(e.currentTarget).attr('href')).split('?')[1]);
});

J’ai dû un peu grater dans le code de Vue.js pour écrire ces lignes mais ainsi ça fonctionne. On va voir qu’li faut quand même créer une réfénrence dans la vue.

La vue adsvue

Il est temps maintenant de créer la vue :

Au passage j’ai supprimer la vue welcome qui ne nous sert pas.

Voici le code :

@extends('layouts.app')

@section('content')
<div id="app">
    @if($region)
        <div id="start"
            data-id={{ $region->id }}
            data-code="{{ $region->code }}"
            data-slug="{{ $region->slug }}"
        @if($departementCode)
            data-departement="{{ $departementCode }}"
            @if($communeCode)
                data-commune="{{ $communeCode }}"
            @endif
        @endif
    @else
        <div id="start" data-id="0"
    @endif
    @if($page != 0)
        data-page="{{ $page }}"
    @endif
    ></div>
    <ad
        url="{{ route('annonces.search') }}"
        :categories="{{ $categories }}"
        :regions="{{ $regions }}"
        ref="adComp"
    ></ad>
</div>
@endsection

@section('script')
    <script src="{{ asset('js/vue.js') }}"></script>
@endsection

On voit qu’on renseigne les props du composant ainsi que la fameuse référence pour assurer le fonctionnement de la pagination qu’on a vu précédemment.

On prévoie aussi les données de départ pour le composant :

  • la région
  • la page

Pourquoi ne pas envoyer ça directement en props ? Tout simplement que dans le cycle de construction du composant ça ne fonctionne malheureusment pas. Alors il faut ruser… Ensuite dans la méthode mounted du composant on vient lire ces données :

mounted(e) {
    this.regionSelected = $('#start').attr('data-id');
    if(this.regionSelected != 0) {
        this.regionSlug = $('#start').attr('data-slug');
        this.fillDepartements($('#start').attr('data-code'));
        const dep = $('#start').attr('data-departement');
        if(dep) {
            this.departementSelected = dep;
            this.fillCommunes(dep);
        }
        const com = $('#start').attr('data-commune');
        if(com) {
            this.communeSelected = com;
        }
    }
    if($('#start').attr('data-page')) {
        this.submit(e, '?page=' + $('#start').attr('data-page'));
    } else {
        this.submit();
    }
}

C’est un peu accrobatique mais je n’ai pas trouvé mieux…

Un repository

Pour être un peu organisés on va créer un repository pour gérer les annonces :

On commence à le remplir avec notre recherche :

<?php

namespace App\Repositories;

use App\Models\Ad;
use Carbon\Carbon;

class AdRepository
{
    /**
     * Search.
     *
     * @param \Illuminate\Http\Request $request
     */
    public function search($request)
    {
        $ads = Ad::query();

        if($request->region != 0) {
            $ads = Ad::whereHas('region', function ($query) use ($request) {
                $query->where('regions.id', $request->region);
            })->when($request->departement != 0, function ($query) use ($request) {
                return $query->where('departement', $request->departement);
            })->when($request->commune != 0, function ($query) use ($request) {
                return $query->where('commune', $request->commune);
            });
        }

        if($request->category != 0) {
            $ads->whereHas('category', function ($query) use ($request) {
                $query->where('categories.id', $request->category);
            });
        }

        return $ads->with('category', 'photos')
            ->whereActive(true)
            ->latest()
            ->paginate(3);
    }
}

Selon catégorie, région, département et commune on extrait les annonces avec une pagination de 3 pages.

Le contrôleur et la vue partielle

Le contrôleur

On en revient au contrôleur où il nous faut la méthode search qui reçoit la demande du composant dans la vue et qui doit utiliser le repository pour aller chercher les annonces correspondantes :

use App\Repositories\AdRepository;

class AdController extends Controller
{
    /**
     * Ad repository.
     *
     * @var App\Repositories\AdRepository
     */
    protected $adRepository;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct(AdRepository $adRepository)
    {
        $this->adRepository = $adRepository;
    }

    /**
     * Search ads.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  String  $slug
     * @return \Illuminate\Http\Response
     */
    public function search(Request $request)
    {
        setlocale (LC_TIME, 'fr_FR');

        $ads = $this->adRepository->search($request);

        return view('partials.ads', compact('ads'));
    }

On voit qu’on fait appel à une vue partielle pour constituer la réponse.

La vue partielle

On crée cette vue :

Avec ce code :

@foreach($ads as $ad)
<a href="{{ route('annonces.show', $ad->id) }}" class="blockAd">
        <div class="card d-flex flex-row">
            <div class="card-header">
                @if($ad->photos->isNotEmpty())
                    <img class="rounded" src="{{ asset('thumbs/' . $ad->photos->first()->filename) }}" alt="">
                @else
                    <img src="{{ asset('thumbs/question.jpg') }}" alt="">
                @endif
            </div>
            <div class="card-body">
                <h4 class="card-title">{{ $ad->title }}</h4>
                <p class="card-text">{{ $ad->category->name }}</p>
                <p class="card-text">
                    {{ $ad->commune_name . ' (' . $ad->commune_postal . ')'}}<br>
                    {{ $ad->created_at->calendar() }}
                </p>
            </div>
        </div>
    </a>
    <br>
@endforeach
<div class="d-flex">
    <div class="mx-auto">
        {{ $ads->links() }}
    </div>
</div>

On constitue ainsi le code HTML pour la liste des annonces qui correspondent à la recherche ainsi que la pagination en partie inférieure.

Le résultat

Maintenant on a tout mis en place il ne nous reste plus qu’à cliquer sur une région et…

Maintenant on peut par exemple sélectionner un département pour n’avoir que les annonces de celui-ci :

Faites des essais en changeant la catégorie, la région… Normalement à chaque fois les annonces s’actualisent et vous avez la pagination qui fonctionne s’il y a plus de 3 annonces.

Remarquez aussi que l’url est actualisée à chaque fois pour qu’un rechargement de la page aboutisse au même résultat. c’est assuré par ce code dans le composant lorsque la requête de recherche renvoie un résultat positif :

.done(data => {
    this.ads = data;
    let ref = '/annonces';
    if(this.regionSelected) {
        ref += '/' + this.regionSlug
    }
    if(this.departementSelected) {
        ref += '/' + this.departementSelected
    }
    if(this.communeSelected) {
        ref += '/' + this.communeSelected
    }
    if(comp) {
        ref += comp;
    }
    history.pushState({}, 'Annonces', ref);
})

Conclusion

On a désormais un affichage d’une liste d’annonce en fonction de critères de recherches : catégorie et localisation. On a aussi la pagination s’il y en a beaucoup. On a vu que Vue.js s’intègre bien dans Laravel même s’il faut parfois un peu jongler. J’ai utilisé JQuery pour les requêtes HTTP puisqu’il est déjà prévu dans le projet.

Dans le prochain article on créera la vue pour afficher une annonce avec tous ses détails. On verra qu’il faudra prévoir quelques précautions pour ne pas afficher une annonce non active. Enfin on va donner la possibilité au visiteur de laisser un message à l’émetteur de l’annonce…

Print Friendly, PDF & Email

36 commentaires sur “Un site d’annonces – les annonces

  1. Bonjour
    Pour ce qui ont eu le fameux probeleme souligné ci-dessus , testez ce code sur votre terminal : php artisan db:seed –class=DatabaseSeeder
    En tout cas, ca resolu mon prrobleme.
    Mon analyse sur le probeme : selon moi la base de données n’etait pas charger des données victives de la classe seeder.

    du passage, je tiens a remerciement milles fois la personne l’auteur de tuto
    je te connais pas mais t es un homme bien
    aller au code

  2. Bonjour s’il vous plaît quelqu’un pourrait m’aider a apporter les modifications en temps réel plus précisément dans les fichiers sasss ?
    NB, je suis entrain de suivre le tutoriel sur le développement d’un site d’annonce en Laravel 5.8!

          1. Merci beaucoup j’ai pu régler ça, s’était du à mon extension chrome AdBlock … Et merci encore pour votre énorme travail !
            Que Dieu vous bénisse !

    1. Je ne sais pas comment afficher du code dans le message, mais en gros j’ai créé un test @if($ad->active===0) il y a la classe blockAd et @else pas de class blockAd

      Je ne sais pas si c’est la meilleure solution mais elle fonctionne.

  3. Bonjour Tout le monde.
    Bestmomo, franchement merci pour ces tutos. Moi qui souhaitait me mettre à Laravel, tu as fais un travail de dingue!! Encore merci à toi.
    Cependant, j’arrive à la même conclusion que pas mal de monde. La page de recherche fonctionne bien sauf l’affichage des annonces… 🙁
    J’ai repris l’ensemble de tes lecons et tout le code. J’ai aussi repis les zip mis à dispo pour vérifier à quel endroit j’ai « bouletté » 😉 Mais je n’ai rien trouvé…
    Je t’avoue que j’ai un soucis pour la compréhension de l’affichage de ads.blade.php(dans partials) Je comprends que cela doit se dérouler au niveau du AdsComponent () mais …. Ce ne se déroule pas comme prévue. As tu une astuce qui me guiderai dans le débug??? (pas la solution please, j’aimerai comprendre).
    Par avance merci
    et encore bravo pour ce site…
    (ps: as tu d’autres petits tutos en préparation???)

    1. Salut,

      Comme pas mal de monde a un souci avec cet affichage j’ai creusé l’affaire et j’ai trouvé le loup bien caché ! En fait j’ai nommé la classe pour le style des annonces AdBlock, or il se trouve que la plupart des gens utilisent AdBlock pour supprimer les publicités et manque de chance il s’y trouve la même classe avec un display none. Donc si je ne me trompe pas de loup tu dois avoir AdBlock activé sur ta page. Désactive le pour voir si ça passe. Si c’est le cas il faudra que je change le nom de cette classe.

    1. Salut tout le monde, j’ai commencé le projet d’annonce avec Laravel 6 je n’arrive pas a afficher la partie du haut avec les différents champs de recherche(région, département, service, commune) mais, impossible de faire afficher la liste des annonces trouvés. j’ai une erreur 500 qui s’affiche dans la console de mon navigateur(chrome).

      Si quelqu’un a une idée, je suis preneur(ça fait 4 jours que j’essaie de résoudre le problème sans succès.

    1. Bonjour,

      Il faudrait vérifier quel est le lien en place, quelle url est envoyée. En général une page vide correspond à une méthode du contrôleur pas encore codée, la requête aboutit mais on n’obtient pas de réponse.

      1. Bonjour, tout fonctionne bien pour moi mais il y a un bug que ne parviens pas à comprendre. J’affiche la carte sur localhost/monprojet/public quand je clique sur un élément de la carte, je suis redirigé vers localhost/monprojet/public/annonces/region puis directement redirigé bers localhost/annonces/region. Je ne comprend pas pourquoi

          1. Ce que je veux dire c’est que ça va pas bien fonctionner avec des urls du genre localhost/monprojet/public/…
            Il faut un host local du genre monhost.oo.

          2. Personnellement comme je suis sous Windows j’utilise Laragon qui est parfait au niveau du développement en local et qui génère automatiquement ces hosts.

      1. Bonjour,

        J’ai remarqué que dans certains ZIP j’avais pas mis ce fichier. Je l’ai rajouté partout. Par contre il y a effectivement un bug avec Chrome pour Vue.js, les cards ne s’affichent pas alors que la variable ads du composant est bien renseignée…

        1. Bonjour,

          Concernant le problème de card qui ne s’affiche pas sur chrome. Cela vient de la class ‘blockAd’ reconnu par les bloqueurs de pub de type adblock.
          En espérant avoir aidé.
          Merci pour ton travail.

Laisser un commentaire