zazzy
Blocks

Recent activity card

A compact dashboard widget listing the org's most recent audit-log entries; labels billing events when billing is present.

A compact "Recent activity" dashboard widget over the org's audit log — a smaller sibling of the full audit-log viewer. It shows the latest few entries; billing-related events are labelled when the billing feature is present (the label logic strips out at install time in a billing-less app). The list query is admin-gated server-side, so a plain member sees the error state.

Requires: audit-log

Preview

Recent activity
The latest changes in your organization.
member.invited
billing.subscription.updated
org.settings.updated
member.role.changed
"use client";import {  Card,  CardContent,  CardDescription,  CardHeader,  CardTitle,} from "@/components/ui/card";import { trpc } from "@/lib/trpc";import { isBillingEvent } from "./is-billing-event";// The list query's inferred output type is very deep; declaring the row shape// inline and casting `query.data` avoids TS2589 ("type instantiation excessively// deep") — the same trick the full AuditLogViewer uses.type AuditEntry = { id: string; action: string; createdAt: string | Date };type AuditPage = { items: AuditEntry[]; nextCursor: string | null };/** How many recent entries the compact card shows. */const RECENT_LIMIT = 8;/** * A compact "Recent activity" dashboard widget over the org's audit log — a * smaller sibling of the full AuditLogViewer. Shows the latest few entries; * billing-related events are labelled when the billing feature is present (the * label logic strips out at install time in a billing-less app). The list query * is admin-gated server-side, so a plain member sees the error state. */export function ActivityCard() {  const query = trpc.auditLog.list.useQuery({ limit: RECENT_LIMIT });  const page = query.data as unknown as AuditPage | undefined;  const entries = page?.items ?? [];  return (    <Card>      <CardHeader>        <CardTitle>Recent activity</CardTitle>        <CardDescription>          The latest changes in your organization.        </CardDescription>      </CardHeader>      <CardContent className="flex flex-col gap-2 text-sm">        {query.isPending && (          <p className="text-muted-foreground">Loading activity…</p>        )}        {query.isError && (          <p className="text-destructive">            Could not load activity: {query.error.message}          </p>        )}        {!query.isPending && !query.isError && entries.length === 0 && (          <p className="text-muted-foreground">No activity yet.</p>        )}        {entries.map((entry) => (          <div            key={entry.id}            className="flex items-center justify-between gap-3"          >            <span className="truncate">              {entry.action}              {isBillingEvent(entry.action) && (                <span className="ml-2 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">                  billing                </span>              )}            </span>            <time className="shrink-0 text-muted-foreground">              {new Date(entry.createdAt).toLocaleDateString()}            </time>          </div>        ))}      </CardContent>    </Card>  );}

Offline preview

The docs site has no backend, so the preview seeds a few representative entries (including a billing one, to show the label). In an app the data comes from the auditLog router.

Install

npx create-saas-starter add audit-log-card

Run it from your app's root. add only writes new files — wiring is up to you:

import { ActivityCard } from "@/components/blocks/audit-log-card/activity-card";

On this page