├── .gitignore ├── .vscode └── settings.json ├── README.md ├── biome.json ├── components.json ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── next.svg ├── vercel.svg └── window.svg ├── src ├── app │ ├── (home) │ │ ├── _components │ │ │ ├── columns.tsx │ │ │ └── examples.tsx │ │ ├── loading.tsx │ │ └── page.tsx │ ├── api │ │ └── fake-data │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ └── layout.tsx ├── components │ ├── enhanced-table │ │ ├── composition-pattern │ │ │ ├── body │ │ │ │ ├── index.tsx │ │ │ │ └── row-editor.tsx │ │ │ ├── filters │ │ │ │ ├── clear.tsx │ │ │ │ ├── hooks │ │ │ │ │ └── use-advanced-filter.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── render-fields.tsx │ │ │ │ ├── types.ts │ │ │ │ ├── utils.tsx │ │ │ │ └── variants │ │ │ │ │ ├── dialog-filter.tsx │ │ │ │ │ ├── sheet-filter.tsx │ │ │ │ │ └── simple-filter.tsx │ │ │ ├── header │ │ │ │ ├── dropdown.tsx │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ ├── pagination.tsx │ │ │ ├── root.tsx │ │ │ ├── toolbar │ │ │ │ ├── expand-collapse.tsx │ │ │ │ ├── export-pdf.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── view-options.tsx │ │ │ ├── types.d.ts │ │ │ └── utils.ts │ │ └── table-context.tsx │ ├── skeletons │ │ └── table.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── date-picker.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── sheet.tsx │ │ ├── skeleton.tsx │ │ ├── table.tsx │ │ └── tabs.tsx ├── hooks │ └── use-media-query.ts ├── lib │ └── utils.ts └── types │ └── @tanstack │ └── react-table.d.ts ├── tailwind.config.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "biomejs.biome", 4 | "editor.codeActionsOnSave": { 5 | "quickfix.biome": "explicit", 6 | "source.organizeImports.biome": "explicit" 7 | }, 8 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This project showcases an enhanced table component built with React, `Shadcn UI`, and `@tanstack/react-table`. It offers a variety of interactive features, including column sorting, row reordering, data fetching, and filtering. The component is designed to be modular and highly customizable for modern web applications. 4 | 5 | ## Features 6 | 7 | - **Export Options**: PDF, CSV 8 | - **Filters**: Advanced and simple filter components (Dialog, Sheet) 9 | - **Reordering**: Column and row reordering (one at a time) 10 | - **Editing** 11 | - **Selection** 12 | - **Subrows** 13 | - **Hide Columns** 14 | - **Sorting** 15 | 16 | ## Preview (gif) 17 | 18 | *Insert preview GIF here* 19 | 20 | ### Full Featured Table 21 | - Expandable rows 22 | - Row selection 23 | - Column reordering 24 | - Editable cells 25 | 26 | ### Sortable Columns Table 27 | - Columns can be sorted 28 | - No row reordering 29 | 30 | ### Reorderable Rows Table 31 | - Rows can be reordered 32 | - No column sorting 33 | 34 | ## Installation 35 | 36 | To run this project locally, follow these steps: 37 | 38 | 1. **Clone the repository:** 39 | ```bash 40 | git clone https://github.com/your-repo/enhanced-table.git 41 | cd enhanced-table 42 | ``` 43 | 2. **Install dependencies:** 44 | ```bash 45 | npm install 46 | # or 47 | yarn install 48 | ``` 49 | 3. **Start the development server:** 50 | ```bash 51 | npm run dev 52 | # or 53 | yarn dev 54 | ``` 55 | 56 | ## Usage 57 | 58 | Ensure that the API endpoint for fetching fake data (`http://localhost:3000/api/fake-data`) is available. Interact with the table using the provided UI controls: 59 | 60 | - **Set Data Count**: Adjust the input field to modify the number of records retrieved. 61 | - **Refresh Data**: Click the "Refresh Data" button to fetch new records. 62 | - **Switch Between Tabs**: Select different table modes using the tabbed interface. 63 | 64 | ## Use Example 65 | 66 | ```tsx 67 | 75 |
76 |
77 | 78 | 79 |
80 |
81 | 82 | 83 | 84 | 85 |
86 |
87 |
88 | 89 | 90 | 91 | 92 |
93 | 94 |
95 | ``` 96 | 97 | ## Custom Row Styles Example 98 | 99 | Rows are styled based on their status: 100 | 101 | - **Active**: Green background 102 | - **Inactive**: Red background 103 | - **Pending**: Yellow background 104 | 105 | Example: 106 | ```tsx 107 | const customRowStyles = (row: Row) => { 108 | const baseStyles = "transition-colors hover:bg-opacity-20"; 109 | const statusStyles = { 110 | active: "hover:bg-green-100 dark:hover:bg-green-900/50", 111 | inactive: "hover:bg-red-100 dark:hover:bg-red-900/50", 112 | pending: "hover:bg-yellow-100 dark:hover:bg-yellow-900/50", 113 | }; 114 | return `${baseStyles} ${statusStyles[row.original.status]}`; 115 | }; 116 | ``` 117 | 118 | ## Code Structure 119 | 120 | - `src/app/(home)/_components/examples.tsx`: Contains the main table component with all functionalities. 121 | - `src/app/(home)/_components/columns.tsx`: Defines the table columns and associated configurations. 122 | - `@/components/enhanced-table`: Includes the core enhanced table component. 123 | 124 | ## Contributing 125 | 126 | Contributions are welcome! Follow these steps to contribute: 127 | 128 | 1. Fork the repository. 129 | 2. Create a new branch: 130 | ```bash 131 | git checkout -b feature-name 132 | ``` 133 | 3. Commit your changes: 134 | ```bash 135 | git commit -m "Add new feature" 136 | ``` 137 | 4. Push to the branch: 138 | ```bash 139 | git push origin feature-name 140 | ``` 141 | 5. Create a Pull Request. 142 | 143 | ## License 144 | 145 | This project is licensed under the MIT License. 146 | 147 | ## Contact 148 | 149 | For any inquiries or support, reach out via [GitHub](https://github.com/drefahl). 150 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", 3 | "organizeImports": { 4 | "enabled": true, 5 | "include": [] 6 | }, 7 | "formatter": { 8 | "enabled": true, 9 | "formatWithErrors": true, 10 | "indentStyle": "space", 11 | "indentWidth": 2, 12 | "lineEnding": "lf", 13 | "lineWidth": 120, 14 | "attributePosition": "auto" 15 | }, 16 | "javascript": { 17 | "formatter": { 18 | "semicolons": "asNeeded" 19 | } 20 | }, 21 | "linter": { 22 | "enabled": true, 23 | "rules": { 24 | "recommended": true, 25 | "suspicious": { 26 | "noExplicitAny": "info", 27 | "noArrayIndexKey": "info" 28 | }, 29 | "correctness": { 30 | "noUnusedImports": "error" 31 | }, 32 | "style": { 33 | "noNonNullAssertion": "info" 34 | } 35 | }, 36 | "ignore": ["node_modules", ".next"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next" 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | images: { 6 | remotePatterns: [{ hostname: "**" }], 7 | }, 8 | } 9 | 10 | export default nextConfig 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tanstack-ui-table", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "tsc -p tsconfig.json --noEmit", 10 | "format": "biome format --write src" 11 | }, 12 | "dependencies": { 13 | "@dnd-kit/core": "^6.3.1", 14 | "@dnd-kit/modifiers": "^9.0.0", 15 | "@dnd-kit/sortable": "^10.0.0", 16 | "@dnd-kit/utilities": "^3.2.2", 17 | "@radix-ui/react-accordion": "^1.2.3", 18 | "@radix-ui/react-checkbox": "^1.1.4", 19 | "@radix-ui/react-dialog": "^1.1.6", 20 | "@radix-ui/react-dropdown-menu": "^2.1.6", 21 | "@radix-ui/react-label": "^2.1.2", 22 | "@radix-ui/react-popover": "^1.1.6", 23 | "@radix-ui/react-scroll-area": "^1.2.3", 24 | "@radix-ui/react-select": "^2.1.6", 25 | "@radix-ui/react-slot": "^1.1.2", 26 | "@radix-ui/react-tabs": "^1.1.3", 27 | "@tanstack/react-table": "^8.21.2", 28 | "class-variance-authority": "^0.7.1", 29 | "clsx": "^2.1.1", 30 | "date-fns": "^4.1.0", 31 | "jspdf": "^3.0.0", 32 | "jspdf-autotable": "^3.8.4", 33 | "lucide-react": "^0.475.0", 34 | "next": "15.1.7", 35 | "next-themes": "^0.4.4", 36 | "react": "^19.0.0", 37 | "react-day-picker": "8.10.1", 38 | "react-dom": "^19.0.0", 39 | "tailwind-merge": "^3.0.1", 40 | "tailwindcss-animate": "^1.0.7", 41 | "vaul": "^1.1.2", 42 | "zod": "^3.24.2" 43 | }, 44 | "devDependencies": { 45 | "@biomejs/biome": "^1.9.4", 46 | "@faker-js/faker": "^9.5.0", 47 | "@types/node": "^20", 48 | "@types/react": "^19", 49 | "@types/react-dom": "^19", 50 | "postcss": "^8", 51 | "tailwindcss": "^3.4.1", 52 | "typescript": "^5" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/(home)/_components/columns.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "@/components/ui/badge" 2 | import type { ColumnDef } from "@tanstack/react-table" 3 | import { format } from "date-fns" 4 | import { Activity, Briefcase, Calendar, TrendingUp, User } from "lucide-react" 5 | 6 | export interface Person { 7 | id: number 8 | rstName: string 9 | lastName: string 10 | age: number 11 | visits: number 12 | status: "active" | "inactive" | "pending" 13 | progress: number 14 | department: "engineering" | "marketing" | "sales" | "design" 15 | createdAt: string 16 | avatar: string 17 | } 18 | 19 | export const columns: ColumnDef[] = [ 20 | { 21 | id: "avatar", 22 | header: "", 23 | accessorKey: "avatar", 24 | cell: ({ row }) => ( 25 | Avatar 26 | ), 27 | filterFn: "filterRows", 28 | meta: { export: { pdf: false } }, 29 | }, 30 | { 31 | id: "firstName", 32 | accessorKey: "firstName", 33 | header: () => ( 34 | 35 | 36 | First Name 37 | 38 | ), 39 | meta: { align: "center", export: { pdf: { header: "First Name" } } }, 40 | filterFn: "filterRows", 41 | }, 42 | { 43 | id: "lastName", 44 | accessorKey: "lastName", 45 | header: "Last Name", 46 | meta: { align: "center" }, 47 | filterFn: "filterRows", 48 | }, 49 | { 50 | id: "age", 51 | accessorKey: "age", 52 | header: "Age", 53 | cell: ({ row }) => {row.getValue("age")}, 54 | meta: { align: "center", export: { pdf: { header: "Age" } } }, 55 | filterFn: "filterRows", 56 | }, 57 | { 58 | id: "visits", 59 | accessorKey: "visits", 60 | header: () => ( 61 | 62 | 63 | Visits 64 | 65 | ), 66 | meta: { align: "center", export: { pdf: { header: "Visits" } } }, 67 | filterFn: "filterRows", 68 | }, 69 | { 70 | id: "status", 71 | accessorKey: "status", 72 | header: "Status", 73 | cell: ({ row }) => ( 74 | 75 | {row.getValue("status")} 76 | 77 | ), 78 | filterFn: "filterRows", 79 | meta: { export: { pdf: { header: "Status" } } }, 80 | }, 81 | { 82 | id: "progress", 83 | accessorKey: "progress", 84 | header: () => ( 85 | 86 | 87 | Progress 88 | 89 | ), 90 | cell: ({ row }) => ( 91 |
92 |
93 |
97 |
98 | {row.getValue("progress")}% 99 |
100 | ), 101 | filterFn: "filterRows", 102 | meta: { export: { pdf: { header: "Progress" } } }, 103 | }, 104 | { 105 | id: "department", 106 | accessorKey: "department", 107 | header: () => ( 108 | 109 | 110 | Department 111 | 112 | ), 113 | cell: ({ row }) => { 114 | const department = row.getValue("department") as string 115 | const iconClass = "w-5 h-5 mr-2" 116 | const icons = { 117 | engineering: , 118 | marketing: , 119 | sales: , 120 | design: , 121 | } 122 | return ( 123 |
124 | {icons[department as keyof typeof icons]} 125 | {department} 126 |
127 | ) 128 | }, 129 | filterFn: "filterRows", 130 | meta: { export: { pdf: { header: "Department" } } }, 131 | }, 132 | { 133 | id: "createdAt", 134 | accessorKey: "createdAt", 135 | header: () => ( 136 | 137 | 138 | Created At 139 | 140 | ), 141 | accessorFn: (row) => format(new Date(row.createdAt), "MM/dd/yyyy"), 142 | cell: ({ row }) => {row.getValue("createdAt")}, 143 | filterFn: "filterRows", 144 | meta: { export: { pdf: { header: "Created At" } } }, 145 | }, 146 | ] 147 | -------------------------------------------------------------------------------- /src/app/(home)/_components/examples.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { EnhancedTable } from "@/components/enhanced-table/composition-pattern" 4 | import { Button } from "@/components/ui/button" 5 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" 6 | import { Input } from "@/components/ui/input" 7 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" 8 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" 9 | import type { Row } from "@tanstack/react-table" 10 | import { useState } from "react" 11 | import TableSkeleton from "../loading" 12 | import { type Person, columns } from "./columns" 13 | 14 | interface ExamplesProps { 15 | initialData: Person[] 16 | } 17 | 18 | export function Examples({ initialData }: ExamplesProps) { 19 | const [data, setData] = useState(initialData) 20 | const [dataCount, setDataCount] = useState(1000) 21 | const [headerVariant, setHeaderVariant] = useState<"default" | "dropdown">("default") 22 | const [isLoading, setIsLoading] = useState(false) 23 | 24 | async function fetchData() { 25 | setIsLoading(true) 26 | const newData = await fetch(`http://localhost:3000/api/fake-data?count=${dataCount}`, { 27 | cache: "no-cache", 28 | }).then((res) => res.json()) 29 | setData(newData) 30 | setIsLoading(false) 31 | } 32 | 33 | return ( 34 |
35 |
36 | setDataCount(Number(e.target.value))} 40 | className="w-32" 41 | /> 42 | 43 | 46 |
47 | 48 | {!isLoading ? ( 49 | 50 | 51 | Full Featured 52 | Sortable Columns 53 | Reorderable Rows 54 | 55 | 56 | 57 | 58 | Full Featured Table 59 | 60 | This example showcases all available features of the EnhancedTable component. 61 | 62 | 63 | 64 | 72 |
73 |
74 | 75 | 76 | 88 |
89 |
90 | 91 | 92 | 93 | 94 |
95 |
96 |
97 | 98 | 99 | 100 | 101 |
102 | 103 |
104 |
105 |
106 |
107 | 108 | 109 | 110 | Sortable Columns Table 111 | 112 | This example demonstrates a table with sortable columns without row reordering. 113 | 114 | 115 | 116 | 117 |
118 | 119 |
120 |
121 | 122 | 123 | 124 | 125 |
126 | 127 |
128 |
129 |
130 |
131 | 132 | 133 | 134 | Reorderable Rows Table 135 | 136 | This example shows a table with reorderable rows without column sorting. 137 | 138 | 139 | 140 | 141 |
142 | 143 |
144 |
145 | 146 | 147 | 148 | 149 |
150 | 151 |
152 |
153 |
154 |
155 |
156 | ) : ( 157 | 158 | )} 159 |
160 | ) 161 | } 162 | 163 | const customRowStyles = (row: Row) => { 164 | const baseStyles = "transition-colors hover:bg-opacity-20" 165 | const statusStyles = { 166 | active: "hover:bg-green-100 dark:hover:bg-green-900/50", 167 | inactive: "hover:bg-red-100 dark:hover:bg-red-900/50", 168 | pending: "hover:bg-yellow-100 dark:hover:bg-yellow-900/50", 169 | } 170 | 171 | return `${baseStyles} ${statusStyles[row.original.status]}` 172 | } 173 | -------------------------------------------------------------------------------- /src/app/(home)/loading.tsx: -------------------------------------------------------------------------------- 1 | import { TableSkeleton } from "@/components/skeletons/table" 2 | 3 | export default async function Loading() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import { Examples } from "./_components/examples" 2 | 3 | export default async function EnhancedTableExamples() { 4 | const initialData = await fetch("http://localhost:3000/api/fake-data?count=1000").then((res) => res.json()) 5 | 6 | return 7 | } 8 | 9 | export const dynamic = "force-dynamic" 10 | -------------------------------------------------------------------------------- /src/app/api/fake-data/route.ts: -------------------------------------------------------------------------------- 1 | import { faker } from "@faker-js/faker" 2 | import { NextResponse } from "next/server" 3 | 4 | export async function GET(request: Request) { 5 | const { searchParams } = new URL(request.url) 6 | const count = Number.parseInt(searchParams.get("count") || "100", 10) 7 | const statuses = ["active", "inactive", "pending"] 8 | const departments = ["engineering", "marketing", "sales", "design"] 9 | 10 | const generateSubRows = (count: number, parentId: number, level: number): any[] => { 11 | if (level > 2) return [] 12 | return Array.from({ length: count }, (_, j) => { 13 | const newId = parentId * 100 + j + 1 14 | return { 15 | id: newId, 16 | firstName: faker.person.firstName(), 17 | lastName: faker.person.lastName(), 18 | age: faker.number.int({ min: 20, max: 70 }), 19 | visits: faker.number.int({ min: 0, max: 100 }), 20 | status: faker.helpers.arrayElement(statuses), 21 | progress: faker.number.int({ min: 0, max: 100 }), 22 | department: faker.helpers.arrayElement(departments), 23 | createdAt: faker.date.past().toISOString(), 24 | subRows: generateSubRows(2, newId, level + 1), 25 | avatar: faker.image.avatar(), 26 | } 27 | }) 28 | } 29 | 30 | const data = Array.from({ length: count }, (_, i) => { 31 | const id = i + 1 32 | return { 33 | id, 34 | firstName: faker.person.firstName(), 35 | lastName: faker.person.lastName(), 36 | age: faker.number.int({ min: 20, max: 70 }), 37 | visits: faker.number.int({ min: 0, max: 100 }), 38 | status: faker.helpers.arrayElement(statuses), 39 | progress: faker.number.int({ min: 0, max: 100 }), 40 | department: faker.helpers.arrayElement(departments), 41 | createdAt: faker.date.past().toISOString(), 42 | subRows: generateSubRows(2, id, 1), 43 | avatar: faker.image.avatar(), 44 | } 45 | }) 46 | 47 | return NextResponse.json(data) 48 | } 49 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drefahl/tanstack-ui-table/5306d993198b67f9900ca8c1997fdc88a18ec54b/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 240 10% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 240 10% 3.9%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 240 10% 3.9%; 17 | --primary: 240 5.9% 10%; 18 | --primary-foreground: 0 0% 98%; 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | --muted: 240 4.8% 95.9%; 22 | --muted-foreground: 240 3.8% 46.1%; 23 | --accent: 240 4.8% 95.9%; 24 | --accent-foreground: 240 5.9% 10%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 0 0% 98%; 27 | --border: 240 5.9% 90%; 28 | --input: 240 5.9% 90%; 29 | --ring: 240 10% 3.9%; 30 | --chart-1: 12 76% 61%; 31 | --chart-2: 173 58% 39%; 32 | --chart-3: 197 37% 24%; 33 | --chart-4: 43 74% 66%; 34 | --chart-5: 27 87% 67%; 35 | --radius: 0.5rem; 36 | --sidebar-background: 0 0% 98%; 37 | --sidebar-foreground: 240 5.3% 26.1%; 38 | --sidebar-primary: 240 5.9% 10%; 39 | --sidebar-primary-foreground: 0 0% 98%; 40 | --sidebar-accent: 240 4.8% 95.9%; 41 | --sidebar-accent-foreground: 240 5.9% 10%; 42 | --sidebar-border: 220 13% 91%; 43 | --sidebar-ring: 217.2 91.2% 59.8%; 44 | } 45 | .dark { 46 | --background: 240 10% 3.9%; 47 | --foreground: 0 0% 98%; 48 | --card: 240 10% 3.9%; 49 | --card-foreground: 0 0% 98%; 50 | --popover: 240 10% 3.9%; 51 | --popover-foreground: 0 0% 98%; 52 | --primary: 0 0% 98%; 53 | --primary-foreground: 240 5.9% 10%; 54 | --secondary: 240 3.7% 15.9%; 55 | --secondary-foreground: 0 0% 98%; 56 | --muted: 240 3.7% 15.9%; 57 | --muted-foreground: 240 5% 64.9%; 58 | --accent: 240 3.7% 15.9%; 59 | --accent-foreground: 0 0% 98%; 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | --border: 240 3.7% 15.9%; 63 | --input: 240 3.7% 15.9%; 64 | --ring: 240 4.9% 83.9%; 65 | --chart-1: 220 70% 50%; 66 | --chart-2: 160 60% 45%; 67 | --chart-3: 30 80% 55%; 68 | --chart-4: 280 65% 60%; 69 | --chart-5: 340 75% 55%; 70 | --sidebar-background: 240 5.9% 10%; 71 | --sidebar-foreground: 240 4.8% 95.9%; 72 | --sidebar-primary: 224.3 76.3% 48%; 73 | --sidebar-primary-foreground: 0 0% 100%; 74 | --sidebar-accent: 240 3.7% 15.9%; 75 | --sidebar-accent-foreground: 240 4.8% 95.9%; 76 | --sidebar-border: 240 3.7% 15.9%; 77 | --sidebar-ring: 217.2 91.2% 59.8%; 78 | } 79 | } 80 | 81 | @layer base { 82 | * { 83 | @apply border-border; 84 | } 85 | body { 86 | @apply bg-background text-foreground; 87 | } 88 | } 89 | 90 | @layer base { 91 | * { 92 | @apply border-border outline-ring/50; 93 | } 94 | body { 95 | @apply bg-background text-foreground; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next" 2 | import { Geist, Geist_Mono } from "next/font/google" 3 | import "./globals.css" 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }) 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }) 14 | 15 | export const metadata: Metadata = { 16 | title: "Create Next App", 17 | description: "Generated by create next app", 18 | } 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode 24 | }>) { 25 | return ( 26 | 27 | {children} 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/body/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { TableCell, TableRow, TableBody as UiTableBody } from "@/components/ui/table" 3 | import { cn } from "@/lib/utils" 4 | import { 5 | SortableContext, 6 | horizontalListSortingStrategy, 7 | useSortable, 8 | verticalListSortingStrategy, 9 | } from "@dnd-kit/sortable" 10 | import { CSS } from "@dnd-kit/utilities" 11 | import { type Cell, type Row, flexRender } from "@tanstack/react-table" 12 | import { Edit2 } from "lucide-react" 13 | import React, { type CSSProperties, useState } from "react" 14 | import { useTableContext } from "../../table-context" 15 | import { getAlignment } from "../utils" 16 | import { TableRowEditor } from "./row-editor" 17 | 18 | interface TableBodyProps { 19 | customRowStyles?: (row: Row) => string 20 | } 21 | 22 | export function TableBody({ customRowStyles }: TableBodyProps) { 23 | const [editingRowId, setEditingRowId] = useState(null) 24 | 25 | const { table, updateData, enableEditing, columnOrder, enableColumnReorder, enableRowReorder, dataIds } = 26 | useTableContext() 27 | 28 | const handleEdit = (row: Row) => { 29 | setEditingRowId(row.id) 30 | } 31 | 32 | const handleSave = (rowIndex: number, updatedData: any) => { 33 | updateData(rowIndex, updatedData) 34 | setEditingRowId(null) 35 | } 36 | 37 | const handleCancel = () => { 38 | setEditingRowId(null) 39 | } 40 | 41 | const renderRow = (row: Row) => { 42 | const rowStyle = customRowStyles ? customRowStyles(row) : "" 43 | const depth = row.depth || 0 44 | 45 | if (editingRowId === row.id) { 46 | return ( 47 | 48 | 49 | 50 | ) 51 | } 52 | 53 | return ( 54 | 55 | 59 | {enableColumnReorder && columnOrder ? ( 60 | 61 | {row.getVisibleCells().map((cell, cellIndex) => ( 62 | 71 | ))} 72 | 73 | ) : enableRowReorder && dataIds ? ( 74 | 75 | {row.getVisibleCells().map((cell, cellIndex) => ( 76 | 86 | ))} 87 | 88 | ) : ( 89 | row.getVisibleCells().map((cell, cellIndex) => ( 90 | 95 | 103 | 104 | )) 105 | )} 106 | 107 | 108 | ) 109 | } 110 | 111 | return ( 112 | 113 | {table.getRowModel().rows?.length ? ( 114 | table.getRowModel().rows.map((row) => renderRow(row)) 115 | ) : ( 116 | 117 | 118 | No results. 119 | 120 | 121 | )} 122 | 123 | ) 124 | } 125 | 126 | interface DraggableTableCellProps { 127 | cell: Cell 128 | cellIndex: number 129 | depth: number 130 | handleEdit: (row: Row) => void 131 | enableEditing: boolean | undefined 132 | row: Row 133 | } 134 | 135 | function DraggableTableCell({ cell, cellIndex, depth, handleEdit, enableEditing, row }: DraggableTableCellProps) { 136 | const { isDragging, setNodeRef, transform } = useSortable({ 137 | id: cell.column.id, 138 | }) 139 | 140 | const style: CSSProperties = { 141 | opacity: isDragging ? 0.8 : 1, 142 | position: "relative", 143 | transform: transform ? CSS.Translate.toString(transform) : undefined, 144 | transition: "width transform 0.2s ease-in-out", 145 | width: cell.column.getSize(), 146 | zIndex: isDragging ? 1 : 0, 147 | } 148 | 149 | return ( 150 | 156 | 164 | 165 | ) 166 | } 167 | 168 | interface DraggableRowProps { 169 | cell: Cell 170 | id: string 171 | cellIndex: number 172 | depth: number 173 | handleEdit: (row: Row) => void 174 | enableEditing: boolean | undefined 175 | row: Row 176 | } 177 | 178 | function DraggableRow({ cell, id, cellIndex, depth, enableEditing, handleEdit, row }: DraggableRowProps) { 179 | const { transform, transition, setNodeRef, isDragging } = useSortable({ 180 | id, 181 | }) 182 | 183 | const style: CSSProperties = { 184 | transform: CSS.Transform.toString(transform), 185 | transition: transition, 186 | opacity: isDragging ? 0.8 : 1, 187 | zIndex: isDragging ? 1 : 0, 188 | position: "relative", 189 | width: cell.column.getSize(), 190 | } 191 | 192 | return ( 193 | 199 | 207 | 208 | ) 209 | } 210 | 211 | interface DefaultTableCellProps { 212 | cell: Cell 213 | cellIndex: number 214 | depth: number 215 | handleEdit: (row: Row) => void 216 | enableEditing: boolean | undefined 217 | row: Row 218 | } 219 | 220 | export function DefaultTableCell({ cell, cellIndex, depth, handleEdit, enableEditing, row }: DefaultTableCellProps) { 221 | return ( 222 | <> 223 | {cellIndex === 0 ? ( 224 |
225 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 226 | 227 | {enableEditing && ( 228 | 231 | )} 232 |
233 | ) : ( 234 | flexRender(cell.column.columnDef.cell, cell.getContext()) 235 | )} 236 | 237 | ) 238 | } 239 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/body/row-editor.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { Input } from "@/components/ui/input" 3 | import { TableCell } from "@/components/ui/table" 4 | import type { Row } from "@tanstack/react-table" 5 | import { useState } from "react" 6 | import { isSpecialId } from "../utils" 7 | 8 | interface TableRowEditorProps { 9 | row: Row 10 | onSave: (rowIndex: number, updatedData: TData) => void 11 | onCancel: () => void 12 | } 13 | 14 | export function TableRowEditor({ row, onSave, onCancel }: TableRowEditorProps) { 15 | const [editedData, setEditedData] = useState>({}) 16 | 17 | const handleInputChange = (columnId: string, value: any) => { 18 | setEditedData((prev) => ({ ...prev, [columnId]: value })) 19 | } 20 | 21 | const handleSave = () => { 22 | onSave(row.index, { ...row.original, ...editedData } as TData) 23 | } 24 | 25 | return ( 26 | <> 27 | {row.getVisibleCells().map((cell) => { 28 | const column = cell.column 29 | if (isSpecialId(column.id)) return 30 | 31 | return ( 32 | 33 | handleInputChange(column.id, e.target.value)} 36 | className="h-8 text-sm" 37 | /> 38 | 39 | ) 40 | })} 41 | 42 | 43 |
44 | 47 | 50 |
51 |
52 | 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/filters/clear.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { SearchX } from "lucide-react" 3 | import { useTableContext } from "../../table-context" 4 | import { useDialogs } from "./hooks/use-advanced-filter" 5 | 6 | export function Clear() { 7 | const { table } = useTableContext() 8 | const { resetFilters } = useDialogs({ table }) 9 | 10 | const isFiltered = table.getState().columnFilters.length > 0 11 | if (!isFiltered) return null 12 | 13 | return ( 14 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/filters/hooks/use-advanced-filter.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import type { Table } from "@tanstack/react-table" 4 | import { useSearchParams } from "next/navigation" 5 | import * as React from "react" 6 | import type { ColumnFilter, FilterType } from "../types" 7 | import { getColumnFilterTypesByValue } from "../utils" 8 | 9 | interface UseDialogsProps { 10 | table: Table 11 | } 12 | 13 | export function useDialogs({ table }: UseDialogsProps) { 14 | const [filters, setFilters] = React.useState([]) 15 | 16 | const searchParams = useSearchParams() 17 | 18 | React.useEffect(() => { 19 | const urlFilters = JSON.parse(searchParams.get("filters") || "[]") 20 | setFilters(urlFilters) 21 | }, [searchParams]) 22 | 23 | const handleChangeSearchParams = React.useCallback( 24 | (filters: ColumnFilter[]) => { 25 | const queryString = new URLSearchParams(searchParams) 26 | queryString.set("filters", JSON.stringify(filters)) 27 | window.history.replaceState({}, "", `${window.location.pathname}?${queryString}`) 28 | }, 29 | [searchParams], 30 | ) 31 | 32 | const applyFilters = React.useCallback(() => { 33 | for (const filter of filters) { 34 | const column = table.getColumn(filter.id) 35 | if (column) { 36 | column.setFilterValue({ value: filter.value, type: filter.type }) 37 | } 38 | } 39 | handleChangeSearchParams(filters) 40 | }, [filters, handleChangeSearchParams, table]) 41 | 42 | const resetFilters = React.useCallback(() => { 43 | table.resetColumnFilters() 44 | setFilters([]) 45 | handleChangeSearchParams([]) 46 | }, [table, handleChangeSearchParams]) 47 | 48 | const updateFilterValue = React.useCallback( 49 | (columnId: string, value: any) => { 50 | setFilters((prev) => { 51 | const existingFilter = prev.find((f) => f.id === columnId) 52 | if (existingFilter) return prev.map((f) => (f.id === columnId ? { ...f, value } : f)) 53 | 54 | const sampleValue = table.getPrePaginationRowModel().rows?.[0]?.getValue(columnId) 55 | const availableFilterTypes = getColumnFilterTypesByValue(sampleValue) 56 | const defaultType = availableFilterTypes[0] 57 | 58 | return [...prev, { id: columnId, type: defaultType, value }] 59 | }) 60 | }, 61 | [table], 62 | ) 63 | 64 | const updateFilterType = React.useCallback((columnId: string, type: FilterType) => { 65 | setFilters((prev) => { 66 | const existingFilter = prev.find((f) => f.id === columnId) 67 | if (existingFilter) { 68 | return prev.map((f) => (f.id === columnId ? { ...f, type, value: "" } : f)) 69 | } 70 | 71 | return [...prev, { id: columnId, type, value: "" }] 72 | }) 73 | }, []) 74 | 75 | const getCurrentFilter = React.useCallback( 76 | (columnId: string, defaultType: FilterType) => { 77 | const foundFilter = filters.find((f) => f.id === columnId) 78 | if (foundFilter) return foundFilter 79 | return { id: columnId, value: "", type: defaultType } 80 | }, 81 | [filters], 82 | ) 83 | 84 | return { 85 | filters, 86 | setFilters, 87 | applyFilters, 88 | resetFilters, 89 | updateFilterValue, 90 | updateFilterType, 91 | getCurrentFilter, 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/filters/index.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic" 2 | 3 | const Simple = dynamic(() => import("./variants/simple-filter").then((mod) => mod.Simple), { ssr: false }) 4 | const Sheet = dynamic(() => import("./variants/sheet-filter").then((mod) => mod.Sheet), { ssr: false }) 5 | const Dialog = dynamic(() => import("./variants/dialog-filter").then((mod) => mod.Dialog), { ssr: false }) 6 | const Clear = dynamic(() => import("./clear").then((mod) => mod.Clear), { ssr: false }) 7 | 8 | export const TableFilters = { 9 | Simple, 10 | Sheet, 11 | Dialog, 12 | Clear, 13 | } 14 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/filters/render-fields.tsx: -------------------------------------------------------------------------------- 1 | import { DatePicker } from "@/components/ui/date-picker" 2 | import { Input } from "@/components/ui/input" 3 | import { Label } from "@/components/ui/label" 4 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" 5 | import type { Column, Table } from "@tanstack/react-table" 6 | import type React from "react" 7 | import { type ColumnFilter, type FilterType, filterTypes } from "./types" 8 | import { detectValueType, getColumnFilterTypesByValue } from "./utils" 9 | 10 | type UpdateFilterValueFn = (columnId: string, value: any) => void 11 | 12 | function renderFilterInput( 13 | table: Table, 14 | column: Column, 15 | filter: ColumnFilter, 16 | updateFilterValue: UpdateFilterValueFn, 17 | ) { 18 | const firstValue = table.getPreFilteredRowModel().flatRows[0]?.getValue(column.id) 19 | const type = detectValueType(firstValue) 20 | 21 | console.log("type", type, firstValue) 22 | 23 | switch (type) { 24 | case "number": 25 | if (filter.type === "between") { 26 | return ( 27 |
28 | 32 | updateFilterValue(column.id, [ 33 | e.target.value === "" ? null : Number(e.target.value), 34 | (filter.value as [number, number])?.[1], 35 | ]) 36 | } 37 | className="w-[100px]" 38 | /> 39 | 40 | 44 | updateFilterValue(column.id, [ 45 | (filter.value as [number, number])?.[0], 46 | e.target.value === "" ? null : Number(e.target.value), 47 | ]) 48 | } 49 | className="w-[100px]" 50 | /> 51 |
52 | ) 53 | } 54 | return ( 55 | updateFilterValue(column.id, e.target.value === "" ? null : Number(e.target.value))} 59 | className="w-[200px]" 60 | /> 61 | ) 62 | 63 | case "date": 64 | if (filter.type === "dateBetween") { 65 | return ( 66 |
67 | updateFilterValue(column.id, [date, (filter.value as [Date, Date])?.[1]])} 70 | /> 71 | 72 | updateFilterValue(column.id, [(filter.value as [Date, Date])?.[0], date])} 75 | /> 76 |
77 | ) 78 | } 79 | return updateFilterValue(column.id, date)} /> 80 | 81 | default: 82 | return ( 83 | updateFilterValue(column.id, e.target.value)} 86 | className="w-[200px]" 87 | /> 88 | ) 89 | } 90 | } 91 | 92 | interface RenderFiltersProps { 93 | table: Table 94 | filters: ColumnFilter[] 95 | setFilters: React.Dispatch> 96 | updateFilterValue: UpdateFilterValueFn 97 | } 98 | 99 | export function RenderFilters({ table, filters, setFilters, updateFilterValue }: RenderFiltersProps) { 100 | const firstRow = table.getPrePaginationRowModel().rows?.[0] 101 | 102 | return ( 103 |
104 | {table 105 | .getAllColumns() 106 | .filter((column) => column.getCanFilter()) 107 | .map((column) => { 108 | const sampleValue = firstRow ? firstRow.getValue(column.id) : null 109 | const filter = filters.find((f) => f.id === column.id) 110 | 111 | const availableFilterTypes = getColumnFilterTypesByValue(sampleValue, filter?.type) 112 | 113 | const currentFilter = filters.find((f) => f.id === column.id) || { 114 | id: column.id, 115 | value: "", 116 | type: availableFilterTypes[0] ?? "contains", 117 | } 118 | 119 | return ( 120 |
121 | 122 | 123 |
124 | 149 | 150 | {renderFilterInput(table, column, currentFilter, updateFilterValue)} 151 |
152 |
153 | ) 154 | })} 155 |
156 | ) 157 | } 158 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/filters/types.ts: -------------------------------------------------------------------------------- 1 | export type FilterType = 2 | | "contains" 3 | | "equals" 4 | | "startsWith" 5 | | "endsWith" 6 | | "greaterThan" 7 | | "lessThan" 8 | | "greaterThanOrEqual" 9 | | "lessThanOrEqual" 10 | | "between" 11 | | "dateEquals" 12 | | "dateBefore" 13 | | "dateAfter" 14 | | "dateBetween" 15 | 16 | export interface ColumnFilter { 17 | id: string 18 | value: string | number | [string, string] | [number, number] | Date | [Date, Date] 19 | type: FilterType 20 | } 21 | 22 | export const filterTypes: { value: FilterType; label: string }[] = [ 23 | { value: "contains", label: "Contains" }, 24 | { value: "equals", label: "Equals" }, 25 | { value: "startsWith", label: "Starts with" }, 26 | { value: "endsWith", label: "Ends with" }, 27 | { value: "greaterThan", label: "Greater than" }, 28 | { value: "lessThan", label: "Less than" }, 29 | { value: "greaterThanOrEqual", label: "Greater than or equal to" }, 30 | { value: "lessThanOrEqual", label: "Less than or equal to" }, 31 | { value: "between", label: "Between" }, 32 | { value: "dateEquals", label: "Date equals" }, 33 | { value: "dateBefore", label: "Date before" }, 34 | { value: "dateAfter", label: "Date after" }, 35 | { value: "dateBetween", label: "Date between" }, 36 | ] 37 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/filters/utils.tsx: -------------------------------------------------------------------------------- 1 | import type { FilterFn, Row } from "@tanstack/react-table" 2 | import type { FilterType } from "./types" 3 | 4 | export const filterRows: FilterFn = ( 5 | row: Row, 6 | columnId: string, 7 | filterValue: { value: any; type: FilterType }, 8 | ): boolean => { 9 | const cellValue = row.getValue(columnId) 10 | 11 | switch (filterValue.type) { 12 | // String filters 13 | case "contains": 14 | return cellValue?.toString().toLowerCase().includes(filterValue.value?.toString().toLowerCase()) ?? true 15 | case "equals": 16 | return cellValue?.toString() === filterValue.value?.toString() 17 | case "startsWith": 18 | return cellValue?.toString().toLowerCase().startsWith(filterValue.value?.toString().toLowerCase()) ?? false 19 | case "endsWith": 20 | return cellValue?.toString().toLowerCase().endsWith(filterValue.value?.toString().toLowerCase()) ?? false 21 | 22 | // Number 23 | case "greaterThan": 24 | return Number(cellValue) > Number(filterValue.value) 25 | case "lessThan": 26 | return Number(cellValue) < Number(filterValue.value) 27 | case "greaterThanOrEqual": 28 | return Number(cellValue) >= Number(filterValue.value) 29 | case "lessThanOrEqual": 30 | return Number(cellValue) <= Number(filterValue.value) 31 | case "between": { 32 | const [min, max] = filterValue.value || [] 33 | return Number(cellValue) >= Number(min) && Number(cellValue) <= Number(max) 34 | } 35 | 36 | // Date 37 | case "dateEquals": 38 | return new Date(cellValue as Date).getTime() === new Date(filterValue.value).getTime() 39 | case "dateBefore": 40 | return new Date(cellValue as Date).getTime() < new Date(filterValue.value).getTime() 41 | case "dateAfter": 42 | return new Date(cellValue as Date).getTime() > new Date(filterValue.value).getTime() 43 | case "dateBetween": { 44 | const [start, end] = filterValue.value || [] 45 | const cellTime = new Date(cellValue as Date).getTime() 46 | return cellTime >= new Date(start).getTime() && cellTime <= new Date(end).getTime() 47 | } 48 | 49 | default: 50 | return true 51 | } 52 | } 53 | 54 | export type DetectedValueType = "number" | "date" | "string" 55 | 56 | export function detectValueType(value: unknown): DetectedValueType { 57 | if (value == null) return "string" 58 | 59 | if (typeof value === "number") { 60 | return "number" 61 | } 62 | 63 | if (typeof value === "string") { 64 | if (!Number.isNaN(Number(value)) && value.trim() !== "") { 65 | return "number" 66 | } 67 | 68 | if (value.includes("-") || value.includes("/")) { 69 | const parsed = new Date(value) 70 | if (!Number.isNaN(parsed.getTime())) { 71 | return "date" 72 | } 73 | } 74 | } 75 | 76 | if (value instanceof Date && !Number.isNaN(value.getTime())) { 77 | return "date" 78 | } 79 | 80 | return "string" 81 | } 82 | 83 | export function getColumnFilterTypesByValue(value: unknown, currentFilterType?: FilterType): FilterType[] { 84 | let type = detectValueType(value) 85 | 86 | if (value == null && currentFilterType) { 87 | type = getTypeFromFilterType(currentFilterType) 88 | } 89 | 90 | switch (type) { 91 | case "number": 92 | return ["equals", "greaterThan", "lessThan", "greaterThanOrEqual", "lessThanOrEqual", "between"] 93 | case "date": 94 | return ["dateEquals", "dateBefore", "dateAfter", "dateBetween"] 95 | default: 96 | return ["contains", "equals", "startsWith", "endsWith"] 97 | } 98 | } 99 | 100 | function getTypeFromFilterType(filterType: FilterType): DetectedValueType { 101 | if (["between", "greaterThan", "lessThan", "greaterThanOrEqual", "lessThanOrEqual"].includes(filterType)) { 102 | return "number" 103 | } 104 | 105 | if (["dateBetween", "dateEquals", "dateBefore", "dateAfter"].includes(filterType)) { 106 | return "date" 107 | } 108 | 109 | return "string" 110 | } 111 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/filters/variants/dialog-filter.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { DialogContent, DialogHeader, DialogTitle, DialogTrigger, Dialog as UiDialog } from "@/components/ui/dialog" 5 | import { MessageCircle } from "lucide-react" 6 | import React from "react" 7 | import { useTableContext } from "../../../table-context" 8 | import { useDialogs } from "../hooks/use-advanced-filter" 9 | import { RenderFilters } from "../render-fields" 10 | 11 | export function Dialog() { 12 | const [open, setOpen] = React.useState(false) 13 | 14 | const { table } = useTableContext() 15 | const { filters, setFilters, applyFilters, resetFilters, updateFilterValue } = useDialogs({ table }) 16 | 17 | return ( 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | Filters 29 | 30 | 31 | 32 | 33 |
34 | 37 | 38 | 39 |
40 |
41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/filters/variants/sheet-filter.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTableContext } from "@/components/enhanced-table/table-context" 4 | import { Button } from "@/components/ui/button" 5 | import { SheetContent, SheetHeader, SheetTitle, SheetTrigger, Sheet as UiSheet } from "@/components/ui/sheet" 6 | import { useMediaQuery } from "@/hooks/use-media-query" 7 | import { cn } from "@/lib/utils" 8 | import { FileSpreadsheet } from "lucide-react" 9 | import { useDialogs } from "../hooks/use-advanced-filter" 10 | import { RenderFilters } from "../render-fields" 11 | 12 | export function Sheet() { 13 | const { table } = useTableContext() 14 | const { filters, setFilters, applyFilters, resetFilters, updateFilterValue } = useDialogs({ table }) 15 | 16 | const isDesktop = useMediaQuery("(min-width: 768px)") 17 | const columns = table.getAllColumns()?.filter((column) => column.getCanFilter() && column.getIsVisible()) 18 | 19 | if (columns.length === 0) return null 20 | 21 | return ( 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | Filters 33 | 34 | 35 | 36 | 37 |
38 | 41 | 42 | 43 |
44 |
45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/filters/variants/simple-filter.tsx: -------------------------------------------------------------------------------- 1 | import { useTableContext } from "@/components/enhanced-table/table-context" 2 | import { Input } from "@/components/ui/input" 3 | 4 | export function Simple() { 5 | const { table } = useTableContext() 6 | 7 | return ( 8 |
9 | {table 10 | .getAllColumns() 11 | .filter((column) => column.getCanFilter()) 12 | .map((column) => ( 13 | column.setFilterValue(event.target.value)} 18 | className="max-w-sm" 19 | /> 20 | ))} 21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/header/dropdown.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { 3 | DropdownMenu, 4 | DropdownMenuContent, 5 | DropdownMenuItem, 6 | DropdownMenuSeparator, 7 | DropdownMenuTrigger, 8 | } from "@/components/ui/dropdown-menu" 9 | import { cn } from "@/lib/utils" 10 | import type { Column, ColumnDefTemplate, HeaderContext } from "@tanstack/react-table" 11 | import { ArrowDownIcon, ArrowUpIcon, ChevronUp, EyeOffIcon, XCircle } from "lucide-react" 12 | 13 | interface HeaderDropdownProps { 14 | title: ColumnDefTemplate> | undefined 15 | column: Column 16 | } 17 | 18 | export function HeaderDropdown({ column, title }: HeaderDropdownProps) { 19 | return ( 20 |
21 | 22 | 23 | 38 | 39 | 40 | 41 | column.toggleSorting(false)}> 42 | 43 | Asc 44 | 45 | 46 | column.toggleSorting(true)}> 47 | 48 | Desc 49 | 50 | 51 | 52 | 53 | column.clearSorting()}> 54 | 55 | Clear Sorting 56 | 57 | 58 | 59 | 60 | column.toggleVisibility(false)}> 61 | 62 | Hide 63 | 64 | 65 | 66 |
67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/header/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { TableHead, TableRow, TableHeader as UiTableHeader } from "@/components/ui/table" 3 | import { cn } from "@/lib/utils" 4 | import { SortableContext, horizontalListSortingStrategy, useSortable } from "@dnd-kit/sortable" 5 | import { CSS } from "@dnd-kit/utilities" 6 | import { type Header, flexRender } from "@tanstack/react-table" 7 | import { ArrowUpDown, GripHorizontal } from "lucide-react" 8 | import type { CSSProperties } from "react" 9 | import { useTableContext } from "../../table-context" 10 | import { getAlignment, isSpecialId } from "../utils" 11 | import { HeaderDropdown } from "./dropdown" 12 | 13 | interface TableHeaderProps { 14 | variant?: "dropdown" | "default" 15 | } 16 | 17 | export function TableHeader({ variant = "default" }: TableHeaderProps) { 18 | const { table, enableColumnReorder, columnOrder } = useTableContext() 19 | 20 | return ( 21 | 22 | {table.getHeaderGroups().map((headerGroup) => ( 23 | 24 | {enableColumnReorder && columnOrder ? ( 25 | 26 | {headerGroup.headers.map((header) => ( 27 | 28 | ))} 29 | 30 | ) : ( 31 | headerGroup.headers.map((header) => ( 32 | 40 | {header.isPlaceholder ? null : ( 41 |
42 | {header.column.getCanSort() ? ( 43 | variant === "dropdown" ? ( 44 | 45 | ) : ( 46 | 54 | ) 55 | ) : ( 56 | flexRender(header.column.columnDef.header, header.getContext()) 57 | )} 58 |
59 | )} 60 |
61 | )) 62 | )} 63 |
64 | ))} 65 |
66 | ) 67 | } 68 | 69 | function DraggableTableHeader({ 70 | header, 71 | variant, 72 | }: { 73 | header: Header 74 | variant?: "dropdown" | "default" 75 | }) { 76 | const { attributes, isDragging, listeners, setNodeRef, transform } = useSortable({ 77 | id: header.column.id, 78 | }) 79 | 80 | const style: CSSProperties = { 81 | opacity: isDragging ? 0.8 : 1, 82 | position: "relative", 83 | transform: CSS.Translate.toString(transform), 84 | transition: "width transform 0.2s ease-in-out", 85 | width: header.column.getSize(), 86 | zIndex: isDragging ? 1 : 0, 87 | whiteSpace: "nowrap", 88 | } 89 | 90 | return ( 91 | 101 |
102 | {header.column.getCanSort() ? ( 103 | variant === "dropdown" ? ( 104 | 105 | ) : ( 106 | 122 | )} 123 | 124 | ) 125 | ) : ( 126 | flexRender(header.column.columnDef.header, header.getContext()) 127 | )} 128 |
129 |
130 | ) 131 | } 132 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/index.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic" 2 | 3 | const Table = dynamic(() => import("@/components/ui/table").then((mod) => mod.Table), { ssr: false }) 4 | const TableBody = dynamic(() => import("./body").then((mod) => mod.TableBody), { ssr: false }) 5 | const TableHeader = dynamic(() => import("./header").then((mod) => mod.TableHeader), { ssr: false }) 6 | const TablePagination = dynamic(() => import("./pagination").then((mod) => mod.TablePagination), { ssr: false }) 7 | const TableRoot = dynamic(() => import("./root").then((mod) => mod.TableRoot), { 8 | ssr: false, 9 | loading: () => , 10 | }) 11 | 12 | import { TableSkeleton } from "@/components/skeletons/table" 13 | import { TableFilters } from "./filters" 14 | import { TableToolbar } from "./toolbar" 15 | 16 | export const EnhancedTable = { 17 | Root: TableRoot, 18 | Toolbar: TableToolbar, 19 | Filters: TableFilters, 20 | Header: TableHeader, 21 | Body: TableBody, 22 | Pagination: TablePagination, 23 | Table: Table, 24 | } 25 | 26 | export default EnhancedTable 27 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/pagination.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { Input } from "@/components/ui/input" 3 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" 4 | import { ChevronLeft, ChevronRight } from "lucide-react" 5 | import type React from "react" 6 | import { useState } from "react" 7 | import { useTableContext } from "../table-context" 8 | 9 | interface TablePaginationProps { 10 | options?: number[] 11 | enableGoToPage?: boolean 12 | } 13 | 14 | export function TablePagination({ options = [10, 20, 30, 40, 50], enableGoToPage = true }: TablePaginationProps) { 15 | const { table } = useTableContext() 16 | const [pageInput, setPageInput] = useState("") 17 | 18 | const handlePageInputChange = (e: React.ChangeEvent) => { 19 | setPageInput(e.target.value) 20 | } 21 | 22 | const handlePageInputKeyDown = (e: React.KeyboardEvent) => { 23 | if (e.key === "Enter") { 24 | const pageNumber = Number.parseInt(pageInput, 10) 25 | if (!Number.isNaN(pageNumber) && pageNumber > 0 && pageNumber <= table.getPageCount()) { 26 | table.setPageIndex(pageNumber - 1) 27 | setPageInput("") 28 | } 29 | } 30 | } 31 | 32 | return ( 33 |
34 |
35 | {table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s) selected. 36 |
37 | 38 |
39 |
40 |

Rows per page

41 | 59 |
60 | 61 | {enableGoToPage && ( 62 |
63 |
64 | Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()} 65 |
66 | 67 | 77 |
78 | )} 79 | 80 |
81 | 90 | 91 | 100 |
101 |
102 |
103 | ) 104 | } 105 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/root.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { Checkbox } from "@/components/ui/checkbox" 3 | import { cn } from "@/lib/utils" 4 | import { 5 | DndContext, 6 | type DragEndEvent, 7 | KeyboardSensor, 8 | MouseSensor, 9 | TouchSensor, 10 | closestCenter, 11 | useSensor, 12 | useSensors, 13 | } from "@dnd-kit/core" 14 | import { restrictToHorizontalAxis, restrictToVerticalAxis } from "@dnd-kit/modifiers" 15 | import { arrayMove, useSortable } from "@dnd-kit/sortable" 16 | import { 17 | type ColumnDef, 18 | type ColumnFiltersState, 19 | type ExpandedState, 20 | type PaginationState, 21 | type RowSelectionState, 22 | type SortingState, 23 | getCoreRowModel, 24 | getExpandedRowModel, 25 | getFilteredRowModel, 26 | getPaginationRowModel, 27 | getSortedRowModel, 28 | useReactTable, 29 | } from "@tanstack/react-table" 30 | import { ChevronDown, ChevronRight, GripVertical } from "lucide-react" 31 | import React from "react" 32 | import { TableProvider } from "../table-context" 33 | import { filterRows } from "./filters/utils" 34 | import type { TableRootProps } from "./types" 35 | import { isSpecialId } from "./utils" 36 | 37 | export function TableRoot({ 38 | data, 39 | columns, 40 | enableExpansion, 41 | enableSelection, 42 | enableEditing, 43 | enableColumnReorder, 44 | enableRowReorder, 45 | rowReorderKey, 46 | children, 47 | }: TableRootProps) { 48 | const [rowSelection, setRowSelection] = React.useState({}) 49 | const [sorting, setSorting] = React.useState([]) 50 | const [columnFilters, setColumnFilters] = React.useState([]) 51 | const [expanded, setExpanded] = React.useState({}) 52 | const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 10 }) 53 | const [tableData, setTableData] = React.useState(data) 54 | 55 | React.useEffect(() => { 56 | setTableData(data) 57 | }, [data]) 58 | 59 | const updateData = (rowIndex: number, updatedData: TData) => { 60 | setTableData((prevData) => { 61 | const newData = [...prevData] 62 | newData[rowIndex] = updatedData 63 | return newData 64 | }) 65 | } 66 | 67 | const dataIds = React.useMemo( 68 | () => tableData.map((data) => (data as any)[rowReorderKey!]), 69 | [tableData, rowReorderKey], 70 | ) 71 | 72 | const memoColumns = React.useMemo(() => { 73 | let newColumns = [...columns] 74 | 75 | if (enableSelection && enableExpansion) { 76 | newColumns = [ 77 | { 78 | id: "select-expand", 79 | header: ({ table }) => ( 80 |
81 | table.toggleAllPageRowsSelected(!!value)} 84 | aria-label="Select all" 85 | /> 86 | 93 |
94 | ), 95 | cell: ({ row }) => ( 96 |
97 | row.toggleSelected(!!value)} 100 | aria-label="Select row" 101 | /> 102 | {row.getCanExpand() && ( 103 | 106 | )} 107 |
108 | ), 109 | enableSorting: false, 110 | enableHiding: false, 111 | } as ColumnDef, 112 | ...newColumns, 113 | ] 114 | } else if (enableSelection && !enableExpansion) { 115 | newColumns = [ 116 | { 117 | id: "select", 118 | header: ({ table }) => ( 119 | table.toggleAllPageRowsSelected(!!value)} 122 | aria-label="Select all" 123 | /> 124 | ), 125 | cell: ({ row }) => ( 126 | row.toggleSelected(!!value)} 129 | aria-label="Select row" 130 | /> 131 | ), 132 | enableSorting: false, 133 | enableHiding: false, 134 | } as ColumnDef, 135 | ...newColumns, 136 | ] 137 | } else if (enableExpansion && !enableSelection) { 138 | newColumns = [ 139 | { 140 | id: "expand", 141 | header: ({ table }) => ( 142 | 149 | ), 150 | cell: ({ row }) => 151 | row.getCanExpand() ? ( 152 | 155 | ) : null, 156 | enableSorting: false, 157 | enableHiding: false, 158 | } as ColumnDef, 159 | ...newColumns, 160 | ] 161 | } 162 | 163 | if (enableRowReorder) { 164 | newColumns = [ 165 | ...newColumns, 166 | { 167 | id: "reorder", 168 | header: () => null, 169 | cell: ({ row, table }) => !table.getIsSomeRowsExpanded() && , 170 | enableSorting: false, 171 | enableHiding: false, 172 | } as ColumnDef, 173 | ] 174 | } 175 | 176 | return newColumns 177 | }, [columns, enableExpansion, enableSelection, enableRowReorder]) 178 | 179 | const [columnOrder, setColumnOrder] = React.useState(() => 180 | memoColumns.map((column) => (enableColumnReorder && column.id && !enableRowReorder ? column.id : "")), 181 | ) 182 | 183 | const table = useReactTable({ 184 | data: tableData, 185 | columns: memoColumns, 186 | getCoreRowModel: getCoreRowModel(), 187 | onRowSelectionChange: setRowSelection, 188 | onSortingChange: setSorting, 189 | getSortedRowModel: getSortedRowModel(), 190 | onColumnFiltersChange: setColumnFilters, 191 | getFilteredRowModel: getFilteredRowModel(), 192 | getPaginationRowModel: getPaginationRowModel(), 193 | onPaginationChange: setPagination, 194 | onExpandedChange: setExpanded, 195 | getExpandedRowModel: getExpandedRowModel(), 196 | getSubRows: (row: any) => row.subRows, 197 | getRowId: (row: any) => (row as any)[rowReorderKey!], 198 | state: { 199 | rowSelection, 200 | sorting, 201 | columnFilters, 202 | pagination, 203 | expanded, 204 | columnOrder, 205 | }, 206 | onColumnOrderChange: setColumnOrder, 207 | filterFns: { 208 | filterRows: filterRows, 209 | }, 210 | }) 211 | 212 | function handleDragEnd(event: DragEndEvent) { 213 | const { active, over } = event 214 | if (!active || !over || active.id === over.id) return 215 | 216 | const activeId = active.id.toString() 217 | const overId = over.id.toString() 218 | 219 | if (isSpecialId(activeId) || isSpecialId(overId)) return 220 | 221 | if (enableColumnReorder) { 222 | setColumnOrder((current) => { 223 | const oldIndex = current.indexOf(activeId) 224 | const newIndex = current.indexOf(overId) 225 | return arrayMove(current, oldIndex, newIndex) 226 | }) 227 | } 228 | 229 | if (enableRowReorder) { 230 | setTableData((prevData) => { 231 | const oldIndex = dataIds.indexOf(activeId) 232 | const newIndex = dataIds.indexOf(overId) 233 | return arrayMove(prevData, oldIndex, newIndex) 234 | }) 235 | } 236 | } 237 | 238 | const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {})) 239 | 240 | if (enableColumnReorder) { 241 | return ( 242 | 248 | 255 |
{children}
256 |
257 |
258 | ) 259 | } 260 | 261 | if (enableRowReorder) { 262 | return ( 263 | 269 | 277 |
{children}
278 |
279 |
280 | ) 281 | } 282 | 283 | return ( 284 | 285 |
{children}
286 |
287 | ) 288 | } 289 | 290 | const RowDragHandleCell = ({ rowId }: { rowId: string }) => { 291 | const { isDragging, attributes, listeners } = useSortable({ 292 | id: rowId, 293 | }) 294 | 295 | return ( 296 | 304 | ) 305 | } 306 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/toolbar/expand-collapse.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { ChevronDown, ChevronUp } from "lucide-react" 3 | import { useTableContext } from "../../table-context" 4 | 5 | export function ExpandCollapse() { 6 | const { table } = useTableContext() 7 | 8 | return ( 9 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/toolbar/export-pdf.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { 3 | DropdownMenu, 4 | DropdownMenuContent, 5 | DropdownMenuItem, 6 | DropdownMenuSeparator, 7 | DropdownMenuTrigger, 8 | } from "@/components/ui/dropdown-menu" 9 | import jsPDF from "jspdf" 10 | import autoTable from "jspdf-autotable" 11 | import { ChevronDown, Download, File, FileText } from "lucide-react" 12 | import { useTableContext } from "../../table-context" 13 | import { isSpecialId } from "../utils" 14 | 15 | export function ExportTable() { 16 | const { table } = useTableContext() 17 | 18 | const exportData = (format: "pdf" | "csv", dataType: "visible" | "all") => { 19 | const columns = table.getAllColumns().filter((column) => { 20 | if (isSpecialId(column.id)) return false 21 | const metaExport = column.columnDef.meta?.export 22 | if (metaExport === false) return false 23 | if (metaExport && typeof metaExport === "object") { 24 | if (format === "pdf" && metaExport.pdf === false) return false 25 | if (format === "csv" && metaExport.csv === false) return false 26 | } 27 | return true 28 | }) 29 | 30 | const headers = columns.map((column) => { 31 | const metaExport = column.columnDef.meta?.export 32 | if (metaExport && typeof metaExport === "object") { 33 | if (format === "pdf" && metaExport.pdf && typeof metaExport.pdf === "object" && metaExport.pdf.header) { 34 | return metaExport.pdf.header 35 | } 36 | if (format === "csv" && metaExport.csv && typeof metaExport.csv === "object" && metaExport.csv.header) { 37 | return metaExport.csv.header 38 | } 39 | } 40 | 41 | const headerContent = column.columnDef.header 42 | 43 | if (typeof headerContent === "function") { 44 | return column.id || "" 45 | } 46 | 47 | return String(headerContent || column.id) 48 | }) 49 | 50 | const rows = (dataType === "visible" ? table.getRowModel().rows : table.getCoreRowModel().rows).map((row) => { 51 | return columns.map((column) => { 52 | const value = row.getValue(column.id) 53 | return typeof value === "object" ? JSON.stringify(value) : String(value) 54 | }) 55 | }) 56 | 57 | if (format === "pdf") { 58 | exportToPDF(headers, rows) 59 | } else { 60 | exportToCSV(headers, rows) 61 | } 62 | } 63 | 64 | const exportToPDF = (headers: string[], rows: string[][]) => { 65 | try { 66 | const doc = new jsPDF() 67 | 68 | autoTable(doc, { 69 | head: [headers], 70 | body: rows, 71 | theme: "grid", 72 | styles: { fontSize: 8, cellPadding: 2 }, 73 | headStyles: { fillColor: [41, 128, 185] }, 74 | margin: { top: 20 }, 75 | }) 76 | 77 | doc.save("table_data.pdf") 78 | } catch (error) { 79 | console.error("Error generating PDF:", error) 80 | } 81 | } 82 | 83 | const exportToCSV = (headers: string[], rows: string[][]) => { 84 | try { 85 | const csvContent = [headers.join(","), ...rows.map((row) => row.join(","))].join("\n") 86 | 87 | const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }) 88 | const link = document.createElement("a") 89 | if (link.download !== undefined) { 90 | const url = URL.createObjectURL(blob) 91 | link.setAttribute("href", url) 92 | link.setAttribute("download", "table_data.csv") 93 | link.style.visibility = "hidden" 94 | document.body.appendChild(link) 95 | link.click() 96 | document.body.removeChild(link) 97 | } 98 | } catch (error) { 99 | console.error("Error generating CSV:", error) 100 | } 101 | } 102 | 103 | return ( 104 | 105 | 106 | 111 | 112 | 113 | 114 | exportData("pdf", "visible")}> 115 | 116 | Export Visible to PDF 117 | 118 | 119 | exportData("pdf", "all")}> 120 | 121 | Export All to PDF 122 | 123 | 124 | 125 | 126 | exportData("csv", "visible")}> 127 | 128 | Export Visible to CSV 129 | 130 | 131 | exportData("csv", "all")}> 132 | 133 | Export All to CSV 134 | 135 | 136 | 137 | ) 138 | } 139 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/toolbar/index.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic" 2 | 3 | const ExpandCollapse = dynamic(() => import("./expand-collapse").then((mod) => mod.ExpandCollapse), { ssr: false }) 4 | const ExportTable = dynamic(() => import("./export-pdf").then((mod) => mod.ExportTable), { ssr: false }) 5 | const ViewOptions = dynamic(() => import("./view-options").then((mod) => mod.ViewOptions), { ssr: false }) 6 | 7 | export const TableToolbar = { 8 | ExpandCollapse, 9 | ExportTable, 10 | ViewOptions, 11 | } 12 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/toolbar/view-options.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu" 4 | 5 | import { Button } from "@/components/ui/button" 6 | import { 7 | DropdownMenu, 8 | DropdownMenuCheckboxItem, 9 | DropdownMenuContent, 10 | DropdownMenuLabel, 11 | DropdownMenuSeparator, 12 | } from "@/components/ui/dropdown-menu" 13 | import { LayoutListIcon } from "lucide-react" 14 | import { useTableContext } from "../../table-context" 15 | 16 | export function ViewOptions() { 17 | const { table } = useTableContext() 18 | 19 | return ( 20 | 21 | 22 | 26 | 27 | 28 | 29 | Toggle columns 30 | 31 | 32 | {table 33 | .getAllColumns() 34 | .filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide()) 35 | .map((column) => { 36 | return ( 37 | column.toggleVisibility(!!value)} 42 | > 43 | {column.id} 44 | 45 | ) 46 | })} 47 | 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/types.d.ts: -------------------------------------------------------------------------------- 1 | interface TableRootBaseProps { 2 | data: TData[] 3 | columns: ColumnDef[] 4 | children: React.ReactNode 5 | enableSelection?: boolean 6 | enableExpansion?: boolean 7 | enableEditing?: boolean 8 | } 9 | 10 | interface TableRootWithColumnReorderProps extends TableRootBaseProps { 11 | enableColumnReorder: true 12 | enableRowReorder?: false 13 | rowReorderKey?: never 14 | } 15 | 16 | interface TableRootWithRowReorderProps extends TableRootBaseProps { 17 | enableRowReorder: true 18 | rowReorderKey: string 19 | enableColumnReorder?: false 20 | } 21 | 22 | interface TableRootWithoutReorderProps extends TableRootBaseProps { 23 | enableColumnReorder?: false 24 | enableRowReorder?: false 25 | rowReorderKey?: never 26 | } 27 | 28 | export type TableRootProps = 29 | | TableRootWithColumnReorderProps 30 | | TableRootWithRowReorderProps 31 | | TableRootWithoutReorderProps 32 | -------------------------------------------------------------------------------- /src/components/enhanced-table/composition-pattern/utils.ts: -------------------------------------------------------------------------------- 1 | export function isSpecialId(id: any) { 2 | return ["select", "expand", "select-expand", "reorder"].includes(id) 3 | } 4 | 5 | export function getAlignment(alignment: string | undefined) { 6 | switch (alignment) { 7 | case "left": 8 | return "text-start" 9 | case "center": 10 | return "text-center" 11 | case "right": 12 | return "text-end" 13 | default: 14 | return "" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/enhanced-table/table-context.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import type { Table } from "@tanstack/react-table" 4 | import type React from "react" 5 | import { createContext, useContext } from "react" 6 | 7 | interface TableContextProps { 8 | table: Table 9 | updateData: (rowIndex: number, updatedData: any) => void 10 | enableEditing?: boolean 11 | enableColumnReorder?: boolean 12 | enableRowReorder?: boolean 13 | dataIds?: any[] 14 | columnOrder?: string[] 15 | } 16 | 17 | const TableContext = createContext(undefined) 18 | 19 | export const useTableContext = () => { 20 | const context = useContext(TableContext) 21 | if (!context) { 22 | throw new Error("useTableContext must be used within a TableProvider") 23 | } 24 | return context 25 | } 26 | 27 | export const TableProvider: React.FC<{ 28 | table: Table 29 | updateData: (rowIndex: number, updatedData: any) => void 30 | enableEditing?: boolean 31 | enableColumnReorder?: boolean 32 | enableRowReorder?: boolean 33 | columnOrder?: string[] 34 | dataIds?: any[] 35 | children: React.ReactNode 36 | }> = ({ table, updateData, enableEditing, enableColumnReorder, enableRowReorder, dataIds, columnOrder, children }) => { 37 | return ( 38 | 41 | {children} 42 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/components/skeletons/table.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton" 2 | 3 | export function TableSkeleton() { 4 | return ( 5 |
6 |
7 | 8 | 9 |
10 | 11 |
12 |
13 | {Array.from({ length: 5 }).map((_, i) => ( 14 | 15 | ))} 16 |
17 | 18 | {Array.from({ length: 10 }).map((_, i) => ( 19 |
20 | {Array.from({ length: 5 }).map((_, j) => ( 21 | 22 | ))} 23 |
24 | ))} 25 |
26 | 27 |
28 | 29 | 30 |
31 | 32 | 33 | 34 | 35 |
36 |
37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDown } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Accordion = AccordionPrimitive.Root 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 16 | )) 17 | AccordionItem.displayName = "AccordionItem" 18 | 19 | const AccordionTrigger = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef 22 | >(({ className, children, ...props }, ref) => ( 23 | 24 | svg]:rotate-180", 28 | className, 29 | )} 30 | {...props} 31 | > 32 | {children} 33 | 34 | 35 | 36 | )) 37 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 38 | 39 | const AccordionContent = React.forwardRef< 40 | React.ElementRef, 41 | React.ComponentPropsWithoutRef 42 | >(({ className, children, ...props }, ref) => ( 43 | 48 |
{children}
49 |
50 | )) 51 | 52 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 53 | 54 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 55 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import { type VariantProps, cva } from "class-variance-authority" 2 | import type * as React from "react" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 12 | secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 13 | destructive: "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 14 | outline: "text-foreground", 15 | active: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100", 16 | inactive: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100", 17 | pending: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | }, 24 | ) 25 | 26 | export interface BadgeProps extends React.HTMLAttributes, VariantProps {} 27 | 28 | function Badge({ className, variant, ...props }: BadgeProps) { 29 | return
30 | } 31 | 32 | export { Badge, badgeVariants } 33 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", 13 | destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 14 | outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 15 | secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 16 | ghost: "hover:bg-accent hover:text-accent-foreground", 17 | link: "text-primary underline-offset-4 hover:underline", 18 | }, 19 | size: { 20 | default: "h-9 px-4 py-2", 21 | sm: "h-8 rounded-md px-3 text-xs", 22 | lg: "h-10 rounded-md px-8", 23 | icon: "h-9 w-9", 24 | }, 25 | }, 26 | defaultVariants: { 27 | variant: "default", 28 | size: "default", 29 | }, 30 | }, 31 | ) 32 | 33 | export interface ButtonProps 34 | extends React.ButtonHTMLAttributes, 35 | VariantProps { 36 | asChild?: boolean 37 | } 38 | 39 | const Button = React.forwardRef( 40 | ({ className, variant, size, asChild = false, ...props }, ref) => { 41 | const Comp = asChild ? Slot : "button" 42 | return 43 | }, 44 | ) 45 | Button.displayName = "Button" 46 | 47 | export { Button, buttonVariants } 48 | -------------------------------------------------------------------------------- /src/components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ChevronLeft, ChevronRight } from "lucide-react" 4 | import type * as React from "react" 5 | import { DayPicker } from "react-day-picker" 6 | 7 | import { buttonVariants } from "@/components/ui/button" 8 | import { cn } from "@/lib/utils" 9 | 10 | export type CalendarProps = React.ComponentProps 11 | 12 | function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) { 13 | return ( 14 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" 37 | : "[&:has([aria-selected])]:rounded-md", 38 | ), 39 | day: cn(buttonVariants({ variant: "ghost" }), "h-8 w-8 p-0 font-normal aria-selected:opacity-100"), 40 | day_range_start: "day-range-start", 41 | day_range_end: "day-range-end", 42 | day_selected: 43 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", 44 | day_today: "bg-accent text-accent-foreground", 45 | day_outside: "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground", 46 | day_disabled: "text-muted-foreground opacity-50", 47 | day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground", 48 | day_hidden: "invisible", 49 | ...classNames, 50 | }} 51 | components={{ 52 | IconLeft: ({ className, ...props }) => , 53 | IconRight: ({ className, ...props }) => , 54 | }} 55 | {...props} 56 | /> 57 | ) 58 | } 59 | Calendar.displayName = "Calendar" 60 | 61 | export { Calendar } 62 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef>(({ className, ...props }, ref) => ( 6 |
7 | )) 8 | Card.displayName = "Card" 9 | 10 | const CardHeader = React.forwardRef>( 11 | ({ className, ...props }, ref) => ( 12 |
13 | ), 14 | ) 15 | CardHeader.displayName = "CardHeader" 16 | 17 | const CardTitle = React.forwardRef>( 18 | ({ className, ...props }, ref) => ( 19 |
20 | ), 21 | ) 22 | CardTitle.displayName = "CardTitle" 23 | 24 | const CardDescription = React.forwardRef>( 25 | ({ className, ...props }, ref) => ( 26 |
27 | ), 28 | ) 29 | CardDescription.displayName = "CardDescription" 30 | 31 | const CardContent = React.forwardRef>( 32 | ({ className, ...props }, ref) =>
, 33 | ) 34 | CardContent.displayName = "CardContent" 35 | 36 | const CardFooter = React.forwardRef>( 37 | ({ className, ...props }, ref) => ( 38 |
39 | ), 40 | ) 41 | CardFooter.displayName = "CardFooter" 42 | 43 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 44 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { Check } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 22 | 23 | 24 | 25 | )) 26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 27 | 28 | export { Checkbox } 29 | -------------------------------------------------------------------------------- /src/components/ui/date-picker.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { format } from "date-fns" 4 | import { CalendarIcon } from "lucide-react" 5 | 6 | import { Button } from "@/components/ui/button" 7 | import { Calendar } from "@/components/ui/calendar" 8 | import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" 9 | import { cn } from "@/lib/utils" 10 | 11 | interface DatePickerProps { 12 | date?: Date 13 | setDate: (date: Date | undefined) => void 14 | } 15 | 16 | export function DatePicker({ date, setDate }: DatePickerProps) { 17 | return ( 18 | 19 | 20 | 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( 57 |
58 | ) 59 | DialogHeader.displayName = "DialogHeader" 60 | 61 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( 62 |
63 | ) 64 | DialogFooter.displayName = "DialogFooter" 65 | 66 | const DialogTitle = React.forwardRef< 67 | React.ElementRef, 68 | React.ComponentPropsWithoutRef 69 | >(({ className, ...props }, ref) => ( 70 | 75 | )) 76 | DialogTitle.displayName = DialogPrimitive.Title.displayName 77 | 78 | const DialogDescription = React.forwardRef< 79 | React.ElementRef, 80 | React.ComponentPropsWithoutRef 81 | >(({ className, ...props }, ref) => ( 82 | 83 | )) 84 | DialogDescription.displayName = DialogPrimitive.Description.displayName 85 | 86 | export { 87 | Dialog, 88 | DialogPortal, 89 | DialogOverlay, 90 | DialogTrigger, 91 | DialogClose, 92 | DialogContent, 93 | DialogHeader, 94 | DialogFooter, 95 | DialogTitle, 96 | DialogDescription, 97 | } 98 | -------------------------------------------------------------------------------- /src/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { Check, ChevronRight, Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )) 40 | DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName 41 | 42 | const DropdownMenuSubContent = React.forwardRef< 43 | React.ElementRef, 44 | React.ComponentPropsWithoutRef 45 | >(({ className, ...props }, ref) => ( 46 | 54 | )) 55 | DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName 56 | 57 | const DropdownMenuContent = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, sideOffset = 4, ...props }, ref) => ( 61 | 62 | 72 | 73 | )) 74 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 75 | 76 | const DropdownMenuItem = React.forwardRef< 77 | React.ElementRef, 78 | React.ComponentPropsWithoutRef & { 79 | inset?: boolean 80 | } 81 | >(({ className, inset, ...props }, ref) => ( 82 | svg]:size-4 [&>svg]:shrink-0", 86 | inset && "pl-8", 87 | className, 88 | )} 89 | {...props} 90 | /> 91 | )) 92 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 93 | 94 | const DropdownMenuCheckboxItem = React.forwardRef< 95 | React.ElementRef, 96 | React.ComponentPropsWithoutRef 97 | >(({ className, children, checked, ...props }, ref) => ( 98 | 107 | 108 | 109 | 110 | 111 | 112 | {children} 113 | 114 | )) 115 | DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName 116 | 117 | const DropdownMenuRadioItem = React.forwardRef< 118 | React.ElementRef, 119 | React.ComponentPropsWithoutRef 120 | >(({ className, children, ...props }, ref) => ( 121 | 129 | 130 | 131 | 132 | 133 | 134 | {children} 135 | 136 | )) 137 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 138 | 139 | const DropdownMenuLabel = React.forwardRef< 140 | React.ElementRef, 141 | React.ComponentPropsWithoutRef & { 142 | inset?: boolean 143 | } 144 | >(({ className, inset, ...props }, ref) => ( 145 | 150 | )) 151 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 152 | 153 | const DropdownMenuSeparator = React.forwardRef< 154 | React.ElementRef, 155 | React.ComponentPropsWithoutRef 156 | >(({ className, ...props }, ref) => ( 157 | 158 | )) 159 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 160 | 161 | const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { 162 | return 163 | } 164 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 165 | 166 | export { 167 | DropdownMenu, 168 | DropdownMenuTrigger, 169 | DropdownMenuContent, 170 | DropdownMenuItem, 171 | DropdownMenuCheckboxItem, 172 | DropdownMenuRadioItem, 173 | DropdownMenuLabel, 174 | DropdownMenuSeparator, 175 | DropdownMenuShortcut, 176 | DropdownMenuGroup, 177 | DropdownMenuPortal, 178 | DropdownMenuSub, 179 | DropdownMenuSubContent, 180 | DropdownMenuSubTrigger, 181 | DropdownMenuRadioGroup, 182 | } 183 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | }, 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70") 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & VariantProps 14 | >(({ className, ...props }, ref) => ( 15 | 16 | )) 17 | Label.displayName = LabelPrimitive.Root.displayName 18 | 19 | export { Label } 20 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor 13 | 14 | const PopoverContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 18 | 19 | 29 | 30 | )) 31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 32 | 33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 34 | -------------------------------------------------------------------------------- /src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 13 | {children} 14 | 15 | 16 | 17 | )) 18 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 19 | 20 | const ScrollBar = React.forwardRef< 21 | React.ElementRef, 22 | React.ComponentPropsWithoutRef 23 | >(({ className, orientation = "vertical", ...props }, ref) => ( 24 | 35 | 36 | 37 | )) 38 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 39 | 40 | export { ScrollArea, ScrollBar } 41 | -------------------------------------------------------------------------------- /src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SelectPrimitive from "@radix-ui/react-select" 5 | import { Check, ChevronDown, ChevronUp } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Select = SelectPrimitive.Root 10 | 11 | const SelectGroup = SelectPrimitive.Group 12 | 13 | const SelectValue = SelectPrimitive.Value 14 | 15 | const SelectTrigger = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, children, ...props }, ref) => ( 19 | span]:line-clamp-1", 23 | className, 24 | )} 25 | {...props} 26 | > 27 | {children} 28 | 29 | 30 | 31 | 32 | )) 33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 34 | 35 | const SelectScrollUpButton = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 44 | 45 | 46 | )) 47 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 48 | 49 | const SelectScrollDownButton = React.forwardRef< 50 | React.ElementRef, 51 | React.ComponentPropsWithoutRef 52 | >(({ className, ...props }, ref) => ( 53 | 58 | 59 | 60 | )) 61 | SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName 62 | 63 | const SelectContent = React.forwardRef< 64 | React.ElementRef, 65 | React.ComponentPropsWithoutRef 66 | >(({ className, children, position = "popper", ...props }, ref) => ( 67 | 68 | 79 | 80 | 87 | {children} 88 | 89 | 90 | 91 | 92 | )) 93 | SelectContent.displayName = SelectPrimitive.Content.displayName 94 | 95 | const SelectLabel = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, ...props }, ref) => ( 99 | 100 | )) 101 | SelectLabel.displayName = SelectPrimitive.Label.displayName 102 | 103 | const SelectItem = React.forwardRef< 104 | React.ElementRef, 105 | React.ComponentPropsWithoutRef 106 | >(({ className, children, ...props }, ref) => ( 107 | 115 | 116 | 117 | 118 | 119 | 120 | {children} 121 | 122 | )) 123 | SelectItem.displayName = SelectPrimitive.Item.displayName 124 | 125 | const SelectSeparator = React.forwardRef< 126 | React.ElementRef, 127 | React.ComponentPropsWithoutRef 128 | >(({ className, ...props }, ref) => ( 129 | 130 | )) 131 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 132 | 133 | export { 134 | Select, 135 | SelectGroup, 136 | SelectValue, 137 | SelectTrigger, 138 | SelectContent, 139 | SelectLabel, 140 | SelectItem, 141 | SelectSeparator, 142 | SelectScrollUpButton, 143 | SelectScrollDownButton, 144 | } 145 | -------------------------------------------------------------------------------- /src/components/ui/sheet.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SheetPrimitive from "@radix-ui/react-dialog" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | import { X } from "lucide-react" 7 | 8 | import { cn } from "@/lib/utils" 9 | 10 | const Sheet = SheetPrimitive.Root 11 | 12 | const SheetTrigger = SheetPrimitive.Trigger 13 | 14 | const SheetClose = SheetPrimitive.Close 15 | 16 | const SheetPortal = SheetPrimitive.Portal 17 | 18 | const SheetOverlay = React.forwardRef< 19 | React.ElementRef, 20 | React.ComponentPropsWithoutRef 21 | >(({ className, ...props }, ref) => ( 22 | 30 | )) 31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName 32 | 33 | const sheetVariants = cva( 34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", 35 | { 36 | variants: { 37 | side: { 38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", 39 | bottom: 40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", 41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", 42 | right: 43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", 44 | }, 45 | }, 46 | defaultVariants: { 47 | side: "right", 48 | }, 49 | }, 50 | ) 51 | 52 | interface SheetContentProps 53 | extends React.ComponentPropsWithoutRef, 54 | VariantProps {} 55 | 56 | const SheetContent = React.forwardRef, SheetContentProps>( 57 | ({ side = "right", className, children, ...props }, ref) => ( 58 | 59 | 60 | 61 | {children} 62 | 63 | 64 | Close 65 | 66 | 67 | 68 | ), 69 | ) 70 | SheetContent.displayName = SheetPrimitive.Content.displayName 71 | 72 | const SheetHeader = ({ className, ...props }: React.HTMLAttributes) => ( 73 |
74 | ) 75 | SheetHeader.displayName = "SheetHeader" 76 | 77 | const SheetFooter = ({ className, ...props }: React.HTMLAttributes) => ( 78 |
79 | ) 80 | SheetFooter.displayName = "SheetFooter" 81 | 82 | const SheetTitle = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef 85 | >(({ className, ...props }, ref) => ( 86 | 87 | )) 88 | SheetTitle.displayName = SheetPrimitive.Title.displayName 89 | 90 | const SheetDescription = React.forwardRef< 91 | React.ElementRef, 92 | React.ComponentPropsWithoutRef 93 | >(({ className, ...props }, ref) => ( 94 | 95 | )) 96 | SheetDescription.displayName = SheetPrimitive.Description.displayName 97 | 98 | export { 99 | Sheet, 100 | SheetPortal, 101 | SheetOverlay, 102 | SheetTrigger, 103 | SheetClose, 104 | SheetContent, 105 | SheetHeader, 106 | SheetFooter, 107 | SheetTitle, 108 | SheetDescription, 109 | } 110 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ className, ...props }: React.HTMLAttributes) { 4 | return
5 | } 6 | 7 | export { Skeleton } 8 | -------------------------------------------------------------------------------- /src/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef>( 6 | ({ className, ...props }, ref) => ( 7 |
8 | 9 | 10 | ), 11 | ) 12 | Table.displayName = "Table" 13 | 14 | const TableHeader = React.forwardRef>( 15 | ({ className, ...props }, ref) => , 16 | ) 17 | TableHeader.displayName = "TableHeader" 18 | 19 | const TableBody = React.forwardRef>( 20 | ({ className, ...props }, ref) => ( 21 | 22 | ), 23 | ) 24 | TableBody.displayName = "TableBody" 25 | 26 | const TableFooter = React.forwardRef>( 27 | ({ className, ...props }, ref) => ( 28 | tr]:last:border-b-0", className)} {...props} /> 29 | ), 30 | ) 31 | TableFooter.displayName = "TableFooter" 32 | 33 | const TableRow = React.forwardRef>( 34 | ({ className, ...props }, ref) => ( 35 | 40 | ), 41 | ) 42 | TableRow.displayName = "TableRow" 43 | 44 | const TableHead = React.forwardRef>( 45 | ({ className, ...props }, ref) => ( 46 |
[role=checkbox]]:translate-y-[2px]", 50 | className, 51 | )} 52 | {...props} 53 | /> 54 | ), 55 | ) 56 | TableHead.displayName = "TableHead" 57 | 58 | const TableCell = React.forwardRef>( 59 | ({ className, ...props }, ref) => ( 60 | [role=checkbox]]:translate-y-[2px]", className)} 63 | {...props} 64 | /> 65 | ), 66 | ) 67 | TableCell.displayName = "TableCell" 68 | 69 | const TableCaption = React.forwardRef>( 70 | ({ className, ...props }, ref) => ( 71 |
72 | ), 73 | ) 74 | TableCaption.displayName = "TableCaption" 75 | 76 | export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption } 77 | -------------------------------------------------------------------------------- /src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Tabs = TabsPrimitive.Root 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )) 23 | TabsList.displayName = TabsPrimitive.List.displayName 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )) 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )) 53 | TabsContent.displayName = TabsPrimitive.Content.displayName 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent } 56 | -------------------------------------------------------------------------------- /src/hooks/use-media-query.ts: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, useState } from "react" 4 | 5 | export function useMediaQuery(query: string) { 6 | const [value, setValue] = useState(false) 7 | 8 | useEffect(() => { 9 | function onChange(event: MediaQueryListEvent) { 10 | setValue(event.matches) 11 | } 12 | 13 | const result = matchMedia(query) 14 | result.addEventListener("change", onChange) 15 | setValue(result.matches) 16 | 17 | return () => result.removeEventListener("change", onChange) 18 | }, [query]) 19 | 20 | return value 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/types/@tanstack/react-table.d.ts: -------------------------------------------------------------------------------- 1 | import "@tanstack/react-table" 2 | 3 | declare module "@tanstack/react-table" { 4 | interface ColumnMeta { 5 | align?: "left" | "center" | "right" 6 | export?: 7 | | boolean 8 | | { 9 | pdf?: boolean | { header?: string } 10 | csv?: boolean | { header?: string } 11 | } 12 | } 13 | 14 | interface FilterFns { 15 | filterRows: FilterFn 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | darkMode: ["class"], 5 | content: [ 6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | background: 'hsl(var(--background))', 14 | foreground: 'hsl(var(--foreground))', 15 | card: { 16 | DEFAULT: 'hsl(var(--card))', 17 | foreground: 'hsl(var(--card-foreground))' 18 | }, 19 | popover: { 20 | DEFAULT: 'hsl(var(--popover))', 21 | foreground: 'hsl(var(--popover-foreground))' 22 | }, 23 | primary: { 24 | DEFAULT: 'hsl(var(--primary))', 25 | foreground: 'hsl(var(--primary-foreground))' 26 | }, 27 | secondary: { 28 | DEFAULT: 'hsl(var(--secondary))', 29 | foreground: 'hsl(var(--secondary-foreground))' 30 | }, 31 | muted: { 32 | DEFAULT: 'hsl(var(--muted))', 33 | foreground: 'hsl(var(--muted-foreground))' 34 | }, 35 | accent: { 36 | DEFAULT: 'hsl(var(--accent))', 37 | foreground: 'hsl(var(--accent-foreground))' 38 | }, 39 | destructive: { 40 | DEFAULT: 'hsl(var(--destructive))', 41 | foreground: 'hsl(var(--destructive-foreground))' 42 | }, 43 | border: 'hsl(var(--border))', 44 | input: 'hsl(var(--input))', 45 | ring: 'hsl(var(--ring))', 46 | chart: { 47 | '1': 'hsl(var(--chart-1))', 48 | '2': 'hsl(var(--chart-2))', 49 | '3': 'hsl(var(--chart-3))', 50 | '4': 'hsl(var(--chart-4))', 51 | '5': 'hsl(var(--chart-5))' 52 | }, 53 | sidebar: { 54 | DEFAULT: 'hsl(var(--sidebar-background))', 55 | foreground: 'hsl(var(--sidebar-foreground))', 56 | primary: 'hsl(var(--sidebar-primary))', 57 | 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', 58 | accent: 'hsl(var(--sidebar-accent))', 59 | 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', 60 | border: 'hsl(var(--sidebar-border))', 61 | ring: 'hsl(var(--sidebar-ring))' 62 | } 63 | }, 64 | borderRadius: { 65 | lg: 'var(--radius)', 66 | md: 'calc(var(--radius) - 2px)', 67 | sm: 'calc(var(--radius) - 4px)' 68 | }, 69 | keyframes: { 70 | 'accordion-down': { 71 | from: { 72 | height: '0' 73 | }, 74 | to: { 75 | height: 'var(--radix-accordion-content-height)' 76 | } 77 | }, 78 | 'accordion-up': { 79 | from: { 80 | height: 'var(--radix-accordion-content-height)' 81 | }, 82 | to: { 83 | height: '0' 84 | } 85 | } 86 | }, 87 | animation: { 88 | 'accordion-down': 'accordion-down 0.2s ease-out', 89 | 'accordion-up': 'accordion-up 0.2s ease-out' 90 | } 91 | } 92 | }, 93 | plugins: [require("tailwindcss-animate")], 94 | } satisfies Config; 95 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules", ".next"] 27 | } 28 | --------------------------------------------------------------------------------