Co-authored-by: William Tran <william.tran1@uwaterloo.ca> Co-authored-by: Aditya Thakral <a3thakra@csclub.uwaterloo.ca> Reviewed-on: #79 Reviewed-by: Aditya Thakral <a3thakra@csclub.uwaterloo.ca> Co-authored-by: w25tran <w25tran@localhost> Co-committed-by: w25tran <w25tran@localhost>
This commit is contained in:
parent
82c311e642
commit
4237da76e3
|
@ -49,7 +49,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected {
|
.selected {
|
||||||
background-color: var(--primary-accent-dim);
|
background-color: var(--primary-accent-lightest);
|
||||||
color: var(--primary-accent);
|
color: var(--primary-accent);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,48 @@
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.burger {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileNavTitle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: calc(768rem / 16)) {
|
@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 {
|
.content h1 {
|
||||||
font-size: calc(18rem / 16);
|
font-size: calc(18rem / 16);
|
||||||
}
|
}
|
||||||
|
@ -138,6 +179,52 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
.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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
import NextLink from "next/link";
|
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";
|
import styles from "./OrganizedContent.module.css";
|
||||||
|
|
||||||
|
@ -17,6 +24,7 @@ interface Props {
|
||||||
sections: Section[];
|
sections: Section[];
|
||||||
id: string;
|
id: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
pageTitle: string;
|
||||||
link: Link;
|
link: Link;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,8 +32,10 @@ export function OrganizedContent({
|
||||||
sections,
|
sections,
|
||||||
id,
|
id,
|
||||||
children,
|
children,
|
||||||
|
pageTitle,
|
||||||
link: Link,
|
link: Link,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const [mobileNavOpen, setMobileNavOpen] = useState(false);
|
||||||
const currentIndex = sections.findIndex(
|
const currentIndex = sections.findIndex(
|
||||||
({ id: sectionId }) => sectionId === id
|
({ id: sectionId }) => sectionId === id
|
||||||
);
|
);
|
||||||
|
@ -36,10 +46,34 @@ export function OrganizedContent({
|
||||||
|
|
||||||
const section = sections[currentIndex];
|
const section = sections[currentIndex];
|
||||||
const isReadAll = section.id === READ_ALL_ID;
|
const isReadAll = section.id === READ_ALL_ID;
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const isVisible = useOnScreen(ref.current);
|
||||||
|
const burgerVisible = useBurger(isVisible);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mobileNavOpen
|
||||||
|
? (document.body.style.overflow = "hidden")
|
||||||
|
: (document.body.style.overflow = "visible");
|
||||||
|
}, [mobileNavOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper} ref={ref}>
|
||||||
<Nav sections={sections} currentIndex={currentIndex} link={Link} />
|
<div
|
||||||
|
className={
|
||||||
|
mobileNavOpen
|
||||||
|
? `${styles.navMobileBackground} ${styles.show}`
|
||||||
|
: styles.navMobileBackground
|
||||||
|
}
|
||||||
|
onClick={() => setMobileNavOpen(false)}
|
||||||
|
/>
|
||||||
|
<Nav
|
||||||
|
sections={sections}
|
||||||
|
currentIndex={currentIndex}
|
||||||
|
link={Link}
|
||||||
|
pageTitle={pageTitle}
|
||||||
|
mobileNavOpen={mobileNavOpen}
|
||||||
|
setMobileNavOpen={setMobileNavOpen}
|
||||||
|
/>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{isReadAll ? (
|
{isReadAll ? (
|
||||||
children
|
children
|
||||||
|
@ -57,6 +91,14 @@ export function OrganizedContent({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
className={`${styles.burger} ${
|
||||||
|
burgerVisible ? styles.burgerVisible : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setMobileNavOpen(!mobileNavOpen)}
|
||||||
|
>
|
||||||
|
<Burger />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -65,11 +107,29 @@ interface NavProps {
|
||||||
sections: Section[];
|
sections: Section[];
|
||||||
currentIndex: number;
|
currentIndex: number;
|
||||||
link: Link;
|
link: Link;
|
||||||
|
pageTitle: string;
|
||||||
|
mobileNavOpen: boolean;
|
||||||
|
setMobileNavOpen: (mobileNavOpen: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Nav({ sections, currentIndex, link: Link }: NavProps) {
|
function Nav({
|
||||||
|
sections,
|
||||||
|
currentIndex,
|
||||||
|
link: Link,
|
||||||
|
pageTitle,
|
||||||
|
mobileNavOpen,
|
||||||
|
setMobileNavOpen,
|
||||||
|
}: NavProps) {
|
||||||
|
const navStyles = mobileNavOpen
|
||||||
|
? [styles.nav, styles.mobileNavOpen]
|
||||||
|
: [styles.nav];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={styles.nav}>
|
<nav
|
||||||
|
className={navStyles.join(" ")}
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<h1 className={styles.mobileNavTitle}>{pageTitle}</h1>
|
||||||
{sections.map((section, index) => {
|
{sections.map((section, index) => {
|
||||||
const classNames = [styles.navItem];
|
const classNames = [styles.navItem];
|
||||||
|
|
||||||
|
@ -82,14 +142,17 @@ function Nav({ sections, currentIndex, link: Link }: NavProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<div
|
||||||
className={classNames.join(" ")}
|
onClick={() => {
|
||||||
id={section.id}
|
setMobileNavOpen(false);
|
||||||
|
}}
|
||||||
key={section.id}
|
key={section.id}
|
||||||
>
|
>
|
||||||
<span className={styles.marker} />
|
<Link className={classNames.join(" ")} id={section.id}>
|
||||||
<div>{section.title}</div>
|
<span className={styles.marker} />
|
||||||
</Link>
|
<div>{section.title}</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -137,6 +200,20 @@ function Footer({ sections, currentIndex, link: Link }: FooterProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useDebounce(func: () => void, delay = 300) {
|
||||||
|
const timerRef = useRef<number | undefined>(undefined);
|
||||||
|
return useCallback(() => {
|
||||||
|
if (timerRef.current != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timerRef.current = window.setTimeout(() => {
|
||||||
|
func();
|
||||||
|
timerRef.current = undefined;
|
||||||
|
}, delay);
|
||||||
|
}, [func, delay]);
|
||||||
|
}
|
||||||
|
|
||||||
export interface SectionWithContent {
|
export interface SectionWithContent {
|
||||||
section: Section;
|
section: Section;
|
||||||
Content: ComponentType;
|
Content: ComponentType;
|
||||||
|
@ -216,3 +293,91 @@ function Arrow({ direction }: { direction: "left" | "right" }) {
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -69,6 +69,7 @@ export function createReadAllPage({
|
||||||
readAllSection.section,
|
readAllSection.section,
|
||||||
...sections.map(({ section }) => section),
|
...sections.map(({ section }) => section),
|
||||||
]}
|
]}
|
||||||
|
pageTitle={title}
|
||||||
link={Link}
|
link={Link}
|
||||||
>
|
>
|
||||||
<readAllSection.Content />
|
<readAllSection.Content />
|
||||||
|
|
|
@ -39,6 +39,7 @@ export function createSectionPage({
|
||||||
<OrganizedContent
|
<OrganizedContent
|
||||||
sections={sections}
|
sections={sections}
|
||||||
id={sections[current].id}
|
id={sections[current].id}
|
||||||
|
pageTitle={title}
|
||||||
link={Link}
|
link={Link}
|
||||||
>
|
>
|
||||||
<MDXRemote {...content} />
|
<MDXRemote {...content} />
|
||||||
|
|
|
@ -22,7 +22,8 @@ export const PALETTE_NAMES = [
|
||||||
"--primary-accent",
|
"--primary-accent",
|
||||||
"--primary-accent-soft",
|
"--primary-accent-soft",
|
||||||
"--primary-accent-light",
|
"--primary-accent-light",
|
||||||
"--primary-accent-dim",
|
"--primary-accent-lighter",
|
||||||
|
"--primary-accent-lightest",
|
||||||
|
|
||||||
"--secondary-accent",
|
"--secondary-accent",
|
||||||
"--secondary-accent-light",
|
"--secondary-accent-light",
|
||||||
|
|
|
@ -238,7 +238,12 @@ export function OrganizedContentDemo() {
|
||||||
)!.Content;
|
)!.Content;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OrganizedContent sections={sections} id={id} link={FakeLink}>
|
<OrganizedContent
|
||||||
|
sections={sections}
|
||||||
|
id={id}
|
||||||
|
link={FakeLink}
|
||||||
|
pageTitle="Playground"
|
||||||
|
>
|
||||||
<Content />
|
<Content />
|
||||||
</OrganizedContent>
|
</OrganizedContent>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,7 +10,8 @@ body {
|
||||||
--primary-accent: #1482e3;
|
--primary-accent: #1482e3;
|
||||||
--primary-accent-soft: #5caff9;
|
--primary-accent-soft: #5caff9;
|
||||||
--primary-accent-light: #c4e0f8;
|
--primary-accent-light: #c4e0f8;
|
||||||
--primary-accent-dim: #f7fbff;
|
--primary-accent-lighter: #e1eefa;
|
||||||
|
--primary-accent-lightest: #f7fbff;
|
||||||
|
|
||||||
--secondary-accent: #4ed4b2;
|
--secondary-accent: #4ed4b2;
|
||||||
--secondary-accent-light: #dcf6f0;
|
--secondary-accent-light: #dcf6f0;
|
||||||
|
|
Loading…
Reference in New Issue