DataTable.tsx 2 KB
// Tiny generic table component.
//
// Each column declares: a header label, a key, and an optional
// `render` function that takes the row and returns a ReactNode.
// Without `render` the column reads `row[key]` and stringifies it.
//
// Intentionally not a heavy table lib (TanStack Table, AG Grid).
// v1 SPA needs columnar reads, link cells, status badges, and
// filtering by a free-text input — all trivially handcoded. A
// future chunk can swap this out the day a real consumer asks for
// virtualised rows or column resizing.

import type { ReactNode } from 'react'

export interface Column<T> {
  header: string
  key: string
  render?: (row: T) => ReactNode
  className?: string
}

interface Props<T> {
  rows: T[]
  columns: Column<T>[]
  empty?: ReactNode
  rowKey?: (row: T) => string
}

export function DataTable<T>({
  rows,
  columns,
  empty = <div className="p-6 text-sm text-slate-400">No rows.</div>,
  rowKey,
}: Props<T>) {
  if (rows.length === 0) {
    return <div className="card">{empty}</div>
  }
  return (
    <div className="card overflow-x-auto">
      <table className="table-base">
        <thead className="bg-slate-50">
          <tr>
            {columns.map((c) => (
              <th key={c.key} className={c.className}>
                {c.header}
              </th>
            ))}
          </tr>
        </thead>
        <tbody className="divide-y divide-slate-100">
          {rows.map((row, idx) => {
            const key = rowKey
              ? rowKey(row)
              : ((row as { id?: string }).id ?? String(idx))
            return (
              <tr key={key} className="hover:bg-slate-50">
                {columns.map((c) => (
                  <td key={c.key} className={c.className}>
                    {c.render
                      ? c.render(row)
                      : ((row as unknown as Record<string, unknown>)[c.key] as ReactNode) ?? ''}
                  </td>
                ))}
              </tr>
            )
          })}
        </tbody>
      </table>
    </div>
  )
}