React Headless UI Spin Transition Night Theme Toggle - reactjs

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;

Related

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

Reactjs setInterval with scroll only trigger once

I have auto scroll function and scroll will start when user click the function as follow.
It is scroll 50px to y axis once when user click play button. But it is only scroll once even thought I have added interval. Interval is working because I saw the "scrolling" console.log is increasing. But scroll is not scrolling again.
May I know why scroll is not move again?
import React, { useState, useEffect, useRef } from "react";
import { useParams, NavLink } from "react-router-dom";
import { useQuery } from "#apollo/client";
import { getTrack, getTrackVariable } from "../../gql/track";
import ChordSheetJS from "chordsheetjs";
import {
YoutubeIcon,
FacebookIcon,
PlayIcon,
PauseIcon,
} from "../../assets/icons/svg_icons";
import { SettingIcon } from "../../assets/icons/svg_icons";
import paths from "../../routes/paths";
import { FacebookShareButton } from "react-share";
import GoTop from "../../components/go_top";
const TrackPage = () => {
const intervalId = useRef(null);
const { trackId } = useParams();
const [track, setTrack] = useState();
const [collapse, setCollapse] = useState(true);
const [play, setPlay] = useState(false);
const [speed, setSpeed] = useState(1);
const { loading, error, data } = useQuery(getTrack, {
variables: getTrackVariable(trackId),
});
const trackRef = useRef();
useEffect(() => {
if (!loading && !error) {
setTrack(data?.track);
}
}, [loading, error, data]);
const getChordSheet = (value) => {
const parser = new ChordSheetJS.ChordProParser();
const song = parser.parse(value);
const formatter = new ChordSheetJS.HtmlTableFormatter();
const chordSheet = formatter.format(song);
return chordSheet;
};
const handleError = (e) => {
e.target.onerror = null;
e.target.src = Monk;
};
const handleMenuCollapse = (e) => {
e.preventDefault();
setCollapse(!collapse);
};
const handleSpeedUp = () => {
setSpeed(speed + 1);
};
const handleSpeedDown = () => {
setSpeed(speed - 1);
};
const handleScroll = () => {
setPlay(!play);
if (play) {
console.log("stop");
clearInterval(intervalId.current);
} else {
let delayInMs = 100;
const onScrollStep = () => {
document.getElementById("content").scroll(0,50);
console.log("srolling")
};
intervalId.current = setInterval(onScrollStep, delayInMs);
console.log("play");
}
};
return (
<>
<div id="setting">
{/** the big div */}
<div
className={` w-36 h-56 bg-primary absolute top-[calc((100vh-384px)/2)] ${
collapse ? "hidden" : "right-0"
} " bg-primary rounded-b-lg items-center justify-center`}
>
<div>
<div className="items-center justify-center mt-5">
<div className="flex text-xs items-center justify-center ">
<span className=" text-sm text-white">Scroll</span>
</div>
<div className="flex text-xs pt-0 mt-0 items-center justify-center ">
<button
className="px-2 btn-sm flex w-20 items-center bg-transparent hover:bg-accent border text-white font-semibold hover:text-white border-white hover:border-transparent rounded "
onClick={handleScroll}
>
{play ? (
<PauseIcon className="text-white mr-2" />
) : (
<PlayIcon className="text-white mr-2" />
)}
{play ? <span>Pause</span> : <span>Play</span>}
</button>
</div>
<div className="flex text-xs items-center justify-center mt-2">
<button
className="w-auto bg-transparent mr-2 hover:bg-accent text-white font-semibold hover:text-white py-1 px-2 border border-white hover:border-transparent rounded"
onClick={handleSpeedDown}
>
-1
</button>
<button
className="w-auto bg-transparent ml-2 hover:bg-accent text-white font-semibold hover:text-white py-1 px-2 border border-white hover:border-transparent rounded"
onClick={handleSpeedUp}
>
+1
</button>
</div>
</div>
</div>
</div>
{/** the icon div */}
<div
className={`flex w-12 absolute top-[calc((100vh-384px)/2)] h-12 bg-primary
${collapse ? "animate-pulse right-0" : "right-36"}
cursor-pointer bg-primary rounded-l-lg items-center justify-center`}
onClick={handleMenuCollapse}
>
{/* <div className="w-5 h-5 bg-white rounded-full " /> */}
<SettingIcon />
</div>
</div>
<div id="track" ref={trackRef}>
<div className="flex flex-col w-full py-1 my-1 items-center bg-gray-50">
<div className="relative my-6 mx-auto md:min-w-[60%] max-h-full">
{track ? (
<div className="w-full">
<pre
className="px-5 textarea"
dangerouslySetInnerHTML={{
__html: getChordSheet(track.lyric),
}}
/>
</div>
) : (
<div></div>
)}
</div>
</div>
</div>
</>
);
};
export default TrackPage;
app.jsx
import React, { useState, useEffect } from "react";
import Header from "./components/header";
import SideMenu from "./components/side_menu";
import AppRoutes from "./routes";
import withUser from "./hocs/with_user";
import { isMobile } from "react-device-detect";
import { useLocation } from "react-router-dom";
import { AuthProvider, setAccessToken } from "./auth/auth_provider";
import { Toaster } from "react-hot-toast";
import AppContext from "./components/app_context";
import "./i18n";
import "./App.css";
function App(props) {
const [collapse, setCollapse] = useState(isMobile);
const [sideBarFull] = useState(true);
const location = useLocation();
const IsNormalPage = () => {
const blankPages = ["/login"];
for (let i = 0; i < blankPages.length; i++) {
if (location.pathname.startsWith(blankPages[i])) return
false;
}
return true;
};
useEffect(() => {
if (props.user) setAccessToken(props.user.t);
}, []);
const PageHeader = () => {
return (
<div className="h-[72px] w-full flex items-center align-middle justify-center bg-neutral shadow">
<div className="w-full text-center">
<Header />
</div>
</div>
);
};
return (
<AuthProvider user={props.user}>
<AppContext.Provider
value={{
collapse: collapse,
setCollapse: setCollapse,
}}
>
<div className="relative w-full min-h-screen h-full">
<div className="flex flex-row min-h-screen">
<div className="w-auto z-0 ">
<div className="flex-1 w-full max-h-screen mx-auto text-lg h-full shadow-lg bg-white overflow-y-auto">
{IsNormalPage() && <SideMenu showFullMenu={sideBarFull} />}
</div>
</div>
<div className="w-full max-h-screen flex flex-col z-10">
{IsNormalPage() && <PageHeader />}
<div id="content" className="flex-1 w-full max-h-screen mx-auto text-lg h-full shadow-lg bg-white overflow-y-auto">
<Toaster />
<AppRoutes />
</div>
</div>
</div>
</div>
</AppContext.Provider>
</AuthProvider>
);
}
export default withUser(App);
I think because you are toggling the play state in your component
setPlay(!play);
Are you trying to scroll to a specific div or just scroll for 50 px in the direction of y-axis? there are two approaches, you can use window or Refs.
an example using the refs to scroll to a specific node in the dom
const ScrollDemo = () => {
const myRef = useRef(null)
const executeScroll = () => { myRef.current.scrollIntoView()}
return (
<div>
<div ref={myRef}>Element to scroll to</div>
<button onClick={executeScroll}> Click to scroll </button>
<div/>
)
}
or if you just want to just scroll 50 pixel in the direction of y-axis
const scrollToTop = () => {
window.scrollTo(0,50);
};
return (
<button onClick={scrollToTop}>
Go down 50 px!
</button>
);
window.scrollTo is only working with html body. document.getElementById is only working overflow div.
useEffect(() => {
if (play) {
const onScrollStep = () => {
var e = document.getElementById("content");
if (e.scrollHeight - e.scrollTop === e.clientHeight) {
clearInterval(intervalId.current);
setPlay(!play);
return;
}
e.scroll(0, e.scrollTop + speed);
};
intervalId.current = setInterval(onScrollStep, delayInMs);
} else {
clearInterval(intervalId.current);
}
},[play, speed])

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

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

