noot
This commit is contained in:
parent
79f90a7478
commit
3d30ab085f
@ -12,6 +12,7 @@
|
||||
"@handsontable/react": "^15.3.0",
|
||||
"@mantine/hooks": "^8.0.0",
|
||||
"@tanstack/react-query": "^5.76.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@types/qs": "^6.9.18",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
@ -19,6 +20,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"arktype": "^2.1.20",
|
||||
"axios": "^1.9.0",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
@ -28,15 +30,19 @@
|
||||
"fuse.js": "^7.1.0",
|
||||
"handsontable": "^15.3.0",
|
||||
"jotai": "^2.12.4",
|
||||
"jotai-optics": "^0.4.0",
|
||||
"jotai-tanstack-query": "^0.9.0",
|
||||
"loglevel": "^1.9.2",
|
||||
"optics-ts": "^2.4.1",
|
||||
"pinia": "^3.0.2",
|
||||
"prettier": "^3.5.3",
|
||||
"qs": "^6.14.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-select": "^5.10.1",
|
||||
"react-spinners": "^0.17.0",
|
||||
"superjson": "^2.2.2",
|
||||
"typescript-cookie": "^1.0.6",
|
||||
"use-local-storage": "^3.0.0",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
@ -44,6 +50,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.6",
|
||||
"@types/node": "^22.15.18",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.1.6",
|
||||
"typescript": "^5.8.3",
|
||||
|
BIN
public/cursor.png
Normal file
BIN
public/cursor.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
16
src/App.tsx
16
src/App.tsx
@ -1,21 +1,17 @@
|
||||
import { FC } from "react";
|
||||
import { LoginWidget } from "./components/login";
|
||||
import { CharacterRoulette } from "./components/characters";
|
||||
import { Inventory } from "./components/inventory";
|
||||
import { Inventory } from "./components/inventory/index";
|
||||
|
||||
export const App: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col mx-auto p-4 w-full">
|
||||
<div className="flex flex-row max-w-6xl">
|
||||
<div className="flex flex-row justify-end w-full">
|
||||
<LoginWidget/>
|
||||
</div>
|
||||
<div className="flex flex-row mx-auto p-4 gap-8 w-full h-full">
|
||||
<div className="flex flex-col">
|
||||
<LoginWidget/>
|
||||
<CharacterRoulette/>
|
||||
</div>
|
||||
<div>
|
||||
<CharacterRoulette/>
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<div className="flex-1">
|
||||
<Inventory/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { TricksterCharacter } from "../lib/trickster"
|
||||
import { useSessionContext } from "../context/SessionContext"
|
||||
import Fuse from 'fuse.js'
|
||||
import { useAtom, useSetAtom } from "jotai"
|
||||
import { useAtom } from "jotai"
|
||||
import { charactersAtom, selectedCharacterAtom } from "../state/atoms"
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
@ -136,24 +135,24 @@ export const CharacterRoulette = ()=>{
|
||||
return <PleaseLogin/>
|
||||
}
|
||||
const searchResults = fuse.search(search || "!-----", {
|
||||
limit: 20,
|
||||
}).map((x)=>{
|
||||
return <div className="flex flex-col" key={`${x.item.character.account_id}`}>
|
||||
<CharacterCard key={x.item.bank.id} character={x.item.bank} />
|
||||
<CharacterCard key={x.item.character.id} character={x.item.character} />
|
||||
</div>
|
||||
})
|
||||
limit: 20,
|
||||
}).map((x)=>{
|
||||
return <div className="flex flex-col" key={`${x.item.character.account_id}`}>
|
||||
<CharacterCard key={x.item.bank.id} character={x.item.bank} />
|
||||
<CharacterCard key={x.item.character.id} character={x.item.character} />
|
||||
</div>
|
||||
})
|
||||
return <>
|
||||
<div className="flex flex-col gap-1">
|
||||
<input
|
||||
className="border border-black-1 bg-gray-100 placeholder-gray-600 p-1 max-w-[200px]"
|
||||
className="border border-black-1 bg-gray-100 placeholder-gray-600 p-1 max-w-[180px]"
|
||||
placeholder="search character..."
|
||||
value={search}
|
||||
onChange={(e)=>{
|
||||
setSearch(e.target.value)
|
||||
}}
|
||||
></input>
|
||||
<div className="flex flex-row overflow-x-scroll gap-1 h-full min-h-36">
|
||||
<div className="flex flex-row flex-wrap overflow-x-scroll gap-1 h-full min-h-36 max-w-48">
|
||||
{searchResults ? searchResults : <>
|
||||
</>}
|
||||
</div>
|
||||
|
149
src/components/inventory/index.tsx
Normal file
149
src/components/inventory/index.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import { clearItemSelectionActionAtom, currentCharacterItemsAtom, filteredCharacterItemsAtom, inventoryFilterAtom, inventoryItemsCurrentPageAtom, inventoryPageRangeAtom, itemSelectionSelectAllFilterActionAtom, itemSelectionSelectAllPageActionAtom, paginateInventoryActionAtom, preferenceInventorySearch, selectedCharacterAtom, setInventoryFilterTabActionAtom} from "@/state/atoms";
|
||||
import {useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { InventoryTargetSelector } from './movetarget';
|
||||
import { InventoryTable } from './table';
|
||||
import { FaArrowLeft, FaArrowRight } from "react-icons/fa";
|
||||
|
||||
|
||||
|
||||
|
||||
const sections = [
|
||||
{ name: 'all', value: '' },
|
||||
{ name: 'consume', value: '1' },
|
||||
{ name: 'equip', value: '2' },
|
||||
{ name: 'drill', value: '3' },
|
||||
{ name: 'pet', value: '4' },
|
||||
{ name: 'etc', value: '5' },
|
||||
|
||||
]
|
||||
|
||||
const cardSections = [
|
||||
{ name: 'skill', value: '10' },
|
||||
{ name: 'char', value: '11' },
|
||||
{ name: 'mon', value: '12' },
|
||||
{ name: 'fortune', value: '13' },
|
||||
{ name: 'secret', value: '14' },
|
||||
{ name: 'arcana', value: '15' },
|
||||
]
|
||||
|
||||
const InventoryTabs = ()=> {
|
||||
|
||||
const inventoryFilter= useAtomValue(inventoryFilterAtom)
|
||||
const setInventoryFilterTab = useSetAtom(setInventoryFilterTabActionAtom)
|
||||
const inventoryRange = useAtomValue(inventoryPageRangeAtom)
|
||||
const items = useAtomValue(filteredCharacterItemsAtom)
|
||||
console.log("items", items)
|
||||
const sharedStyle = "hover:cursor-pointer hover:bg-gray-200 px-2 pr-4 border border-gray-200"
|
||||
const selectedStyle = "bg-gray-200 border-b-2 border-black-1"
|
||||
return <div className="flex flex-row gap-1 justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex flex-row gap-1">
|
||||
{sections.map(x=>{
|
||||
return <div
|
||||
onClick={()=>{
|
||||
setInventoryFilterTab(x.value)
|
||||
}}
|
||||
key={x.name}
|
||||
className={`${sharedStyle}
|
||||
${inventoryFilter.tab === x.value ? selectedStyle : ""}`}
|
||||
>{x.name}</div>
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-row gap-1">
|
||||
{cardSections.map(x=>{
|
||||
return <div
|
||||
onClick={()=>{
|
||||
setInventoryFilterTab(x.value)
|
||||
}}
|
||||
key={x.name}
|
||||
className={`${sharedStyle}
|
||||
${inventoryFilter.tab === x.value ? selectedStyle : ""}`}
|
||||
>{x.name}</div>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-1 items-center px-1 bg-yellow-100">
|
||||
<div className="whitespace-pre select-none">{inventoryRange.start}..{inventoryRange.end}/{items.length} </div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export const Inventory = () => {
|
||||
|
||||
const selectedCharacter = useAtomValue(selectedCharacterAtom)
|
||||
const clearItemSelection = useSetAtom(clearItemSelectionActionAtom)
|
||||
|
||||
const addPageItemSelection = useSetAtom(itemSelectionSelectAllPageActionAtom)
|
||||
const addFilterItemSelection = useSetAtom(itemSelectionSelectAllFilterActionAtom)
|
||||
const [search, setSearch] = useAtom(preferenceInventorySearch)
|
||||
|
||||
|
||||
const paginateInventory = useSetAtom(paginateInventoryActionAtom)
|
||||
|
||||
if(!selectedCharacter){
|
||||
return <div>
|
||||
select a character
|
||||
</div>
|
||||
}
|
||||
return <div className={`flex flex-col h-full w-full`}>
|
||||
<div className="flex flex-col py-2 flex-0 justify-between h-full">
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="flex flex-row gap-2">
|
||||
<div className="whitespace-pre bg-blue-200 px-2 py-1 rounded-xl hover:cursor-pointer hover:bg-blue-300"
|
||||
onClick={()=>{
|
||||
addPageItemSelection()
|
||||
}}
|
||||
>select filtered</div>
|
||||
<div className="whitespace-pre bg-blue-200 px-2 py-1 rounded-xl hover:cursor-pointer hover:bg-blue-300"
|
||||
onClick={()=>{
|
||||
addFilterItemSelection()
|
||||
}}
|
||||
>select page</div>
|
||||
<div className="whitespace-pre bg-blue-200 px-2 py-1 rounded-xl hover:cursor-pointer hover:bg-blue-300"
|
||||
onClick={()=>{
|
||||
clearItemSelection()
|
||||
}}
|
||||
>clear </div>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<InventoryTargetSelector/>
|
||||
<div
|
||||
onClick={(e)=>{
|
||||
// sendOrders()
|
||||
}}
|
||||
className="hover:cursor-pointer whitespace-preborder border-black-1 bg-orange-200 hover:bg-orange-300 px-2 py-1">Move Selected</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 justify-between">
|
||||
<div className="flex flex-row gap-0 items-center">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
className="border border-black-1 px-2 py-1"
|
||||
placeholder="search..."
|
||||
onChange={(e)=>{
|
||||
setSearch(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="hover:cursor-pointer border border-black-1 bg-green-200 hover:bg-green-300 px-2 py-1 h-full flex items-center"
|
||||
onClick={()=>{
|
||||
paginateInventory(-1)
|
||||
}}
|
||||
><FaArrowLeft/></div>
|
||||
<div
|
||||
className="hover:cursor-pointer border border-black-1 bg-green-200 hover:bg-green-300 px-2 py-1 h-full flex items-center"
|
||||
onClick={()=>{
|
||||
paginateInventory(1)
|
||||
}}
|
||||
><FaArrowRight/></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<InventoryTabs />
|
||||
<div className="flex flex-col flex-1 h-full border border-black-2">
|
||||
<InventoryTable />
|
||||
</div>
|
||||
</div>
|
||||
}
|
@ -1,39 +1,20 @@
|
||||
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';
|
||||
import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState} from "react";
|
||||
import { InventoryTable } from "../lib/table";
|
||||
import { DotLoader } from "react-spinners";
|
||||
import { useResizeObserver } from "@mantine/hooks";
|
||||
import { Columns } from "../lib/columns";
|
||||
import { OrderDetails, OrderSender } from "../lib/lifeto/order_manager";
|
||||
import log from "loglevel";
|
||||
import { charactersAtom, currentCharacterInventoryAtom, currentCharacterItemsAtom, LTOApi, selectedTargetInventoryAtom } from "../state/atoms";
|
||||
import Select from 'react-select';
|
||||
import { forwardRef, useId, useMemo, useRef, useState} from "react";
|
||||
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";
|
||||
import { charactersAtom, selectedTargetInventoryAtom } from "@/state/atoms";
|
||||
|
||||
registerAllModules();
|
||||
type Size = {
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface InventoryItemProps {
|
||||
interface AccountInventorySelectorItemProps {
|
||||
children: React.ReactNode;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
const InventoryItem = forwardRef<
|
||||
|
||||
|
||||
const AccountInventorySelectorItem = forwardRef<
|
||||
HTMLDivElement,
|
||||
InventoryItemProps & React.HTMLProps<HTMLDivElement>
|
||||
AccountInventorySelectorItemProps & React.HTMLProps<HTMLDivElement>
|
||||
>(({ children, active, ...rest }, ref) => {
|
||||
const id = useId();
|
||||
return (
|
||||
@ -54,8 +35,7 @@ const InventoryItem = forwardRef<
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const InventoryTargetSelector = () => {
|
||||
export const InventoryTargetSelector = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
||||
@ -133,6 +113,9 @@ const InventoryTargetSelector = () => {
|
||||
value: selectedTargetInventory !== undefined ? selectedTargetInventory.name : inputValue,
|
||||
placeholder: "Target Inventory",
|
||||
"aria-autocomplete": "list",
|
||||
onFocus() {
|
||||
setOpen(true);
|
||||
},
|
||||
onKeyDown(event) {
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
@ -166,7 +149,7 @@ const InventoryTargetSelector = () => {
|
||||
})}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<InventoryItem
|
||||
<AccountInventorySelectorItem
|
||||
{...getItemProps({
|
||||
key: item.path,
|
||||
ref(node) {
|
||||
@ -182,7 +165,7 @@ const InventoryTargetSelector = () => {
|
||||
active={activeIndex === index}
|
||||
>
|
||||
{item.name}
|
||||
</InventoryItem>
|
||||
</AccountInventorySelectorItem>
|
||||
))}
|
||||
</div>
|
||||
</FloatingFocusManager>
|
||||
@ -191,76 +174,3 @@ const InventoryTargetSelector = () => {
|
||||
</>
|
||||
);
|
||||
}
|
||||
export const Inventory = () => {
|
||||
const {activeTable, columns, tags, orders} = useSessionContext()
|
||||
|
||||
const [ref, {height}] = useResizeObserver({})
|
||||
|
||||
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])
|
||||
|
||||
const Loading = ()=>{
|
||||
return <div role="status" className="flex align-center justify-center">
|
||||
<div className="justify-center py-4">
|
||||
<DotLoader color="#dddddd"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const items = useAtomValue(currentCharacterItemsAtom)
|
||||
|
||||
return <div ref={ref} className={``}>
|
||||
<div className="flex flex-row py-2 justify-end">
|
||||
<InventoryTargetSelector/>
|
||||
<div
|
||||
onClick={(e)=>{
|
||||
// sendOrders()
|
||||
}}
|
||||
className="
|
||||
hover:cursor-pointer
|
||||
border border-black-1
|
||||
bg-green-200
|
||||
px-2 py-1
|
||||
">Move Selected</div>
|
||||
</div>
|
||||
{(isLoading || isFetching) ? <Loading/> : <>
|
||||
<div>
|
||||
total: {items.size}
|
||||
</div>
|
||||
</> }
|
||||
</div>
|
||||
}
|
90
src/components/inventory/table.tsx
Normal file
90
src/components/inventory/table.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import { StatsColumns } from "@/lib/columns"
|
||||
import { ItemWithSelection } from "@/lib/table/defs"
|
||||
import { InventoryColumns } from "@/lib/table/tanstack"
|
||||
import { inventoryItemsCurrentPageAtom, preferenceInventoryTab } from "@/state/atoms"
|
||||
import { flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import { atom, useAtom, useAtomValue } from "jotai"
|
||||
import { useMemo } from "react"
|
||||
|
||||
|
||||
const columnVisibilityAtom = atom((get)=>{
|
||||
const itemTab = get(preferenceInventoryTab)
|
||||
if(!["2","4"].includes(itemTab)) {
|
||||
return Object.fromEntries([
|
||||
...StatsColumns.map(x=>["stats."+x,false]),
|
||||
["slots",false]
|
||||
])
|
||||
}
|
||||
return {
|
||||
}
|
||||
})
|
||||
export const InventoryTable = () => {
|
||||
|
||||
const items = useAtomValue(inventoryItemsCurrentPageAtom)
|
||||
|
||||
const columns = useMemo(()=>{
|
||||
return [
|
||||
...Object.values(InventoryColumns)
|
||||
]
|
||||
}, [])
|
||||
|
||||
const columnVisibility = useAtomValue(columnVisibilityAtom)
|
||||
console.log(columnVisibility)
|
||||
|
||||
const table = useReactTable<ItemWithSelection>({
|
||||
getRowId: row =>row.item.unique_id.toString(),
|
||||
data: items,
|
||||
state: {
|
||||
columnVisibility,
|
||||
},
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto h-full mb-32">
|
||||
<table
|
||||
onContextMenu={(e)=>{
|
||||
e.preventDefault()
|
||||
return
|
||||
}}
|
||||
className="border-spacing-x-2 border-separate">
|
||||
<thead className="sticky top-0 z-10 select-none bg-white">
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<tr
|
||||
className=""
|
||||
key={headerGroup.id}>
|
||||
{headerGroup.headers.map(header => (
|
||||
<th
|
||||
key={header.id}
|
||||
className="text-left"
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{table.getRowModel().rows.map(row => (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={""}
|
||||
>
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<td key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,13 +1,22 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
html {
|
||||
cursor: url(/public/cursor.png), auto !important;
|
||||
}
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@theme {
|
||||
--cursor-default: url(/public/cursor.png), auto !important;
|
||||
--cursor-pointer: url(/public/cursor.png), pointer !important;
|
||||
--cursor-text: url(/public/cursor.png), pointer !important;
|
||||
}
|
||||
/*
|
||||
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
|
@ -4,16 +4,20 @@ import { App } from "./App";
|
||||
import AppContext from "./context/AppContext";
|
||||
|
||||
|
||||
import "./lib/superjson";
|
||||
import "./index.css";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Provider } from "jotai";
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("app") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppContext>
|
||||
<App />
|
||||
</AppContext>
|
||||
</QueryClientProvider>
|
||||
<Provider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppContext>
|
||||
<App />
|
||||
</AppContext>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
@ -21,7 +21,7 @@ export const EquipmentColumns = [
|
||||
] as const
|
||||
|
||||
export const StatsColumns = [
|
||||
"AP","GunAP","AC","DX","MP","MA","MD","WT","DA","LK","HP","DP","HV",
|
||||
"HV","AC","LK","WT","HP","MA","DP","DX","MP","AP","MD","DA","GunAP"
|
||||
] as const
|
||||
|
||||
|
||||
|
@ -73,6 +73,7 @@ export class LTOApiv0 implements LTOApi {
|
||||
galders,
|
||||
items: new Map((Object.entries(o.items) as any).map(([k, v]: [string, TricksterItem]):[string, TricksterItem]=>{
|
||||
v.unique_id = Number(k)
|
||||
v.id = k
|
||||
return [k, v]
|
||||
})),
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ export const StoreAccounts = {
|
||||
}
|
||||
|
||||
export const StoreJsonable = {
|
||||
Murder: <T>(s:T):string=>JSON.stringify(Object.entries(s)),
|
||||
Murder: <T extends object>(s:T):string=>JSON.stringify(Object.entries(s)),
|
||||
Revive: <T>(s:string):T=>JSON.parse(s),
|
||||
}
|
||||
|
||||
|
1
src/lib/superjson.ts
Normal file
1
src/lib/superjson.ts
Normal file
@ -0,0 +1 @@
|
||||
import SuperJSON from "superjson";
|
@ -1,7 +1,5 @@
|
||||
import { TricksterInventory } from "./trickster"
|
||||
import {ColumnInfo, ColumnName, Columns, ColumnSorter, LazyColumn} from "./columns"
|
||||
import { PredefinedMenuItemKey } from "handsontable/plugins/contextMenu"
|
||||
import Handsontable from "handsontable"
|
||||
import { HotTableProps } from "@handsontable/react"
|
||||
|
||||
|
||||
@ -95,25 +93,8 @@ export class InventoryTable {
|
||||
getTableColumnNames(): string[] {
|
||||
return this.o.columns.map(x=>x.displayName)
|
||||
}
|
||||
getTableColumnSettings(): Handsontable.ColumnSettings[] {
|
||||
return this.o.columns.map(x=>{
|
||||
let out:Handsontable.ColumnSettings = {
|
||||
renderer: x.renderer ? x.renderer : "text",
|
||||
filters: true,
|
||||
dropdownMenu: x.filtering ? DefaultDropdownItems() : false,
|
||||
readOnly: x.writable ? false : true,
|
||||
selectionMode: (x.writable ? "multiple" : 'single') as any,
|
||||
}
|
||||
if(x.options) {
|
||||
out.type = 'dropdown'
|
||||
if(typeof x.options == "function") {
|
||||
out.source = x.options(this.o.accounts)
|
||||
}else {
|
||||
out.source = x.options
|
||||
}
|
||||
}
|
||||
return out
|
||||
})
|
||||
getTableColumnSettings(){
|
||||
|
||||
}
|
||||
getTableRows():any[][] {
|
||||
return Object.values(this.inv.items)
|
||||
@ -162,47 +143,3 @@ export interface TableRecipe {
|
||||
settings: HotTableProps
|
||||
}
|
||||
|
||||
export const DefaultDropdownItems = ():PredefinedMenuItemKey[]=>['filter_by_condition' , 'filter_operators' ,'filter_by_condition2' , 'filter_by_value' , 'filter_action_bar']
|
||||
export const DefaultSettings = ():HotTableProps=>{
|
||||
return {
|
||||
trimDropdown: true,
|
||||
filters: true,
|
||||
manualRowMove: false,
|
||||
manualColumnMove: false,
|
||||
allowInsertRow: false,
|
||||
allowInsertColumn: false,
|
||||
allowRemoveRow: false,
|
||||
allowRemoveColumn: false,
|
||||
allowHtml: true,
|
||||
disableVisualSelection: false,
|
||||
columnSorting: {
|
||||
indicator: true,
|
||||
headerAction: true,
|
||||
},
|
||||
hiddenColumns: {
|
||||
columns: [0],
|
||||
},
|
||||
// renderAllRows: true,
|
||||
viewportColumnRenderingOffset: 3,
|
||||
viewportRowRenderingOffset: 10,
|
||||
// dropdownMenu: DefaultDropdownItems(),
|
||||
afterGetColHeader: (col, th) => {
|
||||
if(!th.innerHTML.toLowerCase().includes("name")) {
|
||||
const ct = th.querySelector('.changeType')
|
||||
if(ct){
|
||||
ct.parentElement!.removeChild(ct)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeOnCellMouseDown(event:any, coords) {
|
||||
// Deselect the column after clicking on input.
|
||||
if (coords.row === -1 && event.target.nodeName === 'INPUT') {
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
},
|
||||
className: 'htLeft',
|
||||
contextMenu: false,
|
||||
readOnlyCellClassName: "",
|
||||
licenseKey:"non-commercial-and-evaluation",
|
||||
}
|
||||
}
|
||||
|
11
src/lib/table/defs.ts
Normal file
11
src/lib/table/defs.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { TricksterItem } from "../trickster";
|
||||
|
||||
export interface ItemSelectionStatus {
|
||||
selected: boolean;
|
||||
amount?: number;
|
||||
}
|
||||
|
||||
export interface ItemWithSelection {
|
||||
item: TricksterItem
|
||||
status?: ItemSelectionStatus;
|
||||
}
|
138
src/lib/table/tanstack.tsx
Normal file
138
src/lib/table/tanstack.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
import { ItemWithSelection } from './defs';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { currentItemSelectionAtom, itemSelectionSetActionAtom } from '@/state/atoms';
|
||||
import { useMemo } from 'react';
|
||||
import { StatsColumns } from '../columns';
|
||||
|
||||
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 selected = useMemo(()=> {
|
||||
return c[0].has(row.original.item.id);
|
||||
}, [c])
|
||||
return <div
|
||||
className={`no-select flex flex-row ${ row.original.status?.selected ? "animate-pulse" : ""}`}
|
||||
onClick={(e)=>{
|
||||
setItemSelection({
|
||||
[row.original.item.id]: selected ? undefined : row.original.item.item_count,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<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 select-none"/>
|
||||
</div>
|
||||
</div>
|
||||
},
|
||||
}),
|
||||
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 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
|
||||
return <div
|
||||
className={`flex flex-row select-none ${ row.original.status?.selected ? "bg-gray-200" : ""}`}
|
||||
>
|
||||
<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 (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 }){
|
||||
return <div className="flex flex-row whitespace-pre">
|
||||
<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;
|
@ -1,10 +1,13 @@
|
||||
export interface TricksterItem {
|
||||
id: string;
|
||||
unique_id: number;
|
||||
item_name: string;
|
||||
item_count: number;
|
||||
item_comment: string;
|
||||
item_use: string;
|
||||
item_slots?: number;
|
||||
item_tab: number
|
||||
item_type: number,
|
||||
item_min_level?: number;
|
||||
is_equip?: boolean;
|
||||
is_drill?: boolean;
|
||||
|
12
src/main.ts
12
src/main.ts
@ -1,12 +0,0 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
|
||||
import { createPinia } from 'pinia';
|
||||
import log from 'loglevel';
|
||||
|
||||
log.setLevel("debug")
|
||||
const pinia = createPinia()
|
||||
createApp(App).
|
||||
use(pinia).
|
||||
mount('#app')
|
@ -2,9 +2,13 @@ import { AxiosError } from 'axios';
|
||||
import { LTOApiv0 } from '../lib/lifeto'
|
||||
import { LoginHelper, TokenSession } from '../lib/session'
|
||||
import { atomWithQuery } from 'jotai-tanstack-query'
|
||||
import {atomFamily, atomWithRefresh, atomWithStorage} from "jotai/utils";
|
||||
import {atomWithStorage} from "jotai/utils";
|
||||
import { atom } from 'jotai';
|
||||
import { TricksterCharacter, TricksterInventory, TricksterItem } from '../lib/trickster';
|
||||
import {focusAtom} from "jotai-optics";
|
||||
import { createSuperjsonStorage, superJsonStorage } from './storage';
|
||||
import { ItemWithSelection } from '@/lib/table/defs';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
export const LTOApi = new LTOApiv0(new TokenSession())
|
||||
|
||||
@ -77,10 +81,9 @@ export const charactersAtom = atomWithQuery((get) => {
|
||||
}
|
||||
})
|
||||
|
||||
export const selectedCharacterAtom = atom<TricksterCharacter | undefined>(undefined)
|
||||
export const selectedCharacterAtom = atomWithStorage<TricksterCharacter | undefined>("lto_state.selected_character", undefined)
|
||||
export const selectedTargetInventoryAtom = atom<TricksterCharacter | undefined>(undefined)
|
||||
|
||||
export const pageSize = atomWithStorage("preference.page_size", 250)
|
||||
export const currentFilter = atom<undefined>(undefined)
|
||||
|
||||
|
||||
@ -96,9 +99,221 @@ export const currentCharacterInventoryAtom = atomWithQuery((get) => {
|
||||
}
|
||||
})
|
||||
|
||||
const inventoryDisplaySettings= atomWithStorage<{
|
||||
page_size: number
|
||||
}>("preference.inventory_display_settings", {
|
||||
page_size: 25,
|
||||
}, createSuperjsonStorage())
|
||||
|
||||
export const inventoryDisplaySettingsAtoms = {
|
||||
pageSize: focusAtom(inventoryDisplaySettings, x=>x.prop('page_size')),
|
||||
}
|
||||
|
||||
|
||||
export const currentCharacterItemsAtom = atom((get)=>{
|
||||
const {data: inventory} = get(currentCharacterInventoryAtom)
|
||||
return inventory?.items || new Map<string, TricksterItem>()
|
||||
const items = inventory?.items || new Map<string, TricksterItem>()
|
||||
return {
|
||||
items,
|
||||
searcher: new Fuse(Array.from(items.values()), {
|
||||
keys: [
|
||||
'item_name',
|
||||
],
|
||||
useExtendedSearch: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export interface InventoryFilter {
|
||||
search: string
|
||||
tab: string
|
||||
sort: string
|
||||
sort_reverse: boolean
|
||||
}
|
||||
|
||||
export const inventoryFilterAtom = atomWithStorage<InventoryFilter>("preference.inventory_filter", {
|
||||
search: "",
|
||||
tab: "",
|
||||
sort: "",
|
||||
sort_reverse:false,
|
||||
}, createSuperjsonStorage())
|
||||
|
||||
export const preferenceInventorySearch = focusAtom(inventoryFilterAtom, x=>x.prop('search'))
|
||||
export const preferenceInventoryTab = focusAtom(inventoryFilterAtom, x=>x.prop('tab'))
|
||||
export const preferenceInventorySort = focusAtom(inventoryFilterAtom, x=>x.prop('sort'))
|
||||
export const preferenceInventorySortReverse = focusAtom(inventoryFilterAtom, x=>x.prop('sort_reverse'))
|
||||
|
||||
|
||||
|
||||
|
||||
export const setInventoryFilterTabActionAtom = atom(null, (_get, set, tab: string) => {
|
||||
set(inventoryFilterAtom, x=>{
|
||||
return {
|
||||
...x,
|
||||
tab,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export const inventoryPageRangeAtom = atom({
|
||||
start: 0,
|
||||
end: 25,
|
||||
})
|
||||
|
||||
export const nextInventoryPageActionAtom = atom(null, (get, set) => {
|
||||
const {start, end} = get(inventoryPageRangeAtom)
|
||||
set(inventoryPageRangeAtom, {
|
||||
start: start + end,
|
||||
end: end + end,
|
||||
})
|
||||
})
|
||||
|
||||
export const currentItemSelectionAtom = atom<[Map<string, number>, number]>([new Map<string, number>(), 0])
|
||||
export const currentInventorySearchQueryAtom = atom("")
|
||||
|
||||
export const filteredCharacterItemsAtom = atom((get)=>{
|
||||
const { items, searcher } = get(currentCharacterItemsAtom)
|
||||
const [selection] = get(currentItemSelectionAtom)
|
||||
const filter = get(inventoryFilterAtom)
|
||||
const out: ItemWithSelection[] = []
|
||||
for (const [_, value] of items.entries()) {
|
||||
if(filter.search !== "") {
|
||||
if(!value.item_name.toLowerCase().includes(filter.search)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if(filter.tab !== "") {
|
||||
if(value.item_tab !== parseInt(filter.tab)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
let status = undefined
|
||||
if(selection.has(value.id)) {
|
||||
status = {
|
||||
selected: true,
|
||||
}
|
||||
}
|
||||
out.push({item: value, status})
|
||||
}
|
||||
|
||||
switch(filter.sort) {
|
||||
case "count":
|
||||
out.sort((a, b)=>{
|
||||
return b.item.item_count - a.item.item_count
|
||||
})
|
||||
break;
|
||||
case "type":
|
||||
out.sort((a, b)=>{
|
||||
return a.item.item_tab - b.item.item_tab
|
||||
})
|
||||
break;
|
||||
case "name":
|
||||
out.sort((a, b)=>{
|
||||
return a.item.item_name.localeCompare(b.item.item_name)
|
||||
})
|
||||
break;
|
||||
}
|
||||
if(filter.sort && filter.sort_reverse) {
|
||||
out.reverse()
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
export const inventoryItemsCurrentPageAtom = atom((get)=>{
|
||||
const items = get(filteredCharacterItemsAtom)
|
||||
const {start, end} = get(inventoryPageRangeAtom)
|
||||
return items.slice(start, end).map((item): ItemWithSelection =>{
|
||||
return item
|
||||
})
|
||||
})
|
||||
|
||||
export const rowSelectionLastActionAtom = atom<{
|
||||
index: number
|
||||
action: "add" | "remove"
|
||||
}| undefined>(undefined)
|
||||
|
||||
|
||||
export const clearItemSelectionActionAtom = atom(null, (_get, set) => {
|
||||
set(currentItemSelectionAtom, [new Map<string,number>(), 0])
|
||||
})
|
||||
|
||||
export const itemSelectionSetActionAtom = atom(null, (get, set, arg: Record<string,number | undefined> ) => {
|
||||
const cur = get(currentItemSelectionAtom)
|
||||
for(const [key, value] of Object.entries(arg)) {
|
||||
if(value === undefined) {
|
||||
cur[0].delete(key)
|
||||
} else {
|
||||
cur[0].set(key, value)
|
||||
}
|
||||
}
|
||||
set(currentItemSelectionAtom, [cur[0], cur[1] + 1])
|
||||
})
|
||||
|
||||
export const itemSelectionSelectAllFilterActionAtom = atom(null, (get, set) => {
|
||||
const cur = get(currentItemSelectionAtom)
|
||||
const items = get(filteredCharacterItemsAtom)
|
||||
for(const item of items) {
|
||||
cur[0].set(item.item.id, item.item.item_count)
|
||||
}
|
||||
set(currentItemSelectionAtom, [cur[0], cur[1] + 1])
|
||||
})
|
||||
|
||||
export const itemSelectionSelectAllPageActionAtom = atom(null, (get, set) => {
|
||||
const cur = get(currentItemSelectionAtom)
|
||||
const items = get(inventoryItemsCurrentPageAtom)
|
||||
for(const item of items) {
|
||||
cur[0].set(item.item.id, item.item.item_count)
|
||||
}
|
||||
set(currentItemSelectionAtom, [cur[0], cur[1] + 1])
|
||||
})
|
||||
|
||||
export const paginateInventoryActionAtom = atom(null, (get, set, pages: number | undefined) => {
|
||||
const inventoryRange = get(inventoryPageRangeAtom)
|
||||
const pageSize = get(inventoryDisplaySettingsAtoms.pageSize)
|
||||
const filteredItems = get(filteredCharacterItemsAtom)
|
||||
if(pages === undefined) {
|
||||
set(inventoryPageRangeAtom, {
|
||||
start: 0,
|
||||
end: pageSize,
|
||||
})
|
||||
return
|
||||
}
|
||||
if(pageSize > filteredItems.length) {
|
||||
set(inventoryPageRangeAtom, {
|
||||
start: 0,
|
||||
end: filteredItems.length,
|
||||
})
|
||||
return
|
||||
}
|
||||
if(pages > 0) {
|
||||
if(inventoryRange.end >= filteredItems.length) {
|
||||
set(inventoryPageRangeAtom, {
|
||||
start: 0,
|
||||
end: pageSize,
|
||||
})
|
||||
return
|
||||
}
|
||||
}else if(pages < 0) {
|
||||
if(inventoryRange.start <= 0) {
|
||||
set(inventoryPageRangeAtom, {
|
||||
start: filteredItems.length - pageSize,
|
||||
end: filteredItems.length,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
const delta = pages * pageSize
|
||||
let newStart = inventoryRange.start + delta
|
||||
let newEnd = inventoryRange.end + delta
|
||||
if(newEnd > filteredItems.length) {
|
||||
newEnd = filteredItems.length
|
||||
}
|
||||
if(newEnd - newStart != pageSize) {
|
||||
newStart = newEnd - pageSize
|
||||
}
|
||||
|
||||
set(inventoryPageRangeAtom, {
|
||||
start: newStart,
|
||||
end: newEnd,
|
||||
})
|
||||
})
|
||||
|
116
src/state/storage.ts
Normal file
116
src/state/storage.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { AsyncStorage, AsyncStringStorage, SyncStorage, SyncStringStorage } from "jotai/vanilla/utils/atomWithStorage"
|
||||
import superjson from 'superjson'
|
||||
|
||||
const isPromiseLike = (x: unknown): x is PromiseLike<unknown> =>
|
||||
typeof (x as any)?.then === 'function'
|
||||
|
||||
type Unsubscribe = () => void
|
||||
|
||||
type Subscribe<Value> = (
|
||||
key: string,
|
||||
callback: (value: Value) => void,
|
||||
initialValue: Value,
|
||||
) => Unsubscribe | undefined
|
||||
|
||||
type StringSubscribe = (
|
||||
key: string,
|
||||
callback: (value: string | null) => void,
|
||||
) => Unsubscribe | undefined
|
||||
|
||||
export function createSuperjsonStorage<Value>(): SyncStorage<Value>
|
||||
export function createSuperjsonStorage<Value>(
|
||||
getStringStorage: () =>
|
||||
| AsyncStringStorage
|
||||
| SyncStringStorage
|
||||
| undefined = () => {
|
||||
try {
|
||||
return window.localStorage
|
||||
} catch (e) {
|
||||
if (import.meta.env?.MODE !== 'production') {
|
||||
if (typeof window !== 'undefined') {
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
): AsyncStorage<Value> | SyncStorage<Value> {
|
||||
let lastStr: string | undefined
|
||||
let lastValue: Value
|
||||
|
||||
const storage: AsyncStorage<Value> | SyncStorage<Value> = {
|
||||
getItem: (key, initialValue) => {
|
||||
const parse = (str: string | null) => {
|
||||
str = str || ''
|
||||
if (lastStr !== str) {
|
||||
try {
|
||||
lastValue = superjson.parse(str)
|
||||
} catch {
|
||||
return initialValue
|
||||
}
|
||||
lastStr = str
|
||||
}
|
||||
return lastValue
|
||||
}
|
||||
const str = getStringStorage()?.getItem(key) ?? null
|
||||
if (isPromiseLike(str)) {
|
||||
return str.then(parse) as never
|
||||
}
|
||||
return parse(str) as never
|
||||
},
|
||||
setItem: (key, newValue) =>
|
||||
getStringStorage()?.setItem(
|
||||
key,
|
||||
superjson.stringify(newValue),
|
||||
),
|
||||
removeItem: (key) => getStringStorage()?.removeItem(key),
|
||||
}
|
||||
|
||||
const createHandleSubscribe =
|
||||
(subscriber: StringSubscribe): Subscribe<Value> =>
|
||||
(key, callback, initialValue) =>
|
||||
subscriber(key, (v) => {
|
||||
let newValue: Value
|
||||
try {
|
||||
newValue = superjson.parse(v || '')
|
||||
} catch {
|
||||
newValue = initialValue
|
||||
}
|
||||
callback(newValue)
|
||||
})
|
||||
|
||||
let subscriber: StringSubscribe | undefined
|
||||
try {
|
||||
subscriber = getStringStorage()?.subscribe
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (
|
||||
!subscriber &&
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.addEventListener === 'function' &&
|
||||
window.Storage
|
||||
) {
|
||||
subscriber = (key, callback) => {
|
||||
if (!(getStringStorage() instanceof window.Storage)) {
|
||||
return () => {}
|
||||
}
|
||||
const storageEventCallback = (e: StorageEvent) => {
|
||||
if (e.storageArea === getStringStorage() && e.key === key) {
|
||||
callback(e.newValue)
|
||||
}
|
||||
}
|
||||
window.addEventListener('storage', storageEventCallback)
|
||||
return () => {
|
||||
window.removeEventListener('storage', storageEventCallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subscriber) {
|
||||
storage.subscribe = createHandleSubscribe(subscriber)
|
||||
}
|
||||
return storage
|
||||
}
|
||||
|
||||
export const superJsonStorage = createSuperjsonStorage()
|
@ -4,6 +4,9 @@
|
||||
"target": "esnext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"types": [ "node" ],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
@ -1,9 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
// with options
|
||||
|
59
yarn.lock
59
yarn.lock
@ -15,6 +15,18 @@
|
||||
"@jridgewell/gen-mapping" "^0.3.5"
|
||||
"@jridgewell/trace-mapping" "^0.3.24"
|
||||
|
||||
"@ark/schema@0.46.0":
|
||||
version "0.46.0"
|
||||
resolved "https://registry.yarnpkg.com/@ark/schema/-/schema-0.46.0.tgz#81a1a0dc1ff0f2faa098cba05de505a174bdc64e"
|
||||
integrity sha512-c2UQdKgP2eqqDArfBqQIJppxJHvNNXuQPeuSPlDML4rjw+f1cu0qAlzOG4b8ujgm9ctIDWwhpyw6gjG5ledIVQ==
|
||||
dependencies:
|
||||
"@ark/util" "0.46.0"
|
||||
|
||||
"@ark/util@0.46.0":
|
||||
version "0.46.0"
|
||||
resolved "https://registry.yarnpkg.com/@ark/util/-/util-0.46.0.tgz#aee240bdaf413793e5ca4c4e8e3707aa965f4be3"
|
||||
integrity sha512-JPy/NGWn/lvf1WmGCPw2VGpBg5utZraE84I7wli18EDF3p3zc/e9WolT35tINeZO3l7C77SjqRJeAUoT0CvMRg==
|
||||
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.24.7":
|
||||
version "7.24.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465"
|
||||
@ -1989,6 +2001,18 @@
|
||||
dependencies:
|
||||
"@tanstack/query-core" "5.76.0"
|
||||
|
||||
"@tanstack/react-table@^8.21.3":
|
||||
version "8.21.3"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.21.3.tgz#2c38c747a5731c1a07174fda764b9c2b1fb5e91b"
|
||||
integrity sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==
|
||||
dependencies:
|
||||
"@tanstack/table-core" "8.21.3"
|
||||
|
||||
"@tanstack/table-core@8.21.3":
|
||||
version "8.21.3"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c"
|
||||
integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==
|
||||
|
||||
"@tybys/wasm-util@^0.9.0":
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.9.0.tgz#3e75eb00604c8d6db470bf18c37b7d984a0e3355"
|
||||
@ -2044,6 +2068,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||
|
||||
"@types/node@^22.15.18":
|
||||
version "22.15.18"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.18.tgz#2f8240f7e932f571c2d45f555ba0b6c3f7a75963"
|
||||
integrity sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==
|
||||
dependencies:
|
||||
undici-types "~6.21.0"
|
||||
|
||||
"@types/parse-json@^4.0.0":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239"
|
||||
@ -2355,6 +2386,14 @@ aria-query@~5.1.3:
|
||||
dependencies:
|
||||
deep-equal "^2.0.5"
|
||||
|
||||
arktype@^2.1.20:
|
||||
version "2.1.20"
|
||||
resolved "https://registry.yarnpkg.com/arktype/-/arktype-2.1.20.tgz#dd46726b0faf23c2753369876c77bb037e7089d9"
|
||||
integrity sha512-IZCEEXaJ8g+Ijd59WtSYwtjnqXiwM8sWQ5EjGamcto7+HVN9eK0C4p0zDlCuAwWhpqr6fIBkxPuYDl4/Mcj/+Q==
|
||||
dependencies:
|
||||
"@ark/schema" "0.46.0"
|
||||
"@ark/util" "0.46.0"
|
||||
|
||||
array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f"
|
||||
@ -4554,6 +4593,11 @@ jiti@^2.4.2:
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.2.tgz#d19b7732ebb6116b06e2038da74a55366faef560"
|
||||
integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==
|
||||
|
||||
jotai-optics@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/jotai-optics/-/jotai-optics-0.4.0.tgz#aad207090a06390e61cc85324b623b2692e2e272"
|
||||
integrity sha512-osbEt9AgS55hC4YTZDew2urXKZkaiLmLqkTS/wfW5/l0ib8bmmQ7kBXSFaosV6jDDWSp00IipITcJARFHdp42g==
|
||||
|
||||
jotai-tanstack-query@^0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/jotai-tanstack-query/-/jotai-tanstack-query-0.9.0.tgz#f3c4f96c2ec88f6fdd0b39a1d2eca6a10a8878d6"
|
||||
@ -5065,6 +5109,11 @@ once@^1.4.0:
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
optics-ts@^2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/optics-ts/-/optics-ts-2.4.1.tgz#de94bda2b0ed7fde5b7631283031b9699459d40d"
|
||||
integrity sha512-HaYzMHvC80r7U/LqAd4hQyopDezC60PO2qF5GuIwALut2cl5rK1VWHsqTp0oqoJJWjiv6uXKqsO+Q2OO0C3MmQ==
|
||||
|
||||
optionator@^0.9.3:
|
||||
version "0.9.4"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734"
|
||||
@ -5274,6 +5323,11 @@ react-dom@^19.1.0:
|
||||
dependencies:
|
||||
scheduler "^0.26.0"
|
||||
|
||||
react-icons@^5.5.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.5.0.tgz#8aa25d3543ff84231685d3331164c00299cdfaf2"
|
||||
integrity sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==
|
||||
|
||||
react-is@^16.13.1, react-is@^16.7.0:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
@ -6108,6 +6162,11 @@ unbox-primitive@^1.1.0:
|
||||
has-symbols "^1.1.0"
|
||||
which-boxed-primitive "^1.1.1"
|
||||
|
||||
undici-types@~6.21.0:
|
||||
version "6.21.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
|
||||
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
|
||||
|
||||
unicode-canonical-property-names-ecmascript@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"
|
||||
|
Loading…
Reference in New Issue
Block a user