288 lines
8.7 KiB
TypeScript
288 lines
8.7 KiB
TypeScript
'use client'
|
|
import React from 'react'
|
|
import ReactDOM from 'react-dom/client'
|
|
import Link from 'next/link'
|
|
import { defaultImageBlur } from '../lib/constants'
|
|
import {
|
|
keepPreviousData,
|
|
QueryClient,
|
|
useQuery,
|
|
} from '@tanstack/react-query'
|
|
import { format } from 'date-fns'
|
|
import Image from "next/legacy/image"
|
|
import {
|
|
PaginationState,
|
|
useReactTable,
|
|
getCoreRowModel,
|
|
ColumnDef,
|
|
flexRender,
|
|
} from '@tanstack/react-table'
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
|
|
|
|
import { fetchStreamData } from '@/app/lib/streams'
|
|
import { IStream } from '@futureporn/types'
|
|
|
|
const queryClient = new QueryClient()
|
|
|
|
function getStatusClass(value: string) {
|
|
switch (value) {
|
|
case 'issue':
|
|
return 'is-warning';
|
|
case 'missing':
|
|
return 'is-danger';
|
|
case 'good':
|
|
return 'is-success';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
export default function StreamsTable() {
|
|
const rerender = React.useReducer(() => ({}), {})[1]
|
|
|
|
// image & name
|
|
// title
|
|
// platform
|
|
// date & time
|
|
// archiveStatus
|
|
const columns = React.useMemo<ColumnDef<IStream>[]>(
|
|
() => [
|
|
{
|
|
header: 'VTuber',
|
|
accessorFn: d => ({
|
|
displayName: d.vtuber.display_name,
|
|
image: d.vtuber.image,
|
|
imageBlur: d.vtuber.image_blur
|
|
}),
|
|
cell: info => {
|
|
const { displayName, image, imageBlur } = info.getValue<{ displayName: string, image: string, imageBlur: string }>();
|
|
return (
|
|
<>
|
|
<div className="columns is-mobile">
|
|
<div className="column mr-0 is-flex-grow-0">
|
|
<figure className="image is-24x24">
|
|
<Image
|
|
className="is-rounded"
|
|
src={image}
|
|
alt={displayName}
|
|
placeholder="blur"
|
|
objectFit='cover'
|
|
blurDataURL={imageBlur || defaultImageBlur}
|
|
width={32}
|
|
height={32}
|
|
/>
|
|
</figure>
|
|
</div>
|
|
<div className="column ml-0">
|
|
<span>{displayName}</span>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
},
|
|
{
|
|
header: 'Date',
|
|
accessorFn: d => format(new Date(d.date), 'yyyy-MM-dd HH:mm'),
|
|
sortingFn: 'datetime',
|
|
sortDescFirst: true,
|
|
cell: info => (<Link href={`/archive/${info.row.original.uuid}`}>{info.getValue() as string}</Link>)
|
|
},
|
|
{
|
|
header: 'Platform',
|
|
accessorFn: d => [
|
|
(d.is_chaturbate_stream && 'CB'),
|
|
(d.is_fansly_stream && 'Fansly')
|
|
].filter(Boolean).join(', ') || '???'
|
|
},
|
|
{
|
|
header: 'Status',
|
|
accessorFn: d => {
|
|
if (!d.archive_status) return 'missing';
|
|
return d.archive_status
|
|
}
|
|
},
|
|
// {
|
|
// header: 'Name',
|
|
// footer: props => props.column.id,
|
|
// columns: [
|
|
// {
|
|
// accessorKey: 'firstName',
|
|
// cell: info => info.getValue(),
|
|
// footer: props => props.column.id,
|
|
// },
|
|
// {
|
|
// accessorFn: row => row.lastName,
|
|
// id: 'lastName',
|
|
// cell: info => info.getValue(),
|
|
// header: () => <span>Last Name</span>,
|
|
// footer: props => props.column.id,
|
|
// },
|
|
// ],
|
|
// },
|
|
],
|
|
[]
|
|
)
|
|
|
|
const [pagination, setPagination] = React.useState<PaginationState>({
|
|
pageIndex: 0,
|
|
pageSize: 50,
|
|
})
|
|
|
|
const { data, error, isPending } = useQuery({
|
|
queryKey: ['streams', pagination.pageIndex, pagination.pageSize],
|
|
queryFn: () => fetchStreamData(pagination),
|
|
placeholderData: keepPreviousData, // don't have 0 rows flash while changing pages/loading next page,
|
|
staleTime: 1000,
|
|
}, queryClient)
|
|
|
|
const defaultData = React.useMemo(() => [], [])
|
|
|
|
const table = useReactTable({
|
|
data: data?.rows ?? defaultData,
|
|
columns,
|
|
// 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)
|
|
rowCount: data?.rowCount, // new in v8.13.0 - alternatively, just pass in `pageCount` directly
|
|
state: {
|
|
pagination,
|
|
},
|
|
onPaginationChange: setPagination,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
manualPagination: true, //we're doing manual "server-side" pagination
|
|
// getPaginationRowModel: getPaginationRowModel(), // If only doing manual pagination, you don't need this
|
|
debugTable: true,
|
|
})
|
|
|
|
return (
|
|
<div className="p-2">
|
|
<div className="h-2" />
|
|
|
|
{isPending && <FontAwesomeIcon className="mt-5 fa-spin-pulse" icon={faSpinner} ></FontAwesomeIcon>}
|
|
{!isPending && <>
|
|
<table className='table is-hoverable is-fullwidth'>
|
|
<thead>
|
|
{table.getHeaderGroups().map(headerGroup => (
|
|
<tr key={headerGroup.id}>
|
|
{headerGroup.headers.map(header => {
|
|
return (
|
|
<th key={header.id} colSpan={header.colSpan}>
|
|
{header.isPlaceholder ? null : (
|
|
<div>
|
|
{flexRender(
|
|
header.column.columnDef.header,
|
|
header.getContext()
|
|
)}
|
|
</div>
|
|
)}
|
|
</th>
|
|
)
|
|
})}
|
|
</tr>
|
|
))}
|
|
</thead>
|
|
<tbody>
|
|
{table.getRowModel().rows.map(row => {
|
|
return (
|
|
<tr key={row.id}>
|
|
{row.getVisibleCells().map(cell => {
|
|
return (
|
|
<td
|
|
className={getStatusClass(cell.getValue() as string)}
|
|
key={cell.id}
|
|
>
|
|
{flexRender(
|
|
cell.column.columnDef.cell,
|
|
cell.getContext()
|
|
)}
|
|
</td>
|
|
)
|
|
})}
|
|
</tr>
|
|
)
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
|
|
|
|
<div className="columns is-mobile is-vcentered">
|
|
<div className='column is-half'>
|
|
<button
|
|
className="button border rounded mx-1"
|
|
onClick={() => table.firstPage()}
|
|
disabled={!table.getCanPreviousPage()}
|
|
>
|
|
{'<<'}
|
|
</button>
|
|
<button
|
|
className="button border rounded mx-1"
|
|
onClick={() => table.previousPage()}
|
|
disabled={!table.getCanPreviousPage()}
|
|
>
|
|
{'<'}
|
|
</button>
|
|
<button
|
|
className="button border rounded mx-1"
|
|
onClick={() => table.nextPage()}
|
|
disabled={!table.getCanNextPage()}
|
|
>
|
|
{'>'}
|
|
</button>
|
|
<button
|
|
className="button border rounded mx-1"
|
|
onClick={() => table.lastPage()}
|
|
disabled={!table.getCanNextPage()}
|
|
>
|
|
{'>>'}
|
|
</button>
|
|
</div>
|
|
<div className='column is-half'>
|
|
<span>Page </span>
|
|
<strong>
|
|
{table.getState().pagination.pageIndex + 1} of{' '}{table.getPageCount().toLocaleString()}
|
|
</strong>
|
|
</div>
|
|
</div>
|
|
|
|
{/* second row with page number input and pages-per-screen select */}
|
|
<div className='columns is-mobile is-vcentered'>
|
|
<div className='column is-2 '>
|
|
<span className='is-text-centered'>Go to page:</span>
|
|
</div>
|
|
<div className='column is-3'>
|
|
<input
|
|
type="number"
|
|
defaultValue={table.getState().pagination.pageIndex + 1}
|
|
onChange={e => {
|
|
const page = e.target.value ? Number(e.target.value) - 1 : 0
|
|
table.setPageIndex(page)
|
|
}}
|
|
className="input"
|
|
/>
|
|
</div>
|
|
<div className='column is-5'>
|
|
<div className="select">
|
|
<select
|
|
value={table.getState().pagination.pageSize}
|
|
onChange={e => {
|
|
table.setPageSize(Number(e.target.value))
|
|
}}
|
|
>
|
|
{[20, 50, 100].map(pageSize => (
|
|
<option key={pageSize} value={pageSize}>
|
|
Show {pageSize}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
)
|
|
}
|