
In this article of the tutorial ‘creating a restaurant menu app’ we will preview the created menus in the home page and in a separate details page for single menu display.
To show and display any menu, we have to think about the UI necessary that will be used to display the menu sections and menu items. We will display the menu in two places, the homepage and a another custom page.
In this illustration explains what we will implement below:
If you take a look in this illustration:
- The menu title will be displayed at the top
- The menu sections will be displayed in a Tab-like UI.
- When clicking on any section tab it will display the underlying menu items for this section.
- Each menu item composed of an image, a title, description and price.
For these points i have created two Vue components that will serve this purpose:
resources/js/Components/MenuPreview.vue
<script setup> import { ref, onMounted } from 'vue'; import MenuItem from './MenuItem.vue'; const props = defineProps({ menu: { type: Object } }); </script> <template> <div v-if="menu" class="mb-7"> <div class="section-header text-center mb-8"> <p class="text-green-700 text-lg mb-2">Food Menu</p> <h2 class="font-bold text-3xl text-gray-500">Menu Title</h2> </div> <div> <nav> <ul class="flex flex-wrap justify-center text-sm font-medium text-center text-gray-500 dark:text-gray-400"> <li class="me-2 mb-2"> <a href="#" class="inline-block text-[1rem] px-4 py-2 text-white bg-amber-500 rounded-lg capatilize" >Section 1</a> </li> <li class="me-2 mb-2"> <a href="#" class="inline-block text-[1rem] px-4 py-2 text-white bg-amber-500 rounded-lg capatilize" >Section 2</a> </li> <li class="me-2 mb-2"> <a href="#" class="inline-block text-[1rem] px-4 py-2 text-white bg-amber-500 rounded-lg capatilize" >Section 3</a> </li> </ul> </nav> <div class="flex flex-col gap-5 mt-10"> <h3 class="text-gray-500 text-2xl">Section title</h3> <MenuItem /> <MenuItem /> <MenuItem /> </div> </div> </div> </template>
resources/js/Components/MenuItem.vue
<script setup> defineProps({ menu_item: Object }) </script> <template> <div class="menu-item flex items-center gap-2 mb-3"> <div class="menu-img"> <img src="http://dymmyimage" class="rounded-[50%] w-30 h-20" width="80" height="80" /> </div> <div class="menu-text mx-2 w-[1/2] md:size-full"> <h3 class="text-[22px] font-medium capitalize block flex justify-between"> <span class="text-gray-700 capatilize">Title</span> <strong class="text-amber-400 float-end">50$</strong> </h3> <p class="mt-1 text-gray-500 text-[1rem]">lorem ipsum lorem ipsum lorem ipsum lorem ipsum</p> </div> </div> </template>
This is the html only for the components, will update these components below with the actual code. The <MenuPreview />
component displays a single menu with the sections and menu items. The component accepts the menu
as prop. We are using tailwindcss classes and flowbite to create the tabs that will display the menu sections. To learn more about using flowbite tabs click this link.
The <MenuItem />
component displays a menu item with the item image on the left side and the item title, description and price on the right side. It accepts the menu_item
as a prop.
Create a new controller that contains the actions for previewing a menu
php artisan make:controller HomeController
Open the HomeController and add the below code:
app/Http/Controllers/HomeController.php
<?php namespace App\Http\Controllers; use App\Models\Menu; use Inertia\Inertia; class HomeController extends Controller { public function index() { $latestMenu = Menu::with('sections')->latest()->first(); return Inertia::render('Home', [ 'menu' => $latestMenu, ]); } public function preview(int $id) { $menu = Menu::with('sections')->findOrFail($id); return Inertia::render('Preview', [ 'menu' => $menu, ]); } }
The index()
method retrieves the latest menu and sends it as prop to the Home
vue page. While the preview()
method preview a menu using menu id and sends the menu also to Preview
vue page.
Beside this update the sections()
relationship in the Menu
model to include the menuItems
as well, we will need this when displaying the menu items specific to particular sections:
Models/Menu.php
<?php namespace App\Models; ... ... class Menu extends Model { .... .... public function sections(): BelongsToMany { return $this->belongsToMany(Section::class)->with('menuItems'); // Added with('menuItems') } }
Open routes/web.php
and update like so:
<?php use App\Http\Controllers\HomeController; use App\Http\Controllers\MenusController; use Illuminate\Support\Facades\Route; use App\Http\Controllers\SectionsController; use App\Http\Controllers\MenuItemsController; Route::get('/', [HomeController::class, 'index']); Route::get('/preview/{id}', action: [HomeController::class, 'preview']); Route::resource('menu-items', MenuItemsController::class); Route::resource('sections', SectionsController::class); Route::resource('menus', MenusController::class);
I have replaced the home route defined previously using Route::inertia()
with Route::get()
and points to HomeController@index
method. Also i added the preview menu route `/preview/{id}
`.
Displaying Latest Menu In Home Page
To display the latest added menu in the home page, open Home.vue
page and update it like so:
<script setup> import Main from '../Layout/Main.vue'; import MenuPreview from '../Components/MenuPreview.vue'; const props = defineProps({ menu: { type: Object } }); </script> <template> <Main> <h2>Restaurant Menu</h2> <MenuPreview :menu="menu" /> </Main> </template>
In this code we imported the <MenuPreview />
component at the top and then capturing the menu
prop coming from the HomeController
action and passed this prop to the <MenuPreview />
component.
Update MenuPreview.vue
:
<script setup> import { ref, onMounted } from 'vue'; import MenuItem from './MenuItem.vue'; const props = defineProps({ menu: { type: Object } }); const current_section = ref(null); const setCurrentSection = (section) => { current_section.value = section; } onMounted(() => { if(props.menu.sections.length > 0) { setCurrentSection(props.menu.sections[0]); } }) </script> <template> <div v-if="menu" class="mb-7"> <div class="section-header text-center mb-8"> <p class="text-green-700 text-lg mb-2">Food Menu</p> <h2 class="font-bold text-3xl text-gray-500">{{ menu.title }}</h2> </div> <div> <nav> <ul class="flex flex-wrap justify-center text-sm font-medium text-center text-gray-500 dark:text-gray-400"> <li class="me-2 mb-2" v-for="section in menu.sections" :key="section.id"> <a href="#" @click.prevent="setCurrentSection(section)" class="inline-block text-[1rem] px-4 py-2 text-white bg-amber-500 rounded-lg capatilize" :class="{'bg-lime-600': current_section && section.id == current_section.id}">{{ section.title }}</a> </li> </ul> </nav> <div class="flex flex-col gap-5 mt-10" v-if="current_section"> <h3 class="text-gray-500 text-2xl">{{ current_section.title }}</h3> <MenuItem v-for="menu_item in current_section.menu_items" :key="menu_item.id" :menu_item="menu_item"/> </div> </div> </div> <div v-else> No menus yet </div> </template>
MenuItem.vue
<script setup> defineProps({ menu_item: { type: Object, }, }); </script> <template> <div class="menu-item flex items-center gap-2 mb-3"> <div class="menu-img"> <img :src="menu_item.image_url" class="rounded-[50%] w-30 h-20" width="80" height="80" :alt="menu_item.title" /> </div> <div class="menu-text mx-2 w-[1/2] md:size-full"> <h3 class="text-[22px] font-medium capitalize block flex justify-between"> <span class="text-gray-700 capatilize">{{ menu_item.title }}</span> <strong class="text-amber-400 float-end">{{ menu_item.price }}</strong> </h3> <p class="mt-1 text-gray-500 text-[1rem]">{{ menu_item.description }}</p> </div> </div> </template>
In the MenuPreview.vue
i declared the current_section
as ref state variable with default value of null and declared a function setCurrentSection()
to set this variable with the appropriate section. setCurrentSection()
is invoked when clicking on any section tab.
If there is a selected section, then we display the menu items for this section by iterating over the current_section.menu_items
as pass the :menu_item
prop to <MenuItem/>
.
The onMounted()
hook set the current section on page first loads with the first section from the menu sections array.
In MenuItem.vue
we displayed the menu item details like the title, image, price and description.
Fixing Images Display Issues
You may encounter situations where the menu item images not showing, fix this from vite.config.js update the vue() call like this:
vite.config.js
... ... export default defineConfig({ plugins: [ ... vue({ template: { transformAssetUrls: { includeAbsolute: false, }, } }) ] });
Displaying All Menus In Navbar
After displaying the latest menu in the home page, let’s display all the menus in the navbar dropdown in Navbar.vue which allow the user to click any menu for preview.
First update HandleInertiaRequests.php
middleware:
public function share(Request $request): array { return array_merge(parent::share($request), [ 'flash' => [ 'success' => fn(): mixed => $request->session()->get('success'), 'error' => fn(): mixed => $request->session()->get('error') ], 'menus_global' => Menu::with(relations: 'sections')->get(), ]); }
As we need to display all menus in the top nav in all pages, so it’s suitable to be shared globally in the entire vue app. So i added a new key ‘menus_global
‘ to be accessed any where in the vue app.
Open Navbar.vue
and update it like shown below:
<script setup> import { Link, usePage } from '@inertiajs/vue3' const { url } = usePage(); const isManagement = () => { return url.startsWith('/menus') || url.startsWith('/sections') || url.startsWith('/menu-items'); } const isPreview = () => { return url.startsWith('/preview'); } </script> <template> <nav class="bg-white border-gray-200 dark:bg-gray-900 dark:border-gray-700"> <div class="flex flex-wrap items-center justify-between p-4"> <Link href="/" class="flex items-center space-x-3 rtl:space-x-reverse"> <span class="self-center text-2xl font-semibold whitespace-nowrap text-orange-500 dark:text-white">Restaurant Menu</span> </Link> <button data-collapse-toggle="navbar-multi-level" type="button" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600" aria-controls="navbar-multi-level" aria-expanded="false"> <span class="sr-only">Open main menu</span> <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/> </svg> </button> <div class="hidden w-full md:block md:w-auto" id="navbar-multi-level"> <ul class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700"> <li> <Link href="/" class="block py-2 px-3 bg-blue-700 rounded-sm md:bg-transparent md:p-0 md:dark:text-blue-500 dark:bg-blue-600 md:dark:bg-transparent" :class="{'md:text-blue-700': url === '/'}">Home</Link> </li> <li> <button id="dropdownNavbarLink" data-dropdown-toggle="dropdownNavbar" class="flex items-center justify-between w-full py-2 px-3 text-gray-900 hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 md:w-auto dark:text-white md:dark:hover:text-blue-500 dark:focus:text-white dark:hover:bg-gray-700 md:dark:hover:bg-transparent" :class="{'md:text-blue-700': isManagement()}">Manage <svg class="w-2.5 h-2.5 ms-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/> </svg> </button> <!-- Dropdown menu --> <div id="dropdownNavbar" class="z-10 hidden font-normal bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700 dark:divide-gray-600"> <ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownLargeButton"> <li> <Link href="/menus" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Menus</Link> </li> <li> <Link href="/sections" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Sections</Link> </li> <li> <Link href="/menu-items" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Menu Items</Link> </li> </ul> </div> </li> <li> <button data-dropdown-toggle="dropdownNavbar-all-menus" class="flex items-center justify-between w-full py-2 px-3 text-gray-900 hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 md:w-auto dark:text-white md:dark:hover:text-blue-500 dark:focus:text-white dark:hover:bg-gray-700 md:dark:hover:bg-transparent" :class="{'md:text-blue-700': isPreview()}">All Menus <svg class="w-2.5 h-2.5 ms-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/> </svg> </button> <div id="dropdownNavbar-all-menus" class="z-10 hidden font-normal bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700 dark:divide-gray-600"> <ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownLargeButton"> <li v-for="menu in $page.props.menus_global" :key="menu.id"> <Link :href="`/preview/${menu.id}`" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">{{ menu.title }}</Link> </li> </ul> </div> </li> </ul> </div> </div> </nav> </template>
In this code i have added the ability to make top links active or inactive, so i added two function isManagement()
and isPreview()
to refer if the user in the management area or in the preview screen respectively, thereby applying the active classes to the links. To do this i retrieved current url from the usePage()
composable and check if it starts with particular segement:
const { url } = usePage(); const isManagement = () => { return url.startsWith('/menus') || url.startsWith('/sections') || url.startsWith('/menu-items'); }
The menus_global
prop accessed from $page.props
which coming from the middleware above and using v-for
directive we iterated over it and display a <Link />
component which points to `/preview/${menu.id}
`
Create the page Preview.vue
resources/js/Pages/Preview.vue
<script setup> import Main from '../Layout/Main.vue'; import MenuPreview from '../Components/MenuPreview.vue'; const props = defineProps({ menu: { type: Object } }); </script> <template> <Main> <MenuPreview :menu="menu" /> </Main> </template>
Now all the menus is shown in the top nav and you can click on any menu to preview it.