2025-06-20 05:41:10 +00:00
|
|
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
2025-06-23 06:33:03 +00:00
|
|
|
import { useEffect } from 'react'
|
2025-06-20 05:41:10 +00:00
|
|
|
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
|
|
|
|
|
import {
|
|
|
|
|
clearItemSelectionActionAtom,
|
2025-06-23 06:33:03 +00:00
|
|
|
currentCharacterInventoryAtom,
|
2025-06-20 05:41:10 +00:00
|
|
|
filteredCharacterItemsAtom,
|
|
|
|
|
inventoryPageRangeAtom,
|
|
|
|
|
itemSelectionSelectAllFilterActionAtom,
|
|
|
|
|
itemSelectionSelectAllPageActionAtom,
|
2025-06-23 06:33:03 +00:00
|
|
|
moveSelectedItemsAtom,
|
|
|
|
|
openMoveConfirmationAtom,
|
2025-06-20 05:41:10 +00:00
|
|
|
paginateInventoryActionAtom,
|
|
|
|
|
preferenceInventorySearch,
|
|
|
|
|
selectedCharacterAtom,
|
|
|
|
|
} from '@/state/atoms'
|
2025-06-23 06:33:03 +00:00
|
|
|
import { MoveConfirmationPopup } from './MoveConfirmationPopup'
|
2025-06-20 05:41:10 +00:00
|
|
|
import { InventoryTargetSelector } from './movetarget'
|
|
|
|
|
import { InventoryTable } from './table'
|
2025-06-23 06:33:03 +00:00
|
|
|
import { InventoryFilters } from './InventoryFilters'
|
2025-05-25 05:17:41 +00:00
|
|
|
|
2025-06-23 06:33:03 +00:00
|
|
|
const InventoryRangeDisplay = () => {
|
2025-05-25 05:17:41 +00:00
|
|
|
const inventoryRange = useAtomValue(inventoryPageRangeAtom)
|
|
|
|
|
const items = useAtomValue(filteredCharacterItemsAtom)
|
2025-06-23 06:33:03 +00:00
|
|
|
|
2025-06-20 05:41:10 +00:00
|
|
|
return (
|
2025-06-23 06:33:03 +00:00
|
|
|
<div className="flex items-center px-2 bg-yellow-100 border border-black-1 whitespace-pre select-none">
|
|
|
|
|
{inventoryRange.start}..{inventoryRange.end}/{items.length}
|
2025-05-25 05:17:41 +00:00
|
|
|
</div>
|
2025-06-20 05:41:10 +00:00
|
|
|
)
|
2025-05-25 05:17:41 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-23 06:33:03 +00:00
|
|
|
|
2025-05-25 05:17:41 +00:00
|
|
|
export const Inventory = () => {
|
|
|
|
|
const selectedCharacter = useAtomValue(selectedCharacterAtom)
|
|
|
|
|
const clearItemSelection = useSetAtom(clearItemSelectionActionAtom)
|
2025-06-23 06:33:03 +00:00
|
|
|
const { refetch: refetchInventory } = useAtomValue(currentCharacterInventoryAtom)
|
2025-05-25 05:17:41 +00:00
|
|
|
|
|
|
|
|
const addPageItemSelection = useSetAtom(itemSelectionSelectAllPageActionAtom)
|
|
|
|
|
const addFilterItemSelection = useSetAtom(itemSelectionSelectAllFilterActionAtom)
|
|
|
|
|
const [search, setSearch] = useAtom(preferenceInventorySearch)
|
|
|
|
|
|
|
|
|
|
const paginateInventory = useSetAtom(paginateInventoryActionAtom)
|
2025-06-23 06:33:03 +00:00
|
|
|
const openMoveConfirmation = useSetAtom(openMoveConfirmationAtom)
|
|
|
|
|
const moveSelectedItems = useSetAtom(moveSelectedItemsAtom)
|
|
|
|
|
|
|
|
|
|
// Add keyboard navigation
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
|
|
|
// Don't paginate if user is typing in an input
|
|
|
|
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (e.key === 'ArrowLeft') {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
paginateInventory(-1)
|
|
|
|
|
} else if (e.key === 'ArrowRight') {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
paginateInventory(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown)
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener('keydown', handleKeyDown)
|
|
|
|
|
}
|
|
|
|
|
}, [paginateInventory])
|
2025-05-25 05:17:41 +00:00
|
|
|
|
2025-06-20 05:41:10 +00:00
|
|
|
if (!selectedCharacter) {
|
|
|
|
|
return <div>select a character</div>
|
2025-05-25 05:17:41 +00:00
|
|
|
}
|
2025-06-20 05:41:10 +00:00
|
|
|
return (
|
|
|
|
|
<div className={`flex flex-col h-full w-full`}>
|
|
|
|
|
<div className="flex flex-col py-2 flex-0 justify-between h-full">
|
2025-06-23 06:33:03 +00:00
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
<div className="flex flex-row gap-0 items-center justify-between">
|
|
|
|
|
<div className="flex flex-row gap-0 items-stretch">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="hover:cursor-pointer border border-black-1 bg-blue-200 hover:bg-blue-300 px-2 py-1"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (selectedCharacter) {
|
|
|
|
|
refetchInventory()
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
title="Refresh inventory"
|
|
|
|
|
>
|
|
|
|
|
↻
|
|
|
|
|
</button>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={search}
|
|
|
|
|
className="border border-black-1 px-2 py-1"
|
|
|
|
|
placeholder="search..."
|
|
|
|
|
onChange={e => {
|
|
|
|
|
setSearch(e.target.value)
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="hover:cursor-pointer border border-black-1 bg-green-200 hover:bg-green-300 px-2 flex items-center justify-center"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
paginateInventory(-1)
|
|
|
|
|
}}
|
|
|
|
|
aria-label="Previous page"
|
|
|
|
|
title="Previous page (← arrow key)"
|
|
|
|
|
>
|
|
|
|
|
<FaArrowLeft />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="hover:cursor-pointer border border-black-1 bg-green-200 hover:bg-green-300 px-2 flex items-center justify-center"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
paginateInventory(1)
|
|
|
|
|
}}
|
|
|
|
|
aria-label="Next page"
|
|
|
|
|
title="Next page (→ arrow key)"
|
|
|
|
|
>
|
|
|
|
|
<FaArrowRight />
|
|
|
|
|
</button>
|
|
|
|
|
<InventoryRangeDisplay />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-row gap-0">
|
|
|
|
|
<InventoryTargetSelector />
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={async e => {
|
|
|
|
|
if (e.shiftKey) {
|
|
|
|
|
// Shift+click: skip confirmation
|
|
|
|
|
const result = await moveSelectedItems()
|
|
|
|
|
if (result.successCount > 0) {
|
|
|
|
|
clearItemSelection()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Normal click: show confirmation
|
|
|
|
|
openMoveConfirmation()
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
className="hover:cursor-pointer whitespace-preborder border-black-1 bg-orange-200 hover:bg-orange-300 px-2 py-1"
|
|
|
|
|
title="Click to move with confirmation, Shift+Click to move immediately"
|
|
|
|
|
>
|
|
|
|
|
Move Selected
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-row gap-0">
|
2025-06-20 06:18:37 +00:00
|
|
|
<button
|
|
|
|
|
type="button"
|
2025-06-23 06:33:03 +00:00
|
|
|
className="whitespace-pre bg-purple-200 px-2 py-1 hover:cursor-pointer hover:bg-purple-300 border border-black-1"
|
2025-06-20 05:41:10 +00:00
|
|
|
onClick={() => {
|
2025-06-23 06:33:03 +00:00
|
|
|
addFilterItemSelection()
|
2025-06-20 05:41:10 +00:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
select filtered
|
2025-06-20 06:18:37 +00:00
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
2025-06-23 06:33:03 +00:00
|
|
|
className="whitespace-pre bg-cyan-200 px-2 py-1 hover:cursor-pointer hover:bg-cyan-300 border border-black-1"
|
2025-06-20 05:41:10 +00:00
|
|
|
onClick={() => {
|
2025-06-23 06:33:03 +00:00
|
|
|
addPageItemSelection()
|
2025-06-20 05:41:10 +00:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
select page
|
2025-06-20 06:18:37 +00:00
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
2025-06-23 06:33:03 +00:00
|
|
|
className="whitespace-pre bg-red-200 px-2 py-1 hover:cursor-pointer hover:bg-red-300 border border-black-1"
|
2025-06-20 05:41:10 +00:00
|
|
|
onClick={() => {
|
|
|
|
|
clearItemSelection()
|
|
|
|
|
}}
|
|
|
|
|
>
|
2025-06-23 06:33:03 +00:00
|
|
|
clear
|
2025-06-20 06:18:37 +00:00
|
|
|
</button>
|
2025-06-20 05:41:10 +00:00
|
|
|
</div>
|
2025-05-25 05:17:41 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-23 06:33:03 +00:00
|
|
|
<div className="flex flex-row justify-between items-center">
|
|
|
|
|
<InventoryFilters />
|
|
|
|
|
<InventoryRangeDisplay />
|
|
|
|
|
</div>
|
2025-06-20 05:41:10 +00:00
|
|
|
<div className="flex flex-col flex-1 h-full border border-black-2">
|
|
|
|
|
<InventoryTable />
|
|
|
|
|
</div>
|
2025-06-23 06:33:03 +00:00
|
|
|
<MoveConfirmationPopup />
|
2025-05-25 05:17:41 +00:00
|
|
|
</div>
|
2025-06-20 05:41:10 +00:00
|
|
|
)
|
2025-05-25 05:17:41 +00:00
|
|
|
}
|