Dashboard

Dashboard

A dashboard is the main page of your admin interface that displays key metrics, statistics, and quick access to important information. Better Admin makes it easy to build custom dashboards using the useQuery hook to fetch and display data from multiple resources.

Think of it as your admin homepage: The dashboard gives you a quick overview of your application's data - total users, recent orders, active sessions, or any other metrics that matter to your business.

What You Can Build

With Better Admin dashboards, you can:

  • Display statistics - Show counts, sums, and aggregates from your resources
  • Track real-time data - Display live updates using better-query's reactive hooks
  • Visualize trends - Create charts and graphs with your preferred charting library
  • Quick actions - Add buttons for common admin tasks
  • Multi-resource views - Combine data from multiple resources in one view

Basic Dashboard

Simple Stats Dashboard

The simplest dashboard shows key statistics from your resources:

app/admin/page.tsx
"use client";

import { useQuery } from "better-admin";
import { query } from "@/lib/query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

export default function DashboardPage() {
  const { count: userCount } = useQuery("user", query);
  const { count: postCount } = useQuery("post", query);

  const { data: totalUsers } = userCount.useQuery();
  const { data: totalPosts } = postCount.useQuery();

  return (
    <div>
      <h1 className="text-2xl font-bold mb-6">Dashboard</h1>
      <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
        <Card>
          <CardHeader>
            <CardTitle>Total Users</CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-4xl font-bold">{totalUsers || 0}</p>
          </CardContent>
        </Card>
        <Card>
          <CardHeader>
            <CardTitle>Total Posts</CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-4xl font-bold">{totalPosts || 0}</p>
          </CardContent>
        </Card>
      </div>
    </div>
  );
}

What this does:

  1. Uses useQuery to access the count operation for each resource
  2. Fetches the total count with useQuery() hook
  3. Displays the counts in a responsive grid layout

Why use count? The count operation is efficient because it only queries the database for the number of records, not the actual data, making your dashboard fast even with large datasets.

Advanced Dashboards

Dashboard with Filters

Show statistics filtered by specific criteria:

app/admin/page.tsx
"use client";

import { useQuery } from "better-admin";
import { query } from "@/lib/query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

export default function DashboardPage() {
  const { count: userCount, list: userList } = useQuery("user", query);
  const { count: postCount } = useQuery("post", query);

  // Total counts
  const { data: totalUsers } = userCount.useQuery();
  const { data: totalPosts } = postCount.useQuery();

  // Active users (logged in within last 7 days)
  const sevenDaysAgo = new Date();
  sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);

  const { data: activeUsers } = userCount.useQuery({
    where: {
      lastLoginAt: {
        gte: sevenDaysAgo,
      },
    },
  });

  // Published posts
  const { data: publishedPosts } = postCount.useQuery({
    where: {
      published: true,
    },
  });

  // Recent users (last 5)
  const { data: recentUsers } = userList.useQuery({
    orderBy: { createdAt: "desc" },
    take: 5,
  });

  return (
    <div className="space-y-6">
      <h1 className="text-2xl font-bold">Dashboard</h1>
      
      {/* Stats Grid */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
        <Card>
          <CardHeader>
            <CardTitle>Total Users</CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-4xl font-bold">{totalUsers || 0}</p>
          </CardContent>
        </Card>
        
        <Card>
          <CardHeader>
            <CardTitle>Active Users (7d)</CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-4xl font-bold">{activeUsers || 0}</p>
            <p className="text-sm text-muted-foreground mt-2">
              {Math.round(((activeUsers || 0) / (totalUsers || 1)) * 100)}% active
            </p>
          </CardContent>
        </Card>
        
        <Card>
          <CardHeader>
            <CardTitle>Total Posts</CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-4xl font-bold">{totalPosts || 0}</p>
          </CardContent>
        </Card>
        
        <Card>
          <CardHeader>
            <CardTitle>Published Posts</CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-4xl font-bold">{publishedPosts || 0}</p>
            <p className="text-sm text-muted-foreground mt-2">
              {Math.round(((publishedPosts || 0) / (totalPosts || 1)) * 100)}% published
            </p>
          </CardContent>
        </Card>
      </div>

      {/* Recent Activity */}
      <Card>
        <CardHeader>
          <CardTitle>Recent Users</CardTitle>
        </CardHeader>
        <CardContent>
          {recentUsers && recentUsers.length > 0 ? (
            <div className="space-y-2">
              {recentUsers.map((user: any) => (
                <div key={user.id} className="flex justify-between items-center">
                  <div>
                    <p className="font-medium">{user.name}</p>
                    <p className="text-sm text-muted-foreground">{user.email}</p>
                  </div>
                  <p className="text-sm text-muted-foreground">
                    {new Date(user.createdAt).toLocaleDateString()}
                  </p>
                </div>
              ))}
            </div>
          ) : (
            <p className="text-muted-foreground">No recent users</p>
          )}
        </CardContent>
      </Card>
    </div>
  );
}

