mirror of
https://github.com/Since-3/since3-website.git
synced 2026-10-12 19:51:16 +00:00
159 lines
5.6 KiB
TypeScript
159 lines
5.6 KiB
TypeScript
"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>
|
||
</>
|
||
)
|
||
}
|