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
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?
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;
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>
))}
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;
}