What this adds:

  • Filtered counts (active users, published posts)
  • Percentage calculations for quick insights
  • Recent activity list showing the latest users
  • Responsive grid that adapts to screen size

Date filtering: You can use comparison operators like gte (greater than or equal), lte (less than or equal), gt, lt to filter by dates and numbers.

E-commerce Dashboard

A more complex example for an e-commerce application:

app/admin/page.tsx
"use client";

import { useQuery } from "better-admin";
import { query } from "@/lib/query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { DollarSign, ShoppingCart, Users, Package } from "lucide-react";

export default function DashboardPage() {
  const { count: orderCount, list: orderList } = useQuery("order", query);
  const { count: productCount } = useQuery("product", query);
  const { count: customerCount } = useQuery("customer", query);

  // Today's date for filtering
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  // Statistics
  const { data: totalOrders } = orderCount.useQuery();
  const { data: todayOrders } = orderCount.useQuery({
    where: {
      createdAt: { gte: today },
    },
  });

  const { data: pendingOrders } = orderCount.useQuery({
    where: {
      status: "pending",
    },
  });

  const { data: totalProducts } = productCount.useQuery();
  const { data: lowStockProducts } = productCount.useQuery({
    where: {
      stock: { lte: 10 },
    },
  });

  const { data: totalCustomers } = customerCount.useQuery();

  // Recent orders for revenue calculation
  const { data: recentOrders } = orderList.useQuery({
    orderBy: { createdAt: "desc" },
    take: 100,
    where: {
      status: "completed",
    },
  });

  // Calculate revenue
  const totalRevenue = recentOrders?.reduce(
    (sum: number, order: any) => sum + (order.total || 0),
    0
  ) || 0;

  const todayRevenue = recentOrders
    ?.filter((order: any) => new Date(order.createdAt) >= today)
    .reduce((sum: number, order: any) => sum + (order.total || 0), 0) || 0;

  return (
    <div className="space-y-6">
      <div className="flex justify-between items-center">
        <h1 className="text-3xl font-bold">Dashboard</h1>
        <p className="text-sm text-muted-foreground">
          {new Date().toLocaleDateString("en-US", { 
            weekday: "long", 
            year: "numeric", 
            month: "long", 
            day: "numeric" 
          })}
        </p>
      </div>

      {/* Key Metrics */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
        <Card>
          <CardHeader className="flex flex-row items-center justify-between pb-2">
            <CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
            <DollarSign className="h-4 w-4 text-muted-foreground" />
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">
              ${(totalRevenue / 100).toFixed(2)}
            </div>
            <p className="text-xs text-muted-foreground mt-1">
              Today: ${(todayRevenue / 100).toFixed(2)}
            </p>
          </CardContent>
        </Card>

        <Card>
          <CardHeader className="flex flex-row items-center justify-between pb-2">
            <CardTitle className="text-sm font-medium">Orders</CardTitle>
            <ShoppingCart className="h-4 w-4 text-muted-foreground" />
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">{totalOrders || 0}</div>
            <p className="text-xs text-muted-foreground mt-1">
              {todayOrders || 0} today • {pendingOrders || 0} pending
            </p>
          </CardContent>
        </Card>

        <Card>
          <CardHeader className="flex flex-row items-center justify-between pb-2">
            <CardTitle className="text-sm font-medium">Customers</CardTitle>
            <Users className="h-4 w-4 text-muted-foreground" />
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">{totalCustomers || 0}</div>
            <p className="text-xs text-muted-foreground mt-1">
              Total registered customers
            </p>
          </CardContent>
        </Card>

        <Card>
          <CardHeader className="flex flex-row items-center justify-between pb-2">
            <CardTitle className="text-sm font-medium">Products</CardTitle>
            <Package className="h-4 w-4 text-muted-foreground" />
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">{totalProducts || 0}</div>
            <p className="text-xs text-muted-foreground mt-1">
              {lowStockProducts || 0} low stock
            </p>
          </CardContent>
        </Card>
      </div>

      {/* Alerts */}
      {(pendingOrders || 0) > 0 && (
        <Card className="border-orange-500">
          <CardHeader>
            <CardTitle className="text-orange-500">Pending Orders</CardTitle>
          </CardHeader>
          <CardContent>
            <p>You have {pendingOrders} orders waiting to be processed.</p>
          </CardContent>
        </Card>
      )}

      {(lowStockProducts || 0) > 0 && (
        <Card className="border-red-500">
          <CardHeader>
            <CardTitle className="text-red-500">Low Stock Alert</CardTitle>
          </CardHeader>
          <CardContent>
            <p>{lowStockProducts} products have low stock (≤10 units).</p>
          </CardContent>
        </Card>
      )}
    </div>
  );
}

Key features:

  • Revenue calculations from order data
  • Multiple filtered counts (today's orders, pending orders, low stock)
  • Icons for visual appeal
  • Alert cards for important notifications
  • Date formatting for better UX

Performance tip: When calculating totals or aggregates, limit the query with take to avoid fetching too much data. For production, consider using database aggregate functions or custom operations.

Using the useQuery Hook

The useQuery hook is the foundation of dashboard functionality. It provides reactive access to your better-query operations.

Available Operations

const {
  list,      // Fetch multiple records
  get,       // Fetch single record by ID
  create,    // Create new record
  update,    // Update existing record
  remove,    // Delete record
  count,     // Count records
} = useQuery("resourceName", query);

Count Operation

Get the total number of records:

const { count } = useQuery("user", query);
const { data: totalUsers, isLoading, error } = count.useQuery();

// With filters
const { data: activeUsers } = count.useQuery({
  where: { status: "active" }
});

List Operation

Fetch multiple records with options:

const { list } = useQuery("post", query);

const { data: posts, isLoading, error } = list.useQuery({
  take: 10,                        // Limit results
  skip: 0,                         // Pagination offset
  orderBy: { createdAt: "desc" },  // Sort order
  where: { published: true },      // Filters
});

Combining Operations

Create rich dashboards by combining multiple operations:

export default function DashboardPage() {
  const userQuery = useQuery("user", query);
  const postQuery = useQuery("post", query);
  const commentQuery = useQuery("comment", query);

  // Counts
  const { data: userCount } = userQuery.count.useQuery();
  const { data: postCount } = postQuery.count.useQuery();
  const { data: commentCount } = commentQuery.count.useQuery();

  // Lists
  const { data: recentPosts } = postQuery.list.useQuery({
    take: 5,
    orderBy: { createdAt: "desc" },
  });

  const { data: topAuthors } = userQuery.list.useQuery({
    take: 10,
    orderBy: { postCount: "desc" }, // Assuming you have this field
  });

  // Render your dashboard...
}

Best Practices

1. Loading States

Always handle loading states for better UX:

export default function DashboardPage() {
  const { count } = useQuery("user", query);
  const { data: userCount, isLoading } = count.useQuery();

  if (isLoading) {
    return (
      <div className="flex items-center justify-center h-screen">
        <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900" />
      </div>
    );
  }

  return (
    <div>
      <h1>Total Users: {userCount}</h1>
    </div>
  );
}

2. Error Handling

Gracefully handle errors:

export default function DashboardPage() {
  const { count } = useQuery("user", query);
  const { data: userCount, isLoading, error } = count.useQuery();

  if (error) {
    return (
      <div className="p-4 border border-red-500 rounded">
        <h2 className="text-red-500 font-bold">Error loading dashboard</h2>
        <p>{error.message}</p>
      </div>
    );
  }

  // ... rest of component
}

3. Optimize Queries

Keep your dashboard fast:

// ✅ Good: Only fetch what you need
const { data: recentUsers } = list.useQuery({
  take: 5,
  select: ["id", "name", "email"], // Only select needed fields
});

// ❌ Bad: Fetching too much data
const { data: allUsers } = list.useQuery(); // Could be thousands of records

4. Use Proper Caching

Better Query automatically caches results, but you can configure it:

const { data: stats } = count.useQuery(
  { where: { status: "active" } },
  {
    staleTime: 60000, // Cache for 1 minute
    refetchOnWindowFocus: false, // Don't refetch when window regains focus
  }
);

5. Separate Concerns

For complex dashboards, break them into components:

components/dashboard/stats-cards.tsx
export function StatsCards() {
  const { count } = useQuery("user", query);
  const { data: userCount } = count.useQuery();

  return (
    <div className="grid grid-cols-4 gap-4">
      <Card>
        <CardHeader>Total Users</CardHeader>
        <CardContent>{userCount || 0}</CardContent>
      </Card>
      {/* More cards... */}
    </div>
  );
}
app/admin/page.tsx
import { StatsCards } from "@/components/dashboard/stats-cards";
import { RecentActivity } from "@/components/dashboard/recent-activity";
import { AlertsPanel } from "@/components/dashboard/alerts-panel";

export default function DashboardPage() {
  return (
    <div className="space-y-6">
      <h1>Dashboard</h1>
      <StatsCards />
      <RecentActivity />
      <AlertsPanel />
    </div>
  );
}

Common Patterns

Real-time Updates

Better Query supports real-time updates when using the realtime plugin:

import { useQuery } from "better-admin";
import { query } from "@/lib/query";

export default function DashboardPage() {
  const { count } = useQuery("order", query);
  
  // This will automatically update when new orders are created
  const { data: orderCount } = count.useQuery(undefined, {
    refetchInterval: 5000, // Refetch every 5 seconds
  });

  return <div>Active Orders: {orderCount}</div>;
}

Date Range Filtering

Create dashboards with date range filters:

import { useState } from "react";
import { useQuery } from "better-admin";
import { query } from "@/lib/query";

export default function DashboardPage() {
  const [dateRange, setDateRange] = useState({
    from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // 30 days ago
    to: new Date(),
  });

  const { count } = useQuery("order", query);
  
  const { data: orderCount } = count.useQuery({
    where: {
      createdAt: {
        gte: dateRange.from,
        lte: dateRange.to,
      },
    },
  });

  return (
    <div>
      <DateRangePicker value={dateRange} onChange={setDateRange} />
      <p>Orders in range: {orderCount}</p>
    </div>
  );
}

Comparison Metrics

Show comparisons between time periods:

export default function DashboardPage() {
  const { count } = useQuery("order", query);

  const today = new Date();
  const yesterday = new Date(today);
  yesterday.setDate(yesterday.getDate() - 1);

  const { data: todayOrders } = count.useQuery({
    where: {
      createdAt: { gte: today },
    },
  });

  const { data: yesterdayOrders } = count.useQuery({
    where: {
      createdAt: {
        gte: yesterday,
        lt: today,
      },
    },
  });

  const percentageChange = yesterdayOrders
    ? ((todayOrders - yesterdayOrders) / yesterdayOrders) * 100
    : 0;

  return (
    <Card>
      <CardHeader>Today's Orders</CardHeader>
      <CardContent>
        <p className="text-4xl font-bold">{todayOrders || 0}</p>
        <p className={percentageChange >= 0 ? "text-green-500" : "text-red-500"}>
          {percentageChange.toFixed(1)}% vs yesterday
        </p>
      </CardContent>
    </Card>
  );
}

Integration with Charts

You can integrate popular charting libraries for visualizations:

With Recharts

import { useQuery } from "better-admin";
import { query } from "@/lib/query";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from "recharts";

export default function DashboardPage() {
  const { list } = useQuery("order", query);

  // Get orders from last 7 days
  const sevenDaysAgo = new Date();
  sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);

  const { data: orders } = list.useQuery({
    where: {
      createdAt: { gte: sevenDaysAgo },
    },
    orderBy: { createdAt: "asc" },
  });

  // Group orders by day
  const chartData = orders?.reduce((acc: any[], order: any) => {
    const date = new Date(order.createdAt).toLocaleDateString();
    const existing = acc.find((item) => item.date === date);
    
    if (existing) {
      existing.orders += 1;
      existing.revenue += order.total;
    } else {
      acc.push({ date, orders: 1, revenue: order.total });
    }
    
    return acc;
  }, []) || [];

  return (
    <Card>
      <CardHeader>
        <CardTitle>Orders Trend (Last 7 Days)</CardTitle>
      </CardHeader>
      <CardContent>
        <LineChart width={600} height={300} data={chartData}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="date" />
          <YAxis />
          <Tooltip />
          <Legend />
          <Line type="monotone" dataKey="orders" stroke="#8884d8" />
          <Line type="monotone" dataKey="revenue" stroke="#82ca9d" />
        </LineChart>
      </CardContent>
    </Card>
  );
}

Next Steps

Now that you know how to build dashboards:

Need inspiration? Check out the Examples page for complete dashboard implementations you can use as starting points.