33import Link from 'next/link' ;
44import { usePathname } from 'next/navigation' ;
55import { useState } from 'react' ;
6+ import {
7+ Github ,
8+ Database ,
9+ ChevronRight ,
10+ BarChart3 ,
11+ Rocket ,
12+ Bot ,
13+ Layers ,
14+ TrendingUp ,
15+ RefreshCw ,
16+ Zap ,
17+ Wallet ,
18+ Repeat ,
19+ type LucideIcon ,
20+ } from 'lucide-react' ;
621
722interface NavItem {
823 label : string ;
924 href : string ;
25+ icon ?: LucideIcon ;
1026 children ?: NavItem [ ] ;
1127}
1228
1329const navItems : NavItem [ ] = [
14- { label : 'Home' , href : '/' } ,
15- { label : 'Quick Start' , href : '/quick-start' } ,
16- { label : 'Agent Guide' , href : '/agent-guide' } ,
17- { label : 'Publications' , href : '/publications' } ,
30+ { label : 'Quick Start' , href : '/quick-start' , icon : Rocket } ,
31+ { label : 'Agent Guide' , href : '/agent-guide' , icon : Bot } ,
1832 {
1933 label : 'Data' ,
2034 href : '/data' ,
2135 children : [
2236 {
2337 label : 'Sources' ,
2438 href : '/data/sources' ,
39+ icon : Database ,
2540 children : [
2641 { label : 'Open Dev Data' , href : '/data/sources/open-dev-data' } ,
2742 { label : 'GitHub Archive' , href : '/data/sources/github-archive' } ,
@@ -31,6 +46,7 @@ const navItems: NavItem[] = [
3146 {
3247 label : 'Models' ,
3348 href : '/data/models' ,
49+ icon : Layers ,
3450 children : [
3551 { label : 'Ecosystems' , href : '/data/models/ecosystems' } ,
3652 { label : 'Repositories' , href : '/data/models/repositories' } ,
@@ -43,6 +59,7 @@ const navItems: NavItem[] = [
4359 {
4460 label : 'Metric Definitions' ,
4561 href : '/data/metric-definitions' ,
62+ icon : BarChart3 ,
4663 children : [
4764 { label : 'Activity' , href : '/data/metric-definitions/activity' } ,
4865 { label : 'Alignment' , href : '/data/metric-definitions/alignment' } ,
@@ -56,181 +73,153 @@ const navItems: NavItem[] = [
5673 label : 'Insights' ,
5774 href : '/insights' ,
5875 children : [
59- { label : '2025 Developer Trends ' , href : '/insights/developer-report-2025' } ,
60- { label : 'Lifecycle Analysis ' , href : '/insights/developer-lifecycle' } ,
61- { label : 'Speedrun Ethereum ' , href : '/insights/speedrun-ethereum' } ,
62- { label : 'DeFi Builder Journeys ' , href : '/insights/defi-builder-journeys' } ,
63- { label : 'Retention Analysis' , href : '/insights/developer-retention' } ,
76+ { label : 'DeFi Builder Journeys ' , href : '/insights/defi-builder-journeys' , icon : Wallet } ,
77+ { label : 'Speedrun Ethereum ' , href : '/insights/speedrun-ethereum' , icon : Zap } ,
78+ { label : '2025 Developer Trends ' , href : '/insights/developer-report-2025' , icon : TrendingUp } ,
79+ { label : 'Lifecycle Analysis ' , href : '/insights/developer-lifecycle' , icon : RefreshCw } ,
80+ { label : 'Retention Analysis' , href : '/insights/developer-retention' , icon : Repeat } ,
6481 ] ,
6582 } ,
6683] ;
6784
68- function ChevronIcon ( { className } : { className ?: string } ) {
85+ function NavLeaf ( { item, pathname } : { item : NavItem ; pathname : string } ) {
86+ const isActive = pathname === item . href ;
87+
6988 return (
70- < svg
71- width = "14"
72- height = "14"
73- viewBox = "0 0 14 14"
74- fill = "none"
75- xmlns = "http://www.w3.org/2000/svg"
76- className = { className }
77- >
78- < path
79- d = "M5.25 3.5L8.75 7L5.25 10.5"
80- stroke = "currentColor"
81- strokeWidth = "1.5"
82- strokeLinecap = "round"
83- strokeLinejoin = "round"
84- />
85- </ svg >
89+ < li >
90+ < Link
91+ href = { item . href }
92+ className = { `flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors ${
93+ isActive
94+ ? 'bg-[var(--sidebar-accent)] text-[var(--sidebar-primary)] font-medium'
95+ : 'text-[var(--sidebar-foreground)] hover:bg-[var(--sidebar-accent)] hover:text-[var(--sidebar-primary)]'
96+ } `}
97+ >
98+ { item . icon && < item . icon className = "h-4 w-4 shrink-0" /> }
99+ < span className = "truncate" > { item . label } </ span >
100+ </ Link >
101+ </ li >
86102 ) ;
87103}
88104
89- function NavSection ( { item, pathname, level = 0 } : { item : NavItem ; pathname : string ; level ?: number } ) {
90- const [ isOpen , setIsOpen ] = useState (
91- level === 0
92- ? true
93- : ( item . children ?. some ( ( child ) =>
94- child . href === pathname ||
95- child . children ?. some ( ( grandchild ) => grandchild . href === pathname )
96- ) || false )
105+ function NavGroup ( { item, pathname } : { item : NavItem ; pathname : string } ) {
106+ const isChildActive = item . children ?. some (
107+ ( child ) =>
108+ child . href === pathname ||
109+ child . children ?. some ( ( gc ) => gc . href === pathname )
97110 ) ;
98- const isActive = pathname === item . href || ( pathname . startsWith ( item . href + '/' ) && item . href !== '/' ) ;
99- const isExactActive = pathname === item . href ;
100-
101- if ( item . children ) {
102- const isSectionHeader = level === 0 ;
111+ const [ isOpen , setIsOpen ] = useState ( isChildActive ?? false ) ;
103112
104- if ( isSectionHeader ) {
105- // Top-level section headers (DATA, INSIGHTS)
106- return (
107- < div >
108- < button
109- onClick = { ( ) => setIsOpen ( ! isOpen ) }
110- className = "w-full text-left text-[11px] font-medium text-gray-400 uppercase tracking-wider px-6 pt-6 pb-2 flex items-center justify-between group"
111- >
112- < span > { item . label } </ span >
113- < span className = { `transition-transform duration-200 ease-out ${ isOpen ? 'rotate-90' : '' } ` } >
114- < ChevronIcon className = "w-3.5 h-3.5 text-gray-300 group-hover:text-gray-400" />
115- </ span >
116- </ button >
117- { isOpen && (
118- < div >
119- { item . children . map ( ( child ) => (
120- < NavSection key = { child . href } item = { child } pathname = { pathname } level = { level + 1 } />
121- ) ) }
122- </ div >
113+ return (
114+ < li >
115+ < button
116+ onClick = { ( ) => setIsOpen ( ! isOpen ) }
117+ className = { `flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors ${
118+ isChildActive
119+ ? 'text-[var(--sidebar-primary)] font-medium'
120+ : 'text-[var(--sidebar-foreground)] hover:bg-[var(--sidebar-accent)] hover:text-[var(--sidebar-primary)]'
121+ } `}
122+ >
123+ { item . icon && < item . icon className = "h-4 w-4 shrink-0" /> }
124+ < span className = "flex-1 truncate text-left" > { item . label } </ span >
125+ < ChevronRight
126+ className = { `h-3.5 w-3.5 shrink-0 text-[var(--sidebar-muted)] transition-transform duration-200 ${
127+ isOpen ? 'rotate-90' : ''
128+ } `}
129+ />
130+ </ button >
131+ { isOpen && (
132+ < ul className = "mt-0.5 ml-3 border-l border-[var(--sidebar-border)] pl-2 space-y-0.5" >
133+ { item . children ! . map ( ( child ) =>
134+ child . children ? (
135+ < NavGroup key = { child . href } item = { child } pathname = { pathname } />
136+ ) : (
137+ < NavLeaf key = { child . href } item = { child } pathname = { pathname } />
138+ )
123139 ) }
124- </ div >
125- ) ;
126- }
140+ </ ul >
141+ ) }
142+ </ li >
143+ ) ;
144+ }
127145
128- // Nested expandable items (Sources, Models, etc.)
146+ function NavSection ( { item, pathname } : { item : NavItem ; pathname : string } ) {
147+ if ( item . children ) {
129148 return (
130- < div >
131- < button
132- onClick = { ( ) => setIsOpen ( ! isOpen ) }
133- className = { `w-full text-left mx-3 px-3 py-2 rounded-md flex items-center justify-between transition-all duration-150 text-[13px] ${
134- isActive
135- ? 'bg-gray-900/5 text-gray-900 font-medium'
136- : 'text-gray-600 hover:text-gray-900 hover:bg-gray-900/[0.03]'
137- } `}
138- style = { { width : 'calc(100% - 24px)' } }
139- >
140- < span > { item . label } </ span >
141- < span className = { `transition-transform duration-200 ease-out ${ isOpen ? 'rotate-90' : '' } ` } >
142- < ChevronIcon className = "w-3.5 h-3.5 text-gray-400" />
143- </ span >
144- </ button >
145- { isOpen && (
146- < div className = "pl-3" >
147- { item . children . map ( ( child ) => (
148- < NavSection key = { child . href } item = { child } pathname = { pathname } level = { level + 1 } />
149- ) ) }
150- </ div >
151- ) }
149+ < div className = "px-3 py-1" >
150+ < p className = "px-2 pb-1.5 pt-3 text-xs font-medium text-[var(--sidebar-muted)] uppercase tracking-wider" >
151+ { item . label }
152+ </ p >
153+ < ul className = "space-y-0.5" >
154+ { item . children . map ( ( child ) =>
155+ child . children ? (
156+ < NavGroup key = { child . href } item = { child } pathname = { pathname } />
157+ ) : (
158+ < NavLeaf key = { child . href } item = { child } pathname = { pathname } />
159+ )
160+ ) }
161+ </ ul >
152162 </ div >
153163 ) ;
154164 }
155165
156- // Leaf nav items (actual links)
157166 return (
158- < Link
159- href = { item . href }
160- className = { `block mx-3 px-3 py-2 rounded-md transition-all duration-150 text-[13px] ${
161- isExactActive
162- ? 'bg-gray-900/5 text-gray-900 font-medium border-l-2 border-gray-900 pl-[10px]'
163- : 'text-gray-600 hover:text-gray-900 hover:bg-gray-900/[0.03]'
164- } `}
165- >
166- { item . label }
167- </ Link >
167+ < div className = "px-3" >
168+ < ul >
169+ < NavLeaf item = { item } pathname = { pathname } />
170+ </ ul >
171+ </ div >
168172 ) ;
169173}
170174
171175export default function Sidebar ( ) {
172176 const pathname = usePathname ( ) ;
173177
174178 return (
175- < div className = "w-64 bg-gray-50/80 border-r border-gray-200/60 h-full overflow-y-auto flex flex-col" >
176- { /* Logo/Header Section */ }
177- < div className = "px-5 py-5" >
178- < div className = "flex items-center gap-2.5" >
179- < svg
180- xmlns = "http://www.w3.org/2000/svg"
181- fill = "none"
182- viewBox = "0 0 112 154"
183- className = "w-7 h-auto text-gray-900"
184- role = "img"
185- >
186- < path
187- d = "M48.303 153.213c-10.797 0-21.457-2.796-31.1-8.277l5.078-8.934c12.275 6.977 26.536 8.756 40.153 5.008 13.616-3.749 24.963-12.576 31.94-24.863 6.976-12.28 8.756-26.542 5.008-40.158-3.749-13.617-12.577-24.958-24.863-31.94l5.073-8.934c14.67 8.337 25.211 21.882 29.693 38.142 4.476 16.266 2.353 33.294-5.983 47.957-8.337 14.669-21.883 25.212-38.143 29.687a63.538 63.538 0 01-16.857 2.295v.017z"
188- fill = "currentColor"
189- />
190- < path
191- d = "M48.338 132.661c-7.284 0-14.48-1.886-20.983-5.581l5.002-8.828c7.514 4.269 16.307 5.245 24.637 2.951 8.331-2.294 15.267-7.692 19.53-15.207 8.81-15.51 3.358-35.293-12.145-44.097l5.073-8.934c20.434 11.607 27.612 37.67 16.006 58.11-5.623 9.897-14.764 17.016-25.738 20.037a42.877 42.877 0 01-11.382 1.549z"
192- fill = "currentColor"
193- />
194- < path
195- d = "M32.357 118.258c-14.664-8.33-25.212-21.876-29.688-38.136-4.475-16.266-2.353-33.3 5.978-47.969C25.86 1.881 64.486-8.757 94.764 8.443l-5.073 8.935c-25.353-14.404-57.7-5.5-72.11 19.854-6.977 12.28-8.757 26.548-5.002 40.164 3.749 13.617 12.529 24.975 24.81 31.946l-5.026 8.916h-.006z"
196- fill = "currentColor"
197- />
198- < path
199- d = "M42.455 100.408C22.02 88.801 14.896 62.744 26.503 42.304c5.622-9.897 14.775-17.028 25.767-20.073 11.003-3.05 22.515-1.632 32.413 3.991l-5.085 8.91c-7.503-4.262-16.23-5.309-24.585-2.997-8.354 2.312-15.308 7.728-19.576 15.248-8.81 15.51-3.359 35.293 12.144 44.097l-5.126 8.928z"
200- fill = "currentColor"
201- />
202- < path
203- d = "M58.502 72.24c-5.872-3.334-7.894-10.08-4.79-15.544 1.686-2.962 3.986-4.783 6.835-5.41 2.768-.609 5.93-.012 8.905 1.68l5.061-8.911c-5.185-2.945-10.926-3.962-16.17-2.809-5.66 1.242-10.478 4.925-13.57 10.371-5.895 10.377-2.14 23.402 8.62 29.534 5.771 3.281 7.834 10.175 4.695 15.704-1.508 2.66-3.82 4.41-6.675 5.061-2.892.662-6.073.124-8.964-1.52l-5.073 8.934c3.642 2.07 7.621 3.134 11.565 3.134a21.61 21.61 0 004.766-.532c5.664-1.295 10.394-4.849 13.32-10.005 5.978-10.524 2.236-23.561-8.525-29.693v.006z"
204- fill = "currentColor"
205- />
206- </ svg >
207- < div className = "leading-tight" >
208- < p className = "text-[12px] font-semibold text-gray-900" > Developer</ p >
209- < p className = "text-[12px] font-semibold text-gray-900" > Data</ p >
210- < p className = "text-[12px] font-semibold text-gray-900" > Portal</ p >
179+ < aside className = "w-64 shrink-0 bg-[var(--sidebar-bg)] border-r border-[var(--sidebar-border)] h-full flex flex-col overflow-hidden" >
180+ { /* Header */ }
181+ < div className = "px-4 py-3 border-b border-[var(--sidebar-border)]" >
182+ < Link href = "/" className = "flex items-center gap-2.5 rounded-lg px-1 py-1 hover:bg-[var(--sidebar-accent)] transition-colors" >
183+ < div className = "h-7 w-7 rounded-md bg-[#3080B6] flex items-center justify-center text-white shrink-0" >
184+ < svg
185+ xmlns = "http://www.w3.org/2000/svg"
186+ fill = "none"
187+ viewBox = "0 0 112 154"
188+ className = "w-4 h-4"
189+ >
190+ < path d = "M48.303 153.213c-10.797 0-21.457-2.796-31.1-8.277l5.078-8.934c12.275 6.977 26.536 8.756 40.153 5.008 13.616-3.749 24.963-12.576 31.94-24.863 6.976-12.28 8.756-26.542 5.008-40.158-3.749-13.617-12.577-24.958-24.863-31.94l5.073-8.934c14.67 8.337 25.211 21.882 29.693 38.142 4.476 16.266 2.353 33.294-5.983 47.957-8.337 14.669-21.883 25.212-38.143 29.687a63.538 63.538 0 01-16.857 2.295v.017z" fill = "currentColor" />
191+ < path d = "M48.338 132.661c-7.284 0-14.48-1.886-20.983-5.581l5.002-8.828c7.514 4.269 16.307 5.245 24.637 2.951 8.331-2.294 15.267-7.692 19.53-15.207 8.81-15.51 3.358-35.293-12.145-44.097l5.073-8.934c20.434 11.607 27.612 37.67 16.006 58.11-5.623 9.897-14.764 17.016-25.738 20.037a42.877 42.877 0 01-11.382 1.549z" fill = "currentColor" />
192+ < path d = "M32.357 118.258c-14.664-8.33-25.212-21.876-29.688-38.136-4.475-16.266-2.353-33.3 5.978-47.969C25.86 1.881 64.486-8.757 94.764 8.443l-5.073 8.935c-25.353-14.404-57.7-5.5-72.11 19.854-6.977 12.28-8.757 26.548-5.002 40.164 3.749 13.617 12.529 24.975 24.81 31.946l-5.026 8.916h-.006z" fill = "currentColor" />
193+ < path d = "M42.455 100.408C22.02 88.801 14.896 62.744 26.503 42.304c5.622-9.897 14.775-17.028 25.767-20.073 11.003-3.05 22.515-1.632 32.413 3.991l-5.085 8.91c-7.503-4.262-16.23-5.309-24.585-2.997-8.354 2.312-15.308 7.728-19.576 15.248-8.81 15.51-3.359 35.293 12.144 44.097l-5.126 8.928z" fill = "currentColor" />
194+ < path d = "M58.502 72.24c-5.872-3.334-7.894-10.08-4.79-15.544 1.686-2.962 3.986-4.783 6.835-5.41 2.768-.609 5.93-.012 8.905 1.68l5.061-8.911c-5.185-2.945-10.926-3.962-16.17-2.809-5.66 1.242-10.478 4.925-13.57 10.371-5.895 10.377-2.14 23.402 8.62 29.534 5.771 3.281 7.834 10.175 4.695 15.704-1.508 2.66-3.82 4.41-6.675 5.061-2.892.662-6.073.124-8.964-1.52l-5.073 8.934c3.642 2.07 7.621 3.134 11.565 3.134a21.61 21.61 0 004.766-.532c5.664-1.295 10.394-4.849 13.32-10.005 5.978-10.524 2.236-23.561-8.525-29.693v.006z" fill = "currentColor" />
195+ </ svg >
211196 </ div >
212- </ div >
197+ < p className = "text-sm font-semibold text-[var(--sidebar-primary)] truncate" > Developer Data Portal</ p >
198+ </ Link >
213199 </ div >
214200
215201 { /* Navigation */ }
216- < nav className = "flex-1 py-2 " >
202+ < nav className = "flex-1 overflow-y-auto py-1 " >
217203 { navItems . map ( ( item ) => (
218204 < NavSection key = { item . href } item = { item } pathname = { pathname } />
219205 ) ) }
220206 </ nav >
221207
222208 { /* Footer */ }
223- < div className = "mt-auto px-6 py-5 border-t border-gray-200/60 " >
209+ < div className = "border-t border-[var(--sidebar- border)] px-4 py-3 " >
224210 < a
225- href = "https://docs.oso.xyz"
226- className = "inline-flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-900 transition-colors"
211+ href = "https://github.com/opensource-observer/ddp"
212+ target = "_blank"
213+ rel = "noopener noreferrer"
214+ className = "flex items-center gap-2 px-1 text-sm text-[var(--sidebar-muted)] hover:text-[var(--sidebar-primary)] transition-colors"
227215 >
228- Docs
216+ < Github className = "h-4 w-4" />
217+ < span > GitHub</ span >
229218 </ a >
230- < p className = "text-xs text-gray-400 mt-3 " >
231- Powered by < span className = "text-gray-500 font-medium " > OSO</ span >
219+ < p className = "text-xs text-[var(--sidebar-muted)] mt-2 px-1 " >
220+ Powered by < span className = "font-medium text-[var(--sidebar-foreground)] " > OSO</ span >
232221 </ p >
233222 </ div >
234- </ div >
223+ </ aside >
235224 ) ;
236225}
0 commit comments