1
0
forked from a/lifeto-shop
lifeto-shop/src/lib/table/tanstack.tsx
2025-06-23 01:33:03 -05:00

240 lines
7.7 KiB
TypeScript

import { createColumnHelper } from '@tanstack/react-table'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { useMemo } from 'react'
import {
currentItemSelectionAtom,
itemSelectionSetActionAtom,
mouseDragSelectionStateAtom,
} from '@/state/atoms'
import { StatsColumns } from '../columns'
import { ItemWithSelection } from './defs'
const ch = createColumnHelper<ItemWithSelection>()
const columns = {
icon: ch.display({
id: 'icon',
header: function Component(_col) {
return <div className="flex flex-row justify-center"></div>
},
cell: function Component({ row }) {
const setItemSelection = useSetAtom(itemSelectionSetActionAtom)
const c = useAtomValue(currentItemSelectionAtom)
const [dragState, setDragState] = useAtom(mouseDragSelectionStateAtom)
const selected = useMemo(() => {
return c[0].has(row.original.item.id)
}, [c])
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault()
const newSelected = !selected
setItemSelection({
[row.original.item.id]: newSelected ? row.original.item.item_count : undefined,
})
setDragState({
isDragging: true,
lastAction: newSelected ? 'select' : 'deselect',
lastItemId: row.original.item.id,
})
}
const handleMouseEnter = () => {
if (dragState.isDragging && dragState.lastItemId !== row.original.item.id) {
if (dragState.lastAction === 'select' && !selected) {
setItemSelection({
[row.original.item.id]: row.original.item.item_count,
})
} else if (dragState.lastAction === 'deselect' && selected) {
setItemSelection({
[row.original.item.id]: undefined,
})
}
}
}
return (
<button
type="button"
className={`no-select flex flex-row ${row.original.status?.selected ? 'animate-pulse' : ''}`}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter}
>
<div className="flex flex-row w-6 h-6 justify-center">
<img
src={row.original.item.item_image || ''}
alt="icon"
className="select-none object-contain pointer-events-none"
draggable={false}
/>
</div>
</button>
)
},
}),
count: ch.display({
id: 'count',
header: function Component(_col) {
return <div className="flex flex-row justify-center">#</div>
},
cell: function Component({ row }) {
const c = useAtomValue(currentItemSelectionAtom)
const setItemSelection = useSetAtom(itemSelectionSetActionAtom)
const dragState = useAtomValue(mouseDragSelectionStateAtom)
const currentValue = useMemo(() => {
const got = c[0].get(row.original.item.id)
if (got !== undefined) {
return got.toString()
}
return ''
}, [c])
const itemCount = row.original.item.item_count
const selected = useMemo(() => {
return c[0].has(row.original.item.id)
}, [c])
const handleMouseEnter = () => {
if (dragState.isDragging && dragState.lastItemId !== row.original.item.id) {
if (dragState.lastAction === 'select' && !selected) {
setItemSelection({
[row.original.item.id]: row.original.item.item_count,
})
} else if (dragState.lastAction === 'deselect' && selected) {
setItemSelection({
[row.original.item.id]: undefined,
})
}
}
}
return (
// biome-ignore lint/a11y/useSemanticElements: Using div for layout with input child
// biome-ignore lint/a11y/noStaticElementInteractions: Mouse interaction needed for drag select
<div
className={`flex flex-row select-none ${row.original.status?.selected ? 'bg-gray-200' : ''}`}
onMouseEnter={handleMouseEnter}
>
<input
className="w-10 text-center "
value={currentValue}
onChange={e => {
if (e.target.value === '') {
setItemSelection({ [row.original.item.id]: undefined })
return
}
if (e.target.value === '-') {
setItemSelection({
[row.original.item.id]: itemCount,
})
}
let parsedInt = parseInt(e.target.value)
if (Number.isNaN(parsedInt)) {
return
}
if (parsedInt > itemCount) {
parsedInt = itemCount
}
setItemSelection({
[row.original.item.id]: parsedInt,
})
}}
placeholder={itemCount.toString()}
/>
</div>
)
},
}),
name: ch.display({
id: 'name',
header: _col => {
return <div className="flex flex-row text-sm">name</div>
},
cell: function Component({ row }) {
const c = useAtomValue(currentItemSelectionAtom)
const setItemSelection = useSetAtom(itemSelectionSetActionAtom)
const [dragState, setDragState] = useAtom(mouseDragSelectionStateAtom)
const selected = useMemo(() => {
return c[0].has(row.original.item.id)
}, [c])
const handleMouseEnter = () => {
if (dragState.isDragging && dragState.lastItemId !== row.original.item.id) {
if (dragState.lastAction === 'select' && !selected) {
setItemSelection({
[row.original.item.id]: row.original.item.item_count,
})
} else if (dragState.lastAction === 'deselect' && selected) {
setItemSelection({
[row.original.item.id]: undefined,
})
}
}
}
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault()
const newSelected = !selected
setItemSelection({
[row.original.item.id]: newSelected ? row.original.item.item_count : undefined,
})
setDragState({
isDragging: true,
lastAction: newSelected ? 'select' : 'deselect',
lastItemId: row.original.item.id,
})
}
return (
// biome-ignore lint/a11y/useSemanticElements: Using div for text content
// biome-ignore lint/a11y/noStaticElementInteractions: Mouse interaction needed for drag select
<div
className="flex flex-row whitespace-pre cursor-pointer select-none hover:bg-gray-100"
onMouseEnter={handleMouseEnter}
onMouseDown={handleMouseDown}
>
<span>{row.original.item.item_name}</span>
</div>
)
},
}),
slots: ch.display({
id: 'slots',
header: _col => {
return <div className="flex flex-row text-sm">slots</div>
},
cell: function Component({ row }) {
return (
<div className="flex flex-row justify-center">
<span>{row.original.item.item_slots}</span>
</div>
)
},
}),
stats: ch.group({
id: 'stats',
header: _col => {
return <div className="flex flex-row text-sm">stats</div>
},
columns: [
...StatsColumns.map(c => {
return ch.display({
id: `stats.${c}`,
header: _col => {
return <div className="flex flex-row text-sm justify-center">{c}</div>
},
cell: function Component({ row }) {
const stats = row.original.item.stats
const stat = stats ? stats[c] : ''
return (
<div className={`flex flex-row justify-start ${stat ? 'border' : ''}`}>
<span>{stat}</span>
</div>
)
},
})
}),
],
}),
} as const
export const InventoryColumns = columns