2026-04-23 20:52:43 +03:00
|
|
|
'use client';
|
2026-04-19 08:40:08 +03:00
|
|
|
|
2026-04-24 08:38:00 +03:00
|
|
|
import { useEffect, useState } from 'react';
|
2026-04-24 10:07:25 +03:00
|
|
|
import { CONTACT_LINKS, cn } from '$shared/lib';
|
2026-04-23 20:52:43 +03:00
|
|
|
import type { NavItem } from '../model/types';
|
2026-04-19 08:40:08 +03:00
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
/**
|
|
|
|
|
* Navigation items to render
|
|
|
|
|
*/
|
2026-04-23 20:52:43 +03:00
|
|
|
items: NavItem[];
|
2026-04-19 08:40:08 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fixed sidebar navigation, visible on lg+ screens.
|
|
|
|
|
*/
|
|
|
|
|
export function SidebarNav({ items }: Props) {
|
2026-04-23 20:52:43 +03:00
|
|
|
const [activeSection, setActiveSection] = useState('bio');
|
2026-04-19 08:40:08 +03:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const observer = new IntersectionObserver(
|
2026-04-23 20:52:43 +03:00
|
|
|
(entries) => {
|
|
|
|
|
entries.forEach((entry) => {
|
2026-04-19 08:40:08 +03:00
|
|
|
if (entry.isIntersecting) {
|
2026-04-23 20:52:43 +03:00
|
|
|
setActiveSection(entry.target.id);
|
2026-04-19 08:40:08 +03:00
|
|
|
}
|
2026-04-23 20:52:43 +03:00
|
|
|
});
|
2026-04-19 08:40:08 +03:00
|
|
|
},
|
|
|
|
|
{ rootMargin: '-20% 0px -70% 0px', threshold: 0 },
|
2026-04-23 20:52:43 +03:00
|
|
|
);
|
2026-04-19 08:40:08 +03:00
|
|
|
|
2026-04-23 20:52:43 +03:00
|
|
|
items.forEach((item) => {
|
|
|
|
|
const el = document.getElementById(item.id);
|
2026-04-24 08:38:00 +03:00
|
|
|
if (el) {
|
|
|
|
|
observer.observe(el);
|
|
|
|
|
}
|
2026-04-23 20:52:43 +03:00
|
|
|
});
|
2026-04-19 08:40:08 +03:00
|
|
|
|
2026-04-23 20:52:43 +03:00
|
|
|
return () => observer.disconnect();
|
|
|
|
|
}, [items]);
|
2026-04-19 08:40:08 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Scrolls to the section by id with a 40px offset.
|
|
|
|
|
*/
|
|
|
|
|
function scrollToSection(id: string) {
|
2026-04-23 20:52:43 +03:00
|
|
|
const el = document.getElementById(id);
|
2026-04-19 08:40:08 +03:00
|
|
|
if (el) {
|
2026-04-23 20:52:43 +03:00
|
|
|
const top = el.getBoundingClientRect().top + window.scrollY - 40;
|
|
|
|
|
window.scrollTo({ top, behavior: 'smooth' });
|
2026-04-19 08:40:08 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
2026-04-23 20:52:43 +03:00
|
|
|
{items.map((item) => {
|
|
|
|
|
const isActive = activeSection === item.id;
|
2026-04-19 08:40:08 +03:00
|
|
|
return (
|
|
|
|
|
<button
|
2026-04-24 08:38:00 +03:00
|
|
|
type="button"
|
2026-04-19 08:40:08 +03:00
|
|
|
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>
|
2026-04-23 20:52:43 +03:00
|
|
|
);
|
2026-04-19 08:40:08 +03:00
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
<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">
|
2026-04-24 10:07:25 +03:00
|
|
|
<a href={`mailto:${CONTACT_LINKS.email}`} className="block">
|
2026-04-23 20:52:43 +03:00
|
|
|
Email
|
|
|
|
|
</a>
|
2026-04-24 10:07:25 +03:00
|
|
|
<a href={CONTACT_LINKS.linkedin} className="block">
|
2026-04-23 20:52:43 +03:00
|
|
|
LinkedIn
|
|
|
|
|
</a>
|
2026-04-24 10:07:25 +03:00
|
|
|
<a href={CONTACT_LINKS.instagram} className="block">
|
2026-04-23 20:52:43 +03:00
|
|
|
Instagram
|
|
|
|
|
</a>
|
2026-04-24 10:07:25 +03:00
|
|
|
<a href={CONTACT_LINKS.arena} className="block">
|
2026-04-23 20:52:43 +03:00
|
|
|
Are.na
|
|
|
|
|
</a>
|
2026-04-19 08:40:08 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</nav>
|
2026-04-23 20:52:43 +03:00
|
|
|
);
|
2026-04-19 08:40:08 +03:00
|
|
|
}
|