Blog / Créer une page d’erreur personnalisée avec Laravel et Inertia

Image used for article Créer une page d’erreur personnalisée avec Laravel et Inertia

Créer une page d’erreur personnalisée avec Laravel et Inertia




TL;DR: Comment créer un module de feedback dans un projet Laravel et recevoir une notification Discord dès qu’un message est soumis.




Vous trouverez un projet Laravel d’exemple sur ce Github Repository. Découvrez-en plus sur CapsulesX ou Bluesky.




Il est fréquent de rencontrer sur un site internet un formulaire de contact ou une adresse e-mail permettant de contacter l'administrateur du site. Ces formulaires requièrent généralement une adresse e-mail, un titre et un objet. Cet article propose une alternative plus ouverte à l'anonymat, en remplacement de ce format standard. En se servant de Discord.




Un bouton donne accès à un formulaire comprenant un champ de feedback et, en option, un champ pour une adresse e-mail si une réponse au message est souhaitée. Lors de l'envoi, une notification Discord est automatiquement générée pour informer l'administrateur. Aucun e-mail n'est produit, et aucune donnée n'est enregistrée dans une base de données.




Initialement, une seule route et une seule page sont configurées dans notre projet Laravel vierge.



routes/web.php


<?php

use Illuminate\Support\Facades\Route;
use Inertia\Inertia;


Route::get( '/', fn() => Inertia::render( 'Welcome' ) );



/resources/js/pages/Welcome.vue


<script setup>

import logotype from '/public/assets/capsules-logotype-background.svg';

</script>

<template>

    <div class="w-screen h-screen flex flex-col items-center justify-center text-center bg-primary-white">

        <img class="w-24 h-24" v-bind:src="logotype">

        <h1 class="mt-4 text-6xl font-bold select-none header-mode" v-text="'Capsules Codes'" />

    </div>

</template>







Le composant de feedback peut être entièrement contenu dans un fichier Vue. La structure HTML comprend un bouton et un formulaire. Voici le contenu du module.



resources/js/components/Feedback.vue


<script setup>

import { ref } from 'vue';
import { router } from '@inertiajs/vue3';
import logotype from '/public/assets/capsules-logotype.svg';

const isOpen = ref( false );
const isSent = ref( false );
const errors = ref( {} );

const message = ref( '' );
const email = ref( '' );

function toggle()
{
    if( ! isOpen.value )
    {
        message.value = '';
        email.value = '';

        isSent.value = false;
        errors.value = {};
    }

    isOpen.value = ! isOpen.value;
}

function submit()
{
    errors.value = {};

    const data = email.value ? { email : email.value, message : message.value } : { message : message.value };

    router.post( '/feedbacks', data, { onError : error => { errors.value = error; }, onSuccess : () => { isSent.value = true; } } );
}

</script>

<template>

    <div class="m-8 flex flex-col-reverse items-end space-y-reverse space-y-4">

        <button class="w-12 h-12 flex items-center justify-center" v-on:click="toggle()">

            <div v-show="! isOpen" class="w-full h-full rounded-xl bg-white flex items-center justify-center drop-shadow-2xl hover:bg-primary-blue hover:bg-opacity-5"><img class="h-8 w-8" v-bind:src="logotype"></div>

            <div v-show="! isOpen" class="absolute top-0 left-0 w-full h-full rounded-xl bg-white flex items-center justify-center animate-ping opacity-50"><img class="h-8 w-8" v-bind:src="logotype"></div>

            <svg v-show="isOpen" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-primary-blue"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>

        </button>

        <div v-if="isOpen">

            <div v-if="! isSent" class="font-mono rounded-xl bg-white drop-shadow-xl ">

                <div class="p-2">

                    <form class="flex flex-col" v-on:submit.prevent="submit()">

                        <label for="message" hidden />

                        <textarea
                            id="message"
                            class="mb-2 p-2 outline-none rounded-md resize-none text-xs bg-slate-100"
                            v-bind:class="{ 'border border-solid border-red-500 text-red-500' : errors && errors[ 'message' ] } "
                            type="text"
                            cols="30"
                            rows="10"
                            v-bind:placeholder="'Your message'"
                            v-model="message"
                        />

                        <div class="flex">

                            <label for="email" hidden />

                            <input
                                id="email"
                                class="px-2 grow outline-none rounded-md text-xs bg-slate-100"
                                v-bind:class=" { 'border border-solid border-red-500 text-red-500' : errors && errors[ 'mail' ] } "
                                type="text"
                                v-bind:placeholder="'Your email - Optional'"
                                v-model="email"
                            >

                            <button
                                class="ml-2 px-4 py-2 inline-flex items-center rounded-md text-sm font-medium text-primary-blue bg-primary-blue bg-opacity-50 hover:bg-opacity-60"
                                type="submit"
                            >

                                <p v-text="'Send'" />

                            </button>

                        </div>

                    </form>

                    <div>

                        <p v-for=" ( error, key ) in errors " v-bind:key="key" class="first:mt-4 ml-1 text-[10px] text-red-500" v-text="error" />

                    </div>

                </div>

            </div>

            <div v-else class="font-mono p-4 flex items-center justify-center space-x-4 bg-white rounded-xl drop-shadow-xl">

                <p class="w-full text-center text-xs text-primary-black" v-text="'Thank you for your feedback !'" />

                <p v-text="'🎉'" />

                <img class="h-8 w-8" v-bind:src="logotype">

            </div>

        </div>

    </div>

