since3-website/modules/shared/ui/components/site-header.tsx
2026-09-25 00:45:33 +02:00

159 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useEffect, useRef, useState } from "react"
import Image from "next/image"
import { play } from "@/lib/sound"
import { cn } from "@/lib/utils"
import { mainNav, site } from "@/modules/shared/config/site"
import { LiveClock } from "./live-clock"
import { ScrambleLink } from "./scramble-link"
import { SoundToggle } from "./sound-toggle"
/**
* Fixed header in `mix-blend-mode: difference`, so it stays readable on black
* and on white sections. Hides while scrolling down, returns when scrolling up.
*/
export function SiteHeader() {
const [hidden, setHidden] = useState(false)
const [menuOpen, setMenuOpen] = useState(false)
const firstLinkRef = useRef<HTMLAnchorElement>(null)
useEffect(() => {
let lastY = window.scrollY
const onScroll = () => {
const y = window.scrollY
if (Math.abs(y - lastY) < 6) return
setHidden(y > lastY && y > 160)
lastY = y
}
window.addEventListener("scroll", onScroll, { passive: true })
return () => window.removeEventListener("scroll", onScroll)
}, [])
useEffect(() => {
if (!menuOpen) return
firstLinkRef.current?.focus()
const onKey = (event: KeyboardEvent) => {
if (event.key === "Escape") setMenuOpen(false)
}
document.documentElement.style.overflow = "hidden"
window.addEventListener("keydown", onKey)
return () => {
document.documentElement.style.overflow = ""
window.removeEventListener("keydown", onKey)
}
}, [menuOpen])
const close = () => setMenuOpen(false)
return (
<>
<header
data-hidden={hidden && !menuOpen}
className="site-header fixed inset-x-0 top-0 z-85"
>
<nav
aria-label="Hauptnavigation"
className="container-site grid h-20 grid-cols-[1fr_auto] items-center gap-6 md:h-24 lg:grid-cols-[1fr_auto_1fr]"
>
<a href="#top" aria-label="since3 – nach oben" className="w-fit">
<Image
src="/pictures/since3-logo-weiss.svg"
alt=""
width={1617}
height={332}
loading="eager"
className="h-5 w-auto md:h-6"
/>
</a>
<ul className="hidden items-center gap-8 text-label lg:flex">
{mainNav.map((item) => (
<li key={item.href}>
<ScrambleLink href={item.href} className="py-2 opacity-80 hover:opacity-100">
{item.label}
</ScrambleLink>
</li>
))}
</ul>
<div className="flex items-center justify-end gap-2 md:gap-4">
<LiveClock className="hidden font-mono text-label tabular-nums opacity-70 xl:block" />
<SoundToggle />
<a
href="#kontakt"
className="hidden h-10 items-center rounded-full border border-white px-5 text-sm font-medium transition-colors duration-300 hover:bg-white hover:text-black sm:inline-flex"
>
Projekt starten
</a>
<button
type="button"
data-sound="off"
onClick={() => {
play("whoosh")
setMenuOpen((open) => !open)
}}
aria-expanded={menuOpen}
aria-controls="mobile-menu"
className="grid size-10 place-items-center lg:hidden"
>
<span className="sr-only">{menuOpen ? "Menü schließen" : "Menü öffnen"}</span>
<span aria-hidden="true" className="relative block h-3 w-6">
<span
className={cn(
"absolute inset-x-0 top-0 h-0.5 bg-white transition-[translate,rotate] duration-500 ease-in-out-quart",
menuOpen && "translate-y-1.25 rotate-45"
)}
/>
<span
className={cn(
"absolute inset-x-0 bottom-0 h-0.5 bg-white transition-[translate,rotate] duration-500 ease-in-out-quart",
menuOpen && "-translate-y-1.25 -rotate-45"
)}
/>
</span>
</button>
</div>
</nav>
</header>
{/* Outside the header so it does not get blended */}
<div
id="mobile-menu"
data-theme="dark"
inert={!menuOpen}
className={cn(
"fixed inset-0 z-80 flex flex-col justify-end pb-10 transition-[clip-path] duration-700 ease-in-out-quart lg:hidden",
menuOpen ? "[clip-path:inset(0_0_0_0)]" : "[clip-path:inset(0_0_100%_0)]"
)}
>
<nav aria-label="Mobile Navigation" className="container-site">
<ul className="flex flex-col">
{[...mainNav, { label: "Kontakt", href: "#kontakt" }].map((item, index) => (
<li key={item.href} className="overflow-clip border-b border-line">
<a
ref={index === 0 ? firstLinkRef : undefined}
href={item.href}
onClick={close}
style={{ transitionDelay: menuOpen ? `${150 + index * 60}ms` : "0ms" }}
className={cn(
"block py-3 text-display type-poster transition-[translate] duration-700 ease-out-expo",
menuOpen ? "translate-y-0" : "translate-y-full"
)}
>
{item.label}
</a>
</li>
))}
</ul>
<div className="mt-8 flex justify-between font-mono text-label text-fg-muted">
<a href={`mailto:${site.email}`}>{site.email}</a>
<LiveClock className="tabular-nums" />
</div>
</nav>
</div>
</>
)
}