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
74 changes: 52 additions & 22 deletions components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { Menu, X } from "lucide-react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import * as React from "react"
import { createPortal } from "react-dom"

import { Button } from "@/components/ui/button"
import { useHeaderOffset } from "@/lib/hooks/useHeaderOffset"
import { cn } from "@/lib/utils"

const navigation = [
Expand All @@ -22,7 +24,9 @@ const navigation = [
export function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false)
const [scrolled, setScrolled] = React.useState(false)
const [mounted, setMounted] = React.useState(false)
const pathname = usePathname()
const headerOffset = useHeaderOffset() ?? 64

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

React.useEffect(() => {
setMounted(true)
}, [])

React.useEffect(() => {
if (!mobileMenuOpen) {
return
}

const previousOverflow = document.body.style.overflow
document.body.style.overflow = "hidden"

return () => {
document.body.style.overflow = previousOverflow
}
}, [mobileMenuOpen])

return (
<header
className={cn(
Expand All @@ -41,7 +62,7 @@ export function Header() {
: "bg-transparent"
)}
>
<nav className="section-container flex items-center justify-between py-4">
<nav className="section-container flex items-center justify-between py-4" data-header-height>
<div className="flex lg:flex-1">
<Link href="/" className="-m-1.5 p-1.5">
<span className="text-xl font-bold tracking-tight">Mathis Group</span>
Expand All @@ -53,6 +74,8 @@ export function Header() {
size="icon"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
aria-expanded={mobileMenuOpen}
aria-controls="mobile-nav"
>
{mobileMenuOpen ? (
<X className="size-6" />
Expand Down Expand Up @@ -80,27 +103,34 @@ export function Header() {
</nav>

{/* Mobile menu */}
{mobileMenuOpen && (
<div className="lg:hidden">
<div className="space-y-1 border-t border-border p-6">
{navigation.map((item) => (
<Link
key={item.name}
href={item.href}
className={cn(
"block rounded-md px-3 py-2 text-base font-medium transition-colors hover:bg-accent",
pathname === item.href
? "bg-accent text-foreground"
: "text-muted-foreground"
)}
onClick={() => setMobileMenuOpen(false)}
>
{item.name}
</Link>
))}
</div>
</div>
)}
{mounted &&
mobileMenuOpen &&
createPortal(
<div
id="mobile-nav"
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"
style={{ top: headerOffset }}
>
<div className="section-container space-y-1 py-6">
{navigation.map((item) => (
<Link
key={item.name}
href={item.href}
className={cn(
"block rounded-md px-3 py-2 text-base font-medium transition-colors hover:bg-accent",
pathname === item.href
? "bg-accent text-foreground"
: "text-muted-foreground"
)}
onClick={() => setMobileMenuOpen(false)}
>
{item.name}
</Link>
))}
</div>
</div>,
document.body
)}
</header>
)
}
16 changes: 10 additions & 6 deletions lib/hooks/useHeaderOffset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ export function useHeaderOffset(additionalOffset = 0) {
}

const updateOffset = () => {
const header = document.querySelector<HTMLElement>("header")
if (!header) {
const target =
document.querySelector<HTMLElement>("[data-header-height]") ??
document.querySelector<HTMLElement>("header")
if (!target) {
setOffset(additionalOffset)
return
}

const { height } = header.getBoundingClientRect()
const { height } = target.getBoundingClientRect()
setOffset(height + additionalOffset)
}

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

let resizeObserver: ResizeObserver | null = null
if (typeof ResizeObserver !== "undefined") {
const header = document.querySelector<HTMLElement>("header")
if (header) {
const target =
document.querySelector<HTMLElement>("[data-header-height]") ??
document.querySelector<HTMLElement>("header")
if (target) {
resizeObserver = new ResizeObserver(updateOffset)
resizeObserver.observe(header)
resizeObserver.observe(target)
}
}

Expand Down