Mobile Navbar (#75)
continuous-integration/drone/push Build is passing Details

The mobile navbar is done (finally!), except for a swipe gesture to close the navbar.

Closes #30 and #2

Co-authored-by: Amy
Reviewed-on: #75
Reviewed-by: Aditya Thakral <a3thakra@csclub.uwaterloo.ca>
Co-authored-by: Aditya Thakral <a3thakra@csclub.uwaterloo.ca>
Co-committed-by: Aditya Thakral <a3thakra@csclub.uwaterloo.ca>
This commit is contained in:
Aditya Thakral 2021-07-07 18:13:39 -04:00 committed by Amy
parent 5fad324b30
commit b8a7957beb
8 changed files with 443 additions and 77 deletions

View File

@ -13,26 +13,36 @@
justify-content: space-between;
align-items: center;
width: stretch;
width: 100%;
max-width: calc(1440rem / 16);
height: auto;
padding: calc(28rem / 16) calc(136rem / 16);
}
.logo {
.logo,
.logoMobile {
display: flex;
justify-content: center;
align-items: center;
}
.logo:hover {
.logo:hover,
.logoMobile:hover {
cursor: pointer;
}
.logoMobile {
display: none;
}
.logo img {
width: calc(96rem / 16);
}
.hamburger {
display: none;
}
.navMenu {
display: inline-flex;
flex-direction: row;
@ -60,7 +70,7 @@
color: var(--blue-2);
}
.navMenu > li:hover > a {
.navMenu > li > a:hover {
color: var(--blue-2);
font-weight: 600;
}
@ -79,15 +89,19 @@
padding: 1rem;
}
.dropdownWrapper {
.itemWrapper {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
.dropdownIcon {
display: none;
}
.dropdown {
visibility: visible;
visibility: hidden;
display: flex;
flex-direction: column;
@ -107,37 +121,33 @@
font-size: calc(14rem / 16);
}
.dropdown > li {
.dropdown li {
width: 100%;
}
.dropdown > li > a {
.dropdown li a {
padding: calc(8rem / 16);
width: 100%;
box-sizing: border-box;
}
.dropdown > li:hover > a,
.dropdown > li > a:focus {
.dropdown li:hover a,
.dropdown li a:focus {
background-color: var(--blue-1-20);
}
.dropdown > li:first-child > a {
.dropdown li:first-child a {
padding-top: 1rem;
border-radius: calc(8rem / 16) calc(8rem / 16) 0 0;
}
.dropdown > li:last-child > a {
.dropdown li:last-child a {
padding-bottom: 1rem;
border-radius: 0 0 calc(8rem / 16) calc(8rem / 16);
}
.navMenu > li .dropdown {
visibility: hidden;
}
.navMenu > li:hover .dropdown,
.navMenu > li:focus-within .dropdown {
.navMenu li:hover .dropdown,
.navMenu li:focus-within .dropdown {
visibility: visible;
}
@ -149,3 +159,214 @@
padding: calc(28rem / 16) calc(64rem / 16);
}
}
@media screen and (max-width: calc(768rem / 16)) {
.navContent {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-areas: ". logo hamburger";
column-gap: 0;
justify-items: end;
align-items: center;
padding: calc(20rem / 16);
}
.logo {
grid-area: logo;
justify-self: center;
}
.logoMobile {
display: inline-flex;
justify-content: center;
align-items: center;
margin-bottom: calc(20rem / 16);
}
.logo img,
.logoMobile img {
width: calc(80rem / 16);
}
.logoMobile img {
padding: 1rem;
}
.hamburger {
grid-area: hamburger;
display: flex;
justify-content: center;
align-items: center;
padding: calc(12rem / 16);
width: calc(36rem / 16);
box-sizing: content-box;
border: none;
background: none;
}
.hamburger:hover {
cursor: pointer;
}
.navMobileBackground {
position: fixed;
visibility: hidden;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 20;
background-color: var(--navbar-gray);
opacity: 0;
transition: 0.5s;
}
.navMenuWrapper {
position: fixed;
width: calc(288rem / 16);
box-sizing: border-box;
height: 100%;
top: 0;
right: 0;
overflow: auto;
z-index: 30;
padding: calc(calc(64rem / 16) - 1rem);
padding-left: calc(calc(78rem / 16) - 1rem);
background-color: var(--off-white);
transform: translateX(100vw);
transition: 0.5s;
}
.navMenu {
display: flex;
flex-direction: column;
gap: calc(4rem / 16);
width: auto;
font-size: calc(18rem / 16);
font-weight: 500;
text-align: left;
}
.navMenu > .itemWrapper {
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas:
"link button"
"dropdown dropdown";
column-gap: 0;
row-gap: 0;
justify-items: start;
align-items: center;
width: 100%;
}
.navMenu > .itemWrapper > a {
grid-area: link;
}
.dropdownIcon {
grid-area: button;
justify-self: end;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
box-sizing: content-box;
border: none;
background: none;
}
.dropdownIcon:hover {
cursor: pointer;
}
.dropdown {
grid-area: dropdown;
display: none;
visibility: visible;
justify-content: space-between;
align-items: start;
position: static;
margin: 0;
margin-left: calc(18rem / 16);
margin-bottom: calc(18rem / 16);
border-radius: 0;
background: none;
box-shadow: none;
font-size: 1rem;
}
.dropdown > .itemWrapper {
align-items: start;
}
.dropdown li {
width: auto;
text-align: left;
}
.dropdown li a {
width: auto;
}
.dropdown li:hover a,
.dropdown li a:focus {
background: none;
}
.dropdown li:hover a {
color: default;
font-weight: default;
}
.dropdown li:first-child a {
padding-top: calc(8rem / 16);
border-radius: 0;
}
.dropdown li:last-child a {
padding-bottom: calc(8rem / 16);
border-radius: 0;
}
.show {
display: block;
}
.show.navMobileBackground {
visibility: visible;
opacity: 100%;
}
.show.navMobileBackground + .navMenuWrapper {
transform: translateX(0);
}
.rotate {
transform: rotate(180deg);
}
}

View File

@ -1,19 +1,19 @@
import React 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;
}[];
}
}[];
const menu: NavLink[] = [
const menu: Menu = [
{
name: "Home",
route: "/",
@ -66,28 +66,146 @@ const menu: NavLink[] = [
},
{
name: "CS Club Wiki",
route: "https://wiki.csclub.uwaterloo.ca/",
route: "https://wiki.csclub.uwaterloo.ca",
},
],
},
];
function NavItem(props: NavLink) {
export function Navbar() {
const router = useRouter();
const externalLink =
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 })}
>
<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}>
<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();
const isCurrentPage =
router.pathname === props.route ||
(props.submenu != null &&
router.pathname.startsWith(getMainRoute(props.route)));
const isExternalLink =
props.route.includes("http://") || props.route.includes("https://");
function handleClick() {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
props.onClose();
}
return (
<>
{externalLink ? (
{isExternalLink ? (
<a
href={props.route}
target="_blank"
rel="noopener noreferrer"
onClick={() => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}}
onClick={handleClick}
>
{props.name}
</a>
@ -95,61 +213,57 @@ function NavItem(props: NavLink) {
<Link href={props.route}>
<a
title={props.name}
className={
router.pathname === props.route ||
((props.submenu?.length ?? 0) > 0 &&
router.pathname.startsWith(props.route))
? styles.currentPage
: ""
}
onClick={() => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}}
className={isCurrentPage ? styles.currentPage : ""}
onClick={handleClick}
>
{props.name}
</a>
</Link>
)}
{(props.submenu?.length ?? 0) > 0 ? (
<ul className={styles.dropdown}>
<>
<button
className={
props.mainRouteActive
? `${styles.dropdownIcon} ${styles.rotate}`
: styles.dropdownIcon
}
onClick={() => props.onToggle(props.route)}
>
<Image src="/images/dropdown-icon.svg" alt="Dropdown Icon" />
</button>
<ul
className={
props.mainRouteActive
? `${styles.dropdown} ${styles.show}`
: styles.dropdown
}
>
{props.submenu?.map((item) => {
return (
<li className={styles.dropdownWrapper} key={item.name}>
<NavItem name={item.name} route={item.route} />
</li>
);
})}
</ul>
) : null}
</>
);
}
export function Navbar() {
return (
<nav className={styles.navbar}>
<div className={styles.navContent}>
<Link href="/">
<a className={styles.logo}>
<Image src="/images/logo-icon.png" alt="CSC Logo" />
</a>
</Link>
<ul className={styles.navMenu}>
{menu.map((item) => {
return (
<li className={styles.dropdownWrapper} key={item.name}>
<li className={styles.itemWrapper} key={item.name}>
<NavItem
name={item.name}
route={item.route}
submenu={item.submenu}
mainRouteActive={props.mainRouteActive}
onClose={() => props.onClose()}
onToggle={(route) => props.onToggle(route)}
/>
</li>
);
})}
</ul>
</div>
</nav>
</>
) : null}
</>
);
}
function getMainRoute(route: string) {
if (route === "/") {
return "/";
} else if (route.startsWith("http://") || route.startsWith("https://")) {
return route;
}
return "/" + route.split("/")[1];
}

View File

@ -18,6 +18,8 @@ body {
#1481e3 -17.95%,
#4ed4b2 172.82%
);
/* used in mobile navbar background */
--navbar-gray: #787878b2;
color: var(--black);
font-family: "Poppins", "sans-serif";
@ -45,6 +47,8 @@ body {
#1481e3 -17.95%,
#4ed4b2 172.82%
);
/* used in mobile navbar background */
--navbar-gray: #787878b2;
}
h1,

