├── .eslintrc.json ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ └── codeStyleConfig.xml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── test.iml └── vcs.xml ├── @types └── index.ts ├── README.md ├── app ├── favicon.ico ├── globals.css ├── layout.tsx ├── page.tsx └── providers.tsx ├── components.json ├── components ├── data-table │ ├── data-table-add-row.tsx │ ├── data-table-body.tsx │ ├── data-table-cell.tsx │ ├── data-table-checkbox.tsx │ ├── data-table-column-visibility.tsx │ ├── data-table-delete-confirmation.tsx │ ├── data-table-edit-row.tsx │ ├── data-table-export.tsx │ ├── data-table-filter.tsx │ ├── data-table-floating-bar.tsx │ ├── data-table-form.tsx │ ├── data-table-header.tsx │ ├── data-table-input-date.tsx │ ├── data-table-input.tsx │ ├── data-table-pagination.tsx │ ├── data-table-selections.tsx │ ├── data-table-skeleton.tsx │ └── index.tsx └── ui │ ├── alert-dialog.tsx │ ├── alert.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── checkbox.tsx │ ├── command.tsx │ ├── context-menu.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── radio-group.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── skeleton.tsx │ ├── table.tsx │ ├── toggle-group.tsx │ ├── toggle.tsx │ └── tooltip.tsx ├── hooks └── useDebounce.tsx ├── interface └── IDataTable.ts ├── lib ├── columns.ts ├── exportExcel.ts ├── makeData.ts └── utils.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── next.svg └── vercel.svg ├── store ├── dataTableStore.ts └── dataTableStoreProvider.tsx ├── tailwind.config.ts ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.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.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/test.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /@types/index.ts: -------------------------------------------------------------------------------- 1 | import {z} from "zod"; 2 | import React, {ReactNode} from "react"; 3 | 4 | export type TDataTableExportProps = { 5 | exportFileName: string; 6 | excludeColumns?: string[]; 7 | onUserExport?: (data: any[])=> void; 8 | }; 9 | 10 | export type TDataTableContextMenuProps = { 11 | enableEdit: boolean; 12 | enableDelete: boolean; 13 | onDelete: (prop: any) => void; 14 | extra?: { [menuName: string]: (prop: any)=> void; } 15 | }; 16 | 17 | export type TDataTableDataValidation = { 18 | id: string; 19 | component: "select" | "input" | "radio" | "date" | "date-range" | "checkbox" | "combobox"; 20 | componentCssProps?: { 21 | parent?: string; 22 | child?: string; 23 | }; 24 | label?: string; 25 | description?: string; 26 | placeholder?: string; 27 | data?: { 28 | value: string; 29 | children: React.ReactNode; 30 | }[]; 31 | schema: z.ZodType; 32 | }; 33 | 34 | export type TDataTableAddDataProps = { 35 | enable: boolean; 36 | onSubmitNewData: (data: T) => void; 37 | title: string; 38 | description: string; 39 | }; 40 | 41 | export type TDataTableEditDataProps = { 42 | onSubmitEditData?: (data: T) => void; 43 | title: string; 44 | description: string; 45 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A featureful example of TanStack React Table 2 | 3 | 4 | 5 | https://github.com/mjm918/React-Advance-Table/assets/17846525/4e40745e-9320-4872-a613-0a05c7f8da90 6 | 7 | 8 | 9 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 10 | 11 | ## Getting Started 12 | 13 | First, run the development server: 14 | 15 | ```bash 16 | npm run dev 17 | # or 18 | yarn dev 19 | # or 20 | pnpm dev 21 | # or 22 | bun dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | 27 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 28 | 29 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 30 | 31 | ## Learn More 32 | 33 | To learn more about Next.js, take a look at the following resources: 34 | 35 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 36 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 37 | 38 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 39 | 40 | ## Deploy on Vercel 41 | 42 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 43 | 44 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 45 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjm918/React-Advance-Table/0cec0dbfa2412894d53c13ee6a7e2385ad460fbf/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | 78 | .resizer { 79 | position: absolute; 80 | top: 0; 81 | height: 100%; 82 | width: 2px; 83 | background: rgba(0, 0, 0, 0.5); 84 | cursor: col-resize; 85 | user-select: none; 86 | touch-action: none; 87 | } 88 | 89 | .resizer.ltr { 90 | right: 0; 91 | } 92 | 93 | .resizer.rtl { 94 | left: 0; 95 | } 96 | 97 | .resizer.isResizing { 98 | background: black; 99 | opacity: 1; 100 | } 101 | 102 | @media (hover: hover) { 103 | .resizer { 104 | opacity: 0; 105 | } 106 | 107 | *:hover > .resizer { 108 | opacity: 1; 109 | } 110 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type {Metadata} from "next"; 2 | import {Inter} from "next/font/google"; 3 | import "./globals.css"; 4 | import {Providers} from "@/app/providers"; 5 | 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export const metadata: Metadata = { 9 | title: "React Advance Table", 10 | description: "Example of TanStack Table", 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: Readonly<{ 16 | children: React.ReactNode; 17 | }>) { 18 | return ( 19 | 20 | 21 | 22 |
23 | {children} 24 |
25 |
26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import {useEffect, useMemo, useState} from "react"; 5 | import {ColumnDef, Table} from "@tanstack/react-table"; 6 | import {makeData, Person} from "@/lib/makeData"; 7 | import {isWithinInterval} from "date-fns"; 8 | import {AdvancedDataTable} from "@/components/data-table"; 9 | import {DataTableCheckBox} from "@/components/data-table/data-table-checkbox"; 10 | import {Button} from "@/components/ui/button"; 11 | import {GitHubLogoIcon} from "@radix-ui/react-icons"; 12 | import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert"; 13 | import {z} from "zod"; 14 | import {faker} from "@faker-js/faker"; 15 | 16 | const data = makeData(100_000); 17 | export default function Home() { 18 | const [isLoading, setLoading] = useState(true); 19 | const filename = "exampleExport"; 20 | const columns = useMemo[]>( 21 | () => [ 22 | { 23 | id: "select", 24 | header: ({ table }: { table: Table }) => ( 25 |
26 | 33 |
34 | ), 35 | cell: ({ row }) => ( 36 |
37 | 45 |
46 | ), 47 | size: 50 48 | }, 49 | { 50 | header: "First Name", 51 | accessorKey: "firstName", 52 | id: "firstName", 53 | cell: info => info.getValue() 54 | }, 55 | { 56 | accessorFn: row => row.lastName, 57 | id: "lastName", 58 | cell: info => info.getValue(), 59 | header: "Last Name", 60 | }, 61 | { 62 | accessorKey: "gender", 63 | id: "gender", 64 | header: "Gender", 65 | meta: { 66 | filterVariant: "select", 67 | }, 68 | }, 69 | { 70 | accessorFn: row => row.jobType, 71 | id: "jobType", 72 | cell: info => info.getValue(), 73 | header: "Job Type", 74 | }, 75 | { 76 | accessorFn: row => row.address, 77 | id: "address", 78 | cell: info => info.getValue(), 79 | header: "Address" 80 | }, 81 | { 82 | accessorFn: row => row.locality, 83 | id: "locality", 84 | cell: info => info.getValue(), 85 | header: "Locality", 86 | meta: { 87 | filterVariant: "select", 88 | } 89 | }, 90 | { 91 | accessorKey: "age", 92 | id: "age", 93 | header: "Age", 94 | meta: { 95 | filterVariant: "range", 96 | }, 97 | }, 98 | { 99 | accessorKey: "visits", 100 | id: "visits", 101 | header: "Visits", 102 | meta: { 103 | filterVariant: "range", 104 | }, 105 | }, 106 | { 107 | accessorKey: "status", 108 | id: "status", 109 | header: "Status", 110 | meta: { 111 | filterVariant: "select", 112 | }, 113 | }, 114 | { 115 | accessorKey: "lastUpdate", 116 | id: "lastUpdate", 117 | header: "Last Update", 118 | cell: info => { 119 | const str = info.getValue() as Date; 120 | return str.toLocaleDateString(); 121 | }, 122 | meta: { 123 | filterVariant: "date", 124 | }, 125 | filterFn: (row, columnId, filterValue) => { 126 | const columnDate = row.getValue(columnId) as Date; 127 | const {from, to} = filterValue; 128 | return isWithinInterval(columnDate,{ start: from, end: to || from }); 129 | } 130 | } 131 | ], 132 | [] 133 | ); 134 | 135 | useEffect(()=>{ 136 | const tmo = setTimeout(()=>{ 137 | setLoading(false); 138 | clearTimeout(tmo); 139 | },5000); 140 | },[]); 141 | 142 | return ( 143 | <> 144 | 145 | 146 | React Advance Table - Using TanStack Table 147 | 148 | 149 | This is not a library or anything. Just an example of TanStack React Table. 150 |
151 | 152 | Project available on 153 | 154 | 159 |
160 |
161 |
162 | 163 | id={"example-advance-table"} 164 | columns={columns} 165 | data={data} 166 | exportProps={{ 167 | exportFileName: filename 168 | }} 169 | actionProps={{ 170 | onDelete: (props) => { 171 | console.log("actionProps",props); 172 | } 173 | }} 174 | onRowClick={(prop) => { 175 | console.log("onRowClick",prop); 176 | }} 177 | contextMenuProps={{ 178 | enableEdit: true, 179 | enableDelete: true, 180 | onDelete: (prop)=> { 181 | console.log("contextMenuProps:onDelete",prop); 182 | }, 183 | extra: { 184 | "Copy to clipboard": (data) => { 185 | console.log("contextMenuProps:onClipboard", data); 186 | } 187 | } 188 | }} 189 | addDataProps={{ 190 | enable: true, 191 | title: "Add a new netizen", 192 | description: "Netizens can be rude sometimes. Add them with caution.", 193 | onSubmitNewData: netizen => { 194 | console.log("onSubmitNewData",netizen); 195 | } 196 | }} 197 | editDataProps={{ 198 | title: "Amend netizen data", 199 | description: "Netizens can be rude sometimes. Edit them with caution.", 200 | onSubmitEditData: netizen => { 201 | console.log("onSubmitEditData",netizen); 202 | } 203 | }} 204 | isLoading={isLoading} 205 | dataValidationProps={[ 206 | { 207 | id: "firstName", 208 | component: "input", 209 | label: "First Name", 210 | schema: z.string().min(3, "First name must be at least 3 characters") 211 | }, 212 | { 213 | id: "lastName", 214 | component: "input", 215 | label: "Last Name", 216 | schema: z.string().min(3, "Last name must be at least 3 characters") 217 | }, 218 | { 219 | id: "address", 220 | component: "input", 221 | label: "Address", 222 | schema: z.string().min(3, "Address must be at least 3 characters") 223 | }, 224 | { 225 | id: "status", 226 | component: "select", 227 | label: "Relationship Status", 228 | placeholder: "Your current relationship status?", 229 | data: [ 230 | { 231 | value: "relationship", 232 | children: "relationship" 233 | }, 234 | { 235 | value: "complicated", 236 | children: "complicated" 237 | }, 238 | { 239 | value: "single", 240 | children: "single" 241 | } 242 | ], 243 | schema: z.enum([ 244 | "relationship", 245 | "complicated", 246 | "single" 247 | ]), 248 | componentCssProps: { 249 | parent: "w-full" 250 | } 251 | }, 252 | { 253 | id: "gender", 254 | component: "radio", 255 | label: "Gender", 256 | placeholder: "There are only 2 genders", 257 | data: [ 258 | { 259 | value: "male", 260 | children: "Male" 261 | }, 262 | { 263 | value: "female", 264 | children: "Female" 265 | } 266 | ], 267 | schema: z.enum([ 268 | "male", 269 | "female" 270 | ]), 271 | componentCssProps: { 272 | parent: "w-full" 273 | } 274 | }, 275 | { 276 | id: "locality", 277 | component: "combobox", 278 | label: "Locality", 279 | placeholder: "Your current location?", 280 | data: new Array(120).fill(0).map((_it, _idx) => { 281 | const country = faker.location.country(); 282 | return { 283 | value: country, 284 | children: country 285 | }; 286 | }), 287 | schema: z.string().min(3, "You must choose your locality"), 288 | componentCssProps: { 289 | parent: "w-full" 290 | } 291 | } 292 | ]} 293 | /> 294 |
295 | 296 | Shared by ❤️ 297 | 298 | 303 |
304 | 305 | ); 306 | } -------------------------------------------------------------------------------- /app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {TooltipProvider} from "@/components/ui/tooltip"; 4 | import {DataTableStoreProvider} from "@/store/dataTableStoreProvider"; 5 | 6 | export function Providers({children}: { children: React.ReactNode }) { 7 | return ( 8 | 9 | 10 | {children} 11 | 12 | 13 | ); 14 | } -------------------------------------------------------------------------------- /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": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/data-table/data-table-add-row.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger} from "@/components/ui/sheet"; 4 | import {ListPlusIcon} from "lucide-react"; 5 | import {Button} from "@/components/ui/button"; 6 | import {useDataTableStore} from "@/store/dataTableStore"; 7 | import _ from "lodash"; 8 | import {z} from "zod"; 9 | import {useForm} from "react-hook-form"; 10 | import {zodResolver} from "@hookform/resolvers/zod"; 11 | import {Separator} from "@/components/ui/separator"; 12 | import React from "react"; 13 | import {DataTableForm} from "@/components/data-table/data-table-form"; 14 | 15 | export function DataTableAddRow() { 16 | const {title, description, onSubmitNewData, schemas} = useDataTableStore(state => ({ 17 | ...state.addDataProps, 18 | schemas: state.dataValidationProps 19 | })); 20 | const getFormSchema = () => { 21 | const defaultValues: { [k: string]: string } = {}; 22 | if (schemas instanceof Array && schemas.length > 0) { 23 | const obj: { [k: string]: z.ZodType } = {}; 24 | schemas.forEach(item => { 25 | obj[item.id] = item.schema; 26 | defaultValues[item.id] = ""; 27 | }); 28 | return {schema: z.object(obj), defaultValues}; 29 | } 30 | return {schema: z.object({}), defaultValues}; 31 | }; 32 | const FormSchema = getFormSchema(); 33 | const form = useForm>({ 34 | resolver: zodResolver(FormSchema.schema), 35 | defaultValues: FormSchema.defaultValues, 36 | }); 37 | const onSubmit = (data: z.infer) => { 38 | onSubmitNewData && onSubmitNewData(data); 39 | }; 40 | return ( 41 | 42 | 43 | 51 | 52 | 53 | 54 | {title ?? "Create a new record in the list"} 55 | { 56 | !_.isEmpty(description) && ( 57 | 58 | {description} 59 | 60 | ) 61 | } 62 | 63 | 64 | { 65 | Object.keys(FormSchema.defaultValues).length > 0 && schemas !== undefined && ( 66 | 67 | ) 68 | } 69 | 70 | 71 | ); 72 | } -------------------------------------------------------------------------------- /components/data-table/data-table-body.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {TableBody, TableCell, TableRow} from "@/components/ui/table"; 4 | import {horizontalListSortingStrategy, SortableContext} from "@dnd-kit/sortable"; 5 | import {DataTableCell} from "@/components/data-table/data-table-cell"; 6 | import * as React from "react"; 7 | import {IDataTableBody} from "@/interface/IDataTable"; 8 | import {Row} from "@tanstack/table-core"; 9 | 10 | export function DataTableBody(props: IDataTableBody) { 11 | const {table, virtualColumns, columnOrder, rowVirtualizer, virtualPaddingRight, virtualPaddingLeft} = props; 12 | const virtualRows = rowVirtualizer.getVirtualItems(); 13 | const {rows} = table.getRowModel(); 14 | return ( 15 | 21 | {virtualRows.map(virtualRow => { 22 | const row = rows[virtualRow.index] as Row; 23 | const visibleCells = row.getVisibleCells(); 24 | 25 | return ( 26 | props.onClick && props.onClick(row.original)} key={row.id} 27 | className={props.onClick ? "cursor-pointer" : ""} 28 | data-index={virtualRow.index} //needed for dynamic row height measurement 29 | ref={node => rowVirtualizer.measureElement(node)} //measure dynamic row height 30 | style={{ 31 | display: "flex", 32 | position: "absolute", 33 | transform: `translateY(${virtualRow.start}px)`, //this should always be a `style` as it changes on scroll 34 | width: "100%", 35 | }} 36 | > 37 | {virtualPaddingLeft ? ( 38 | 41 | ) : null} 42 | {virtualColumns.map(vc => { 43 | const cell = visibleCells[vc.index]; 44 | return ( 45 | 49 | 50 | 51 | ); 52 | })} 53 | {virtualPaddingRight ? ( 54 | 57 | ) : null} 58 | 59 | ); 60 | })} 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /components/data-table/data-table-cell.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {flexRender} from "@tanstack/react-table"; 4 | import {useSortable} from "@dnd-kit/sortable"; 5 | import * as React from "react"; 6 | import {CSSProperties} from "react"; 7 | import {CSS} from "@dnd-kit/utilities"; 8 | import {TableCell} from "@/components/ui/table"; 9 | import {getCommonPinningStyles} from "@/lib/columns"; 10 | import { 11 | ContextMenu, 12 | ContextMenuContent, 13 | ContextMenuItem, ContextMenuNotItem, 14 | ContextMenuSeparator, 15 | ContextMenuShortcut, 16 | ContextMenuSub, 17 | ContextMenuSubContent, 18 | ContextMenuSubTrigger, 19 | ContextMenuTrigger 20 | } from "@/components/ui/context-menu"; 21 | import _ from "lodash"; 22 | import {useDataTableStore} from "@/store/dataTableStore"; 23 | import ReactHotkeys from "react-hot-keys"; 24 | import {IDataTableCellEdit} from "@/interface/IDataTable"; 25 | import {DataTableEditRow} from "@/components/data-table/data-table-edit-row"; 26 | import {RequestDeleteConfirmation} from "@/components/data-table/data-table-delete-confirmation"; 27 | 28 | export function DataTableCell({cell}: IDataTableCellEdit) { 29 | const {isDragging, setNodeRef, transform} = useSortable({ 30 | id: cell.column.id, 31 | }); 32 | const {contextMenuProps, isSelecting} = useDataTableStore(state => ({...state})); 33 | const pinStyle = getCommonPinningStyles(cell.column); 34 | const combinedStyle: CSSProperties = { 35 | opacity: isDragging ? 0.8 : 1, 36 | position: "relative", 37 | transform: CSS.Translate.toString(transform), 38 | transition: "width transform 0.2s ease-in-out", 39 | width: cell.column.getSize(), 40 | zIndex: isDragging ? 1 : 0, 41 | ...(pinStyle || {}), 42 | alignContent: "center" 43 | }; 44 | const onContextMenuItemClick = (event: React.MouseEvent, handler?: (probably: T) => void) => { 45 | event.stopPropagation(); 46 | handler && handler(cell.row.original); 47 | }; 48 | const showContextMenu = isSelecting !== true && contextMenuProps !== undefined; 49 | if (showContextMenu) { 50 | return ( 51 | 52 | 53 | 54 | {flexRender( 55 | cell.column.columnDef.cell, 56 | cell.getContext() 57 | )} 58 | 59 | 60 | { 61 | contextMenuProps.enableEdit && ( 62 | 63 | ) 64 | } 65 | { 66 | !_.isEmpty(contextMenuProps?.extra) && ( 67 | 68 | ) 69 | } 70 | { 71 | !_.isEmpty(contextMenuProps?.extra) && ( 72 | 73 | 74 | More Tools 75 | 76 | 77 | { 78 | Object.keys(contextMenuProps?.extra).map((name, index) => 79 | onContextMenuItemClick(event, contextMenuProps.extra[name], true)} 82 | key={"Sub-data-table-context-menu-item-".concat(index.toString())}> 83 | {name} 84 | ) 85 | } 86 | 87 | 88 | ) 89 | } 90 | { 91 | contextMenuProps.enableDelete && ( 92 | 93 | ) 94 | } 95 | { 96 | contextMenuProps.enableDelete && ( 97 | contextMenuProps.onDelete(cell.row.original)} multiple={false}> 98 | 99 | Delete Row 100 | 101 | 102 | ) 103 | } 104 | 105 | 106 | 107 | ); 108 | } 109 | return ( 110 | 111 | {flexRender( 112 | cell.column.columnDef.cell, 113 | cell.getContext() 114 | )} 115 | 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /components/data-table/data-table-checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import {HTMLProps, useEffect, useRef} from "react"; 5 | 6 | export function DataTableCheckBox({ 7 | indeterminate, 8 | className = "", 9 | ...rest 10 | }: { indeterminate?: boolean } & HTMLProps) { 11 | const ref = useRef(null!); 12 | 13 | useEffect(() => { 14 | if (typeof indeterminate === "boolean") { 15 | ref.current.indeterminate = !rest.checked && indeterminate; 16 | } 17 | }, [ref, indeterminate]); 18 | 19 | return ( 20 | 26 | ); 27 | } -------------------------------------------------------------------------------- /components/data-table/data-table-column-visibility.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {MixerHorizontalIcon} from "@radix-ui/react-icons"; 4 | import {Button} from "@/components/ui/button"; 5 | import { 6 | DropdownMenu, 7 | DropdownMenuCheckboxItem, 8 | DropdownMenuContent, 9 | DropdownMenuTrigger 10 | } from "@/components/ui/dropdown-menu"; 11 | import {DataTableViewOptionsProps} from "@/interface/IDataTable"; 12 | 13 | export function DataTableColumnVisibility({table}: DataTableViewOptionsProps) { 14 | return ( 15 | 16 | 17 | 25 | 26 | 27 | {table 28 | .getAllColumns() 29 | .filter( 30 | (column) => 31 | typeof column.accessorFn !== "undefined" && column.getCanHide() 32 | ) 33 | .map((column) => { 34 | return ( 35 | column.toggleVisibility(!!value)} 40 | > 41 | {String(column.columnDef.header)} 42 | 43 | ); 44 | })} 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /components/data-table/data-table-delete-confirmation.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, {ReactNode} from "react"; 4 | import { 5 | AlertDialog, 6 | AlertDialogAction, 7 | AlertDialogCancel, 8 | AlertDialogContent, 9 | AlertDialogDescription, 10 | AlertDialogFooter, 11 | AlertDialogHeader, 12 | AlertDialogTitle, 13 | AlertDialogTrigger 14 | } from "@/components/ui/alert-dialog"; 15 | 16 | export function RequestDeleteConfirmation({children, onConfirm, multiple}: { 17 | children: ReactNode; 18 | onConfirm: () => void; 19 | multiple?: boolean; 20 | }) { 21 | return ( 22 | 23 | 24 | {children} 25 | 26 | 27 | 28 | This will permanently delete {multiple ? "all selected rows" : "the selected row"}. 29 | Are you sure you want to 30 | proceed? 31 | 32 | Are you sure you want to delete {multiple ? "all selected rows" : "the selected row"}? You 33 | won't be able to undo this action. 34 | 35 | 36 | 37 | Cancel 38 | Delete 39 | 40 | 41 | 42 | ); 43 | } -------------------------------------------------------------------------------- /components/data-table/data-table-edit-row.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger} from "@/components/ui/sheet"; 4 | import {Button} from "@/components/ui/button"; 5 | import {ListPlusIcon} from "lucide-react"; 6 | import React from "react"; 7 | import {useDataTableStore} from "@/store/dataTableStore"; 8 | import {z} from "zod"; 9 | import {useForm} from "react-hook-form"; 10 | import {zodResolver} from "@hookform/resolvers/zod"; 11 | import _ from "lodash"; 12 | import {Separator} from "@/components/ui/separator"; 13 | import {DataTableForm} from "@/components/data-table/data-table-form"; 14 | import ReactHotkeys from "react-hot-keys"; 15 | import {ContextMenuItem, ContextMenuNotItem, ContextMenuShortcut} from "@/components/ui/context-menu"; 16 | 17 | export function DataTableEditRow({presetData}:{presetData: {[k:string]: any;}}) { 18 | const {title, description, onSubmitEditData, schemas} = useDataTableStore(state => ({ 19 | ...state.editDataProps, 20 | schemas: state.dataValidationProps 21 | })); 22 | const getFormSchema = () => { 23 | const defaultValues: { [k: string]: string } = {}; 24 | if (schemas instanceof Array && schemas.length > 0) { 25 | const obj: { [k: string]: z.ZodType } = {}; 26 | schemas.forEach(item => { 27 | obj[item.id] = item.schema; 28 | defaultValues[item.id] = (item.id in presetData) ? presetData[item.id] : ""; 29 | }); 30 | return {schema: z.object(obj), defaultValues}; 31 | } 32 | return {schema: z.object({}), defaultValues}; 33 | }; 34 | const FormSchema = getFormSchema(); 35 | const form = useForm>({ 36 | resolver: zodResolver(FormSchema.schema), 37 | defaultValues: FormSchema.defaultValues, 38 | }); 39 | const onSubmit = (data: z.infer) => { 40 | onSubmitEditData && onSubmitEditData(data); 41 | }; 42 | return ( 43 | 44 | event.stopPropagation()}> 45 | 46 | Edit Row 47 | 48 | 49 | 50 | 51 | {title ?? "Create a new record in the list"} 52 | { 53 | !_.isEmpty(description) && ( 54 | 55 | {description} 56 | 57 | ) 58 | } 59 | 60 | 61 | { 62 | Object.keys(FormSchema.defaultValues).length > 0 && schemas !== undefined && ( 63 | 64 | ) 65 | } 66 | 67 | 68 | ); 69 | } -------------------------------------------------------------------------------- /components/data-table/data-table-export.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {DownloadIcon} from "lucide-react"; 4 | import {Button} from "@/components/ui/button"; 5 | import {useDataTableStore} from "@/store/dataTableStore"; 6 | import {exportExcel, exportExcelData} from "@/lib/exportExcel"; 7 | import {IDataTableExport} from "@/interface/IDataTable"; 8 | 9 | export function DataTableExport({table, onUserExport}: IDataTableExport) { 10 | const {exportProps} = useDataTableStore(state => ({...state})); 11 | const onPress = () => { 12 | const data = exportExcelData(table.options.data, table.getAllColumns(), exportProps?.excludeColumns ?? []); 13 | if (onUserExport) { 14 | onUserExport(data); 15 | } else { 16 | exportExcel(data, exportProps?.exportFileName ?? ""); 17 | } 18 | }; 19 | 20 | return ( 21 | 30 | ); 31 | } -------------------------------------------------------------------------------- /components/data-table/data-table-filter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {Column} from "@tanstack/react-table"; 4 | import * as React from "react"; 5 | import {useMemo} from "react"; 6 | import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"; 7 | import {DataTableInput} from "@/components/data-table/data-table-input"; 8 | 9 | export function DataTableFilter({column}: { column: Column }) { 10 | const {filterVariant} = column.columnDef.meta ?? {}; 11 | 12 | const columnFilterValue = column.getFilterValue(); 13 | 14 | const sortedUniqueValues = useMemo( 15 | () => 16 | filterVariant === "range" 17 | ? [] 18 | : Array.from(column.getFacetedUniqueValues().keys()) 19 | .sort() 20 | .slice(0, 5000), 21 | [column.getFacetedUniqueValues(), filterVariant] 22 | ); 23 | 24 | return filterVariant === "range" ? ( 25 |
26 |
27 | 33 | column.setFilterValue((old: [number, number]) => [value, old?.[1]]) 34 | } 35 | placeholder={`Min ${ 36 | column.getFacetedMinMaxValues()?.[0] !== undefined 37 | ? `(${column.getFacetedMinMaxValues()?.[0]})` 38 | : "" 39 | }`} 40 | className="w-6/12 border shadow rounded" 41 | /> 42 | 48 | column.setFilterValue((old: [number, number]) => [old?.[0], value]) 49 | } 50 | placeholder={`Max ${ 51 | column.getFacetedMinMaxValues()?.[1] 52 | ? `(${column.getFacetedMinMaxValues()?.[1]})` 53 | : "" 54 | }`} 55 | className="w-6/12 border shadow rounded" 56 | /> 57 |
58 |
59 |
60 | ) : filterVariant === "select" ? ( 61 | 78 | ) : ( 79 | <> 80 | 81 | {sortedUniqueValues.map((value: any) => ( 82 | 85 | column.setFilterValue(value)} 89 | placeholder={`Search... (${column.getFacetedUniqueValues().size})`} 90 | className="w-full border shadow rounded" 91 | list={column.id + "list"} 92 | /> 93 |
94 | 95 | ); 96 | } -------------------------------------------------------------------------------- /components/data-table/data-table-floating-bar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import {Button} from "@/components/ui/button"; 5 | import {DownloadIcon, ListXIcon, TrashIcon, XIcon} from "lucide-react"; 6 | import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip"; 7 | import {exportExcel, exportExcelData} from "@/lib/exportExcel"; 8 | import {useDataTableStore} from "@/store/dataTableStore"; 9 | import {IDataTableFloatingBar} from "@/interface/IDataTable"; 10 | import {format, parseISO} from "date-fns"; 11 | import {RequestDeleteConfirmation} from "@/components/data-table/data-table-delete-confirmation"; 12 | 13 | export function DataTableFloatingBar({table, onUserExport, onDelete}: IDataTableFloatingBar) { 14 | const {exportProps} = useDataTableStore(state => ({...state})); 15 | const isFiltered = table.getState().columnFilters.length > 0 || !!table.getState().globalFilter; 16 | const isRowSelected = table.getIsSomeRowsSelected() || table.getIsAllRowsSelected(); 17 | const naming: { [k: string]: any } = {}; 18 | for (let i = 0; i < table.options.columns.length; i++) { 19 | const col = table.options.columns[i]; 20 | naming[col.id as string] = col.header as string; 21 | } 22 | 23 | const currentFilters = table.getState().columnFilters.map((item: any, _index: number) => { 24 | const fieldName = naming[item.id]; 25 | if (item.value instanceof Array) { 26 | item.value = item.value.map((ii: any, _idx: number) => !ii ? "♾️" : ii); 27 | // range filter 28 | return { 29 | columnId: item.id, 30 | filter: `${fieldName} In Range Of ( ${item.value.join(" - ")} )` 31 | }; 32 | } 33 | if (typeof item.value === "string") { 34 | // either search string or select 35 | return { 36 | columnId: item.id, 37 | filter: `${fieldName} Equals/Contains '${item.value}'` 38 | }; 39 | } 40 | if (typeof item.value === "object" && item.value !== null && !(item.value instanceof Array)) { 41 | if (Object.keys(item.value).includes("from")) { 42 | // datetime 43 | if (typeof item.value.from === "string") { 44 | item.value.from = format(parseISO(item.value.from), "yyyy/MM/dd"); 45 | item.value.to = format(parseISO(item.value.to), "yyyy/MM/dd"); 46 | } else { 47 | item.value.from = format(item.value.from, "yyyy/MM/dd"); 48 | item.value.to = format(item.value.to, "yyyy/MM/dd"); 49 | } 50 | return { 51 | columnId: item.id, 52 | filter: `${fieldName} Is Between ( ${item.value.from} - ${item.value.to} )` 53 | }; 54 | } 55 | } 56 | return item; 57 | }); 58 | const onRemoveColumnFilter = (columnId: string) => { 59 | table.setColumnFilters(table.getState().columnFilters.filter(item => item.id !== columnId)); 60 | }; 61 | const onPressResetFilter = () => { 62 | table.resetColumnFilters(); 63 | table.resetGlobalFilter(); 64 | }; 65 | const onDeleteInner = () => { 66 | const rows = table.getSelectedRowModel().rows.map(item => item.original); 67 | onDelete && onDelete(rows); 68 | }; 69 | const onExport = () => { 70 | const rows = table.getSelectedRowModel().rows.map(item => item.original); 71 | const data = exportExcelData(rows, table.getAllColumns(), exportProps?.excludeColumns ?? []); 72 | if (onUserExport) { 73 | onUserExport(data); 74 | } else { 75 | exportExcel(data, exportProps?.exportFileName ?? ""); 76 | } 77 | }; 78 | 79 | return ( 80 |
81 |
82 | { 83 | isFiltered && ( 84 |
85 |
86 | 91 |
92 | {currentFilters.length > 0 ? "●" : ""} 93 | { 94 | currentFilters.length > 0 && currentFilters.map((f: { 95 | columnId: string; 96 | filter: string; 97 | }, index: number) => 98 | 104 | ) 105 | } 106 |
107 | ) 108 | } 109 | { 110 | isRowSelected && ( 111 |
112 |
113 | 118 |
119 | ● 120 | { 121 | isRowSelected && onDelete !== undefined ? 122 | 123 | 124 | 125 | 128 | 129 | 130 | 131 |

Delete current rows

132 |
133 |
: null 134 | } 135 | 136 | 137 | 140 | 141 | 142 |

Export current rows

143 |
144 |
145 |
146 | ) 147 | } 148 |
149 |
150 | ); 151 | } -------------------------------------------------------------------------------- /components/data-table/data-table-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {ControllerRenderProps, UseFormReturn} from "react-hook-form"; 4 | import {z, ZodType} from "zod"; 5 | import {TDataTableDataValidation} from "@/@types"; 6 | import React, {useState} from "react"; 7 | import {Input} from "@/components/ui/input"; 8 | import { 9 | Select, 10 | SelectContent, 11 | SelectGroup, 12 | SelectItem, 13 | SelectLabel, 14 | SelectTrigger, 15 | SelectValue 16 | } from "@/components/ui/select"; 17 | import _ from "lodash"; 18 | import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover"; 19 | import {Button} from "@/components/ui/button"; 20 | import {CaretSortIcon, CheckIcon} from "@radix-ui/react-icons"; 21 | import {Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList} from "@/components/ui/command"; 22 | import {cn} from "@/lib/utils"; 23 | import {RadioGroup, RadioGroupItem} from "@/components/ui/radio-group"; 24 | import {Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form"; 25 | import {Checkbox} from "@/components/ui/checkbox"; 26 | 27 | export function DataTableForm>({schemas, form, onSubmit}:{schemas: TDataTableDataValidation[]; form: UseFormReturn<{},any,undefined>; onSubmit: z.infer}) { 28 | return ( 29 |
30 | 31 | { 32 | schemas.map((schemaProp, index) => { 33 | const { 34 | id, 35 | component, 36 | label, 37 | description 38 | } = schemaProp; 39 | if (component === "checkbox") { 40 | return ( 41 | 42 | ); 43 | } 44 | return ( 45 | ( 50 | 51 | { 52 | !_.isEmpty(label) && ( 53 | 54 | {label} 55 | 56 | ) 57 | } 58 | 59 | 60 | 61 | { 62 | !_.isEmpty(description) && ( 63 | 64 | {description} 65 | 66 | ) 67 | } 68 | 69 | 70 | )} 71 | /> 72 | ); 73 | }) 74 | } 75 |
76 | 77 | 78 | 79 | ); 80 | } 81 | 82 | function UserRequiredField({id: formId,placeholder,component,componentCssProps,data,label,description,formProps,formFieldProps}:Partial & { formFieldProps?: ControllerRenderProps<{},never>; formProps?: UseFormReturn<{}, any, undefined> }) { 83 | const [inStateOpen, setInStateOpen] = useState(false); 84 | const [inStateValue, setInStateValue] = useState(formFieldProps?.value || ""); 85 | if (component === "input") { 86 | return ( 87 | 88 | ); 89 | } 90 | if (component === "select" && formFieldProps) { 91 | return ( 92 | 109 | ); 110 | } 111 | if (component === "combobox" && formFieldProps && formProps) { 112 | const onSelect = (value1: string) => { 113 | setInStateValue(value1); 114 | setInStateOpen(false); 115 | formProps.setValue(formId as never, value1 as never); 116 | }; 117 | return ( 118 | 119 | 120 | 130 | 131 | 132 | 133 | 134 | 135 | No record found. 136 | 137 | { 138 | !_.isEmpty(data) && data?.map(({value, children},index) => { 139 | return ( 140 | 145 | {children} 146 | 152 | 153 | ); 154 | }) 155 | } 156 | 157 | 158 | 159 | 160 | 161 | ); 162 | } 163 | if (component === "radio" && formFieldProps) { 164 | return ( 165 | 169 | { 170 | !_.isEmpty(data) && data?.map(({value,children},index)=>{ 171 | return ( 172 | 173 | 174 | 175 | 176 | 177 | {children} 178 | 179 | 180 | ); 181 | }) 182 | } 183 | 184 | ); 185 | } 186 | if (component === "checkbox" && formProps) { 187 | return ( 188 | ( 192 | 193 |
194 | { 195 | !_.isEmpty(label) && ( 196 | {label} 197 | ) 198 | } 199 | { 200 | !_.isEmpty(description) && ( 201 | 202 | {description} 203 | 204 | ) 205 | } 206 |
207 | {(data || []).map((item) => ( 208 | { 213 | return ( 214 | 217 | 218 | { 221 | return checked 222 | ? field.onChange([...field.value, item.value]) 223 | : field.onChange( 224 | (field.value as any[])?.filter( 225 | (value: string) => value !== item.value 226 | ) 227 | ) 228 | }} 229 | /> 230 | 231 | 232 | {item.children} 233 | 234 | 235 | ) 236 | }} 237 | /> 238 | ))} 239 | 240 |
241 | )} 242 | /> 243 | ); 244 | } 245 | return null; 246 | } -------------------------------------------------------------------------------- /components/data-table/data-table-header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { flexRender, Header, Table } from "@tanstack/react-table"; 4 | import { useSortable } from "@dnd-kit/sortable"; 5 | import { CSSProperties } from "react"; 6 | import { CSS } from "@dnd-kit/utilities"; 7 | import { TableHead } from "@/components/ui/table"; 8 | import { Button } from "@/components/ui/button"; 9 | import { 10 | ArrowDownIcon, 11 | ArrowDownNarrowWideIcon, 12 | ArrowUpIcon, 13 | ArrowUpNarrowWideIcon, 14 | EyeOffIcon, 15 | FilterIcon, 16 | GripVerticalIcon, 17 | MoveLeftIcon, 18 | MoveRightIcon, 19 | PinIcon, 20 | PinOffIcon 21 | } from "lucide-react"; 22 | import { CaretSortIcon } from "@radix-ui/react-icons"; 23 | import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; 24 | import { DataTableFilter } from "@/components/data-table/data-table-filter"; 25 | import { DataTableInputDate } from "@/components/data-table/data-table-input-date"; 26 | import { getCommonPinningStyles } from "@/lib/columns"; 27 | import { 28 | ContextMenu, 29 | ContextMenuContent, 30 | ContextMenuItem, 31 | ContextMenuSeparator, 32 | ContextMenuShortcut, 33 | ContextMenuTrigger 34 | } from "@/components/ui/context-menu"; 35 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; 36 | import { useDataTableStore } from "@/store/dataTableStore"; 37 | 38 | export function DataTableHeader({ header, table }: { header: Header; table: Table }) { 39 | const { isSelecting } = useDataTableStore(state => ({ ...state })); 40 | const { attributes, isDragging, listeners, setNodeRef, transform } = 41 | useSortable({ 42 | id: header.column.id, 43 | }); 44 | const { column, isPlaceholder } = header; 45 | 46 | const pinStyle = getCommonPinningStyles(column); 47 | const combinedStyles: CSSProperties = { 48 | opacity: isDragging ? 0.8 : 1, 49 | position: "relative", 50 | transform: CSS.Translate.toString(transform), // translate instead of transform to avoid squishing 51 | transition: "width transform 0.2s ease-in-out", 52 | whiteSpace: "nowrap", 53 | width: header.column.getSize(), 54 | zIndex: isDragging ? 1 : 0, 55 | ...(pinStyle || {}), 56 | alignContent: "center" 57 | }; 58 | 59 | if (isSelecting) { 60 | return ( 61 | 66 | { flexRender( 67 | column.columnDef.header, 68 | header.getContext() 69 | )} 70 | 71 | ); 72 | } 73 | 74 | return ( 75 | 80 | 81 | 82 |
83 | { column.getIsPinned() === false ? 84 | 85 | 86 | 88 | 89 | 90 |

Rearrange this column

91 |
92 |
: null 93 | } 94 | 95 | 96 | { isPlaceholder ? null : 97 | 121 | } 122 | 123 | 124 |

Sort rows by this column

125 |
126 |
127 | 128 | 129 | 130 | 131 | 132 |

Filter rows by this column

133 |
134 |
135 | 136 | 137 | { column.getIsPinned() ? 138 | column.pin(false)} 140 | /> : 141 | column.pin("left")}/> 142 | } 143 | 144 | 145 |