</template>




Ce composant représente un bouton qui, lorsqu'il est cliqué, fait apparaître un formulaire grâce à la variable isOpen. Lorsqu'on clique sur le bouton 'Send', la méthode submit() est appelée, envoyant une requête POST à la route /feedbacks. Si tout est en ordre, la variable isSent devient vraie, et un message de remerciement remplace le formulaire. Sinon, les champs incorrects sont mis en rouge.




Il faut maintenant ajouter ce composant à la page Welcome.



resources/js/pages/Welcome.vue


<script setup>

import Feedback from '/resources/js/components/Feedback.vue';
import logotype from '/public/assets/capsules-logotype-background.svg';

</script>

<template>

    <Feedback class="fixed z-10 bottom-0 right-0" />

    <div class="w-screen h-screen flex flex-col items-center justify-center text-center bg-primary-white">

        <img class="w-24 h-24" v-bind:src="logotype">

        <h1 class="mt-4 text-6xl font-bold select-none header-mode" v-text="'Capsules Codes'" />

    </div>

</template>







Le composant Feedback est importé et fixé en bas à droite de l'écran. Maintenant que le module fonctionne côté client, il est temps de créer la route, d'implémenter la validation et d'envoyer les données vers Discord. Pour cet article, il n'est pas nécessaire de créer un contrôleur spécifique.



app/Http/Requests/FeedbackRequest.php


<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;


class FeedbackRequest extends FormRequest
{
    public function rules() : array
    {
        return [
            'message' => [ 'required', 'min:1', 'max:499' ],
            'email' => [ 'sometimes', 'email' ],
        ];
    }
}




Le FeedbackRequest permet de renvoyer des erreurs si une donnée n'a pas été correctement envoyée.







routes/web.php


<?php

use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use App\Http\Requests\FeedbackRequest;


Route::get( '/', fn() => Inertia::render( 'Welcome' ) );

Route::post( 'feedbacks', function( FeedbackRequest $request ){} );







La prochaine étape consiste à connecter le projet Laravel à son serveur Discord. À cette fin, un webhook doit être créé. Aller dans les paramètres du serveur Discord > Intégrations > Consulter les webhooks > Nouveau webhook. Il faut alors lui donner un nom et choisir un canal.




Le webhook est alors disponible et son URL est copiable via le bouton Copier l'URL du webhook.




Ce webhook est à ajouter dans la variable d’environnement LOG_DISCORD_WEBHOOK_URL, celle-ci est accessible dans le fichier de configration logging.



config/logging.php


<?php

return [

    'channels' => [

				'discord' => [
		
				    'driver' => 'discord',
				    'url' => env( 'LOG_DISCORD_WEBHOOK_URL' )
		
				]

    ]

];



.env.example


LOG_DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/{webhook-key}




La notification peut maintenant être envoyée depuis la route /feedbacks



routes/web.php


<?php

use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use App\Http\Requests\FeedbackRequest;
use Illuminate\Support\Facades\Notification;
use App\Notifications\FeedbackReceived;


Route::get( '/', fn() => Inertia::render( 'Welcome' ) );

Route::post( 'feedbacks', fn( FeedbackRequest $request ) => Notification::route( 'discord', config( 'logging.channels.discord.url' ) )->notify( new FeedbackReceived( $request ) ) );




Il ne manque plus qu’à construire la notification FeedbackReceived.



app/Notifications/FeedbackReceived.php


<?php

namespace App\Notifications;

use Illuminate\Notifications\Notification;
use App\Http\Requests\FeedbackRequest;
use App\Notifications\Discord\DiscordChannel;
use App\Notifications\Discord\DiscordMessage;


class FeedbackReceived extends Notification
{
    private FeedbackRequest $request;

    public function __construct( FeedbackRequest $request )
    {
        $this->request = $request;
    }

    public function via() : string
    {
        return DiscordChannel::class;
    }

    public function toDiscord() : DiscordMessage
    {
        $email = $this->request->email ?? 'Anonymous';

        return ( new DiscordMessage() )->content( "New Capsules Codes Feedback : \"{$this->request->message}\" by {$email}" );
    }
}



app/Notifications/Discord/DiscordChannel.php


<?php

namespace App\Notifications\Discord;

use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Http;


class DiscordChannel
{
    public function send( object $notifiable, Notification $notification ) : void
    {
        $discordMessage = $notification->toDiscord();

        $discordWebhook = $notifiable->routeNotificationFor( 'discord' );

        Http::post( $discordWebhook, $discordMessage->toArray() );
    }
}



app/Notifications/Discord/DiscordMessage.php


<?php

namespace App\Notifications\Discord;

use Carbon\Carbon;


class DiscordMessage
{
    protected string $content = '';

    public function content( string $content ) : self
    {
        $this->content = $content;

        return $this;
    }

    public function toArray() : array
    {
        return [
            "embeds" => [

                [
                    "title" => $this->content,
                    "type" => "rich",
                    "timestamp" => Carbon::now(),
                    "color" => "14497651"
                ]

            ]
        ];
    }
}







Une notification sauvage apparaît !




Ravi d’avoir pu aider !

v1.4.0

Icône XIcône Github