Skip to content

Commit f4a85da

Browse files
authored
Merge pull request #6 from opensource-observer/update-sidebar
polish: sidebar icons and insights ordering
2 parents 8d9b6a7 + 34668bd commit f4a85da

2 files changed

Lines changed: 137 additions & 140 deletions

File tree

app/app/globals.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
--foreground-rgb: 0, 0, 0;
55
--background-start-rgb: 255, 255, 255;
66
--background-end-rgb: 255, 255, 255;
7+
8+
/* Sidebar tokens (matching OSO kuma frontend-ui) */
9+
--sidebar-bg: hsl(0 0% 98%);
10+
--sidebar-foreground: hsl(240 5.3% 26.1%);
11+
--sidebar-primary: hsl(240 5.9% 10%);
12+
--sidebar-accent: hsl(240 4.8% 95.9%);
13+
--sidebar-border: hsl(220 13% 91%);
14+
--sidebar-muted: hsl(240 3.8% 46.1%);
715
}
816

917
* {

app/components/Sidebar.tsx

Lines changed: 129 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,40 @@
33
import Link from 'next/link';
44
import { usePathname } from 'next/navigation';
55
import { 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

722
interface NavItem {
823
label: string;
924
href: string;
25+
icon?: LucideIcon;
1026
children?: NavItem[];
1127
}
1228

1329
const 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

171175
export 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

Comments
 (0)