Dashboard widgets
Stats, charts, activity feed.
Logged-in user landing surface. Three sections: stat cards across the top, a chart in the middle, an activity feed on the side. The Grit scaffold ships these as starter widgets ā this lesson covers how they wire up.
The dashboard shell
import { StatCards } from './stat-cards'import { RevenueChart } from './revenue-chart'import { ActivityFeed } from './activity-feed'import { apiFetch } from '@/lib/api'export default async function DashboardPage() {const stats = await apiFetch('/api/dashboard/stats').then((r) => r.json())return (<div className="grid grid-cols-1 lg:grid-cols-3 gap-4"><div className="lg:col-span-3"><StatCards stats={stats.data} /></div><div className="lg:col-span-2"><RevenueChart /></div><div><ActivityFeed /></div></div>)}
Server component ā fetches stats on the server, passes to the client widgets that need interactivity (chart hover, feed polling).
Stat cards
import { formatCurrency } from '@workspace/shared/utils/currency'interface Stats {users: numberrevenue: numberorders: numberconversion: number}export function StatCards({ stats }: { stats: Stats }) {return (<div className="grid grid-cols-2 md:grid-cols-4 gap-3"><Card label="Users" value={stats.users.toLocaleString()} /><Card label="Revenue" value={formatCurrency(stats.revenue)} /><Card label="Orders" value={stats.orders.toLocaleString()} /><Card label="Conversion" value={(stats.conversion * 100).toFixed(1) + '%'} /></div>)}function Card({ label, value }) {return (<div className="rounded-xl border p-4 bg-card"><p className="text-xs text-muted-foreground">{label}</p><p className="text-2xl font-semibold mt-1">{value}</p></div>)}
Pure server component. No JS shipped to the browser for the cards.
The chart ā Recharts (client component)
'use client'import { useQuery } from '@tanstack/react-query'import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'export function RevenueChart() {const { data } = useQuery({queryKey: ['revenue-30d'],queryFn: () => fetch('/api/dashboard/revenue/30d').then(r => r.json()),})return (<div className="rounded-xl border p-4 bg-card"><h3 className="font-semibold mb-3">Revenue, last 30 days</h3><ResponsiveContainer width="100%" height={240}><LineChart data={data?.data ?? []}><XAxis dataKey="date" /><YAxis /><Tooltip /><Line type="monotone" dataKey="revenue" stroke="#6c5ce7" /></LineChart></ResponsiveContainer></div>)}
Recharts is the practical pick ā small bundle, declarative, composable. Charts are inherently interactive so they live in client components.
The activity feed
'use client'import { useQuery } from '@tanstack/react-query'export function ActivityFeed() {const { data } = useQuery({queryKey: ['activity'],queryFn: () => fetch('/api/activity?limit=10').then(r => r.json()),refetchInterval: 30_000, // poll every 30s for fresh events})return (<div className="rounded-xl border p-4 bg-card"><h3 className="font-semibold mb-3">Recent activity</h3><ul className="space-y-2 text-sm">{(data?.data ?? []).map((a) => (<li key={a.id}><span className="text-muted-foreground">{a.created_at}</span>{' '}{a.event}</li>))}</ul></div>)}
Polls every 30 seconds for new events. For real-time, swap polling for the Grit WebSocket plugin (covered in the plugins course).
Quick check
Try it
For chapter 3's assignment, build the dashboard:
- Sign up a new user (from last lesson).
- Add 3 stat cards backed by real API endpoints.
- Add the activity feed pulling from
/api/activity. - Screenshot the dashboard. Paste it in
notes.md.
What's next
Chapter 4 ā The Admin Panel. Filament-style CRUD pages built from one defineResource() call.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled ā suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub