Next.js: Update Navbar component when navigating to new route - reactjs

Goal: Update Navbar accent when page is navigated to
Current: The Navbar accent has a delayed update. It updates to the last visited page.
For example: if I start on page1, then click on page2, the accent remains on page1 until I click on page2 again. If I then click on page3, page2 will now have the accent.
Attempted: I am using next.router in useEffect to try to update the accent on change of router.
Code:
app.js
import "tailwindcss/tailwind.css";
import Layout from "../components/Layout";
function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
export default MyApp;
layout.js
import Navbar from "./Navbar";
export default function Layout({ children }) {
return (
<div className="layout">
<Navbar />
<div className="">{children}</div>
</div>
);
}
navbar.js
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
function classNames(...classes) {
return classes.filter(Boolean).join(" ");
}
const navigation = [
{ name: "page1", href: "/", current: true },
{ name: "page2", href: "/page2", current: false },
{ name: "page3", href: "/page3", current: false },
];
export default function Navbar() {
const router = useRouter();
useEffect(
(e) => {
navigation.map((item) => {
if (router.route == item.href) {
item.current = true;
} else {
item.current = false;
}
});
console.log(router.route);
},
[router]
);
return (
<div>
<div>
{navigation.map((item) => (
<Link href={item.href} key={item.name}>
<a
className={classNames(
item.current
? "border-indigo-500 text-gray-900"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
"inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
)}
aria-current={item.current ? "page" : undefined}
>
{item.name}
</a>
</Link>
))}
</div>
</div>
);
}
index.js page2 & page3 are similar.
export default function Home() {
return (
<div className="flex flex-col items-center justify-center min-h-screen py-2">
Hello
</div>
);
}

Drop the useEffect and just go with
import Link from "next/link";
import { useRouter } from "next/router";
function classNames(...classes) {
return classes.filter(Boolean).join(" ");
}
const navigation = [
{ name: "page1", href: "/" },
{ name: "page2", href: "/page2" },
{ name: "page3", href: "/page3" },
];
export default function Navbar() {
const router = useRouter();
return (
<div>
<div>
{navigation.map((item) => (
<Link href={item.href} key={item.name}>
<a
className={classNames("inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium", router.route === item.href ? "border-indigo-500 text-gray-900" : "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700")}
aria-current={router.route === item.href ? "page" : undefined}
>
{item.name}
</a>
</Link>
))}
</div>
</div>
);
}

Maybe using the item.current booleans in your return statement could help you. e.g.
because you have
{ name: "page1", href: "/", current: true },
and you're specifying that it's "current" value is true when clicked.
You could do something like this:
return (
<div>
<div>
{navigation.map((item) => (
{item.current===false
<Link href={item.href} key={item.name}>
<a
className={classNames(
item.current
? "border-indigo-500 text-gray-900"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
"inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
)}
aria-current={item.current ? "page" : undefined}
>
{item.name}
</a>
</Link>}
{item.current===true
*** WHATEVER YOU WAN'T TO BE DIFFERENT ***
}
))}
</div>
</div>
);
}

Related

Array in Array or how how to embed code in a visual element

