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:
And this is how we are going to list restaurant menu.
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.
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 nameaddress
- will generate random addressThen 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:
Since we have a restaurant list now we can implement final view to display menu to customers.
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) }} €</div> <div class="grow flex items-end"> <button class="btn btn-primary btn-sm" type="button"> Add {{ (product.price / 100).toFixed(2) }} € (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.