10
pages/resources/index.tsx Normal file
View File

@ -0,0 +1,10 @@
import React from "react";
import Head from "next/head";
export default function Resources() {
return (
<Head>
<meta httpEquiv="refresh" content="0;url=/resources/services" />
</Head>
);
}

View File

@ -0,0 +1,3 @@
<svg width="14" height="9" viewBox="0 0 14 9" fill="none" 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>

After

Width:  |  Height:  |  Size: 318 B

View File

@ -0,0 +1,5 @@
<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" stroke="#2A2A62" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="28" y1="11.375" x2="2" y2="11.375" stroke="#2A2A62" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="28" y1="20.75" x2="2" y2="20.75" stroke="#2A2A62" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

View File

@ -0,0 +1,9 @@
<svg width="175" height="76" viewBox="0 0 175 76" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31.3226 0.526582C17.4935 3.13665 6.39763 12.7205 2.0327 25.8116C-1.14921 35.3954 -0.578098 45.3055 3.62366 54.1145C5.86731 58.8044 8.23335 61.9447 12.2719 65.819C22.7967 75.7291 38.5023 78.7062 52.0866 73.3637L54.2079 72.5073L58.3689 73.9347C72.4427 78.5838 86.8837 75.2805 97.2861 64.9626C104.67 57.6625 107.974 49.9547 108.831 37.8424C109.035 34.8245 109.524 31.3172 109.891 30.0937C111.074 26.1786 113.4 22.3043 116.5 19.1233L119.396 16.1054L121.069 17.4104C124.944 20.4691 128.738 26.9535 129.921 32.5407C131.145 38.3317 130.084 45.3871 127.106 51.0966C125.475 54.1553 119.682 60.0279 116.663 61.6592C115.439 62.3117 113.155 63.2497 111.605 63.7391L108.831 64.6363L111.278 66.8793C114.501 69.7749 119.56 72.6296 124.169 74.0978C137.917 78.5431 152.317 75.0766 162.556 64.9626C169.246 58.3558 172.959 50.4033 173.938 40.6155C174.549 34.2943 172.51 25.363 168.961 19.1233C166.717 15.1674 161.781 9.74337 158.028 7.05174C147.87 -0.207499 134.612 -2.00192 122.782 2.321L119.519 3.50368L117.112 2.52491C112.543 0.689709 109.443 0.159538 103.364 0.159538C97.1229 0.159538 94.4713 0.648929 88.9642 2.76961C80.3159 6.11375 72.0348 14.1479 68.4041 22.7937C66.5684 27.1574 65.6302 31.4395 65.2222 37.4345C65.0183 40.4524 64.5695 43.9597 64.2024 45.1832C63.1009 49.3429 60.8165 53.258 57.6346 56.5614L54.6566 59.6609L53.5144 59.0083C51.6787 57.948 47.8849 53.1765 46.2939 49.9547C44.4174 46.1211 43.6015 42.5731 43.6015 38.0463C43.6015 33.3563 44.4174 29.9714 46.5387 25.6077C49.5166 19.5719 56.0028 14.0255 62.1627 12.1495C63.0602 11.9048 64.0392 11.5786 64.3656 11.497C64.8143 11.3747 64.6511 10.9668 63.7536 10.0696C60.4901 6.60314 53.1881 2.60648 47.4769 1.09753C43.1936 -0.00358955 35.5652 -0.289063 31.3226 0.526582ZM42.5409 11.1707C43.0304 11.3747 42.7041 12.0272 40.9499 14.2702C35.0348 21.8557 32.1385 30.9501 32.7096 40.2485C33.2399 48.6089 36.6666 57.01 41.8882 62.8011C42.6633 63.6575 43.1936 64.4732 43.0712 64.5955C42.9488 64.7586 41.1947 64.9626 39.2366 65.0441C26.4682 65.7374 14.5156 56.7653 11.6192 44.3675C10.7218 40.6155 10.8034 34.9876 11.7824 31.1541C14.1484 21.8149 21.7361 14.1886 31.0371 11.7417C33.9334 11.0076 41.1131 10.6406 42.5409 11.1707ZM107.811 11.1707C108.3 11.3747 107.974 12.0272 106.22 14.2702C100.917 21.0808 97.8572 29.6043 97.8572 37.72C97.8572 41.6351 96.7558 46.2027 94.9201 50.077C93.5739 52.891 92.5948 54.196 89.7393 57.0508C86.8837 59.9056 85.5783 60.8843 82.7635 62.2301C77.6643 64.6771 71.5453 65.6966 66.7724 64.9218L65.263 64.6363L67.0171 62.5564C72.7691 55.7865 75.4207 48.6497 76.1957 37.8424C76.8484 28.8295 78.7658 24.3026 84.1913 18.8786C89.6169 13.4546 95.4912 11.0076 103.242 10.9668C105.404 10.9261 107.444 11.0484 107.811 11.1707ZM142.2 11.5378C150.155 13.4138 156.6 18.6339 160.312 26.2194C164.065 33.8865 164.025 42.0837 160.19 50.077C158.844 52.891 157.865 54.196 155.009 57.0508C152.154 59.9056 150.848 60.8843 148.034 62.2301C142.934 64.6771 136.815 65.6966 132.042 64.9218L130.533 64.6363L132.287 62.5564C139.018 54.6446 142.282 44.0005 141.099 33.8865C140.201 26.3418 136.897 18.5931 132.491 13.6177C130.819 11.7825 130.615 11.3747 131.186 11.2115C132.858 10.7629 139.875 10.9668 142.2 11.5378Z" fill="url(#paint0_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="-19.8935" y1="117.914" x2="316.186" y2="-16.9617" gradientUnits="userSpaceOnUse">
<stop stop-color="#1481E3"/>
<stop offset="1" stop-color="#4ED4B2"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB