In this tutorial we are going to build a nice looking notification for InertiaJS admin. Here we are using vue 3. First let us install laravel
composer create-project laravel/laravel .
Let us use SQLite for ease of use. You just have to add this in .env. And don't forget to remove all other DB_ variables in .env
DB_CONNECTION=sqlite
If you prefer any other database feel free to skip this step.
Laravel Jetstream
Laravel Jetstream is a beautifully designed application starter kit for Laravel and provides the perfect starting point for your next Laravel application. Jetstream provides the implementation for your application's login, registration, email verification, two-factor authentication, session management, API via Laravel Sanctum, and optional team management features. These commands will install Jetstream for us.
composer require laravel/jetstream
php artisan jetstream:install inertia
npm install
npm install @heroicons/vue
npm run build
php artisan migrate
And run php artisan serve
in one cli and npm run dev
in another to keep running vite and artisan servers.
Now go to this file database\seeders\DatabaseSeeder.php
and add these code. This will seed two users (admin@admin.com and admin2@admin.com) in users table.
\App\Models\User::factory()->create([
'name' => 'admin',
'email' => 'admin@admin.com',
]);
\App\Models\User::factory()->create([
'name' => 'admin2',
'email' => 'admin2@admin.com',
]);
Now run this command
php artisan migrate:fresh --seed
Now you have two users admin@admin.com and admin2@admin.com in table with password password. you can loigin from one of those accounts.
Now let us build the UI
For UI we are build a notification icon with notification count in the menu. We use 'resources\js\Layouts\AppLayout.vue' file since menu is located there.
Just before <!-- Settings Dropdown --> Add this code.
<div class="relative inline-block cursor-pointer">
<Dropdown align="right" width="96">
<template #trigger>
<BellIcon class="h-7 w-7 text-gray-600" />
<span
class="absolute bottom-3 left-3 flex items-center justify-center h-5 w-5 rounded-full bg-red-600 text-white text-xs"
>
{{ 2 }}
</span>
</template>
<template #content>
<!-- Account Management -->
<div class="block px-4 py-2 text-xs text-gray-400 w-[350px]">
Notifications
</div>
<div class="border-t border-gray-200" />
<div>
<DropdownLink :href="route('dashboard')">
<div class="block text-xs">Title</div>
<div>Notification description</div>
</DropdownLink>
<div class="border-t border-gray-200" />
</div>
<div>
<DropdownLink :href="route('dashboard')">
<div class="block text-xs">Title 2</div>
<div>Notification description 2</div>
</DropdownLink>
<div class="border-t border-gray-200" />
</div>
</template>
</Dropdown>
</div>
And don't forget to import BellIcon like this,
import { BellIcon } from '@heroicons/vue/24/solid'
Now you will see this in browser,
You can see this changes here in github (Notice that branch build_ui is for this section only)
https://github.com/vimuths123/notification/tree/build_ui
Build database table
Now let us create Notification model with migration file
php artisan make:model Notification -m
Add this to the migration file
Schema::create('notifications', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title'); // Title of the notification
$table->text('body'); // Body content of the notification
$table->boolean('read')->default(false); // Status to check if the notification has been read
$table->timestamps(); // Timestamps for created_at and updated_at
});
Now run the migration.
php artisan migrate
Now let us come to the model. Add fillable, cast relevant fields and add relationship to user table.
Please check that cast. It converts true/false values to save in db as 1/0. Cause db does not have a Boolean type. It saves 0 or 1.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Notification extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'user_id',
'title',
'body',
'read'
];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'read' => 'boolean',
];
/**
* Get the user that the notification belongs to.
*
* This method defines an inverse one-to-many relationship with the User model.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
This branch has changes for this
https://github.com/vimuths123/notification/tree/database_changes
Send Notification
In this section we are creating a small ui and implement functionality to save a notification in database. Here we give ability to choose the user to sent the notification.
First add the route.
Route::get('/send_notifications', function () {
$users = User::all();
return Inertia::render('SendNotification', [
'users' => $users
]);
});
Here we have get all the users and passing them to view for using on a dropdown. Here is the view. this is the file path
resources/js/Pages/SendNotification.vue
<template>
<AppLayout title="Send Notification">
<div class="container mx-auto p-4">
<h1 class="text-3xl font-bold text-gray-900">Send a Notification</h1>
<form @submit.prevent="createNotification" class="mt-4">
<div class="mb-4">
<label for="user" class="block text-sm font-medium text-gray-700"
>User</label
>
<select
id="user"
v-model="form.user_id"
class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
>
<option disabled value="">Please select a user</option>
<option v-for="user in users" :key="user.id" :value="user.id">
{{ user.name }}
</option>
</select>
</div>
<div class="mb-4">
<label for="title" class="block text-sm font-medium text-gray-700"
>Title</label
>
<input
type="text"
id="title"
v-model="form.title"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-500 focus:ring-opacity-50"
placeholder="Notification title"
/>
</div>
<div class="mb-6">
<label for="body" class="block text-sm font-medium text-gray-700"
>Body</label
>
<textarea
id="body"
v-model="form.body"
rows="3"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-500 focus:ring-opacity-50"
placeholder="Notification message"
></textarea>
</div>
<button
type="submit"
class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-bold rounded-md"
>
Send Notification
</button>
</form>
</div>
</AppLayout>
</template>
<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import { useForm } from "@inertiajs/vue3";
const props = defineProps(["users"]);
const form = useForm({
user_id: "",
title: "",
body: "",
});
const createNotification = () =>
form.post(route("send_notifications"), {
preserveScroll: true,
onSuccess: () => form.reset(),
});
</script>
Here we are filling vue js form and sending data to backend. So let's create backend functionality,
Route::post('/send_notifications', function (Request $request) {
$notification = Notification::create([
'user_id' => $request->input('user_id'),
'title' => $request->input('title'),
'body' => $request->input('body')
]);
return redirect()->back()->banner('Notification added.');
})->name('send_notifications');
Now you should be able to send a notification to db using UI. Here is the code up to this,
https://github.com/vimuths123/notification/tree/save_notification
Push the notification to Pusher
Now let's push the notification to pusher. First you have to go to
https://pusher.com/ login create an app an get API keys. Then fill them in your .env file
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
And don't forget to change this to pusher
BROADCAST_DRIVER=pusher
Then lets create the event.
php artisan make:event NotificationCreated
This is our event. Here we have implements it from ShouldBroadcast and passed the notification object for broadcasting. Also we are broadcasting from a private channel for the time.
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class NotificationCreated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $notification;
/**
* Create a new event instance.
*/
public function __construct($notification)
{
$this->notification = $notification;
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new Channel('notifications'),
];
}
}
Let's call the event and pass the notification object now inside routes,
Route::post('/send_notifications', function (Request $request) {
$notification = Notification::create([
'user_id' => $request->input('user_id'),
'title' => $request->input('title'),
'body' => $request->input('body')
]);
event(new NotificationCreated($notification));
return redirect()->back()->banner('Notification added.');
})->name('send_notifications');
Then install pusher with this command
composer require pusher/pusher-php-server
Now after adding a notification when you goes to pusher dashboard you will see this
Listen to the notification
Echo
When an event is fired on the server, it's broadcasted over a channel. Clients subscribed to that channel through Laravel Echo can listen for these events in real-time and take actions, like updating the UI immediately without a page refresh.
Now let us listen to the notification using echo. First we need to install it using npm
npm install laravel-echo pusher-js
Now go to this file, resources\js\bootstrap.js
And uncomment these lines
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1',
wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});
Then we need to add listen on our page. Add this to the bottom of script tag of resources\js\Layouts\AppLayout.vue
page,
const notificationCount = ref(0);
const notifications = ref([]);
const newNotification = ref({});
window.Echo.channel("notifications").listen("NotificationCreated", (e) => {
notificationCount.value++;
newNotification.value = {
id: e.notification.id,
title: e.notification.title,
body: e.notification.body
}
notifications.value.unshift(newNotification.value);
});
And change the dropdown like this,
<Dropdown align="right" width="96">
<template #trigger>
<BellIcon class="h-7 w-7 text-gray-600" />
<span v-if="notificationCount > 0" class="absolute bottom-3 left-3 flex items-center justify-center h-5 w-5 rounded-full bg-red-600 text-white text-xs">
{{ notificationCount }}
</span>
</template>
<template #content>
<!-- Account Management -->
<div class="block px-4 py-2 text-xs text-gray-400 w-[350px]">
Notifications
</div>
<div class="border-t border-gray-200" />
<div v-for="(notification, index) in notifications" :key="index">
<DropdownLink :href="route('dashboard')">
<div class="block text-xs">{{ notification.title }}</div>
<div>{{ notification.body }}</div>
</DropdownLink>
<div class="border-t border-gray-200" />
</div>
</template>
</Dropdown>
Now if you send a notification using form you will be able to see it receiving like this without refreshing.
Here is the git code until now.
https://github.com/vimuths123/notification/tree/sending_to_pusher
Making notifications private
Now we are sending the notifications. But think about this. We are selecting a user to send a notification. But currently we are sending the notification to all users. We can prevent this and send the notification only to selected user. This is how to do it
First we create a separate channel for user. In app\Events\NotificationCreated.php
we change public function broadcastOn(): array
to this
public function broadcastOn(): array
{
return [
new PrivateChannel('notifications.'.$this->notification->user_id),
];
}
Here we are sending the channel with user id at the end. And this is how we listen to it using echo in resources\js\Layouts\AppLayout.vue
.
import { usePage } from '@inertiajs/vue3';
...........
window.Echo.private("notifications." + usePage().props.auth.user.id).listen("NotificationCreated", (e) => {
notificationCount.value++;
newNotification.value = {
id: e.notification.id,
title: e.notification.title,
body: e.notification.body
}
notifications.value.unshift(newNotification.value);
});
And you have to uncomment this line in this page config\app.php
App\Providers\BroadcastServiceProvider::class,
And in this file routes/channels.php
Broadcast::channel('notifications.{userId}', function ($user, $userId) {
return (int) $user->id === (int) $userId;
});
At the end we created two login users with our seeders. So login with admin@admin.com and admin2@admin.com from two different browsers and check notifications are coming only to needed user.
This is the git for this topic.
https://github.com/vimuths123/notification/tree/private_notifications
Now let us do some fine tunings,
Show pervious notifications and view notification functionality
First let us write backend code to get notifications
Route::get('get_notifications', function (Request $request) {
return $user_notifications = Notification::where('user_id', $request->user()->id)
->where('read', false)
->latest()
->get();
})->name('get_notifications');
Then let us take them and show inside front end.
import { onMounted, ref } from 'vue';
....
onMounted(() => {
axios.get('/get_notifications')
.then(response => {
notifications.value = response.data;
notificationCount.value = response.data.length;
})
.catch(error => {
console.error('Error fetching notifications:', error);
});
});
And now you will be able to see the notifications when you log in to the site.
First we write backend code
Route::get('click_notification/{notification}', function (Notification $notification) {
$notification->read = true;
$notification->save();
return redirect()->back()->banner('Notification clicked.');
})->name('click_notification');
Here what you do is just change the flag and redirect back to the page. Remember in previous code we got only unread notifications. And now let's change the front end. In resources\js\Layouts\AppLayout.vue
inside loop add this.
<DropdownLink :href="route('click_notification', notification.id)">
<div class="block text-xs">{{ notification.title }}</div>
<div>{{ notification.body }}</div>
</DropdownLink>
Remember in real world don't forget to add a nice page to read the notification.
This is the code up to now.
https://github.com/vimuths123/notification/tree/view_and_read_notifications
Optimise with Job queues and Redis
We can make this code asynchronous with jobs. This will make event asynchronous and save a lot of time.
this is how we do that.
Step 1: Configure the Queue Driver
First, update your .env file to use the database queue driver:
QUEUE_CONNECTION=database
This setting tells Laravel to use the database for queueing jobs.
Step 2: Create the Queue Table
Laravel needs a table in your database to store queued jobs. You can create this table by running the queue table migration that comes with Laravel. In your terminal, execute:
php artisan queue:table
Then, apply the migration to create the table:
php artisan migrate
php artisan queue:work
And now we can use Redis for this jobs. This is how to do that.
Redis operates in memory, offering much faster read and write operations compared to disk-based systems. This is crucial for chat applications where timely processing of messages and notifications is critical for user experience.
By using Redis for queue management and temporary data storage (e.g., unread messages or active users), you can significantly reduce the load on your main database, reserving it for more critical tasks like persisting chat logs or user information.
Let's check how to use redis for job queues
First install predis
composer require predis/predis
Then let's tell laravel to use redis for queue. Add this in .env
REDIS_CLIENT=predis
QUEUE_CONNECTION=redis
And don't forget to install redis on your system before
This is it. Enjoy the code.
Top comments (0)