Skip to content

Commit fa1d329

Browse files
authored
feat: add live updates demo and row grouping demo (jbetancur#1331)
- Introduced LiveUpdatesDemo component for a real-time price grid with flashing rows on price changes. - Added RowGroupingDemo component to showcase grouping of orders by region with expandable rows and aggregate totals. - Updated DocsLayout to include links to new demos and recipes for live updates and row grouping. - Enhanced comparisons documentation to include Material UI Table. - Created new recipes for CSV import, editable inventory grid, and grouped rows with aggregates. - Refactored existing recipe pages to utilize a consistent demo component for better layout and presentation.
1 parent 9513ca0 commit fa1d329

30 files changed

Lines changed: 890 additions & 49 deletions

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ The docs live in `apps/docs/src/pages/docs/`. Each feature has its own `.astro`
5757
- Add new pages to the sidebar in `apps/docs/src/layouts/DocsLayout.astro`.
5858
- If removing a page, add a `[[redirects]]` entry in `netlify.toml` pointing the old URL to its replacement.
5959

60+
**Keeping `apps/docs/public/llms.txt` in sync:**
61+
62+
- Every docs page listed in `DocsLayout.astro`'s sidebar nav should have a matching entry in `llms.txt`, with a one-line factual (non-marketing) description.
63+
- Add/remove/re-describe its `llms.txt` entry in the same change whenever a docs page is added, removed, or its scope changes materially — this applies especially to `comparisons.md`, since claims there go stale fastest.
64+
- Keep descriptions neutral and factual — `llms.txt` is read by LLMs/crawlers, not just humans; slanted copy reads as manipulation and can get the file distrusted or ignored.
65+
6066
---
6167

6268
## Updating CHANGELOG.md

apps/docs/public/llms.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const columns = [
2222
- [Getting Started](https://reactdatatable.com/docs/getting-started): install and render a first table
2323
- [Installation](https://reactdatatable.com/docs/installation): package managers, CSS import, framework setup
2424
- [API Reference](https://reactdatatable.com/docs/api): all props and types
25+
- [Comparisons](https://reactdatatable.com/docs/comparisons): how this compares to TanStack Table, AG Grid, MUI X Data Grid, and the plain MUI Table
2526
- [Migration Guide](https://reactdatatable.com/docs/migration): upgrading from v7 to v8
2627

2728
## Columns
@@ -73,6 +74,10 @@ const columns = [
7374
- [Approval workflow with bulk actions](https://reactdatatable.com/docs/recipes/bulk-action-toolbar)
7475
- [URL-synced table state](https://reactdatatable.com/docs/recipes/editable-grid)
7576
- [Dashboard drill-down](https://reactdatatable.com/docs/recipes/master-detail)
77+
- [Live-updating data grid](https://reactdatatable.com/docs/recipes/live-updates): flash-on-change rows using conditionalRowStyles and keyField
78+
- [Editable inventory grid](https://reactdatatable.com/docs/recipes/inventory-editor): spreadsheet-style inline editing with a derived total column
79+
- [Grouped rows with aggregates](https://reactdatatable.com/docs/recipes/row-groups): per-group totals and drill-down without a dedicated groupBy API
80+
- [CSV import](https://reactdatatable.com/docs/recipes/csv-import): load a user-uploaded CSV file into the table
7681
- [Audit log viewer](https://reactdatatable.com/docs/recipes/sticky-footer)
7782
- [Persist column widths](https://reactdatatable.com/docs/recipes/persist-column-widths)
7883
- [Inline row actions](https://reactdatatable.com/docs/recipes/row-grouping)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React, { useState } from 'react';
2+
import DataTable, { type TableColumn } from 'react-data-table-component';
3+
4+
interface Contact {
5+
name: string;
6+
email: string;
7+
company: string;
8+
}
9+
10+
const SAMPLE_CSV = `name,email,company
11+
Aria Chen,aria@acme.com,Acme Corp
12+
Marcus Webb,marcus@blueharbor.com,Blue Harbor Inc
13+
Priya Kapoor,priya@candlesys.com,Candle Systems`;
14+
15+
// Naive CSV parser: no quoted-comma support. For production, use a proper CSV library.
16+
function parseCsv(text: string): Contact[] {
17+
const [headerLine, ...lines] = text.trim().split('\n');
18+
const headers = headerLine.split(',').map(h => h.trim());
19+
20+
return lines.filter(Boolean).map(line => {
21+
const cells = line.split(',').map(c => c.trim());
22+
return Object.fromEntries(headers.map((h, i) => [h, cells[i] ?? ''])) as unknown as Contact;
23+
});
24+
}
25+
26+
const columns: TableColumn<Contact>[] = [
27+
{ id: 'name', name: 'Name', selector: r => r.name, sortable: true },
28+
{ id: 'email', name: 'Email', selector: r => r.email, sortable: true, grow: 1 },
29+
{ id: 'company', name: 'Company', selector: r => r.company, sortable: true },
30+
];
31+
32+
export default function CsvImportDemo() {
33+
const [data, setData] = useState<Contact[]>([]);
34+
const [error, setError] = useState<string | null>(null);
35+
36+
function handleFile(file: File) {
37+
const reader = new FileReader();
38+
reader.onload = () => {
39+
try {
40+
setData(parseCsv(String(reader.result)));
41+
setError(null);
42+
} catch {
43+
setError('Could not parse that file as CSV.');
44+
}
45+
};
46+
reader.readAsText(file);
47+
}
48+
49+
return (
50+
<div className="space-y-3">
51+
<div className="flex items-center gap-2 flex-wrap">
52+
<label className="px-3 py-1.5 text-xs font-medium rounded-md border border-gray-200 text-gray-600 hover:border-gray-300 cursor-pointer">
53+
Upload CSV file…
54+
<input
55+
type="file"
56+
accept=".csv,text/csv"
57+
className="hidden"
58+
onChange={e => e.target.files?.[0] && handleFile(e.target.files[0])}
59+
/>
60+
</label>
61+
<button
62+
onClick={() => { setData(parseCsv(SAMPLE_CSV)); setError(null); }}
63+
className="px-3 py-1.5 text-xs font-medium rounded-md bg-brand-50 text-brand-700 hover:bg-brand-100"
64+
>
65+
Load sample CSV
66+
</button>
67+
{error && <span className="text-xs text-red-600">{error}</span>}
68+
</div>
69+
<div className="rounded-xl border border-gray-200 overflow-hidden">
70+
<DataTable
71+
columns={columns}
72+
data={data}
73+
highlightOnHover
74+
noDataComponent={<div className="py-8 text-sm text-gray-400">Upload a CSV or load the sample to see it here</div>}
75+
/>
76+
</div>
77+
</div>
78+
);
79+
}

apps/docs/src/components/demos/DashboardDrilldownDemo.tsx

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import DataTable, { type ConditionalStyles, type ExpanderComponentProps, type TableColumn } from 'react-data-table-component';
33

44
interface TeamMember {
@@ -90,20 +90,31 @@ function SpendBar({ budget, spent }: { budget: number; spent: number }) {
9090
);
9191
}
9292

93-
const memberColumns: TableColumn<TeamMember>[] = [
94-
{ name: 'Name', selector: m => m.name, grow: 1 },
95-
{ name: 'Role', selector: m => m.role, grow: 1 },
96-
{ name: 'Open tickets',selector: m => m.tickets, width: '110px', right: true },
97-
{ name: 'Utilization', selector: m => m.utilization, width: '160px',
98-
cell: m => <UtilBar pct={m.utilization} />,
99-
},
100-
];
93+
function initials(name: string) {
94+
return name.split(' ').map(part => part[0]).join('').slice(0, 2).toUpperCase();
95+
}
10196

10297
function DepartmentDetail({ data: dept }: ExpanderComponentProps<Department>) {
10398
return (
10499
<div className="px-8 py-4 bg-gray-50 border-b border-gray-100">
105100
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-3">Team members</p>
106-
<DataTable columns={memberColumns} data={dept.members} dense noHeader />
101+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
102+
{dept.members.map(member => (
103+
<div key={member.name} className="flex items-center gap-3 bg-white border border-gray-200 rounded-lg px-3 py-2.5">
104+
<div className="w-9 h-9 rounded-full bg-brand-100 text-brand-700 text-xs font-semibold flex items-center justify-center shrink-0">
105+
{initials(member.name)}
106+
</div>
107+
<div className="flex-1 min-w-0">
108+
<div className="flex items-center justify-between gap-2">
109+
<p className="text-sm font-medium text-gray-900 truncate">{member.name}</p>
110+
<span className="text-[11px] text-gray-400 shrink-0">{member.tickets} open</span>
111+
</div>
112+
<p className="text-xs text-gray-500 truncate mb-1">{member.role}</p>
113+
<UtilBar pct={member.utilization} />
114+
</div>
115+
</div>
116+
))}
117+
</div>
107118
</div>
108119
);
109120
}
@@ -145,15 +156,26 @@ const columns: TableColumn<Department>[] = [
145156
];
146157

147158
export default function DashboardDrilldownDemo() {
159+
const [allExpanded, setAllExpanded] = useState(false);
160+
148161
return (
149162
<div className="space-y-2">
150-
<p className="text-xs text-gray-400">Expand any row to see team member utilization.</p>
163+
<div className="flex items-center justify-between">
164+
<p className="text-xs text-gray-400">Expand any row to see team member utilization.</p>
165+
<button
166+
onClick={() => setAllExpanded(v => !v)}
167+
className="px-2.5 py-1 text-xs font-medium rounded-md border border-gray-200 text-gray-600 hover:border-gray-300"
168+
>
169+
{allExpanded ? 'Collapse all' : 'Expand all'}
170+
</button>
171+
</div>
151172
<div className="rounded-xl border border-gray-200 overflow-hidden">
152173
<DataTable
153174
columns={columns}
154175
data={departments}
155176
expandableRows
156177
expandableRowsComponent={DepartmentDetail}
178+
expandableRowExpanded={() => allExpanded}
157179
conditionalRowStyles={conditionalRowStyles}
158180
defaultSortFieldId="name"
159181
highlightOnHover
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import React, { useEffect, useState } from 'react';
2+
import DataTable, { type ConditionalStyles, type TableColumn } from 'react-data-table-component';
3+
4+
interface Ticker {
5+
symbol: string;
6+
name: string;
7+
price: number;
8+
change: number;
9+
}
10+
11+
const INITIAL: Ticker[] = [
12+
{ symbol: 'ACME', name: 'Acme Corp', price: 182.4, change: 0 },
13+
{ symbol: 'BLTC', name: 'Baltic Freight', price: 64.12, change: 0 },
14+
{ symbol: 'CNDL', name: 'Candle Systems', price: 305.9, change: 0 },
15+
{ symbol: 'DYNM', name: 'Dynamo Energy', price: 48.77, change: 0 },
16+
{ symbol: 'EVRG', name: 'Evergreen Foods', price: 96.3, change: 0 },
17+
];
18+
19+
const columns: TableColumn<Ticker>[] = [
20+
{ id: 'symbol', name: 'Symbol', selector: r => r.symbol, sortable: true, width: '110px', style: { fontWeight: 600 } },
21+
{ id: 'name', name: 'Name', selector: r => r.name, grow: 1 },
22+
{
23+
id: 'price',
24+
name: 'Price',
25+
selector: r => r.price,
26+
right: true,
27+
width: '110px',
28+
format: r => `$${r.price.toFixed(2)}`,
29+
},
30+
{
31+
id: 'change',
32+
name: 'Change',
33+
selector: r => r.change,
34+
right: true,
35+
width: '110px',
36+
cell: r => (
37+
<span className={r.change === 0 ? 'text-gray-400' : r.change > 0 ? 'text-green-600' : 'text-red-600'}>
38+
{r.change === 0 ? '—' : `${r.change > 0 ? '▲' : '▼'} ${Math.abs(r.change).toFixed(2)}`}
39+
</span>
40+
),
41+
},
42+
];
43+
44+
export default function LiveUpdatesDemo() {
45+
const [data, setData] = useState<Ticker[]>(INITIAL);
46+
const [flashed, setFlashed] = useState<Set<string>>(new Set());
47+
48+
useEffect(() => {
49+
const tick = setInterval(() => {
50+
const symbol = INITIAL[Math.floor(Math.random() * INITIAL.length)].symbol;
51+
const delta = +(Math.random() * 4 - 2).toFixed(2);
52+
53+
setData(prev => prev.map(r => (r.symbol === symbol ? { ...r, price: +(r.price + delta).toFixed(2), change: delta } : r)));
54+
setFlashed(prev => new Set(prev).add(symbol));
55+
setTimeout(() => setFlashed(prev => {
56+
const next = new Set(prev);
57+
next.delete(symbol);
58+
return next;
59+
}), 600);
60+
}, 1200);
61+
62+
return () => clearInterval(tick);
63+
}, []);
64+
65+
const conditionalRowStyles: ConditionalStyles<Ticker>[] = [
66+
{ when: r => flashed.has(r.symbol), style: { backgroundColor: '#fefce8', transition: 'background-color 0.6s ease' } },
67+
];
68+
69+
return (
70+
<div className="rounded-xl border border-gray-200 overflow-hidden">
71+
<DataTable
72+
columns={columns}
73+
data={data}
74+
keyField="symbol"
75+
conditionalRowStyles={conditionalRowStyles}
76+
highlightOnHover
77+
dense
78+
/>
79+
</div>
80+
);
81+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React, { useState } from 'react';
2+
import DataTable, { type ExpanderComponentProps, type TableColumn } from 'react-data-table-component';
3+
4+
interface Order {
5+
id: number;
6+
region: string;
7+
customer: string;
8+
amount: number;
9+
}
10+
11+
interface RegionGroup {
12+
region: string;
13+
count: number;
14+
total: number;
15+
orders: Order[];
16+
}
17+
18+
const ORDERS: Order[] = [
19+
{ id: 1, region: 'West', customer: 'Acme Corp', amount: 4200 },
20+
{ id: 2, region: 'West', customer: 'Blue Harbor Inc', amount: 1850 },
21+
{ id: 3, region: 'West', customer: 'Candle Systems', amount: 3100 },
22+
{ id: 4, region: 'East', customer: 'Dynamo Energy', amount: 2600 },
23+
{ id: 5, region: 'East', customer: 'Evergreen Foods', amount: 5400 },
24+
{ id: 6, region: 'Central', customer: 'Frontier Retail', amount: 990 },
25+
{ id: 7, region: 'Central', customer: 'Granite Works', amount: 2200 },
26+
{ id: 8, region: 'Central', customer: 'Harbor Logistics', amount: 1475 },
27+
{ id: 9, region: 'Central', customer: 'Ironclad Freight', amount: 3050 },
28+
];
29+
30+
// Group flat rows into per-region summary rows — a plain reduce, no groupBy API required.
31+
const groups: RegionGroup[] = Object.values(
32+
ORDERS.reduce<Record<string, RegionGroup>>((acc, order) => {
33+
const g = (acc[order.region] ??= { region: order.region, count: 0, total: 0, orders: [] });
34+
g.count += 1;
35+
g.total += order.amount;
36+
g.orders.push(order);
37+
return acc;
38+
}, {}),
39+
);
40+
41+
const columns: TableColumn<RegionGroup>[] = [
42+
{ id: 'region', name: 'Region', selector: r => r.region, sortable: true, style: { fontWeight: 600 } },
43+
{ id: 'count', name: 'Orders', selector: r => r.count, sortable: true, right: true, width: '110px' },
44+
{
45+
id: 'total',
46+
name: 'Total',
47+
selector: r => r.total,
48+
sortable: true,
49+
right: true,
50+
width: '130px',
51+
format: r => `$${r.total.toLocaleString()}`,
52+
footer: (rows: RegionGroup[]) => `$${rows.reduce((sum, r) => sum + r.total, 0).toLocaleString()}`,
53+
},
54+
];
55+
56+
function RegionOrders({ data }: ExpanderComponentProps<RegionGroup>) {
57+
return (
58+
<div style={{ padding: '10px 32px' }} className="bg-gray-50">
59+
<div className="space-y-1.5">
60+
{data.orders.map(order => (
61+
<div
62+
key={order.id}
63+
className="flex items-center justify-between text-sm bg-white border border-gray-200 rounded-md px-3 py-2"
64+
>
65+
<span className="text-gray-700">{order.customer}</span>
66+
<span className="font-medium text-gray-900">${order.amount.toLocaleString()}</span>
67+
</div>
68+
))}
69+
</div>
70+
</div>
71+
);
72+
}
73+
74+
export default function RowGroupingDemo() {
75+
const [allExpanded, setAllExpanded] = useState(false);
76+
77+
return (
78+
<div className="space-y-2">
79+
<div className="flex justify-end">
80+
<button
81+
onClick={() => setAllExpanded(v => !v)}
82+
className="px-2.5 py-1 text-xs font-medium rounded-md border border-gray-200 text-gray-600 hover:border-gray-300"
83+
>
84+
{allExpanded ? 'Collapse all' : 'Expand all'}
85+
</button>
86+
</div>
87+
<div className="rounded-xl border border-gray-200 overflow-hidden">
88+
<DataTable
89+
columns={columns}
90+
data={groups}
91+
keyField="region"
92+
expandableRows
93+
expandableRowsComponent={RegionOrders}
94+
expandableRowExpanded={() => allExpanded}
95+
highlightOnHover
96+
dense
97+
/>
98+
</div>
99+
</div>
100+
);
101+
}

0 commit comments

Comments
 (0)