Standard.site landing page built in Next.js
at main 119 lines 4.2 kB view raw
1'use client' 2 3import { useEffect, useState } from 'react' 4import Link from 'next/link' 5import { AnimateIn, StandardSiteLogo } from '@/app/components' 6import {EXTERNAL_LINKS, NAV_ITEMS, SECONDARY_NAV_ITEMS} from '@/app/data/content' 7import { scrollToElement } from '@/app/lib/scroll' 8import { ArrowUpRightIcon } from 'lucide-react' 9 10export function Sidebar() { 11 const [activeSection, setActiveSection] = useState<string>('#') 12 13 useEffect(() => { 14 const sectionIds = NAV_ITEMS 15 .map(item => item.href) 16 .filter(href => href !== '#') 17 .map(href => href.slice(1)) 18 19 const observers: IntersectionObserver[] = [] 20 21 sectionIds.forEach(id => { 22 const element = document.getElementById(id) 23 if (!element) return 24 25 const observer = new IntersectionObserver( 26 (entries) => { 27 entries.forEach(entry => { 28 if (entry.isIntersecting) { 29 setActiveSection(`#${id}`) 30 } 31 }) 32 }, 33 { 34 rootMargin: '-20% 0px -70% 0px', 35 threshold: 0 36 } 37 ) 38 39 observer.observe(element) 40 observers.push(observer) 41 }) 42 43 // Handle scroll to top (home section) 44 const handleScroll = () => { 45 if (window.scrollY < 100) { 46 setActiveSection('#') 47 } 48 } 49 50 window.addEventListener('scroll', handleScroll) 51 handleScroll() 52 53 return () => { 54 observers.forEach(observer => observer.disconnect()) 55 window.removeEventListener('scroll', handleScroll) 56 } 57 }, []) 58 59 return ( 60 <AnimateIn 61 as="aside" 62 direction="left" 63 delay={ 0.75 } 64 onScroll={ false } 65 className="flex sticky top-8 w-32 shrink-0 flex-col gap-6" 66 > 67 <StandardSiteLogo className="size-7 text-base-content" /> 68 69 <nav className="flex flex-col gap-2"> 70 { NAV_ITEMS.map((item) => ( 71 <a 72 key={ item.label } 73 href={ item.href } 74 onClick={ (e) => { 75 e.preventDefault() 76 scrollToElement(item.href) 77 } } 78 className={ `font-medium text-base tracking-tight ${ 79 activeSection === item.href ? 'text-base-content' : 'text-muted-content' 80 } hover:text-base-content transition-colors` } 81 > 82 { item.label } 83 </a> 84 )) } 85 </nav> 86 87 <div className="h-px w-full bg-border" /> 88 89 <nav className="flex flex-col gap-2"> 90 { SECONDARY_NAV_ITEMS.map((item) => ( 91 <Link 92 key={ item.label } 93 href={ item.href } 94 className="font-medium text-base tracking-tight text-muted-content hover:text-base-content transition-colors" 95 > 96 { item.label } 97 </Link> 98 )) } 99 </nav> 100 101 <div className="h-px w-full bg-border" /> 102 103 <nav className="flex flex-col gap-2"> 104 { EXTERNAL_LINKS.map((link) => ( 105 <a 106 key={ link.label } 107 href={ link.href } 108 target="_blank" 109 rel="noopener noreferrer" 110 className="flex items-end group font-medium text-base tracking-tight text-muted-content hover:text-base-content transition-colors" 111 > 112 { link.label } 113 <ArrowUpRightIcon className="ml-auto size-5 tranform translate-y-2 -translate-x-2 group-hover:translate-0 transition-all duration-300 opacity-0 group-hover:opacity-100"/> 114 </a> 115 )) } 116 </nav> 117 </AnimateIn> 118 ) 119}