Mobile Navbar #75
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue