import NextLink from "next/link"; import React, { ReactNode, ComponentType, useState, useRef, useEffect, useCallback, } from "react"; import styles from "./OrganizedContent.module.css"; type Link = ComponentType; interface Section { id: string; title: string; } const READ_ALL_TITLE = "Read All"; export const READ_ALL_ID = "read-all"; interface Props { sections: Section[]; id: string; children: ReactNode; pageTitle: string; link: Link; } export function OrganizedContent({ sections, id, children, pageTitle, link: Link, }: Props) { const [mobileNavOpen, setMobileNavOpen] = useState(false); const currentIndex = sections.findIndex( ({ id: sectionId }) => sectionId === id ); if (currentIndex < 0) { throw new Error(`Section with ID ${id} 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); useEffect(() => { mobileNavOpen ? (document.body.style.overflow = "hidden") : (document.body.style.overflow = "visible"); }, [mobileNavOpen]); return (
setMobileNavOpen(false)} />
); } interface NavProps { sections: Section[]; currentIndex: number; link: Link; pageTitle: string; mobileNavOpen: boolean; setMobileNavOpen: (mobileNavOpen: boolean) => void; } function Nav({ sections, currentIndex, link: Link, pageTitle, mobileNavOpen, setMobileNavOpen, }: NavProps) { const navStyles = mobileNavOpen ? [styles.nav, styles.mobileNavOpen] : [styles.nav]; return ( ); } 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]); } export interface SectionWithContent { section: Section; Content: ComponentType; } export function createReadAllSection( sections: Section[], content: false ): Section; export function createReadAllSection( sections: SectionWithContent[], content: true ): SectionWithContent; export function createReadAllSection( sections: SectionWithContent[] | Section[], content = true ): SectionWithContent | Section { const readAllSection = { id: READ_ALL_ID, title: READ_ALL_TITLE, }; return content ? { section: readAllSection, Content: function ReadAllContent() { return ( <> {(sections as SectionWithContent[]).map( ({ section: { id, title }, Content }) => (

{title}

) )} ); }, } : readAllSection; } export interface LinkProps { className?: string; id: string; children: ReactNode; } export function createLink(page: string) { let base = page.startsWith("/") ? page : `/${page}`; base = base.endsWith("/") ? base : `${base}/`; return function Link({ className, id, children }: LinkProps) { const href = id === READ_ALL_ID ? base : base + id; return ( {children} ); }; } 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; } // Inlining this svg because we want to fill in colors using css variables function Burger() { return ( ); }