Data-Driven Header Navigation

/**
 * Data-Driven Header Navigation
 * http://www.github.com/zhaohuijian
 * based on Tailwind UI v3.4.17 and Heroicons v2.2.0
 */

"use client";

import { useState } from "react";
import {
  Dialog,
  DialogPanel,
  Disclosure,
  DisclosureButton,
  DisclosurePanel,
  Popover,
  PopoverButton,
  PopoverGroup,
  PopoverPanel,
} from "@headlessui/react";
import {
  ArrowPathIcon,
  Bars3Icon,
  ChartPieIcon,
  CursorArrowRaysIcon,
  FingerPrintIcon,
  SquaresPlusIcon,
  XMarkIcon,
} from "@heroicons/react/24/outline";
import {
  ChevronDownIcon,
  PhoneIcon,
  PlayCircleIcon,
} from "@heroicons/react/20/solid";

/**
 * 数据驱动的导航结构示例
 */
const navigation = {
  logo: {
    href: "https://github.com/zhaohuijian",
    srOnly: "Huijian's Blog",
    src: "https://2.gravatar.com/avatar/2883808b7665c2a281d37dca1e69adf1c14ed0fd5394260e25c1332d1fd365ff",
  },
  navItems: [
    {
      title: "Product",
      /*
       * 子菜单项(subItems)
       * 用于 Popover / Disclosure 下拉时列出的链接
       */
      subItems: [
        {
          name: "Analytics",
          description: "Get a better understanding of your traffic",
          href: "#",
          icon: ChartPieIcon,
        },
        {
          name: "Engagement",
          description: "Speak directly to your customers",
          href: "#",
          icon: CursorArrowRaysIcon,
        },
        {
          name: "Security",
          description: "Your customers’ data will be safe and secure",
          href: "#",
          icon: FingerPrintIcon,
        },
        {
          name: "Integrations",
          description: "Connect with third-party tools",
          href: "#",
          icon: SquaresPlusIcon,
        },
        {
          name: "Automations",
          description: "Build strategic funnels that will convert",
          href: "#",
          icon: ArrowPathIcon,
        },
      ],
      /**
       * callsToAction:
       * 用于子菜单底部那两格
       */
      callsToAction: [
        { name: "Watch demo", href: "#", icon: PlayCircleIcon },
        { name: "Contact sales", href: "#", icon: PhoneIcon },
      ],
    },
    {
      title: "Features",
      /*
       * 子菜单项(subItems)
       * 用于 Popover / Disclosure 下拉时列出的链接
       */
      subItems: [
        {
          name: "Analytics",
          description: "Get a better understanding of your traffic",
          href: "#",
        },
        {
          name: "Engagement",
          description: "Speak directly to your customers",
          href: "#",
        },
        {
          name: "Security",
          description: "Your customers’ data will be safe and secure",
          href: "#",
        },
        {
          name: "Integrations",
          description: "Connect with third-party tools",
          href: "#",
        },
        {
          name: "Automations",
          description: "Build strategic funnels that will convert",
          href: "#",
        },
      ],
    },
    {
      title: "Marketplace",
      href: "#",
    },
    {
      title: "Company",
      /*
       * 子菜单项(subItems)
       * 用于 Popover / Disclosure 下拉时列出的链接
       */
      subItems: [
        {
          name: "Analytics",
          href: "#",
        },
        {
          name: "Engagement",
          href: "#",
        },
        {
          name: "Security",
          href: "#",
        },
        {
          name: "Integrations",
          href: "#",
        },
        {
          name: "Automations",
          href: "#",
        },
      ],
    },
  ],
  /**
   * 登录链接
   */
  loginLink: {
    href: "#",
    label: "Log in",
  },
};

