116 lines
3.6 KiB
TypeScript
116 lines
3.6 KiB
TypeScript
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import { useEffect, useState } from 'react'
|
|
import { cn } from '@/lib/utils'
|
|
import { useScrollContext } from '@/app/providers/LenisProvider'
|
|
import { useDoomOverlay } from '@/app/providers/DoomOverlayProvider'
|
|
import { useWhiteboard } from '@/app/providers/WhiteboardProvider'
|
|
|
|
type Item = { href: string; label: string }
|
|
|
|
const items: Item[] = [
|
|
{ href: '#about', label: 'About' },
|
|
{ href: '#writing', label: 'Writing' },
|
|
{ href: '#contact', label: 'Contact' },
|
|
]
|
|
|
|
export default function SidebarMenu() {
|
|
const [active, setActive] = useState<string>('')
|
|
const { lenis } = useScrollContext()
|
|
const { open: openDoom } = useDoomOverlay()
|
|
const { open: openWhiteboard } = useWhiteboard()
|
|
|
|
useEffect(() => {
|
|
const observers: IntersectionObserver[] = []
|
|
|
|
const onIntersect: IntersectionObserverCallback = (entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
setActive(`#${entry.target.id}`)
|
|
}
|
|
})
|
|
}
|
|
|
|
const opts: IntersectionObserverInit = { rootMargin: '-30% 0px -60% 0px', threshold: 0.1 }
|
|
const observer = new IntersectionObserver(onIntersect, opts)
|
|
|
|
items.forEach((it) => {
|
|
const id = it.href.replace('#', '')
|
|
const el = document.getElementById(id)
|
|
if (el) observer.observe(el)
|
|
})
|
|
|
|
observers.push(observer)
|
|
|
|
return () => {
|
|
observers.forEach((o) => o.disconnect())
|
|
}
|
|
}, [])
|
|
|
|
return (
|
|
<aside className="sticky top-24 h-fit">
|
|
<p className="mb-2 text-xs font-medium uppercase tracking-wider text-neutral-800 dark:text-neutral-300">
|
|
Menu
|
|
</p>
|
|
<nav aria-label="Section menu" className="flex flex-col gap-1">
|
|
{items.map((it) => {
|
|
const isActive = active === it.href
|
|
return (
|
|
<Link
|
|
key={it.href}
|
|
href={`/${it.href}`}
|
|
onClick={(e) => {
|
|
const id = it.href.replace('#', '')
|
|
const el = document.getElementById(id)
|
|
if (el && lenis) {
|
|
e.preventDefault()
|
|
lenis.scrollTo(el, { offset: -80 })
|
|
try { history.replaceState(null, '', `/${it.href}`) } catch {}
|
|
}
|
|
}}
|
|
className={cn(
|
|
'px-3 py-2 text-sm rounded-md transition-colors text-neutral-800 dark:text-neutral-200',
|
|
'hover:bg-black/[0.04] dark:hover:bg-white/5',
|
|
isActive && 'bg-black/[0.06] dark:bg-white/10'
|
|
)}
|
|
aria-current={isActive ? 'true' : undefined}
|
|
>
|
|
{it.label}
|
|
</Link>
|
|
)
|
|
})}
|
|
|
|
<button
|
|
onClick={(e) => {
|
|
// Prevent the opening click from bubbling to the modal backdrop and instantly closing it
|
|
e.stopPropagation();
|
|
openDoom();
|
|
}}
|
|
className={cn(
|
|
'px-3 py-2 text-sm rounded-md transition-colors text-left w-full',
|
|
'hover:bg-black/[0.04] dark:hover:bg-white/5',
|
|
'text-neutral-900 dark:text-neutral-100'
|
|
)}
|
|
>
|
|
running on a potato
|
|
</button>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
openWhiteboard();
|
|
}}
|
|
className={cn(
|
|
'px-3 py-2 text-sm rounded-md transition-colors text-left w-full',
|
|
'hover:bg-black/[0.04] dark:hover:bg-white/5',
|
|
'text-neutral-900 dark:text-neutral-100'
|
|
)}
|
|
aria-label="Open whiteboard"
|
|
>
|
|
Whiteboard
|
|
</button>
|
|
</nav>
|
|
</aside>
|
|
)
|
|
}
|