Mobile Navbar #75

Merged
a258wang merged 17 commits from feat/navbar-mobile into main 2021-07-07 18:13:40 -04:00
1 changed files with 127 additions and 81 deletions
Showing only changes of commit 4ec4fdb097 - Show all commits

View File

@ -1,29 +1,19 @@
import React, { useState } from "react";
import React, { useReducer } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { Image } from "./Image";
import styles from "./Navbar.module.css";
interface NavLink {
type Menu = {
name: string;
route: string;
submenu?: {
name: string;
route: string;
}[];
}
}[];
interface NavItemProps {
name: string;
route: string;
submenu?: {
name: string;
route: string;
}[];
closeMenu: () => void;
}
const menu: NavLink[] = [
const menu: Menu = [
{
name: "Home",
route: "/",
@ -76,26 +66,140 @@ const menu: NavLink[] = [
},
{
name: "CS Club Wiki",
route: "https://wiki.csclub.uwaterloo.ca/",
route: "https://wiki.csclub.uwaterloo.ca",
},
],
},
];
export function Navbar() {
const router = useRouter();
const [state, dispatch] = useReducer(reducer, initialState);
const parentPath =
router.pathname === "/"
? "/"
: menu.find(
(item) => item.route !== "/" && router.pathname.startsWith(item.route)
)?.route;
return (
<nav className={styles.navbar}>
<div className={styles.navContent}>
<Link href="/">
<a className={styles.logo}>
<Image src="/images/logo-icon.svg" alt="CSC Logo" />
</a>
</Link>
<button
className={styles.hamburger}
onClick={() => dispatch({ type: "open", route: parentPath ?? "" })}
>
<Image src="/images/hamburger.svg" alt="Menu" />
</button>
<div
className={
state.isNavOpen
? `${styles.navMobileBackground} ${styles.show}`
: styles.navMobileBackground
}
onClick={() => dispatch({ type: "close" })}
>
<div
className={styles.navMenuWrapper}
onClick={(e) => e.stopPropagation()}
>
<Link href="/">
<a
className={styles.logoMobile}
onClick={() => dispatch({ type: "close" })}
>
<Image src="/images/logo-icon.svg" alt="CSC Logo" />
</a>
</Link>
<ul className={styles.navMenu}>
{menu.map((item) => {
return (
<li className={styles.itemWrapper} key={item.name}>
<NavItem
name={item.name}
route={item.route}
submenu={item.submenu}
activeSubmenus={state.activeSubmenus}
onClick={(type) => dispatch({ type, route: item.route })}
/>
</li>
);
})}
</ul>
</div>
</div>
</div>
</nav>
);
}
interface MobileState {
isNavOpen: boolean;
activeSubmenus: Set<string>; // strings are NavLink routes
}
type MobileAction =
| { type: "open"; route: string }
| { type: "toggle"; route: string }
| { type: "close" };
const initialState: MobileState = {
isNavOpen: false,
activeSubmenus: new Set(),
};
function reducer(state: MobileState, action: MobileAction): MobileState {
switch (action.type) {
case "open":
return {
isNavOpen: true,
activeSubmenus: new Set([action.route]),
};
case "toggle":
const newSet = new Set(state.activeSubmenus);
if (state.activeSubmenus.has(action.route)) {
newSet.delete(action.route);
} else {
newSet.add(action.route);
}
return {
isNavOpen: state.isNavOpen,
activeSubmenus: newSet,
};
case "close":
return initialState;
}
}
interface NavItemProps {
name: string;
route: string;
submenu?: {
name: string;
route: string;
}[];
activeSubmenus: Set<string>;
onClick: (type: MobileAction["type"]) => void;
}
function NavItem(props: NavItemProps) {
const router = useRouter();
const isCurrentPage =
router.pathname === props.route ||
(props.submenu != null && router.pathname.includes(props.route));
(props.submenu != null && router.pathname.startsWith(props.route));
const isExternalLink =
props.route.includes("http://") || props.route.includes("https://");
const [mobileDropdownOpen, setMobileDropdownOpen] = useState(false);
function handleClick() {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
props.closeMenu();
props.onClick("close");
}
return (
@ -124,17 +228,17 @@ function NavItem(props: NavItemProps) {
<>
<button
className={
mobileDropdownOpen
props.activeSubmenus.has(props.route)
? `${styles.dropdownIcon} ${styles.rotate}`
: styles.dropdownIcon
}
onClick={() => setMobileDropdownOpen(!mobileDropdownOpen)}
onClick={() => props.onClick("toggle")}
>
<Image src="/images/dropdown-icon.svg" alt="Dropdown Icon" />
</button>
<ul
className={
mobileDropdownOpen
props.activeSubmenus.has(props.route)
? `${styles.dropdown} ${styles.show}`
: styles.dropdown
}
@ -145,7 +249,8 @@ function NavItem(props: NavItemProps) {
<NavItem
name={item.name}
route={item.route}
closeMenu={props.closeMenu}
activeSubmenus={props.activeSubmenus}
onClick={props.onClick}
/>
</li>
);
@ -156,62 +261,3 @@ function NavItem(props: NavItemProps) {
</>
);
}
export function Navbar() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
return (
<nav className={styles.navbar}>
<div className={styles.navContent}>
<Link href="/">
<a className={styles.logo}>
<Image src="/images/logo-icon.svg" alt="CSC Logo" />
</a>
</Link>
<button
className={styles.hamburger}
onClick={() => setMobileMenuOpen(true)}
>
<Image src="/images/hamburger.svg" alt="Menu" />
</button>
<div
className={
mobileMenuOpen
? `${styles.navMobileBackground} ${styles.show}`
: styles.navMobileBackground
}
onClick={() => setMobileMenuOpen(false)}
>
<div
className={styles.navMenuWrapper}
onClick={(e) => e.stopPropagation()}
>
<Link href="/">
<a
className={styles.logoMobile}
onClick={() => setMobileMenuOpen(false)}
>
<Image src="/images/logo-icon.svg" alt="CSC Logo" />
</a>
</Link>
<ul className={styles.navMenu}>
{menu.map((item) => {
return (
<li className={styles.itemWrapper} key={item.name}>
<NavItem
name={item.name}
route={item.route}
submenu={item.submenu}
closeMenu={() => {
setMobileMenuOpen(false);
}}
/>
</li>
);
})}
</ul>
</div>
</div>
</div>
</nav>
);
}