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-tableCopy 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';
