Admin Dashboard Components

Build powerful admin dashboards with analytics charts, user management, subscription tracking, and business intelligence components.

Admin Dashboard Overview

The admin dashboard provides comprehensive insights into your business with real-time analytics, user management tools, and subscription monitoring.

📊 Analytics Features

  • • Revenue tracking (MRR/ARR)
  • • User growth metrics
  • • Churn analysis
  • • Cohort retention charts
  • • Subscription analytics
  • • Performance indicators

👥 Management Tools

  • • User search and filtering
  • • Subscription management
  • • Account status controls
  • • Bulk operations
  • • Support ticket integration
  • • Audit logs

Analytics Components

Revenue Analytics

Track monthly recurring revenue and growth trends:

src/components/admin/RevenueChart.tsx
1"use client";
2
3import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
4import { Badge } from "~/components/ui/badge";
5import { TrendingUp, TrendingDown, DollarSign } from "lucide-react";
6import { useMRRAnalytics } from "~/hooks/useAnalytics";
7
8export function RevenueChart() {
9 const { data: mrrData, isLoading, error } = useMRRAnalytics();
10
11 if (isLoading) {
12 return (
13 <div className="grid gap-4 md:grid-cols-3">
14 {[...Array(3)].map((_, i) => (
15 <Card key={i}>
16 <CardHeader className="animate-pulse">
17 <div className="h-4 bg-muted rounded w-3/4"></div>
18 </CardHeader>
19 <CardContent className="animate-pulse">
20 <div className="h-8 bg-muted rounded w-1/2 mb-2"></div>
21 <div className="h-3 bg-muted rounded w-2/3"></div>
22 </CardContent>
23 </Card>
24 ))}
25 </div>
26 );
27 }
28
29 if (error || !mrrData) {
30 return (
31 <Card>
32 <CardContent className="p-6">
33 <p className="text-sm text-muted-foreground">
34 Failed to load revenue data
35 </p>
36 </CardContent>
37 </Card>
38 );
39 }
40
41 const currentMonth = mrrData[mrrData.length - 1];
42 const previousMonth = mrrData[mrrData.length - 2];
43
44 const mrrGrowth = previousMonth
45 ? ((currentMonth.totalMRR - previousMonth.totalMRR) / previousMonth.totalMRR) * 100
46 : 0;
47
48 return (
49 <div className="grid gap-4 md:grid-cols-3">
50 {/* Total MRR */}
51 <Card>
52 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
53 <CardTitle className="text-sm font-medium">Monthly Recurring Revenue</CardTitle>
54 <DollarSign className="h-4 w-4 text-muted-foreground" />
55 </CardHeader>
56 <CardContent>
57 <div className="text-2xl font-bold">
58 ${currentMonth.totalMRR.toLocaleString()}
59 </div>
60 <div className="flex items-center text-xs text-muted-foreground">
61 {mrrGrowth > 0 ? (
62 <>
63 <TrendingUp className="h-3 w-3 mr-1 text-green-500" />
64 <span className="text-green-500">+{mrrGrowth.toFixed(1)}%</span>
65 </>
66 ) : (
67 <>
68 <TrendingDown className="h-3 w-3 mr-1 text-red-500" />
69 <span className="text-red-500">{mrrGrowth.toFixed(1)}%</span>
70 </>
71 )}
72 <span className="ml-1">from last month</span>
73 </div>
74 </CardContent>
75 </Card>
76
77 {/* New MRR */}
78 <Card>
79 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
80 <CardTitle className="text-sm font-medium">New MRR</CardTitle>
81 <Badge variant="secondary">+</Badge>
82 </CardHeader>
83 <CardContent>
84 <div className="text-2xl font-bold text-green-600">
85 +${currentMonth.newMRR.toLocaleString()}
86 </div>
87 <p className="text-xs text-muted-foreground">
88 From new subscriptions
89 </p>
90 </CardContent>
91 </Card>
92
93 {/* Churn MRR */}
94 <Card>
95 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
96 <CardTitle className="text-sm font-medium">Churned MRR</CardTitle>
97 <Badge variant="destructive">-</Badge>
98 </CardHeader>
99 <CardContent>
100 <div className="text-2xl font-bold text-red-600">
101 -${currentMonth.churnMRR.toLocaleString()}
102 </div>
103 <p className="text-xs text-muted-foreground">
104 From cancellations
105 </p>
106 </CardContent>
107 </Card>
108 </div>
109 );
110}
111
112// MRR Trend Chart Component
113export function MRRTrendChart() {
114 const { data: mrrData } = useMRRAnalytics();
115
116 if (!mrrData) return null;
117
118 return (
119 <Card>
120 <CardHeader>
121 <CardTitle>MRR Trend (Last 12 Months)</CardTitle>
122 </CardHeader>
123 <CardContent>
124 <div className="h-80">
125 {/* Integration with your preferred charting library */}
126 <div className="flex h-full items-end justify-between gap-2">
127 {mrrData.map((month, index) => (
128 <div
129 key={month.month}
130 className="flex flex-col items-center"
131 >
132 <div
133 className="bg-primary w-8 rounded-t"
134 style={{
135 height: `${(month.totalMRR / Math.max(...mrrData.map(m => m.totalMRR))) * 200}px`,
136 minHeight: "4px"
137 }}
138 ></div>
139 <span className="text-xs mt-2 text-muted-foreground">
140 {new Date(month.month).toLocaleDateString('en', { month: 'short' })}
141 </span>
142 </div>
143 ))}
144 </div>
145 </div>
146 </CardContent>
147 </Card>
148 );
149}
User Growth Analytics

