Building with AdonisJS & Inertia #4.6

Completing Our AppLayout & Navigation Bar

In This Lesson

We'll finish setting up our application's layout shell and navigation bar.

Created by
@tomgobich
Published

Final Layout Components

This lesson contains a lot of HTML markup. If you're not interested in how Shadcn-Vue's Sheet or DropDown components work, you can skip this lesson, install the needed Shadcn-Vue components, and copy-paste the below if you're following along.

  1. Install The Shadcn-Vue Components

npx shadcn-vue@latest add sheet
Copied!
npx shadcn-vue@latest add dropdown-menu
Copied!
  1. Add A Navigation Component


<script lang="ts" setup>
import { Menu, Slash, Route } from 'lucide-vue-next'
</script>

<template>
  <nav class="hidden gap-5 text-sm items-center md:flex lg:gap-6">
    <Link href="/courses" class="flex items-center gap-2 text-lg font-semibold md:text-base">
      <Route class="h-6 w-6" />
      <span class="sr-only">PlotMyCourse</span>
    </Link>

    <div class="flex items-center">
      <Slash class="text-slate-300 w-4 h-4 -rotate-12" />
      <span>TODO: ORG SELECT</span>
      <Slash class="text-slate-300 w-4 h-4 -rotate-12" />
    </div>

    <Link
      href="/courses"
      class="desktop-link"
      :class="{ active: $page.url.startsWith('/courses') }"
    >
      Courses
    </Link>

    <Link
      href="/difficulties"
      class="desktop-link"
      :class="{ active: $page.url.startsWith('/difficulties') }"
    >
      Difficulties
    </Link>

    <Link
      href="/statuses"
      class="desktop-link"
      :class="{ active: $page.url.startsWith('/statuses') }"
    >
      Statuses
    </Link>

    <Link
      href="/access-levels"
      class="desktop-link"
      :class="{ active: $page.url.startsWith('/access-levels') }"
    >
      Access Levels
    </Link>
  </nav>

  <Sheet>
    <SheetTrigger as-child>
      <Button variant="outline" size="icon" class="shrink-0 md:hidden">
        <Menu class="w-5 h-5" />
        <span class="sr-only">Toggle navigation menu</span>
      </Button>
    </SheetTrigger>
    <SheetContent side="left">
      <nav class="grid gap-6 text-lg font-medium">
        <Link href="/courses" class="flex items-center gap-2 text-lg font-semibold">
          <Route class="h-6 w-6" />
          <span class="sr-only">PlotMyCourse</span>
        </Link>

        <span>TODO: ORG SELECT</span>

        <Link
          href="/courses"
          class="mobile-link"
          :class="{ active: $page.url.startsWith('/courses') }"
        >
          Courses
        </Link>

        <Link
          href="/difficulties"
          class="mobile-link"
          :class="{ active: $page.url.startsWith('/difficulties') }"
        >
          Difficulties
        </Link>

        <Link
          href="/statuses"
          class="mobile-link"
          :class="{ active: $page.url.startsWith('/statuses') }"
        >
          Statuses
        </Link>

        <Link
          href="/access-levels"
          class="mobile-link"
          :class="{ active: $page.url.startsWith('/access-levels') }"
        >
          Access Levels
        </Link>
      </nav>
    </SheetContent>
  </Sheet>
</template>

<style scope>
.desktop-link {
  @apply text-muted-foreground transition-colors hover:text-foreground;

  &.active {
    @apply text-foreground;
  }
}

.mobile-link {
  @apply text-muted-foreground hover:text-foreground;

  &.active {
    @apply text-foreground;
  }
}
</style>
Copied!
  • inertia
  • components
  • Navigation.vue
  1. Update The AppLayout


<script setup lang="ts">
import UserDto from '#dtos/user'
import { Link } from '@inertiajs/vue3'
import { UserCircle } from 'lucide-vue-next'

const props = defineProps<{
  user: UserDto
}>()
</script>

