From 50ee4f3e6a64264401859bed10ab74de5944c534 Mon Sep 17 00:00:00 2001 From: armorbreak001 Date: Tue, 14 Apr 2026 19:09:25 +0800 Subject: [PATCH] feat: generic sortable data table component - Create DataTable component using @tanstack/react-table v8 (headless) - Implement Ascending/Descending column sorting on header click - Build sleek 'No Data Available' empty state with glassmorphism styling - Integrate Tailwind classes matching existing dark theme (stellar-* colors) - Export from components/ui/index.ts - Install @tanstack/react-table dependency Fixes #273 --- frontend/package-lock.json | 34 +++++ frontend/package.json | 1 + frontend/src/components/ui/DataTable.tsx | 154 +++++++++++++++++++++++ frontend/src/components/ui/index.ts | 1 + 4 files changed, 190 insertions(+) create mode 100644 frontend/src/components/ui/DataTable.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 99bc0b2..8f59668 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "@radix-ui/react-slot": "^1.2.4", "@stellar/freighter-api": "^6.0.1", "@stellar/stellar-sdk": "^14.5.0", + "@tanstack/react-table": "^8.21.3", "chart.js": "^4.5.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -2279,6 +2280,39 @@ "node": ">=4" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "http://mirrors.tencentyun.com/npm/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "http://mirrors.tencentyun.com/npm/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5c2f769..6163472 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "@radix-ui/react-slot": "^1.2.4", "@stellar/freighter-api": "^6.0.1", "@stellar/stellar-sdk": "^14.5.0", + "@tanstack/react-table": "^8.21.3", "chart.js": "^4.5.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/frontend/src/components/ui/DataTable.tsx b/frontend/src/components/ui/DataTable.tsx new file mode 100644 index 0000000..28c75a3 --- /dev/null +++ b/frontend/src/components/ui/DataTable.tsx @@ -0,0 +1,154 @@ +'use client'; + +import React, { useState, useMemo } from 'react'; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + flexRender, + type ColumnDef, + type SortingState, +} from '@tanstack/react-table'; +import { cn } from '@/lib/utils'; + +export interface DataTableProps { + data: TData[]; + columns: ColumnDef[]; + className?: string; + emptyMessage?: string; +} + +function SortIcon({ direction }: { direction: 'asc' | 'desc' | false }) { + if (!direction) { + return ( + + + + ); + } + return ( + + + + ); +} + +export function DataTable({ + data, + columns, + className, + emptyMessage = 'No data available', +}: DataTableProps) { + const [sorting, setSorting] = useState([]); + + const table = useReactTable({ + data, + columns, + state: { sorting }, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + }); + + const isEmpty = data.length === 0; + + if (isEmpty) { + return ( +
+
+
📊
+

{emptyMessage}

+
+
+ ); + } + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row, rowIndex) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+
+ {flexRender( + header.column.columnDef.header, + header.getContext(), + )} + {header.column.getIsSorted() !== false && ( + + )} +
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ ); +} + +export default DataTable; diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts index 531166f..527cc4d 100644 --- a/frontend/src/components/ui/index.ts +++ b/frontend/src/components/ui/index.ts @@ -14,3 +14,4 @@ export { DropdownMenuItem, } from "./DropdownMenu"; export { Pagination } from "./Pagination"; +export { DataTable } from "./DataTable";