Skip to content

Commit be41866

Browse files
authored
Update/mobile menu (#4)
* fix: scrolling gap; add: Pauline, research tab, epfl footer, student google form * fix: mobile menu now part of body
1 parent 22bcee4 commit be41866

File tree

2 files changed

+62
-28
lines changed

2 files changed

+62
-28
lines changed

components/header.tsx

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { Menu, X } from "lucide-react"
44
import Link from "next/link"
55
import { usePathname } from "next/navigation"
66
import * as React from "react"
7+
import { createPortal } from "react-dom"
78

89
import { Button } from "@/components/ui/button"
10+
import { useHeaderOffset } from "@/lib/hooks/useHeaderOffset"
911
import { cn } from "@/lib/utils"
1012

1113
const navigation = [
@@ -22,7 +24,9 @@ const navigation = [
2224
export function Header() {
2325
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false)
2426
const [scrolled, setScrolled] = React.useState(false)
27+
const [mounted, setMounted] = React.useState(false)
2528
const pathname = usePathname()
29+
const headerOffset = useHeaderOffset() ?? 64
2630

2731
React.useEffect(() => {
2832
const handleScroll = () => {
@@ -32,6 +36,23 @@ export function Header() {
3236
return () => window.removeEventListener("scroll", handleScroll)
3337
}, [])
3438

39+
React.useEffect(() => {
40+
setMounted(true)
41+
}, [])
42+
43+
React.useEffect(() => {
44+
if (!mobileMenuOpen) {
45+
return
46+
}
47+
48+
const previousOverflow = document.body.style.overflow
49+
document.body.style.overflow = "hidden"
50+
51+
return () => {
52+
document.body.style.overflow = previousOverflow
53+
}
54+
}, [mobileMenuOpen])
55+
3556
return (
3657
<header
3758
className={cn(
@@ -41,7 +62,7 @@ export function Header() {
4162
: "bg-transparent"
4263
)}
4364
>
44-
<nav className="section-container flex items-center justify-between py-4">
65+
<nav className="section-container flex items-center justify-between py-4" data-header-height>
4566
<div className="flex lg:flex-1">
4667
<Link href="/" className="-m-1.5 p-1.5">
4768
<span className="text-xl font-bold tracking-tight">Mathis Group</span>
@@ -53,6 +74,8 @@ export function Header() {
5374
size="icon"
5475
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
5576
aria-label="Toggle menu"
77+
aria-expanded={mobileMenuOpen}
78+
aria-controls="mobile-nav"
5679
>
5780
{mobileMenuOpen ? (
5881
<X className="size-6" />
@@ -80,27 +103,34 @@ export function Header() {
80103
</nav>
81104

82105
{/* Mobile menu */}
83-
{mobileMenuOpen && (
84-
<div className="lg:hidden">
85-
<div className="space-y-1 border-t border-border p-6">
86-
{navigation.map((item) => (
87-
<Link
88-
key={item.name}
89-
href={item.href}
90-
className={cn(
91-
"block rounded-md px-3 py-2 text-base font-medium transition-colors hover:bg-accent",
92-
pathname === item.href
93-
? "bg-accent text-foreground"
94-
: "text-muted-foreground"
95-
)}
96-
onClick={() => setMobileMenuOpen(false)}
97-
>
98-
{item.name}
99-
</Link>
100-
))}
101-
</div>
102-
</div>
103-
)}
106+
{mounted &&
107+
mobileMenuOpen &&
108+
createPortal(
109+
<div
110+
id="mobile-nav"
111+
className="fixed inset-x-0 bottom-0 z-50 overflow-y-auto border-t border-border bg-background/95 shadow-lg backdrop-blur supports-[backdrop-filter]:bg-background/80 lg:hidden"
112+
style={{ top: headerOffset }}
113+
>
114+
<div className="section-container space-y-1 py-6">
115+
{navigation.map((item) => (
116+
<Link
117+
key={item.name}
118+
href={item.href}
119+
className={cn(
120+
"block rounded-md px-3 py-2 text-base font-medium transition-colors hover:bg-accent",
121+
pathname === item.href
122+
? "bg-accent text-foreground"
123+
: "text-muted-foreground"
124+
)}
125+
onClick={() => setMobileMenuOpen(false)}
126+
>
127+
{item.name}
128+
</Link>
129+
))}
130+
</div>
131+
</div>,
132+
document.body
133+
)}
104134
</header>
105135
)
106136
}

lib/hooks/useHeaderOffset.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ export function useHeaderOffset(additionalOffset = 0) {
1616
}
1717

1818
const updateOffset = () => {
19-
const header = document.querySelector<HTMLElement>("header")
20-
if (!header) {
19+
const target =
20+
document.querySelector<HTMLElement>("[data-header-height]") ??
21+
document.querySelector<HTMLElement>("header")
22+
if (!target) {
2123
setOffset(additionalOffset)
2224
return
2325
}
2426

25-
const { height } = header.getBoundingClientRect()
27+
const { height } = target.getBoundingClientRect()
2628
setOffset(height + additionalOffset)
2729
}
2830

@@ -31,10 +33,12 @@ export function useHeaderOffset(additionalOffset = 0) {
3133

3234
let resizeObserver: ResizeObserver | null = null
3335
if (typeof ResizeObserver !== "undefined") {
34-
const header = document.querySelector<HTMLElement>("header")
35-
if (header) {
36+
const target =
37+
document.querySelector<HTMLElement>("[data-header-height]") ??
38+
document.querySelector<HTMLElement>("header")
39+
if (target) {
3640
resizeObserver = new ResizeObserver(updateOffset)
37-
resizeObserver.observe(header)
41+
resizeObserver.observe(target)
3842
}
3943
}
4044

0 commit comments

Comments
 (0)