Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions website/src/components/Navbar/MobileDrawer/_vars.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$navbar-drawer-width: 80vw;
50 changes: 50 additions & 0 deletions website/src/components/Navbar/MobileDrawer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import c from "clsx"

import s from "./styles.module.scss"

import { Dispatch, ReactNode, SetStateAction, useState } from "react"

interface MobileDrawerProps {
isOpen: boolean
setIsOpen: Dispatch<SetStateAction<boolean>>
secondaryMenu?: ReactNode
children: ReactNode
}

const MobileDrawer = ({ isOpen, setIsOpen, secondaryMenu, children }: MobileDrawerProps) => {
const [menu, setMenu] = useState<'primary' | 'secondary'>(secondaryMenu ? 'secondary' : 'primary')

return (
<>
<div
className={c(s.backdrop, isOpen && s.backdropVisible)}
role="presentation"
onClick={() => { setIsOpen(false); }}
/>
<div className={c(s.drawer, isOpen && s.show)}>
<div className={c(s.panes, menu === 'secondary' && s.showSecondaryPane)}>
<div className={s.pane}>
<button
className={s.switchPaneButton}
onClick={() => { setMenu('secondary') }}
>
Go to secondary menu →
</button>
{children}
</div>
<div className={s.pane}>
<button
className={s.switchPaneButton}
onClick={() => { setMenu('primary') }}
>
← Back to primary menu
</button>
{secondaryMenu}
</div>
</div>
</div>
</>
)
}

export default MobileDrawer
66 changes: 66 additions & 0 deletions website/src/components/Navbar/MobileDrawer/styles.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
@use "./vars";

.backdrop {
position: fixed;
inset: 0;
visibility: hidden;
opacity: 0;
background-color: hsl(0 0% 0% / 0.1);

&.backdropVisible {
visibility: visible;
opacity: 1;
}
}

