Back to Course |
Laravel Vue Inertia: Food Ordering Project Step-By-Step

Restaurants Index Table

In this lesson, we aim to display the table of Restaurants with owner details for admin.

Restaurant Management Index Table

First, to have some data to display, we need to add a Vendor (restaurant owner) user with their restaurant to the database.


Seed Vendor Role

Add the createVendorRole method to RoleSeeder:

database/seeders/RoleSeeder.php

class RoleSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$this->createAdminRole();
$this->createVendorRole();
}
 
// ...
 
protected function createVendorRole(): void
{
$this->createRole(RoleName::VENDOR, collect());
}
}

Notice that we create only the role and pass the empty Collection as permissions. Permissions to the vendor role will be added as we need them.


Seed Vendor User

Add the createVendorUser method to UserSeeder:

database/seeders/UserSeeder.php

use App\Models\City;
 
// ...
 
class UserSeeder extends Seeder
{
public function run(): void
{
$this->createAdminUser();
$this->createVendorUser();
}
 
// ...
 
public function createVendorUser()
{
$vendor = User::create([
'name' => 'Restaurant owner',
'email' => 'vendor@admin.com',
'password' => bcrypt('password'),
]);
 
$vendor->roles()->sync(Role::where('name', RoleName::VENDOR->value)->first());
 
$vendor->restaurant()->create([
'city_id' => City::where('name', 'Vilnius')->value('id'),
'name' => 'Restaurant 001',
'address' => 'Address SJV14',
]);
}
}

City id is assigned by looking up the cities table by city name City::where('name', 'Vilnius')->value('id'). If you plan to add more users, consider assigning city id directly to avoid the N+1 query problem.


Index View

Create RestaurantController using the Artisan command:

php artisan make:controller Admin/RestaurantController

We immediately create a namespace, Admin/. Later, we might have Controllers with the same name for other user roles with different logic.

app/Http/Controllers/Admin/RestaurantController.php

namespace App\Http\Controllers\Admin;
 
use App\Http\Controllers\Controller;
use App\Models\Restaurant;
use Inertia\Inertia;
use Inertia\Response;
 
class RestaurantController extends Controller
{
public function index(): Response
{
$this->authorize('restaurant.viewAny');
 
return Inertia::render('Admin/Restaurants/Index', [
'restaurants' => Restaurant::with(['city', 'owner'])->get(),
]);
}
}

Before rendering the view, we check if the user is authorized to restaurant.viewAny using the $this->authorize() method.

Inertia renders views similarly to Laravel's view() method. The equivalent for Inertia is the Inertia::render() method. Data is passed as a second argument as an array. Restaurants' collection is automatically serialized to JSON array.

Now let's create the Admin/Restaurants/Index view:

resources/js/Pages/Admin/Restaurants/Index.vue

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import { Head } from '@inertiajs/vue3'
 
defineProps({
restaurants: {
type: Array
}
})
</script>
 
<template>
<Head title="Restaurants" />
 
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Restaurants</h2>
</template>
 
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 overflow-x-scroll">
<table>
<thead>
<tr>
<th>ID</th>
<th>Restaurant Name</th>
<th>City</th>
<th>Address</th>
<th>Owner Name</th>
<th>Owner Email</th>
</tr>
</thead>
<tbody>
<tr v-for="restaurant in restaurants" :key="restaurant.id">
<td>{{ restaurant.id }}</td>
<td>{{ restaurant.name }}</td>
<td>{{ restaurant.city.name }}</td>
<td>{{ restaurant.address }}</td>
<td>{{ restaurant.owner.name }}</td>
<td>
<a :href="'mailto:' + restaurant.owner.email">{{ restaurant.owner.email }}</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>

In the Index view component, to have access to passed restaurant data, we need to define restaurants property and specify its type:

defineProps({
restaurants: {
type: Array
}
})

The defineProps() method is available globally in all components and doesn't need to be imported.

