forked from
standard.site/standard.site
Standard.site landing page built in Next.js
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}