Collect feedback via Discord notifications in your Laravel project
Tags: vue, laravel, tutorial
Published: 2024-07-17 10:28:21

TL;DR:How to create a feedback module in a Laravel project and receive a Discord notification when a message is submitted.

&nbsp;
&nbsp;

A sample Laravel project can be found on this [**Github Repository**](https://github.com/capsulescodes/articles).

&nbsp;
&nbsp;

Find out more on [**Capsules**](https://capsules.codes/en/blog) or [**X**](https://x.com/capsulescodes).

&nbsp;
&nbsp;

It is common to come across a contact form or an email address on a website, allowing users to contact the site administrator. These forms typically request an email address, a subject, and a title. This article suggests a more open alternative to anonymity, replacing this standard format. By using Discord.

&nbsp;
&nbsp;

A button provides access to a form with a feedback field and, optionally, a field for an email address if a response to the message is desired. Upon submission, a Discord notification is automatically generated to inform the administrator. No email is generated, and no data is stored in a database.

&nbsp;
&nbsp;

Initially, only one route and one page are configured in our blank Laravel project.

&nbsp;
&nbsp;

`routes/web.php`

```php
<?php

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

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

&nbsp;
&nbsp;

`/resources/js/pages/Welcome.vue`

```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>
```

&nbsp;
&nbsp;

![capsules-discord-001.png](https://capsules.codes/storage/canvas/images/wicZVdqXw9lFmQxDyhH8Q89aDL3J3O3hnz9nZhVU.png)

&nbsp;
&nbsp;

The feedback component can be entirely contained in a Vue file. The HTML structure includes a button and a form. Here is the content of the module.

&nbsp;
&nbsp;

`resources/js/components/Feedback.vue`

```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>
```

&nbsp;
&nbsp;

This component represents a button that, when clicked, reveals a form through the `isOpen` variable. When the 'Send' button is clicked, the `submit()` method is called, sending a POST request to the `/feedbacks` route. If everything is in order, the `isSent` variable becomes true, and a thank-you message replaces the form. Otherwise, incorrect fields are highlighted in red.

&nbsp;
&nbsp;

Now, it's time to add this component to the `Welcome` page.

&nbsp;
&nbsp;

`resources/js/pages/Welcome.vue` 

```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>
```

&nbsp;
&nbsp;

![capsules-discord-002.png](https://capsules.codes/storage/canvas/images/cwew8OJF2gANANbfmgT1o1DmsTLLtUkc8UBJwgd3.png)

&nbsp;
&nbsp;

The `Feedback` component is imported and positioned at the bottom right of the screen. 
Now that the module is working on the client side, it's time to create the route, implement validation, and send the data to Discord. For this article, there is no need to create a specific controller.

&nbsp;
&nbsp;

`app/Http/FeedbackRequest.php` 

```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' ],
        ];
    }
}
```

&nbsp;
&nbsp;

The `FeedbackRequest` allows for returning errors if data has not been sent correctly.

&nbsp;
&nbsp;

![capsules-discord-003.png](https://capsules.codes/storage/canvas/images/jHELA6wvUuDIYl7xrFO7hXDi4OkAJMXWAqA965H4.png)

&nbsp;
&nbsp;

`routes/web.php`

```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 ){} );
```

&nbsp;
&nbsp;

![capsules-discord-004.png](https://capsules.codes/storage/canvas/images/DtktQaPB5Pxfoj5fZV31IH3LjUYjxNgUwhL2IFWf.png)

&nbsp;
&nbsp;

The next step is to connect the Laravel project to the Discord workspace. For this purpose, a webhook needs to be created. Go to the Discord Server Settings > Integrations > View webhooks > New Webhook. It needs a name and a channel.

&nbsp;
&nbsp;

The webhook is then available and its URL can be copied via the button `Copy Webhook URL`.  

&nbsp;
&nbsp;

This webhook needs to be added to the `LOG_DISCORD_WEBHOOK_URL` environment variable, which is accessible in the configuration file `config/logging.php`.

&nbsp;
&nbsp;

`config/logging.php`

```php
<?php

return [

    'channels' => [

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

    ]

];
```

&nbsp;
&nbsp;

`.env` 

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

&nbsp;
&nbsp;

The notification can now be sent from the `/feedbacks` route.

&nbsp;
&nbsp;

`routes/web.php`

```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 ) ) );
```

&nbsp;
&nbsp;

All that's left is to create the `FeedbackReceived` notification. 

&nbsp;
&nbsp;

`app/Notifications/FeedbackReceived.php`

```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}" );
    }
}
```

&nbsp;
&nbsp;

`app/Notifications/Discord/DiscordChannel.php`

```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() );
    }
}
```

&nbsp;
&nbsp;

`app/Notifications/Discord/DiscordMessage.php`

```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->title,
                    "type" => "rich",
                    "timestamp" => Carbon::now(),
                    "color" => "14497651"
                ]

            ]
            
        ]; 
    }
}
```

&nbsp;
&nbsp;

![capsules-discord-005.png](https://capsules.codes/storage/canvas/images/s2xqiK6BlaXHnYrTTIzJR8CxB2S3DYP8xyvTiL3U.png)

&nbsp;
&nbsp;

A wild notification appears!

&nbsp;
&nbsp;

Glad this helped.