Monitor user acquisition and engagement metrics:

User growth analytics
1"use client";
2
3import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
4import { Users, UserPlus, UserMinus, Activity } from "lucide-react";
5import { useGrowthMetrics } from "~/hooks/useAnalytics";
6
7export function UserGrowthChart() {
8 const { data: growth, isLoading } = useGrowthMetrics();
9
10 if (isLoading || !growth) {
11 return <div>Loading growth metrics...</div>;
12 }
13
14 return (
15 <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
16 {/* Total Users */}
17 <Card>
18 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
19 <CardTitle className="text-sm font-medium">Total Users</CardTitle>
20 <Users className="h-4 w-4 text-muted-foreground" />
21 </CardHeader>
22 <CardContent>
23 <div className="text-2xl font-bold">
24 {growth.totalUsers.toLocaleString()}
25 </div>
26 <p className="text-xs text-muted-foreground">
27 +{growth.customerGrowthRate}% this month
28 </p>
29 </CardContent>
30 </Card>
31
32 {/* Active Subscriptions */}
33 <Card>
34 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
35 <CardTitle className="text-sm font-medium">Paid Users</CardTitle>
36 <UserPlus className="h-4 w-4 text-muted-foreground" />
37 </CardHeader>
38 <CardContent>
39 <div className="text-2xl font-bold">
40 {Math.round(growth.totalMRR / growth.averageRevenuePerUser).toLocaleString()}
41 </div>
42 <p className="text-xs text-muted-foreground">
43 {((growth.totalMRR / growth.averageRevenuePerUser / growth.totalUsers) * 100).toFixed(1)}% conversion rate
44 </p>
45 </CardContent>
46 </Card>
47
48 {/* ARPU */}
49 <Card>
50 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
51 <CardTitle className="text-sm font-medium">ARPU</CardTitle>
52 <Activity className="h-4 w-4 text-muted-foreground" />
53 </CardHeader>
54 <CardContent>
55 <div className="text-2xl font-bold">
56 ${growth.averageRevenuePerUser.toFixed(2)}
57 </div>
58 <p className="text-xs text-muted-foreground">
59 Average revenue per user
60 </p>
61 </CardContent>
62 </Card>
63
64 {/* LTV */}
65 <Card>
66 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
67 <CardTitle className="text-sm font-medium">Customer LTV</CardTitle>
68 <UserMinus className="h-4 w-4 text-muted-foreground" />
69 </CardHeader>
70 <CardContent>
71 <div className="text-2xl font-bold">
72 ${growth.lifetimeValue.toFixed(2)}
73 </div>
74 <p className="text-xs text-muted-foreground">
75 Lifetime value
76 </p>
77 </CardContent>
78 </Card>
79 </div>
80 );
81}

