Skip to content

Commit c2b8b99

Browse files
Boshenclaude
andcommitted
feat: modernize dashboard with sidebar navigation and dark mode
Transform the dashboard into a modern, professional layout with: - Sidebar navigation with collapsible menu and mobile support - Dark mode with system preference detection - Unified dashboard homepage with metrics overview - Reusable component library (Card, Button, Badge, Skeleton) - Enhanced page layouts with quick stats cards - Responsive design with mobile-friendly navigation - Smooth animations and transitions - Professional gradient accents and color scheme 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 6cf5bcc commit c2b8b99

File tree

14 files changed

+1064
-84
lines changed

14 files changed

+1064
-84
lines changed

apps/dashboard/src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Route, Routes } from 'react-router-dom';
22
import Layout from './components/Layout';
3+
import DashboardPage from './pages/DashboardPage';
34
import MinificationBenchmarksPage from './pages/MinificationBenchmarksPage';
45
import NpmDownloadsPage from './pages/NpmDownloadsPage';
56
import RolldownStatsPage from './pages/RolldownStatsPage';
@@ -8,7 +9,8 @@ function App() {
89
return (
910
<Routes>
1011
<Route path='/' element={<Layout />}>
11-
<Route index element={<RolldownStatsPage />} />
12+
<Route index element={<DashboardPage />} />
13+
<Route path='rolldown-stats' element={<RolldownStatsPage />} />
1214
<Route path='minification' element={<MinificationBenchmarksPage />} />
1315
<Route path='npm-downloads' element={<NpmDownloadsPage />} />
1416
</Route>

apps/dashboard/src/components/Layout.tsx

Lines changed: 16 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,24 @@
1-
import { BarChart3, Download, Package, Zap } from 'lucide-react';
2-
import { Link, Outlet, useLocation } from 'react-router-dom';
1+
import { Outlet } from 'react-router-dom';
2+
import { Sidebar } from './layout/Sidebar';
3+
import { AppBar } from './layout/AppBar';
34

45
function Layout() {
5-
const location = useLocation();
6-
76
return (
8-
<>
9-
{/* Rolldown-Vite Dashboard Banner */}
10-
<div className='bg-slate-100 border-b border-slate-300 px-8 py-3 sticky top-0 z-50'>
11-
<div className='max-w-6xl mx-auto flex items-center gap-2.5 text-sm font-semibold text-slate-600 tracking-tight'>
12-
<BarChart3 size={16} />
13-
<span>Rolldown-Vite Dashboard</span>
7+
<div className="min-h-screen bg-slate-50 dark:bg-slate-950">
8+
<Sidebar />
9+
10+
{/* Main Content Area */}
11+
<div className="lg:pl-64 transition-all duration-300">
12+
<div className="flex flex-col h-screen">
13+
<AppBar />
14+
15+
{/* Page Content */}
16+
<main className="flex-1 overflow-auto">
17+
<Outlet />
18+
</main>
1419
</div>
1520
</div>
16-
17-
{/* Page Navigation */}
18-
<nav className='bg-slate-50 border-b border-slate-200 px-8 py-4 flex gap-2 max-w-6xl mx-auto'>
19-
<Link
20-
to='/'
21-
className={`flex items-center gap-2 px-5 py-3 border rounded-lg cursor-pointer font-medium text-sm transition-all duration-200 tracking-tight text-decoration-none min-w-36 justify-center ${
22-
location.pathname === '/'
23-
? 'bg-slate-600 border-slate-600 text-white hover:bg-slate-800 hover:border-slate-800'
24-
: 'bg-white border-slate-300 text-slate-600 hover:bg-slate-100 hover:border-slate-400 hover:text-slate-800'
25-
}`}
26-
>
27-
<Package size={20} />
28-
Rolldown Stats
29-
</Link>
30-
<Link
31-
to='/minification'
32-
className={`flex items-center gap-2 px-5 py-3 border rounded-lg cursor-pointer font-medium text-sm transition-all duration-200 tracking-tight text-decoration-none min-w-36 justify-center ${
33-
location.pathname === '/minification'
34-
? 'bg-slate-600 border-slate-600 text-white hover:bg-slate-800 hover:border-slate-800'
35-
: 'bg-white border-slate-300 text-slate-600 hover:bg-slate-100 hover:border-slate-400 hover:text-slate-800'
36-
}`}
37-
>
38-
<Zap size={20} />
39-
Minification Benchmarks
40-
</Link>
41-
<Link
42-
to='/npm-downloads'
43-
className={`flex items-center gap-2 px-5 py-3 border rounded-lg cursor-pointer font-medium text-sm transition-all duration-200 tracking-tight text-decoration-none min-w-36 justify-center ${
44-
location.pathname === '/npm-downloads'
45-
? 'bg-slate-600 border-slate-600 text-white hover:bg-slate-800 hover:border-slate-800'
46-
: 'bg-white border-slate-300 text-slate-600 hover:bg-slate-100 hover:border-slate-400 hover:text-slate-800'
47-
}`}
48-
>
49-
<Download size={20} />
50-
NPM Downloads
51-
</Link>
52-
</nav>
53-
54-
{/* Render the current route's component */}
55-
<Outlet />
56-
</>
21+
</div>
5722
);
5823
}
5924

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Bell, Search, User } from 'lucide-react';
2+
import { Button } from '../ui/Button';
3+
4+
interface AppBarProps {
5+
title?: string;
6+
subtitle?: string;
7+
}
8+
9+
export function AppBar({ title, subtitle }: AppBarProps) {
10+
return (
11+
<header className="h-16 bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-700 px-6">
12+
<div className="h-full flex items-center justify-between">
13+
{/* Left side - Page title */}
14+
<div>
15+
{title && (
16+
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
17+
{title}
18+
</h2>
19+
)}
20+
{subtitle && (
21+
<p className="text-sm text-slate-500 dark:text-slate-400">
22+
{subtitle}
23+
</p>
24+
)}
25+
</div>
26+
27+
{/* Right side - Actions */}
28+
<div className="flex items-center gap-3">
29+
{/* Search */}
30+
<div className="hidden md:flex items-center">
31+
<div className="relative">
32+
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
33+
<input
34+
type="text"
35+
placeholder="Search..."
36+
className="pl-10 pr-4 py-2 bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
37+
/>
38+
</div>
39+
</div>
40+
41+
{/* Notifications */}
42+
<Button
43+
variant="ghost"
44+
size="sm"
45+
className="relative"
46+
icon={
47+
<>
48+
<Bell size={18} />
49+
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full" />
50+
</>
51+
}
52+
/>
53+
54+
{/* User Menu */}
55+
<div className="flex items-center gap-3 pl-3 border-l border-slate-200 dark:border-slate-700">
56+
<div className="hidden sm:block text-right">
57+
<p className="text-sm font-medium text-slate-900 dark:text-white">Admin User</p>
58+
<p className="text-xs text-slate-500 dark:text-slate-400">[email protected]</p>
59+
</div>
60+
<button className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center">
61+
<User size={18} className="text-white" />
62+
</button>
63+
</div>
64+
</div>
65+
</div>
66+
</header>
67+
);
68+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { ReactNode } from 'react';
2+
3+
interface PageContainerProps {
4+
children: ReactNode;
5+
className?: string;
6+
}
7+
8+
export function PageContainer({ children, className = '' }: PageContainerProps) {
9+
return (
10+
<div className={`flex-1 p-6 ${className}`}>
11+
<div className="max-w-7xl mx-auto">
12+
{children}
13+
</div>
14+
</div>
15+
);
16+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { BarChart3, ChevronLeft, Download, Home, Menu, Moon, Package, Sun, X, Zap } from 'lucide-react';
2+
import { Link, useLocation } from 'react-router-dom';
3+
import { useState, useEffect } from 'react';
4+
import { Button } from '../ui/Button';
5+
6+
interface NavItem {
7+
path: string;
8+
label: string;
9+
icon: React.ReactNode;
10+
badge?: string;
11+
}
12+
13+
const navItems: NavItem[] = [
14+
{
15+
path: '/',
16+
label: 'Dashboard',
17+
icon: <Home size={20} />
18+
},
19+
{
20+
path: '/rolldown-stats',
21+
label: 'Rolldown Stats',
22+
icon: <Package size={20} />
23+
},
24+
{
25+
path: '/minification',
26+
label: 'Minification',
27+
icon: <Zap size={20} />,
28+
badge: 'Benchmarks'
29+
},
30+
{
31+
path: '/npm-downloads',
32+
label: 'NPM Downloads',
33+
icon: <Download size={20} />
34+
}
35+
];
36+
37+
export function Sidebar() {
38+
const location = useLocation();
39+
const [collapsed, setCollapsed] = useState(false);
40+
const [mobileOpen, setMobileOpen] = useState(false);
41+
const [darkMode, setDarkMode] = useState(false);
42+
43+
useEffect(() => {
44+
const isDark = localStorage.getItem('theme') === 'dark' ||
45+
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
46+
setDarkMode(isDark);
47+
if (isDark) {
48+
document.documentElement.classList.add('dark');
49+
}
50+
}, []);
51+
52+
const toggleDarkMode = () => {
53+
const newDarkMode = !darkMode;
54+
setDarkMode(newDarkMode);
55+
if (newDarkMode) {
56+
document.documentElement.classList.add('dark');
57+
localStorage.setItem('theme', 'dark');
58+
} else {
59+
document.documentElement.classList.remove('dark');
60+
localStorage.setItem('theme', 'light');
61+
}
62+
};
63+
64+
const isActive = (path: string) => {
65+
if (path === '/' && location.pathname === '/') return true;
66+
if (path !== '/' && location.pathname.startsWith(path)) return true;
67+
return false;
68+
};
69+
70+
return (
71+
<>
72+
{/* Mobile Menu Button */}
73+
<button
74+
onClick={() => setMobileOpen(!mobileOpen)}
75+
className="lg:hidden fixed top-4 left-4 z-50 p-2 rounded-lg bg-white dark:bg-slate-800 shadow-md border border-slate-200 dark:border-slate-700"
76+
>
77+
{mobileOpen ? <X size={24} /> : <Menu size={24} />}
78+
</button>
79+
80+
{/* Mobile Overlay */}
81+
{mobileOpen && (
82+
<div
83+
className="lg:hidden fixed inset-0 bg-black/50 z-40"
84+
onClick={() => setMobileOpen(false)}
85+
/>
86+
)}
87+
88+
{/* Sidebar */}
89+
<aside
90+
className={`
91+
fixed top-0 left-0 h-full bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-700
92+
transition-all duration-300 z-40
93+
${collapsed ? 'w-20' : 'w-64'}
94+
${mobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
95+
`}
96+
>
97+
{/* Logo Header */}
98+
<div className="h-16 flex items-center justify-between px-4 border-b border-slate-200 dark:border-slate-700">
99+
<div className="flex items-center gap-3">
100+
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
101+
<BarChart3 size={24} className="text-white" />
102+
</div>
103+
{!collapsed && (
104+
<div>
105+
<h1 className="font-bold text-slate-900 dark:text-white">Vibe</h1>
106+
<p className="text-xs text-slate-500 dark:text-slate-400">Dashboard</p>
107+
</div>
108+
)}
109+
</div>
110+
<button
111+
onClick={() => setCollapsed(!collapsed)}
112+
className="hidden lg:flex p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
113+
>
114+
<ChevronLeft
115+
size={18}
116+
className={`text-slate-600 dark:text-slate-400 transition-transform ${collapsed ? 'rotate-180' : ''}`}
117+
/>
118+
</button>
119+
</div>
120+
121+
{/* Navigation */}
122+
<nav className="flex-1 px-3 py-4">
123+
<ul className="space-y-1">
124+
{navItems.map((item) => (
125+
<li key={item.path}>
126+
<Link
127+
to={item.path}
128+
onClick={() => setMobileOpen(false)}
129+
className={`
130+
flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200
131+
${isActive(item.path)
132+
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-medium'
133+
: 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800'
134+
}
135+
${collapsed ? 'justify-center' : ''}
136+
`}
137+
title={collapsed ? item.label : undefined}
138+
>
139+
<span className="flex-shrink-0">{item.icon}</span>
140+
{!collapsed && (
141+
<>
142+
<span className="flex-1">{item.label}</span>
143+
{item.badge && (
144+
<span className="px-2 py-0.5 text-xs bg-slate-200 dark:bg-slate-700 rounded-full">
145+
{item.badge}
146+
</span>
147+
)}
148+
</>
149+
)}
150+
</Link>
151+
</li>
152+
))}
153+
</ul>
154+
</nav>
155+
156+
{/* Bottom Actions */}
157+
<div className="p-3 border-t border-slate-200 dark:border-slate-700">
158+
<Button
159+
variant="ghost"
160+
size="sm"
161+
onClick={toggleDarkMode}
162+
className={`w-full ${collapsed ? 'justify-center' : ''}`}
163+
icon={darkMode ? <Sun size={18} /> : <Moon size={18} />}
164+
>
165+
{!collapsed && (darkMode ? 'Light Mode' : 'Dark Mode')}
166+
</Button>
167+
</div>
168+
</aside>
169+
</>
170+
);
171+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { ReactNode } from 'react';
2+
3+
interface BadgeProps {
4+
children: ReactNode;
5+
variant?: 'default' | 'success' | 'warning' | 'danger' | 'info';
6+
size?: 'sm' | 'md';
7+
className?: string;
8+
}
9+
10+
export function Badge({
11+
children,
12+
variant = 'default',
13+
size = 'md',
14+
className = ''
15+
}: BadgeProps) {
16+
const variantClasses = {
17+
default: 'bg-slate-100 text-slate-700 dark:bg-slate-700 dark:text-slate-300',
18+
success: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
19+
warning: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400',
20+
danger: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
21+
info: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400'
22+
};
23+
24+
const sizeClasses = {
25+
sm: 'px-2 py-0.5 text-xs',
26+
md: 'px-2.5 py-1 text-sm'
27+
};
28+
29+
return (
30+
<span
31+
className={`inline-flex items-center font-medium rounded-full ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
32+
>
33+
{children}
34+
</span>
35+
);
36+
}

0 commit comments

Comments
 (0)