Objectif

Créer un blog minimaliste avec Blade : layout partagé, composants réutilisables (carte article, alerte, navigation), formulaire de création avec validation, et affichage des erreurs.

ConceptMis en pratique
Layout@extends / @section / @yield
Composantx-alert, x-article-card, x-nav-link
Directives@forelse, @auth, @can, @error
Formulaire@csrf, old(), validation + messages
Stack@push('scripts') sur la page d'édition

Consignes

  1. Créer resources/views/layouts/app.blade.php avec @stack('styles') et @stack('scripts')
  2. Créer le composant x-alert avec prop type (success/error/info) et couleurs correspondantes
  3. Créer le composant x-article-card acceptant :article="$article"
  4. Page /articles : @forelse avec message si vide, flash success après création
  5. Page /articles/create : formulaire avec old() + @error sur chaque champ
  6. Utiliser @auth pour afficher le bouton "Créer" et @can pour "Modifier/Supprimer"
Astuce : utilise session('success') dans le layout pour afficher automatiquement les messages flash sur toutes les pages.

Structure attendue

resources/views/
├── layouts/
│   └── app.blade.php          # Layout principal
├── components/
│   ├── alert.blade.php        # Composant alerte
│   ├── article-card.blade.php # Carte article
│   └── nav-link.blade.php     # Lien de navigation actif
├── articles/
│   ├── index.blade.php        # Liste des articles
│   ├── show.blade.php         # Détail
│   ├── create.blade.php       # Formulaire création
│   └── edit.blade.php         # Formulaire édition
└── partials/
    └── navbar.blade.php       # Navigation

Solution commentée

{{-- layouts/app.blade.php --}}
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>@yield('title', 'Blog Laravel') — MonBlog</title>
    <link rel="stylesheet" href="/css/app.css">
    @stack('styles')
</head>
<body class="bg-gray-50">
    @include('partials.navbar')

    <main class="container mx-auto py-8 px-4">
        {{-- Flash messages globaux --}}
        @if(session('success'))
            <x-alert type="success">{{ session('success') }}</x-alert>
        @endif
        @if(session('error'))
            <x-alert type="error">{{ session('error') }}</x-alert>
        @endif

        @yield('content')
    </main>

    <script src="/js/app.js"></script>
    @stack('scripts')
</body>
</html>
{{-- components/alert.blade.php --}}
@props(['type' => 'info', 'dismissible' => false])

@php
$styles = match($type) {
    'success' => 'bg-green-50 border-green-400 text-green-800',
    'error'   => 'bg-red-50 border-red-400 text-red-800',
    'warning' => 'bg-yellow-50 border-yellow-400 text-yellow-800',
    default   => 'bg-blue-50 border-blue-400 text-blue-800',
};
$icon = match($type) {
    'success' => '✅',
    'error'   => '❌',
    'warning' => '⚠️',
    default   => 'ℹ️',
};
@endphp

<div {{ $attributes->merge(['class' => "border-l-4 p-4 mb-4 rounded {$styles}"]) }}
     role="alert" x-data x-show="true">
    <div class="flex justify-between items-start">
        <span>{{ $icon }} {{ $slot }}</span>
        @if($dismissible)
            <button @click="$el.closest('[role=alert]').remove()"
                    class="ml-4 font-bold opacity-70 hover:opacity-100">✕</button>
        @endif
    </div>
</div>
{{-- components/article-card.blade.php --}}
@props(['article'])

<div class="bg-white rounded-lg shadow p-6 hover:shadow-md transition">
    <div class="flex justify-between items-start">
        <h3 class="text-lg font-semibold">
            <a href="{{ route('articles.show', $article) }}"
               class="hover:text-red-600">
                {{ $article->title }}
            </a>
        </h3>
        <span class="text-xs px-2 py-1 rounded
            {{ $article->status === 'published' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600' }}">
            {{ $article->status }}
        </span>
    </div>

    <p class="text-gray-600 mt-2 text-sm">{{ $article->excerpt }}</p>

    <div class="mt-4 flex items-center justify-between text-sm text-gray-500">
        <span>Par {{ $article->author->name }}</span>
        <span>{{ $article->created_at->diffForHumans() }}</span>
    </div>

    @can('update', $article)
    <div class="mt-4 flex gap-2">
        <a href="{{ route('articles.edit', $article) }}"
           class="text-sm text-blue-600 hover:underline">Modifier</a>
        <form action="{{ route('articles.destroy', $article) }}" method="POST"
              onsubmit="return confirm('Supprimer ?')">
            @csrf @method('DELETE')
            <button class="text-sm text-red-600 hover:underline">Supprimer</button>
        </form>
    </div>
    @endcan
</div>
{{-- articles/index.blade.php --}}
@extends('layouts.app')

@section('title', 'Tous les articles')

@section('content')
    <div class="flex justify-between items-center mb-6">
        <h1 class="text-2xl font-bold">Articles</h1>
        @auth
            <a href="{{ route('articles.create') }}"
               class="btn btn-laravel">+ Nouvel article</a>
        @endauth
    </div>

    @forelse($articles as $article)
        <x-article-card :article="$article" class="mb-4" />
    @empty
        <x-alert type="info">
            Aucun article pour le moment.
            @auth Soyez le premier à <a href="{{ route('articles.create') }}">publier</a> ! @endauth
        </x-alert>
    @endforelse

    {{-- Pagination --}}
    <div class="mt-6">{{ $articles->links() }}</div>
@endsection
{{-- articles/create.blade.php --}}
@extends('layouts.app')

@section('title', 'Créer un article')

@section('content')
    <div class="max-w-2xl mx-auto">
        <h1 class="text-2xl font-bold mb-6">Créer un article</h1>

        <form action="{{ route('articles.store') }}" method="POST" class="space-y-4">
            @csrf

            <div>
                <label class="block text-sm font-medium mb-1">Titre *</label>
                <input type="text" name="title"
                       value="{{ old('title') }}"
                       class="w-full border rounded px-3 py-2
                              @error('title') border-red-500 @enderror">
                @error('title')
                    <p class="text-red-600 text-sm mt-1">{{ $message }}</p>
                @enderror
            </div>

            <div>
                <label class="block text-sm font-medium mb-1">Contenu *</label>
                <textarea name="body" rows="8"
                          class="w-full border rounded px-3 py-2
                                 @error('body') border-red-500 @enderror">
                    {{ old('body') }}
                </textarea>
                @error('body')
                    <p class="text-red-600 text-sm mt-1">{{ $message }}</p>
                @enderror
            </div>

            <div>
                <label class="block text-sm font-medium mb-1">Statut</label>
                <select name="status" class="border rounded px-3 py-2">
                    @foreach(['draft' => 'Brouillon', 'published' => 'Publié'] as $value => $label)
                        <option value="{{ $value }}"
                            {{ old('status', 'draft') === $value ? 'selected' : '' }}>
                            {{ $label }}
                        </option>
                    @endforeach
                </select>
            </div>

            <div class="flex gap-3">
                <button type="submit" class="btn btn-laravel">Créer</button>
                <a href="{{ route('articles.index') }}" class="btn">Annuler</a>
            </div>
        </form>
    </div>
@endsection

@push('scripts')
    <script>
        // Confirmation avant de quitter si le formulaire est modifié
        let formDirty = false;
        document.querySelector('form').addEventListener('input', () => formDirty = true);
        window.addEventListener('beforeunload', e => {
            if (formDirty) e.preventDefault();
        });
    </script>
@endpush
← Cours Module 03 🧠 QCM Module 04 →