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

Public Homepage and Restaurant Page

Now it is time to work on home page to browse restaurants and list the menu of the restaurant for customers.

By the end of this lesson our home page will look like this:

More Restaurants Homepage

And this is how we are going to list restaurant menu.

Restaurant Page


Homepage View

First let's create a controller for our home page.

php artisan make:controller HomeController

app/Http/Controllers/HomeController.php

namespace App\Http\Controllers;
 
use App\Models\Restaurant;
use Inertia\Inertia;
use Inertia\Response;
 
class HomeController extends Controller
{
public function index(): Response
{
return Inertia::render('Home', [
'restaurants' => Restaurant::get(),
]);
}
}

And the new page to list restaurants in a grid.

resources/js/Pages/Home.vue

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import { Head, Link } from '@inertiajs/vue3'
 
defineProps({
restaurants: {
type: Array
}
})
</script>
 
<template>
<Head title="Home" />
 
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Home</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">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-8">
<div v-for="restaurant in restaurants" :key="restaurant.id">
<Link href="#" class="flex flex-col gap-2">
<div>
<img
class="w-full aspect-video rounded-xl"
:src="`https://picsum.photos/seed/${restaurant.id}/380/220?blur=2`"
/>
</div>
<div class="font-bold text-lg truncate">
{{ restaurant.name }} ({{ restaurant.address }})
</div>
</Link>
</div>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>

We do not store any images on the application. Random are images shown using Lorem Picsum service. It removes the need to manually add photos when prototyping apps.

Now we can update web.php routes file by replacing default route for / path and remove /dashboard route, it is no longer needed.

routes/web.php

use App\Http\Controllers\HomeController;
use Illuminate\Foundation\Application;
use Inertia\Inertia;
 
// ...
 
Route::get('/', function () {
return Inertia::render('Welcome', [
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
'laravelVersion' => Application::VERSION,
'phpVersion' => PHP_VERSION,
]);
});
 
Route::get('/dashboard', function () {
return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
 
Route::get('/', [HomeController::class, 'index'])->name('home');

Then update HOME route in RouteServiceProvider. Without this change new logged in users will get 404 error because we removed /dashboard route in previous step.

app/Providers/RouteServiceProvider.php

public const HOME = '/dashboard';
public const HOME = '/';

It is time to update change dashboard route references to home on main layout:

resources/js/Layouts/AuthenticatedLayout.vue

<Link :href="route('dashboard')">
<Link :href="route('home')">
<ApplicationLogo class="block h-9 w-auto fill-current text-gray-800" />
</Link>

We need to hide Settings Dropdown and display Login / Register buttons for guest users. Apply these changes to the same AuthenticatedLayout.vue file.

<div class="hidden sm:flex sm:items-center sm:ml-6">
<div v-if="$page.props.auth.user" class="hidden sm:flex sm:items-center sm:ml-6">
<!-- Settings Dropdown -->
<div class="ml-3 relative">
<Dropdown align="right" width="48">
<!-- ... -->
</Dropdown>
</div>
</div>
<div v-else class="hidden sm:flex gap-4 items-center sm:ml-6">
<Link :href="route('login')" class="btn btn-secondary">Login</Link>
<Link :href="route('register')" class="btn btn-primary">Register</Link>
</div>

Update dashboard route to home routes on <ResponsiveNavLink>.

<div class="pt-2 pb-3 space-y-1">
<ResponsiveNavLink :href="route('dashboard')" :active="route().current('dashboard')">
Dashboard
<ResponsiveNavLink :href="route('home')" :active="route().current('home')">
Home
</ResponsiveNavLink>
</div>

And hide the actual dropdown for guest users by adding v-if="$page.props.auth.user" under Responsive Settings Options section.

<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200">
<div v-if="$page.props.auth.user" class="pt-4 pb-1 border-t border-gray-200">

Without all these changes to AuthenticatedLayout.vue application won't render.

The home page now should list single restaurant entry.

Restaurants Homepage


Seed More Restaurants With Categories And Products

By adding more restaurants to the home page, we can have a better overview how users interact with application.

First we need extend UserFactory and new vendor() method. When vendor() method is called on a factory vendor role will be automatically assigned to the newly created user.

database/factories/UserFactory.php

use App\Enums\RoleName;
use App\Models\Role;
use App\Models\User;
 
// ...
 
public function vendor()
{
return $this->afterCreating(function (User $user) {
$user->roles()->sync(Role::where('name', RoleName::VENDOR->value)->first());
});
}

Then create a new RestaurantFactory using this command.

php artisan make:factory RestaurantFactory

And define the field array in the definition() method to be randomly populated when new Restaurant is created.

database/factories/RestaurantFactory.php

namespace Database\Factories;
 
use App\Models\City;
use Illuminate\Database\Eloquent\Factories\Factory;
 
class RestaurantFactory extends Factory
{
public function definition(): array
{
$cities = City::pluck('id');
 
return [
'city_id' => $cities->random(),
'name' => fake()->company(),
'address' => fake()->address(),
];
}
}
  • city_id - is completely random id, but actual ids from City Model.
  • name - fake() is a helper method from Faker library and will generate random company name
  • address - will generate random address

Then in the same way we create CategoryFactory.

php artisan make:factory CategoryFactory

Except this time, we define category names to pick from manually. This way we have more sensible data.

database/factories/CategoryFactory.php

namespace Database\Factories;
 
use Illuminate\Database\Eloquent\Factories\Factory;
 
class CategoryFactory extends Factory
{
public function definition(): array
{
$categories = collect([
'Pizza',
'Snacks',
'Soups',
'Desserts',
'Kids menu',
'Drinks',
'Salads',
'Chicken',
'Duck',
'Pork',
'Beef',
'Fish',
'Pasta',
'Burgers',
'Dumplings',
'Ramen',
]);
 
return [
'name' => $categories->random(),
];
}
}

And the last piece is ProductFactory.

php artisan make:factory ProductFactory

Add field definitions:

database/factories/ProductFactory.php

namespace Database\Factories;
 
use Illuminate\Database\Eloquent\Factories\Factory;
 
class ProductFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->words(3, true),
'price' => rand(499, 5999),
];
}
}
  • name - fake()->words(3, true) will generate three words for a product name, argument true tells to return those words as a string, otherwise it would be an array.
  • price - we store price in database as cents, so it will result in random price between 4.99 and 59.99 per product.

