Storybook Navbar component breaks through useRouter() - next.js

I have a Navbar with a Navigation Links, which are highlighted as active, depending on the page a user is on.
import { useRouter } from 'next/dist/client/router'
const navigation = [
{ name: 'Home', url: '/', active: true},
{ name: 'Quiz', url:'/quiz', active: false}
]
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
export const Navigation = () => {
const router = useRouter();
return (
<div className="flex space-x-4 justify-self-center">
{navigation.map((item) => (
<a
key={item.name}
href={item.url}
className={`px-3 py-2 rounded-md text-sm font-medium ${
router.asPath === item.url ? "bg-gray-900 text-white" : 'text-gray-300 hover:text-gray-700'
}`}
aria-current={item.active? 'page' : undefined}
>
{item.name}
</a>
))}
</div>
)
}
It works as is should, but I can't create a story for it, because of the Next Router. I get the following error: Cannot read property 'asPath' of null.
I tried to follow the instructions in the answer I found here: How to use NextJS's 'useRouter()' hook inside Storybook , but unfortunately it didn't give me the result I am striving for. So basically my "Story-Navbar" shouldn't redirect, but just highlight the Navigation Link, while I click on it. Is that possible? Here is the story for Navigation:
import { Navigation } from './Navigation';
export default {
title: 'Example/Navigation',
component: Navigation,
};
export const DefautlNavigation = () => {
return (
<Navigation />
)
}

The storybook Next.js router add-on works for useRouter() hook. Here is the example in their docs:
import { withNextRouter } from 'storybook-addon-next-router';
import MyComponentThatHasANextLink from '../component-that-has-a-next-link';
export default {
title: 'My Story',
decorators: [withNextRouter], // not necessary if defined in preview.js
};
// if you have the actions addon
// you can click the links and see the route change events there
export const Example = () => <MyComponentThatHasANextLink />;
Example.story = {
parameters: {
nextRouter: {
path: '/profile/[id]',
asPath: '/profile/lifeiscontent',
query: {
id: 'lifeiscontent',
},
},
},
};

Related

Make react swiper pagination as a separate component and plug it in swiper

