From 69cad1c8b3190c78d873d9a820c4818ac29e232a Mon Sep 17 00:00:00 2001 From: Nathan Alder Date: Thu, 9 Jan 2025 22:19:02 +0100 Subject: [PATCH] Feature/46 desktop-navigation with example (#50) * feat: Create Desktop navigation with example in theme * chore: Add "use client" in nav-context.tsx --- nextjs/src/app/[locale]/theme/page.tsx | 27 ++++--- nextjs/src/app/[locale]/theme/texts.ts | 56 ++++++++++++++ nextjs/src/components/hero.tsx | 26 ++++++- nextjs/src/components/nav-bar/nav-context.tsx | 29 +++++++ .../components/nav-bar/nav-desktop-items.tsx | 76 +++++++++++++++++++ .../components/{ => nav-bar}/nav-desktop.tsx | 41 +++++++--- .../components/{ => nav-bar}/nav-mobile.tsx | 17 +++-- nextjs/src/components/nav-bar/nav.ts | 5 ++ nextjs/src/components/topbar-actions.tsx | 2 - nextjs/src/components/topbar.tsx | 15 ++-- 10 files changed, 256 insertions(+), 38 deletions(-) create mode 100644 nextjs/src/components/nav-bar/nav-context.tsx create mode 100644 nextjs/src/components/nav-bar/nav-desktop-items.tsx rename nextjs/src/components/{ => nav-bar}/nav-desktop.tsx (55%) rename nextjs/src/components/{ => nav-bar}/nav-mobile.tsx (81%) create mode 100644 nextjs/src/components/nav-bar/nav.ts diff --git a/nextjs/src/app/[locale]/theme/page.tsx b/nextjs/src/app/[locale]/theme/page.tsx index c5b09b0..e3996b2 100644 --- a/nextjs/src/app/[locale]/theme/page.tsx +++ b/nextjs/src/app/[locale]/theme/page.tsx @@ -5,6 +5,7 @@ import Link from "@/components/link-button" import Title from "@/components/title" import Text from "@/components/text" import { + navItems, hero, intro, solutions, @@ -44,6 +45,8 @@ import { type Locale } from "@/hooks/useLocale" import SectionWhitepaper from "@/components/sections/section-whitepaper" import SectionCalendly from "@/components/sections/section-calendly" import ExternalScript from "@/components/external-script" +import Topbar from "@/components/topbar" +import { NavProvider } from "@/components/nav-bar/nav-context" export default function Theme({ params: { locale }, @@ -52,16 +55,20 @@ export default function Theme({ }) { return (
- - - <Text markdown={hero.text} /> - <Link href="https://www.adfinis.com" size="large"> - Learn how - </Link> - </Hero> + <NavProvider> + <Topbar navItems={navItems} /> + <Hero + color="white" + imageUrl="https://images.unsplash.com/photo-1682687220198-88e9bdea9931?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" + > + <Title markdown={hero.title} /> + <Text markdown={hero.text} /> + <Link href="https://www.adfinis.com" size="large"> + Learn how + </Link> + </Hero> + </NavProvider> + <Intro> <Title markdown={intro.title} align="center" /> <Text markdown={intro.text} className="grid gap-8" /> diff --git a/nextjs/src/app/[locale]/theme/texts.ts b/nextjs/src/app/[locale]/theme/texts.ts index 6abc37e..07cda67 100644 --- a/nextjs/src/app/[locale]/theme/texts.ts +++ b/nextjs/src/app/[locale]/theme/texts.ts @@ -1,6 +1,62 @@ import { type Card } from "@/components/cards/card" +import { NavItem } from "@/components/nav-bar/nav" import { CTA } from "@/lib/cta" +export const navItems: NavItem[] = [ + { + title: "Solutions", + items: [ + { + title: "HashiCorp", + items: [ + { + title: "Vault", + url: "/solutions/vault", + }, + { + title: "Terraform", + url: "/solutions/terraform", + }, + { + title: "Consul", + url: "/solutions/consul", + }, + ], + }, + { + title: "Red Hat", + items: [ + { + title: "OpenShift", + url: "/solutions/openshift", + }, + { + title: "Enterprise Linux & SAP Workloads", + url: "/solutions/enterprise-linux-sap-workloads", + }, + { + title: "Ansible Automation Platform", + url: "/solutions/ansible-automation-platform", + }, + { + title: "Red Hat Satellite", + url: "/solutions/red-hat-satellite", + }, + ], + }, + ], + }, + { + title: "Partners & Products", + items: [ + { + title: "Github", + url: "/partners/github", + }, + ], + }, +] + export const hero = { title: `## Potential. **Unlocked.**`, text: `Open Source is our key to innovation and sustainable digitalization! diff --git a/nextjs/src/components/hero.tsx b/nextjs/src/components/hero.tsx index 50ae82a..3fcc5a7 100644 --- a/nextjs/src/components/hero.tsx +++ b/nextjs/src/components/hero.tsx @@ -4,6 +4,7 @@ import Triangle from "./triangle" import clsx from "clsx" import React from "react" import { colors } from "@/lib/colors" +import { useNavContext } from "./nav-bar/nav-context" type HeroProps = { color: keyof typeof colors @@ -12,6 +13,7 @@ type HeroProps = { } const Hero: React.FC<HeroProps> = ({ imageUrl, children, color }) => { + const { navActive } = useNavContext() return ( <div className={clsx([ @@ -29,15 +31,33 @@ const Hero: React.FC<HeroProps> = ({ imageUrl, children, color }) => { alt="Hero Image" width={1920} height={1080} - className="absolute inset-0 object-cover object-center z-0 h-full w-full" + className={clsx([ + "absolute inset-0 object-cover object-center z-0 h-full w-full", + "transition-all duration-75", + { + "blur-sm": navActive, + }, + ])} + /> + <div + className={clsx([ + "z-0 absolute inset-0 bg-gradient-to-r from-stone/50 to-stone/0", + "transition-all duration-75", + { "bg-stone/60": navActive }, + ])} /> - <div className="z-0 absolute inset-0 bg-gradient-to-r from-stone/50 to-stone/0" /> <Triangle color={color} className="w-[50vw] h-auto absolute right-0 bottom-0" /> <section - className="relative container px-4 lg:px-0 mt-28 lg:mt-44" + className={clsx([ + "relative container px-4 lg:px-0 mt-28 lg:mt-44", + "transition-all duration-75", + { + "blur-sm": navActive, + }, + ])} data-scheme="dark" > <div className="w-full lg:w-1/2"> diff --git a/nextjs/src/components/nav-bar/nav-context.tsx b/nextjs/src/components/nav-bar/nav-context.tsx new file mode 100644 index 0000000..314361f --- /dev/null +++ b/nextjs/src/components/nav-bar/nav-context.tsx @@ -0,0 +1,29 @@ +"use client" +import React, { createContext, useState, useContext, ReactNode } from "react" + +type NavState = { + navActive: boolean + setNavActive: React.Dispatch<React.SetStateAction<boolean>> +} + +const NavContext = createContext<NavState | undefined>(undefined) + +/** + * @description NavProvider is a context provider that has a single shared state about whether any desktop nav item is active or not. + */ +export const NavProvider = ({ children }: { children: ReactNode }) => { + const [navActive, setNavActive] = useState<boolean>(false) + return ( + <NavContext.Provider value={{ navActive, setNavActive }}> + {children} + </NavContext.Provider> + ) +} + +export const useNavContext = () => { + const context = useContext(NavContext) + if (!context) { + throw new Error("useAppContext must be used within a NavProvider") + } + return context +} diff --git a/nextjs/src/components/nav-bar/nav-desktop-items.tsx b/nextjs/src/components/nav-bar/nav-desktop-items.tsx new file mode 100644 index 0000000..b5f691a --- /dev/null +++ b/nextjs/src/components/nav-bar/nav-desktop-items.tsx @@ -0,0 +1,76 @@ +import React, { useState } from "react" +import { Transition } from "@headlessui/react" +import { useNavContext } from "./nav-context" + +import type { NavItem } from "./nav" +import Link from "next/link" + +type NavDesktopItemsProps = { + navItem: NavItem +} +const NavDesktopItems: React.FC<NavDesktopItemsProps> = ({ navItem }) => { + const [isShowing, setIsShowing] = useState(false) + const { navActive, setNavActive } = useNavContext() + + const showDesktopItems = () => { + setIsShowing(true) + setNavActive(true) + } + + const hideDesktopItems = () => { + setIsShowing(false) + setNavActive(false) + } + + return ( + <div className="isolate z-50 pr-8" onMouseLeave={hideDesktopItems}> + <div className="mx-auto max-w-7xl"> + <div + onMouseEnter={showDesktopItems} + className="cursor-pointer inline-flex items-center gap-x-1 text-sm/6 font-semibold text-neutral py-4" + > + {navItem.title} + </div> + </div> + + <Transition + show={isShowing} + enter="transition delay-300 duration-300 ease-out" + enterFrom="opacity-0" + enterTo="translate-y-0 opacity-100" + leave="transition duration-75 ease-in" + leaveFrom="translate-y-0 opacity-100" + leaveTo="opacity-0" + > + <div className="absolute inset-x-0 top-14 -z-10 py-10"> + <div className="flex justify-start items-start gap-x-4 text-neutral"> + {navItem.items?.map((item, index) => ( + <div + className="grid grid-cols-1 content-start gap-4 pr-2 border-r min-h-72 border-neutral/30 w-1/6" + key={index} + > + <h3 className="text-16 leading-5 font-semibold"> + {item.title} + </h3> + {item.items?.map( + (subItem, subIndex) => + subItem.url && ( + <Link + className="font-normal" + key={subIndex} + href={subItem.url} + > + {subItem.title} + </Link> + ), + )} + </div> + ))} + </div> + </div> + </Transition> + </div> + ) +} + +export default NavDesktopItems diff --git a/nextjs/src/components/nav-desktop.tsx b/nextjs/src/components/nav-bar/nav-desktop.tsx similarity index 55% rename from nextjs/src/components/nav-desktop.tsx rename to nextjs/src/components/nav-bar/nav-desktop.tsx index 5a9e500..6e96420 100644 --- a/nextjs/src/components/nav-desktop.tsx +++ b/nextjs/src/components/nav-bar/nav-desktop.tsx @@ -1,11 +1,18 @@ -import React, { useEffect } from "react" +import React, { useEffect, useState } from "react" import clsx from "clsx" import useDetectScroll, { Axis } from "@smakss/react-scroll-direction" -import Logo from "./logo" -import TopbarActions from "./topbar-actions" +import Logo from "../logo" +import TopbarActions from "../topbar-actions" +import { NavItem } from "./nav" +import Link from "next/link" +import NavDesktopItems from "./nav-desktop-items" -const NavDesktop: React.FC = () => { - const [menuExpanded, setMenuExpanded] = React.useState(true) +type NavDesktopProps = { + navItems: NavItem[] +} + +const NavDesktop: React.FC<NavDesktopProps> = ({ navItems }) => { + const [menuExpanded, setMenuExpanded] = useState(true) const { scrollDir, scrollPosition } = useDetectScroll({ thr: 20, axis: Axis.Y, @@ -30,19 +37,21 @@ const NavDesktop: React.FC = () => { /** * @description only collapse the menu when the user has scrolled down */ - const onMouseMenuLeave = () => { + const handleMouseMenuLeave = () => { if (scrollPosition.top > 200) { setMenuExpanded(false) } } + const handleMouseEnter = () => { + setMenuExpanded(true) + } + return ( - <div - className="hidden lg:grid divide-y divide-jumbo/30 pr-[50px]" - onMouseEnter={() => setMenuExpanded(true)} - onMouseLeave={() => onMouseMenuLeave()} - > + <div className="hidden lg:grid divide-y divide-jumbo/30 pr-[50px]"> <section + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseMenuLeave} className={clsx([ "min-h-16 h-16 py-4 transition-all duration-150 flex justify-between items-center", { @@ -55,7 +64,15 @@ const NavDesktop: React.FC = () => { <TopbarActions /> </section> {menuExpanded && ( - <section className="hidden lg:block min-h-12" id="nav-items"></section> + <section className="hidden lg:block min-h-12" id="nav-items"> + <div className="flex justify-start items-center h-full relative"> + {navItems.map((item, index) => ( + <div className="" key={index}> + {item && <NavDesktopItems navItem={item} />} + </div> + ))} + </div> + </section> )} </div> ) diff --git a/nextjs/src/components/nav-mobile.tsx b/nextjs/src/components/nav-bar/nav-mobile.tsx similarity index 81% rename from nextjs/src/components/nav-mobile.tsx rename to nextjs/src/components/nav-bar/nav-mobile.tsx index 61fd420..d10f4bc 100644 --- a/nextjs/src/components/nav-mobile.tsx +++ b/nextjs/src/components/nav-bar/nav-mobile.tsx @@ -3,14 +3,19 @@ import clsx from "clsx" import { Transition } from "@headlessui/react" import { useClickAway } from "@uidotdev/usehooks" -import IconHamburgerMenu from "./icons/icon-hamburger-menu" +import IconHamburgerMenu from "../icons/icon-hamburger-menu" import Link from "next/link" -import ButtonLink from "./link-button" -import IconChevronRight from "./icons/icon-chevron-right" -import Logo from "./logo" -import TopbarActions from "./topbar-actions" +import ButtonLink from "../link-button" +import IconChevronRight from "../icons/icon-chevron-right" +import Logo from "../logo" +import TopbarActions from "../topbar-actions" +import type { NavItem } from "./nav" -const NavMobile: React.FC = () => { +type NavMobileProps = { + navItems: NavItem[] +} + +const NavMobile: React.FC<NavMobileProps> = ({ navItems }) => { const [isOpen, setIsOpen] = useState(false) const ref = useClickAway(() => { setIsOpen(false) diff --git a/nextjs/src/components/nav-bar/nav.ts b/nextjs/src/components/nav-bar/nav.ts new file mode 100644 index 0000000..2ba8ee2 --- /dev/null +++ b/nextjs/src/components/nav-bar/nav.ts @@ -0,0 +1,5 @@ +export type NavItem = { + title: string + url?: string + items?: NavItem[] +} diff --git a/nextjs/src/components/topbar-actions.tsx b/nextjs/src/components/topbar-actions.tsx index 9952694..33ecb01 100644 --- a/nextjs/src/components/topbar-actions.tsx +++ b/nextjs/src/components/topbar-actions.tsx @@ -3,8 +3,6 @@ import LocaleSwitcher from "./locale-switcher" import Search from "./search" const TopbarActions: React.FC = () => { - // Add your component logic here - return ( <div className="flex justify-end items-center gap-4 lg:gap-6 text-neutral"> <Search /> diff --git a/nextjs/src/components/topbar.tsx b/nextjs/src/components/topbar.tsx index f5b1975..cba91e1 100644 --- a/nextjs/src/components/topbar.tsx +++ b/nextjs/src/components/topbar.tsx @@ -1,9 +1,14 @@ "use client" import React, { useEffect } from "react" -import NavMobile from "./nav-mobile" -import NavDesktop from "./nav-desktop" +import NavMobile from "./nav-bar/nav-mobile" +import NavDesktop from "./nav-bar/nav-desktop" +import { NavItem } from "./nav-bar/nav" -const Topbar: React.FC = () => { +type TopbarProps = { + navItems: NavItem[] +} + +const Topbar: React.FC<TopbarProps> = ({ navItems }) => { return ( <div> <div @@ -11,8 +16,8 @@ const Topbar: React.FC = () => { id="navbar" > <div className="container mr-0"> - <NavMobile /> - <NavDesktop /> + <NavMobile navItems={navItems} /> + <NavDesktop navItems={navItems} /> </div> </div> </div>