|
|
|
@ -35,7 +35,7 @@ interface Props { |
|
|
|
|
export function OrganizedContent(props: Props) { |
|
|
|
|
const sections = createSections(props.sections); |
|
|
|
|
const currentIndex = sections.findIndex(({ id }) => id === props.currentId); |
|
|
|
|
const [open, setOpen] = useState(false); |
|
|
|
|
const [mobileNavOpen, setMobileNavOpen] = useState(false); |
|
|
|
|
|
|
|
|
|
if (currentIndex < 0) { |
|
|
|
|
throw new Error(`Section with ID ${props.currentId} was not found`); |
|
|
|
@ -43,19 +43,37 @@ export function OrganizedContent(props: Props) { |
|
|
|
|
|
|
|
|
|
const section = sections[currentIndex]; |
|
|
|
|
const isReadAll = section.id === READ_ALL_ID; |
|
|
|
|
const ref = useRef<HTMLDivElement>(null); |
|
|
|
|
const isVisible = useOnScreen(ref.current); |
|
|
|
|
const burgerVisible = useBurger(isVisible); |
|
|
|
|
const navWrapperStyles = mobileNavOpen |
|
|
|
|
? [styles.navWrapper, styles.mobileNavOpen] |
|
|
|
|
: [styles.navWrapper]; |
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
open |
|
|
|
|
mobileNavOpen |
|
|
|
|
? (document.body.style.overflow = "hidden") |
|
|
|
|
: (document.body.style.overflow = "visible"); |
|
|
|
|
}, [open]); |
|
|
|
|
|
|
|
|
|
const ref = useRef<HTMLDivElement>(null); |
|
|
|
|
const isVisible = useOnScreen(ref); |
|
|
|
|
}, [mobileNavOpen]); |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<div className={styles.wrapper} ref={ref}> |
|
|
|
|
<Nav sections={sections} currentIndex={currentIndex} link={props.link} /> |
|
|
|
|
<div |
|
|
|
|
className={navWrapperStyles.join(" ")} |
|
|
|
|
onClick={() => setMobileNavOpen(false)} |
|
|
|
|
> |
|
|
|
|
<div |
|
|
|
|
className={styles.mobileNav} |
|
|
|
|
onClick={(event) => event.stopPropagation()} |
|
|
|
|
> |
|
|
|
|
<Nav |
|
|
|
|
sections={sections} |
|
|
|
|
currentIndex={currentIndex} |
|
|
|
|
link={props.link} |
|
|
|
|
pageTitle={props.pageTitle} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
<div className={styles.content}> |
|
|
|
|
{isReadAll ? ( |
|
|
|
|
<section.Content /> |
|
|
|
@ -73,15 +91,49 @@ export function OrganizedContent(props: Props) { |
|
|
|
|
</> |
|
|
|
|
)} |
|
|
|
|
</div> |
|
|
|
|
<MobileWrapper |
|
|
|
|
open={open} |
|
|
|
|
setOpen={setOpen} |
|
|
|
|
sections={sections} |
|
|
|
|
currentIndex={currentIndex} |
|
|
|
|
link={props.link} |
|
|
|
|
pageTitle={props.pageTitle} |
|
|
|
|
componentIsVisible={isVisible} |
|
|
|
|
/> |
|
|
|
|
<button |
|
|
|
|
className={`${styles.burger} ${ |
|
|
|
|
burgerVisible ? styles.burgerVisible : "" |
|
|
|
|
}`}
|
|
|
|
|
onClick={() => setMobileNavOpen(!mobileNavOpen)} |
|
|
|
|
> |
|
|
|
|
{/* this is copied from hamburger.svg with changed colors */} |
|
|
|
|
<svg |
|
|
|
|
width="30" |
|
|
|
|
height="23" |
|
|
|
|
viewBox="0 0 30 23" |
|
|
|
|
fill="none" |
|
|
|
|
xmlns="http://www.w3.org/2000/svg" |
|
|
|
|
> |
|
|
|
|
<line |
|
|
|
|
x1="28" |
|
|
|
|
y1="2" |
|
|
|
|
x2="2" |
|
|
|
|
y2="2" |
|
|
|
|
strokeWidth="4" |
|
|
|
|
strokeLinecap="round" |
|
|
|
|
strokeLinejoin="round" |
|
|
|
|
/> |
|
|
|
|
<line |
|
|
|
|
x1="28" |
|
|
|
|
y1="11.375" |
|
|
|
|
x2="2" |
|
|
|
|
y2="11.375" |
|
|
|
|
strokeWidth="4" |
|
|
|
|
strokeLinecap="round" |
|
|
|
|
strokeLinejoin="round" |
|
|
|
|
/> |
|
|
|
|
<line |
|
|
|
|
x1="28" |
|
|
|
|
y1="20.75" |
|
|
|
|
x2="2" |
|
|
|
|
y2="20.75" |
|
|
|
|
strokeWidth="4" |
|
|
|
|
strokeLinecap="round" |
|
|
|
|
strokeLinejoin="round" |
|
|
|
|
/> |
|
|
|
|
</svg> |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
@ -90,13 +142,13 @@ interface NavProps { |
|
|
|
|
sections: Section[]; |
|
|
|
|
currentIndex: number; |
|
|
|
|
link: Link; |
|
|
|
|
pageTitle?: string; |
|
|
|
|
pageTitle: string; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function Nav({ sections, currentIndex, link: Link, pageTitle }: NavProps) { |
|
|
|
|
return ( |
|
|
|
|
<div className={styles.nav}> |
|
|
|
|
{pageTitle && <h1 className={styles.mobileNavTitle}>{pageTitle}</h1>} |
|
|
|
|
<h1 className={styles.mobileNavTitle}>{pageTitle}</h1> |
|
|
|
|
{sections.map((section, index) => { |
|
|
|
|
const classNames = [styles.navItem]; |
|
|
|
|
|
|
|
|
@ -164,116 +216,6 @@ function Footer({ sections, currentIndex, link: Link }: FooterProps) { |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
interface MobileProps { |
|
|
|
|
open: boolean; |
|
|
|
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>; |
|
|
|
|
sections: Section[]; |
|
|
|
|
currentIndex: number; |
|
|
|
|
link: Link; |
|
|
|
|
pageTitle: string; |
|
|
|
|
componentIsVisible: boolean; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function MobileWrapper(mobileProps: MobileProps) { |
|
|
|
|
const wrapperRef = useRef<HTMLDivElement>(null); |
|
|
|
|
useOutsideAlerter(wrapperRef, mobileProps.setOpen); |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<div ref={wrapperRef}> |
|
|
|
|
<Burger {...mobileProps} /> |
|
|
|
|
<Menu {...mobileProps} /> |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
interface BurgerProps { |
|
|
|
|
open: boolean; |
|
|
|
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>; |
|
|
|
|
componentIsVisible: boolean; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const Burger = ({ open, setOpen, componentIsVisible }: BurgerProps) => { |
|
|
|
|
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 ( |
|
|
|
|
<div |
|
|
|
|
className={ |
|
|
|
|
styles.burger + " " + (burgerVisible ? "" : styles.hiddenBurger) |
|
|
|
|
} |
|
|
|
|
onClick={() => setOpen(!open)} |
|
|
|
|
> |
|
|
|
|
<div> |
|
|
|
|
<div /> |
|
|
|
|
<div /> |
|
|
|
|
<div /> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
interface MenuProps { |
|
|
|
|
open: boolean; |
|
|
|
|
sections: Section[]; |
|
|
|
|
currentIndex: number; |
|
|
|
|
link: Link; |
|
|
|
|
pageTitle: string; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const Menu = ({ open, sections, currentIndex, link, pageTitle }: MenuProps) => { |
|
|
|
|
const mobileNav = open |
|
|
|
|
? styles.mobileNav |
|
|
|
|
: styles.mobileNav + " " + styles.mobileNavClosed; |
|
|
|
|
return ( |
|
|
|
|
<div className={mobileNav}> |
|
|
|
|
<Nav |
|
|
|
|
sections={sections} |
|
|
|
|
currentIndex={currentIndex} |
|
|
|
|
link={link} |
|
|
|
|
pageTitle={pageTitle} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
function useOutsideAlerter( |
|
|
|
|
ref: React.RefObject<HTMLDivElement>, |
|
|
|
|
setOpen: React.Dispatch<React.SetStateAction<boolean>> |
|
|
|
|
) { |
|
|
|
|
useEffect(() => { |
|
|
|
|
const handleClickOutside = (event: Event) => { |
|
|
|
|
if (ref.current && !ref.current.contains(event.target as Node)) { |
|
|
|
|
setOpen(false); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
document.addEventListener("mousedown", handleClickOutside); |
|
|
|
|
return () => { |
|
|
|
|
document.removeEventListener("mousedown", handleClickOutside); |
|
|
|
|
}; |
|
|
|
|
}, [ref, setOpen]); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function useDebounce(func: () => void, delay = 300) { |
|
|
|
|
const timerRef = useRef<number | undefined>(undefined); |
|
|
|
|
return useCallback(() => { |
|
|
|
@ -326,7 +268,7 @@ function Arrow({ direction }: { direction: "left" | "right" }) { |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function useOnScreen(ref: React.RefObject<HTMLDivElement>) { |
|
|
|
|
function useOnScreen(element: HTMLDivElement | null) { |
|
|
|
|
const [isIntersecting, setIntersecting] = useState(false); |
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
@ -334,14 +276,41 @@ function useOnScreen(ref: React.RefObject<HTMLDivElement>) { |
|
|
|
|
setIntersecting(entry.isIntersecting) |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
if (ref.current) { |
|
|
|
|
observer.observe(ref.current); |
|
|
|
|
if (element) { |
|
|
|
|
observer.observe(element); |
|
|
|
|
} |
|
|
|
|
// Remove the observer as soon as the component is unmounted
|
|
|
|
|
return () => { |
|
|
|
|
observer.disconnect(); |
|
|
|
|
}; |
|
|
|
|
}, [ref]); |
|
|
|
|
}, [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; |
|
|
|
|
} |
|
|
|
|