Notre site d'annonces est presque terminé. Il ne nous reste plus qu'à coder l'interface de l'utilisateur. Il doit pouvoir gérer ses annonces (voir celles qui sont en attentes, les actives, prolonger une annonce, la détruire). Il doit aussi avoie accès à ses données personnelles et éventuellement les changer.
Pour vous simplifier la vie vous pouvez télécharger le dossier complet pour le code de cet article. C'est d'ailleurs l'état final de ce projet. Vous pouvez aussi récupérer la version pour Laravel 8 mise à disposition par Everyways (merci à lui !).
Un middleware
De la même manière qu'on a créé un middleware pour filtrer les administrateurs on va en créer un pour filtrer les utilisateurs :
php artisan make:middleware User
Avec ce code :
public function handle($request, Closure $next)
{
if ($request->user()) {
return $next($request);
}
return redirect()->route('home');
}
Et on le déclare dans le kernel :
protected $routeMiddleware = [
...
'user' => \App\Http\Middleware\User::class,
];
Les routes
On va avoir besoin de routes pour les actions de l'utilisateur :- Affichage de l'accueil
- Gestion des annonces
- Annonces actives
- Annonces obsolètes
- Annonces en attente de modération (donc non actives)
- Profil
- Affichage du profil
- Affichage formulaire de modification de l'email
- Mise à jour de l'email
- Affichage des données personnelles
Route::prefix('utilisateur')->middleware('user')->group(function () {
Route::get('/', 'UserController@index')->name('user.index');
Route::prefix('annonces')->group(function () {
Route::get('actives', 'UserController@actives')->name('user.actives');
Route::get('obsoletes', 'UserController@obsoletes')->name('user.obsoletes');
Route::get('attente', 'UserController@attente')->name('user.attente');
});
Route::prefix('profil')->group(function () {
Route::get('email', 'UserController@emailEdit')->name('user.email.edit');
Route::put('email', 'UserController@emailUpdate')->name('user.email.update');
Route::get('donnees', 'UserController@data')->name('user.data');
});
});
L’accès à son compte
On va prévoir un accès à l’interface utilisateur à partir de la page d’accueil dans la barre de menu. Dans le layout app on ajoute le lien :@admin
...
@else
<li class="nav-item">
<a class="nav-link" href="{{ route('user.index') }}">Mon compte</a>
</li>
@endadmin
UserController
On doit créer la méthode index dans le contrôleur pour récupérer les données et les envoyer sur la vue de l’accueil :public function index(Request $request)
{
$ads = $this->adRepository->getByUser($request->user());
$adAttenteCount = $this->adRepository->noActiveCount($ads);
$adActivesCount = $this->adRepository->activeCount($ads);
$adPerimesCount = $this->adRepository->obsoleteCount($ads);
return view('user.index', compact('adActivesCount', 'adPerimesCount', 'adAttenteCount'));
}
Est-ce qu'on a déjà toutes les méthodes dans le repository ? On voit qu'il nous manque getByUser et activeCount.
AdRepository
On va donc créer ces deux méthodes- pour aller chercher les annonces de l'utilisateur (getByUser)
- pour connaître le nombre d'annonces actives (activeCount)
public function activeCount($ads)
{
return $ads->where('active', true)->where('limit', '>=', Carbon::now())->count();
}
public function getByUser($user)
{
return $user->ads()->get();
}
Layout user
On crée un layout pour les pages des utilisateurs : Avec ce code :@include('layouts.back-head')
<body id="page-top">
<!-- Page Wrapper -->
<div id="wrapper">
<!-- Sidebar -->
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
<!-- Sidebar - Brand -->
<a class="sidebar-brand d-flex align-items-center justify-content-center" href="{{ url('/') }}">
<div class="sidebar-brand-icon rotate-n-15">
<i class="fab fa-earlybirds fa-2x"></i>
</div>
<div class="sidebar-brand-text mx-3">Annonces</div>
</a>
<!-- Divider -->
<hr class="sidebar-divider my-0">
<!-- Nav Item - Dashboard -->
<li class="nav-item @if(request()->route()->getName() == 'user.index') active @endif">
<a class="nav-link" href="{{ route('user.index') }}">
<i class="fas fa-fw fa-tachometer-alt"></i>
<span>Panneau</span></a>
</li>
<!-- Divider -->
<hr class="sidebar-divider">
<!-- Heading -->
<div class="sidebar-heading">
Annonces
</div>
<li class="nav-item @if(request()->route()->getName() == 'user.actives') active @endif">
<a class="nav-link" href="{{ route('user.actives') }}">
<i class="fas fa-fw fa-hiking"></i>
<span>Actives</span></a>
</li>
<li class="nav-item @if(request()->route()->getName() == 'user.attente') active @endif">
<a class="nav-link" href="{{ route('user.attente') }}">
<i class="fas fa-fw fa-hourglass-start"></i>
<span>En attente</span></a>
</li>
<li class="nav-item @if(request()->route()->getName() == 'user.obsoletes') active @endif">
<a class="nav-link" href="{{ route('user.obsoletes') }}">
<i class="fas fa-fw fa-hourglass-end"></i>
<span>Obsolètes</span></a>
</li>
<!-- Divider -->
<hr class="sidebar-divider">
<!-- Heading -->
<div class="sidebar-heading">
Profil
</div>
<li class="nav-item @if(request()->route()->getName() == 'user.email.edit') active @endif">
<a class="nav-link" href="{{ route('user.email.edit') }}">
<i class="fas fa-fw fa-at"></i>
<span>Email</span></a>
</li>
<li class="nav-item @if(request()->route()->getName() == 'user.data') active @endif">
<a class="nav-link" href="{{ route('user.data') }}">
<i class="fas fa-fw fa-database"></i>
<span>Mes données</span></a>
</li>
<!-- Divider -->
<hr class="sidebar-divider d-none d-md-block">
<!-- Sidebar Toggler (Sidebar) -->
<div class="text-center d-none d-md-inline">
<button class="rounded-circle border-0" id="sidebarToggle"></button>
</div>
</ul>
<!-- End of Sidebar -->
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column">
<!-- Main Content -->
<div id="content">
<!-- Topbar -->
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
<!-- Sidebar Toggle (Topbar) -->
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
<i class="fa fa-bars"></i>
</button>
<!-- Topbar Navbar -->
<ul class="navbar-nav ml-auto">
<!-- Nav Item - User Information -->
<li class="nav-item dropdown no-arrow">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="d-lg-inline text-gray-600 small">{{ auth()->user()->name }}</span>
</a>
<!-- Dropdown - User Information -->
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="userDropdown">
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{{ route('logout') }}"
onclick="event.preventDefault(); document.getElementById('logout-form').submit();">
<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
Déconnexion
</a>
<form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
@csrf
</form>
</div>
</li>
</ul>
</nav>
<!-- End of Topbar -->
<!-- Begin Page Content -->
<div class="container-fluid">
@yield('content')
</div>
<!-- /.container-fluid -->
</div>
<!-- End of Main Content -->
<!-- Footer -->
<footer class="sticky-footer bg-white">
<div class="container my-auto">
<div class="copyright text-center my-auto">
<span>Copyright © Annonces 2019</span>
</div>
</div>
</footer>
<!-- End of Footer -->
</div>
<!-- End of Content Wrapper -->
</div>
<!-- End of Page Wrapper -->
@include('layouts.back-footer')
On inclut des vues pour l'entête et le bas de page qu'on a déjà créées pour l'adminstration.
Vue user.index
Il ne nous manque plus que la vue pour l'accueil : Avec ce code :@extends('layouts.user')
@section('content')
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">Tableau de bord</h1>
</div>
<!-- Content Row -->
<div class="row">
<div class="col-xl-4 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">Annonces actives</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ $adActivesCount }}</div>
</div>
<div class="col-auto">
<i class="fas fa-hiking fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">Annonces en attente</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ $adAttenteCount }}</div>
</div>
<div class="col-auto">
<i class="fas fa-hourglass-start fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-md-6 mb-4">
<div class="card border-left-danger shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">Annonces obsolètes</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ $adPerimesCount }}</div>
</div>
<div class="col-auto">
<i class="fas fa-hourglass-end fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
@endsection
Et on arrive enfin dans l'accueil de l'utilisateur :
Redirection après connexion
Tant qu'on y est on va faire en sorte que quand un utilisateur se connecte il arrive directement dans son interface personnelle. Et pour faire bonne mesure on va aussi s'arranger pour que les administrateurs arrivent eux aussi directement sur la page d'administration. POur faire ça il faut jouer avec la redirection après connexion.
Dans le contrôleur LoginController ajoutez cette méthode :public function redirectTo()
{
return auth()->user()->admin ? route('admin.index') : route('user.index');
}
Et maintenant la redirection fonctionne comme on le voulait !
Les annonces actives
Traitons maintenant le cas des annonces actives. L'utilisateur doit pouvoir :- voir son annonce
- modifier son annonce (uniquement le titre et le texte pour simplifier)
- prolonger son annonce d'une semaine
- supprimer son annonce
AdRepository
Dans le repository on crée la méthode active qui va nous donnée les annonces actives paginées pour l'utilisateur en cours :
public function active($user, $nbr)
{
return $user->ads()->whereActive(true)->where('limit', '>=', Carbon::now())->paginate($nbr);
}
UserController
Dans le contrôleur on utilise cette méthode et on envoie les données à la vue :public function actives(Request $request)
{
$ads = $this->adRepository->active($request->user(), 5);
return view('user.actives', compact('ads'));
}
Vue user.actives
On crée enfin la vue : Avec ce code :@extends('layouts.user')
@section('content')
@include('partials.alerts', ['title' => 'Annonces actives'])
@include('partials.table-add-del-view', ['edit' => true])
@endsection
@section('script')
@include('partials.script-add-del-view')
@endsection
Et par la magie des vues partielles on récupère tout le code qu'on a déjà écrit !
Et du coup on a déjà 3 commandes qui fonctionnent : voir une annonce, prolonger une annonce, supprimer une annonce.
Par contre il nous faut ajouter la modification d'une annonce. Comme on avait créé un contrôleur de ressource (AdController) avec les routes associées on a déjà une partie de l'intendance en place.
Modifier une annonce
AdController
Dans le contrôleur on complète les deux méthodes :use App\Http\Requests\ { AdStore, AdUpdate };
...
public function edit(Ad $ad)
{
$this->authorize('manage', $ad);
return view('edit', compact('ad'));
}
public function update(AdUpdate $request, Ad $ad)
{
$this->authorize('manage', $ad);
$this->adRepository->update($ad, $request->all());
$request->session()->flash('status', "L'annonce a bien été modifiée.");
return back();
}
AdRepository
Dans le repository on ajoute la méthode update :public function update($ad, $data)
{
$ad->update($data);
}
Form request AdUpdate
Comme on va avoir une validation on ajoute une Form Request :php artisan make:request AdUpdate
Avec ce code :
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class AdUpdate extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'title' => ['required', 'string', 'max:100'],
'texte' => ['required', 'string', 'max:1000'],
];
}
}
La vue edit
On crée la vue pour le formulaire de modification :@extends('layouts.app')
@section('content')
<div class="container">
<div class="card bg-light">
<h5 class="card-header">Votre annonce</h5>
<div class="card-body">
<div class="alert alert-warning alert-dismissible fade show" role="alert">
Vous ne pouvez modifier que le titre et le texte de votre annonce.
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
@if(session()->has('status'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ session('status') }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
@endif
<form method="POST" action="{{ route('annonces.update', $ad->id) }}">
@method('PUT')
@csrf
@include('partials.form-group', [
'title' => 'Titre',
'type' => 'text',
'name' => 'title',
'value' => $ad->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 : $ad->texte) }}</textarea>
@if ($errors->has('texte'))
<div class="invalid-feedback">
{{ $errors->first('texte') }}
</div>
@endif
</div>
<br>
<button type="submit" class="btn btn-primary">Valider</button>
</form>
</div>
</div>
</div>
@endsection
On prévoit un petit message pour préciser qu'on ne peut changer que le titre et le texte de l'annonce.
La validation doit fonctionner : Si tout se passe bien on affiche l'alerte :Les annonces en attente de modération
Traitons maintenant le cas des annonces en attente . L'utilisateur doit pouvoir :- voir son annonce
- supprimer son annonce
AdRepository
Dans le repository on crée la méthode attente qui va nous donnée les annonces pas encore modérées pur l'utilisateur en cours :
public function attente($user, $nbr)
{
return $user->ads()->whereActive(false)->paginate($nbr);
}
UserController
Dans le contrôleur on utilise cette méthode et on envoie les données à la vue :public function attente(Request $request)
{
$ads = $this->adRepository->attente($request->user(), 5);
return view('user.waiting', compact('ads'));
}
Vue user.waiting
On crée enfin la vue :@extends('layouts.user')
@section('content')
@include('partials.alerts', ['title' => 'Annonces en attente'])
@include('partials.table-add-del-view', ['noAdd' => true])
@endsection
@section('script')
@include('partials.script-add-del-view')
@endsection
Là aussi on bénéficie de tout le code écrit précédemment...
Et on a déjà tout le code pour le fonctionnement !
Les annonces obsolètes
Traitons maintenant le cas des annonces en attente . L'utilisateur doit pouvoir :- voir son annonce
- ajouter une semaine de délai
- supprimer son annonce
AdRepository
Dans le repository on crée la méthode obsoleteForUser qui va nous donnée les annonces qui ont dépassé la date de validité :
public function obsoleteForUser($user, $nbr)
{
return $user->ads()->where('limit', '<', Carbon::now())->latest('limit')->paginate($nbr);
}
UserController
Dans le contrôleur on utilise cette méthode et on envoie les données à la vue :public function obsoletes(Request $request)
{
$ads = $this->adRepository->obsoleteForUser($request->user(), 5);
return view('user.obsoletes', compact('ads'));
}
Vue user.obsoletes
On crée enfin la vue :@extends('layouts.user')
@section('content')
@include('partials.alerts', ['title' => 'Annonces obsolètes'])
@include('partials.table-add-del-view')
@endsection
@section('script')
@include('partials.script-add-del-view')
@endsection
Et là aussi on a déjà tout le code pour le fonctionnement !
Changement de l'email
L'utilisateur doit pouvoir modifier son email. On va lui proposer un formulaire pour le faire.
UserController
Dans le contrôleur on crée les deux méthodes ;use App\Http\Requests\ { MessageAd, EmailUpdate };
...
public function emailEdit()
{
return view('user.email');
}
public function emailUpdate(EmailUpdate $request)
{
auth()->user()->email = $request->email;
auth()->user()->save();
$request->session()->flash('status', "Votre email a bien été mis à jour.");
return back();
}
Form request EmailUpdate
Comme on va avoir une validation on ajoute une Form Request :php artisan make:request EmailUpdate
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class EmailUpdate extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'email' => [
'required', 'string', 'email', 'max:255',
Rule::unique('users')->ignore(auth()->id()),
],
];
}
}
La vue user.email
On crée la vue pour le formulaire de modification :@extends('layouts.user')
@section('content')
<div class="container">
<div class="card">
<h5 class="card-header">Vous pouvez modifier votre email ici</h5>
<div class="card-body">
@if(session()->has('status'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ session('status') }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
@endif
<form method="POST" action="{{ route('user.email.update') }}">
@method('PUT')
@csrf
@include('partials.form-group', [
'title' => '',
'type' => 'text',
'name' => 'email',
'value' => auth()->user()->email,
'required' => true,
])
<br>
<button type="submit" class="btn btn-primary">Valider</button>
</form>
</div>
</div>
</div>
@endsection
On a un traitement classique sur lequel je ne m'étends pas.
Les données personnelles
Le RGPD oblige à fournir à un utilisateur, s'il le désire, toutes les données le concernant. On va le faire ici même s'il y a peu de données.
UserController
On code la méthode data dans le contrôleur :public function data()
{
$user = auth()->user();
return view('user.data', compact('user'));
}
Vue user.data
On crée la vue :@extends('layouts.user')
@section('content')
<div class="container">
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">Vos données personnelles</h1>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title">A propos</h5>
<table class="table">
<tbody>
<tr>
<td>Rapport généré pour</td>
<td>{{ $user->name }}</td>
</tr>
<tr>
<td>Pour le site</td>
<td>Annonces</td>
</tr>
<tr>
<td>A l'url</td>
<td>annonces.oo</td>
</tr>
<tr>
<td>Le</td>
<td>{{ \Carbon\Carbon::now()->format('d-m-Y') }}</td>
</tr>
</tbody>
</table>
<em>Vous pouvez enregistrer cette page pour conserver vos données en utilisant le menu de votre navigateur.</em>
</div>
</div>
<br>
<div class="card">
<div class="card-body">
<h5 class="card-title">Utilisateur</h5>
<table class="table">
<tbody>
<tr>
<td>ID</td>
<td>{{ $user->id }}</td>
</tr>
<tr>
<td>Nom de connexion</td>
<td>{{ $user->name }}</td>
</tr>
<tr>
<td>Email</td>
<td>{{ $user->email }}</td>
</tr>
<tr>
<td>Date d'inscription</td>
<td>{{ $user->created_at->format('d-m-Y') }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
@endsection
On est ainsi en règle ! Enfin presque...
Les pages légales
Si vous ne savez pas ce que sont les mentions légales je vous coneille un petit tour sur le site officiel. On a prévu des liens en bas de la page d'accueil de notre site pour les mentions légales :Routes
On ajoute les routes :Route::view('legal', 'legal')->name('legal');
Route::view('confidentialite', 'confidentialite')->name('confidentialite');
Layout app
On complète les liens dans le layout :<nav class="navbar navbar-expand fixed-bottom navbar-dark">
<div class="navbar-nav ml-auto">
<a class="nav-item nav-link " href="{{ route('legal') }}">Mentions légales</a>
<a class="nav-item nav-link " href="{{ route('confidentialite') }}">Politique de confidentialité</a>
</div>
</nav>
Vue legal
@extends('layouts.app')
@section('content')
<div class="container">
<div class="card text-white bg-secondary">
<div class="card-body">
<h5 class="card-title">Mentions légales</h5>
<table class="table text-white">
<tbody>
<tr>
<td>Raison sociale</td>
<td>Association des petits dénicheurs</td>
</tr>
<tr>
<td>Email</td>
<td>cestici@parla.fr</td>
</tr>
<tr>
<td>Téléphone</td>
<td>00 00 00 00 00</td>
</tr>
<tr>
<td>Directeur de publication</td>
<td>Monsieur Durand François</td>
</tr>
<tr>
<td>Hébergeur du site</td>
<td>Je stocke tout à Petaouchnoc, téléphone 00 00 00 00 00</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
@endsection
Politique de confidentialité
Le RGPD régit désormais la gesion des données personnelles. Là aussi si vous avez des lacunes je vous conseile d'aller sur le site de la CNIL. Ils ont d'ailleurs mis en place un atelier très bien pensé sur le sujet.
Vue confidentialite
@extends('layouts.app')
@section('content')
<div class="container">
<div class="card text-white bg-secondary">
<div class="card-body">
<h5 class="card-title">Politique de confidentialité</h5>
<div class="card-body">
<p>Ici le long texte que personne ne lit mais que vous devez obligatoirement prévoir si vous recueillez des données utilisateur et qui doit être conforme au RGPD.</p>
<p>Pour mémoire une adresse email est considrée comme une donnée personnelle au regard du RGPD.</p>
</div>
</div>
</div>
</div>
@endsection
Conclusion
On arrive au bout de ce projet avec un site maintenant parfaitement fonctionnel. Je l'ai voulu suffisamment élaboré pour être réaliste sans le rendre toutefois trop chargé, un équilibre pas toujours facile à tenir. D'autre part je suis conscient de ne pas avoir tout détaillé et d'être même parfois passé assez rapidement sur certaines parties. Sinon le nombre d'articles aurait été trop important ! Je peux évidemment revenir sur certains points si certains le désirent. Bonne lecture !
Par bestmomo
Nombre de commentaires : 30