Contact List Pattern
Use this pattern for any entity list page with DataTable, search, sorting, pagination, and StatusBadge.
Live Demo
| Name | Company | Status | ||
|---|---|---|---|---|
| Sarah ChenCTO | sarah.chen@acme.com | Acme Corp | Customer | |
| Marcus JohnsonFounder | marcus.j@techstart.io | TechStart | Lead | |
| Elena RodriguezPartner | elena@globalventures.com | Global Ventures | Active | |
| David KimVP Engineering | dkim@enterprise.co | Enterprise Co | Customer | |
| Aisha PatelCEO | aisha.patel@innovate.dev | Innovate Dev | Inactive |
Source
'use client';
import { createColumnHelper } from '@tanstack/react-table';
import Link from 'next/link';
import { useMemo, useState } from 'react';
import { Button, StatusBadge } from '@stackmates/ui-interactive/atoms';
import { DataTable } from '@stackmates/ui-interactive/organisms';
import { CONTACTS, CONTACT_STATUS_BADGE_MAP, STATUS_LABELS,
STATUS_OPTIONS, type Contact, type ContactStatus } from '../_data';
const columnHelper = createColumnHelper<Contact>();
const columns = [
columnHelper.accessor(row => `${row.firstName} ${row.lastName}`, {
id: 'name', header: 'Name',
cell: info => <Link href={`/crm/contacts/${info.row.original.id}`}>
<span className="font-medium">{info.getValue()}</span>
</Link>,
}),
columnHelper.accessor('email', { header: 'Email' }),
columnHelper.accessor('company', { header: 'Company' }),
columnHelper.accessor('status', {
header: 'Status',
cell: info => (
<StatusBadge status={CONTACT_STATUS_BADGE_MAP[info.getValue()]}
showIcon={false} size="sm">
{STATUS_LABELS[info.getValue()]}
</StatusBadge>
),
}),
];
export default function ContactListPage() {
const [globalFilter, setGlobalFilter] = useState('');
const [statusFilter, setStatusFilter] = useState<ContactStatus | 'all'>('all');
const filtered = useMemo(() => {
if (statusFilter === 'all') return CONTACTS;
return CONTACTS.filter(c => c.status === statusFilter);
}, [statusFilter]);
return (
<div>
<select value={statusFilter}
onChange={e => setStatusFilter(e.target.value as ContactStatus | 'all')}>
<option value="all">All Statuses</option>
{STATUS_OPTIONS.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
</select>
<Link href="/crm/contacts/new"><Button>Add Contact</Button></Link>
<DataTable
data={filtered} columns={columns} getRowId={row => row.id}
enableSorting enablePagination
globalFilter={globalFilter} onGlobalFilterChange={setGlobalFilter}
searchPlaceholder="Search contacts..."
pageSizeOptions={[10, 25, 50]}
emptyMessage="No contacts found." hoverable
/>
</div>
);
}Copy these 2 files
crud/_data.tsTypes, mock data, CONTACT_STATUS_BADGE_MAP, search helperscrud/contact-list/page.tsxList page with DataTable, globalFilter search, status filterCustomization
- Replace Contact with your entity name and update column definitions
- Map your status enum to StatusBadge variants via a badge map
- Add enableRowSelection for bulk operations
- Use CRUDTable instead of DataTable if you need inline row actions
- Replace the status <select> with SearchFilterBar for complex filtering
Anti-patterns
Wrong
Use raw <table> with manual thead/tbody
Right
Use DataTable from @stackmates/ui-interactive/organisms with enableSorting + enablePagination
Wrong
Hardcode inline status color classes (STATUS_COLORS map)
Right
Use StatusBadge from @stackmates/ui-interactive/atoms with a status mapping
Wrong
Build search from scratch with raw <input type="search">
Right
Use DataTable globalFilter prop for built-in search, or SearchFilterBar for complex filters