User Management

User Management Table

Comprehensive user management with search, filtering, and actions:

src/components/admin/UserManagementTable.tsx
1"use client";
2
3import { useState, useEffect } from "react";
4import { Button } from "~/components/ui/button";
5import { Input } from "~/components/ui/input";
6import { Badge } from "~/components/ui/badge";
7import {
8 Table,
9 TableBody,
10 TableCell,
11 TableHead,
12 TableHeader,
13 TableRow,
14} from "~/components/ui/table";
15import {
16 DropdownMenu,
17 DropdownMenuContent,
18 DropdownMenuItem,
19 DropdownMenuLabel,
20 DropdownMenuSeparator,
21 DropdownMenuTrigger,
22} from "~/components/ui/dropdown-menu";
23import { MoreHorizontal, Search, Filter } from "lucide-react";
24
25interface User {
26 id: string;
27 name?: string;
28 email: string;
29 createdAt: Date;
30 subscription?: {
31 type: string;
32 status: string;
33 stripeCurrentPeriodEnd?: Date;
34 };
35}
36
37interface UserManagementTableProps {
38 users: User[];
39 onUserAction: (userId: string, action: string) => void;
40}
41
42export function UserManagementTable({ users, onUserAction }: UserManagementTableProps) {
43 const [searchTerm, setSearchTerm] = useState("");
44 const [statusFilter, setStatusFilter] = useState("all");
45 const [filteredUsers, setFilteredUsers] = useState(users);
46
47 useEffect(() => {
48 let filtered = users;
49
50 // Search filter
51 if (searchTerm) {
52 filtered = filtered.filter(user =>
53 user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
54 user.name?.toLowerCase().includes(searchTerm.toLowerCase())
55 );
56 }
57
58 // Status filter
59 if (statusFilter !== "all") {
60 filtered = filtered.filter(user => {
61 if (statusFilter === "free") return !user.subscription || user.subscription.type === "FREE";
62 if (statusFilter === "paid") return user.subscription && user.subscription.type !== "FREE";
63 if (statusFilter === "active") return user.subscription?.status === "active";
64 if (statusFilter === "canceled") return user.subscription?.status === "canceled";
65 return true;
66 });
67 }
68
69 setFilteredUsers(filtered);
70 }, [users, searchTerm, statusFilter]);
71
72 const getSubscriptionBadge = (user: User) => {
73 if (!user.subscription || user.subscription.type === "FREE") {
74 return <Badge variant="secondary">Free</Badge>;
75 }
76
77 const isActive = user.subscription.status === "active";
78 return (
79 <Badge variant={isActive ? "default" : "destructive"}>
80 {user.subscription.type} - {user.subscription.status}
81 </Badge>
82 );
83 };
84
85 return (
86 <div className="space-y-4">
87 {/* Filters */}
88 <div className="flex flex-col sm:flex-row gap-4">
89 <div className="relative flex-1">
90 <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
91 <Input
92 placeholder="Search users by email or name..."
93 value={searchTerm}
94 onChange={(e) => setSearchTerm(e.target.value)}
95 className="pl-10"
96 />
97 </div>
98
99 <DropdownMenu>
100 <DropdownMenuTrigger asChild>
101 <Button variant="outline" className="flex items-center gap-2">
102 <Filter className="h-4 w-4" />
103 Filter: {statusFilter === "all" ? "All" : statusFilter}
104 </Button>
105 </DropdownMenuTrigger>
106 <DropdownMenuContent>
107 <DropdownMenuItem onClick={() => setStatusFilter("all")}>
108 All Users
109 </DropdownMenuItem>
110 <DropdownMenuItem onClick={() => setStatusFilter("free")}>
111 Free Users
112 </DropdownMenuItem>
113 <DropdownMenuItem onClick={() => setStatusFilter("paid")}>
114 Paid Users
115 </DropdownMenuItem>
116 <DropdownMenuItem onClick={() => setStatusFilter("active")}>
117 Active Subscriptions
118 </DropdownMenuItem>
119 <DropdownMenuItem onClick={() => setStatusFilter("canceled")}>
120 Canceled Subscriptions
121 </DropdownMenuItem>
122 </DropdownMenuContent>
123 </DropdownMenu>
124 </div>
125
126 {/* Results count */}
127 <div className="text-sm text-muted-foreground">
128 Showing {filteredUsers.length} of {users.length} users
129 </div>
130
131 {/* Users Table */}
132 <div className="border rounded-lg">
133 <Table>
134 <TableHeader>
135 <TableRow>
136 <TableHead>User</TableHead>
137 <TableHead>Subscription</TableHead>
138 <TableHead>Joined</TableHead>
139 <TableHead>Next Billing</TableHead>
140 <TableHead className="w-[70px]">Actions</TableHead>
141 </TableRow>
142 </TableHeader>
143 <TableBody>
144 {filteredUsers.map((user) => (
145 <TableRow key={user.id}>
146 <TableCell>
147 <div>
148 <div className="font-medium">
149 {user.name || "Anonymous User"}
150 </div>
151 <div className="text-sm text-muted-foreground">
152 {user.email}
153 </div>
154 </div>
155 </TableCell>
156 <TableCell>
157 {getSubscriptionBadge(user)}
158 </TableCell>
159 <TableCell>
160 {new Date(user.createdAt).toLocaleDateString()}
161 </TableCell>
162 <TableCell>
163 {user.subscription?.stripeCurrentPeriodEnd
164 ? new Date(user.subscription.stripeCurrentPeriodEnd).toLocaleDateString()
165 : "-"
166 }
167 </TableCell>
168 <TableCell>
169 <DropdownMenu>
170 <DropdownMenuTrigger asChild>
171 <Button variant="ghost" className="h-8 w-8 p-0">
172 <MoreHorizontal className="h-4 w-4" />
173 </Button>
174 </DropdownMenuTrigger>
175 <DropdownMenuContent align="end">
176 <DropdownMenuLabel>Actions</DropdownMenuLabel>
177 <DropdownMenuItem
178 onClick={() => onUserAction(user.id, "view")}
179 >
180 View Details
181 </DropdownMenuItem>
182 <DropdownMenuItem
183 onClick={() => onUserAction(user.id, "impersonate")}
184 >
185 Impersonate User
186 </DropdownMenuItem>
187 <DropdownMenuSeparator />
188 <DropdownMenuItem
189 onClick={() => onUserAction(user.id, "suspend")}
190 className="text-red-600"
191 >
192 Suspend Account
193 </DropdownMenuItem>
194 </DropdownMenuContent>
195 </DropdownMenu>
196 </TableCell>
197 </TableRow>
198 ))}
199 </TableBody>
200 </Table>
201 </div>
202
203 {filteredUsers.length === 0 && (
204 <div className="text-center py-8">
205 <p className="text-muted-foreground">No users found matching your criteria.</p>
206 </div>
207 )}
208 </div>
209 );
210}
Admin Actions

