Loading...
Flyout part2
Author Cloudapp
E.G.

Next.js 14 - Flyout menus with TailwindCSS & Contentful - Part 2

July 23, 2024
Table of Contents

In the previous story, we created the components for the flyout menu and did the basic integration, but the displayed data came from a static variable. Now, we will connect the menu with our headless CMS Contentful.

Here is the GitHub repo with the entire code. Below, you will find the link to the example page.

Example page hosted on Vercel -> https://nextjs14-contentful-syntax-highlighting.vercel.app/

Used Stack

I will start with my default stack:

  • Next.js 14 as the web framework, and I will use the provided middleware edge function

  • TailwindCss for Styling

  • Contentful CMS (Free Plan)

  • Vercel for hosting

Extending Contentmodel in Contentful

Before starting the coding, we must adapt the existing content model for the content type “NavItem” in Contentful. We will add 4 new attributes.

  1. ShortDescription — Self-explaining

  2. Icon — for the Icon shown in front of the menu point in the flyout

  3. Type — Menu or Submenu so that we can handle it in the code

  4. SubMenuItems — To link the submenu items

Nav-item-content-model
Nav-item-content-model

Modifying Graphql-Query for “NavItems”

We add the 4 new fields in the file src/lib/graphql/navItemFields.graphql

fragment NavItemFields on NavItem {
  __typename
  sys {
    id
  }
  name
  shortDescription
  href
  type
  icon
  subMenuItemsCollection {
    items {
      name
      shortDescription
      href
      type
      icon
    }
  }
}

, and then we execute

npm run graphql-codegen:generate

to update the corresponding files like sdk.ts, graphql.schema.graphql, and graphql.schema.json

Now, we are ready for the connection. Let’s adapt the component file src/components/header/navbar.components.tsx

//Line 39 we adapt the inteface
interface Linkitems {
  key: number;
  name: string;
  shortDescription?: string;
  href: string;
  icon?: any;
  submenutitems?: any;
  breakpoint?: string;
}
function SubMenu({
    href,
    name,
    shortDescription,
    submenutitems,
    breakpoint,
  }: Linkitems) {
    return (
      <Popover className="relative">
        <Popover.Button className="border-transparent text-gray-500 dark:text-gray-50 hover:border-gray-300 hover:text-gray-700 inline-flex items-center text-base px-1 pt-1 border-b-2 font-medium">
          {breakpoint === "desktop" ? (
            <>
              <span>{name}</span>
              <ChevronDownIcon className="w-5 h-5" aria-hidden="true" />
            </>
          ) : (
            <div className="pl-3 flex">
              <span>{name}</span>
              <ChevronDownIcon className="ml-2 w-5 h-5" aria-hidden="true" />
            </div>
          )}
        </Popover.Button>

        <Popover.Panel className="absolute left-1/2 z-10 mt-5 flex w-screen max-w-max -translate-x-1/2 px-4 transition data-[closed]:translate-y-1 data-[closed]:opacity-0 data-[enter]:duration-200 data-[leave]:duration-150 data-[enter]:ease-out data-[leave]:ease-in">
          <div className="flex-auto w-screen max-w-md overflow-hidden text-sm leading-6 bg-white dark:bg-gray-500 shadow-lg rounded-3xl ring-1 ring-gray-900/5">
            <div className="p-4">
              {submenutitems.map((item: Linkitems) => (
                <div
                  key={item.name}
                  className="relative flex p-4 rounded-lg group gap-x-6 hover:bg-gray-50"
                >
                  <div className="flex items-center justify-center flex-none mt-1 rounded-lg h-11 w-11 bg-gray-50 group-hover:bg-white">
                    {item.icon === "BookmarkSquareIcon" ? (
                      <BookmarkSquareIcon
                        className="w-6 h-6 text-gray-600 group-hover:text-indigo-600"
                        aria-hidden="true"
                      />
                    ) : (
                      <LifebuoyIcon
                        className="w-6 h-6 text-gray-600 group-hover:text-indigo-600"
                        aria-hidden="true"
                      />
                    )}
                  </div>
                  <div>
                    <a href={item.href} className="font-semibold text-gray-900">
                      {item.name}
                      <span className="absolute inset-0" />
                    </a>
                    <div className="mt-1 text-gray-800">
                      {item.shortDescription}
                    </div>
                  </div>
                </div>
              ))}
            </div>
          </div>
        </Popover.Panel>
      </Popover>
    );
  }

Changes on Desktop View and Mobile View

Desktop View — We use a condition based on the type

       {/* Desktop View */}
              {menuItems.map((menuItem: any, index: number) =>
                menuItem.type === "Menu" ? (
                  <NavigationLink
                    key={index}
                    href={`/${locale}${menuItem.href}`}
                    name={menuItem.name}
                  />
                ) : (
                  <SubMenu
                    key={index}
                    href={`/${locale}${menuItem.href}`}
                    name={menuItem.name}
                    submenutitems={menuItem.subMenuItemsCollection.items}
                    breakpoint="desktop"
                  />
                )
              )} 

Mobile View — Here, we use a condition based on the type as well

   {/* Only show up in Mobile view */}
              {menuItems.map((menuItem: any, index: number) =>
                menuItem.type === "Menu" ? (
                  <NavigationLinkDisclosure
                    key={index}
                    href={`/${locale}${menuItem.href}`}
                    name={menuItem.name}
                  />
                ) : (
                  <SubMenu
                    key={index}
                    href={`/${locale}${menuItem.href}`}
                    name={menuItem.name}
                    submenutitems={menuItem.subMenuItemsCollection.items}
                    breakpoint="mobile"
                  />
                )
              )}

Final Result

Below is the flyout menu, which gets the data from Contentful and is optimized for the desktop and mobile breakpoint.

Cut Menu example
Cut Menu example

Cloudapp-dev, and before you leave us

Thank you for reading until the end. Before you go:

Please consider clapping and following the writer! 👏 on our Medium Account

Or follow us on twitter -> Cloudapp.dev

Related articles