319 lines
12 KiB
TypeScript
319 lines
12 KiB
TypeScript
|
'use client'
|
||
|
|
||
|
import {
|
||
|
Column,
|
||
|
Table as ReactTable,
|
||
|
useReactTable,
|
||
|
ColumnFiltersState,
|
||
|
getCoreRowModel,
|
||
|
getFilteredRowModel,
|
||
|
getFacetedRowModel,
|
||
|
getFacetedUniqueValues,
|
||
|
getFacetedMinMaxValues,
|
||
|
getPaginationRowModel,
|
||
|
sortingFns,
|
||
|
getSortedRowModel,
|
||
|
FilterFn,
|
||
|
SortingFn,
|
||
|
ColumnDef,
|
||
|
flexRender,
|
||
|
FilterFns,
|
||
|
ColumnOrderState,
|
||
|
createColumnHelper,
|
||
|
} from '@tanstack/react-table'
|
||
|
import Image from 'next/image';
|
||
|
import { useState } from "react";
|
||
|
import { IStream } from "@/lib/streams";
|
||
|
import { LocalizedDate } from './localized-date';
|
||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||
|
import { faAngleLeft, faAngleRight, faAnglesLeft, faAnglesRight, faChevronCircleRight, faChevronRight } from '@fortawesome/free-solid-svg-icons';
|
||
|
import Link from 'next/link';
|
||
|
|
||
|
function Filter({
|
||
|
column,
|
||
|
table,
|
||
|
}: {
|
||
|
column: Column<any, any>
|
||
|
table: ReactTable<any>
|
||
|
}) {
|
||
|
const firstValue = table
|
||
|
.getPreFilteredRowModel()
|
||
|
.flatRows[0]?.getValue(column.id)
|
||
|
|
||
|
const columnFilterValue = column.getFilterValue()
|
||
|
|
||
|
|
||
|
|
||
|
if (typeof firstValue === 'number') {
|
||
|
return (
|
||
|
<div className="flex space-x-2">
|
||
|
<input
|
||
|
type="number"
|
||
|
value={(columnFilterValue as [number, number])?.[0] ?? ''}
|
||
|
onChange={e =>
|
||
|
column.setFilterValue((old: [number, number]) => [
|
||
|
e.target.value,
|
||
|
old?.[1],
|
||
|
])
|
||
|
}
|
||
|
placeholder={`Min`}
|
||
|
className="w-24 border shadow rounded"
|
||
|
/>
|
||
|
<input
|
||
|
type="number"
|
||
|
value={(columnFilterValue as [number, number])?.[1] ?? ''}
|
||
|
onChange={e =>
|
||
|
column.setFilterValue((old: [number, number]) => [
|
||
|
old?.[0],
|
||
|
e.target.value,
|
||
|
])
|
||
|
}
|
||
|
placeholder={`Max`}
|
||
|
className="w-24 border shadow rounded"
|
||
|
/>
|
||
|
</div>
|
||
|
)
|
||
|
}
|
||
|
|
||
|
if (typeof firstValue === 'boolean') {
|
||
|
return (
|
||
|
<>
|
||
|
<div className='select'>
|
||
|
<select
|
||
|
onChange={(evt) => {
|
||
|
if (evt.target.value === "any")
|
||
|
column?.setFilterValue(null);
|
||
|
if (evt.target.value === "yes")
|
||
|
column?.setFilterValue(true);
|
||
|
if (evt.target.value === "no")
|
||
|
column?.setFilterValue(false);
|
||
|
}}
|
||
|
>
|
||
|
<option>any</option>
|
||
|
<option>yes</option>
|
||
|
<option>no</option>
|
||
|
</select>
|
||
|
</div>
|
||
|
</>
|
||
|
)
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<input
|
||
|
type="text"
|
||
|
value={(columnFilterValue ?? '') as string}
|
||
|
onChange={e => column.setFilterValue(e.target.value)}
|
||
|
placeholder={`Search...`}
|
||
|
className="input"
|
||
|
/>
|
||
|
)
|
||
|
}
|
||
|
|
||
|
const archiveStatusClassName = (archiveStatus: string): string => {
|
||
|
if (archiveStatus === 'missing') return 'is-danger';
|
||
|
if (archiveStatus === 'issue') return 'is-warning';
|
||
|
if (archiveStatus === 'good') return 'is-success';
|
||
|
return 'is-info';
|
||
|
};
|
||
|
|
||
|
export default function StreamsTable({ streams }: { streams: IStream[] }) {
|
||
|
|
||
|
|
||
|
|
||
|
const columnHelper = createColumnHelper<IStream>()
|
||
|
|
||
|
|
||
|
const columns = [
|
||
|
columnHelper.accessor('attributes.cuid', {
|
||
|
cell: info => <Link href={`/streams/${info.getValue()}`}>{info.getValue()}</Link>,
|
||
|
header: () => <span>ID</span>
|
||
|
}),
|
||
|
columnHelper.accessor('attributes.vtuber.data.attributes.image', {
|
||
|
cell: info => <figure className='image is-24x24'><Image className="is-rounded" width={24} height={24} alt="" src={info.getValue()}></Image></figure>,
|
||
|
header: () => <span></span>,
|
||
|
enableColumnFilter: false
|
||
|
}),
|
||
|
columnHelper.accessor('attributes.vtuber.data.attributes.displayName', {
|
||
|
cell: info => info.getValue(),
|
||
|
header: () => <span>VTuber</span>
|
||
|
}),
|
||
|
columnHelper.accessor('attributes.date', {
|
||
|
cell: info => <LocalizedDate date={new Date(info.getValue())}/>,
|
||
|
header: () => <span>Date</span>
|
||
|
}),
|
||
|
columnHelper.accessor('attributes.isChaturbateStream', {
|
||
|
id: 'isChaturbateStream',
|
||
|
cell: info => info.getValue() === true ? 'yes' : 'no',
|
||
|
header: () => <span>Chaturbate</span>
|
||
|
}),
|
||
|
columnHelper.accessor('attributes.isFanslyStream', {
|
||
|
id: 'isFanslyStream',
|
||
|
cell: info => info.getValue() === true ? 'yes' : 'no',
|
||
|
header: () => <span>Fansly</span>
|
||
|
}),
|
||
|
columnHelper.accessor('attributes.archiveStatus', {
|
||
|
cell: info => <div className={`tag ${archiveStatusClassName(info.getValue())}`} >{info.getValue()}</div>,
|
||
|
header: () => <span>Status</span>
|
||
|
})
|
||
|
]
|
||
|
|
||
|
const [columnVisibility, setColumnVisibility] = useState({})
|
||
|
const [columnOrder, setColumnOrder] = useState<ColumnOrderState>([])
|
||
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
||
|
const [data, setData] = useState(() => streams)
|
||
|
|
||
|
const table = useReactTable({
|
||
|
data,
|
||
|
columns,
|
||
|
getCoreRowModel: getCoreRowModel(),
|
||
|
getFilteredRowModel: getFilteredRowModel(),
|
||
|
getPaginationRowModel: getPaginationRowModel(),
|
||
|
state: {
|
||
|
columnVisibility,
|
||
|
columnOrder,
|
||
|
columnFilters
|
||
|
},
|
||
|
onColumnOrderChange: setColumnOrder,
|
||
|
onColumnFiltersChange: setColumnFilters,
|
||
|
})
|
||
|
|
||
|
|
||
|
|
||
|
return (
|
||
|
<>
|
||
|
|
||
|
|
||
|
<div className="p-2">
|
||
|
<div className="h-2" />
|
||
|
|
||
|
|
||
|
|
||
|
<table className='table'>
|
||
|
<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()
|
||
|
)}
|
||
|
{header.column.getCanFilter() ? (
|
||
|
<div>
|
||
|
<Filter column={header.column} table={table} />
|
||
|
</div>
|
||
|
) : null}
|
||
|
</div>
|
||
|
)}
|
||
|
</th>
|
||
|
)
|
||
|
})}
|
||
|
</tr>
|
||
|
))}
|
||
|
</thead>
|
||
|
<tbody>
|
||
|
{table.getRowModel().rows.map(row => {
|
||
|
return (
|
||
|
<tr key={row.id}>
|
||
|
{row.getVisibleCells().map(cell => {
|
||
|
return (
|
||
|
<td key={cell.id}>
|
||
|
{flexRender(
|
||
|
cell.column.columnDef.cell,
|
||
|
cell.getContext()
|
||
|
)}
|
||
|
</td>
|
||
|
)
|
||
|
})}
|
||
|
</tr>
|
||
|
)
|
||
|
})}
|
||
|
</tbody>
|
||
|
</table>
|
||
|
<div className="columns is-multiline is-mobile" />
|
||
|
<div className="column is-12">
|
||
|
<button
|
||
|
className="button icon is-rounded is-medium p-1 m-1"
|
||
|
onClick={() => table.setPageIndex(0)}
|
||
|
disabled={!table.getCanPreviousPage()}
|
||
|
>
|
||
|
<FontAwesomeIcon className='fa-solid fa-angles-left' icon={faAnglesLeft}></FontAwesomeIcon>
|
||
|
</button>
|
||
|
<button
|
||
|
className="button icon is-rounded is-medium p-1 m-1"
|
||
|
onClick={() => table.previousPage()}
|
||
|
disabled={!table.getCanPreviousPage()}
|
||
|
>
|
||
|
<FontAwesomeIcon className='fa-solid fa-angle-left' icon={faAngleLeft}></FontAwesomeIcon>
|
||
|
</button>
|
||
|
<button
|
||
|
className="button icon is-rounded is-medium p-1 m-1"
|
||
|
onClick={() => table.nextPage()}
|
||
|
disabled={!table.getCanNextPage()}
|
||
|
>
|
||
|
<FontAwesomeIcon className="fa-solid fa-angle-right" icon={faAngleRight}></FontAwesomeIcon>
|
||
|
</button>
|
||
|
<button
|
||
|
className="button icon is-medium is-rounded p-1 m-1"
|
||
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||
|
disabled={!table.getCanNextPage()}
|
||
|
>
|
||
|
<FontAwesomeIcon className='fa-solid fa-angles-right' icon={faAnglesRight}></FontAwesomeIcon>
|
||
|
</button>
|
||
|
</div>
|
||
|
<div className='column is-2'>
|
||
|
|
||
|
<div className=''>
|
||
|
<div className=''>
|
||
|
<span className='mr-1'>Page</span>
|
||
|
{table.getState().pagination.pageIndex + 1} of{' '}
|
||
|
{table.getPageCount()}
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div className=''>
|
||
|
<label className='label'>
|
||
|
Go to page:
|
||
|
</label>
|
||
|
<div className="control is-expanded">
|
||
|
<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 p-1"
|
||
|
/>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div className='column is-2'>
|
||
|
<div className="m-1">
|
||
|
<span className='mr-1'>Page</span>
|
||
|
</div>
|
||
|
<div className='select'>
|
||
|
<select
|
||
|
value={table.getState().pagination.pageSize}
|
||
|
onChange={e => {
|
||
|
table.setPageSize(Number(e.target.value))
|
||
|
}}
|
||
|
>
|
||
|
{[10, 20, 30, 40, 50].map(pageSize => (
|
||
|
<option key={pageSize} value={pageSize}>
|
||
|
Show {pageSize}
|
||
|
</option>
|
||
|
))}
|
||
|
</select>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
</>
|
||
|
)
|
||
|
}
|