diff --git a/components/OrganizedContent.module.css b/components/OrganizedContent.module.css index b7bbe06f..2a6f518a 100644 --- a/components/OrganizedContent.module.css +++ b/components/OrganizedContent.module.css @@ -49,7 +49,7 @@ } .selected { - background-color: var(--primary-accent-dim); + background-color: var(--primary-accent-lightest); color: var(--primary-accent); font-weight: 700; } @@ -124,7 +124,48 @@ text-decoration: none; } +.burger { + display: none; +} + +.mobileNavTitle { + display: none; +} + @media only screen and (max-width: calc(768rem / 16)) { + + .burger { + display: flex; + position: fixed; + border: none; + bottom: calc(32rem / 16); + left: calc(16rem / 16); + width: calc(62rem / 16); + height: calc(57rem / 16); + cursor: pointer; + z-index: 9; + background: var(--primary-accent-light); + border-radius: calc(5rem / 16); + transition: transform 0.3s ease-in-out; + transform: translateY(calc(94rem / 16)); + padding: calc(11rem / 16) calc(9rem / 16); + } + + .burgerVisible { + transform: translateY(0); + } + + .burger > svg { + width: 100%; + height: 100%; + stroke: var(--primary-accent); + } + + .navItem { + width: auto; + padding: 0 calc(16rem / 16); + } + .content h1 { font-size: calc(18rem / 16); } @@ -138,6 +179,52 @@ } .nav { - display: none; + position: fixed; + + top: 0; + left: 0; + overflow-y: auto; + z-index: 30; + + margin: 0; + background: var(--primary-accent-lighter); + width: calc(288rem / 16); + + transform: translateX(-100vw); + transition: 0.5s; + } + + .navMobileBackground { + position: fixed; + visibility: hidden; + + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 20; + + background-color: var(--navbar-page-overlay); + opacity: 0; + + transition: 0.5s; + } + + .mobileNavOpen { + transform: translateX(0); + } + + .mobileNavTitle { + display: flex; + font-size: calc(24rem / 16); + font-weight: 700; + margin: calc(14rem / 16); + margin-top: calc(39rem / 16); + } + + .show.navMobileBackground { + visibility: visible; + opacity: 100%; } } + diff --git a/components/OrganizedContent.tsx b/components/OrganizedContent.tsx index 2a490397..c2f26627 100644 --- a/components/OrganizedContent.tsx +++ b/components/OrganizedContent.tsx @@ -1,5 +1,12 @@ import NextLink from "next/link"; -import React, { ReactNode, ComponentType } from "react"; +import React, { + ReactNode, + ComponentType, + useState, + useRef, + useEffect, + useCallback, +} from "react"; import styles from "./OrganizedContent.module.css"; @@ -17,6 +24,7 @@ interface Props { sections: Section[]; id: string; children: ReactNode; + pageTitle: string; link: Link; } @@ -24,8 +32,10 @@ export function OrganizedContent({ sections, id, children, + pageTitle, link: Link, }: Props) { + const [mobileNavOpen, setMobileNavOpen] = useState(false); const currentIndex = sections.findIndex( ({ id: sectionId }) => sectionId === id ); @@ -36,10 +46,34 @@ export function OrganizedContent({ 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 ( -
- @@ -137,6 +200,20 @@ function Footer({ sections, currentIndex, link: Link }: FooterProps) { ); } +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; @@ -216,3 +293,91 @@ function Arrow({ direction }: { direction: "left" | "right" }) { ); } + +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 ( + + + + + + ); +} diff --git a/components/OrganizedContent/ReadAll.tsx b/components/OrganizedContent/ReadAll.tsx index 5275d6c0..60019432 100644 --- a/components/OrganizedContent/ReadAll.tsx +++ b/components/OrganizedContent/ReadAll.tsx @@ -69,6 +69,7 @@ export function createReadAllPage({ readAllSection.section, ...sections.map(({ section }) => section), ]} + pageTitle={title} link={Link} > diff --git a/components/OrganizedContent/Section.tsx b/components/OrganizedContent/Section.tsx index ee08f75b..b5dd64fb 100644 --- a/components/OrganizedContent/Section.tsx +++ b/components/OrganizedContent/Section.tsx @@ -39,6 +39,7 @@ export function createSectionPage({ diff --git a/components/Theme.tsx b/components/Theme.tsx index 15698e21..78729d3c 100644 --- a/components/Theme.tsx +++ b/components/Theme.tsx @@ -22,7 +22,8 @@ export const PALETTE_NAMES = [ "--primary-accent", "--primary-accent-soft", "--primary-accent-light", - "--primary-accent-dim", + "--primary-accent-lighter", + "--primary-accent-lightest", "--secondary-accent", "--secondary-accent-light", diff --git a/components/playground.tsx b/components/playground.tsx index fa6c0faf..7aa9facc 100644 --- a/components/playground.tsx +++ b/components/playground.tsx @@ -238,7 +238,12 @@ export function OrganizedContentDemo() { )!.Content; return ( - + ); diff --git a/pages/_app.css b/pages/_app.css index b0c6d313..92507f49 100644 --- a/pages/_app.css +++ b/pages/_app.css @@ -10,7 +10,8 @@ body { --primary-accent: #1482e3; --primary-accent-soft: #5caff9; --primary-accent-light: #c4e0f8; - --primary-accent-dim: #f7fbff; + --primary-accent-lighter: #e1eefa; + --primary-accent-lightest: #f7fbff; --secondary-accent: #4ed4b2; --secondary-accent-light: #dcf6f0;