Now let's glue everything together in the DatabaseSeeder:

database/seeders/DatabaseSeeder.php

namespace Database\Seeders;
 
use App\Models\Category;
use App\Models\Product;
use App\Models\Restaurant;
use App\Models\User;
use Illuminate\Database\Seeder;
 
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
PermissionSeeder::class,
RoleSeeder::class,
CitySeeder::class,
UserSeeder::class,
]);
 
$this->seedDemoRestaurants();
}
 
public function seedDemoRestaurants()
{
$products = Product::factory(7);
$categories = Category::factory(5)->has($products);
$restaurant = Restaurant::factory()->has($categories);
 
User::factory(50)->vendor()->has($restaurant)->create();
}
}

It may look a bit odd how we defined these Factories in Seeder, but let me explain what is actually going on in seedDemoRestaurants() method.

Product::factory(7) defines a Product Factory of seven Products. It doesn't actually create these Products immediately because we didn't call create() method.

Category::factory(5)->has($products) - this is where things are getting interesting. Again, we define a Factory of five Categories and each category will contain seven distinct products.

Following the same logic each Restaurant will have five distinct Categores defined by CategoryFactory, and each Vendor user will have a single restaurant.

When we finally call ->create() method, all entries in the database are created at once and all models will have correct relationships.

By convention, when passing a ProductFactory to the has method, Laravel will assume that the Category model must have a products method that defines the relationship. If necessary, you may explicitly specify the name of the relationship that you would like to manipulate:

Category::factory(5)->has($products, 'products');

Now we can repopulate the database by running migrate:fresh --seed command.

php artisan migrate:fresh --seed

And this is how homepage now should look like:

More Restaurants Homepage


Restaurant View

Since we have a restaurant list now we can implement final view to display menu to customers.

Restaurant Page

For that we need to create a new RestaurantController.

php artisan make:controller RestaurantController

app/Http/Controllers/RestaurantController.php

namespace App\Http\Controllers;
 
use App\Models\Restaurant;
use Inertia\Inertia;
use Inertia\Response;
 
class RestaurantController extends Controller
{
public function show(Restaurant $restaurant): Response
{
return Inertia::render('Restaurant', [
'restaurant' => $restaurant->load('categories.products'),
]);
}
}

Show method will pass restaurant object with loaded categories and products to Restaurant view.

Let's create the Restaurant.vue view to render:

resources/js/Pages/Restaurant.vue

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import { Head } from '@inertiajs/vue3'
 
defineProps({
restaurant: {
type: Object
}
})
</script>
 
<template>
<Head :title="restaurant.name" />
 
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ restaurant.name }}
</h2>
</template>
 
<div class="py-12">
<div class="max-w-2xl 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 flex flex-col gap-8">
<div
v-for="category in restaurant.categories"
:key="category.id"
class="flex flex-col gap-4"
>
<div class="flex justify-between">
<div class="">
<div class="text-2xl font-bold">{{ category.name }}</div>
</div>
</div>
<div class="flex flex-col gap-6">
<div
v-for="product in category.products"
:key="product.id"
class="flex justify-between pb-6 border-b gap-4"
>
<div class="grow flex flex-col gap-2">
<div class="font-bold">{{ product.name }}</div>
<div class="">{{ (product.price / 100).toFixed(2) }} &euro;</div>
<div class="grow flex items-end">
<button class="btn btn-primary btn-sm" type="button">
Add {{ (product.price / 100).toFixed(2) }} &euro; (Coming soon)
</button>
</div>
</div>
 
<div class="flex-none w-48">
<img
class="w-full aspect-video rounded"
:src="`https://picsum.photos/seed/${product.id}/200/110?blur=2`"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>

Add a new route to web.php

routes/web.php

use App\Http\Controllers\RestaurantController;
 
// ...
 
Route::get('restaurant/{restaurant}', [RestaurantController::class, 'show'])
->name('restaurant');

And finally, update the href attribute of the <Link> Component with the route to restaurant view on Home.vue.

resources/js/Pages/Home.vue

<div v-for="restaurant in restaurants" :key="restaurant.id">
<Link href="#" class="flex flex-col gap-2">
<Link :href="route('restaurant', restaurant)" class="flex flex-col gap-2">

This is it for this lesson.