import React, { ReactNode, ComponentType, useState, useRef, useEffect, useCallback, } from "react"; import styles from "./OrganizedContent.module.css"; export interface LinkProps { className?: string; id: string; children: ReactNode; } type Link = ComponentType; interface Section { id: string; title: string; Content: ComponentType; } const READ_ALL_TITLE = "Read All"; export const READ_ALL_ID = "read-all"; interface Props { sections: Section[]; currentId: string; pageTitle: string; link: Link; } export function OrganizedContent(props: Props) { const sections = createSections(props.sections); const currentIndex = sections.findIndex(({ id }) => id === props.currentId); const [mobileNavOpen, setMobileNavOpen] = useState(false); if (currentIndex < 0) { throw new Error(`Section with ID ${props.currentId} was not found`); } const section = sections[currentIndex]; const isReadAll = section.id === READ_ALL_ID; const ref = useRef(null); const isVisible = useOnScreen(ref.current); const burgerVisible = useBurger(isVisible); const navWrapperStyles = mobileNavOpen ? [styles.navWrapper, styles.mobileNavOpen] : [styles.navWrapper]; useEffect(() => { mobileNavOpen ? (document.body.style.overflow = "hidden") : (document.body.style.overflow = "visible"); }, [mobileNavOpen]); return (
setMobileNavOpen(false)} >
event.stopPropagation()} >
{isReadAll ? ( ) : ( <>

{section.title}

)}
); } interface NavProps { sections: Section[]; currentIndex: number; link: Link; pageTitle: string; } function Nav({ sections, currentIndex, link: Link, pageTitle }: NavProps) { return (

{pageTitle}

{sections.map((section, index) => { const classNames = [styles.navItem]; if (index === currentIndex) { classNames.push(styles.selected); } if (section.id === READ_ALL_ID) { classNames.push(styles.readAll); } return (
{section.title}
); })}
); } interface FooterProps { sections: Section[]; currentIndex: number; link: Link; } function Footer({ sections, currentIndex, link: Link }: FooterProps) { const prevSection = currentIndex > 0 && sections[currentIndex - 1].id !== READ_ALL_ID ? sections[currentIndex - 1] : undefined; const nextSection = currentIndex < sections.length - 1 && sections[currentIndex + 1].id !== READ_ALL_ID ? sections[currentIndex + 1] : undefined; return (
{prevSection && (
Previous
{prevSection.title}
)} {nextSection && (
Next
{nextSection.title}
)}
); } function useDebounce(func: () => void, delay = 300) { const timerRef = useRef(undefined); return useCallback(() => { if (timerRef.current != null) { return; } timerRef.current = window.setTimeout(() => { func(); timerRef.current = undefined; }, delay); }, [func, delay]); } function createSections(sections: Section[]) { return [ { id: READ_ALL_ID, title: READ_ALL_TITLE, Content() { return ( <> {sections.map(({ id, title, Content: SectionContent }) => (

{title}

))} ); }, }, ...sections, ]; } function Arrow({ direction }: { direction: "left" | "right" }) { return ( ); } function useOnScreen(element: HTMLDivElement | null) { const [isIntersecting, setIntersecting] = useState(false); useEffect(() => { const observer = new IntersectionObserver(([entry]) => setIntersecting(entry.isIntersecting) ); if (element) { observer.observe(element); } // Remove the observer as soon as the component is unmounted return () => { observer.disconnect(); }; }, [element]); return isIntersecting; } function useBurger(componentIsVisible: boolean): boolean { const [prevScrollPos, setPrevScrollPos] = useState(0); const [burgerVisible, setBurgerVisible] = useState(true); const handleScroll = useDebounce(() => { // find current scroll position const currentScrollPos = window.pageYOffset; setBurgerVisible( componentIsVisible && ((prevScrollPos > currentScrollPos && prevScrollPos - currentScrollPos > 70) || currentScrollPos < 10) ); // set state to new scroll position setPrevScrollPos(currentScrollPos); }); useEffect(() => { window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); }, [handleScroll]); return burgerVisible; }