export default function Example() {
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);

  return (
    <div className="bg-white">
      <header className="absolute inset-x-0 top-0 z-5">
        <nav
          className="mx-auto flex max-w-7xl items-center justify-between p-6 lg:px-8"
          aria-label="Global"
        >
          {/* Logo 区域 */}
          <div className="flex lg:flex-1 lg:items-center lg:gap-x-8">
            <a href={navigation.logo.href} className="-m-1.5 p-1.5">
              <span className="sr-only">{navigation.logo.srOnly}</span>
              <img
                className="h-8 w-auto rounded-sm"
                src={navigation.logo.src}
                alt=""
              />
            </a>
          </div>

          {/* 移动端菜单按钮 */}
          <div className="flex lg:hidden">
            <button
              type="button"
              className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-700"
              onClick={() => setMobileMenuOpen(true)}
            >
              <span className="sr-only">Open main menu</span>
              <Bars3Icon className="size-6" aria-hidden="true" />
            </button>
          </div>

          {/* 桌面端的菜单 */}
          <PopoverGroup className="hidden lg:flex lg:gap-x-12">
            {navigation.navItems.map((item) => {
              // 如果有 subItems,渲染一个 Popover,里面包含子菜单
              if (item.subItems && item.subItems.length > 0) {
                return (
                  <Popover className="relative" key={item.title}>
                    <PopoverButton className="flex items-center gap-x-1 text-sm font-semibold text-gray-900">
                      {item.title}
                      <ChevronDownIcon
                        className="size-5 flex-none text-gray-400"
                        aria-hidden="true"
                      />
                    </PopoverButton>

                    {/* PopoverPanel,显示子菜单 */}
                    <PopoverPanel
                      transition
                      className="absolute top-full -left-8 z-10 mt-3 w-max max-w-md overflow-hidden rounded-3xl bg-white shadow-lg ring-1 ring-gray-900/5 transition
                               data-closed:translate-y-1 data-closed:opacity-0 data-enter:duration-200 data-enter:ease-out data-leave:duration-150 data-leave:ease-in"
                      /* 也可以配合 Transition 组件做过渡 */
                    >
                      <div className="p-4">
                        {item.subItems.map((sub) => (
                          <div
                            key={sub.name}
                            className="group relative flex items-center gap-x-6 rounded-lg p-4 text-sm hover:bg-gray-50"
                          >
                            {sub.icon && (
                              <div className="flex size-11 flex-none items-center justify-center rounded-lg bg-gray-50 group-hover:bg-white">
                                <sub.icon
                                  className="size-6 text-gray-600 group-hover:text-indigo-600"
                                  aria-hidden="true"
                                />
                              </div>
                            )}
                            <div className="flex-auto">
                              <a
                                href={sub.href}
                                className="block font-semibold text-gray-900"
                              >
                                {sub.name}
                                <span className="absolute inset-0" />
                              </a>
                              {sub.description && (
                                <p className="mt-1 text-gray-600">
                                  {sub.description}
                                </p>
                              )}
                            </div>
                          </div>
                        ))}
                      </div>
                      {/* callsToAction 区域 */}
                      {item.callsToAction && item.callsToAction.length > 0 && (
                        <div className="grid grid-cols-2 divide-x divide-gray-900/5 bg-gray-50">
                          {item.callsToAction.map((cta) => (
                            <a
                              key={cta.name}
                              href={cta.href}
                              className="flex items-center justify-center gap-x-2.5 p-3 text-sm font-semibold text-gray-900 hover:bg-gray-100"
                            >
                              <cta.icon
                                className="size-5 flex-none text-gray-400"
                                aria-hidden="true"
                              />
                              {cta.name}
                            </a>
                          ))}
                        </div>
                      )}
                    </PopoverPanel>
                  </Popover>
                );
              }

              // 如果没有 subItems,则只是普通的链接
              return (
                <a
                  key={item.title}
                  href={item.href}
                  className="text-sm font-semibold text-gray-900"
                >
                  {item.title}
                </a>
              );
            })}
          </PopoverGroup>

          {/* 桌面端:右侧的登录按钮 */}
          <div className="hidden lg:ml-8 lg:flex lg:items-center lg:border-l lg:border-slate-900/15 lg:pl-8">
            <a href="/login">Sign in</a>
            <a
              className="inline-flex justify-center rounded-lg text-sm font-semibold py-2.5 px-4 bg-slate-900 text-white hover:bg-slate-700 -my-2.5 ml-8"
              href="/all-access"
            >
              <span>
                Get all-access <span aria-hidden="true">→</span>
              </span>
            </a>
          </div>
        </nav>

        {/* 移动端侧边栏菜单 */}
        <Dialog
          as="div"
          className="lg:hidden"
          open={mobileMenuOpen}
          onClose={setMobileMenuOpen}
        >
          {/* 背景遮罩 */}
          <div className="fixed inset-0 z-10 bg-slate-900/25 backdrop-blur-sm transition-opacity" />

          <DialogPanel className="fixed inset-y-0 right-0 z-10 w-full overflow-y-auto bg-white px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10">
            {/* 顶部Logo 和 关闭按钮 */}
            <div className="flex items-center justify-between">
              <a href={navigation.logo.href} className="-m-1.5 p-1.5">
                <span className="sr-only">{navigation.logo.srOnly}</span>
                <img className="h-8 w-auto" src={navigation.logo.src} alt="" />
              </a>
              <button
                type="button"
                className="-m-2.5 rounded-md p-2.5 text-gray-700"
                onClick={() => setMobileMenuOpen(false)}
              >
                <span className="sr-only">Close menu</span>
                <XMarkIcon className="size-6" aria-hidden="true" />
              </button>
            </div>

            {/* 移动端菜单本体 */}
            <div className="mt-6 flow-root">
              <div className="-my-6 divide-y divide-gray-500/10">
                {/* 顶层导航区 */}
                <div className="space-y-2 py-6">
                  {navigation.navItems.map((item) => {
                    // 如果有 subItems,则在移动端使用 Disclosure
                    if (item.subItems && item.subItems.length > 0) {
                      return (
                        <Disclosure as="div" className="-mx-3" key={item.title}>
                          <DisclosureButton className="group flex w-full items-center justify-between rounded-lg py-2 pr-3.5 pl-3 text-base font-semibold text-gray-900 hover:bg-gray-50">
                            {item.title}
                            <ChevronDownIcon
                              className="size-5 flex-none group-data-open:rotate-180"
                              aria-hidden="true"
                            />
                          </DisclosureButton>
                          <DisclosurePanel className="mt-2 space-y-2">
                            {[
                              ...item.subItems,
                              ...(item.callsToAction || []),
                            ].map((sub) => (
                              <DisclosureButton
                                key={sub.name}
                                as="a"
                                href={sub.href}
                                className="block rounded-lg py-2 pr-3 pl-6 text-sm font-semibold text-gray-900 hover:bg-gray-50"
                              >
                                {sub.name}
                              </DisclosureButton>
                            ))}
                          </DisclosurePanel>
                        </Disclosure>
                      );
                    }

                    // 否则普通的菜单项
                    return (
                      <a
                        key={item.title}
                        href={item.href}
                        className="-mx-3 block rounded-lg px-3 py-2 text-base font-semibold text-gray-900 hover:bg-gray-50"
                      >
                        {item.title}
                      </a>
                    );
                  })}
                </div>

                {/* 登录按钮区 */}
                <div className="py-6">
                  <div className="-my-2 space-y-4">
                    <a
                      className="block w-full py-2 font-semibold"
                      href="/login"
                    >
                      Sign in
                    </a>
                    <a
                      className="inline-flex justify-center rounded-lg text-sm font-semibold py-3 px-4 bg-slate-900 text-white hover:bg-slate-700 w-full"
                      href="/all-access"
                    >
                      <span>
                        Get all-access <span aria-hidden="true">→</span>
                      </span>
                    </a>
                  </div>
                </div>
              </div>
            </div>
          </DialogPanel>
        </Dialog>
      </header>
    </div>
  );
}