To render the list, we use the v-for Vue directive. The :key attribute helps Vue track the node's identity in case some changes are made. It is required to specify a unique value, and restaurant.id is perfect.


Restaurant routes: Admin file

As our application grows, it is a lot easier to maintain smaller route files.

Like we separate Controllers, we can do the same for routes. I personally like to separate route files by roles. So, let's create a new admin.php route file:

routes/admin.php

use App\Http\Controllers\Admin\RestaurantController;
use Illuminate\Support\Facades\Route;
 
Route::group([
'prefix' => 'admin',
'as' => 'admin.',
'middleware' => ['auth'],
], function () {
Route::resource('/restaurants', RestaurantController::class);
});

Instead of manually registering every route, we can register resource routes using the Route::resource() method, which points to RestaurantController.

We already have implemented the index method in our RestaurantController. Later we will implement others.

Finally, let's include the admin.php file at the end of our web.php file:

routes/web.php

// ...
 
require __DIR__.'/auth.php';
require __DIR__.'/admin.php';

And update the AuthenticatedLayout file replacing the Dashboard link with Restaurants as shown:

resources/js/Layouts/AuthenticatedLayout.vue

<template>
<!-- ... -->
 
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<NavLink :href="route('dashboard')" :active="route().current('dashboard')">
Dashboard
</NavLink>
<NavLink
:href="route('admin.restaurants.index')"
:active="route().current('admin.restaurants.index')"
>
Restaurants
</NavLink>
</div>
 
<!-- ... -->
<template>

Now let's run Vite via npm run dev to compile the front-end and leave it in the background to auto-refresh our future front-end changes.

When we log in as an administrator and navigate to the /restaurants route, you should see the following table:

Restaurants Index Table

The data looks good, but not the styling, right?


CSS Styles For Table

First, include all colors and define color aliases in the TailwindCSS config. Color aliases are a convenient way to quickly change colors in a single place.

tailwind.config.js

import defaultTheme from 'tailwindcss/defaultTheme'
import forms from '@tailwindcss/forms'
import colors from 'tailwindcss/colors'
 
/** @type {import('tailwindcss').Config} */
export default {
// ...
 
theme: {
extend: {
colors: {
primary: colors.green,
danger: colors.red
},
 
// ...
}

Let's add our table classes to the app.css file.

resources/css/app.css

@tailwind base;
@tailwind components;
@tailwind utilities;
 
@layer components {
.table {
@apply w-full;
}
 
.table thead th {
@apply text-left p-3 bg-gray-100;
}
 
.table tbody tr {
@apply even:bg-gray-50/50;
}
 
.table tbody td {
@apply p-3;
}
 
.badge {
@apply inline-flex text-sm rounded px-2 font-semibold border;
}
 
.badge-primary {
@apply bg-primary-50 text-primary-700 border-primary-300;
}
 
.text-link {
@apply text-blue-600 hover:underline;
}
}

Then apply CSS classes to elements of the Index view:

resources/js/Pages/Admin/Restaurants/Index.vue

<template>
<!-- ... -->
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 overflow-x-scroll">
<table>
<table class="table">
<thead>
<tr>
<th>ID</th>
 
<tr v-for="restaurant in restaurants" :key="restaurant.id">
<td>{{ restaurant.id }}</td>
<td>{{ restaurant.name }}</td>
<td>{{ restaurant.city.name }}</td>
<td>
<div class="badge badge-primary">{{ restaurant.city.name }}</div>
</td>
<td>{{ restaurant.address }}</td>
<td>{{ restaurant.owner.name }}</td>
<td>
<a :href="'mailto:' + restaurant.owner.email">{{ restaurant.owner.email }}</a>
<a :href="'mailto:' + restaurant.owner.email" class="text-link">{{
restaurant.owner.email
}}</a>
</td>
</tr>
</tbody>

Do you have the npm run dev running in the background? It should have re-compiled everything.

If not, you can manually run npm run build with each new front-end change.

The final result should look like this:

Restaurants Index Table After