From fullstack-agents
Create Next.js data table pages with SSR initial load, simplified state management, and server-response-based UI updates. Use when asked to create a new data table page, entity management page, CRUD table, or admin list view. Generates page.tsx (SSR), table components, columns, context, actions, and API routes following a proven architecture with centralized reusable data-table component.
npx claudepluginhub adelabdelgawad/fullstack-agents --plugin fullstack-agentsThis skill uses the workspace's default tool permissions.
Create production-ready data table pages with:
examples.mdreferences/api-routes-pattern.mdreferences/columns-pattern.mdreferences/context-pattern.mdreferences/data-table-requirements.mdreferences/edit-sheet-pattern.mdreferences/form-sheet-guard.mdreferences/select-in-tables.mdreferences/table-component-pattern.mdreferences/types-pattern.mdscripts/helper.pySearches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Create production-ready data table pages with:
useState) as the default — SWR only when justified@/components/data-tableThe project includes a comprehensive reusable data-table component library at @/components/data-table. Always use these components — never build table UI from scratch.
components/data-table/
├── index.ts # Main exports (14 components)
├── types.ts # Shared type definitions
├── table/
│ ├── data-table.tsx # Core TanStack Table wrapper (generic <TData>)
│ ├── data-table-bar.tsx # Flexible 3-section toolbar (left/middle/right)
│ └── pagination.tsx # Server-side pagination synced to URL params
├── controls/
│ ├── search-input.tsx # Debounced URL-synced search (default 2000ms)
│ ├── column-toggle.tsx # Column visibility dropdown
│ ├── sort-list.tsx # Advanced multi-column sort with drag-reorder
│ ├── column-header.tsx # Per-column sort header dropdown
│ └── refresh-button.tsx # Manual refresh trigger
├── filters/
│ ├── status-filter-bar.tsx # Tab-style status filters (All/Active/Inactive)
│ └── status-badge-filter.tsx # Badge-style status filters (inline mode available)
├── actions/
│ ├── selection-display.tsx # Selected count with clear button
│ ├── export-button.tsx # CSV export of visible columns
│ ├── print-button.tsx # HTML/PDF print of table
│ ├── enable-button.tsx # Bulk enable with confirmation dialog
│ └── disable-button.tsx # Bulk disable with confirmation dialog
└── ui/
├── button.tsx # Custom button with icon + tooltip
├── status-badge.tsx # Status indicator (Online/Offline/Warning)
└── status-circle.tsx # Circular status with click-filtering
import { DataTable } from "@/components/data-table";
<DataTable
data={items}
columns={columns}
onRowSelectionChange={(selectedRows) => setSelected(selectedRows)}
renderToolbar={(table) => <MyToolbar table={table} />}
isLoading={isLoading}
enableRowSelection={true}
enableSorting={true}
/>
Key features: Generic <TData>, column visibility, row selection, column resizing, RTL support, i18n, loading overlay.
import { DynamicTableBar } from "@/components/data-table";
// Header bar (search, filters, column toggle)
<DynamicTableBar
variant="header"
left={<SearchInput />}
middle={<StatusBadgeFilter totalCount={25} activeCount={20} inactiveCount={5} />}
right={<ColumnToggleButton table={table} />}
/>
// Controller bar (selection count, bulk actions) — shows accent bg when items selected
<DynamicTableBar
variant="controller"
hasSelection={selectedCount > 0}
left={<SelectionDisplay selectedCount={3} onClearSelection={clear} i18n={{...}} />}
right={
<>
<EnableButton selectedIds={ids} onEnable={enable} />
<DisableButton selectedIds={ids} onDisable={disable} />
</>
}
/>
import { Pagination } from "@/components/data-table";
<Pagination
currentPage={page}
totalPages={totalPages}
pageSize={limit}
totalItems={totalItems}
/>
Syncs page and limit URL params automatically. Supports page sizes 10/25/50/100, first/prev/next/last buttons, entry count display.
import { StatusBadgeFilter, StatusFilterBar } from "@/components/data-table";
// Badge-style (preferred, supports inline mode for embedding in toolbars)
<StatusBadgeFilter
totalCount={total}
activeCount={activeCount}
inactiveCount={inactiveCount}
inline={true} // Render without wrapper for embedding in DynamicTableBar
/>
// Tab-style alternative
<StatusFilterBar totalCount={total} activeCount={activeCount} inactiveCount={inactiveCount} />
Both filter on is_active URL param and reset pagination to page 1 on change.
import { SearchInput, DataTableSortList, DataTableColumnHeader, ColumnToggleButton } from "@/components/data-table";
// Debounced search synced to URL ?filter= param
<SearchInput placeholder="Search..." debounceMs={2000} urlParam="filter" />
// Multi-column sort with drag reorder
<DataTableSortList sortableColumns={[
{ id: "firstName", label: "First Name" },
{ id: "role", label: "Role" },
]} />
// Per-column sort header (use in column definitions)
<DataTableColumnHeader column={column} title="Name" />
// Column visibility toggle
<ColumnToggleButton table={table} />
Sort syncs to URL: ?sort=firstName:asc,role:desc
import { SelectionDisplay, EnableButton, DisableButton, ExportButton, PrintButton } from "@/components/data-table";
<SelectionDisplay selectedCount={3} onClearSelection={clear} i18n={{ selected: "{count} {item} selected", clearSelection: "Clear", itemName: "user" }} />
<EnableButton selectedIds={ids} onEnable={handleEnable} />
<DisableButton selectedIds={ids} onDisable={handleDisable} />
<ExportButton table={table} />
<PrintButton table={table} title="Users" />
<TData>: All components use TypeScript genericsltr: / rtl: Tailwind classes throughoutDynamicTableBar + controls = flexible toolbarsuseConfirmationDialog hookThis application uses Strategy A (Simple Fetching) exclusively. All current tables use useState + server response updates.
Does this table's data change without user action?
| Answer | Strategy | Use When |
|---|---|---|
| No | A: Simple Fetching (Default) | Settings, admin CRUD, most entity tables |
| Yes | B: SWR Fetching | Dashboards, multi-user editing, live monitoring |
useState for local data managementuseSWR with documented justificationDecision framework: nextjs/references/data-fetching-strategy.md
app/(pages)/[section]/[entity]/
├── page.tsx # SSR entry point
├── context/
│ └── [entity]-actions-context.tsx # Actions provider
└── _components/
├── table/
│ ├── [entity]-table.tsx # Main client wrapper (useState)
│ ├── [entity]-table-body.tsx # DataTable + columns
│ ├── [entity]-table-columns.tsx# Column definitions
│ ├── [entity]-table-controller.tsx # Toolbar/bulk actions
│ └── [entity]-table-actions.tsx # Bulk action hooks
├── actions/
│ ├── add-[entity]-button.tsx # Add button + sheet
│ └── actions-menu.tsx # Row action menu
├── modal/
│ ├── add-[entity]-sheet.tsx # Create form
│ ├── edit-[entity]-sheet.tsx # Edit form
│ └── view-[entity]-sheet.tsx # View details
└── sidebar/
└── status-panel.tsx # Stats sidebar
@/components/data-table — never build table UI from scratchDefine response types in @/lib/types/api/[entity].ts. See references/types-pattern.md.
Create server actions in @/lib/actions/[entity].actions.ts.
page.tsx)import { auth } from "@/lib/auth/server-auth";
import { get[Entity]s } from "@/lib/actions/[entity].actions";
import { redirect } from "next/navigation";
import [Entity]Table from "./_components/table/[entity]-table";
export default async function [Entity]Page({
searchParams,
}: {
searchParams: Promise<{
is_active?: string;
search?: string;
page?: string;
limit?: string;
}>;
}) {
const session = await auth();
if (!session?.accessToken) redirect("/login");
const params = await searchParams;
const pageNumber = Number(params.page) || 1;
const limitNumber = Number(params.limit) || 10;
const skip = (pageNumber - 1) * limitNumber;
const data = await get[Entity]s(limitNumber, skip, {
is_active: params.is_active,
search: params.search,
});
return <[Entity]Table initialData={data} />;
}
See references/table-component-pattern.md for the full pattern with:
useState with initialData (Strategy A — default)updateItems() function for state mutationActionsProvider wrapperDataTable, DynamicTableBar, Pagination, and filter componentsSee references/columns-pattern.md for:
updatingIds loading statesDataTableColumnHeader for sortable column headersSee references/context-pattern.md for:
See references/edit-sheet-pattern.md for:
// ✅ CORRECT: Use server response
const updated = await api.put<EntityResponse>(`/entity/${id}`, body);
updateItems([updated]); // Update local state with server data
// ❌ WRONG: Optimistic update
const optimistic = { ...current, ...changes };
setData({ items: [...items.filter(i => i.id !== id), optimistic] });
// React state — no SWR
const [data, setData] = useState<Response>(initialData);
const updateItems = (serverResponse: Item[]) => {
setData(current => {
if (!current) return current;
const responseMap = new Map(serverResponse.map(i => [i.id, i]));
return {
...current,
items: current.items.map(item =>
responseMap.has(item.id) ? responseMap.get(item.id)! : item
),
};
});
};
// In [entity]-table-body.tsx — compose using data-table components
import { DataTable, DynamicTableBar, SearchInput, StatusBadgeFilter, ColumnToggleButton, RefreshButton } from "@/components/data-table";
<DataTable
data={items}
columns={columns}
onRowSelectionChange={handleSelectionChange}
renderToolbar={(table) => (
<>
<DynamicTableBar
variant="header"
left={<SearchInput />}
middle={<StatusBadgeFilter inline totalCount={total} activeCount={active} inactiveCount={inactive} />}
right={
<>
<DataTableSortList sortableColumns={sortableColumns} />
<ColumnToggleButton table={table} />
<RefreshButton onRefresh={onRefresh} />
</>
}
/>
{selectedCount > 0 && (
<DynamicTableBar
variant="controller"
hasSelection
left={<SelectionDisplay selectedCount={selectedCount} onClearSelection={clear} i18n={i18n} />}
right={
<>
<EnableButton selectedIds={selectedIds} onEnable={handleEnable} />
<DisableButton selectedIds={selectedIds} onDisable={handleDisable} />
</>
}
/>
)}
</>
)}
/>
src/frontend/app/(pages)/setting/users/ — canonical users table (Strategy A)src/frontend/app/(pages)/setting/roles/ — roles table (Strategy A)Ensure project has:
@tanstack/react-table - Table primitives@/components/data-table - Reusable table components (must exist)swr - Only if using Strategy B (SWR fetching)lib/actions/useState with initialDataDataTable, DynamicTableBar, filters, controls from @/components/data-tableDataTableColumnHeader for sortable headersEnableButton/DisableButton with confirmation