95 lines
2.9 KiB
TypeScript
95 lines
2.9 KiB
TypeScript
|
|
'use client'
|
||
|
|
|
||
|
|
import { useState, useEffect } from 'react'
|
||
|
|
import { cn } from '$shared/lib'
|
||
|
|
import type { NavItem } from '../model/types'
|
||
|
|
|
||
|
|
interface Props {
|
||
|
|
/**
|
||
|
|
* Navigation items to render
|
||
|
|
*/
|
||
|
|
items: NavItem[]
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fixed sidebar navigation, visible on lg+ screens.
|
||
|
|
*/
|
||
|
|
export function SidebarNav({ items }: Props) {
|
||
|
|
const [activeSection, setActiveSection] = useState('bio')
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const observer = new IntersectionObserver(
|
||
|
|
entries => {
|
||
|
|
entries.forEach(entry => {
|
||
|
|
if (entry.isIntersecting) {
|
||
|
|
setActiveSection(entry.target.id)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
},
|
||
|
|
{ rootMargin: '-20% 0px -70% 0px', threshold: 0 },
|
||
|
|
)
|
||
|
|
|
||
|
|
items.forEach(item => {
|
||
|
|
const el = document.getElementById(item.id)
|
||
|
|
if (el) observer.observe(el)
|
||
|
|
})
|
||
|
|
|
||
|
|
return () => observer.disconnect()
|
||
|
|
}, [items])
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Scrolls to the section by id with a 40px offset.
|
||
|
|
*/
|
||
|
|
function scrollToSection(id: string) {
|
||
|
|
const el = document.getElementById(id)
|
||
|
|
if (el) {
|
||
|
|
const top = el.getBoundingClientRect().top + window.scrollY - 40
|
||
|
|
window.scrollTo({ top, behavior: 'smooth' })
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<nav className="fixed top-0 left-0 h-screen w-full lg:w-1/3 bg-ochre-clay brutal-border-right hidden lg:block overflow-y-auto z-50">
|
||
|
|
<div className="px-8 py-12 space-y-2">
|
||
|
|
<div className="mb-12">
|
||
|
|
<h2>Index</h2>
|
||
|
|
<div className="brutal-border-top pt-4">
|
||
|
|
<p className="text-sm opacity-60">Digital Monograph</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{items.map(item => {
|
||
|
|
const isActive = activeSection === item.id
|
||
|
|
return (
|
||
|
|
<button
|
||
|
|
key={item.id}
|
||
|
|
onClick={() => scrollToSection(item.id)}
|
||
|
|
className={cn(
|
||
|
|
'w-full text-left brutal-border bg-ochre-clay px-6 py-4 transition-all duration-300',
|
||
|
|
isActive
|
||
|
|
? 'shadow-[12px_12px_0_var(--carbon-black)] opacity-100 translate-x-0'
|
||
|
|
: 'opacity-40 shadow-none hover:opacity-60',
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
<div className="flex items-baseline gap-4">
|
||
|
|
<span className="text-sm opacity-60">{item.number}</span>
|
||
|
|
<span className="font-heading text-xl font-black">{item.label}</span>
|
||
|
|
</div>
|
||
|
|
</button>
|
||
|
|
)
|
||
|
|
})}
|
||
|
|
|
||
|
|
<div className="mt-12 pt-12 brutal-border-top">
|
||
|
|
<p className="text-sm uppercase tracking-wider mb-4 opacity-60">Quick Links</p>
|
||
|
|
<div className="space-y-3">
|
||
|
|
<a href="mailto:hello@allmy.work" className="block">Email</a>
|
||
|
|
<a href="https://linkedin.com" className="block">LinkedIn</a>
|
||
|
|
<a href="https://instagram.com" className="block">Instagram</a>
|
||
|
|
<a href="https://are.na" className="block">Are.na</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</nav>
|
||
|
|
)
|
||
|
|
}
|