Admin Dashboard Components
Build powerful admin dashboards with analytics charts, user management, subscription tracking, and business intelligence components.
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
Track monthly recurring revenue and growth trends:
1"use client";23import { 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";78export function RevenueChart() {9 const { data: mrrData, isLoading, error } = useMRRAnalytics();1011 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 }2829 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 data35 </p>36 </CardContent>37 </Card>38 );39 }4041 const currentMonth = mrrData[mrrData.length - 1];42 const previousMonth = mrrData[mrrData.length - 2];4344 const mrrGrowth = previousMonth45 ? ((currentMonth.totalMRR - previousMonth.totalMRR) / previousMonth.totalMRR) * 10046 : 0;4748 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>7677 {/* 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 subscriptions89 </p>90 </CardContent>91 </Card>9293 {/* 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 cancellations105 </p>106 </CardContent>107 </Card>108 </div>109 );110}111112// MRR Trend Chart Component113export function MRRTrendChart() {114 const { data: mrrData } = useMRRAnalytics();115116 if (!mrrData) return null;117118 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 <div129 key={month.month}130 className="flex flex-col items-center"131 >132 <div133 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}
Monitor user acquisition and engagement metrics:
1"use client";23import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";4import { Users, UserPlus, UserMinus, Activity } from "lucide-react";5import { useGrowthMetrics } from "~/hooks/useAnalytics";67export function UserGrowthChart() {8 const { data: growth, isLoading } = useGrowthMetrics();910 if (isLoading || !growth) {11 return <div>Loading growth metrics...</div>;12 }1314 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 month28 </p>29 </CardContent>30 </Card>3132 {/* 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 rate44 </p>45 </CardContent>46 </Card>4748 {/* 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 user60 </p>61 </CardContent>62 </Card>6364 {/* 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 value76 </p>77 </CardContent>78 </Card>79 </div>80 );81}
User Management
Comprehensive user management with search, filtering, and actions:
1"use client";23import { 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";2425interface 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}3637interface UserManagementTableProps {38 users: User[];39 onUserAction: (userId: string, action: string) => void;40}4142export function UserManagementTable({ users, onUserAction }: UserManagementTableProps) {43 const [searchTerm, setSearchTerm] = useState("");44 const [statusFilter, setStatusFilter] = useState("all");45 const [filteredUsers, setFilteredUsers] = useState(users);4647 useEffect(() => {48 let filtered = users;4950 // Search filter51 if (searchTerm) {52 filtered = filtered.filter(user =>53 user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||54 user.name?.toLowerCase().includes(searchTerm.toLowerCase())55 );56 }5758 // Status filter59 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 }6869 setFilteredUsers(filtered);70 }, [users, searchTerm, statusFilter]);7172 const getSubscriptionBadge = (user: User) => {73 if (!user.subscription || user.subscription.type === "FREE") {74 return <Badge variant="secondary">Free</Badge>;75 }7677 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 };8485 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 <Input92 placeholder="Search users by email or name..."93 value={searchTerm}94 onChange={(e) => setSearchTerm(e.target.value)}95 className="pl-10"96 />97 </div>9899 <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 Users109 </DropdownMenuItem>110 <DropdownMenuItem onClick={() => setStatusFilter("free")}>111 Free Users112 </DropdownMenuItem>113 <DropdownMenuItem onClick={() => setStatusFilter("paid")}>114 Paid Users115 </DropdownMenuItem>116 <DropdownMenuItem onClick={() => setStatusFilter("active")}>117 Active Subscriptions118 </DropdownMenuItem>119 <DropdownMenuItem onClick={() => setStatusFilter("canceled")}>120 Canceled Subscriptions121 </DropdownMenuItem>122 </DropdownMenuContent>123 </DropdownMenu>124 </div>125126 {/* Results count */}127 <div className="text-sm text-muted-foreground">128 Showing {filteredUsers.length} of {users.length} users129 </div>130131 {/* 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?.stripeCurrentPeriodEnd164 ? 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 <DropdownMenuItem178 onClick={() => onUserAction(user.id, "view")}179 >180 View Details181 </DropdownMenuItem>182 <DropdownMenuItem183 onClick={() => onUserAction(user.id, "impersonate")}184 >185 Impersonate User186 </DropdownMenuItem>187 <DropdownMenuSeparator />188 <DropdownMenuItem189 onClick={() => onUserAction(user.id, "suspend")}190 className="text-red-600"191 >192 Suspend Account193 </DropdownMenuItem>194 </DropdownMenuContent>195 </DropdownMenu>196 </TableCell>197 </TableRow>198 ))}199 </TableBody>200 </Table>201 </div>202203 {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}
Server actions for admin user management:
1"use server";23import { auth } from "~/auth";4import { db } from "~/server/db";5import { logger } from "~/lib/logger";6import { revalidatePath } from "next/cache";78async function requireAdmin() {9 const session = await auth();1011 if (!session?.user) {12 throw new Error("Not authenticated");13 }1415 // Check if user has admin role16 const user = await db.user.findUnique({17 where: { id: session.user.id },18 select: { role: true },19 });2021 if (user?.role !== "admin") {22 throw new Error("Insufficient permissions");23 }2425 return session.user;26}2728export async function getUsersForAdmin(page = 1, limit = 50, filter?: string) {29 await requireAdmin();3031 const skip = (page - 1) * limit;3233 const where = filter ? {34 OR: [35 { email: { contains: filter, mode: "insensitive" } },36 { name: { contains: filter, mode: "insensitive" } },37 ],38 } : {};3940 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 ]);5859 return {60 users,61 pagination: {62 page,63 limit,64 total,65 pages: Math.ceil(total / limit),66 },67 };68}6970export async function suspendUser(userId: string, reason?: string) {71 const admin = await requireAdmin();7273 try {74 // Update user status75 await db.user.update({76 where: { id: userId },77 data: {78 // You might want to add a 'status' field to track suspended users79 // status: "suspended"80 },81 });8283 // Cancel active subscription if any84 const subscription = await db.subscription.findUnique({85 where: { userId },86 });8788 if (subscription?.stripeSubscriptionId && subscription.status === "active") {89 // Cancel in Stripe90 await stripe.subscriptions.update(subscription.stripeSubscriptionId, {91 cancel_at_period_end: true,92 });93 }9495 // Log admin action96 logger.info("User suspended by admin", {97 userId,98 adminId: admin.id,99 reason,100 });101102 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}109110export async function getUserDetails(userId: string) {111 await requireAdmin();112113 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 });125126 if (!user) {127 throw new Error("User not found");128 }129130 // Get usage statistics131 const usageStats = {132 // Add your app-specific usage metrics here133 projectsCreated: 0, // Query your projects table134 apiCallsThisMonth: 0, // Query your usage logs135 lastActivity: user.subscription?.updatedAt,136 };137138 return {139 user,140 usageStats,141 };142}
Dashboard Layout
Full admin dashboard layout with navigation and components:
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";1011async function requireAdmin() {12 const session = await auth();1314 if (!session?.user) {15 redirect("/auth/signin");16 }1718 // Check admin role19 const user = await db.user.findUnique({20 where: { id: session.user.id },21 select: { role: true },22 });2324 if (user?.role !== "admin") {25 redirect("/");26 }2728 return session.user;29}3031export default async function AdminDashboard() {32 await requireAdmin();3334 return (35 <div className="min-h-screen bg-background">36 {/* Admin Navigation */}37 <AdminNavigation />3839 <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 users46 </p>47 </div>4849 {/* 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>5758 {/* 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>6465 {/* User Growth */}66 <Suspense fallback={<div>Loading growth data...</div>}>67 <UserGrowthChart />68 </Suspense>6970 {/* MRR Trend */}71 <Suspense fallback={<div>Loading trend data...</div>}>72 <MRRTrendChart />73 </Suspense>74 </TabsContent>7576 {/* 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 <UserManagementTable85 users={[]} // Load from server86 onUserAction={async (userId, action) => {87 "use server";88 // Handle user actions89 }}90 />91 </Suspense>92 </CardContent>93 </Card>94 </TabsContent>9596 {/* 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>107108 <Card>109 <CardHeader>110 <CardTitle>Cohort Retention</CardTitle>111 </CardHeader>112 <CardContent>113 {/* Cohort components */}114 </CardContent>115 </Card>116 </div>117 </TabsContent>118119 {/* 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}
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
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
1// Optimize admin queries2const 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, // Pagination18});
Admin Dashboard Complete!
Your admin dashboard provides comprehensive business insights and user management. Next, learn about waitlist signup components.