Skip to content

Commit 804db91

Browse files
feat: add mobile hamburger menu
1 parent de1a265 commit 804db91

2 files changed

Lines changed: 248 additions & 39 deletions

File tree

src/components/header.tsx

Lines changed: 246 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useEffect, useState } from "react";
12
import { Box, Container, Flex, NavLink } from "theme-ui";
23

34
import { genericInformation } from "~/data/generic";
@@ -6,6 +7,17 @@ import { Logo } from "./logo";
67

78
type LinkComponent = React.FC<{ href: string; children: React.ReactNode }>;
89

10+
const menuItems = [
11+
{ href: "#about", label: "About 🍕" },
12+
{ href: "#cfp", label: "CFP 🙋🏻‍♀️" },
13+
{ href: "/compact-agenda/", label: "Compact agenda 🔗" },
14+
{ href: "#schedule", label: "Program 📅" },
15+
{ href: "#organizers", label: "Organizers 👩🏻" },
16+
{ href: "#venue", label: "Venue 🏰" },
17+
{ href: "#sponsors", label: "Sponsors 💛" },
18+
{ href: "#coc", label: "CoC 💂" },
19+
];
20+
921
const MenuLink: LinkComponent = ({ href, children }) => {
1022
return (
1123
<NavLink
@@ -61,6 +73,37 @@ const MenuButton: LinkComponent = ({ children, href }) => (
6173
</Box>
6274
);
6375

76+
type MobileMenuLinkProps = {
77+
href: string;
78+
children: React.ReactNode;
79+
onClick: () => void;
80+
};
81+
82+
const MobileMenuLink: React.FC<MobileMenuLinkProps> = ({
83+
href,
84+
children,
85+
onClick,
86+
}) => (
87+
<NavLink
88+
href={href}
89+
onClick={onClick}
90+
sx={{
91+
color: "white",
92+
fontSize: "2.8rem",
93+
lineHeight: 1.15,
94+
py: ".7rem",
95+
textShadow: "0 .2rem 1rem rgba(80, 0, 0, 0.22)",
96+
"&:hover, &:focus": {
97+
color: "white",
98+
textDecoration: "underline",
99+
textUnderlineOffset: ".5rem",
100+
},
101+
}}
102+
>
103+
{children}
104+
</NavLink>
105+
);
106+
64107
export const ContentButton: LinkComponent = ({ children, href }) => (
65108
<Box
66109
sx={{
@@ -94,54 +137,220 @@ export const ContentButton: LinkComponent = ({ children, href }) => (
94137
</Box>
95138
);
96139

97-
export const Header = () => (
98-
<Box
99-
sx={{
100-
position: "absolute",
101-
top: 0,
102-
left: 0,
103-
zIndex: 10,
140+
export const Header = () => {
141+
const [isMenuOpen, setIsMenuOpen] = useState(false);
104142

105-
width: "100%",
106-
}}
107-
>
108-
<Container
109-
sx={{
110-
width: "100%",
143+
useEffect(() => {
144+
const mediaQuery = window.matchMedia("(min-width: 64em)");
145+
const closeMenuOnDesktop = () => {
146+
if (mediaQuery.matches) {
147+
setIsMenuOpen(false);
148+
}
149+
};
111150

112-
display: "flex",
113-
alignItems: "center",
114-
justifyContent: "space-between",
115-
flexDirection: ["column", null, "row"],
151+
closeMenuOnDesktop();
152+
mediaQuery.addEventListener("change", closeMenuOnDesktop);
116153

117-
color: "white",
118-
p: "secondary",
154+
return () => {
155+
mediaQuery.removeEventListener("change", closeMenuOnDesktop);
156+
};
157+
}, []);
158+
159+
useEffect(() => {
160+
if (!isMenuOpen) {
161+
return;
162+
}
163+
164+
const handleKeyDown = (event: KeyboardEvent) => {
165+
if (event.key === "Escape") {
166+
setIsMenuOpen(false);
167+
}
168+
};
169+
170+
document.body.style.overflow = "hidden";
171+
document.addEventListener("keydown", handleKeyDown);
172+
173+
return () => {
174+
document.body.style.overflow = "";
175+
document.removeEventListener("keydown", handleKeyDown);
176+
};
177+
}, [isMenuOpen]);
178+
179+
const closeMenu = () => setIsMenuOpen(false);
180+
181+
return (
182+
<Box
183+
sx={{
184+
position: "absolute",
185+
top: 0,
186+
left: 0,
187+
zIndex: isMenuOpen ? 30 : 10,
188+
189+
width: "100%",
119190
}}
120191
>
121-
<Logo />
122-
<Flex
192+
<Container
123193
sx={{
194+
width: "100%",
195+
196+
display: "flex",
124197
alignItems: "center",
125-
flexDirection: ["column", "row"],
126-
zIndex: 5,
198+
justifyContent: "space-between",
199+
200+
color: "white",
201+
p: "secondary",
127202
}}
203+
>
204+
<Logo />
205+
<button
206+
type="button"
207+
aria-controls="mobile-menu"
208+
aria-expanded={isMenuOpen}
209+
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
210+
onClick={() => setIsMenuOpen((isOpen) => !isOpen)}
211+
sx={{
212+
display: ["inline-flex", null, "none"],
213+
position: "relative",
214+
zIndex: 40,
215+
width: "5.2rem",
216+
height: "5.2rem",
217+
alignItems: "center",
218+
justifyContent: "center",
219+
border: "1px solid rgba(255, 255, 255, 0.65)",
220+
borderRadius: "50%",
221+
background: "rgba(237, 67, 55, 0.2)",
222+
color: "white",
223+
cursor: "pointer",
224+
p: 0,
225+
backdropFilter: "blur(.5rem)",
226+
}}
227+
>
228+
<Box
229+
as="span"
230+
sx={{
231+
position: "relative",
232+
width: "2.4rem",
233+
height: ".2rem",
234+
borderRadius: ".2rem",
235+
background: isMenuOpen ? "transparent" : "white",
236+
transition: "background .2s ease",
237+
"&:before, &:after": {
238+
content: '""',
239+
position: "absolute",
240+
left: 0,
241+
width: "2.4rem",
242+
height: ".2rem",
243+
borderRadius: ".2rem",
244+
background: "white",
245+
transition: "transform .2s ease, top .2s ease",
246+
},
247+
"&:before": {
248+
top: isMenuOpen ? 0 : "-.8rem",
249+
transform: isMenuOpen ? "rotate(45deg)" : "none",
250+
},
251+
"&:after": {
252+
top: isMenuOpen ? 0 : ".8rem",
253+
transform: isMenuOpen ? "rotate(-45deg)" : "none",
254+
},
255+
}}
256+
/>
257+
</button>
258+
<Flex
259+
sx={{
260+
display: ["none", null, "flex"],
261+
alignItems: "center",
262+
flexDirection: "row",
263+
zIndex: 5,
264+
}}
265+
as="nav"
266+
>
267+
{menuItems.map(({ href, label }) => (
268+
<MenuLink key={href} href={href}>
269+
{label}
270+
</MenuLink>
271+
))}
272+
<MenuButton href={genericInformation.ticketsUrl}>
273+
Buy tickets 🎫
274+
</MenuButton>
275+
</Flex>
276+
</Container>
277+
<Flex
278+
id="mobile-menu"
128279
as="nav"
280+
aria-hidden={!isMenuOpen}
281+
sx={{
282+
display: ["flex", null, "none"],
283+
position: "fixed",
284+
inset: 0,
285+
zIndex: 30,
286+
flexDirection: "column",
287+
alignItems: "center",
288+
justifyContent: "center",
289+
minHeight: "100vh",
290+
px: "secondary",
291+
py: "primary",
292+
background: "url(city.png) center / cover",
293+
color: "white",
294+
opacity: isMenuOpen ? 1 : 0,
295+
pointerEvents: isMenuOpen ? "auto" : "none",
296+
transform: isMenuOpen ? "translateY(0)" : "translateY(-1rem)",
297+
transition: "opacity .22s ease, transform .22s ease",
298+
"&:before": {
299+
content: '""',
300+
position: "absolute",
301+
inset: 0,
302+
backgroundColor: "primary",
303+
opacity: 0.78,
304+
},
305+
}}
129306
>
130-
<MenuLink href="#about">About 🍕</MenuLink>
131-
<MenuLink href="#cfp">CFP 🙋🏻‍♀️</MenuLink>
132-
<MenuLink href="/compact-agenda/">Compact agenda 🔗</MenuLink>
133-
<MenuLink href="#schedule">Program 📅</MenuLink>
134-
<MenuLink href="#organizers">Organizers 👩🏻</MenuLink>
135-
<MenuLink href="#venue">Venue 🏰</MenuLink>
136-
<MenuLink href="#sponsors">Sponsors 💛</MenuLink>
137-
<MenuLink href="#coc">CoC 💂</MenuLink>
138-
<MenuButton href={genericInformation.ticketsUrl}>
139-
Buy tickets 🎫
140-
</MenuButton>
307+
<Box sx={{ position: "relative", zIndex: 1, mb: "secondary" }}>
308+
<Logo />
309+
</Box>
310+
<Flex
311+
sx={{
312+
position: "relative",
313+
zIndex: 1,
314+
alignItems: "center",
315+
flexDirection: "column",
316+
gap: ".4rem",
317+
}}
318+
>
319+
{menuItems.map(({ href, label }) => (
320+
<MobileMenuLink key={href} href={href} onClick={closeMenu}>
321+
{label}
322+
</MobileMenuLink>
323+
))}
324+
<Box sx={{ mt: "secondary", perspective: "24rem" }}>
325+
<NavLink
326+
href={genericInformation.ticketsUrl}
327+
target="_blank"
328+
rel="noopener noreferrer"
329+
onClick={closeMenu}
330+
sx={{
331+
display: "inline-flex",
332+
px: "2.8rem",
333+
py: "1.3rem",
334+
border: "1px solid white",
335+
borderRadius: "10rem",
336+
background: "white",
337+
boxShadow: "0.6rem 0.4rem 2.4rem -0.8rem rgba(25, 0, 0, 0.5)",
338+
color: "primary",
339+
fontSize: "2rem",
340+
"&:hover, &:focus": {
341+
color: "primary",
342+
transform: "translateY(-0.2rem)",
343+
},
344+
}}
345+
>
346+
Buy tickets 🎫
347+
</NavLink>
348+
</Box>
349+
</Flex>
141350
</Flex>
142-
</Container>
143-
</Box>
144-
);
351+
</Box>
352+
);
353+
};
145354

146355
export const FloatingTicketsButton = () => (
147356
<NavLink

src/sections/hero.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const HeroHeading: React.FC<HeroHeadingProps> = ({
1616
<Heading
1717
{...props}
1818
sx={{
19-
fontSize: large ? "large" : "heading",
19+
fontSize: large ? ["4.2rem", "large"] : ["2rem", "heading"],
2020
textAlign: "center",
2121
color: "white",
2222
}}
@@ -34,7 +34,7 @@ export const Hero = () => (
3434
background: "url(city.png) center / cover",
3535
minHeight: "100vh",
3636
position: "relative",
37-
pt: ["52rem", "20rem", "14rem"],
37+
pt: ["12rem", "20rem", "14rem"],
3838
"&:before": {
3939
content: '""',
4040
position: "absolute",

0 commit comments

Comments
 (0)