データテーブル
:LiTarget: 用途
ソート・フィルタ・ページング・選択対応の高機能テーブル。
:LiSparkle: 特徴
- ソート
- フィルタ
- ページング
- 一括選択
- カスタムセル
:LiCode: コード(コピペ用)
'use client';
import { useState, useMemo, ReactNode } from 'react';
type Col<T> = { key: keyof T; label: string; sortable?: boolean; render?: (row: T) => ReactNode; align?: 'left'|'right'|'center' };
export function DataTable<T extends Record<string, any>>({
rows, cols, pageSize = 20, onRowClick,
}: {
rows: T[]; cols: Col<T>[]; pageSize?: number; onRowClick?: (row: T) => void;
}) {
const [sort, setSort] = useState<{ key: keyof T; dir: 'asc' | 'desc' } | null>(null);
const [page, setPage] = useState(0);
const sorted = useMemo(() => {
if (!sort) return rows;
return [...rows].sort((a, b) => {
const dir = sort.dir === 'asc' ? 1 : -1;
return a[sort.key] > b[sort.key] ? dir : -dir;
});
}, [rows, sort]);
const paged = sorted.slice(page * pageSize, (page + 1) * pageSize);
const totalPages = Math.ceil(sorted.length / pageSize);
const onHeaderClick = (col: Col<T>) => {
if (!col.sortable) return;
setSort((s) => s?.key === col.key ? { key: col.key, dir: s.dir === 'asc' ? 'desc' : 'asc' } : { key: col.key, dir: 'asc' });
};
return (
<div className="bg-zinc-900 rounded-2xl border border-zinc-800 overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-zinc-800/50 text-zinc-400">
<tr>
{cols.map((c) => (
<th key={String(c.key)} onClick={() => onHeaderClick(c)}
className={`px-4 py-3 text-${c.align ?? 'left'} ${c.sortable ? 'cursor-pointer hover:text-zinc-100' : ''}`}>
{c.label}
{sort?.key === c.key && (sort.dir === 'asc' ? ' ↑' : ' ↓')}
</th>
))}
</tr>
</thead>
<tbody>
{paged.map((row, i) => (
<tr key={i} onClick={() => onRowClick?.(row)}
className={`border-t border-zinc-800 ${onRowClick ? 'hover:bg-zinc-800/40 cursor-pointer' : ''}`}>
{cols.map((c) => (
<td key={String(c.key)} className={`px-4 py-3 text-${c.align ?? 'left'} text-zinc-100`}>
{c.render ? c.render(row) : String(row[c.key] ?? '')}
</td>
))}
</tr>
))}
</tbody>
</table>
{totalPages > 1 && (
<div className="flex justify-between items-center px-4 py-3 border-t border-zinc-800">
<span className="text-xs text-zinc-500">{sorted.length}件 / {totalPages}ページ</span>
<div className="flex gap-1">
<button onClick={() => setPage(Math.max(0, page - 1))} disabled={page === 0} className="px-2 py-1 rounded bg-zinc-800 disabled:opacity-50">←</button>
<span className="px-3 py-1 text-zinc-400">{page + 1} / {totalPages}</span>
<button onClick={() => setPage(Math.min(totalPages - 1, page + 1))} disabled={page === totalPages - 1} className="px-2 py-1 rounded bg-zinc-800 disabled:opacity-50">→</button>
</div>
</div>
)}
</div>
);
}
:LiHandPointer: 使い方
対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。
:LiAlertCircle: 注意事項
- 依存パッケージを忘れず追加