{ column.getIsPinned() ? "Unpin this column" : "Pin this column" }

146 |
147 |
148 |
149 |
150 | 151 | { column.getIsPinned() ? 152 | column.pin(false)}> 153 | Unpin 154 | 155 | 156 | 157 | : null 158 | } 159 | { column.getIsPinned() ? null : 160 | column.pin("left")}> 161 | Pin Left 162 | 163 | 164 | 165 | 166 | } 167 | { column.getIsPinned() ? null : 168 | column.pin("right")}> 169 | Pin Right 170 | 171 | 172 | 173 | 174 | } 175 | 176 | { column.getIsSorted() === "asc" ? null : 177 | column.toggleSorting(false)}> 178 | Sort Ascending 179 | 180 | 181 | 182 | 183 | } 184 | { column.getIsSorted() === "desc" ? null : 185 | column.toggleSorting(true)}> 186 | Sort Descending 187 | 188 | 189 | 190 | 191 | } 192 | { column.getIsSorted() === "asc" || column.getIsSorted() === "desc" ? 193 | column.clearSorting()}> 194 | Clear Sorting 195 | 196 | 197 | 198 | : null 199 | } 200 | 201 | column.toggleVisibility()}> 202 | Hide 203 | 204 | 205 | 206 | 207 | 208 |
209 |
header.column.resetSize(), 211 | onMouseDown: header.getResizeHandler(), 212 | onTouchStart: header.getResizeHandler(), 213 | className: `resizer ltr ${header.column.getIsResizing() ? "isResizing" : ""}`, 214 | }} 215 | /> 216 | 217 | ); 218 | } 219 | 220 | function FilterPopover({ header }: { header: Header; }) { 221 | const { column } = header; 222 | const { filterVariant } = column.columnDef.meta ?? {}; 223 | return ( 224 | 225 | 226 | 227 | 228 | 229 |
230 |
231 |

Column filter

232 |

233 | Filter will be applied to current column only. 234 |

235 |
236 | { header.column.getCanFilter() ? 237 | filterVariant === "date" ? 238 | 239 | : 240 | : 241 | null 242 | } 243 |
244 |
245 |
246 | ); 247 | } 248 | -------------------------------------------------------------------------------- /components/data-table/data-table-input-date.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, {useState} from "react"; 4 | import {addDays} from "date-fns"; 5 | import {DateRange} from "react-day-picker"; 6 | import {Column} from "@tanstack/react-table"; 7 | import {Calendar} from "@/components/ui/calendar"; 8 | 9 | export function DataTableInputDate({column}: { column: Column }) { 10 | const [date, setDate] = useState({ 11 | from: new Date(), 12 | to: addDays(new Date(), 20), 13 | }); 14 | 15 | const onDateChange = (date: DateRange | undefined) => { 16 | setDate(date); 17 | column.setFilterValue(date); 18 | }; 19 | 20 | return ( 21 | 28 | ); 29 | } -------------------------------------------------------------------------------- /components/data-table/data-table-input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import {InputHTMLAttributes, useEffect, useState} from "react"; 5 | import {Input} from "@/components/ui/input"; 6 | 7 | export function DataTableInput({ 8 | value: initialValue, 9 | onChange, 10 | debounce = 500, 11 | ...props 12 | }: { 13 | value: string | number 14 | onChange: (value: string | number) => void 15 | debounce?: number 16 | } & Omit, "onChange">) { 17 | const [value, setValue] = useState(initialValue); 18 | 19 | useEffect(() => { 20 | setValue(initialValue); 21 | }, [initialValue]); 22 | 23 | useEffect(() => { 24 | const timeout = setTimeout(() => { 25 | onChange(value); 26 | }, debounce); 27 | 28 | return () => clearTimeout(timeout); 29 | }, [value]); 30 | 31 | return ( 32 | setValue(e.target.value)}/> 33 | ); 34 | } -------------------------------------------------------------------------------- /components/data-table/data-table-pagination.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {ChevronLeftIcon, ChevronRightIcon, DoubleArrowLeftIcon, DoubleArrowRightIcon,} from "@radix-ui/react-icons"; 4 | import {Button} from "@/components/ui/button"; 5 | import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select"; 6 | import {DataTablePaginationProps} from "@/interface/IDataTable"; 7 | 8 | export function DataTablePagination({ 9 | table, 10 | pageSizeOptions = [10, 20, 30, 40, 50, 100], 11 | }: DataTablePaginationProps) { 12 | return ( 13 |
15 | { 16 | table.getFilteredSelectedRowModel().rows.length > 0 ? 17 |
18 | {table.getFilteredSelectedRowModel().rows.length.toLocaleString()} of{" "} 19 | {table.getFilteredRowModel().rows.length.toLocaleString()} row(s) selected. 20 |
:
21 | } 22 |
23 |
24 |

