summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--components/client-table/README.md159
-rw-r--r--package-lock.json44
-rw-r--r--package.json3
3 files changed, 206 insertions, 0 deletions
diff --git a/components/client-table/README.md b/components/client-table/README.md
new file mode 100644
index 00000000..053175d4
--- /dev/null
+++ b/components/client-table/README.md
@@ -0,0 +1,159 @@
+# Client Table Components
+
+A set of reusable, virtualized table components for client-side data rendering, built on top of `@tanstack/react-table` and `@tanstack/react-virtual`.
+
+## Features
+
+- **Virtualization**: Efficiently renders large datasets (50,000+ rows) by only rendering visible rows.
+- **Sorting**: Built-in column sorting (Ascending/Descending).
+- **Filtering**:
+ - Global search (all columns).
+ - Column-specific filters: Text (default), Select, Boolean.
+- **Pagination**: Supports both client-side and server-side (manual) pagination.
+- **Column Management**:
+ - **Reordering**: Drag and drop columns to change order.
+ - **Hiding**: Right-click header to hide columns.
+ - **Pinning**: Right-click header to pin columns (Left/Right).
+- **Excel Export**: Export current table view or custom datasets to `.xlsx`.
+- **Excel Import**: Utility to parse Excel files into JSON objects.
+- **Template Generation**: Create Excel templates for users to fill out and import.
+
+## Installation / Usage
+
+The components are located in `@/components/client-table`.
+
+### 1. Basic Usage
+
+```tsx
+import { ClientVirtualTable, ClientTableColumnDef } from "@/components/client-table"
+
+// 1. Define Data Type
+interface User {
+ id: string
+ name: string
+ role: string
+ active: boolean
+}
+
+// 2. Define Columns
+const columns: ClientTableColumnDef<User>[] = [
+ {
+ accessorKey: "name",
+ header: "Name",
+ // Default filter is text
+ },
+ {
+ accessorKey: "role",
+ header: "Role",
+ meta: {
+ filterType: "select",
+ filterOptions: [
+ { label: "Admin", value: "admin" },
+ { label: "User", value: "user" },
+ ]
+ }
+ },
+ {
+ accessorKey: "active",
+ header: "Active",
+ meta: {
+ filterType: "boolean"
+ }
+ }
+]
+
+// 3. Render Component
+export default function UserTable({ data }: { data: User[] }) {
+ return (
+ <ClientVirtualTable
+ data={data}
+ columns={columns}
+ height="600px" // Required for virtualization
+ enableExport={true} // Shows export button
+ enablePagination={true} // Shows pagination footer
+ />
+ )
+}
+```
+
+### 2. Server-Side Pagination
+
+For very large datasets where you don't want to fetch everything at once.
+
+```tsx
+<ClientVirtualTable
+ data={currentData} // Only the current page data
+ columns={columns}
+ manualPagination={true}
+ pageCount={totalPages}
+ rowCount={totalRows}
+ pagination={{ pageIndex, pageSize }}
+ onPaginationChange={setPaginationState}
+ enablePagination={true}
+/>
+```
+
+### 3. Excel Utilities
+
+#### Exporting Data
+
+Automatic export is available via the `enableExport` prop. For custom export logic:
+
+```tsx
+import { exportToExcel } from "@/components/client-table"
+
+await exportToExcel(data, columns, "my-data.xlsx")
+```
+
+#### Creating Import Templates
+
+Generate a blank Excel file with headers for users to fill in.
+
+```tsx
+import { createExcelTemplate } from "@/components/client-table"
+
+await createExcelTemplate({
+ columns,
+ filename: "user-import-template.xlsx",
+ excludeColumns: ["id", "createdAt"], // Columns to skip
+ includeColumns: [{ key: "notes", header: "Notes" }] // Extra columns
+})
+```
+
+#### Importing Data
+
+Parses an uploaded Excel file into a raw JSON array. Does **not** handle validation or DB insertion.
+
+```tsx
+import { importFromExcel } from "@/components/client-table"
+
+const handleFileUpload = async (file: File) => {
+ const { data, errors } = await importFromExcel({
+ file,
+ columnMapping: { "Name": "name", "Role": "role" } // Optional header mapping
+ })
+
+ if (errors.length > 0) {
+ console.error(errors)
+ return
+ }
+
+ // Send `data` to your API/Service for validation and insertion
+ await saveUsers(data)
+}
+```
+
+## Types
+
+We use a custom column definition type to support our extended `meta` properties.
+
+```typescript
+import { ClientTableColumnDef } from "@/components/client-table"
+
+const columns: ClientTableColumnDef<MyData>[] = [ ... ]
+```
+
+Supported `meta` properties:
+
+- `filterType`: `"text" | "select" | "boolean"`
+- `filterOptions`: `{ label: string; value: string }[]` (Required for `select`)
diff --git a/package-lock.json b/package-lock.json
index b95cecae..1423b5df 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -62,6 +62,7 @@
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-hover-card": "^1.1.4",
+ "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-menubar": "^1.1.4",
"@radix-ui/react-navigation-menu": "^1.2.3",
@@ -82,6 +83,7 @@
"@radix-ui/react-toggle-group": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.6",
"@t3-oss/env-nextjs": "^0.11.1",
+ "@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-table": "^8.20.6",
"@tanstack/react-virtual": "^3.13.12",
"@tiptap/extension-blockquote": "^2.23.1",
@@ -142,6 +144,7 @@
"knex": "^3.1.0",
"libphonenumber-js": "^1.12.10",
"lucide-react": "^0.468.0",
+ "match-sorter": "^8.2.0",
"next": "15.1.0",
"next-auth": "^4.24.11",
"next-i18n-router": "^5.5.1",
@@ -4138,6 +4141,15 @@
}
}
},
+ "node_modules/@radix-ui/react-icons": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
+ "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
"node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
@@ -5094,6 +5106,22 @@
}
}
},
+ "node_modules/@tanstack/match-sorter-utils": {
+ "version": "8.19.4",
+ "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz",
+ "integrity": "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==",
+ "license": "MIT",
+ "dependencies": {
+ "remove-accents": "0.5.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
"node_modules/@tanstack/react-table": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
@@ -12622,6 +12650,16 @@
"markdown-it": "bin/markdown-it.mjs"
}
},
+ "node_modules/match-sorter": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-8.2.0.tgz",
+ "integrity": "sha512-qRVB7wYMJXizAWR4TKo5UYwgW7oAVzA8V9jve0wGzRvV91ou9dcqL+/2gJtD0PZ/Pm2Fq6cVT4VHXHmDFVMGRA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.8",
+ "remove-accents": "0.5.0"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -15403,6 +15441,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/remove-accents": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
+ "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==",
+ "license": "MIT"
+ },
"node_modules/request": {
"version": "2.16.6",
"resolved": "https://registry.npmjs.org/request/-/request-2.16.6.tgz",
diff --git a/package.json b/package.json
index 6c9a85a5..2f435dbe 100644
--- a/package.json
+++ b/package.json
@@ -64,6 +64,7 @@
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-hover-card": "^1.1.4",
+ "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-menubar": "^1.1.4",
"@radix-ui/react-navigation-menu": "^1.2.3",
@@ -84,6 +85,7 @@
"@radix-ui/react-toggle-group": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.6",
"@t3-oss/env-nextjs": "^0.11.1",
+ "@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-table": "^8.20.6",
"@tanstack/react-virtual": "^3.13.12",
"@tiptap/extension-blockquote": "^2.23.1",
@@ -144,6 +146,7 @@
"knex": "^3.1.0",
"libphonenumber-js": "^1.12.10",
"lucide-react": "^0.468.0",
+ "match-sorter": "^8.2.0",
"next": "15.1.0",
"next-auth": "^4.24.11",
"next-i18n-router": "^5.5.1",