11import { useState } from 'react' ;
2- import { ExclamationTriangleIcon } from '@radix-ui/react-icons' ;
2+ import { CheckCircledIcon , DownloadIcon , ExclamationTriangleIcon , ReloadIcon } from '@radix-ui/react-icons' ;
33import {
44 QueryClient ,
55 useMutation ,
@@ -8,20 +8,32 @@ import {
88} from '@tanstack/react-query' ;
99import {
1010 ActionFunctionArgs ,
11+ Link ,
1112 LoaderFunctionArgs ,
1213 redirect ,
1314 useLoaderData ,
1415 useNavigate ,
1516} from 'react-router-dom' ;
17+ import {
18+ Table ,
19+ TableBody ,
20+ TableCell ,
21+ TableHead ,
22+ TableHeader ,
23+ TableRow ,
24+ } from '~/components/tables' ;
1625import { toast } from '~/components/alerts' ;
1726import { Button } from '~/components/buttons' ;
1827import { DangerDialog } from '~/components/dialogs' ;
1928import { PropertyForm } from '~/components/forms' ;
2029import { SEO } from '~/components/layout' ;
2130import { propertyQuery } from '~/queries/properties' ;
22- import { deleteProperty , sendToScan , updateProperty } from '~/services' ;
31+ import { deleteProperty , getScan , IPage , IPageScan , sendToScan , updateProperty } from '~/services' ;
2332import { assertNonNull } from '~/utils/safety' ;
2433import { LoadingProperty } from './loading' ;
34+ import { ColumnDef , flexRender , getCoreRowModel , PaginationState , useReactTable } from '@tanstack/react-table' ;
35+ import React from 'react' ;
36+ import * as Tooltip from '@radix-ui/react-tooltip' ;
2537
2638/**
2739 * Loader function to fetch property data
@@ -98,6 +110,12 @@ const EditProperty = () => {
98110 initialData : initialProperty ,
99111 } ) ;
100112
113+ // pagination
114+ const [ pagination , setPagination ] = useState < PaginationState > ( {
115+ pageIndex : 0 ,
116+ pageSize : 10 ,
117+ } ) ;
118+
101119 const { mutate : deleteMutate } = useMutation ( {
102120 mutationFn : ( ) => {
103121 const response = deleteProperty ( propertyId ! ) ;
@@ -152,6 +170,155 @@ const EditProperty = () => {
152170 setIsSending ( false ) ;
153171 }
154172 } ;
173+
174+ // returns the index of the newest scan
175+ const getIndexOfNewestScan = ( scansArray : IPageScan [ ] ) => {
176+ return scansArray . reduce (
177+ ( highestIndex , scan , index , arr ) =>
178+ new Date ( scan . updated_at ) . getTime ( ) >
179+ new Date ( arr [ highestIndex ] . updated_at ) . getTime ( )
180+ ? index
181+ : highestIndex ,
182+ 0 ,
183+ ) ;
184+ } ;
185+
186+ // Define the columns
187+ const columns = React . useMemo < ColumnDef < IPage > [ ] > (
188+ ( ) => [
189+ {
190+ accessorKey : 'url' ,
191+ header : 'URL' ,
192+ cell : ( { row } ) => (
193+ < Link
194+ className = "text-blue-500 hover:opacity-50"
195+ to = { './' + row . original . id }
196+ >
197+ { row . original . url }
198+ </ Link >
199+ ) ,
200+ } ,
201+ {
202+ accessorKey : 'status' ,
203+ header : 'Status' ,
204+ cell : ( { row } ) => (
205+ < div >
206+ { row . original ?. scans . length > 0 ? (
207+ row . original . scans [ getIndexOfNewestScan ( row . original . scans ) ] . processing ? (
208+ < ReloadIcon aria-label = "Processing" className = "animate-spin" />
209+ ) :(
210+
211+ < div className = "inline-flex items-center" >
212+ < Tooltip . Provider >
213+ < Tooltip . Root >
214+ < Tooltip . Trigger >
215+ < CheckCircledIcon aria-label = "Complete" />
216+ </ Tooltip . Trigger >
217+ < Tooltip . Portal >
218+ < Tooltip . Content
219+ className = "TooltipContent"
220+ sideOffset = { 5 }
221+ >
222+ < div className = "text-center text-sm" >
223+ Last scanned < br />
224+ { new Date (
225+ row . original . scans [
226+ getIndexOfNewestScan ( row . original . scans )
227+ ] . updated_at ,
228+ ) . toLocaleString ( ) }
229+ </ div >
230+ < Tooltip . Arrow className = "TooltipArrow" />
231+ </ Tooltip . Content >
232+ </ Tooltip . Portal >
233+ </ Tooltip . Root >
234+ </ Tooltip . Provider >
235+ </ div >
236+ )
237+
238+ ) : (
239+ < ExclamationTriangleIcon aria-label = "No Scans Found!" />
240+ ) }
241+ </ div >
242+ ) ,
243+ } ,
244+ {
245+ accessorKey : 'report' ,
246+ header : 'Results JSON' ,
247+ cell : ( { row } ) =>
248+ row . original ?. scans . length > 0 ? (
249+ row . original . scans [ getIndexOfNewestScan ( row . original . scans ) ]
250+ . processing ? (
251+ < span className = "select-none text-[#666]" > Not ready</ span >
252+ ) : (
253+ < button
254+ className = "inline-flex items-center text-blue-500 hover:opacity-50"
255+ onClick = { async ( ) => {
256+ const element = document . getElementById ( 'downloadReportLink' ) ;
257+ if ( element ) {
258+ const response = await getScan (
259+ row . original . scans [
260+ getIndexOfNewestScan ( row . original . scans )
261+ ] . id ,
262+ ) ;
263+ element . setAttribute (
264+ 'href' ,
265+ 'data:text/json;charset=utf-8,' +
266+ encodeURIComponent ( JSON . stringify ( response ) ) ,
267+ ) ;
268+ element . setAttribute ( 'download' , 'results.json' ) ;
269+ element . click ( ) ;
270+ } else {
271+ console . log (
272+ 'Error fetching scan:' ,
273+ row . original . scans [
274+ getIndexOfNewestScan ( row . original . scans )
275+ ] . id ,
276+ ) ;
277+ }
278+ } }
279+ >
280+ < DownloadIcon className = "ml-1" aria-label = "Download" />
281+ </ button >
282+ )
283+ ) : (
284+ < > </ >
285+ ) ,
286+ } ,
287+ ] ,
288+ [ ] ,
289+ ) ;
290+
291+ //const defaultData = React.useMemo(() => [], []);
292+ // data fetching
293+ /* const dataQuery = useQuery({
294+ queryKey: ['property', pagination],
295+ queryFn: async () => {
296+ const theParams = {
297+ property_id:
298+ limit: pagination.pageSize,
299+ offset: pagination.pageIndex * pagination.pageSize,
300+ };
301+ console.log(theParams);
302+ return getPages({ params: theParams });
303+ },
304+ placeholderData: keepPreviousData,
305+ }); */
306+ const table = useReactTable ( {
307+ data : property . urls as IPage [ ] ?? initialProperty . urls ,
308+ columns,
309+ // pageCount: dataQuery.data?.pageCount ?? -1, //you can now pass in `rowCount` instead of pageCount and `pageCount` will be calculated internally (new in v8.13.0)
310+ //rowCount: dataQuery.data?.total, // new in v8.13.0 - alternatively, just pass in `pageCount` directly
311+ state : {
312+ pagination,
313+ } ,
314+ enableRowSelection : false ,
315+ onPaginationChange : setPagination ,
316+ getCoreRowModel : getCoreRowModel ( ) ,
317+ manualPagination : true , //we're doing manual "server-side" pagination
318+ //debugTable: true,
319+ } ) ;
320+
321+
155322 return (
156323 < >
157324 < SEO
@@ -220,6 +387,156 @@ const EditProperty = () => {
220387 </ div >
221388 </ section >
222389
390+ { table . getRowCount ( ) === 0 ? (
391+ < div className = "mt-7 text-center" >
392+ < h2 className = "text-xl font-semibold text-gray-700" >
393+ No Pages Added
394+ </ h2 >
395+ < p className = "mt-2 text-gray-600" >
396+ You haven't added any pages yet. Get started by adding your first
397+ page and monitor its accessibility status.
398+ </ p >
399+ < Link
400+ to = "/pages/add"
401+ className = "mt-4 inline-flex h-9 items-center justify-center whitespace-nowrap rounded-md bg-[#005031] px-4 py-2 text-sm text-white shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[#1D781D] focus-visible:ring-offset-2"
402+ >
403+ Add Your First Page
404+ </ Link >
405+ </ div >
406+ ) : (
407+ < section
408+ aria-labelledby = "pages-list-heading"
409+ className = "mt-7 space-y-6 rounded-lg bg-white p-6 shadow"
410+ >
411+ < div className = "w-full overflow-x-auto" >
412+ < div className = "p-2" >
413+ < Table role = "table" aria-label = "Pages List" >
414+ < TableHeader >
415+ { table . getHeaderGroups ( ) . map ( ( headerGroup ) => (
416+ < TableRow key = { headerGroup . id } >
417+ { headerGroup . headers . map ( ( header ) => {
418+ return (
419+ < TableHead key = { header . id } role = "columnheader" >
420+ { header . isPlaceholder
421+ ? null
422+ : flexRender (
423+ header . column . columnDef . header ,
424+ header . getContext ( ) ,
425+ ) }
426+ </ TableHead >
427+ ) ;
428+ } ) }
429+ </ TableRow >
430+ ) ) }
431+ </ TableHeader >
432+ < TableBody >
433+ { table . getRowModel ( ) . rows ?. length ? (
434+ table . getRowModel ( ) . rows . map ( ( row ) => (
435+ < TableRow
436+ key = { row . id }
437+ role = "row"
438+ >
439+ { row . getVisibleCells ( ) . map ( ( cell ) => (
440+ < TableCell key = { cell . id } role = "cell" >
441+ { flexRender (
442+ cell . column . columnDef . cell ,
443+ cell . getContext ( ) ,
444+ ) }
445+ </ TableCell >
446+ ) ) }
447+ </ TableRow >
448+ ) )
449+ ) : (
450+ < TableRow role = "row" >
451+ < TableCell
452+ colSpan = { columns . length }
453+ className = "h-24 text-center"
454+ role = "cell"
455+ >
456+ No results.
457+ </ TableCell >
458+ </ TableRow >
459+ ) }
460+ </ TableBody >
461+ </ Table >
462+ < nav
463+ role = "navigation"
464+ aria-label = "Pagination Navigation"
465+ className = "flex items-center gap-2"
466+ >
467+ < button
468+ className = "rounded border p-1"
469+ onClick = { ( ) => table . firstPage ( ) }
470+ disabled = { ! table . getCanPreviousPage ( ) }
471+ >
472+ { '<<' }
473+ </ button >
474+ < button
475+ className = "rounded border p-1"
476+ onClick = { ( ) => table . previousPage ( ) }
477+ disabled = { ! table . getCanPreviousPage ( ) }
478+ >
479+ { '<' }
480+ </ button >
481+ < button
482+ className = "rounded border p-1"
483+ onClick = { ( ) => table . nextPage ( ) }
484+ disabled = { ! table . getCanNextPage ( ) }
485+ >
486+ { '>' }
487+ </ button >
488+ < button
489+ className = "rounded border p-1"
490+ onClick = { ( ) => table . lastPage ( ) }
491+ disabled = { ! table . getCanNextPage ( ) }
492+ >
493+ { '>>' }
494+ </ button >
495+ < span className = "flex items-center gap-1" >
496+ < div > Page</ div >
497+ < strong >
498+ { table . getState ( ) . pagination . pageIndex + 1 } of{ ' ' }
499+ { table . getPageCount ( ) . toLocaleString ( ) }
500+ </ strong >
501+ </ span >
502+ < span className = "flex items-center gap-1" >
503+ | Go to page:
504+ < input
505+ type = "number"
506+ min = "1"
507+ max = { table . getPageCount ( ) }
508+ defaultValue = { table . getState ( ) . pagination . pageIndex + 1 }
509+ onChange = { ( e ) => {
510+ const page = e . target . value
511+ ? Number ( e . target . value ) - 1
512+ : 0 ;
513+ table . setPageIndex ( page ) ;
514+ } }
515+ className = "w-16 rounded border p-1"
516+ />
517+ </ span >
518+ < select
519+ value = { table . getState ( ) . pagination . pageSize }
520+ onChange = { ( e ) => {
521+ table . setPageSize ( Number ( e . target . value ) ) ;
522+ } }
523+ >
524+ { [ 10 , 20 , 30 , 40 , 50 ] . map ( ( pageSize ) => (
525+ < option key = { pageSize } value = { pageSize } >
526+ Show { pageSize }
527+ </ option >
528+ ) ) }
529+ </ select >
530+
531+ </ nav >
532+
533+
534+ </ div >
535+ </ div >
536+ < a id = "downloadReportLink" style = { { display : 'none' } } > </ a >
537+ </ section >
538+ ) }
539+
223540 < section
224541 aria-labelledby = "danger-zone-heading"
225542 className = "mt-7 space-y-6 rounded-lg bg-white p-6 shadow"
0 commit comments