Rows per page

25 | 41 |
42 |
43 | Page {table.getState().pagination.pageIndex + 1} of{" "} 44 | {table.getPageCount().toLocaleString()} 45 |
46 |
47 | 56 | 66 | 76 | 86 |
87 |
88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /components/data-table/data-table-selections.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import {Button} from "@/components/ui/button"; 4 | import {ListChecksIcon, ListXIcon} from "lucide-react"; 5 | import {useDataTableStore} from "@/store/dataTableStore"; 6 | import {Table} from "@tanstack/react-table"; 7 | 8 | export function DataTableSelections({table}: { table: Table }) { 9 | const {toggleSelection, isSelecting} = useDataTableStore(state => ({...state})); 10 | const onPress = () => { 11 | toggleSelection(); 12 | table.resetRowSelection(); 13 | }; 14 | return ( 15 | 24 | ); 25 | } -------------------------------------------------------------------------------- /components/data-table/data-table-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import {cn} from "@/lib/utils"; 2 | import {Skeleton} from "@/components/ui/skeleton"; 3 | import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/components/ui/table"; 4 | 5 | interface DataTableSkeletonProps extends React.HTMLAttributes { 6 | /** 7 | * The number of columns in the table. 8 | * @type number 9 | */ 10 | columnCount: number 11 | 12 | /** 13 | * The number of rows in the table. 14 | * @default 10 15 | * @type number | undefined 16 | */ 17 | rowCount?: number 18 | 19 | /** 20 | * The number of searchable columns in the table. 21 | * @default 0 22 | * @type number | undefined 23 | */ 24 | searchableColumnCount?: number 25 | 26 | /** 27 | * The number of filterable columns in the table. 28 | * @default 0 29 | * @type number | undefined 30 | */ 31 | filterableColumnCount?: number 32 | 33 | /** 34 | * Flag to show the table view options. 35 | * @default undefined 36 | * @type boolean | undefined 37 | */ 38 | showViewOptions?: boolean 39 | 40 | /** 41 | * The width of each cell in the table. 42 | * The length of the array should be equal to the columnCount. 43 | * Any valid CSS width value is accepted. 44 | * @default ["auto"] 45 | * @type string[] | undefined 46 | */ 47 | cellWidths?: string[] 48 | 49 | /** 50 | * Flag to show the pagination bar. 51 | * @default true 52 | * @type boolean | undefined 53 | */ 54 | withPagination?: boolean 55 | 56 | /** 57 | * Flag to prevent the table cells from shrinking. 58 | * @default false 59 | * @type boolean | undefined 60 | */ 61 | shrinkZero?: boolean 62 | } 63 | 64 | export function DataTableSkeleton(props: DataTableSkeletonProps) { 65 | const { 66 | columnCount, 67 | rowCount = 10, 68 | searchableColumnCount = 1, 69 | filterableColumnCount = 0, 70 | showViewOptions = true, 71 | cellWidths = ["auto"], 72 | withPagination = true, 73 | shrinkZero = false, 74 | className, 75 | ...skeletonProps 76 | } = props; 77 | 78 | return ( 79 |
83 |
84 |
85 | {searchableColumnCount > 0 86 | ? Array.from({length: searchableColumnCount}).map((_, i) => ( 87 | 88 | )) 89 | : null} 90 | {filterableColumnCount > 0 91 | ? Array.from({length: filterableColumnCount}).map((_, i) => ( 92 | 93 | )) 94 | : null} 95 |
96 | {showViewOptions ? ( 97 | 98 | ) : null} 99 |
100 |
101 | 102 | 103 | {Array.from({length: 1}).map((_, i) => ( 104 | 105 | {Array.from({length: columnCount}).map((_, j) => ( 106 | 113 | 114 | 115 | ))} 116 | 117 | ))} 118 | 119 | 120 | {Array.from({length: rowCount}).map((_, i) => ( 121 | 122 | {Array.from({length: columnCount}).map((_, j) => ( 123 | 130 | 131 | 132 | ))} 133 | 134 | ))} 135 | 136 |
137 |
138 | {withPagination ? ( 139 |
140 | 141 |
142 |
143 | 144 | 145 |
146 |
147 | 148 |
149 |
150 | 151 | 152 | 153 | 154 |
155 |
156 |
157 | ) : null} 158 |
159 | ); 160 | } 161 | -------------------------------------------------------------------------------- /components/data-table/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | ColumnDef, 5 | ColumnFiltersState, 6 | getCoreRowModel, 7 | getFacetedMinMaxValues, 8 | getFacetedRowModel, 9 | getFacetedUniqueValues, 10 | getFilteredRowModel, 11 | getPaginationRowModel, 12 | getSortedRowModel, 13 | useReactTable, 14 | VisibilityState 15 | } from "@tanstack/react-table"; 16 | import {useDataTableStore} from "@/store/dataTableStore"; 17 | import * as React from "react"; 18 | import {useEffect, useRef, useState} from "react"; 19 | import {fuzzyFilter} from "@/lib/utils"; 20 | import { 21 | closestCenter, 22 | DndContext, 23 | DragEndEvent, 24 | KeyboardSensor, 25 | MouseSensor, 26 | TouchSensor, 27 | useSensor, 28 | useSensors 29 | } from "@dnd-kit/core"; 30 | import {arrayMove, horizontalListSortingStrategy, SortableContext} from "@dnd-kit/sortable"; 31 | import {restrictToHorizontalAxis} from "@dnd-kit/modifiers"; 32 | import {DataTableInput} from "@/components/data-table/data-table-input"; 33 | import {DataTableExport} from "@/components/data-table/data-table-export"; 34 | import {DataTableColumnVisibility} from "@/components/data-table/data-table-column-visibility"; 35 | import {Table, TableHead, TableHeader, TableRow} from "@/components/ui/table"; 36 | import {DataTableHeader} from "@/components/data-table/data-table-header"; 37 | import {DataTableBody} from "@/components/data-table/data-table-body"; 38 | import {DataTablePagination} from "@/components/data-table/data-table-pagination"; 39 | import {DataTableFloatingBar} from "@/components/data-table/data-table-floating-bar"; 40 | import {FilterFn} from "@tanstack/table-core"; 41 | import {RankingInfo} from "@tanstack/match-sorter-utils"; 42 | import {SlashIcon} from "lucide-react"; 43 | import {DataTableSelections} from "@/components/data-table/data-table-selections"; 44 | import _ from "lodash"; 45 | import {IAdvancedDataTable} from "@/interface/IDataTable"; 46 | import {DataTableSkeleton} from "@/components/data-table/data-table-skeleton"; 47 | import {DataTableAddRow} from "@/components/data-table/data-table-add-row"; 48 | import {useVirtualizer} from "@tanstack/react-virtual"; 49 | 50 | declare module "@tanstack/react-table" { 51 | interface ColumnMeta { 52 | filterVariant?: "text" | "range" | "select" | "date"; 53 | } 54 | 55 | interface FilterFns { 56 | fuzzy: FilterFn 57 | } 58 | 59 | interface FilterMeta { 60 | itemRank: RankingInfo 61 | } 62 | } 63 | 64 | export function AdvancedDataTable(props: IAdvancedDataTable) { 65 | const { 66 | columns, 67 | data, 68 | id 69 | } = props; 70 | if (_.isEmpty(id.trim())) { 71 | throw new Error("AdvancedDataTable required field missing `id`. Must be an unique identifier"); 72 | } 73 | const {isSelecting, setExtraProps} = useDataTableStore(state => ({ 74 | ...state 75 | })); 76 | const [columnFilters, setColumnFilters] = useState( 77 | [] 78 | ); 79 | const [columnVisibility, setColumnVisibility] = 80 | useState({}); 81 | const [columnPinning, setColumnPinning] = useState({}); 82 | const [columnOrder, setColumnOrder] = useState(() => 83 | columns.map(c => c.id!) 84 | ); 85 | const [internalColumns, setInternalColumns] = useState[]>([]); 86 | const [globalFilter, setGlobalFilter] = useState(""); 87 | const [rowSelection, setRowSelection] = useState({}); 88 | 89 | useEffect(() => { 90 | if (isSelecting) { 91 | setInternalColumns(columns); 92 | } else { 93 | setInternalColumns(columns.filter(item => item.id !== "select").map(item => ({...item, size: item.size ?? 200}))); 94 | } 95 | }, [columns, isSelecting]); 96 | 97 | useEffect(() => { 98 | setExtraProps( 99 | props?.exportProps, 100 | props?.contextMenuProps, 101 | props?.addDataProps, 102 | props?.editDataProps, 103 | props?.dataValidationProps 104 | ); 105 | }, [props]); 106 | 107 | const table = useReactTable({ 108 | data: data, 109 | columns: internalColumns, 110 | state: { 111 | columnFilters, 112 | columnOrder, 113 | columnVisibility, 114 | columnPinning, 115 | globalFilter, 116 | rowSelection 117 | }, 118 | filterFns: { 119 | fuzzy: fuzzyFilter 120 | }, 121 | // globalFilterFn: "fuzzy", 122 | onGlobalFilterChange: setGlobalFilter, 123 | onRowSelectionChange: setRowSelection, 124 | onColumnVisibilityChange: setColumnVisibility, 125 | onColumnPinningChange: setColumnPinning, 126 | onColumnOrderChange: setColumnOrder, 127 | onColumnFiltersChange: setColumnFilters, 128 | columnResizeMode: "onChange", 129 | columnResizeDirection:"ltr", 130 | getCoreRowModel: getCoreRowModel(), 131 | getFilteredRowModel: getFilteredRowModel(), 132 | getSortedRowModel: getSortedRowModel(), 133 | getPaginationRowModel: getPaginationRowModel(), 134 | getFacetedRowModel: getFacetedRowModel(), 135 | getFacetedUniqueValues: getFacetedUniqueValues(), 136 | getFacetedMinMaxValues: getFacetedMinMaxValues(), 137 | }); 138 | 139 | function onDragEnd(event: DragEndEvent) { 140 | const {active, over} = event; 141 | if (active && over && active.id !== over.id) { 142 | setColumnOrder(columnOrder => { 143 | const oldIndex = columnOrder.indexOf(active.id as string); 144 | const newIndex = columnOrder.indexOf(over.id as string); 145 | return arrayMove(columnOrder, oldIndex, newIndex); //this is just a splice util 146 | }); 147 | } 148 | } 149 | 150 | const sensors = useSensors( 151 | useSensor(MouseSensor, {}), 152 | useSensor(TouchSensor, {}), 153 | useSensor(KeyboardSensor, {}) 154 | ); 155 | 156 | const isFiltered = table.getState().columnFilters.length > 0 || !!table.getState().globalFilter; 157 | const isRowSelected = table.getIsSomeRowsSelected() || table.getIsAllRowsSelected(); 158 | 159 | const tableContainerRef = useRef(null); 160 | const visibleColumns = table.getVisibleLeafColumns(); 161 | const columnVirtualizer = useVirtualizer({ 162 | count: visibleColumns.length, 163 | estimateSize: index => visibleColumns[index].getSize(), 164 | getScrollElement: () => tableContainerRef.current, 165 | horizontal: true, 166 | overscan: 50 167 | }); 168 | const rowVirtualizer = useVirtualizer({ 169 | count: table.getRowModel().rows.length, 170 | estimateSize: () => 33, 171 | getScrollElement: () => tableContainerRef.current, 172 | measureElement: 173 | typeof window !== "undefined" && 174 | navigator.userAgent.indexOf("Firefox") === -1 175 | ? element => element?.getBoundingClientRect().height 176 | : undefined, 177 | overscan: 100 178 | }); 179 | const virtualColumns = columnVirtualizer.getVirtualItems(); 180 | let virtualPaddingLeft: number | undefined; 181 | let virtualPaddingRight: number | undefined; 182 | 183 | if (columnVirtualizer && virtualColumns?.length) { 184 | virtualPaddingLeft = virtualColumns[0]?.start ?? 0; 185 | virtualPaddingRight = 186 | columnVirtualizer.getTotalSize() - 187 | (virtualColumns[virtualColumns.length - 1]?.end ?? 0); 188 | } 189 | 190 | if (props?.isLoading) { 191 | return ( 192 | 194 | ); 195 | } 196 | 197 | return ( 198 | 203 |
204 |
205 |
206 | setGlobalFilter(String(value))} 209 | className="p-2 font-lg border border-block" 210 | placeholder="Filter anything..." 211 | /> 212 |
213 |
214 | 215 | 216 | 217 | 218 | { 219 | props?.exportProps && ( 220 | 224 | ) 225 | } 226 |
227 |
228 |
236 | 237 | 243 | {table.getHeaderGroups().map(headerGroup => ( 244 | 245 | {virtualPaddingLeft ? ( 246 | 247 | ) : null} 248 | 251 | {virtualColumns.map(vc => { 252 | const header = headerGroup.headers[vc.index]; 253 | return ( 254 | 255 | ); 256 | })} 257 | 258 | {virtualPaddingRight ? ( 259 | 260 | ) : null} 261 | 262 | ))} 263 | 264 | 272 |
273 |
274 |
275 | 276 |
277 | {(isFiltered || isRowSelected) && ( 278 | 279 | onUserExport={props.actionProps?.onUserExport} 280 | onDelete={props.actionProps?.onDelete} 281 | table={table}/> 282 | )} 283 | 284 | ); 285 | } -------------------------------------------------------------------------------- /components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | import { buttonVariants } from "@/components/ui/button"; 8 | 9 | const AlertDialog = AlertDialogPrimitive.Root; 10 | 11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger; 12 | 13 | const AlertDialogPortal = AlertDialogPrimitive.Portal; 14 | 15 | const AlertDialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )); 28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; 29 | 30 | const AlertDialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, ...props }, ref) => ( 34 | 35 | 36 | 44 | 45 | )); 46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; 47 | 48 | const AlertDialogHeader = ({ 49 | className, 50 | ...props 51 | }: React.HTMLAttributes) => ( 52 |
59 | ); 60 | AlertDialogHeader.displayName = "AlertDialogHeader"; 61 | 62 | const AlertDialogFooter = ({ 63 | className, 64 | ...props 65 | }: React.HTMLAttributes) => ( 66 |
73 | ); 74 | AlertDialogFooter.displayName = "AlertDialogFooter"; 75 | 76 | const AlertDialogTitle = React.forwardRef< 77 | React.ElementRef, 78 | React.ComponentPropsWithoutRef 79 | >(({ className, ...props }, ref) => ( 80 | 85 | )); 86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; 87 | 88 | const AlertDialogDescription = React.forwardRef< 89 | React.ElementRef, 90 | React.ComponentPropsWithoutRef 91 | >(({ className, ...props }, ref) => ( 92 | 97 | )); 98 | AlertDialogDescription.displayName = 99 | AlertDialogPrimitive.Description.displayName; 100 | 101 | const AlertDialogAction = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )); 111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; 112 | 113 | const AlertDialogCancel = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 126 | )); 127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; 128 | 129 | export { 130 | AlertDialog, 131 | AlertDialogPortal, 132 | AlertDialogOverlay, 133 | AlertDialogTrigger, 134 | AlertDialogContent, 135 | AlertDialogHeader, 136 | AlertDialogFooter, 137 | AlertDialogTitle, 138 | AlertDialogDescription, 139 | AlertDialogAction, 140 | AlertDialogCancel, 141 | }; 142 | -------------------------------------------------------------------------------- /components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ); 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )); 33 | Alert.displayName = "Alert"; 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )); 45 | AlertTitle.displayName = "AlertTitle"; 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )); 57 | AlertDescription.displayName = "AlertDescription"; 58 | 59 | export { Alert, AlertTitle, AlertDescription }; 60 | -------------------------------------------------------------------------------- /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 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", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | } 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"; 5 | import { DayPicker } from "react-day-picker"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | import { buttonVariants } from "@/components/ui/button"; 9 | 10 | export type CalendarProps = React.ComponentProps 11 | 12 | function Calendar({ 13 | className, 14 | classNames, 15 | showOutsideDays = true, 16 | ...props 17 | }: CalendarProps) { 18 | return ( 19 | .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" 43 | : "[&:has([aria-selected])]:rounded-md" 44 | ), 45 | day: cn( 46 | buttonVariants({ variant: "ghost" }), 47 | "h-8 w-8 p-0 font-normal aria-selected:opacity-100" 48 | ), 49 | day_range_start: "day-range-start", 50 | day_range_end: "day-range-end", 51 | day_selected: 52 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", 53 | day_today: "bg-accent text-accent-foreground", 54 | day_outside: 55 | "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30", 56 | day_disabled: "text-muted-foreground opacity-50", 57 | day_range_middle: 58 | "aria-selected:bg-accent aria-selected:text-accent-foreground", 59 | day_hidden: "invisible", 60 | ...classNames, 61 | }} 62 | components={{ 63 | IconLeft: ({ ...props }) => , 64 | IconRight: ({ ...props }) => , 65 | }} 66 | {...props} 67 | /> 68 | ); 69 | } 70 | Calendar.displayName = "Calendar"; 71 | 72 | export { Calendar }; 73 | -------------------------------------------------------------------------------- /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 { CheckIcon } from "@radix-ui/react-icons"; 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 | 24 | 25 | 26 | 27 | )); 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 29 | 30 | export { Checkbox }; 31 | -------------------------------------------------------------------------------- /components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { type DialogProps } from "@radix-ui/react-dialog"; 5 | import { MagnifyingGlassIcon } from "@radix-ui/react-icons"; 6 | import { Command as CommandPrimitive } from "cmdk"; 7 | 8 | import { cn } from "@/lib/utils"; 9 | import { Dialog, DialogContent } from "@/components/ui/dialog"; 10 | 11 | const Command = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )); 24 | Command.displayName = CommandPrimitive.displayName; 25 | 26 | interface CommandDialogProps extends DialogProps {} 27 | 28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => { 29 | return ( 30 | 31 | 32 | 33 | {children} 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | const CommandInput = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 |
45 | 46 | 54 |
55 | )); 56 | 57 | CommandInput.displayName = CommandPrimitive.Input.displayName; 58 | 59 | const CommandList = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, ...props }, ref) => ( 63 | 68 | )); 69 | 70 | CommandList.displayName = CommandPrimitive.List.displayName; 71 | 72 | const CommandEmpty = React.forwardRef< 73 | React.ElementRef, 74 | React.ComponentPropsWithoutRef 75 | >((props, ref) => ( 76 | 81 | )); 82 | 83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName; 84 | 85 | const CommandGroup = React.forwardRef< 86 | React.ElementRef, 87 | React.ComponentPropsWithoutRef 88 | >(({ className, ...props }, ref) => ( 89 | 97 | )); 98 | 99 | CommandGroup.displayName = CommandPrimitive.Group.displayName; 100 | 101 | const CommandSeparator = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )); 111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName; 112 | 113 | const CommandItem = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 125 | )); 126 | 127 | CommandItem.displayName = CommandPrimitive.Item.displayName; 128 | 129 | const CommandShortcut = ({ 130 | className, 131 | ...props 132 | }: React.HTMLAttributes) => { 133 | return ( 134 | 141 | ); 142 | }; 143 | CommandShortcut.displayName = "CommandShortcut"; 144 | 145 | export { 146 | Command, 147 | CommandDialog, 148 | CommandInput, 149 | CommandList, 150 | CommandEmpty, 151 | CommandGroup, 152 | CommandItem, 153 | CommandShortcut, 154 | CommandSeparator, 155 | }; 156 | -------------------------------------------------------------------------------- /components/ui/context-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; 5 | import { 6 | CheckIcon, 7 | ChevronRightIcon, 8 | DotFilledIcon, 9 | } from "@radix-ui/react-icons"; 10 | 11 | import { cn } from "@/lib/utils"; 12 | 13 | const ContextMenu = ContextMenuPrimitive.Root; 14 | 15 | const ContextMenuTrigger = ContextMenuPrimitive.Trigger; 16 | 17 | const ContextMenuGroup = ContextMenuPrimitive.Group; 18 | 19 | const ContextMenuPortal = ContextMenuPrimitive.Portal; 20 | 21 | const ContextMenuSub = ContextMenuPrimitive.Sub; 22 | 23 | const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; 24 | 25 | const ContextMenuSubTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef & { 28 | inset?: boolean 29 | } 30 | >(({ className, inset, children, ...props }, ref) => ( 31 | 40 | {children} 41 | 42 | 43 | )); 44 | ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; 45 | 46 | const ContextMenuSubContent = React.forwardRef< 47 | React.ElementRef, 48 | React.ComponentPropsWithoutRef 49 | >(({ className, ...props }, ref) => ( 50 | 58 | )); 59 | ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; 60 | 61 | const ContextMenuContent = React.forwardRef< 62 | React.ElementRef, 63 | React.ComponentPropsWithoutRef 64 | >(({ className, ...props }, ref) => ( 65 | 66 | 74 | 75 | )); 76 | ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; 77 | 78 | const ContextMenuItem = React.forwardRef< 79 | React.ElementRef, 80 | React.ComponentPropsWithoutRef & { 81 | inset?: boolean 82 | } 83 | >(({ className, inset, ...props }, ref) => ( 84 | 93 | )); 94 | ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; 95 | 96 | const ContextMenuCheckboxItem = React.forwardRef< 97 | React.ElementRef, 98 | React.ComponentPropsWithoutRef 99 | >(({ className, children, checked, ...props }, ref) => ( 100 | 109 | 110 | 111 | 112 | 113 | 114 | {children} 115 | 116 | )); 117 | ContextMenuCheckboxItem.displayName = 118 | ContextMenuPrimitive.CheckboxItem.displayName; 119 | 120 | const ContextMenuRadioItem = React.forwardRef< 121 | React.ElementRef, 122 | React.ComponentPropsWithoutRef 123 | >(({ className, children, ...props }, ref) => ( 124 | 132 | 133 | 134 | 135 | 136 | 137 | {children} 138 | 139 | )); 140 | ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; 141 | 142 | const ContextMenuLabel = React.forwardRef< 143 | React.ElementRef, 144 | React.ComponentPropsWithoutRef & { 145 | inset?: boolean 146 | } 147 | >(({ className, inset, ...props }, ref) => ( 148 | 157 | )); 158 | ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; 159 | 160 | const ContextMenuSeparator = React.forwardRef< 161 | React.ElementRef, 162 | React.ComponentPropsWithoutRef 163 | >(({ className, ...props }, ref) => ( 164 | 169 | )); 170 | ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; 171 | 172 | const ContextMenuShortcut = ({ 173 | className, 174 | ...props 175 | }: React.HTMLAttributes) => { 176 | return ( 177 | 184 | ); 185 | }; 186 | 187 | const ContextMenuNotItem = ({children}:{children: React.ReactNode}) => ( 188 |
189 | {children} 190 |
191 | ); 192 | 193 | ContextMenuShortcut.displayName = "ContextMenuShortcut"; 194 | 195 | export { 196 | ContextMenu, 197 | ContextMenuTrigger, 198 | ContextMenuContent, 199 | ContextMenuItem, 200 | ContextMenuCheckboxItem, 201 | ContextMenuRadioItem, 202 | ContextMenuLabel, 203 | ContextMenuSeparator, 204 | ContextMenuShortcut, 205 | ContextMenuGroup, 206 | ContextMenuPortal, 207 | ContextMenuSub, 208 | ContextMenuSubContent, 209 | ContextMenuSubTrigger, 210 | ContextMenuRadioGroup, 211 | ContextMenuNotItem 212 | }; 213 | -------------------------------------------------------------------------------- /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 { Cross2Icon } from "@radix-ui/react-icons"; 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 = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ); 68 | DialogHeader.displayName = "DialogHeader"; 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ); 82 | DialogFooter.displayName = "DialogFooter"; 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )); 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )); 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | }; 123 | -------------------------------------------------------------------------------- /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 { 6 | CheckIcon, 7 | ChevronRightIcon, 8 | DotFilledIcon, 9 | } from "@radix-ui/react-icons"; 10 | 11 | import { cn } from "@/lib/utils"; 12 | 13 | const DropdownMenu = DropdownMenuPrimitive.Root; 14 | 15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 16 | 17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group; 18 | 19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal; 20 | 21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub; 22 | 23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; 24 | 25 | const DropdownMenuSubTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef & { 28 | inset?: boolean 29 | } 30 | >(({ className, inset, children, ...props }, ref) => ( 31 | 40 | {children} 41 | 42 | 43 | )); 44 | DropdownMenuSubTrigger.displayName = 45 | DropdownMenuPrimitive.SubTrigger.displayName; 46 | 47 | const DropdownMenuSubContent = React.forwardRef< 48 | React.ElementRef, 49 | React.ComponentPropsWithoutRef 50 | >(({ className, ...props }, ref) => ( 51 | 59 | )); 60 | DropdownMenuSubContent.displayName = 61 | DropdownMenuPrimitive.SubContent.displayName; 62 | 63 | const DropdownMenuContent = React.forwardRef< 64 | React.ElementRef, 65 | React.ComponentPropsWithoutRef 66 | >(({ className, sideOffset = 4, ...props }, ref) => ( 67 | 68 | 78 | 79 | )); 80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; 81 | 82 | const DropdownMenuItem = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef & { 85 | inset?: boolean 86 | } 87 | >(({ className, inset, ...props }, ref) => ( 88 | 97 | )); 98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; 99 | 100 | const DropdownMenuCheckboxItem = React.forwardRef< 101 | React.ElementRef, 102 | React.ComponentPropsWithoutRef 103 | >(({ className, children, checked, ...props }, ref) => ( 104 | 113 | 114 | 115 | 116 | 117 | 118 | {children} 119 | 120 | )); 121 | DropdownMenuCheckboxItem.displayName = 122 | DropdownMenuPrimitive.CheckboxItem.displayName; 123 | 124 | const DropdownMenuRadioItem = React.forwardRef< 125 | React.ElementRef, 126 | React.ComponentPropsWithoutRef 127 | >(({ className, children, ...props }, ref) => ( 128 | 136 | 137 | 138 | 139 | 140 | 141 | {children} 142 | 143 | )); 144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; 145 | 146 | const DropdownMenuLabel = React.forwardRef< 147 | React.ElementRef, 148 | React.ComponentPropsWithoutRef & { 149 | inset?: boolean 150 | } 151 | >(({ className, inset, ...props }, ref) => ( 152 | 161 | )); 162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; 163 | 164 | const DropdownMenuSeparator = React.forwardRef< 165 | React.ElementRef, 166 | React.ComponentPropsWithoutRef 167 | >(({ className, ...props }, ref) => ( 168 | 173 | )); 174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; 175 | 176 | const DropdownMenuShortcut = ({ 177 | className, 178 | ...props 179 | }: React.HTMLAttributes) => { 180 | return ( 181 | 185 | ); 186 | }; 187 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; 188 | 189 | export { 190 | DropdownMenu, 191 | DropdownMenuTrigger, 192 | DropdownMenuContent, 193 | DropdownMenuItem, 194 | DropdownMenuCheckboxItem, 195 | DropdownMenuRadioItem, 196 | DropdownMenuLabel, 197 | DropdownMenuSeparator, 198 | DropdownMenuShortcut, 199 | DropdownMenuGroup, 200 | DropdownMenuPortal, 201 | DropdownMenuSub, 202 | DropdownMenuSubContent, 203 | DropdownMenuSubTrigger, 204 | DropdownMenuRadioGroup, 205 | }; 206 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | import { Slot } from "@radix-ui/react-slot"; 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form"; 12 | 13 | import { cn } from "@/lib/utils"; 14 | import { Label } from "@/components/ui/label"; 15 | 16 | const Form = FormProvider; 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ); 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext); 44 | const itemContext = React.useContext(FormItemContext); 45 | const { getFieldState, formState } = useFormContext(); 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState); 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within "); 51 | } 52 | 53 | const { id } = itemContext; 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | }; 63 | }; 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ); 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId(); 78 | 79 | return ( 80 | 81 |
82 | 83 | ); 84 | }); 85 | FormItem.displayName = "FormItem"; 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField(); 92 | 93 | return ( 94 |