feat(ST-509): rebuild todo list as TanStack Data Table
- @tanstack/react-table voor kolommen, paginering en rij-selectie - Kolommen: multi-select checkbox, titel (line-clamp-2), productnaam-badge, datum - Toolbar: product-filter dropdown, bulk-archiveer knop (telt selectie), + knop - Paginering: 10 rijen per pagina met paginatelling (x–y van n) - Rij-klik opent detail-kaart (placeholder; volgt in ST-510) - Promote dialogs behouden voor gebruik in ST-510 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6fa768aabe
commit
5dc8033b85
3 changed files with 268 additions and 148 deletions
|
|
@ -1,18 +1,28 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useTransition, useActionState, useEffect, useRef, useCallback } from 'react'
|
import { useState, useTransition, useMemo, useEffect, useRef, useCallback } from 'react'
|
||||||
import { useFormStatus } from 'react-dom'
|
|
||||||
import { toast } from 'sonner'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import {
|
import {
|
||||||
createTodoAction,
|
useReactTable,
|
||||||
toggleTodoAction,
|
getCoreRowModel,
|
||||||
archiveCompletedTodosAction,
|
getPaginationRowModel,
|
||||||
|
flexRender,
|
||||||
|
type ColumnDef,
|
||||||
|
type RowSelectionState,
|
||||||
|
type PaginationState,
|
||||||
|
} from '@tanstack/react-table'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||||
|
import {
|
||||||
|
archiveSelectedTodosAction,
|
||||||
promoteTodoToPbiAction,
|
promoteTodoToPbiAction,
|
||||||
promoteTodoToStoryAction,
|
promoteTodoToStoryAction,
|
||||||
} from '@/actions/todos'
|
} from '@/actions/todos'
|
||||||
|
import { useActionState } from 'react'
|
||||||
|
|
||||||
interface Todo {
|
interface Todo {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -40,63 +50,23 @@ interface TodoListProps {
|
||||||
isDemo: boolean
|
isDemo: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function QuickInput({
|
// Checkbox with indeterminate support for TanStack row selection
|
||||||
products,
|
function IndeterminateCheckbox({
|
||||||
isDemo,
|
indeterminate,
|
||||||
selectedProductId,
|
className,
|
||||||
onProductChange,
|
...props
|
||||||
}: {
|
}: React.InputHTMLAttributes<HTMLInputElement> & { indeterminate?: boolean }) {
|
||||||
products: Product[]
|
const ref = useRef<HTMLInputElement>(null)
|
||||||
isDemo: boolean
|
|
||||||
selectedProductId: string
|
|
||||||
onProductChange: (id: string) => void
|
|
||||||
}) {
|
|
||||||
const [state, formAction] = useActionState(createTodoAction, undefined)
|
|
||||||
const titleRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state?.success && titleRef.current) titleRef.current.value = ''
|
if (ref.current) ref.current.indeterminate = indeterminate ?? false
|
||||||
}, [state])
|
}, [indeterminate])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 space-y-1">
|
<input
|
||||||
<form action={formAction} className="flex gap-2">
|
ref={ref}
|
||||||
<select
|
type="checkbox"
|
||||||
name="productId"
|
className={cn('size-4 cursor-pointer accent-primary', className)}
|
||||||
value={selectedProductId}
|
{...props}
|
||||||
onChange={e => onProductChange(e.target.value)}
|
/>
|
||||||
disabled={isDemo}
|
|
||||||
className="border border-border rounded-lg px-3 py-1.5 text-sm bg-input-background shrink-0"
|
|
||||||
>
|
|
||||||
<option value="all">Alles</option>
|
|
||||||
<option value="">Geen product</option>
|
|
||||||
{products.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
|
|
||||||
</select>
|
|
||||||
<Input
|
|
||||||
ref={titleRef}
|
|
||||||
name="title"
|
|
||||||
placeholder={isDemo ? 'Alleen-lezen in demo' : 'Nieuwe todo… (Enter om op te slaan)'}
|
|
||||||
disabled={isDemo}
|
|
||||||
className="flex-1"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
<DemoTooltip show={isDemo}>
|
|
||||||
<QuickSubmitButton isDemo={isDemo} />
|
|
||||||
</DemoTooltip>
|
|
||||||
</form>
|
|
||||||
{typeof state?.error === 'string' && (
|
|
||||||
<p className="text-xs text-error">{state.error}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function QuickSubmitButton({ isDemo }: { isDemo: boolean }) {
|
|
||||||
const { pending } = useFormStatus()
|
|
||||||
return (
|
|
||||||
<Button type="submit" disabled={pending || isDemo}>
|
|
||||||
{pending ? '…' : 'Toevoegen'}
|
|
||||||
</Button>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,107 +217,222 @@ function PromoteStoryDialog({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Main list ---
|
// --- Main component ---
|
||||||
export function TodoList({ todos, products, isDemo }: TodoListProps) {
|
export function TodoList({ todos, products, isDemo }: TodoListProps) {
|
||||||
const [, startTransition] = useTransition()
|
const [isPending, startTransition] = useTransition()
|
||||||
|
const [selectedProductId, setSelectedProductId] = useState('all')
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 10 })
|
||||||
|
const [activeRowId, setActiveRowId] = useState<string | null>(null)
|
||||||
|
const [mode, setMode] = useState<'idle' | 'create'>('idle')
|
||||||
const [promotePbi, setPromotePbi] = useState<Todo | null>(null)
|
const [promotePbi, setPromotePbi] = useState<Todo | null>(null)
|
||||||
const [promoteStory, setPromoteStory] = useState<Todo | null>(null)
|
const [promoteStory, setPromoteStory] = useState<Todo | null>(null)
|
||||||
const [selectedProductId, setSelectedProductId] = useState('all')
|
|
||||||
|
|
||||||
const filtered =
|
const filtered = useMemo(() => {
|
||||||
selectedProductId === 'all'
|
if (selectedProductId === 'all') return todos
|
||||||
? todos
|
if (selectedProductId === '') return todos.filter(t => t.product_id === null)
|
||||||
: selectedProductId === ''
|
return todos.filter(t => t.product_id === selectedProductId)
|
||||||
? todos.filter(t => t.product_id === null)
|
}, [todos, selectedProductId])
|
||||||
: todos.filter(t => t.product_id === selectedProductId)
|
|
||||||
|
|
||||||
const open = filtered.filter(t => !t.done)
|
useEffect(() => {
|
||||||
const done = filtered.filter(t => t.done)
|
setPagination(p => ({ ...p, pageIndex: 0 }))
|
||||||
|
setRowSelection({})
|
||||||
|
}, [selectedProductId])
|
||||||
|
|
||||||
function handleToggle(id: string, current: boolean) {
|
const columns = useMemo<ColumnDef<Todo>[]>(() => [
|
||||||
startTransition(async () => {
|
{
|
||||||
await toggleTodoAction(id, !current)
|
id: 'select',
|
||||||
})
|
header: ({ table }) => (
|
||||||
|
<IndeterminateCheckbox
|
||||||
|
checked={table.getIsAllPageRowsSelected()}
|
||||||
|
indeterminate={table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected()}
|
||||||
|
onChange={e => table.toggleAllPageRowsSelected(e.target.checked)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<IndeterminateCheckbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
disabled={!row.getCanSelect()}
|
||||||
|
onChange={e => row.toggleSelected(e.target.checked)}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'title',
|
||||||
|
header: 'Titel',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<p className={cn(
|
||||||
|
'line-clamp-2 text-sm leading-snug',
|
||||||
|
row.original.done && 'line-through text-muted-foreground'
|
||||||
|
)}>
|
||||||
|
{row.original.title}
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'product_name',
|
||||||
|
header: 'Product',
|
||||||
|
cell: ({ row }) => row.original.product_name ? (
|
||||||
|
<Badge variant="outline" className="text-xs font-normal">{row.original.product_name}</Badge>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground">—</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: 'Datum',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
|
{new Date(row.original.created_at).toLocaleDateString('nl-NL')}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
], [])
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: filtered,
|
||||||
|
columns,
|
||||||
|
state: { rowSelection, pagination },
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedRows = table.getSelectedRowModel().rows
|
||||||
|
const selectedCount = selectedRows.length
|
||||||
|
const { pageIndex, pageSize } = table.getState().pagination
|
||||||
|
const totalRows = filtered.length
|
||||||
|
const start = totalRows === 0 ? 0 : pageIndex * pageSize + 1
|
||||||
|
const end = Math.min((pageIndex + 1) * pageSize, totalRows)
|
||||||
|
const activeTodo = todos.find(t => t.id === activeRowId) ?? null
|
||||||
|
|
||||||
|
function handleRowClick(todo: Todo) {
|
||||||
|
setActiveRowId(prev => prev === todo.id ? null : todo.id)
|
||||||
|
setMode('idle')
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleArchive() {
|
function handleNew() {
|
||||||
|
setActiveRowId(null)
|
||||||
|
setRowSelection({})
|
||||||
|
setMode('create')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBulkArchive() {
|
||||||
|
const ids = selectedRows.map(r => r.original.id)
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
const result = await archiveCompletedTodosAction()
|
const result = await archiveSelectedTodosAction(ids)
|
||||||
if (result && 'error' in result) toast.error(result.error ?? 'Archiveren mislukt')
|
if (result?.error) {
|
||||||
else toast.success('Afgeronde todos gearchiveerd')
|
toast.error(typeof result.error === 'string' ? result.error : 'Archiveren mislukt')
|
||||||
|
} else {
|
||||||
|
const n = ids.length
|
||||||
|
toast.success(`${n} todo${n === 1 ? '' : "'s"} gearchiveerd`)
|
||||||
|
setRowSelection({})
|
||||||
|
setActiveRowId(null)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<QuickInput
|
{/* Toolbar */}
|
||||||
products={products}
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
isDemo={isDemo}
|
<select
|
||||||
selectedProductId={selectedProductId}
|
value={selectedProductId}
|
||||||
onProductChange={setSelectedProductId}
|
onChange={e => setSelectedProductId(e.target.value)}
|
||||||
/>
|
className="border border-border rounded-lg px-3 py-1.5 text-sm bg-input-background"
|
||||||
|
>
|
||||||
|
<option value="all">Alles</option>
|
||||||
|
<option value="">Geen product</option>
|
||||||
|
{products.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
|
||||||
|
</select>
|
||||||
|
|
||||||
{filtered.length === 0 ? (
|
<div className="flex-1" />
|
||||||
<div className="bg-surface-container-low border border-border rounded-xl p-12 text-center">
|
|
||||||
<p className="text-muted-foreground text-sm">
|
{selectedCount > 0 && !isDemo && (
|
||||||
{todos.length === 0 ? 'Geen todo’s. Voeg er een toe hierboven.' : 'Geen todo’s voor deze selectie.'}
|
<Button variant="outline" size="sm" onClick={handleBulkArchive} disabled={isPending}>
|
||||||
</p>
|
Archiveer geselecteerde ({selectedCount})
|
||||||
</div>
|
</Button>
|
||||||
) : (
|
)}
|
||||||
<>
|
|
||||||
<div className="bg-surface-container-low border border-border rounded-xl divide-y divide-border">
|
<DemoTooltip show={isDemo}>
|
||||||
{open.map(todo => (
|
<Button size="sm" onClick={handleNew} disabled={isDemo}>+</Button>
|
||||||
<div key={todo.id} className="group flex items-center gap-3 px-4 py-3">
|
</DemoTooltip>
|
||||||
<input
|
</div>
|
||||||
type="checkbox"
|
|
||||||
checked={false}
|
{/* Table */}
|
||||||
onChange={() => handleToggle(todo.id, false)}
|
<div className="rounded-xl border border-border overflow-hidden">
|
||||||
disabled={isDemo}
|
<Table>
|
||||||
className="w-4 h-4 rounded accent-primary cursor-pointer"
|
<TableHeader>
|
||||||
/>
|
{table.getHeaderGroups().map(hg => (
|
||||||
<span className="flex-1 text-sm">{todo.title}</span>
|
<TableRow key={hg.id} className="hover:bg-transparent">
|
||||||
{todo.product_name && (
|
{hg.headers.map(header => (
|
||||||
<span className="text-xs text-muted-foreground bg-surface-container px-1.5 py-0.5 rounded shrink-0">
|
<TableHead key={header.id}>
|
||||||
{todo.product_name}
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
</span>
|
</TableHead>
|
||||||
)}
|
))}
|
||||||
{!isDemo && (
|
</TableRow>
|
||||||
<div className="opacity-0 group-hover:opacity-100 flex gap-2 shrink-0">
|
|
||||||
<button onClick={() => setPromotePbi(todo)} className="text-xs text-muted-foreground hover:text-foreground">→ PBI</button>
|
|
||||||
<button onClick={() => setPromoteStory(todo)} className="text-xs text-muted-foreground hover:text-foreground">→ Story</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{done.map(todo => (
|
|
||||||
<div key={todo.id} className="flex items-center gap-3 px-4 py-3 opacity-60">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={true}
|
|
||||||
onChange={() => handleToggle(todo.id, true)}
|
|
||||||
disabled={isDemo}
|
|
||||||
className="w-4 h-4 rounded accent-primary cursor-pointer"
|
|
||||||
/>
|
|
||||||
<span className="flex-1 text-sm line-through text-muted-foreground">{todo.title}</span>
|
|
||||||
{todo.product_name && (
|
|
||||||
<span className="text-xs text-muted-foreground bg-surface-container px-1.5 py-0.5 rounded shrink-0">
|
|
||||||
{todo.product_name}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows.length > 0 ? (
|
||||||
|
table.getRowModel().rows.map(row => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
className={cn(
|
||||||
|
'cursor-pointer hover:bg-surface-container-low',
|
||||||
|
activeRowId === row.original.id
|
||||||
|
? 'bg-primary/5'
|
||||||
|
: row.getIsSelected()
|
||||||
|
? 'bg-primary/10'
|
||||||
|
: ''
|
||||||
|
)}
|
||||||
|
onClick={() => handleRowClick(row.original)}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map(cell => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="h-32 text-center text-sm text-muted-foreground">
|
||||||
|
{todos.length === 0
|
||||||
|
? "Nog geen todo's. Gebruik + om er een aan te maken."
|
||||||
|
: "Geen todo's voor deze selectie."}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{totalRows > pageSize && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-muted-foreground">{start}–{end} van {totalRows}</span>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>‹</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>›</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{done.length > 0 && !isDemo && (
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button variant="ghost" size="sm" className="text-muted-foreground text-xs" onClick={handleArchive}>
|
|
||||||
Archiveer afgeronde items ({done.length})
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Detail card — ST-510 */}
|
||||||
|
<div className="rounded-xl border border-border bg-surface-container-low p-5 min-h-[88px] flex items-center">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{mode === 'create'
|
||||||
|
? 'Nieuwe todo aanmaken — kaart wordt gebouwd in ST-510.'
|
||||||
|
: activeTodo
|
||||||
|
? `Geselecteerd: ${activeTodo.title}`
|
||||||
|
: 'Selecteer een rij of klik op + om te beginnen.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{promotePbi && (
|
{promotePbi && (
|
||||||
<PromotePbiDialog todo={promotePbi} products={products} onClose={() => setPromotePbi(null)} />
|
<PromotePbiDialog todo={promotePbi} products={products} onClose={() => setPromotePbi(null)} />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
38
package-lock.json
generated
38
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "scrum4me",
|
"name": "scrum4me",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "scrum4me",
|
"name": "scrum4me",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.4.1",
|
"@base-ui/react": "^1.4.1",
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@prisma/adapter-pg": "^7.8.0",
|
"@prisma/adapter-pg": "^7.8.0",
|
||||||
"@prisma/client": "^7.8.0",
|
"@prisma/client": "^7.8.0",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@vercel/analytics": "^2.0.1",
|
"@vercel/analytics": "^2.0.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|
@ -3909,6 +3910,26 @@
|
||||||
"tailwindcss": "4.2.4"
|
"tailwindcss": "4.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/react-table": {
|
||||||
|
"version": "8.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
|
||||||
|
"integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/table-core": "8.21.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8",
|
||||||
|
"react-dom": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tanstack/react-virtual": {
|
"node_modules/@tanstack/react-virtual": {
|
||||||
"version": "3.13.24",
|
"version": "3.13.24",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.24.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.24.tgz",
|
||||||
|
|
@ -3927,6 +3948,19 @@
|
||||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/table-core": {
|
||||||
|
"version": "8.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
|
||||||
|
"integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tanstack/virtual-core": {
|
"node_modules/@tanstack/virtual-core": {
|
||||||
"version": "3.14.0",
|
"version": "3.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.14.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@prisma/adapter-pg": "^7.8.0",
|
"@prisma/adapter-pg": "^7.8.0",
|
||||||
"@prisma/client": "^7.8.0",
|
"@prisma/client": "^7.8.0",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@vercel/analytics": "^2.0.1",
|
"@vercel/analytics": "^2.0.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue