components/we-inc/table-with-row-selection-and-bulk-actions.tsx
import { useEffect, useRef, useState } from "react";
const initial = [
{ id: 1, name: "Priya Natarajan", email: "priya@fieldnote.co", plan: "Annual", joined: "12 Mar 2026" },
{ id: 2, name: "Owen Gallagher", email: "owen.g@harbourmail.net", plan: "Monthly", joined: "2 Apr 2026" },
{ id: 3, name: "Sofia Lindqvist", email: "sofia@lindqvist.se", plan: "Free", joined: "19 May 2026" },
{ id: 4, name: "Kwame Asante", email: "kwame@asante.studio", plan: "Annual", joined: "7 Jul 2026" },
{ id: 5, name: "Maud Fischer", email: "maud.fischer@postbox.de", plan: "Monthly", joined: "30 Aug 2026" },
];
export default function TableWithRowSelectionAndBulkActions() {
const [rows, setRows] = useState(initial);
const [picked, setPicked] = useState<Set<number>>(() => new Set([2, 4]));
const all = useRef<HTMLInputElement>(null);
const count = picked.size;
const allOn = count > 0 && count === rows.length;
useEffect(() => {
if (all.current) all.current.indeterminate = count > 0 && !allOn;
}, [count, allOn]);
const flip = (id: number) =>
setPicked((p) => {
const n = new Set(p);
if (n.has(id)) n.delete(id);
else n.add(id);
return n;
});
const archive = () => {
setRows((r) => r.filter((x) => !picked.has(x.id)));
setPicked(new Set());
};
return (
<section className="bg-rose-50/60 px-5 py-14 sm:py-20">
<div className="mx-auto max-w-4xl">
<div className="overflow-hidden rounded-2xl border border-rose-100 bg-white shadow-sm">
<div className="flex min-h-[64px] flex-wrap items-center justify-between gap-3 px-5 py-3" aria-live="polite">
{count === 0 ? (
<p className="text-sm text-slate-600">{rows.length} subscribers. Tick rows to act on them.</p>
) : (
<>
<p className="text-sm font-medium text-slate-900">{count} selected</p>
<div className="flex gap-2">
<button type="button" className="h-10 rounded-lg border border-slate-300 px-3.5 text-sm font-medium text-slate-700 hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-600">Export CSV</button>
<button type="button" onClick={archive} className="h-10 rounded-lg bg-rose-700 px-3.5 text-sm font-medium text-white hover:bg-rose-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-600 focus-visible:ring-offset-2">Archive {count}</button>
</div>
</>
)}
</div>
<div className="overflow-x-auto">
<table className="w-full min-w-[600px] border-collapse text-left text-sm">
<caption className="sr-only">Newsletter subscribers for The Saturday Loaf</caption>
<thead className="border-y border-slate-200 bg-slate-50 text-slate-600">
<tr>
<th scope="col" className="w-12 px-5 py-3">
<input ref={all} type="checkbox" aria-label="Select all subscribers" checked={allOn} onChange={() => setPicked(allOn ? new Set() : new Set(rows.map((r) => r.id)))} className="h-4 w-4 accent-rose-700" />
</th>
<th scope="col" className="px-3 py-3 font-medium">Name</th>
<th scope="col" className="px-3 py-3 font-medium">Email</th>
<th scope="col" className="px-3 py-3 font-medium">Plan</th>
<th scope="col" className="px-5 py-3 font-medium">Joined</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{rows.map((r) => {
const on = picked.has(r.id);
return (
<tr key={r.id} className={on ? "bg-rose-50" : "hover:bg-slate-50"}>
<td className="px-5 py-3.5">
<input type="checkbox" aria-label={`Select ${r.name}`} checked={on} onChange={() => flip(r.id)} className="h-4 w-4 accent-rose-700" />
</td>
<th scope="row" className="px-3 py-3.5 font-medium text-slate-900">{r.name}</th>
<td className="px-3 py-3.5 text-slate-600">{r.email}</td>
<td className="px-3 py-3.5 text-slate-700">{r.plan}</td>
<td className="px-5 py-3.5 text-slate-600">{r.joined}</td>
</tr>
);
})}
{rows.length === 0 && (
<tr><td colSpan={5} className="px-5 py-10 text-center text-slate-600">Everyone is archived. Reload to bring them back.</td></tr>
)}
</tbody>
</table>
</div>
</div>
</div>
</section>
);
}