1
0
forked from a/lifeto-shop
lifeto-shop/src/lib/table/tanstack.tsx

240 lines
7.7 KiB
TypeScript
Raw Normal View History

2025-06-20 05:41:10 +00:00
import { createColumnHelper } from '@tanstack/react-table'
2025-06-23 06:33:03 +00:00
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
2025-06-20 05:41:10 +00:00
import { useMemo } from 'react'
2025-06-23 06:33:03 +00:00
import {
currentItemSelectionAtom,
itemSelectionSetActionAtom,
mouseDragSelectionStateAtom,
} from '@/state/atoms'
2025-06-20 05:41:10 +00:00
import { StatsColumns } from '../columns'
import { ItemWithSelection } from './defs'
2025-05-25 05:17:41 +00:00
2025-06-20 05:41:10 +00:00
const ch = createColumnHelper<ItemWithSelection>()
2025-05-25 05:17:41 +00:00
const columns = {
icon: ch.display({
id: 'icon',
2025-06-20 05:41:10 +00:00
header: function Component(_col) {
2025-05-25 05:17:41 +00:00
return <div className="flex flex-row justify-center"></div>
},
2025-06-20 05:41:10 +00:00
cell: function Component({ row }) {
const setItemSelection = useSetAtom(itemSelectionSetActionAtom)
const c = useAtomValue(currentItemSelectionAtom)
2025-06-23 06:33:03 +00:00
const [dragState, setDragState] = useAtom(mouseDragSelectionStateAtom)
2025-06-20 05:41:10 +00:00
const selected = useMemo(() => {
return c[0].has(row.original.item.id)
2025-05-25 05:17:41 +00:00
}, [c])
2025-06-23 06:33:03 +00:00
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,
})
}
}
}
2025-06-20 05:41:10 +00:00
return (
2025-06-20 06:18:37 +00:00
<button
type="button"
2025-06-20 05:41:10 +00:00
className={`no-select flex flex-row ${row.original.status?.selected ? 'animate-pulse' : ''}`}
2025-06-23 06:33:03 +00:00
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter}
2025-06-20 05:41:10 +00:00
>
<div className="flex flex-row w-6 h-6 justify-center">
<img
src={row.original.item.item_image || ''}
alt="icon"
2025-06-23 06:33:03 +00:00
className="select-none object-contain pointer-events-none"
draggable={false}
2025-06-20 05:41:10 +00:00
/>
</div>
2025-06-20 06:18:37 +00:00
</button>
2025-06-20 05:41:10 +00:00
)
2025-05-25 05:17:41 +00:00
},
}),
count: ch.display({
id: 'count',
2025-06-20 05:41:10 +00:00
header: function Component(_col) {
2025-05-25 05:17:41 +00:00
return <div className="flex flex-row justify-center">#</div>
},
2025-06-20 05:41:10 +00:00
cell: function Component({ row }) {
const c = useAtomValue(currentItemSelectionAtom)
const setItemSelection = useSetAtom(itemSelectionSetActionAtom)
2025-06-23 06:33:03 +00:00
const dragState = useAtomValue(mouseDragSelectionStateAtom)
2025-06-20 05:41:10 +00:00
const currentValue = useMemo(() => {
const got = c[0].get(row.original.item.id)
if (got !== undefined) {
return got.toString()
2025-05-25 05:17:41 +00:00
}
2025-06-20 05:41:10 +00:00
return ''
2025-05-25 05:17:41 +00:00
}, [c])
const itemCount = row.original.item.item_count
2025-06-23 06:33:03 +00:00
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,
})
}
}
}
2025-06-20 05:41:10 +00:00
return (
2025-06-23 06:33:03 +00:00
// biome-ignore lint/a11y/useSemanticElements: Using div for layout with input child
// biome-ignore lint/a11y/noStaticElementInteractions: Mouse interaction needed for drag select
2025-06-20 05:41:10 +00:00
<div
className={`flex flex-row select-none ${row.original.status?.selected ? 'bg-gray-200' : ''}`}
2025-06-23 06:33:03 +00:00
onMouseEnter={handleMouseEnter}
2025-06-20 05:41:10 +00:00
>
<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
}
2025-05-25 05:17:41 +00:00
setItemSelection({
2025-06-20 05:41:10 +00:00
[row.original.item.id]: parsedInt,
2025-05-25 05:17:41 +00:00
})
2025-06-20 05:41:10 +00:00
}}
placeholder={itemCount.toString()}
/>
</div>
)
2025-05-25 05:17:41 +00:00
},
}),
name: ch.display({
id: 'name',
2025-06-20 05:41:10 +00:00
header: _col => {
return <div className="flex flex-row text-sm">name</div>
2025-05-25 05:17:41 +00:00
},
2025-06-20 05:41:10 +00:00
cell: function Component({ row }) {
2025-06-23 06:33:03 +00:00
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,
})
}
2025-06-20 05:41:10 +00:00
return (
2025-06-23 06:33:03 +00:00
// 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}
>
2025-06-20 05:41:10 +00:00
<span>{row.original.item.item_name}</span>
</div>
)
2025-05-25 05:17:41 +00:00
},
}),
slots: ch.display({
id: 'slots',
2025-06-20 05:41:10 +00:00
header: _col => {
return <div className="flex flex-row text-sm">slots</div>
2025-05-25 05:17:41 +00:00
},
2025-06-20 05:41:10 +00:00
cell: function Component({ row }) {
return (
<div className="flex flex-row justify-center">
<span>{row.original.item.item_slots}</span>
</div>
)
2025-05-25 05:17:41 +00:00
},
}),
stats: ch.group({
id: 'stats',
2025-06-20 05:41:10 +00:00
header: _col => {
return <div className="flex flex-row text-sm">stats</div>
2025-05-25 05:17:41 +00:00
},
columns: [
2025-06-20 05:41:10 +00:00
...StatsColumns.map(c => {
2025-05-25 05:17:41 +00:00
return ch.display({
2025-06-20 05:41:10 +00:00
id: `stats.${c}`,
header: _col => {
return <div className="flex flex-row text-sm justify-center">{c}</div>
2025-05-25 05:17:41 +00:00
},
2025-06-20 05:41:10 +00:00
cell: function Component({ row }) {
2025-05-25 05:17:41 +00:00
const stats = row.original.item.stats
2025-06-20 05:41:10 +00:00
const stat = stats ? stats[c] : ''
return (
<div className={`flex flex-row justify-start ${stat ? 'border' : ''}`}>
<span>{stat}</span>
</div>
)
2025-05-25 05:17:41 +00:00
},
})
2025-06-20 05:41:10 +00:00
}),
],
2025-05-25 05:17:41 +00:00
}),
2025-06-20 05:41:10 +00:00
} as const
2025-05-25 05:17:41 +00:00
2025-06-20 05:41:10 +00:00
export const InventoryColumns = columns