| "use client";
|
|
|
| import { useTranslations } from "next-intl";
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| interface DataTableColumn {
|
| key: string;
|
| label: string;
|
| maxWidth?: string;
|
| }
|
|
|
| interface DataTableRow {
|
| id?: string | number;
|
| [key: string]: unknown;
|
| }
|
|
|
| interface DataTableProps {
|
| columns?: DataTableColumn[];
|
| data?: DataTableRow[];
|
| renderCell: (row: DataTableRow, column: DataTableColumn) => React.ReactNode;
|
| renderHeader?: (column: DataTableColumn) => React.ReactNode;
|
| onRowClick?: (row: DataTableRow) => void;
|
| selectedId?: string | number;
|
| loading?: boolean;
|
| maxHeight?: string;
|
| emptyIcon?: string;
|
| emptyMessage?: string;
|
| }
|
|
|
| export default function DataTable({
|
| columns = [],
|
| data = [],
|
| renderCell,
|
| renderHeader,
|
| onRowClick,
|
| selectedId,
|
| loading = false,
|
| maxHeight = "calc(100vh - 320px)",
|
| emptyIcon = "📭",
|
| emptyMessage,
|
| }: DataTableProps) {
|
| const t = useTranslations("common");
|
| const resolvedEmptyMessage = emptyMessage ?? t("noData");
|
|
|
| if (loading) {
|
| return (
|
| <div
|
| style={{
|
| display: "flex",
|
| alignItems: "center",
|
| justifyContent: "center",
|
| padding: "48px 24px",
|
| color: "var(--text-secondary, #888)",
|
| fontSize: "14px",
|
| }}
|
| >
|
| <span style={{ animation: "spin 1s linear infinite", marginRight: "8px" }}>⏳</span>
|
| {t("loading")}
|
| <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
| </div>
|
| );
|
| }
|
|
|
| if (data.length === 0) {
|
| return (
|
| <div
|
| style={{
|
| display: "flex",
|
| flexDirection: "column",
|
| alignItems: "center",
|
| justifyContent: "center",
|
| padding: "48px 24px",
|
| color: "var(--text-secondary, #888)",
|
| fontSize: "14px",
|
| }}
|
| >
|
| <span style={{ fontSize: "32px", marginBottom: "8px", opacity: 0.6 }}>{emptyIcon}</span>
|
| {resolvedEmptyMessage}
|
| </div>
|
| );
|
| }
|
|
|
| return (
|
| <div style={{ overflow: "auto", maxHeight, borderRadius: "8px" }}>
|
| <table
|
| style={{
|
| width: "100%",
|
| borderCollapse: "collapse",
|
| fontSize: "12px",
|
| tableLayout: "auto",
|
| }}
|
| >
|
| <thead>
|
| <tr>
|
| {columns.map((col) => (
|
| <th
|
| key={col.key}
|
| style={{
|
| padding: "8px 10px",
|
| textAlign: "left",
|
| fontWeight: 600,
|
| color: "var(--text-secondary, #888)",
|
| borderBottom: "1px solid rgba(255,255,255,0.08)",
|
| position: "sticky",
|
| top: 0,
|
| background: "var(--bg-table-header, rgba(15,15,25,0.95))",
|
| zIndex: 1,
|
| whiteSpace: "nowrap",
|
| fontSize: "11px",
|
| textTransform: "uppercase",
|
| letterSpacing: "0.5px",
|
| }}
|
| >
|
| {renderHeader ? renderHeader(col) : col.label}
|
| </th>
|
| ))}
|
| </tr>
|
| </thead>
|
| <tbody>
|
| {data.map((row, idx) => (
|
| <tr
|
| key={row.id || idx}
|
| onClick={() => onRowClick?.(row)}
|
| style={{
|
| cursor: onRowClick ? "pointer" : "default",
|
| background:
|
| row.id === selectedId
|
| ? "rgba(99,102,241,0.1)"
|
| : idx % 2 === 0
|
| ? "transparent"
|
| : "rgba(255,255,255,0.02)",
|
| transition: "background 0.15s",
|
| }}
|
| onMouseEnter={(e) => {
|
| if (row.id !== selectedId) {
|
| e.currentTarget.style.background = "rgba(255,255,255,0.04)";
|
| }
|
| }}
|
| onMouseLeave={(e) => {
|
| if (row.id !== selectedId) {
|
| e.currentTarget.style.background =
|
| idx % 2 === 0 ? "transparent" : "rgba(255,255,255,0.02)";
|
| }
|
| }}
|
| >
|
| {columns.map((col) => (
|
| <td
|
| key={col.key}
|
| style={{
|
| padding: "6px 10px",
|
| borderBottom: "1px solid rgba(255,255,255,0.04)",
|
| whiteSpace: "nowrap",
|
| maxWidth: col.maxWidth || "200px",
|
| overflow: "hidden",
|
| textOverflow: "ellipsis",
|
| }}
|
| >
|
| {renderCell(row, col)}
|
| </td>
|
| ))}
|
| </tr>
|
| ))}
|
| </tbody>
|
| </table>
|
| </div>
|
| );
|
| }
|
|
|