I am working on a project with a setup combination of react, tailwind and react icons. In my project multiple sliders are required (some has navigation, some has pagination etc…)
So, I created a slider factory component Carousel and controlling variation through props
// Carousel/index.jsx
import { Children, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { HiOutlineArrowLeft, HiOutlineArrowRight } from "react-icons/hi";
import { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay, Keyboard, Mousewheel, Navigation, Pagination } from "swiper";
import "swiper/css";
import "swiper/css/pagination";
import "swiper/css/navigation";
import NextSlideBtn from "./NextSlideBtn";
import PrevSlideBtn from "./PrevSlideBtn";
const Carousel = ({
children,
loop,
pagination,
navigation,
autoplay,
breakpoints,
}) => {
const childrenArray = Children.toArray(children);
const [swiperRef, setSwiperRef] = useState();
return (
<div className="group relative">
<Swiper
className="rounded overflow-hidden"
onSwiper={setSwiperRef}
modules={[
Autoplay,
Keyboard,
Mousewheel,
Pagination,
Navigation,
]}
mousewheel
keyboard
grabCursor
loop={loop || false}
autoplay={
autoplay && {
delay: 5000,
disableOnInteraction: true,
}
}
spaceBetween={breakpoints && 20}
breakpoints={
breakpoints && {
450: {
slidesPerView: 2,
},
768: {
slidesPerView: 3,
},
1536: {
slidesPerView: 4,
},
}
}
pagination={
pagination && {
clickable: true,
}
}
>
{childrenArray.map((item) => (
<SwiperSlide key={uuidv4()}>{item}</SwiperSlide>
))}
</Swiper>
{navigation && (
<div className="hidden group-hover:block">
<PrevSlideBtn swiperRef={swiperRef}>
<HiOutlineArrowLeft className="text-violet-600" />
</PrevSlideBtn>
<NextSlideBtn swiperRef={swiperRef}>
<HiOutlineArrowRight className="text-violet-600" />
</NextSlideBtn>
</div>
)}
</div>
);
};
export default Carousel;
// Carousel/NextSlideBtn.jsx
const NextSlideBtn = ({ children, swiperRef }) => {
const handleNextSlide = () => swiperRef.slideNext();
return (
<button
onClick={handleNextSlide}
className="absolute z-50 top-1/2 -translate-y-1/2 -right-2 translate-x-2 bg-white shadow-md rounded-full p-3"
>
{children}
</button>
);
};
export default NextSlideBtn;
// Carousel/PrevSlideBtn.jsx
const PrevSlideBtn = ({ children, swiperRef }) => {
const handlePrevSlide = () => swiperRef.slidePrev();
return (
<button
onClick={handlePrevSlide}
className="absolute z-50 top-1/2 -translate-y-1/2 -left-2 -translate-x-2 bg-white shadow-md rounded-full p-3"
>
{children}
</button>
);
};
export default PrevSlideBtn;
One of the requirement was to create custom navigation button, which I managed to accomplish as you can see in the code above, but for more visibility I am marking the flow in short
// Carousel/index.jsx
const [swiperRef, setSwiperRef] = useState();
<div>
<Swiper
onSwiper={setSwiperRef}
...
>
...
</Swiper>
<PrevSlideBtn swiperRef={swiperRef} />
<NextSlideBtn swiperRef={swiperRef} />
</div>
// Carousel/NextSlideBtn.jsx
const NextSlideBtn = ({ children, swiperRef }) => {
const handleNextSlide = () => swiperRef.slideNext();
return (
<button
onClick={handleNextSlide}
...
>
{children}
</button>
);
};
export default NextSlideBtn;
// Carousel/PrevSlideBtn.jsx
const PrevSlideBtn = ({ children, swiperRef }) => {
const handlePrevSlide = () => swiperRef.slidePrev();
return (
<button
onClick={handlePrevSlide}
...
>
{children}
</button>
);
};
export default PrevSlideBtn;
Couple of github issues, stackoverflow questions and swiper docs helped me out to accomplish this, and it was pretty straight forward to understand.
Now my question is how can I customize swiper react pagination (like radio buttons which some websites use, or any other styles…) using tailwind and I want to make it as a separate component like I did for navigation buttons.
I’ve tried many solutions eg:
const paginationBullets = {
type: "custom",
clickable: true,
renderCustom: (_, current, total) => {
return <MyCustomSwiperPaginationComponent current={current} total={total} />;
},
};
But nothing seems to work as expected.
Please help me out.
Component separation is my goal.

NEXT JS: Build exportPathMap for a dynamic page Route

I wanted to build a static export for my NEXT project that looks like following:
- pages
---- index.tsx
---- [pageRoute].tsx
Now I want to statically generate routeId for home page that I have handled as shown below:
import { useRouter } from 'next/router';
import React from 'react';
import { PAGE_ROUTES } from '../constants/config';
import Home from './Home/Home';
type Props = {};
export default function Base({}: Props) {
const router = useRouter();
const route = router.query.pageRoute as string;
let RenderComponent = <div>404: Page Not Found</div>;
switch (route) {
case PAGE_ROUTES.HOME: {
RenderComponent = <Home />;
break;
}
default: {
}
}
return (
<div className='flex flex-col items-center max-w-sm mx-auto'>
{RenderComponent}
</div>
);
}
I am not sure what do I specify in exportPathMaps in next.config.js in order to create static export of home page:
/** #type {import('next').NextConfig} */
module.exports = {
reactStrictMode: true,
exportPathMap: async function (
defaultPathMap,
{ dev, dir, outDir, distDir, buildId }
) {
return {
'/': { page: '/' },
// how do I add configuration for '/home': {page: '/[pageRoute]',query:{pageRoute:'home'}}
};
},
};
when I do this:
'/home': { page: '/[pageRoute]', query: { pageRoute: 'home' } },
It throws error saying:
Error: you provided query values for /home which is an auto-exported page. These can not be applied since the page can no longer be re-rendered on the server. To disable auto-export for this page addgetInitia
lProps
In order to statically pre-render dynamic paths, you should return them from getStaticPaths:
import { useRouter } from 'next/router';
import React from 'react';
import { PAGE_ROUTES } from '../constants/config';
import Home from './Home/Home';
import type { GetStaticPaths } from 'next'
export const getStaticPaths: GetStaticPaths = async () => {
const paths = Object.values(PAGE_ROUTES)
.map(route => [{ params: { pageRoute: route } }])
return {
paths,
fallback: false, // meaning any path not returned by `getStaticPaths` will result in a 404 page
}
}
type Props = {};
export default function Base({}: Props) {
return (
<div className='flex flex-col items-center max-w-sm mx-auto'>
<Home />
</div>
);
}
And, as #juliomalves said, in that case you don't need exportPathMap in next.config.js.
For custom 404 page create 404.tsx in /pages
More about getStaticPaths - https://nextjs.org/docs/api-reference/data-fetching/get-static-paths
fallback: false - https://nextjs.org/docs/api-reference/data-fetching/get-static-paths#fallback-false

Cypress component test with NextJS useRouter function

My Navbar component relies on the useRouter function provided by nextjs/router in order to style the active links.
I'm trying to test this behavior using Cypress, but I'm unsure of how I'm supposed to organize it. Cypress doesn't seem to like getRoutePathname() and undefined is returned while within my testing environment.
Here's the component I'm trying to test:
import Link from 'next/link'
import { useRouter } from 'next/router'
function getRoutePathname() {
const router = useRouter()
return router.pathname
}
const Navbar = props => {
const pathname = getRoutePathname()
return (
<nav>
<div className="mr-auto">
<h1>Cody Bontecou</h1>
</div>
{props.links.map(link => (
<Link key={link.to} href={link.to}>
<a
className={`border-transparent border-b-2 hover:border-blue-ninja
${pathname === link.to ? 'border-blue-ninja' : ''}`}
>
{link.text}
</a>
</Link>
))}
</nav>
)
}
export default Navbar
I have the skeleton setup for the Cypress component test runner and have been able to get the component to load when I hardcode pathname, but once I rely on useRouter, the test runner is no longer happy.
import { mount } from '#cypress/react'
import Navbar from '../../component/Navbar'
const LINKS = [
{ text: 'Home', to: '/' },
{ text: 'About', to: '/about' },
]
describe('<Navbar />', () => {
it('displays links', () => {
mount(<Navbar links={LINKS} />)
})
})
Ideally, there'd be a provider for Next.js's useRouter to set the router object and wrap the component in the provider in mount. Without going through the code or Next.js supplying the documentation, here's a workaround to mock useRouter's pathname and push:
import * as NextRouter from 'next/router'
// ...inside your test:
const pathname = 'some-path'
const push = cy.stub()
cy.stub(NextRouter, 'useRouter').returns({ pathname, push })
I've added push because that's the most common use case, which you may also need.

Rendered fewer hooks than expected error in the return statement

I am trying to build a simple navbar but when I define a setResponsivness function inside my useEffect
I am getting the error Rendered fewer hooks than expected. This may be caused by an accidental early return statement. I looked at similar answers for the same but till wasn't able to fix
Here s my code
import React,{useEffect,useState} from 'react'
import {AppBar ,Toolbar, Container ,makeStyles,Button, IconButton} from '#material-ui/core'
import MenuIcon from '#material-ui/icons/Menu'
const usestyles = makeStyles({
root:{
display:'flex',
justifyContent:'space-between' ,
maxWidth:'700px'
},
menubtn:{
fontFamily: "Work Sans, sans-serif",
fontWeight: 500,
paddingRight:'79px',
color: "white",
textAlign: "left",
},
menuicon:{
edge: "start",color: "inherit",paddingLeft:'0'
}
})
const menudata = [
{
label: "home",
href: "/",
},
{
label: "About",
href: "/about",
},
{
label: "Skill",
href: "/skills",
},
{
label: "Projects",
href: "/projects",
},
{
label: "Contact",
href: "/contact",
},
];
//yet to target link for the smooth scroll
function getmenubuttons(){
const {menubtn} = usestyles();
return menudata.map(({label,href})=>{
return <Button className={menubtn}>{label}</Button>
})
}
//to display navbar on desktop screen
function displaydesktop(){
const { root } = usestyles() //destructuring our custom defined css classes
return <Toolbar ><Container maxWidth={false} className={root}>{getmenubuttons()}</Container> </Toolbar>
}
//to display navbar on mobile screen
function displaymobile(){
const {menuicon} =usestyles() ;
return <Toolbar><IconButton className={menuicon}><MenuIcon /> </IconButton></Toolbar>
}
function Navbar() {
const [state, setState] = useState({mobileview:false});
const {mobileview} = state;
useEffect(() => {
const setResponsiveness = () => {
return window.innerWidth < 900
? setState((prevState) => ({ ...prevState, mobileview: true }))
: setState((prevState) => ({ ...prevState, mobileview: false }));
};
setResponsiveness();
window.addEventListener("resize", () => setResponsiveness());
}, []);
return (
<div>
<AppBar> {mobileview?displaymobile():displaydesktop()} </AppBar>
</div>
)
}
export default Navbar;
Your problem seems to be here
{mobileview?displaymobile():displaydesktop()}
For example the displaymobile function inside uses hooks right (usestyles)? Then it means you are rendering hooks inside conditions (mobileview being condition) which is not allowed by rules of hooks.
You can fix it like this:
<div>
<AppBar> {mobileview ? <Displaymobile /> : <Displaydesktop />} </AppBar>
</div>
Also change definition of component using capital letters as that is how react refers to components. e.g.
function Displaydesktop() {
const { root } = usestyles(); //destructuring our custom defined css classes
return (
<Toolbar>
<Container maxWidth={false} className={root}>
{getmenubuttons()}
</Container>{" "}
</Toolbar>
);
}
Now we consume them as components. Probably when you used lower case letters and called those as functions in your render, react interpreted them as custom hooks, hence the warnings.

Gutenberg block fails validation

I'm trying to create a custom WordPress gutenberg block that allows me to select any image and text.
I can create the block using this code and I can see content on the frontend of the site however the block crashes if I view the block to edit it.
The console gives the following error: -
Block validation: Block validation failed for cgb/block-imagecta (
Object { name: "cgb/block-imagecta", icon: {…}, attributes: {…}, keywords: (3) […], save: save(), title: "imagecta - CGB Block", category: "common", edit: edit()
}
).
Content generated by save function:
<div class="wp-block-cgb-block-imagecta"><div class="imageCtaBg"><img class="bg" alt="sergserge"/><p></p></div></div>
Content retrieved from post body:
<div class="wp-block-cgb-block-imagecta"><div class="imageCtaBg"><img class="bg" alt="sergserge"/><p>dxfbxdfbxdfbfb</p></div></div>
Is this related to the attributes at all?
/**
* BLOCK: imagecta
*
* Registering a basic block with Gutenberg.
* Simple block, renders and saves the same content without any interactivity.
*/
// Import CSS.
import './editor.scss';
import './style.scss';
const {__} = wp.i18n; // Import __() from wp.i18n
const {registerBlockType} = wp.blocks; // Import registerBlockType() from wp.blocks
const {FormFileUpload} = wp.components;
const {RichText, MediaUpload} = wp.blockEditor;
registerBlockType('cgb/block-imagecta', {
title: __('imagecta - CGB Block'),
icon: 'shield',
category: 'common',
keywords: [
__('imagecta — CGB Block'),
__('CGB Example'),
__('create-guten-block'),
],
attributes: {
content: {
type: 'html',
selector: '.captionText',
},
logo: {
type: 'string',
default: null,
},
},
edit: (props) => {
const {attributes: {content}, setAttributes} = props;
function onImageSelect(imageObject) {
setAttributes({
logo: imageObject.sizes.full.url
});
}
const onChangeContent = (newContent) => {
setAttributes({content: newContent});
};
// Creates a <p class='wp-block-cgb-block-imagecta'></p>.
return (
<div className={props.className}>
<MediaUpload
onSelect={onImageSelect}
type="image"
value={logo} // make sure you destructured backgroundImage from props.attributes!
render={({open}) => (
<button onClick={open}>
Upload Image!
</button>
)}
/>
<div className='imageCtaBg'>
<img src={logo} class="bg" alt={'sergserge'}/>
<RichText
tagName={'p'}
className='captionText'
onChange={onChangeContent}
value={content}
placeholder='Enter text...'
/>
</div>
</div>
);
},
save: (props) => {
const {attributes: {content}, setAttributes} = props;
return (
<div className={props.className}>
<div className={'imageCtaBg'}>
<img src={props.attributes.logo} className="bg" alt={'sergserge'}/>
<RichText.Content tagName={"p"} value={props.attributes.content}/>
</div>
</div>
);
},
});
The issue was to do with the content attribute.
Instead of giving the richtext the class of 'captionText' I moved it do a wrapping div and changed the content attribute to the following: -
content: {
type: 'string',
source:'html',
selector: '.captionText',
},
This finds the text inside of that div and allows me to update the block with no validation issues.

Resources