Server actions for admin user management:

Admin user management actions
1"use server";
2
3import { auth } from "~/auth";
4import { db } from "~/server/db";
5import { logger } from "~/lib/logger";
6import { revalidatePath } from "next/cache";
7
8async function requireAdmin() {
9 const session = await auth();
10
11 if (!session?.user) {
12 throw new Error("Not authenticated");
13 }
14
15 // Check if user has admin role
16 const user = await db.user.findUnique({
17 where: { id: session.user.id },
18 select: { role: true },
19 });
20
21 if (user?.role !== "admin") {
22 throw new Error("Insufficient permissions");
23 }
24
25 return session.user;
26}
27
28export async function getUsersForAdmin(page = 1, limit = 50, filter?: string) {
29 await requireAdmin();
30
31 const skip = (page - 1) * limit;
32
33 const where = filter ? {
34 OR: [
35 { email: { contains: filter, mode: "insensitive" } },
36 { name: { contains: filter, mode: "insensitive" } },
37 ],
38 } : {};
39
40 const [users, total] = await Promise.all([
41 db.user.findMany({
42 where,
43 include: {
44 subscription: {
45 select: {
46 type: true,
47 status: true,
48 stripeCurrentPeriodEnd: true,
49 },
50 },
51 },
52 orderBy: { createdAt: "desc" },
53 skip,
54 take: limit,
55 }),
56 db.user.count({ where }),
57 ]);
58
59 return {
60 users,
61 pagination: {
62 page,
63 limit,
64 total,
65 pages: Math.ceil(total / limit),
66 },
67 };
68}
69
70export async function suspendUser(userId: string, reason?: string) {
71 const admin = await requireAdmin();
72
73 try {
74 // Update user status
75 await db.user.update({
76 where: { id: userId },
77 data: {
78 // You might want to add a 'status' field to track suspended users
79 // status: "suspended"
80 },
81 });
82
83 // Cancel active subscription if any
84 const subscription = await db.subscription.findUnique({
85 where: { userId },
86 });
87
88 if (subscription?.stripeSubscriptionId && subscription.status === "active") {
89 // Cancel in Stripe
90 await stripe.subscriptions.update(subscription.stripeSubscriptionId, {
91 cancel_at_period_end: true,
92 });
93 }
94
95 // Log admin action
96 logger.info("User suspended by admin", {
97 userId,
98 adminId: admin.id,
99 reason,
100 });
101
102 revalidatePath("/admin");
103 return { success: true };
104 } catch (error) {
105 logger.error("Failed to suspend user", error, { userId, adminId: admin.id });
106 return { error: "Failed to suspend user" };
107 }
108}
109
110export async function getUserDetails(userId: string) {
111 await requireAdmin();
112
113 const user = await db.user.findUnique({
114 where: { id: userId },
115 include: {
116 subscription: true,
117 accounts: {
118 select: {
119 provider: true,
120 createdAt: true,
121 },
122 },
123 },
124 });
125
126 if (!user) {
127 throw new Error("User not found");
128 }
129
130 // Get usage statistics
131 const usageStats = {
132 // Add your app-specific usage metrics here
133 projectsCreated: 0, // Query your projects table
134 apiCallsThisMonth: 0, // Query your usage logs
135 lastActivity: user.subscription?.updatedAt,
136 };
137
138 return {
139 user,
140 usageStats,
141 };
142}