Array in Array or how how to embed code in a visual element.
I want to use collapse sub menu , for my dashboard. I try with one element from Sidebar, but, i don't know how shapes this code.
Here is my code , i shortened it a bit. I want to add in collapse visual element children .
I know how to map again children . Somting like this:
{children && children.map((item) => {
const { icon, label, ref } = item;
return (
<Link to={ref} key={label} className={`flex gap-3 items-center py-3 px-4 font-medium`}>
<span>{icon}</span>
<span>{label}</span>
</Link>
)
})}
import { Link, useNavigate } from 'react-router-dom';
import EqualizerIcon from '#mui/icons-material/Equalizer';
import ShoppingBagIcon from '#mui/icons-material/ShoppingBag';
import InventoryIcon from '#mui/icons-material/Inventory';
import AddBoxIcon from '#mui/icons-material/AddBox';
import Avatar from '#mui/material/Avatar';
import { useDispatch, useSelector } from 'react-redux';
import './Sidebar.css';
import { useSnackbar } from 'notistack';
import { logoutUser } from '../../../actions/userAction';
import { BsChevronDown } from 'react-icons/bs';
const navMenu = [
{
icon: <EqualizerIcon />,
label: "Dashboard",
ref: "/admin/dashboard",
},
{
icon: <ShoppingBagIcon />,
label: "Orders",
ref: "/admin/orders",
},
{
icon: <InventoryIcon />,
label: "Products",
ref: "/admin/products",
collapse: <BsChevronDown />,
children: [
{
icon: <AddBoxIcon />,
label: "Add Product",
ref: "/admin/new_product",
},
]
},
];
const Sidebar = ({ activeTab, setToggleSidebar }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { enqueueSnackbar } = useSnackbar();
const { user } = useSelector((state) => state.user);
const handleLogout = () => {
dispatch(logoutUser());
enqueueSnackbar("Logout Successfully", { variant: "success" });
navigate("/login");
}
return (
<aside className="sidebar z-10 sm:z-0 block min-h-screen left-0 pb-14 max-h-screen w-3/4 sm:w-1/5 bg-gray-800 text-white overflow-x-hidden border-r">
<div className="flex items-center gap-3 bg-gray-700 p-2 rounded-lg shadow-lg my-4 mx-3.5">
<Avatar
alt="Avatar"
src={user.avatar.url}
/>
<div className="flex flex-col gap-0">
<span className="font-medium text-lg">{user.name}</span>
<span className="text-gray-300 text-sm">{user.email}</span>
</div>
</div>
<div className="flex flex-col w-full gap-0 my-8">
{navMenu.map((item, index) => {
const { icon, label, ref, children, collapse } = item;
return (
<>
<Link to={ref} className={`${activeTab === index ? "bg-gray-700" : "hover:bg-gray-700"} flex gap-3 items-center py-3 px-4 font-medium`}>
<span>{icon}</span>
<span>{label}</span>
<span >{collapse}</span>
</Link>
</>
)
}
)}
</div>
</aside>
)
};
export default Sidebar;
You can have an object with the isCollapsed property added to it. You can set the initial state to false, and when a user clicks on it, you can toggle the state, so it'll go from false to true. This is a sample code, but I hope this will help conceptualize it.
import React, { useState } from 'react';
const navMenu = {
"Dashboard": {
icon: <EqualizerIcon />,
ref: "/admin/dashboard",
isCollapsed: false
},
"Orders": {
icon: <ShoppingBagIcon />,
ref: "/admin/orders",
isCollapsed: false
},
"Products": {
icon: <InventoryIcon />,
ref: "/admin/products",
isCollapsed: false,
collapse: <BsChevronDown />,
children: [
{
icon: <AddBoxIcon />,
label: "Add Product",
ref: "/admin/new_product",
},
]
},
}
const [navMenuBar, setNavMenuBar] = useState(navMenu)
function Collapse() {
Object.keys(navMenuBar).map(key => {
const { icon, label, ref } = navMenuBar[key];
return (
<Link to={ref} key={label} className={`flex gap-3 items-center py-3 px-4 font-medium`}>
<button onClick={() => {
navMenuBar[key]['isCollapsed'] = !navMenuBar[key]['isCollapsed']
setNavMenuBar(navMenuBar)
}}>{isCollapsed ? expand : minimize}</button>
<span>{icon}</span>
<span>{label}</span>
</Link>
)
})
}
exactly how to do this, because i use bootstrap model in span
<span onClick={() => setOpen(!open)}
aria-controls="example-collapse-text"
aria-expanded={open}>{collapse}</span>
<Collapse in={open}>
<div id="example-collapse-text">
Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus
terry richardson ad squid. Nihil anim keffiyeh helvetica, craft beer
labore wes anderson cred nesciunt sapiente ea proident.
</div>
</Collapse>
that's how they all unfold

How to avoid closing DropDown on MouseOver?

I made Navigation component with dynamic menu items.
`
import React, { useState } from "react";
import NavMenuItems from "../data/NavMenuItems";
function NavBar() {
const [dropDown, setDropDown] = useState({});
const setDropDownOpen = (name) => {
setDropDown({[name]: true });
};
const setDropDownClose = (name) => {
setDropDown({[name]: false });
};
return (
<div className="flex flex-row my-2 mx-5">
{NavMenuItems.map((menu, index) => (
<>
<div key={menu.item} className="relative flex flex-col mx-1">
<div
className="bg-[#121C24] px-2 h-5 text-white text-sm hover:bg-green-700 hover:text-black hover:cursor-pointer "
onMouseEnter={() => setDropDownOpen(menu.item)}
onMouseLeave={() => setDropDownClose(menu.item)}
>
{menu.item}
</div>
{dropDown[menu.item] && (
<div className="bg-slate-200 absolute top-6 px-4 py-2"
onMouseEnter={() => setDropDownOpen(menu.item)}
onMouseLeave={() => setDropDownClose(menu.item)}
>
{menu.subitems.map((submenu, index) => (
<div key={index}>{submenu}</div>
))}
</div>
)}
</div>
</>
))}
</div>
);
}
export default NavBar;
NavMenuItems.js
`
const NavMenuItems = [
{
item: "Events",
subitems: ["Event1", "Event2", "Event2"],
},
{
item: "Reports",
subitems: ["Reports1", "Reports2", "Reports3"],
},
];
export default NavMenuItems
When i mouseover on tabs, its working fine. but when i move over dropdown sub menu, it closes and cant select anything in submenu items.
Can someone help with this?

React Headless UI Spin Transition Night Theme Toggle