Make only one post request with new state after clicking on one of multiple identical components

I have a small issue and do not have a good idea how to solve it. Hope you can help
I have created a simple Starrating component. You have five stars. If you click on one of the stars the state changes and so on ... (5 stars. Rating from 1 to 5 :D ). Just basic stuff.
The main problem is based on the fact that the Starrating component is a part of another component (AlbumList.js), which is rendered 5 times on the homepage (5 different pictures which you can rate)
(between there is another component AlbumCard.js which is holding the Starrating component but I assume that's not important.
Basically I have 5 components which are the same and each one of them has the Starrating Component.
My main goal is to click on one of the pictures, rate that and send the right state to my database.
The function which is sending the right rating to the database (rateAlbum), is invoked in useEffect, because only there I am able to send the new state to my database(
outside useEffect I only have access to the new state after rendering, I guess).
Unfortunately if I reload the page or just make one rate the function is invoked as many times as pictures there are (5 times)
How do I call the function just ones if I just rated one picture or just after the onClick on the right picture with the new state ?
Starrating.js
import React, { useEffect, useState } from 'react'
import { FaStar } from 'react-icons/fa'
import { rateAlbum } from '../../store/actions/userAlbumRatingAction'
function Starrating({ width }) {
const [rating, setRating] = useState(null)
const [hover, setHover] = useState(null)
const ratePicture = (rating) => {
setRating(rating)
// ratePicture() do not have the new state of rating
}
useEffect(() => {
rateAlbum({ // function which is making the axios call
//... not imporant information just the right IDs and so on
rating: rating,
})
}, [rating])
return (
<div className='flex h-full' style={{ width: width }}>
{[...Array(5)].map((star, i) => {
const ratinValue = i + 1
return (
<label key={ratinValue} className='flex items-center w-full'>
<input
className='hidden'
type='radio'
name='raiting'
value={ratinValue}
onClick={
() => ratePicture(ratinValue)
// () => setRating(ratinValue)
}
/>
<FaStar
className='md:m-1 w-full h-full delay-200 cursor-pointer'
color={ratinValue <= (hover || rating) ? '#ffc107' : '#e4e5e9'}
onMouseEnter={() => setHover(ratinValue)}
onMouseLeave={() => setHover(null)}
/>
</label>
)
})}
</div>
)
}
export default Starrating
AlbumList.js (Starrting component is a part of the AlbumCard Component)
import { connect, useSelector } from 'react-redux'
import { fetchAlbum } from '../../store/actions/albumAction'
import AlbumCard from './AlbumCard'
import { setView } from '../../store/actions/uiAction'
import { useHistory } from 'react-router'
function AlbumList(props) {
const newreleases = useSelector((state) => state.newReleases.NewReleases)
const view = useSelector((state) => state.ui.view)
const searchAlbum = useSelector((state) => state.search.albums)
const history = useHistory()
const onAlbumCardClick = (dataId) => {
props.fetchAlbum(dataId)
history.push('/home/album')
}
return (
<section className='sm:flex sm:justify-between sm:flex-nowrap grid grid-cols-3'>
{view === 'noSearch' ? (
<>
{newreleases.slice(0, 5).map((data) => (
<AlbumCard
url={data.images[0].url}
key={data.id}
id={data.id}
albumname={data.name}
onClick={() => onAlbumCardClick(data.id)}
/>
))}
</>
) : view === 'search' ? (
<>
{searchAlbum.slice(0, 5).map((data, index) => (
<AlbumCard
url={data.images[0].url}
key={data.id}
id={data.id}
albumname={data.name}
onClick={() => onAlbumCardClick(data.id)}
/>
))}
</>
) : null}
</section>
)
}
const mapDispatch = { fetchAlbum, setView }
export default connect(null, mapDispatch)(AlbumList)
rateAlbum function
export const rateAlbum = (data) => {
axios.post('....', data)
}
AlbumCard.js ( not important, but has the Starrting component and Albumcard.js is part of
ALbumList.js)
import React from 'react'
import CardButtons from './CardButtons'
import Starrating from '../HelperComponents/Starrating'
function AlbumCard({ url, albumname, onClick }) {
return (
<>
<div className=' sm:m-2 sm:w-40 dark:bg-white w-24 m-1 rounded-lg shadow-md'>
<div onClick={onClick} id='hi' className='group relative rounded-lg'>
<img
className='md:w-72 block w-full h-full rounded-lg'
src={url}
alt=''
/>
<div className='group-hover:bg-opacity-60 group-hover:opacity-100 justify-evenly absolute top-0 flex items-center w-full h-full transition bg-black bg-opacity-0 rounded-md'>
<CardButtons />
</div>
</div>
<div className=' flex flex-col items-center justify-center pt-3 pb-3'>
<p className='dark:text-black font-body whitespace-nowrap flex justify-center w-11/12 mb-2 overflow-hidden text-xs text-black'>
{albumname}
</p>
<Starrating />
</div>
</div>
</>
)
}
export default AlbumCard
Startrating.js
import React, { useEffect, useState } from 'react'
import { FaStar } from 'react-icons/fa'
import { rateAlbum } from '../../store/actions/userAlbumRatingAction'
function Starrating({ width, onClickStar }) {
const [rating, setRating] = useState(null)
const [hover, setHover] = useState(null)
const ratePicture = (rating) => {
setRating(rating)
// ratePicture() do not have the new state of rating
}
useEffect(() => {
rateAlbum({ // function which is making the axios call
//... not imporant information just the right IDs and so on
rating: rating,
})
}, [rating])
return (
<div className='flex h-full' style={{ width: width }}>
{[...Array(5)].map((star, i) => {
const ratinValue = i + 1
return (
<label key={ratinValue} className='flex items-center w-full'>
<input
className='hidden'
type='radio'
name='raiting'
value={ratinValue}
onClick={
() => onClickStart(ratinValue)
// () => setRating(ratinValue)
}
/>
<FaStar
className='md:m-1 w-full h-full delay-200 cursor-pointer'
color={ratinValue <= (hover || rating) ? '#ffc107' : '#e4e5e9'}
onMouseEnter={() => setHover(ratinValue)}
onMouseLeave={() => setHover(null)}
/>
</label>
)
})}
</div>
)
}
export default Starrating
AlbumCard,js
import React from 'react'
import CardButtons from './CardButtons'
import Starrating from '../HelperComponents/Starrating'
function AlbumCard({ url, albumname, onClick, onClickStar }) {
return (
<>
<div className=' sm:m-2 sm:w-40 dark:bg-white w-24 m-1 rounded-lg shadow-md'>
<div onClick={onClick} id='hi' className='group relative rounded-lg'>
<img
className='md:w-72 block w-full h-full rounded-lg'
src={url}
alt=''
/>
<div className='group-hover:bg-opacity-60 group-hover:opacity-100 justify-evenly absolute top-0 flex items-center w-full h-full transition bg-black bg-opacity-0 rounded-md'>
<CardButtons />
</div>
</div>
<div className=' flex flex-col items-center justify-center pt-3 pb-3'>
<p className='dark:text-black font-body whitespace-nowrap flex justify-center w-11/12 mb-2 overflow-hidden text-xs text-black'>
{albumname}
</p>
<Starrating onClickStar={onClickStar} />
</div>
</div>
</>
)
}
export default AlbumCard
AlbumList.js
import { connect, useSelector } from 'react-redux'
import { fetchAlbum } from '../../store/actions/albumAction'
import AlbumCard from './AlbumCard'
import { setView } from '../../store/actions/uiAction'
import { useHistory } from 'react-router'
function AlbumList(props) {
const newreleases = useSelector((state) => state.newReleases.NewReleases)
const view = useSelector((state) => state.ui.view)
const searchAlbum = useSelector((state) => state.search.albums)
const [releases, setReleases] = useState([])
useEffect(() => {
setReleases(newreleases)
}, [newreleases])
const history = useHistory()
const setRating = (ratingValue, index) => {
let updatedReleases = [...releases]
updatedReleases[index].rating = ratingValue // i'm assuming you have //rating field
setReleases(updatedReleases)
// then send this new releases to the api
}
const onAlbumCardClick = (dataId) => {
props.fetchAlbum(dataId)
history.push('/home/album')
}
return (
<section className='sm:flex sm:justify-between sm:flex-nowrap grid grid-cols-3'>
{view === 'noSearch' ? (
<>
{newreleases.slice(0, 5).map((data, index) => (
<AlbumCard
url={data.images[0].url}
key={data.id}
id={data.id}
albumname={data.name}
onClickStar={(ratingValue) => setRating(ratingValue, index)}
onClick={() => onAlbumCardClick(data.id)}
/>
))}
</>
) : view === 'search' ? (
<>
{searchAlbum.slice(0, 5).map((data, index) => (
<AlbumCard
url={data.images[0].url}
key={data.id}
id={data.id}
onClickStar={(ratingValue) => setRating(ratingValue, index)}
albumname={data.name}
onClick={() => onAlbumCardClick(data.id)}
/>
))}
</>
) : null}
</section>
)
}
const mapDispatch = { fetchAlbum, setView }
export default connect(null, mapDispatch)(AlbumList)
I implement your StarRating component in an optimized & efficient manner.
import React, { useState } from "react";
import { FaStar } from "react-icons/fa";
import { rateAlbum } from "../../store/actions/userAlbumRatingAction";
function StarRating({ width }) {
const [rating, setRating] = useState(0);
const ratePicture = (rating) => {
setRating(++rating);
console.log("clicked me");
rateAlbum({
rating: ++rating,
}); // Dispatch the action to save the rating
};
return (
<div className="flex h-full" style={{ width: width, display: "flex" }}>
{[...Array(5)].map((star, i) => (
<div key={i} className="flex items-center w-full">
<FaStar
className="md:m-1 w-full h-full delay-200 cursor-pointer"
color={i < rating ? "#ffc107" : "#e4e5e9"}
onClick={() => ratePicture(i)}
/>
</div>
))}
</div>
);
}
export default StarRating;
Let me know if you already implement rendering the StarRating component with its album's default rate to include that too in this code.
I hope this will solve all the problems related to rating.

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