Dashboard Layout

Complete Admin Dashboard

Full admin dashboard layout with navigation and components:

src/app/admin/page.tsx
1import { Suspense } from "react";
2import { auth } from "~/auth";
3import { redirect } from "next/navigation";
4import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
5import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
6import { RevenueChart, MRRTrendChart } from "~/components/admin/RevenueChart";
7import { UserGrowthChart } from "~/components/admin/UserGrowthChart";
8import { UserManagementTable } from "~/components/admin/UserManagementTable";
9import { AdminNavigation } from "~/components/admin/AdminNavigation";
10
11async function requireAdmin() {
12 const session = await auth();
13
14 if (!session?.user) {
15 redirect("/auth/signin");
16 }
17
18 // Check admin role
19 const user = await db.user.findUnique({
20 where: { id: session.user.id },
21 select: { role: true },
22 });
23
24 if (user?.role !== "admin") {
25 redirect("/");
26 }
27
28 return session.user;
29}
30
31export default async function AdminDashboard() {
32 await requireAdmin();
33
34 return (
35 <div className="min-h-screen bg-background">
36 {/* Admin Navigation */}
37 <AdminNavigation />
38
39 <div className="container mx-auto px-4 py-8">
40 <div className="space-y-8">
41 {/* Header */}
42 <div>
43 <h1 className="text-3xl font-bold">Admin Dashboard</h1>
44 <p className="text-muted-foreground">
45 Monitor your business performance and manage users
46 </p>
47 </div>
48
49 {/* Main Content */}
50 <Tabs defaultValue="overview" className="space-y-6">
51 <TabsList>
52 <TabsTrigger value="overview">Overview</TabsTrigger>
53 <TabsTrigger value="users">Users</TabsTrigger>
54 <TabsTrigger value="analytics">Analytics</TabsTrigger>
55 <TabsTrigger value="settings">Settings</TabsTrigger>
56 </TabsList>
57
58 {/* Overview Tab */}
59 <TabsContent value="overview" className="space-y-6">
60 {/* Revenue Metrics */}
61 <Suspense fallback={<div>Loading revenue data...</div>}>
62 <RevenueChart />
63 </Suspense>
64
65 {/* User Growth */}
66 <Suspense fallback={<div>Loading growth data...</div>}>
67 <UserGrowthChart />
68 </Suspense>
69
70 {/* MRR Trend */}
71 <Suspense fallback={<div>Loading trend data...</div>}>
72 <MRRTrendChart />
73 </Suspense>
74 </TabsContent>
75
76 {/* Users Tab */}
77 <TabsContent value="users" className="space-y-6">
78 <Card>
79 <CardHeader>
80 <CardTitle>User Management</CardTitle>
81 </CardHeader>
82 <CardContent>
83 <Suspense fallback={<div>Loading users...</div>}>
84 <UserManagementTable
85 users={[]} // Load from server
86 onUserAction={async (userId, action) => {
87 "use server";
88 // Handle user actions
89 }}
90 />
91 </Suspense>
92 </CardContent>
93 </Card>
94 </TabsContent>
95
96 {/* Analytics Tab */}
97 <TabsContent value="analytics" className="space-y-6">
98 <div className="grid gap-6 md:grid-cols-2">
99 <Card>
100 <CardHeader>
101 <CardTitle>Churn Analysis</CardTitle>
102 </CardHeader>
103 <CardContent>
104 {/* Churn components */}
105 </CardContent>
106 </Card>
107
108 <Card>
109 <CardHeader>
110 <CardTitle>Cohort Retention</CardTitle>
111 </CardHeader>
112 <CardContent>
113 {/* Cohort components */}
114 </CardContent>
115 </Card>
116 </div>
117 </TabsContent>
118
119 {/* Settings Tab */}
120 <TabsContent value="settings" className="space-y-6">
121 <Card>
122 <CardHeader>
123 <CardTitle>Admin Settings</CardTitle>
124 </CardHeader>
125 <CardContent>
126 {/* Admin settings form */}
127 </CardContent>
128 </Card>
129 </TabsContent>
130 </Tabs>
131 </div>
132 </div>
133 </div>
134 );
135}
💡 Admin Dashboard Best Practices

Security & Access

  • • Implement role-based access control
  • • Log all admin actions for audit
  • • Use secure session management
  • • Validate permissions server-side
  • • Monitor for suspicious activity

User Experience

  • • Provide real-time data updates
  • • Include proper loading states
  • • Make actions clearly reversible
  • • Show actionable insights
  • • Optimize for mobile usage
⚡ Performance Optimization

Data Loading

Use React Query for caching, pagination for large datasets, and streaming for real-time updates.

Chart Performance

Virtualize large datasets, debounce filter inputs, and use memoization for expensive calculations.

Database Optimization

Optimized queries
1// Optimize admin queries
2const users = await db.user.findMany({
3 select: {
4 id: true,
5 email: true,
6 name: true,
7 createdAt: true,
8 subscription: {
9 select: {
10 type: true,
11 status: true,
12 stripeCurrentPeriodEnd: true,
13 },
14 },
15 },
16 orderBy: { createdAt: "desc" },
17 take: 50, // Pagination
18});

Admin Dashboard Complete!

Your admin dashboard provides comprehensive business insights and user management. Next, learn about waitlist signup components.