lifeto-shop/src/components/inventory.tsx

267 lines
7.7 KiB
TypeScript
Raw Normal View History

2024-08-12 01:13:42 +00:00
import { TricksterCharacter } from "../lib/trickster"
import { useSessionContext } from "../context/SessionContext"
import 'handsontable/dist/handsontable.full.min.css';
import { registerAllModules } from 'handsontable/registry';
import { HotTable, HotTableClass } from '@handsontable/react';
2025-05-13 21:02:59 +00:00
import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState} from "react";
2024-08-12 01:13:42 +00:00
import { InventoryTable } from "../lib/table";
import { DotLoader } from "react-spinners";
2025-05-13 21:02:59 +00:00
import { useResizeObserver } from "@mantine/hooks";
2024-08-12 01:13:42 +00:00
import { Columns } from "../lib/columns";
import { OrderDetails, OrderSender } from "../lib/lifeto/order_manager";
import log from "loglevel";
2025-05-13 21:02:59 +00:00
import { charactersAtom, currentCharacterInventoryAtom, currentCharacterItemsAtom, LTOApi, selectedTargetInventoryAtom } from "../state/atoms";
import Select from 'react-select';
import { useAtom, useAtomValue } from "jotai";
import { autoUpdate, flip, FloatingFocusManager, FloatingPortal, size, useDismiss, useFloating, useInteractions, useListNavigation, useRole } from "@floating-ui/react";
import Fuse from "fuse.js";
2024-08-12 01:13:42 +00:00
registerAllModules();
type Size = {
width?: number
height?: number
}
2025-05-13 21:02:59 +00:00
interface InventoryItemProps {
children: React.ReactNode;
active: boolean;
}
const InventoryItem = forwardRef<
HTMLDivElement,
InventoryItemProps & React.HTMLProps<HTMLDivElement>
>(({ children, active, ...rest }, ref) => {
const id = useId();
return (
<div
ref={ref}
role="option"
id={id}
aria-selected={active}
{...rest}
style={{
background: active ? "lightblue" : "none",
padding: 4,
cursor: "default",
...rest.style,
}}
>
{children}
</div>
);
});
const InventoryTargetSelector = () => {
const [open, setOpen] = useState(false);
const [inputValue, setInputValue] = useState("");
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const listRef = useRef<Array<HTMLElement | null>>([]);
const { refs, floatingStyles, context } = useFloating<HTMLInputElement>({
whileElementsMounted: autoUpdate,
open,
onOpenChange: setOpen,
middleware: [
flip({ padding: 10 }),
size({
apply({ rects, availableHeight, elements }) {
Object.assign(elements.floating.style, {
width: `${rects.reference.width}px`,
maxHeight: `${availableHeight}px`,
});
},
padding: 10,
}),
],
});
const role = useRole(context, { role: "listbox" });
const dismiss = useDismiss(context);
const listNav = useListNavigation(context, {
listRef,
activeIndex,
onNavigate: setActiveIndex,
virtual: true,
loop: true,
});
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions(
[role, dismiss, listNav]
);
function onChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = event.target.value;
setInputValue(value);
setSelectedTargetInventory(undefined)
if (value) {
setOpen(true);
setActiveIndex(0);
} else {
setOpen(false);
}
}
const { data: subaccounts } = useAtomValue(charactersAtom)
const [selectedTargetInventory, setSelectedTargetInventory] = useAtom(selectedTargetInventoryAtom)
const searcher = useMemo(()=>{
return new Fuse(subaccounts?.flatMap(x=>[
x.bank,
x.character,
])||[], {
keys:["path","name"],
findAllMatches: true,
threshold: 0.8,
useExtendedSearch: true,
})
}, [subaccounts])
const items = searcher.search(inputValue || "!-", {limit: 10}).map(x=>x.item)
return (
<>
<input
className="border border-black-1 bg-gray-100 placeholder-gray-600"
{...getReferenceProps({
ref: refs.setReference,
onChange,
value: selectedTargetInventory !== undefined ? selectedTargetInventory.name : inputValue,
placeholder: "Target Inventory",
"aria-autocomplete": "list",
onKeyDown(event) {
if (
event.key === "Enter" &&
activeIndex != null &&
items[activeIndex]
) {
setSelectedTargetInventory(items[activeIndex])
setInputValue(items[activeIndex].name);
setActiveIndex(null);
setOpen(false);
}
},
})}
/>
{open && (
<FloatingPortal>
<FloatingFocusManager
context={context}
initialFocus={-1}
visuallyHiddenDismiss
>
<div
{...getFloatingProps({
ref: refs.setFloating,
style: {
...floatingStyles,
background: "#eee",
color: "black",
overflowY: "auto",
},
})}
>
{items.map((item, index) => (
<InventoryItem
{...getItemProps({
key: item.path,
ref(node) {
listRef.current[index] = node;
},
onClick() {
setInputValue(item.name);
setSelectedTargetInventory(item);
setOpen(false);
refs.domReference.current?.focus();
},
})}
active={activeIndex === index}
>
{item.name}
</InventoryItem>
))}
</div>
</FloatingFocusManager>
</FloatingPortal>
)}
</>
);
}
2024-08-12 01:13:42 +00:00
export const Inventory = () => {
const {activeTable, columns, tags, orders} = useSessionContext()
2025-05-13 21:02:59 +00:00
const [ref, {height}] = useResizeObserver({})
2024-08-12 01:13:42 +00:00
2025-05-13 21:02:59 +00:00
const {data:character, isLoading, isFetching } = useAtomValue(currentCharacterInventoryAtom)
//const sendOrders = useCallback(()=>{
// if(!hotTableComponent.current?.hotInstance){
// return
// }
// const hott = hotTableComponent.current?.hotInstance
// const headers = hott.getColHeader()
// const dat = hott.getData()
// const idxNumber = headers.indexOf(Columns.MoveCount.displayName)
// const idxTarget = headers.indexOf(Columns.Move.displayName)
// const origin = activeTable
// const pending:OrderDetails[] = [];
// for(const row of dat) {
// try{
// const nm = Number(row[idxNumber].replace("x",""))
// const target = (row[idxTarget] as string).replaceAll("-","").trim()
// if(!isNaN(nm) && nm > 0 && target.length > 0){
// const info:OrderDetails = {
// item_uid: row[0].toString(),
// count: nm,
// origin_path: activeTable,
// target_path: target,
// }
// pending.push(info)
// }
// }catch(e){
// }
// }
// log.debug("OrderDetails", pending)
// const chars = new Map<string,TricksterCharacter>()
// const manager = new OrderSender(orders, chars)
// for(const d of pending){
// const order = manager.send(d)
// //order.tick(api)
// }
//}, [orders])
2024-08-12 01:13:42 +00:00
const Loading = ()=>{
return <div role="status" className="flex align-center justify-center">
<div className="justify-center py-4">
<DotLoader color="#dddddd"/>
</div>
</div>
}
2025-05-13 21:02:59 +00:00
const items = useAtomValue(currentCharacterItemsAtom)
2024-08-12 01:13:42 +00:00
return <div ref={ref} className={``}>
2025-05-13 21:02:59 +00:00
<div className="flex flex-row py-2 justify-end">
<InventoryTargetSelector/>
2024-08-12 01:13:42 +00:00
<div
onClick={(e)=>{
2025-05-13 21:02:59 +00:00
// sendOrders()
2024-08-12 01:13:42 +00:00
}}
className="
hover:cursor-pointer
border border-black-1
bg-green-200
px-2 py-1
2025-05-13 21:02:59 +00:00
">Move Selected</div>
2024-08-12 01:13:42 +00:00
</div>
2025-05-13 21:02:59 +00:00
{(isLoading || isFetching) ? <Loading/> : <>
<div>
total: {items.size}
</div>
</> }
2024-08-12 01:13:42 +00:00
</div>
}