Admin Dashboard
A comprehensive admin dashboard with real-time analytics, user management, and system monitoring. Features custom components with minimal shadcn usage.
Folder structure
Installation and setup
To set up the Admin Dashboard, follow these steps:
Add admin-sidebar, dashboard-card, dashboard-header, quick-actions, recent-activity, revenue-chart, system-status, and user-table components to your project.
Run the following command to install the necessary components:
npx mvpblocks add admin-sidebar dashboard-card dashboard-header quick-actions recent-activity revenue-chart system-status users-table
Copy the code from the following files and paste it into your project:
Add admin-sidebar.tsx:
'use client';import { memo } from 'react';import { useTheme } from 'next-themes';import Link from 'next/link';import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarRail,} from '@/components/ui/sidebar';import { LayoutDashboard, Users, BarChart3, FileText, Activity, Database, Shield, Zap, Bell, Settings, Moon, Sun, User,} from 'lucide-react';const menuItems = [ { title: 'Dashboard', icon: LayoutDashboard, href: '#dashboard' }, { title: 'Analytics', icon: BarChart3, href: '#analytics' }, { title: 'Users', icon: Users, href: '#users' }, { title: 'Content', icon: FileText, href: '#content' }, { title: 'Activity', icon: Activity, href: '#activity' }, { title: 'Database', icon: Database, href: '#database' }, { title: 'Security', icon: Shield, href: '#security' }, { title: 'Performance', icon: Zap, href: '#performance' }, { title: 'Notifications', icon: Bell, href: '#notifications' }, { title: 'Settings', icon: Settings, href: '#settings' },];export const AdminSidebar = memo(() => { const { theme, setTheme } = useTheme(); return ( <Sidebar collapsible="icon"> <SidebarHeader> <SidebarMenu> <SidebarMenuItem> <SidebarMenuButton size="lg" asChild> <Link prefetch={false} href="#dashboard"> <div className="bg-primary text-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg"> <LayoutDashboard className="h-5 w-5" /> </div> <div className="grid flex-1 text-left text-sm leading-tight"> <span className="truncate font-semibold">TechCorp</span> <span className="truncate text-xs">Admin Panel</span> </div> </Link> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </SidebarHeader> <SidebarContent> <SidebarGroup> <SidebarGroupLabel>Navigation</SidebarGroupLabel> <SidebarGroupContent> <SidebarMenu> {menuItems.map((item) => { const Icon = item.icon; return ( <SidebarMenuItem key={item.href}> <SidebarMenuButton asChild> <Link prefetch={false} href={item.href}> <Icon /> <span>{item.title}</span> </Link> </SidebarMenuButton> </SidebarMenuItem> ); })} </SidebarMenu> </SidebarGroupContent> </SidebarGroup> </SidebarContent> <SidebarFooter> <SidebarMenu> <SidebarMenuItem> <SidebarMenuButton onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} > {theme === 'dark' ? <Sun /> : <Moon />} <span>{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}</span> </SidebarMenuButton> </SidebarMenuItem> <SidebarMenuItem> <SidebarMenuButton asChild> <Link prefetch={false} href="#profile"> <User /> <span>Admin Profile</span> </Link> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </SidebarFooter> <SidebarRail /> </Sidebar> );});AdminSidebar.displayName = 'AdminSidebar';
Add dashboard-card.tsx:
'use client';import { memo } from 'react';import { motion } from 'framer-motion';import { TrendingUp } from 'lucide-react';interface DashboardCardProps { stat: { title: string; value: string; change: string; changeType: 'positive' | 'negative'; icon: any; color: string; bgColor: string; }; index: number;}export const DashboardCard = memo(({ stat, index }: DashboardCardProps) => { const Icon = stat.icon; return ( <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.1 }} whileHover={{ scale: 1.02, transition: { duration: 0.2 } }} className="group relative cursor-pointer" > <div className="border-border bg-card/40 rounded-xl border p-6 transition-all duration-300 hover:shadow-lg"> <div className="to-primary/5 absolute inset-0 rounded-xl bg-gradient-to-br from-transparent via-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" /> <div className="relative"> <div className="mb-4 flex items-center justify-between"> <div className={`rounded-lg p-3 ${stat.bgColor}`}> <Icon className={`h-6 w-6 ${stat.color}`} /> </div> <div className={`flex items-center gap-1 text-sm font-medium ${ stat.changeType === 'positive' ? 'text-green-500' : 'text-red-500' }`} > <TrendingUp className={`h-4 w-4 ${ stat.changeType === 'negative' ? 'rotate-180' : '' }`} /> <span>{stat.change}</span> </div> </div> <div className="mb-3"> <h3 className="text-foreground mb-1 text-3xl font-bold"> {stat.value} </h3> <p className="text-muted-foreground text-sm font-medium"> {stat.title} </p> </div> <div className="bg-muted h-2 overflow-hidden rounded-full"> <motion.div initial={{ width: 0 }} animate={{ width: `${65 + index * 8}%` }} transition={{ duration: 1, delay: index * 0.1 }} className={`h-full rounded-full ${stat.color.replace('text-', 'bg-')}`} /> </div> </div> </div> </motion.div> );});DashboardCard.displayName = 'DashboardCard';
Add dashboard-header.tsx:
'use client';import { memo } from 'react';import { motion } from 'framer-motion';import { Button } from '@/components/ui/button';import { Input } from '@/components/ui/input';import { SidebarTrigger } from '@/components/ui/sidebar';import { Separator } from '@/components/ui/separator';import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,} from '@/components/ui/dropdown-menu';import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator,} from '@/components/ui/breadcrumb';import { Bell, Search, Filter, Download, RefreshCw, MoreHorizontal,} from 'lucide-react';interface DashboardHeaderProps { searchQuery: string; onSearchChange: (value: string) => void; onRefresh: () => void; onExport: () => void; isRefreshing: boolean;}export const DashboardHeader = memo( ({ searchQuery, onSearchChange, onRefresh, onExport, isRefreshing, }: DashboardHeaderProps) => { return ( <header className="bg-background/95 sticky top-0 z-50 flex h-16 w-full shrink-0 items-center gap-2 border-b backdrop-blur transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12"> <div className="flex items-center gap-2 px-4"> <SidebarTrigger className="-ml-1" /> <Separator orientation="vertical" className="mr-2 h-4" /> <Breadcrumb> <BreadcrumbList> <BreadcrumbItem className="hidden md:block"> <BreadcrumbLink href="#">Home</BreadcrumbLink> </BreadcrumbItem> </BreadcrumbList> </Breadcrumb> </div> <div className="ml-auto flex items-center gap-2 px-4"> <motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} className="flex items-center gap-2" > {/* Search Input - Hide on Mobile */} <div className="relative hidden md:block"> <Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform" /> <Input placeholder="Search..." value={searchQuery} onChange={(e) => onSearchChange(e.target.value)} className="w-64 pl-10" /> </div> {/* Desktop Actions */} <div className="hidden items-center gap-2 md:flex"> <Button variant="outline" size="sm"> <Filter className="mr-2 h-4 w-4" /> Filter </Button> <Button variant="outline" size="sm" onClick={onExport}> <Download className="mr-2 h-4 w-4" /> Export </Button> <Button variant="outline" size="sm" onClick={onRefresh} disabled={isRefreshing} > <RefreshCw className={`mr-2 h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} /> Refresh </Button> </div> {/* Mobile Menu */} <DropdownMenu> <DropdownMenuTrigger asChild className="md:hidden"> <Button variant="outline" size="icon"> <MoreHorizontal className="h-4 w-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-48"> <DropdownMenuItem onClick={() => onSearchChange('')}> <Search className="mr-2 h-4 w-4" /> Search </DropdownMenuItem> <DropdownMenuItem> <Filter className="mr-2 h-4 w-4" /> Filter </DropdownMenuItem> <DropdownMenuItem onClick={onExport}> <Download className="mr-2 h-4 w-4" /> Export </DropdownMenuItem> <DropdownMenuItem onClick={onRefresh}> <RefreshCw className="mr-2 h-4 w-4" /> Refresh </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> <Button variant="outline" size="sm"> <Bell className="h-4 w-4" /> </Button> </motion.div> </div> </header> ); },);DashboardHeader.displayName = 'DashboardHeader';
Add quick-actions.tsx:
'use client';import { memo } from 'react';import { motion } from 'framer-motion';import { Button } from '@/components/ui/button';import { Users, BarChart3, Download, Settings } from 'lucide-react';interface QuickActionsProps { onAddUser: () => void; onExport: () => void;}const actions = [ { icon: Users, label: 'Add New User', color: 'blue', shortcut: 'Ctrl+N', action: 'addUser', }, { icon: BarChart3, label: 'View Analytics', color: 'green', shortcut: 'Ctrl+A', action: 'analytics', }, { icon: Download, label: 'Export Data', color: 'purple', shortcut: 'Ctrl+E', action: 'export', }, { icon: Settings, label: 'System Settings', color: 'orange', shortcut: 'Ctrl+S', action: 'settings', },];export const QuickActions = memo( ({ onAddUser, onExport }: QuickActionsProps) => { const handleAction = (action: string) => { switch (action) { case 'addUser': onAddUser(); break; case 'analytics': console.log('Viewing analytics...'); break; case 'export': onExport(); break; case 'settings': console.log('Opening settings...'); break; } }; return ( <div className="border-border bg-card/40 rounded-xl border p-6"> <h3 className="mb-4 text-xl font-semibold">Quick Actions</h3> <div className="space-y-3"> {actions.map((action, index) => { const Icon = action.icon; return ( <motion.div key={action.label} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} > <Button variant="outline" className={`h-12 w-full justify-start hover:bg-${action.color}-500/10 hover:border-${action.color}-500/50 transition-all duration-200`} onClick={() => handleAction(action.action)} > <Icon className={`mr-3 h-5 w-5 text-${action.color}-500`} /> <span className="font-medium">{action.label}</span> <div className="text-muted-foreground ml-auto text-xs"> {action.shortcut} </div> </Button> </motion.div> ); })} </div> </div> ); },);QuickActions.displayName = 'QuickActions';
Add recent-activity.tsx:
'use client';import { memo } from 'react';import { motion } from 'framer-motion';import { User, Download, Settings, Users } from 'lucide-react';const activities = [ { action: 'User login', user: 'john@example.com', time: '2 min ago', icon: User, color: 'text-blue-500', }, { action: 'Data export', user: 'admin', time: '5 min ago', icon: Download, color: 'text-green-500', }, { action: 'Settings updated', user: 'admin', time: '10 min ago', icon: Settings, color: 'text-orange-500', }, { action: 'New user registered', user: 'sarah@example.com', time: '15 min ago', icon: Users, color: 'text-purple-500', },];export const RecentActivity = memo(() => { return ( <div className="border-border bg-card/40 rounded-xl border p-6"> <h3 className="mb-4 text-xl font-semibold">Recent Activity</h3> <div className="space-y-3"> {activities.map((activity, index) => { const Icon = activity.icon; return ( <motion.div key={index} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.1 }} className="hover:bg-accent/50 flex items-center gap-3 rounded-lg p-2 transition-colors" > <div className={`bg-accent/50 rounded-lg p-2`}> <Icon className={`h-4 w-4 ${activity.color}`} /> </div> <div className="min-w-0 flex-1"> <div className="text-sm font-medium">{activity.action}</div> <div className="text-muted-foreground truncate text-xs"> {activity.user} </div> </div> <div className="text-muted-foreground text-xs"> {activity.time} </div> </motion.div> ); })} </div> </div> );});RecentActivity.displayName = 'RecentActivity';
Add revenue-chart.tsx:
'use client';import { memo } from 'react';import { motion } from 'framer-motion';import { Button } from '@/components/ui/button';import { BarChart3, Calendar } from 'lucide-react';const chartData = [ { month: 'Jan', value: 4000, growth: 12, color: 'bg-blue-500' }, { month: 'Feb', value: 3000, growth: -8, color: 'bg-red-500' }, { month: 'Mar', value: 5000, growth: 25, color: 'bg-green-500' }, { month: 'Apr', value: 4500, growth: 15, color: 'bg-yellow-500' }, { month: 'May', value: 6000, growth: 33, color: 'bg-purple-500' }, { month: 'Jun', value: 5500, growth: 22, color: 'bg-cyan-500' },];export const RevenueChart = memo(() => { return ( <div className="border-border bg-card/40 rounded-xl border p-6"> <div className="mb-6 flex items-center justify-between"> <div> <h3 className="flex items-center gap-2 text-lg font-semibold"> <BarChart3 className="h-5 w-5 text-green-500" /> Revenue Analytics </h3> <p className="text-muted-foreground text-sm"> Monthly revenue performance </p> </div> <Button variant="outline" size="sm"> <Calendar className="mr-2 h-4 w-4" /> Last 6 months </Button> </div> {/* Fixed Chart Area */} <div className="relative mb-4 h-64 rounded-lg p-4"> <div className="flex h-full items-end justify-between gap-3"> {chartData.map((item, index) => ( <div key={item.month} className="group flex flex-1 flex-col items-center" > <motion.div initial={{ height: 0 }} animate={{ height: `${(item.value / 6000) * 180}px` }} transition={{ duration: 1, delay: index * 0.1 }} className={`w-full ${item.color} relative min-h-[20px] cursor-pointer rounded-t-lg transition-opacity hover:opacity-80`} > {/* Tooltip */} <div className="border-border bg-popover absolute -top-16 left-1/2 z-10 -translate-x-1/2 transform rounded-lg border px-3 py-2 text-sm whitespace-nowrap opacity-0 shadow-lg transition-opacity group-hover:opacity-100"> <div className="font-medium"> ${item.value.toLocaleString()} </div> <div className={`text-xs ${item.growth > 0 ? 'text-green-500' : 'text-red-500'}`} > {item.growth > 0 ? '+' : ''} {item.growth}% </div> </div> </motion.div> <div className="text-muted-foreground mt-2 text-center text-xs font-medium"> {item.month} </div> </div> ))} </div> </div> {/* Summary Stats */} <div className="border-border/50 grid grid-cols-3 gap-4 border-t pt-4"> <div className="text-center"> <div className="text-2xl font-bold text-green-500">$27K</div> <div className="text-muted-foreground text-xs">Total Revenue</div> </div> <div className="text-center"> <div className="text-2xl font-bold text-blue-500">+18%</div> <div className="text-muted-foreground text-xs">Growth Rate</div> </div> <div className="text-center"> <div className="text-2xl font-bold text-purple-500">$4.5K</div> <div className="text-muted-foreground text-xs">Average</div> </div> </div> </div> );});RevenueChart.displayName = 'RevenueChart';
Add system-status.tsx:
'use client';import { memo } from 'react';import { motion } from 'framer-motion';import { Shield, Database, Zap, Activity } from 'lucide-react';const statusItems = [ { label: 'Server Status', status: 'Online', color: 'text-green-500', icon: Shield, percentage: 100, }, { label: 'Database', status: 'Healthy', color: 'text-green-500', icon: Database, percentage: 95, }, { label: 'API Response', status: 'Fast', color: 'text-green-500', icon: Zap, percentage: 98, }, { label: 'Storage', status: '85% Used', color: 'text-yellow-500', icon: Activity, percentage: 85, },];export const SystemStatus = memo(() => { return ( <div className="border-border bg-card/40 rounded-xl border p-6"> <h3 className="mb-4 text-xl font-semibold">System Status</h3> <div className="space-y-4"> {statusItems.map((item, index) => { const Icon = item.icon; return ( <motion.div key={item.label} initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: index * 0.1 }} className="hover:bg-accent/50 flex cursor-pointer items-center justify-between rounded-lg p-3 transition-colors" > <div className="flex items-center gap-3"> <Icon className={`h-4 w-4 ${item.color}`} /> <span className="text-sm font-medium">{item.label}</span> </div> <div className="flex items-center gap-3"> <div className="bg-muted h-2 w-16 overflow-hidden rounded-full"> <motion.div initial={{ width: 0 }} animate={{ width: `${item.percentage}%` }} transition={{ duration: 1, delay: index * 0.1 }} className={`h-full rounded-full ${item.color.replace('text-', 'bg-')}`} /> </div> <span className={`text-sm font-medium ${item.color} min-w-[60px] text-right`} > {item.status} </span> </div> </motion.div> ); })} </div> </div> );});SystemStatus.displayName = 'SystemStatus';
Add user-table.tsx:
'use client';import { memo } from 'react';import { motion } from 'framer-motion';import { Button } from '@/components/ui/button';import { TrendingUp, Plus, Calendar, Mail, MapPin, MoreHorizontal,} from 'lucide-react';const users = [ { id: 1, name: 'Alex Johnson', email: 'alex@example.com', avatar: 'https://i.pravatar.cc', role: 'Admin', status: 'active', joinDate: '2024-01-15', location: 'New York, US', }, { id: 2, name: 'Sarah Chen', email: 'sarah@example.com', avatar: 'https://i.pravatar.cc', role: 'User', status: 'active', joinDate: '2024-02-20', location: 'San Francisco, US', }, { id: 3, name: 'Michael Brown', email: 'michael@example.com', avatar: 'https://i.pravatar.cc', role: 'Moderator', status: 'inactive', joinDate: '2024-01-08', location: 'London, UK', },];interface UsersTableProps { onAddUser: () => void;}export const UsersTable = memo(({ onAddUser }: UsersTableProps) => { return ( <div className="border-border bg-card/40 rounded-xl border p-3 sm:p-6"> <div className="mb-6 flex flex-col justify-between gap-4 sm:flex-row sm:items-center"> <div> <h3 className="text-lg font-semibold sm:text-xl">Recent Users</h3> <p className="text-muted-foreground text-sm"> Latest user registrations and activity </p> </div> <div className="flex items-center gap-2"> <div className="flex items-center gap-1 text-sm text-green-500"> <TrendingUp className="h-4 w-4" /> <span>+12%</span> </div> <Button variant="outline" size="sm" onClick={onAddUser}> <Plus className="mr-2 h-4 w-4" /> <span className="hidden sm:inline">Add User</span> <span className="sm:hidden">Add</span> </Button> </div> </div> <div className="space-y-2"> {users.map((user, index) => ( <motion.div key={user.id} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.05 }} className="group hover:bg-accent/50 flex flex-col items-start gap-4 rounded-lg p-4 transition-colors sm:flex-row sm:items-center" > <div className="flex w-full items-center gap-4 sm:w-auto"> <div className="relative"> <img src={user.avatar} alt={user.name} width={40} height={40} className="rounded-full" /> <div className={`border-background absolute -right-1 -bottom-1 h-3 w-3 rounded-full border-2 ${ user.status === 'active' ? 'bg-green-500' : 'bg-red-500' }`} /> </div> <div className="min-w-0 flex-1"> <div className="flex flex-wrap items-center gap-2"> <h4 className="truncate text-sm font-medium">{user.name}</h4> <span className={`rounded-full px-2 py-1 text-xs font-medium ${ user.role === 'Admin' ? 'bg-purple-500/10 text-purple-500' : user.role === 'Moderator' ? 'bg-blue-500/10 text-blue-500' : 'bg-gray-500/10 text-gray-500' }`} > {user.role} </span> </div> <div className="text-muted-foreground mt-1 flex flex-col gap-2 text-xs sm:flex-row sm:items-center sm:gap-4"> <div className="flex items-center gap-1"> <Mail className="h-3 w-3" /> <span className="truncate">{user.email}</span> </div> <div className="flex items-center gap-1"> <MapPin className="h-3 w-3" /> <span>{user.location}</span> </div> </div> </div> </div> <div className="ml-auto flex items-center gap-3"> <div className="text-muted-foreground flex items-center gap-1 text-xs"> <Calendar className="h-3 w-3" /> <span>{new Date(user.joinDate).toLocaleDateString()}</span> </div> <Button variant="ghost" size="sm" className="ml-auto"> <MoreHorizontal className="h-4 w-4" /> </Button> </div> </motion.div> ))} </div> </div> );});UsersTable.displayName = 'UsersTable';