@@ -11,6 +11,7 @@ import {
1111 getSortedRowModel ,
1212 getFacetedUniqueValues ,
1313 getFacetedMinMaxValues ,
14+ getPaginationRowModel ,
1415 Column ,
1516 RowData ,
1617 SortingState ,
@@ -20,14 +21,22 @@ import { Button } from "@/components/primitives/button";
2021import { Input } from "@/components/primitives/input" ;
2122
2223import { Table , TableCell , TableRow } from "@/components/primitives/table" ;
23- import React , { CSSProperties , useMemo , useState } from "react" ;
24- import { ChartArea , ListFilterIcon , TableIcon } from "lucide-react" ;
24+ import React , { CSSProperties , Fragment , useMemo , useState } from "react" ;
25+ import {
26+ ChartArea ,
27+ ChevronLeftIcon ,
28+ ChevronRightIcon ,
29+ ListFilterIcon ,
30+ SearchIcon ,
31+ TableIcon ,
32+ } from "lucide-react" ;
2533import { DataTableProps } from "@/components/forms/applications/dataTableWrapper" ;
2634import AnalyticsPage from "@/components/forms/applications/AnalyticsPage" ;
2735import {
2836 FormFields ,
2937 ReferenceItem ,
3038 ReferenceMap ,
39+ STATUS_COLORS ,
3140} from "@/components/forms/applications/columns" ;
3241import MultiSelect from "@/components/general/multiSelect" ;
3342
@@ -54,10 +63,16 @@ export function DataTable<TData, TValue>({
5463 data,
5564 refMap,
5665 config,
66+ onRowClick,
5767} : DataTableProps < TData , TValue > ) {
5868 const [ columnFilters , setColumnFilters ] = useState < ColumnFiltersState > ( [ ] ) ;
5969 const [ columnVisibility , setColumnVisibility ] = useState < VisibilityState > ( { } ) ;
6070 const [ sorting , setSorting ] = React . useState < SortingState > ( [ ] ) ;
71+ const [ globalFilter , setGlobalFilter ] = useState ( "" ) ;
72+ const [ pagination , setPagination ] = React . useState ( {
73+ pageIndex : 0 ,
74+ pageSize : 50 ,
75+ } ) ;
6176 const [ tabView , setTabView ] = useState < "table" | "chart" > ( "table" ) ;
6277
6378 const table = useReactTable ( {
@@ -66,6 +81,10 @@ export function DataTable<TData, TValue>({
6681 getCoreRowModel : getCoreRowModel ( ) ,
6782 onColumnFiltersChange : setColumnFilters ,
6883 getFilteredRowModel : getFilteredRowModel ( ) ,
84+ onGlobalFilterChange : setGlobalFilter ,
85+ globalFilterFn : "includesString" ,
86+ onPaginationChange : setPagination ,
87+ getPaginationRowModel : getPaginationRowModel ( ) ,
6988 onColumnVisibilityChange : setColumnVisibility ,
7089 onSortingChange : setSorting ,
7190 getSortedRowModel : getSortedRowModel ( ) ,
@@ -81,10 +100,50 @@ export function DataTable<TData, TValue>({
81100 columnFilters,
82101 columnVisibility,
83102 sorting,
103+ globalFilter,
104+ pagination,
84105 ...( config ?. columnOrder ? { columnOrder : config . columnOrder } : { } ) ,
85106 } ,
86107 } ) ;
87108
109+ const statusCounts = useMemo ( ( ) => {
110+ const counts : Record < string , number > = { } ;
111+ ( config ?. statusOptions || [ ] ) . forEach ( ( option : any ) => {
112+ counts [ option . id ] = 0 ;
113+ } ) ;
114+ table
115+ . getCoreRowModel ( )
116+ . rows . forEach ( ( row ) => {
117+ const status = ( row . original as any ) ?. status ;
118+ if ( status && status in counts ) {
119+ counts [ status ] += 1 ;
120+ } else if ( status ) {
121+ counts [ status ] = ( counts [ status ] || 0 ) + 1 ;
122+ }
123+ } ) ;
124+ return counts ;
125+ } , [ config ?. statusOptions , table ] ) ;
126+
127+ const activeStatusFilter = columnFilters . find ( ( c ) => c . id === "status" )
128+ ?. value as string | undefined ;
129+
130+ function toggleStatusFilter ( statusId : string ) {
131+ const isActive = activeStatusFilter === statusId ;
132+ setColumnFilters ( [
133+ ...columnFilters . filter ( ( c ) => c . id !== "status" ) ,
134+ ...( isActive ? [ ] : [ { id : "status" , value : statusId } ] ) ,
135+ ] ) ;
136+ }
137+
138+ function handleRowClick ( e : React . MouseEvent , row : any ) {
139+ if ( ! onRowClick ) return ;
140+ const target = e . target as HTMLElement ;
141+ if ( target . closest ( "button, input, textarea, select, a, [role=combobox]" ) ) {
142+ return ;
143+ }
144+ onRowClick ( row ) ;
145+ }
146+
88147 return (
89148 < div className = { "flex flex-col px-10 overflow-hidden pb-4" } >
90149 < div className = "flex items-center w-full gap-2 py-4 flex-wrap" >
@@ -94,6 +153,36 @@ export function DataTable<TData, TValue>({
94153 </ span >
95154 </ div >
96155
156+ < Input
157+ placeholder = "Search applicants..."
158+ value = { globalFilter }
159+ onChange = { ( e ) => setGlobalFilter ( e . target . value ) }
160+ className = "w-64 h-10 bg-background-600 border-background-500"
161+ />
162+
163+ { config ?. statusOptions ?. length > 0 && (
164+ < div className = "flex items-center gap-1 flex-wrap" >
165+ { ( config . statusOptions as any [ ] ) . map ( ( option ) => {
166+ const count = statusCounts [ option . id ] || 0 ;
167+ const active = activeStatusFilter === option . id ;
168+ return (
169+ < button
170+ key = { option . id }
171+ onClick = { ( ) => toggleStatusFilter ( option . id ) }
172+ className = { `rounded-full px-3 py-1 text-xs border transition-colors ${
173+ active
174+ ? "border-lp-400 bg-lp-500 text-white"
175+ : "border-background-500 bg-background-600 text-neutral-200 hover:border-background-400"
176+ } `}
177+ >
178+ { option . label }
179+ < span className = "ml-1 opacity-70" > { count } </ span >
180+ </ button >
181+ ) ;
182+ } ) }
183+ </ div >
184+ ) }
185+
97186 < div
98187 className = {
99188 "flex relative items-center bg-background-600 rounded-lg border-background-500 border-none h-10 p-1 gap-2"
@@ -115,7 +204,7 @@ export function DataTable<TData, TValue>({
115204 setTabView ( "chart" ) ;
116205 } }
117206 size = { "fit" }
118- className = { `bg-background-600 w-24 h-full border-none border-background-500 gap-2 ${ tabView === "chart" ? "bg-background-500" : "" } ` }
207+ className = { `bg-background-600 w-24 h-full border-none border-background-500 gap-2 ${ tabView === "chart" ? "bg-background-500" : "" } ` }
119208 >
120209 < ChartArea className = { "h-4 w-4" } />
121210 Chart
@@ -132,7 +221,10 @@ export function DataTable<TData, TValue>({
132221 setColumnFilters = { setColumnFilters }
133222 />
134223 ) }
135- < DownloadCSV data = { data } fileName = "data" />
224+ < DownloadCSV
225+ data = { table . getFilteredRowModel ( ) . rows . map ( ( row ) => row . original ) }
226+ fileName = { config ?. title || "submissions" }
227+ />
136228 </ div >
137229 { table . getFilteredRowModel ( ) . rows . length > 0 && tabView === "chart" && (
138230 < AnalyticsPage
@@ -178,10 +270,11 @@ export function DataTable<TData, TValue>({
178270 table . getRowModel ( ) . rows . map ( ( row ) => (
179271 < tr
180272 className = {
181- "border-background-500 p-0 flex-shrink-0 w-full bg-background-600 odd:bg-background-700"
273+ "border-background-500 p-0 flex-shrink-0 w-full bg-background-600 odd:bg-background-700 hover:bg-background-500 cursor-pointer transition-colors "
182274 }
183275 key = { row . id }
184276 data-state = { row . getIsSelected ( ) && "selected" }
277+ onClick = { ( e ) => handleRowClick ( e , row ) }
185278 >
186279 { row . getVisibleCells ( ) . map ( ( cell ) => (
187280 < td
@@ -211,6 +304,52 @@ export function DataTable<TData, TValue>({
211304 </ Table >
212305 </ div >
213306 ) }
307+ { tabView === "table" && (
308+ < Fragment >
309+ < div className = "flex items-center gap-3 py-2 text-sm text-neutral-300" >
310+ < Button
311+ size = { "fit" }
312+ className = "bg-background-600 min-h-none h-8 gap-1"
313+ onClick = { ( ) => table . previousPage ( ) }
314+ disabled = { ! table . getCanPreviousPage ( ) }
315+ >
316+ < ChevronLeftIcon className = "h-4 w-4" />
317+ Prev
318+ </ Button >
319+ < span >
320+ Page { table . getState ( ) . pagination . pageIndex + 1 } of{ " " }
321+ { table . getPageCount ( ) || 1 }
322+ </ span >
323+ < Button
324+ size = { "fit" }
325+ className = "bg-background-600 min-h-none h-8 gap-1"
326+ onClick = { ( ) => table . nextPage ( ) }
327+ disabled = { ! table . getCanNextPage ( ) }
328+ >
329+ Next
330+ < ChevronRightIcon className = "h-4 w-4" />
331+ </ Button >
332+ < select
333+ className = "bg-background-600 border border-background-500 rounded p-1 h-8"
334+ value = { table . getState ( ) . pagination . pageSize }
335+ onChange = { ( e ) => {
336+ const size =
337+ e . target . value === "all"
338+ ? table . getFilteredRowModel ( ) . rows . length || 1
339+ : Number ( e . target . value ) ;
340+ table . setPageSize ( size ) ;
341+ } }
342+ >
343+ { [ 25 , 50 , 100 ] . map ( ( size ) => (
344+ < option key = { size } value = { size } >
345+ { size } / page
346+ </ option >
347+ ) ) }
348+ < option value = "all" > All</ option >
349+ </ select >
350+ </ div >
351+ </ Fragment >
352+ ) }
214353 </ div >
215354 ) ;
216355}
0 commit comments