I've been trying to make a spin animation for my night and sun icon when toggled but I just can't figure it out. I'm using Nextjs, Tailwind, and Headless UI for the animation library. I feel like I'm close any help would be very much appreciated. Thx
This is what I have so far, all of the code pertaining to my question is in the handleThemeChange function:
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useTheme } from 'next-themes';
import {
Brain,
MoonStars,
RocketLaunch,
PaperPlaneTilt,
Sun,
House,
Link as LinkIcon,
} from 'phosphor-react';
import { Transition } from '#headlessui/react';
const Navbar = () => {
const { systemTheme, theme, setTheme } = useTheme();
const [isShowingMoon, setIsShowingMoon] = useState(true);
const [mounted, setMount] = useState(false);
const [navStyles, setNavStyles] = useState(false);
const router = useRouter();
useEffect(() => {
setMount(true);
const handleNavStyles = () => {
if (window.scrollY > 45) {
setNavStyles(true);
} else {
setNavStyles(false);
}
};
window.addEventListener('scroll', handleNavStyles);
}, []);
const handleThemeChange = () => {
if (!mounted) return null;
const currentTheme = theme === 'system' ? systemTheme : theme;
if (currentTheme === 'dark') {
return (
<Transition
show={isShowingMoon === true}
enter="transform transition duration-[1000ms]"
enterFrom="opacity-0 rotate-[-120deg] scale-50"
enterTo="opacity-100 rotate-0 scale-100"
leave="transform duration-200 transition ease-in-out"
leaveFrom="opacity-100 rotate-0 scale-100 "
leaveTo="opacity-0 scale-95 "
>
<MoonStars
className="icon-style"
role="button"
onClick={() => {
setTheme('light');
setIsShowingMoon(false);
}}
/>
</Transition>
);
} else {
return (
<Transition
show={isShowingMoon === false}
enter="transform transition duration-[1000ms]"
enterFrom="opacity-0 rotate-[-120deg] scale-50"
enterTo="opacity-100 rotate-0 scale-100"
leave="transform duration-200 transition ease-in-out"
leaveFrom="opacity-100 rotate-0 scale-100 "
leaveTo="opacity-0 scale-95 "
>
<Sun
className="icon-style"
role="button"
onClick={() => {
setTheme('dark');
setIsShowingMoon(true);
}}
/>
</Transition>
);
}
};
return (
<header className="sticky top-0 z-10 backdrop-blur-md">
<nav
className={`container flex items-center justify-between space-x-3 py-3 sm:py-5 ${
navStyles ? 'border-color border-b' : ''
}`}
>
<Link href={'/'} aria-label="Home Page">
<House
className={`icon-style cursor-pointer ${
router.pathname === '/' ? 'highlight' : ''
}`}
/>
</Link>
<Link href={'/projects'} aria-label="Projects">
<RocketLaunch
className={`icon-style cursor-pointer ${
router.pathname === '/projects' ||
router.pathname === '/projects/[id]'
? 'highlight'
: ''
}`}
/>
</Link>
<Link href={'/skills'} aria-label="Skills">
<Brain
className={`icon-style cursor-pointer ${
router.pathname === '/skills' ? 'highlight' : ''
}`}
/>
</Link>
<Link href={'/links'} aria-label="Links">
<LinkIcon
className={`icon-style cursor-pointer ${
router.pathname === '/links' ? 'highlight' : ''
}`}
/>
</Link>
<a href="mailto:xilaluna2#gmail.com" aria-label="Send Email">
<PaperPlaneTilt className="icon-style" />
</a>
{handleThemeChange()}
</nav>
</header>
);
};
export default Navbar;

Nextjs navbar active class only becomes active on a second click

I'm trying to create header component for next.js/tailwindcss app. The nav active class only shows active when clicked on a second time. I'd like for it to be active upon the first click. Where am I going wrong? What element should I target with tailwindcss to reflect active state?
navlink.js file:
import Link from 'next/link';
const NavItem = ({ text, href, active }) => {
return (
<Link href={href}>
<a
className={`nav__item ${
active ? 'active underline underline-offset-8' : ''
}`}
>
{text}
</a>
</Link>
);
};
export default NavItem;
header.js :
import Image from 'next/image';
import React, { useState } from 'react';
import NavItem from './NavItem';
const MENU_LIST = [
{ text: 'About', href: '/about' },
{ text: 'My Work', href: '/MyWork' },
{ text: 'Blog', href: '/blog' },
{ text: 'Contact', href: '/contact' },
];
const Header = () => {
const [navActive, setNavActive] = useState(null);
const [activeIdx, setActiveIdx] = useState(-1);
return (
<header className="bg-white">
<nav className="max-w-5xl mx-auto border border-top-gray">
{/*containment div*/}
<div className="flex justify-between">
{/*Logo Container*/}
<div className="cursor-pointer">
<a href="/">
<Image
src="/../public/images/soulLogo.webp"
alt="site logo"
width={233}
height={144}
/>
</a>
</div>
{/*Link Container*/}
<div className="hidden md:flex">
<div
onClick={() => setNavActive(!navActive)}
className={`nav__menu-bar md:underline underline-offset-8 decoration-black `}
></div>
<div
className={`${
navActive
? 'active underline underline-offset-8 decoration-black'
: ''
} nav__menu-list flex items-center space-x-8 text-gray-700 tracking-wider `}
>
{MENU_LIST.map((menu, idx) => (
<div
onClick={() => {
setActiveIdx(idx);
setNavActive(false);
}}
key={menu.text}
>
<NavItem active={activeIdx === idx} {...menu} />
</div>
))}
</div>
</div>
{/*Mobile Menu button*/}
<div className="flex relative flex-col gap-y-2 cursor-pointer pt-14 pr-3 md:hidden">
<div className="w-24 h-1 bg-black shadow-gray-700 rounded"></div>
<div className="w-24 h-1 bg-black shadow-gray-700 rounded"></div>
<div className="w-24 h-1 bg-black shadow-gray-700 rounded"></div>
</div>
{/*Mobile Menu*/}
<
</div>
</nav>
</header>
);
};
export default Header;
I think we used the same code and I ran into the same problem.
I fixed it with with using next/router and comparing the paths inside the NavItem.js component.
navitem.js
import Link from "next/link";
import { useRouter } from 'next/router';
const NavItem = ({ href, text }) => {
const router = useRouter();
const currentRoute = router.pathname;
return (
<Link href={href} className={currentRoute === `${href}` ? 'active' : ''}> {text} </Link>
);
};
export default NavItem;
navbar.js
{PRIMARY_NAVIGATION_LIST.map((menu) => (
<div key={menu.text} >
<NavItem
href={menu.href}
text={menu.text}
/>
</div>
))}

