2021-05-18 03:16:32 -04:00
|
|
|
import Link from "next/link";
|
|
|
|
import { useRouter } from "next/router";
|
2021-08-13 04:16:37 -04:00
|
|
|
import React, { useReducer } from "react";
|
|
|
|
|
2021-05-20 23:30:52 -04:00
|
|
|
import { Image } from "./Image";
|
2021-08-13 04:16:37 -04:00
|
|
|
|
2021-05-18 03:16:32 -04:00
|
|
|
import styles from "./Navbar.module.css";
|
|
|
|
|
2021-07-07 18:13:39 -04:00
|
|
|
type Menu = {
|
2021-05-18 03:16:32 -04:00
|
|
|
name: string;
|
|
|
|
route: string;
|
2021-08-31 23:07:19 -04:00
|
|
|
exact?: boolean;
|
|
|
|
submenu?: Menu;
|
2021-07-07 18:13:39 -04:00
|
|
|
}[];
|
2021-05-18 03:16:32 -04:00
|
|
|
|
2021-07-07 18:13:39 -04:00
|
|
|
const menu: Menu = [
|
2021-05-18 03:16:32 -04:00
|
|
|
{
|
|
|
|
name: "Home",
|
|
|
|
route: "/",
|
2021-08-31 23:07:19 -04:00
|
|
|
exact: true,
|
2021-05-18 03:16:32 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "About",
|
|
|
|
route: "/about",
|
|
|
|
submenu: [
|
|
|
|
{
|
|
|
|
name: "About Us",
|
|
|
|
route: "/about",
|
2021-08-31 23:07:19 -04:00
|
|
|
exact: true,
|
2021-05-18 03:16:32 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Meet the Team",
|
|
|
|
route: "/about/team",
|
|
|
|
},
|
2021-11-22 12:47:00 -05:00
|
|
|
{
|
|
|
|
name: "Members",
|
|
|
|
route: "/about/members",
|
|
|
|
},
|
2021-05-18 03:16:32 -04:00
|
|
|
{
|
|
|
|
name: "Constitution",
|
|
|
|
route: "/about/constitution",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Code of Conduct",
|
|
|
|
route: "/about/code-of-conduct",
|
|
|
|
},
|
2021-06-21 21:22:54 -04:00
|
|
|
{
|
|
|
|
name: "Our Supporters",
|
|
|
|
route: "/about/our-supporters",
|
|
|
|
},
|
2021-05-18 03:16:32 -04:00
|
|
|
],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Get Involved",
|
|
|
|
route: "/get-involved",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Events",
|
|
|
|
route: "/events",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Resources",
|
2021-05-26 12:13:54 -04:00
|
|
|
route: "/resources/services",
|
|
|
|
submenu: [
|
|
|
|
{
|
|
|
|
name: "Services",
|
|
|
|
route: "/resources/services",
|
|
|
|
},
|
2021-08-25 15:12:09 -04:00
|
|
|
{
|
|
|
|
name: "Machine Usage",
|
|
|
|
route: "/resources/machine-usage-agreement",
|
|
|
|
},
|
2021-05-26 12:13:54 -04:00
|
|
|
{
|
|
|
|
name: "Tech Talks",
|
|
|
|
route: "/resources/tech-talks",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "CS Club Wiki",
|
2021-07-07 18:13:39 -04:00
|
|
|
route: "https://wiki.csclub.uwaterloo.ca",
|
2021-05-26 12:13:54 -04:00
|
|
|
},
|
2021-08-23 14:31:04 -04:00
|
|
|
{
|
|
|
|
name: "Advice",
|
2021-08-31 23:07:19 -04:00
|
|
|
route: "/resources/advice/co-op",
|
|
|
|
submenu: [
|
|
|
|
{
|
|
|
|
name: "Co-op Advice",
|
|
|
|
route: "/resources/advice/co-op",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Academic Advice",
|
|
|
|
route: "/resources/advice/academic",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Additional Resources",
|
|
|
|
route: "/resources/advice/misc",
|
|
|
|
},
|
|
|
|
],
|
2021-08-23 14:31:04 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Internships",
|
2022-10-12 17:14:07 -04:00
|
|
|
route: "https://github.com/uwcsc/2023-internships",
|
2021-08-23 14:31:04 -04:00
|
|
|
},
|
2021-05-26 12:13:54 -04:00
|
|
|
],
|
2021-05-18 03:16:32 -04:00
|
|
|
},
|
|
|
|
];
|
|
|
|
|
2021-07-07 18:13:39 -04:00
|
|
|
export function Navbar() {
|
2021-05-18 03:16:32 -04:00
|
|
|
const router = useRouter();
|
2021-07-07 18:13:39 -04:00
|
|
|
const [state, dispatch] = useReducer(reducer, initialState);
|
|
|
|
|
|
|
|
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: router.pathname })}
|
|
|
|
>
|
2022-06-08 08:45:28 -04:00
|
|
|
<HamburgerSvg />
|
2021-07-07 18:13:39 -04:00
|
|
|
</button>
|
|
|
|
<div
|
|
|
|
className={
|
|
|
|
state.isNavOpen
|
|
|
|
? `${styles.navMobileBackground} ${styles.show}`
|
|
|
|
: styles.navMobileBackground
|
|
|
|
}
|
|
|
|
onClick={() => dispatch({ type: "close" })}
|
|
|
|
/>
|
|
|
|
<div className={styles.navMenuWrapper}>
|
|
|
|
<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}
|
|
|
|
mainRouteActive={state.activeSubmenus.has(
|
|
|
|
getMainRoute(item.route)
|
|
|
|
)}
|
|
|
|
onClose={() => dispatch({ type: "close" })}
|
|
|
|
onToggle={(route) => dispatch({ type: "toggle", route })}
|
|
|
|
/>
|
|
|
|
</li>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</ul>
|
|
|
|
</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([getMainRoute(action.route)]),
|
|
|
|
};
|
|
|
|
case "toggle": {
|
|
|
|
const newSet = new Set(state.activeSubmenus);
|
|
|
|
if (state.activeSubmenus.has(getMainRoute(action.route))) {
|
|
|
|
newSet.delete(getMainRoute(action.route));
|
|
|
|
} else {
|
|
|
|
newSet.add(getMainRoute(action.route));
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
isNavOpen: state.isNavOpen,
|
|
|
|
activeSubmenus: newSet,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
case "close":
|
|
|
|
return initialState;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
interface NavItemProps {
|
|
|
|
name: string;
|
|
|
|
route: string;
|
|
|
|
submenu?: {
|
|
|
|
name: string;
|
|
|
|
route: string;
|
|
|
|
}[];
|
|
|
|
mainRouteActive: boolean;
|
|
|
|
onToggle(route: string): void;
|
|
|
|
onClose(): void;
|
|
|
|
}
|
|
|
|
|
|
|
|
function NavItem(props: NavItemProps) {
|
|
|
|
const router = useRouter();
|
2021-08-31 23:07:19 -04:00
|
|
|
const isCurrentPage = shouldHighlight(
|
|
|
|
router.pathname,
|
|
|
|
props.name,
|
|
|
|
props.route
|
|
|
|
);
|
2021-07-07 18:13:39 -04:00
|
|
|
const isExternalLink =
|
2021-05-26 12:13:54 -04:00
|
|
|
props.route.includes("http://") || props.route.includes("https://");
|
2021-07-07 18:13:39 -04:00
|
|
|
|
|
|
|
function handleClick() {
|
|
|
|
if (document.activeElement instanceof HTMLElement) {
|
|
|
|
document.activeElement.blur();
|
|
|
|
}
|
|
|
|
props.onClose();
|
|
|
|
}
|
|
|
|
|
2021-05-18 03:16:32 -04:00
|
|
|
return (
|
|
|
|
<>
|
2021-07-07 18:13:39 -04:00
|
|
|
{isExternalLink ? (
|
2021-05-18 03:16:32 -04:00
|
|
|
<a
|
2021-05-26 12:13:54 -04:00
|
|
|
href={props.route}
|
|
|
|
target="_blank"
|
|
|
|
rel="noopener noreferrer"
|
2021-07-07 18:13:39 -04:00
|
|
|
onClick={handleClick}
|
2021-05-18 03:16:32 -04:00
|
|
|
>
|
|
|
|
{props.name}
|
|
|
|
</a>
|
2021-05-26 12:13:54 -04:00
|
|
|
) : (
|
|
|
|
<Link href={props.route}>
|
|
|
|
<a
|
|
|
|
title={props.name}
|
2021-07-07 18:13:39 -04:00
|
|
|
className={isCurrentPage ? styles.currentPage : ""}
|
|
|
|
onClick={handleClick}
|
2021-05-26 12:13:54 -04:00
|
|
|
>
|
|
|
|
{props.name}
|
|
|
|
</a>
|
|
|
|
</Link>
|
|
|
|
)}
|
2021-05-18 03:16:32 -04:00
|
|
|
{(props.submenu?.length ?? 0) > 0 ? (
|
2021-07-07 18:13:39 -04:00
|
|
|
<>
|
|
|
|
<button
|
|
|
|
className={
|
|
|
|
props.mainRouteActive
|
|
|
|
? `${styles.dropdownIcon} ${styles.rotate}`
|
|
|
|
: styles.dropdownIcon
|
|
|
|
}
|
|
|
|
onClick={() => props.onToggle(props.route)}
|
|
|
|
>
|
2022-06-08 08:45:28 -04:00
|
|
|
<DropdownSvg />
|
2021-07-07 18:13:39 -04:00
|
|
|
</button>
|
|
|
|
<ul
|
|
|
|
className={
|
|
|
|
props.mainRouteActive
|
|
|
|
? `${styles.dropdown} ${styles.show}`
|
|
|
|
: styles.dropdown
|
|
|
|
}
|
|
|
|
>
|
|
|
|
{props.submenu?.map((item) => {
|
|
|
|
return (
|
|
|
|
<li className={styles.itemWrapper} key={item.name}>
|
|
|
|
<NavItem
|
|
|
|
name={item.name}
|
|
|
|
route={item.route}
|
|
|
|
mainRouteActive={props.mainRouteActive}
|
|
|
|
onClose={() => props.onClose()}
|
|
|
|
onToggle={(route) => props.onToggle(route)}
|
|
|
|
/>
|
|
|
|
</li>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</ul>
|
|
|
|
</>
|
2021-05-18 03:16:32 -04:00
|
|
|
) : null}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-08-31 23:07:19 -04:00
|
|
|
interface Leaf {
|
|
|
|
name: string;
|
|
|
|
route: string;
|
|
|
|
exact?: boolean;
|
|
|
|
ancestors: { name: string; route: string }[];
|
|
|
|
}
|
|
|
|
|
|
|
|
function collectLeaves(
|
|
|
|
accumulator: Leaf[],
|
|
|
|
entry: {
|
|
|
|
name: string;
|
|
|
|
route: string;
|
|
|
|
exact?: boolean;
|
|
|
|
submenu?: Menu;
|
|
|
|
}
|
|
|
|
): Leaf[] {
|
|
|
|
if (entry.submenu == null) {
|
|
|
|
return [...accumulator, { ...entry, ancestors: [] }];
|
|
|
|
}
|
|
|
|
|
|
|
|
const subleaves = entry.submenu.reduce(collectLeaves, [] as Leaf[]);
|
|
|
|
return [
|
|
|
|
...accumulator,
|
|
|
|
...subleaves.map((leaf) => ({
|
|
|
|
...leaf,
|
|
|
|
ancestors: [...leaf.ancestors, { name: entry.name, route: entry.route }],
|
|
|
|
})),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
const leaves: Leaf[] = menu.reduce(collectLeaves, [] as Leaf[]);
|
|
|
|
|
|
|
|
function shouldHighlight(
|
|
|
|
pathname: string,
|
|
|
|
name: string,
|
|
|
|
route: string
|
|
|
|
): boolean {
|
|
|
|
const match = leaves.find((leaf) =>
|
|
|
|
leaf.exact ? leaf.route === pathname : pathname.startsWith(leaf.route)
|
|
|
|
);
|
|
|
|
return match
|
|
|
|
? (match.name === name && match.route === route) ||
|
|
|
|
match.ancestors.find(
|
|
|
|
(ancestor) => ancestor.name === name && ancestor.route === route
|
|
|
|
) != null
|
|
|
|
: false;
|
|
|
|
}
|
|
|
|
|
2021-07-07 18:13:39 -04:00
|
|
|
function getMainRoute(route: string) {
|
|
|
|
if (route === "/") {
|
|
|
|
return "/";
|
|
|
|
} else if (route.startsWith("http://") || route.startsWith("https://")) {
|
|
|
|
return route;
|
|
|
|
}
|
|
|
|
return "/" + route.split("/")[1];
|
2021-05-18 03:16:32 -04:00
|
|
|
}
|
2022-06-08 08:45:28 -04:00
|
|
|
|
|
|
|
function HamburgerSvg() {
|
|
|
|
return (
|
|
|
|
<svg
|
|
|
|
width="30"
|
|
|
|
height="23"
|
|
|
|
viewBox="0 0 30 23"
|
|
|
|
className={styles.icon}
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
>
|
|
|
|
<line
|
|
|
|
x1="28"
|
|
|
|
y1="2"
|
|
|
|
x2="2"
|
|
|
|
y2="2"
|
|
|
|
stroke="#2A2A62"
|
|
|
|
strokeWidth="4"
|
|
|
|
strokeLinecap="round"
|
|
|
|
strokeLinejoin="round"
|
|
|
|
/>
|
|
|
|
<line
|
|
|
|
x1="28"
|
|
|
|
y1="11.375"
|
|
|
|
x2="2"
|
|
|
|
y2="11.375"
|
|
|
|
stroke="#2A2A62"
|
|
|
|
strokeWidth="4"
|
|
|
|
strokeLinecap="round"
|
|
|
|
strokeLinejoin="round"
|
|
|
|
/>
|
|
|
|
<line
|
|
|
|
x1="28"
|
|
|
|
y1="20.75"
|
|
|
|
x2="2"
|
|
|
|
y2="20.75"
|
|
|
|
stroke="#2A2A62"
|
|
|
|
strokeWidth="4"
|
|
|
|
strokeLinecap="round"
|
|
|
|
strokeLinejoin="round"
|
|
|
|
/>
|
|
|
|
</svg>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function DropdownSvg() {
|
|
|
|
return (
|
|
|
|
<svg
|
|
|
|
width="14"
|
|
|
|
height="9"
|
|
|
|
viewBox="0 0 14 9"
|
|
|
|
fill="none"
|
|
|
|
className={styles.icon}
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
>
|
|
|
|
<path
|
|
|
|
d="M7.75593 8.12713C7.35716 8.58759 6.64284 8.58759 6.24407 8.12713L0.638743 1.65465C0.0778675 1.00701 0.537921 0 1.39467 0L12.6053 0C13.4621 0 13.9221 1.00701 13.3613 1.65465L7.75593 8.12713Z"
|
|
|
|
fill="#2A2A62"
|
|
|
|
/>
|
|
|
|
</svg>
|
|
|
|
);
|
|
|
|
}
|