<template>
  <div class="flex min-h-screen w-full flex-col">
    <header
      class="sticky top-0 z-50 flex h-16 items-center gap-4 border-b bg-background px-4 md:px-6"
    >
      <Navigation v-bind="props" />

      <!-- User Panel -->
      <div class="flex flex-1 items-center justify-end">
        <DropdownMenu>
          <DropdownMenuTrigger as-child>
            <Button variant="secondary" size="icon" class="rounded-full">
              <UserCircle class="w-5 h-5" />
              <span class="sr-only">Toggle user menu</span>
            </Button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end">
            <DropdownMenuLabel>USER NAME</DropdownMenuLabel>
            <DropdownMenuSeparator />
            <DropdownMenuItem :as="Link" href="/settings/profile">
              Profile Settings
            </DropdownMenuItem>
            <DropdownMenuItem :as="Link" href="/settings/account">
              Account Settings
            </DropdownMenuItem>
            <DropdownMenuItem :as="Link" href="/settings/organization">
              Organization Settings
            </DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem :as="Link" href="/logout" method="post"> Logout </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </div>
    </header>

    <main
      class="flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:gap-8 md:p-10"
    >
      <slot />
    </main>
  </div>
</template>
Copied!
  • inertia
  • layouts
  • AppLayout.vue

Join the Discussion 2 comments

Create a free account to join in on the discussion
  1. @aaron-ford

    I have been going through trying to clean up my console errors. I have an error for 'Extraneous non-props attributes (user, messages) were passed to component but could not be automatically inherited because component renders fragment or text root nodes'. I ran your code and see the error exists there too. How do we fix this? I also get Invalid prop: type check failed for prop "href". Expected String with value "null", got Null for the pagination links. I tried modifying the components to allow null for the href, but that didn't work. Does the pagination dto npm package need to be modified?

    Update: I wrapped the nav and sheet into a parent div and that seems to fix the hydration issues. Either that, or listing user and messages in the props, but since the nav doesn't directly use them, I went with wrapping it in a div.

    1
    1. Responding to aaron-ford
      @tomgobich

      Hey Aaron! Ah yes, sorry about that! That warning is in regards to Vue's fallthrough attributes, ie: values passed to the to the component that aren't declared as props. When the component has a single root node, Vue will default to just passing that undeclared prop as an attribute. However, when there are multiple root nodes, as I accidentally did with the Navigation.vue component Vue isn't sure what to do with them and issues that warning.

      I agree, just wrapping the component template contents in a div would be the preferred solution here!

      As for the pagination links, is this in regard to the next/prev links? If so, I think you should be able to do something like the below. Allow the href prop to be null, then use a v-if to determine if the link should be utilized. Or, you could use nullish coalesce when passing in the prop to default to an empty string. The next/prev links, coming directly from the paginator, will be null if there isn't a next or previous page to go to.

      So something like this using nullish coalesce:

      <PaginationPrev :href="lessons.meta.previousPageUrl ?? ''" />
      Copied!

      Or this inside the next/prev components:

      <script setup lang="ts">
      import { ChevronLeft } from 'lucide-vue-next'
      import { PaginationPrev, type PaginationPrevProps } from 'radix-vue'
      import { type HTMLAttributes, computed } from 'vue'
      import { Button } from '~/components/ui/button'
      import { cn } from '~/lib/utils'
      
      const props = withDefaults(
        defineProps<PaginationPrevProps & { 
          class?: HTMLAttributes['class']; 
          href: string | null
        }>(),
        {
          asChild: true,
        }
      )
      
      const delegatedProps = computed(() => {
        const { class: _, ...delegated } = props
      
        return delegated
      })
      </script>
      
      <template>
        <PaginationPrev v-bind="delegatedProps">
          <Button :class="cn('w-8 h-8 p-0', props.class)" variant="outline">
            <Link v-if="href" :href="href">
              <ChevronLeft class="w-4 h-4" />
            </Link>
            <template v-else>
              <ChevronLeft class="w-4 h-4" />
            </template>
          </Button>
        </PaginationPrev>
      </template>
      
      Copied!

      0