.drawer {
position: fixed;
top: 0;
bottom: 0;
left: 0;
width: vars.$navbar-drawer-width;

transform: translate3d(-100%, 0, 0);
visibility: hidden;
opacity: 0;

background-color: #fff; // TODO
box-shadow: 0px 8px 8px hsl(0 0 0 / 0.3); // TODO

overflow-x: hidden;

transition: opacity 400ms ease,
visibility 400ms ease,
transform 400ms ease;

&.show {
transform: translateZ(0);
visibility: visible;
opacity: 1;
}

.panes {
display: flex;
height: 100%;

transition: transform 400ms ease;

&.showSecondaryPane {
transform: translate3d(#{-1 * vars.$navbar-drawer-width}, 0, 0);
}

.pane {
flex-shrink: 0;
padding: 1rem 0.5rem 0.5rem;
width: vars.$navbar-drawer-width;
overflow-x: hidden;

.switchPaneButton {
width: 100%;
padding: 0.5rem 1.5rem;
background-color: hsl(0 0% 0% / 0.05);
margin-bottom: 0.5rem;
}
}
}
}
17 changes: 17 additions & 0 deletions website/src/components/Navbar/MobileDrawer/styles.module.scss.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// AUTOGENERATED FILE -- DO NOT EDIT DIRECTLY
declare namespace StylesModuleScssNamespace {
export interface IStylesModuleScss {
backdrop: string;
backdropVisible: string;
drawer: string;
pane: string;
panes: string;
show: string;
showSecondaryPane: string;
switchPaneButton: string;
}
}

declare const StylesModuleScssModule: StylesModuleScssNamespace.IStylesModuleScss;

export = StylesModuleScssModule;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@use "@/styles/utils";
@use "./vars" as *;

:export {
iconSize: utils.strip-units($icon-size);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// AUTOGENERATED FILE -- DO NOT EDIT DIRECTLY
declare namespace ExportsModuleScssNamespace {
export interface IExportsModuleScss {
iconSize: string;
}
}

declare const ExportsModuleScssModule: ExportsModuleScssNamespace.IExportsModuleScss;

export = ExportsModuleScssModule;
1 change: 1 addition & 0 deletions website/src/components/Navbar/MobileMenuButton/_vars.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$icon-size: 32px;
46 changes: 46 additions & 0 deletions website/src/components/Navbar/MobileMenuButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import c from "clsx";
import type { MouseEventHandler } from "react";

import { iconSize as iconSizeString } from "./_exports.module.scss";
import s from "./styles.module.scss"


interface MobileMenuButtonProps {
isOpen?: boolean
onClick: MouseEventHandler<HTMLButtonElement>
className?: string
}

const MobileMenuButton = ({ isOpen, onClick, className }: MobileMenuButtonProps) => {
return (
<button
type="button"
title={isOpen ? 'Close menu' : 'Open menu'}
className={c(s.button, className)}
onClick={onClick}
>
<span className={s.icon} aria-hidden="true">
<svg
className={c(s.hamburgerIcon, isOpen ? s.closed : s.open)}
width={iconSizeString}
height={iconSizeString}
viewBox="0 0 32 32"
>
<path d="M4 24V22H28V24ZM4 17V15H28V17ZM4 10V8H28V10Z" />
</svg>
</span>
<span className={s.icon} aria-hidden="true">
<svg
className={c(s.closeIcon, isOpen ? s.open : s.closed)}
width={iconSizeString}
height={iconSizeString}
viewBox="0 0 32 32"
>
<path d="M8.3 25.1 6.9 23.7 14.6 16 6.9 8.3 8.3 6.9 16 14.6 23.7 6.9 25.1 8.3 17.4 16 25.1 23.7 23.7 25.1 16 17.4Z"/>
</svg>
</span>
</button>
)
}

export default MobileMenuButton
35 changes: 35 additions & 0 deletions website/src/components/Navbar/MobileMenuButton/styles.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@use "./vars";

.button {
width: vars.$icon-size;
height: vars.$icon-size;
cursor: pointer;
position: relative;

.icon {
position: absolute;
top: 0;
left: 0;

.hamburgerIcon, .closeIcon {
position: relative;
transition: all 0.15s ease;
width: vars.$icon-size;
height: vars.$icon-size;
}

.closeIcon {
transform-origin: center center;
}

.open {
transform: scale(1);
opacity: 1;
}

.closed {
transform: scale(0);
opacity: 0;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// AUTOGENERATED FILE -- DO NOT EDIT DIRECTLY
declare namespace StylesModuleScssNamespace {
export interface IStylesModuleScss {
button: string;
closeIcon: string;
closed: string;
hamburgerIcon: string;
icon: string;
open: string;
}
}

declare const StylesModuleScssModule: StylesModuleScssNamespace.IStylesModuleScss;

export = StylesModuleScssModule;
108 changes: 81 additions & 27 deletions website/src/components/Navbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,101 @@
import { useEffect, useState } from "react";
import Link from "next/link"
import c from "clsx";

import { useWindowSize } from "@/hooks/useWindowSize";
import MobileSidebar from "@/components/Sidebar/Mobile";
import MobileMenuButton from "./MobileMenuButton";
import MobileDrawer from "./MobileDrawer";

import s from "./styles.module.scss"

const navLinks = [
{ href: '/workshops', label: 'Workshops' },
{ href: 'https://ai.acmucsd.com', label: 'Home' },
import type { WindowSize } from "@/hooks/useWindowSize";
import type { SidebarItem } from "@/lib/helpers/sidebar";

const navLinks: SidebarItem[] = [
{ type: 'doc', href: '/workshops', label: 'Workshops' },
{ type: 'doc', href: 'https://ai.acmucsd.com', label: 'Home' },
]

const Navbar = (): JSX.Element => {
const useMobileMenu = (size: WindowSize) => {
const [isOpen, setIsOpen] = useState(false)

useEffect(() => {
if (size !== 'mobile') {
setIsOpen(false);
}
}, [size])

return { isOpen, setIsOpen }
}

interface NavbarProps {
sidebar?: SidebarItem[]
path: string
}

const Navbar = ({ sidebar, path }: NavbarProps): JSX.Element => {
const size = useWindowSize()
const { isOpen, setIsOpen } = useMobileMenu(size);

return (
<nav className={s.navbar}>
{/* logo */}
<div className={s.left}>
<Link href="/">
<a className={s.logo}>
<img src="/static/logo.svg" alt="ACM AI Logo" />
<p>ACM AI Wiki</p>
</a>
</Link>
</div>

{/* desktop nav links */}
<div className={c(
s.right,
size === 'mobile' && s.hidden,
)}>
{navLinks.map(({ href, label }) => (
<Link key={label} href={href}>
<a className={s.navItem}>{label}</a>
{/* the actual navbar row */}
<div className={s.row}>
{/* logo */}
<div className={s.left}>
<Link href="/">
<a className={s.logo}>
<img src="/static/logo.svg" alt="ACM AI Logo" />
<p>ACM AI Wiki</p>
</a>
</Link>
))}
<Link href={"https://github.com/acmucsd/acm-ai-workshops"}><a>
<img src="/static/github.svg" alt="GitHub" />
</a></Link>
</div>

{/* desktop nav items */}
<div className={s.right}>
{navLinks.map(({ href, label }) => (
<Link key={label} href={href}>
<a className={s.navItem}>{label}</a>
</Link>
))}
<Link href={"https://github.com/acmucsd/acm-ai-workshops"}><a>
<img src="/static/github.svg" alt="GitHub" />
</a></Link>
</div>

{/* mobile hamburger menu button */}
<MobileMenuButton
className={s.mobileMenuButton}
isOpen={isOpen}
onClick={() => { setIsOpen((x) => !x) }}
/>
</div>

{/* TODO - mobile nav links */}
{/* mobile navbar + sidebar */}
{size !== 'mobile' ? null : (
<MobileDrawer
isOpen={isOpen}
setIsOpen={setIsOpen}
// this goes in the "secondary" menu of the mobile navbar menu
secondaryMenu={sidebar === undefined
? undefined
: <MobileSidebar items={sidebar} activePath={path} />
}
>
{/* all of these go in the "primary" menu of the mobile navbar menu */}

{/* navbar items */}
<MobileSidebar items={navLinks} activePath="" />

{/* row of icons at the bottom */}
<div className={s.mobileNavbarIcons}>
<Link href={"https://github.com/acmucsd/acm-ai-workshops"}><a>
<img src="/static/github.svg" alt="GitHub" />
</a></Link>
</div>
</MobileDrawer>
)}
</nav>
);
}
Expand Down
Loading