Error pulling menus wordpress typescript react nextjs

I'm starting in react with typescript and I must be making a mistake, in typing, I get the following error: "TypeError: Cannot read property 'map' of undefined"
import React from 'react'
import { NextPage } from 'next'
interface HeaderProps {
menu: Array<{
title: string
url: string
}>
}
const Header: NextPage<HeaderProps> = (props: HeaderProps) => {
...
I'm trying to pull menus from Wordpress
Header.getInitialProps = async () => {
const res = await fetch('http://localhost/wp-json/myroutes/menu')
const json = await res.json()
return {
props: {
menu: json
}
}
}
export default Header
So for this, assuming this is still an issue you're grappling with, you can use a query with a fragment like so to get the data externally from headless wordpress (highly advise you use the WP-GraphQL plugin)
fragment DynamicNavFragment on WordpressMenuItem {
id
label
path
parentId
}
Then for the query, you import the fragment using a # at the top of the file:
# import DynamicNavFragment from '../Partials/dynamic-nav-fields.graphql'
query DynamicNav(
$idHead: ID!
$idTypeHead: WordpressMenuNodeIdTypeEnum!
$idFoot: ID!
$idTypeFoot: WordpressMenuNodeIdTypeEnum!
) {
Header: wordpress {
menu(id: $idHead, idType: $idTypeHead) {
menuItems(where: { parentId: 0 }) {
edges {
node {
...DynamicNavFragment
childItems {
edges {
node {
...DynamicNavFragment
childItems {
edges {
node {
...DynamicNavFragment
}
}
}
}
}
}
}
}
}
}
}
Footer: wordpress {
menu(id: $idFoot, idType: $idTypeFoot) {
menuItems(where: { parentId: 0 }) {
edges {
node {
...DynamicNavFragment
childItems {
edges {
node {
...DynamicNavFragment
}
}
}
}
}
}
}
}
}
To generate type definitions from this code, us a codegen.yml file as follows:
overwrite: true
schema:
${ONEGRAPH_API_URL_YML}:
headers:
Authorization: Bearer ${WORDPRESS_AUTH_REFRESH_TOKEN_YML}
documents: 'graphql/**/*.graphql'
generates:
graphql/generated/graphql.tsx:
plugins:
- typescript:
constEnums: false
enumsAsTypes: false
numericEnums: false
namingConvention: keep
futureProofEnums: false
enumsAsConst: false
onlyOperationTypes: false
maybeValue: T | null | undefined
noExport: false
enumPrefix: true
fieldWrapperValue: T
wrapFieldDefinitions: true
skipTypename: false
nonOptionalTypename: false
useTypeImports: false
avoidOptionals: true
declarationKind:
input: interface
type: interface
- typescript-operations:
declarationKind:
input: interface
type: interface
avoidOptionals: true
exportFragmentSpreadSubTypes: true
- typescript-react-apollo:
addDocBlocks: true
reactApolloVersion: 3
documentMode: documentNodeImportFragments
config:
maybeValue: T | null | undefined
declarationKind:
input: interface
type: interface
documentNodeImportFragments: true
reactApolloVersion: 3
withHooks: true
numericEnums: false
namingConvention: keep
withHOC: false
avoidOptionals: true
withComponent: false
exportFragmentSpreadSubTypes: true
addDocBlocks: true
graphql/graphql.schema.graphql:
plugins:
- schema-ast
config:
commentDescriptions: true
graphql/graphql.schema.json:
plugins:
- introspection
config:
commentDescriptions: true
hooks:
afterAllFileWrite:
- prettier --write
This goes down several layers to get sub-paths and sub-sub-paths dynamically injected in the nav (header). Now for using the data itself, I can include my navbar + dynamic nav and layout components. It is a bit of code, but map through it and you'll follow along (this is all nextjs+typescript)
headless-navbar.tsx
import { useState, FC, useRef, Fragment } from 'react';
import cn from 'classnames';
import { useRouter } from 'next/router';
import Link from 'next/link';
import { Transition, Listbox } from '#headlessui/react';
import { NavArrow } from '#/components/UI/Icons';
import { DynamicNavQuery } from '#/graphql/generated/graphql';
import css from './headless-navbar.module.css';
import { DynamicNavDissected } from '#/types/dynamic-nav';
export interface HeadlessNavbarProps
extends DynamicNavDissected {
header: DynamicNavQuery['Header'];
className?: string;
}
const HeadlessNavbar: FC<HeadlessNavbarProps> = ({
header,
className,
node
}) => {
const [isOpen, setIsOpen] = useState(false);
const [subOpen, setSubOpen] = useState(false);
const [selectedCategory, setSelectedCategory] = useState(node);
const { pathname } = useRouter();
const refAcceptor = useRef() as React.MutableRefObject<HTMLDivElement>;
return (
<>
{header != null &&
header.menu != null &&
header.menu.menuItems != null &&
header.menu.menuItems.edges != null &&
header.menu.menuItems.edges.length > 0 ? (
header.menu.menuItems.edges.map((top, i) => {
return top != null &&
top.node != null &&
top.node.label != null ? (
<>
<Link
href={top.node.path}
as={top.node.path}
passHref
scroll={true}
key={top.node.id}
>
<a
id='top'
className={cn(className, {
[css.active]: pathname === top.node.path,
[css.link]: pathname !== top.node.path
})}
>
<p>{top.node.label}</p>
</a>
</Link>
{top.node.childItems != null &&
top.node.childItems.edges != null &&
top.node.childItems.edges.length > 0 ? (
<div className='relative z-150 -ml-2'>
<button
onClick={() => setIsOpen(!isOpen)}
id='sub-menu'
aria-haspopup={true}
aria-expanded={true}
type='button'
className={cn(css.topButton, {
'lg:-translate-x-4 ring-secondary-0 ring-1 rotate-0': isOpen,
'lg:-translate-x-5 ring-redditNav ring-0 -rotate-90': !isOpen
})}
>
<NavArrow className='select-none lg:w-5 lg:h-5' />
</button>
<Transition
show={isOpen}
enter='transition ease-out duration-200 '
enterFrom='transform opacity-0 translate-y-1'
enterTo='transform opacity-100 translate-y-0'
leave='transition ease-in duration-150'
leaveFrom='transform opacity-100 translate-y-0'
leaveTo='transform opacity-0 translate-y-1'
>
<div className={cn(css.transitionAlpha, '')}>
<div
className={css.transitionBeta}
ref={refAcceptor}
role='menu'
aria-orientation='vertical'
aria-labelledby='sub-menu'
>
<div className={css.transitionGamma}>
{top!.node!.childItems!.edges!.map((sub, j) => {
return sub != null &&
sub.node != null &&
sub.node.label != null &&
sub.node.parentId != null ? (
<Listbox
key={sub.node.id}
value={selectedCategory}
onChange={setSelectedCategory}
>
{({ open }) => (
<>
<div className={cn(css.divOpen)}>
<p className='text-lg'>{sub!.node!.label!}</p>
<Listbox.Button
aria-haspopup={true}
id='sub'
aria-expanded={true}
onClick={() => setSubOpen(!subOpen)}
className={cn(css.topButton, {
'lg:-translate-x-4 ring-secondary-0 ring-1 rotate-0': open,
'lg:-translate-x-5 ring-redditSearch ring-0 -rotate-90': !open
})}
>
<NavArrow className='select-none lg:w-5 lg:h-5' />
</Listbox.Button>
</div>
<Transition
show={open}
enter='transition ease-out duration-100'
enterFrom='transform opacity-0 scale-95'
enterTo='transform opacity-100 scale-100'
leave='transition ease-in duration-75'
leaveFrom='transform opacity-100 scale-100'
leaveTo='transform opacity-0 scale-95'
>
<Listbox.Options
static
className='outline-none select-none focus:outline-none'
>
{sub!.node!.childItems != null &&
sub!.node!.childItems.edges != null &&
sub!.node!.childItems.edges.length > 0 ? (
sub!.node!.childItems!.edges!.map(
(subsub, k) => {
return subsub != null &&
subsub.node != null &&
subsub.node.label != null &&
subsub.node.parentId != null ? (
<>
{open && (
<Listbox.Option
key={subsub.node.id}
className={cn(
css.subsub,
'text-base font-medium list-none outline-none'
)}
value={subsub!.node!.label}
>
<>
<Link
href={subsub!.node!.path}
passHref
key={k++}
>
<a
id='subsub'
className={cn(css.subsubanchor)}
key={subsub!.node!.id}
>
{subsub!.node!.label}
</a>
</Link>
</>
</Listbox.Option>
)}
</>
) : (
<></>
);
}
)
) : (
<></>
)}
</Listbox.Options>
</Transition>
</>
)}
</Listbox>
) : (
<></>
);
})}
</div>
</div>
</div>
</Transition>
</div>
) : (
<></>
)}
</>
) : (
<></>
);
})
) : (
<></>
)}
</>
);
};
export default HeadlessNavbar;
NOTE: DynamicNavDissected is defined in the #/types directory as follows
import {
Maybe,
DynamicNavFragmentFragment
} from '#/graphql/generated/graphql';
export interface DynamicNavDissected {
node?: Maybe<
{ __typename?: 'WordpressMenuItem' } & {
childItems: Maybe<
{
__typename?: 'WordpressMenuItemToMenuItemConnection';
} & {
edges: Maybe<
Array<
Maybe<
{
__typename?: 'WordpressMenuItemToMenuItemConnectionEdge';
} & {
node: Maybe<
{
__typename?: 'WordpressMenuItem';
} & DynamicNavFragmentFragment
>;
}
>
>
>;
}
>;
} & DynamicNavFragmentFragment
>;
}
headless-navbar.module.css
.active {
#apply p-2 rounded-md text-base text-secondary-0 font-bold 2xl:text-lg 2xl:tracking-tight;
&:hover {
#apply text-opacity-75 transition-opacity transform-gpu duration-300;
}
}
.link {
#apply p-2 rounded-md text-base text-secondary-0 font-semibold 2xl:text-lg 2xl:tracking-tight;
&:hover {
#apply text-opacity-75 transition-opacity transform-gpu duration-300;
}
}
.topButton {
#apply bg-redditNav rounded-full flex ml-4 items-center my-auto text-secondary-0 transition-transform transform-gpu ease-in-out duration-200 outline-none md:py-0 lg:ml-0 lg:mx-auto;
&:focus {
#apply outline-none;
}
}
.bottomButton {
#apply bg-redditSearch rounded-full inline-flex ml-4 items-center my-auto text-secondary-0 transition-transform transform-gpu ease-in-out delay-200 duration-200 outline-none md:py-0 lg:ml-0 lg:mx-auto;
&:focus {
#apply outline-none;
}
}
.subsub p {
#apply ml-10 !important;
}
.transitionAlpha {
#apply relative z-150 -ml-4 mt-3 transform px-2 w-screen max-w-sm sm:px-0 lg:absolute lg:ml-0 lg:left-1/2 lg:-translate-x-1/2 lg:mx-auto;
}
.transitionBeta {
#apply flex-grow rounded-lg ring-opacity-5 overflow-hidden lg:shadow-lg lg:ring-1 lg:ring-black;
}
.transitionGamma {
#apply relative grid bg-redditSearch text-secondary-0 sm:gap-8 sm:p-8 lg:flex-col lg:gap-3 lg:py-6 transition-transform transform-gpu;
}
.subanchor {
#apply -m-1 p-3 flex items-start rounded-md lg:-m-3;
&:hover {
#apply bg-redditNav;
}
}
.subsubanchor {
#apply -m-1 p-3 flex items-start rounded-md lg:-m-3 lg:ml-2;
&:hover {
#apply bg-redditNav;
}
}
.subtosubsub {
#apply relative px-5 bg-redditSearch text-secondary-0 flex-grow sm:gap-8 lg:gap-3;
}
.divOpen {
#apply grid grid-cols-3 w-full min-w-full transition-transform ease-in-out duration-300 transform-gpu;
}
Now, for the Navbar component into which the headless-navbar data is injected -- I use two separate sites to inject Desktop vs Mobile dynamic nav respectively. Confers more granular control + separation of concerns
navbar.tsx
import { FC, useState, useEffect } from 'react';
import cn from 'classnames';
import NavbarLinks from './navbar-links';
import css from './navbar.module.css';
import { Transition } from '#headlessui/react/dist';
import { Logo } from '../../UI';
import Link from 'next/link';
import throttle from 'lodash.throttle';
import MenuIcon from '../../UI/Icons/menu-icon';
import XIcon from '../../UI/Icons/x-icon';
interface NavbarProps {
root?: string;
Desktop?: React.ReactNode;
Mobile?: React.ReactNode;
}
const Navbar: FC<NavbarProps> = ({ root, Desktop, Mobile }) => {
const [menuOpen, setMenuOpen] = useState(true);
const [isOpen] = useState(false);
const [hasScrolled, setHasScrolled] = useState(false);
useEffect(() => {
const handleScroll = throttle(() => {
const offset = 0;
const { scrollTop } = document.documentElement;
const scrolled = scrollTop > offset;
setHasScrolled(scrolled);
}, 200);
document.addEventListener('scroll', handleScroll);
return () => {
document.removeEventListener('scroll', handleScroll);
};
}, [hasScrolled]);
return (
<>
<nav className={cn(root, css.root, css.stickyNav)}>
<div
className={cn(
css.stickyNav,
{ 'shadow-magical': hasScrolled },
'max-w-full mx-auto px-4 sm:px-6 lg:px-8 font-sans text-secondary-0 transform-gpu duration-500 ease-in-out transition-all'
)}
>
<div
className={cn(
'flex flex-row-reverse justify-between transform-gpu duration-500 ease-in-out transition-all',
css.stickyNav,
{
'h-24': !hasScrolled,
'h-20': hasScrolled
}
)}
>
<div className='flex'>
<div className='-ml-2 mr-2 flex items-center lg:hidden w-full min-w-full'>
<button
className='inline-flex items-center justify-center p-2 rounded-md text-secondary-0 hover:text-opacity-80 hover:bg-opacity-80 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-secondary-0'
aria-expanded={false}
onClick={() => setMenuOpen(!menuOpen)}
>
<span className='sr-only'>Open Main Menu</span>
{menuOpen ? (
<MenuIcon
className={cn('h-8 w-8 focus:outline-none', {
hidden: !menuOpen,
block: menuOpen
})}
/>
) : (
<XIcon
className={cn('h-8 w-8 focus:outline-none', {
hidden: menuOpen,
block: !menuOpen
})}
/>
)}
</button>
</div>
<div className='hidden lg:ml-6 lg:flex lg:items-center lg:space-x-4'>
{Desktop ?? <NavbarLinks />}
</div>
</div>
<div className='flex items-center'>
<div className='flex-shrink-0'>
<div className='lg:mx-4 lg:flex-shrink-0 lg:flex lg:items-center'>
<div className='ml-3 '>
<div className=''>
<span className='sr-only'>The Faderoom Inc.</span>
<Link href='/' passHref scroll={true}>
<a className='#logo'>
<Logo
className={cn(
css.svg,
'cursor-default focus:outline-none transition-all transform-gpu ease-in-out duration-500',
{
'w-18 h-18': !hasScrolled,
'w-14 h-14': hasScrolled
}
)}
/>
</a>
</Link>
</div>
<Transition
show={isOpen && !hasScrolled}
>
<Transition.Child
enter='transition ease-out duration-200'
enterFrom='transform opacity-0 scale-95'
enterTo='transform opacity-100 scale-100'
leave='transition ease-in duration-200'
leaveFrom='transform opacity-100 scale-100'
leaveTo='transform opacity-0 scale-95'
>
<div
role='menu'
aria-orientation='vertical'
aria-labelledby='user-menu'
className={
'origin-top-right absolute right-0 mt-2 h-40 w-44 rounded-md shadow-lg ring-2 ring-red-900 outline-none grid grid-cols-1 bg-secondary-0 z-50 px-3 py-2 hover:bg-opacity-80'
}
>
<NavbarLinks
root={cn('px-3 py-2 hover:bg-secondary-0')}
/>
</div>
</Transition.Child>
</Transition>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className={cn('lg:hidden text-secondary-0', {
block: !menuOpen,
hidden: menuOpen
})}
>
<div className='px-2 pt-2 pb-3 space-y-1 sm:px-3 align-middle'>
{Mobile ?? (
<NavbarLinks
root={cn(
'block px-3 py-2 rounded-md text-2xl font-medium text-secondary-0'
)}
/>
)}
</div>
</div>
</nav>
</>
);
};
export default Navbar;
navbar.module.css
.epiRoot {
#apply sticky top-0 bg-black z-40 transition-all duration-150;
}
.root {
#apply bg-redditNav select-none;
}
.svg {
z-index: 100;
#apply block sm:content-center sm:mx-auto;
}
.stickyNav {
position: sticky;
z-index: 100;
top: 0;
backdrop-filter: saturate(180%) blur(20px);
transition: 0.1 ease-in-out;
&.root {
transition: 0.1 ease-in-out;
}
}
.navLinks div > a {
#apply text-lg lg:text-xl;
}
Now for the global layout, and lastly the index page to illustrate how to use apollo with getStaticProps
layout.tsx
import { FooterFixture } from './Footer';
import { HeadlessFooter } from './HeadlessFooter';
import { HeadlessNavbar } from './HeadlessNavbar';
import { Navbar } from './Navbar';
import { Meta } from './Meta';
import cn from 'classnames';
import {
DynamicNavQuery,
useDynamicNavQuery,
WordpressMenuNodeIdTypeEnum
} from '#/graphql/generated/graphql';
import {
ApolloError,
Button,
Fallback,
Modal,
LandingHero
} from '../UI';
import { useGlobal } from '../Context';
import { useAcceptCookies } from '#/lib/use-accept-cookies';
import Head from 'next/head';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
const dynamicProps = {
loading: () => <Fallback />
};
const LoginView = dynamic(
() => import('../Auth/wp-login-holder'),
dynamicProps
);
const Featurebar = dynamic(
() => import('./Featurebar'),
dynamicProps
);
export interface LayoutProps {
Header: DynamicNavQuery['Header'];
Footer: DynamicNavQuery['Footer'];
className?: string;
title?: string;
hero?: React.ReactNode;
children?: React.ReactNode;
}
function AppLayout({
Header,
Footer,
className,
title,
children,
hero
}: LayoutProps) {
const { acceptedCookies, onAcceptCookies } = useAcceptCookies();
const { displayModal, modalView, closeModal } = useGlobal();
const { loading, error, data } = useDynamicNavQuery({
variables: {
idHead: 'Header',
idTypeHead: WordpressMenuNodeIdTypeEnum.NAME,
idTypeFoot: WordpressMenuNodeIdTypeEnum.NAME,
idFoot: 'Footer'
},
notifyOnNetworkStatusChange: true
});
const router = useRouter();
Header =
data != null && data.Header != null ? data.Header : undefined;
Footer =
data != null && data.Footer != null ? data.Footer : undefined;
return (
<>
<Head>
<title>{title ?? '✂ The Fade Room Inc. ✂'}</title>
</Head>
<Meta />
{error ? (
<>
<ApolloError error={error} />
</>
) : loading && !error ? (
<Fallback />
) : (
<Navbar
Desktop={<HeadlessNavbar header={Header} />}
Mobile={
<HeadlessNavbar
header={Header}
className={
'block px-3 py-2 rounded-md text-base font-semibold text-secondary-0 hover:bg-redditSearch'
}
/>
}
/>
)}
<>
{router.pathname === '/' ? <LandingHero /> : hero}
<Modal open={displayModal} onClose={closeModal}>
{modalView === 'LOGIN_VIEW' && <LoginView />}
</Modal>
<div className={cn('bg-redditSearch min-h-full', className)}>
<main className='fit min-h-full'>{children}</main>
{error ? (
<>
<ApolloError error={error} />
</>
) : loading && !error ? (
<Fallback />
) : (
<FooterFixture
children={<HeadlessFooter footer={Footer} />}
/>
)}
<div className='font-sans z-150'>
<Featurebar
title='This site uses cookies to improve your experience. By clicking, you agree to our Privacy Policy.'
hide={acceptedCookies}
className='prose-lg sm:prose-xl bg-opacity-90 sm:text-center'
action={
<Button
type='submit'
variant='slim'
className='mx-auto text-secondary-0 text-center rounded-xl border-secondary-0 border-1 hover:bg-gray-700 hover:bg-opacity-80 hover:border-secondary-0 duration-500 ease-in-out transform-gpu transition-colors'
onClick={() => onAcceptCookies()}
>
Accept Cookies
</Button>
}
/>
</div>
</div>
</>
</>
);
}
export default AppLayout;
pages/index.tsx -- apollo client implicitly passes in props indicated by GetStaticPropsResult(their respective queries are also present). reviews is not handled by apolloclient but by a fetching method, so it has to be explicitly passed in
import { initializeApollo, addApolloState } from '#/lib/apollo';
import { AppLayout } from '#/components/Layout';
import {
GetStaticPropsContext,
GetStaticPropsResult,
InferGetStaticPropsType
} from 'next';
import {
LandingDataQuery,
LandingDataQueryVariables,
LandingDataDocument,
DynamicNavDocument,
DynamicNavQueryVariables,
DynamicNavQuery,
WordpressMenuNodeIdTypeEnum
} from '#/graphql/generated/graphql';
import { LandingCoalesced } from '#/components/Landing';
import { BooksyReviews } from '#/components/Landing/Booksy';
import { getLatestBooksyReviews } from '#/lib/booksy';
import { BooksyReviewFetchResponse } from '#/types/booksy';
import {
useWordpressLoginMutation,
WordpressLoginMutation,
WordpressLoginMutationVariables,
WordpressLoginDocument
} from '#/graphql/generated/graphql';
export function Index({
other,
popular,
Header,
Footer,
places,
businessHours,
reviews
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<>
<AppLayout
title={'✂ The Fade Room Inc. ✂'}
Header={Header}
Footer={Footer}
>
<LandingCoalesced
other={other}
popular={popular}
places={places}
businessHours={businessHours}
>
<BooksyReviews reviews={reviews.reviews} />
</LandingCoalesced>
</AppLayout>
</>
);
}
export async function getStaticProps(
ctx: GetStaticPropsContext
): Promise<
GetStaticPropsResult<{
other: LandingDataQuery['other'];
popular: LandingDataQuery['popular'];
places: LandingDataQuery['Places'];
businessHours: LandingDataQuery['businessHours'];
Header: DynamicNavQuery['Header'];
Footer: DynamicNavQuery['Footer'];
reviews: BooksyReviewFetchResponse;
// Context: WordpressLoginMutation;
}>
> {
const apolloClient = initializeApollo();
await apolloClient.query<
DynamicNavQuery,
DynamicNavQueryVariables
>({
query: DynamicNavDocument,
variables: {
idHead: 'Header',
idTypeHead: WordpressMenuNodeIdTypeEnum.NAME,
idTypeFoot: WordpressMenuNodeIdTypeEnum.NAME,
idFoot: 'Footer'
}
});
await apolloClient.query<
LandingDataQuery,
LandingDataQueryVariables
>({
query: LandingDataDocument,
variables: {
other: 'all',
popular: 'popular-service',
path: process.env.NEXT_PUBLIC_GOOGLE_PLACES_PATH!,
googleMapsKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY!
}
});
const response = await getLatestBooksyReviews();
const reviews: BooksyReviewFetchResponse = await response.json();
return addApolloState(apolloClient, {
props: {
reviews
},
revalidate: 60
});
}
export default Index;
For the sake of thoroughness, here is my #/lib/apollo.ts file
import { useMemo } from 'react';
import {
ApolloClient,
InMemoryCache,
NormalizedCacheObject,
HttpLink
} from '#apollo/client';
import {
getAccessToken,
getAccessTokenAsCookie
} from './cookies';
import { setContext } from '#apollo/client/link/context';
import type {
PersistentContext,
CookieOptions
} from '#/types/index';
import { getCookiesFromContext } from '#/utils/regex-parsing';
import {
WORDPRESS_USER_COOKIE_EXPIRE,
WORDPRESS_USER_TOKEN_COOKIE
} from './const';
export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__';
let apolloClient:
| ApolloClient<NormalizedCacheObject>
| undefined;
function createApolloClient(
options?: CookieOptions
): ApolloClient<NormalizedCacheObject> {
const authLink = setContext((_, { ...headers }: Headers) => {
const token =
getAccessToken(options) ?? getAccessTokenAsCookie(options);
if (!token) {
return {};
}
return {
headers: {
...headers,
'Content-Type': 'application/json; charset=utf-8',
'X-JWT-REFRESH': `Bearer ${process.env
.WORDPRESS_AUTH_REFRESH_TOKEN!}`,
'x-jwt-auth': token ? `Bearer ${token}` : ''
}
};
});
const httpLink = new HttpLink({
uri: `${process.env.NEXT_PUBLIC_ONEGRAPH_API_URL}`,
credentials: 'same-origin',
...(typeof window !== undefined && { fetch })
});
return new ApolloClient({
ssrMode: typeof window === 'undefined',
link: authLink.concat(httpLink),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
wordpress: {
merge(existing, incoming, { mergeObjects }) {
// Invoking nested merge functions
return mergeObjects(existing, incoming);
}
},
google: {
merge(existing, incoming, { mergeObjects }) {
// Invoking nested merge functions
return mergeObjects(existing, incoming);
}
}
}
}
}
})
});
}
export function initializeApollo(
initialState: any | any = null,
context?: PersistentContext
) {
const _apolloClient =
apolloClient ??
createApolloClient({
cookies: getCookiesFromContext(
context ?? WORDPRESS_USER_TOKEN_COOKIE
)
});
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
// gets hydrated here
if (initialState) {
// Get existing cache, loaded during client side data fetching
const existingCache = _apolloClient.extract();
// Merge the existing cache into data passed from getStaticProps/getServerSideProps
const data = { ...existingCache, ...initialState };
// const data = deepmerge(initialState, existingCache, { clone: false });
// Restore the cache with the merged data
_apolloClient.cache.restore(data);
}
// for SSG and SSR ALWAYS create a new Apollo Client
if (typeof window === 'undefined') return _apolloClient;
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient;
return _apolloClient;
}
export function addApolloState(client: any, pageProps: any) {
if (pageProps?.props) {
pageProps.props[
APOLLO_STATE_PROP_NAME
] = client.cache.extract();
}
return pageProps;
}
export function useApollo(pageProps: any) {
const state = pageProps[APOLLO_STATE_PROP_NAME];
const store = useMemo(() => initializeApollo(state), [state]);
return store;
}

Resources