forked from a/lifeto-shop
noot
This commit is contained in:
parent
e39ba4d052
commit
6e99ef1501
|
@ -0,0 +1,54 @@
|
||||||
|
module.exports = {
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
'plugin:react/jsx-runtime',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs', '**/vendor/**', '**/locales/**', 'generated.*'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: true,
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: ['react-refresh', 'react'],
|
||||||
|
rules: {
|
||||||
|
'no-extra-semi': 'off', // this one needs to stay off
|
||||||
|
'@react/no-children-prop': 'off', // tanstack form uses this as a pattern
|
||||||
|
'react/no-children-prop': 'off', // tanstack form uses this as a pattern
|
||||||
|
'no-duplicate-imports': 'warn',
|
||||||
|
'@typescript-eslint/no-extra-semi': 'off',
|
||||||
|
'sort-imports': 'off',
|
||||||
|
'react-hooks/exhaustive-deps': 'off',
|
||||||
|
'react-refresh/only-export-components': 'off',
|
||||||
|
'no-case-declarations': 'off',
|
||||||
|
'no-redeclare': 'off',
|
||||||
|
'no-undef': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/strict-boolean-expressions': ['error', {
|
||||||
|
"allowString": true,
|
||||||
|
"allowNumber": true,
|
||||||
|
"allowNullableObject": true,
|
||||||
|
"allowNullableBoolean": true,
|
||||||
|
"allowNullableString": true,
|
||||||
|
"allowNullableNumber": false,
|
||||||
|
"allowNullableEnum": true,
|
||||||
|
"allowAny": true
|
||||||
|
}],
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'no-lonely-if': 2,
|
||||||
|
'no-console': 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>lto inventory</title>
|
<title>lto inventory</title>
|
||||||
</head>
|
</head>
|
||||||
<body style="overflow-y: hidden;">
|
<body style="overflow-y: hidden;" class="w-screen h-screen">
|
||||||
<div id="app"></div>
|
<div id="app" class="w-full h-full"></div>
|
||||||
<script type="module" src="/src/index.tsx"></script>
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
22
package.json
22
package.json
|
@ -4,26 +4,38 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@handsontable/vue3": "^12.0.1",
|
"@handsontable/react": "^14.5.0",
|
||||||
|
"@tanstack/react-query": "^5.51.21",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||||
|
"@typescript-eslint/parser": "^8.0.1",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"@vueuse/core": "^8.7.5",
|
"@vueuse/core": "^8.7.5",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"handsontable": "^12.0.1",
|
"eslint": "^9.8.0",
|
||||||
|
"eslint-config-react-app": "^7.0.1",
|
||||||
|
"eslint-plugin-react": "^7.35.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.9",
|
||||||
|
"handsontable": "^14.5.0",
|
||||||
"loglevel": "^1.8.0",
|
"loglevel": "^1.8.0",
|
||||||
"pinia": "^2.0.14",
|
"pinia": "^2.0.14",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
"qs": "^6.10.5",
|
"qs": "^6.10.5",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"typescript-cookie": "^1.0.4",
|
"react-spinners": "^0.14.1",
|
||||||
"uuid": "^8.3.2",
|
"typescript-cookie": "^1.0.6",
|
||||||
|
"use-local-storage": "^3.0.0",
|
||||||
|
"usehooks-ts": "^3.1.0",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
"vue": "^3.2.25"
|
"vue": "^3.2.25"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 52 KiB |
Binary file not shown.
After Width: | Height: | Size: 593 B |
36
src/App.tsx
36
src/App.tsx
|
@ -1,10 +1,30 @@
|
||||||
import { FC } from "react"
|
import { FC } from "react";
|
||||||
|
import { LoginWidget } from "./components/login";
|
||||||
|
import { CharacterRoulette } from "./components/characters";
|
||||||
|
import { Inventory } from "./components/inventory";
|
||||||
|
|
||||||
export const App: FC = () => {
|
export const App: FC = () => {
|
||||||
return <>
|
return (
|
||||||
<div>
|
<>
|
||||||
</div>
|
<div className="flex flex-col p-4 h-full">
|
||||||
</>
|
<div className="grid grid-cols-6 gap-x-4">
|
||||||
}
|
<div className="col-span-1">
|
||||||
|
<LoginWidget/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-5 h-full">
|
||||||
|
<CharacterRoulette/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-6 h-full">
|
||||||
|
<div className="col-span-1">
|
||||||
|
</div>
|
||||||
|
<div className="col-span-5 h-full">
|
||||||
|
<div className="overflow-hidden h-5/6">
|
||||||
|
<Inventory/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { useLtoContext } from "../context/LtoContext"
|
||||||
|
import { JobNumberToString, TricksterAccount, TricksterCharacter } from "../lib/trickster"
|
||||||
|
import { keepPreviousData, useQuery } from "@tanstack/react-query"
|
||||||
|
import { useSessionContext } from "../context/SessionContext"
|
||||||
|
|
||||||
|
export const CharacterCard = ({character}:{
|
||||||
|
character: TricksterCharacter,
|
||||||
|
})=>{
|
||||||
|
|
||||||
|
const {activeTable, setActiveTable} = useSessionContext()
|
||||||
|
return <>
|
||||||
|
<div onClick={()=>{
|
||||||
|
setActiveTable(character.path)
|
||||||
|
}}
|
||||||
|
className={`
|
||||||
|
flex flex-col border border-black
|
||||||
|
hover:cursor-pointer
|
||||||
|
hover:bg-blue-100
|
||||||
|
h-full
|
||||||
|
p-2 ${character.path === activeTable ? `bg-blue-200 hover:bg-blue-100 border-double border-4` : ""}`}>
|
||||||
|
<div className="flex"></div>
|
||||||
|
<div className="flex flex-col justify-between h-full">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex flex-row justify-center text-md">
|
||||||
|
<span>{character.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-center">
|
||||||
|
{character.base_job === -8 ?
|
||||||
|
<img src={`https://knowledge.lifeto.co/animations/npc/npc041_5.png`}/>
|
||||||
|
:
|
||||||
|
<img
|
||||||
|
src={`https://knowledge.lifeto.co/animations/character/chr00${character.base_job}_13.png`}/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-1">
|
||||||
|
<span>class: </span>
|
||||||
|
<span>{JobNumberToString(character.current_job)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-1 text-xs">
|
||||||
|
<span>path: </span>
|
||||||
|
<span>{character.path}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const PleaseLogin = () => {
|
||||||
|
return <><div className="align-center">no characters (not logged in?)</div></>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CharacterRoulette = ()=>{
|
||||||
|
const {API, loggedIn} = useLtoContext()
|
||||||
|
const {data:characters} = useQuery({
|
||||||
|
queryKey:["characters", API.s.user],
|
||||||
|
queryFn: async ()=> {
|
||||||
|
return API.GetAccounts().then(x=>{
|
||||||
|
if(!x) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return x.flatMap(x=>{return x?.characters})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
enabled: loggedIn,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if(!characters || characters.length == 0) {
|
||||||
|
return <PleaseLogin/>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className="flex flex-row overflow-x-scroll gap-4 h-full">
|
||||||
|
{characters.map(x=>{
|
||||||
|
return <CharacterCard key={x.id} character={x} />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
import { keepPreviousData, useQuery } from "@tanstack/react-query"
|
||||||
|
import { TricksterCharacter } from "../lib/trickster"
|
||||||
|
import { useSessionContext } from "../context/SessionContext"
|
||||||
|
import { useLtoContext } from "../context/LtoContext"
|
||||||
|
|
||||||
|
import 'handsontable/dist/handsontable.full.min.css';
|
||||||
|
|
||||||
|
import { registerAllModules } from 'handsontable/registry';
|
||||||
|
import { HotTable, HotTableClass } from '@handsontable/react';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { InventoryTable } from "../lib/table";
|
||||||
|
import { DotLoader } from "react-spinners";
|
||||||
|
import { useDebounceCallback, useResizeObserver } from "usehooks-ts";
|
||||||
|
import { Columns } from "../lib/columns";
|
||||||
|
import { OrderDetails, OrderSender } from "../lib/lifeto/order_manager";
|
||||||
|
import log from "loglevel";
|
||||||
|
|
||||||
|
registerAllModules();
|
||||||
|
type Size = {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
}
|
||||||
|
export const Inventory = () => {
|
||||||
|
const {activeTable, columns, tags, orders} = useSessionContext()
|
||||||
|
const {API, loggedIn} = useLtoContext()
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
const [{ height }, setSize] = useState<Size>({
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onResize = useDebounceCallback(setSize, 200)
|
||||||
|
|
||||||
|
useResizeObserver({
|
||||||
|
ref,
|
||||||
|
onResize,
|
||||||
|
})
|
||||||
|
|
||||||
|
const hotTableComponent = useRef<HotTableClass>(null);
|
||||||
|
const {data:character, isLoading, isFetching } = useQuery({
|
||||||
|
queryKey:["inventory", activeTable],
|
||||||
|
queryFn: async ()=> {
|
||||||
|
return API.GetInventory(activeTable)
|
||||||
|
},
|
||||||
|
enabled: loggedIn,
|
||||||
|
// placeholderData: keepPreviousData,
|
||||||
|
})
|
||||||
|
const {data:characters} = useQuery({
|
||||||
|
queryKey:["characters", API.s.user],
|
||||||
|
queryFn: async ()=> {
|
||||||
|
return API.GetAccounts().then(x=>{
|
||||||
|
return x.flatMap(x=>{return x.characters})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
enabled: loggedIn,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
if(!character) {
|
||||||
|
hotTableComponent.current?.hotInstance?.updateSettings({
|
||||||
|
data: [],
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const it = new InventoryTable(character, {
|
||||||
|
columns: columns,
|
||||||
|
tags: tags,
|
||||||
|
accounts: characters?.map(x=>{
|
||||||
|
return x.name
|
||||||
|
}) || [],
|
||||||
|
})
|
||||||
|
const build = it.BuildTable()
|
||||||
|
hotTableComponent.current?.hotInstance?.updateSettings(build.settings)
|
||||||
|
}, [hotTableComponent, character, height])
|
||||||
|
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
return <div ref={ref} className={``}>
|
||||||
|
<div className="flex flex-row py-2 px-3">
|
||||||
|
<div
|
||||||
|
onClick={(e)=>{
|
||||||
|
sendOrders()
|
||||||
|
}}
|
||||||
|
className="
|
||||||
|
hover:cursor-pointer
|
||||||
|
border border-black-1
|
||||||
|
bg-green-200
|
||||||
|
px-2 py-1
|
||||||
|
">ayy lmao button</div>
|
||||||
|
</div>
|
||||||
|
{(isLoading || isFetching) ? <Loading/> : <></> }
|
||||||
|
<div
|
||||||
|
className={`${isLoading || isFetching ? "invisible" : ""}`}>
|
||||||
|
<HotTable
|
||||||
|
ref={hotTableComponent as any}
|
||||||
|
licenseKey="non-commercial-and-evaluation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { useLtoContext } from "../context/LtoContext"
|
||||||
|
import useLocalStorage from "use-local-storage"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const LoginWidget = () => {
|
||||||
|
const {loggedIn, login, logout} = useLtoContext()
|
||||||
|
|
||||||
|
const [username, setUsername] = useLocalStorage("input_username","", {syncData: false})
|
||||||
|
const [password, setPassword] = useState("")
|
||||||
|
return <>
|
||||||
|
<div className="flex flex-col border border-gray-400">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row bg-blue-400">
|
||||||
|
<span className="text-white pb-1 pl-2 m-y-1">
|
||||||
|
account
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row flex-wrap gap-1 p-2 justify-center">
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
onChange={(e)=>{
|
||||||
|
setUsername(e.target.value)
|
||||||
|
}}
|
||||||
|
value={username}
|
||||||
|
placeholder="username" className="w-32 pl-2 pb-1 border border-gray-600"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
onChange={(e)=>{
|
||||||
|
setPassword(e.target.value)
|
||||||
|
}}
|
||||||
|
value={password}
|
||||||
|
type="password" placeholder="password" className="w-32 pl-2 pb-1 border border-gray-600"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row p-2 justify-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={async ()=>{
|
||||||
|
login(username,password).catch((e)=>{
|
||||||
|
alert(e.toString())
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className="border border-gray-600 px-2 py-1 hover:bg-blue-200">
|
||||||
|
login
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={()=>{
|
||||||
|
logout()
|
||||||
|
return
|
||||||
|
}}
|
||||||
|
disabled={!loggedIn} className="border border-gray-600 px-2 py-1 hover:bg-red-200 disabled:border-gray-400 disabled:text-gray-300 disabled:bg-white ">
|
||||||
|
logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { LtoContextProvider } from "./LtoContext";
|
||||||
|
import { SessionContextProvider } from "./SessionContext";
|
||||||
|
|
||||||
|
interface IContext {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppContext(props: IContext): any {
|
||||||
|
const { children } = props;
|
||||||
|
const providers = [
|
||||||
|
SessionContextProvider,
|
||||||
|
LtoContextProvider
|
||||||
|
];
|
||||||
|
const res = providers.reduceRight(
|
||||||
|
(acc, CurrVal) => <CurrVal>{acc as any}</CurrVal>,
|
||||||
|
children,
|
||||||
|
);
|
||||||
|
return res as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppContext;
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
import { LTOApiv0 } from "../lib/lifeto";
|
||||||
|
import { storage } from "../session_storage";
|
||||||
|
import { LoginHelper, LogoutHelper } from "../lib/session";
|
||||||
|
|
||||||
|
interface LtoContextProps {
|
||||||
|
API: LTOApiv0;
|
||||||
|
login: (username:string, password:string)=>Promise<void>;
|
||||||
|
logout: ()=>void;
|
||||||
|
loggedIn: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const LtoContext = createContext({} as LtoContextProps);
|
||||||
|
|
||||||
|
export const LtoContextProvider = ({ children }: { children: any }) => {
|
||||||
|
const [API, setAPI] = useState(new LTOApiv0(storage.GetSession()));
|
||||||
|
const login = async (username:string , password:string ) =>{
|
||||||
|
console.log("attempting logiun", username)
|
||||||
|
return new LoginHelper(username, password).login()
|
||||||
|
.catch((e)=>{
|
||||||
|
if(e.code == "ERR_BAD_REQUEST") {
|
||||||
|
throw "invalid username/password"
|
||||||
|
}
|
||||||
|
console.warn("throwing error", e)
|
||||||
|
throw "unknown error, please report"
|
||||||
|
})
|
||||||
|
.then((session)=>{
|
||||||
|
setAPI(new LTOApiv0(session))
|
||||||
|
storage.AddSession(session)
|
||||||
|
setLoggedIn(true)
|
||||||
|
}) }
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
new LogoutHelper().logout().then(()=>{
|
||||||
|
storage.RemoveSession()
|
||||||
|
localStorage.clear()
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const [loggedIn, setLoggedIn] = useState(false)
|
||||||
|
useEffect(()=>{
|
||||||
|
if(!API) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
API?.GetLoggedin().then((x)=>{
|
||||||
|
setLoggedIn(x)
|
||||||
|
})
|
||||||
|
}, [API])
|
||||||
|
|
||||||
|
return <LtoContext.Provider value={{
|
||||||
|
API,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
loggedIn
|
||||||
|
}}>{children}</LtoContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLtoContext = (): LtoContextProps => {
|
||||||
|
const context = useContext<LtoContextProps>(LtoContext);
|
||||||
|
if (context === null) {
|
||||||
|
throw new Error(
|
||||||
|
'"useLtoContext" should be used inside a "LtoContextProvider"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { createContext, Dispatch, SetStateAction, useContext, useState } from "react";
|
||||||
|
import { LTOApiv0 } from "../lib/lifeto";
|
||||||
|
|
||||||
|
type Setter<T> = React.Dispatch<React.SetStateAction<T | undefined>>;
|
||||||
|
type MustSetter<T> = React.Dispatch<React.SetStateAction<T>>;
|
||||||
|
import useLocalStorage from "use-local-storage";
|
||||||
|
import { OrderTracker } from "../lib/lifeto/order_manager";
|
||||||
|
import { ColumnSet } from "../lib/table";
|
||||||
|
import { StoreAccounts, StoreChars, StoreColSet, StoreStr } from "../lib/storage";
|
||||||
|
import { BasicColumns, ColumnInfo, ColumnName, Columns, DetailsColumns, MoveColumns } from '../lib/columns'
|
||||||
|
|
||||||
|
interface SessionContextProps {
|
||||||
|
orders: OrderTracker;
|
||||||
|
activeTable: string;
|
||||||
|
screen: string;
|
||||||
|
columns: ColumnSet;
|
||||||
|
tags: ColumnSet;
|
||||||
|
dirty: number;
|
||||||
|
currentSearch: string;
|
||||||
|
|
||||||
|
setActiveTable: Setter<string>
|
||||||
|
setScreen: Setter<string>
|
||||||
|
setDirty: MustSetter<number>
|
||||||
|
setCurrentSearch: MustSetter<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const _defaultColumn:(ColumnInfo| ColumnName)[] = [
|
||||||
|
...BasicColumns,
|
||||||
|
...MoveColumns,
|
||||||
|
...DetailsColumns,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
const SessionContext = createContext({} as SessionContextProps);
|
||||||
|
|
||||||
|
const dotry = (x:any, d: any)=>{
|
||||||
|
try{
|
||||||
|
return x()
|
||||||
|
}catch{
|
||||||
|
return d
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SessionContextProvider = ({ children }: { children: any }) => {
|
||||||
|
const [activeTable, setActiveTable] = useLocalStorage<string>("activeTable","")
|
||||||
|
const [screen, setScreen] = useLocalStorage<string>("screen","")
|
||||||
|
const [columns ] = useState<ColumnSet>(new ColumnSet(_defaultColumn));
|
||||||
|
const [tags ] = useState<ColumnSet>(dotry(()=>StoreColSet.Revive("tags"), new ColumnSet()));
|
||||||
|
|
||||||
|
const [orders ] = useState<OrderTracker>(new OrderTracker());
|
||||||
|
const [dirty, setDirty] = useState<number>(0);
|
||||||
|
const [currentSearch, setCurrentSearch] = useState<string>("");
|
||||||
|
return (
|
||||||
|
<SessionContext.Provider value={{
|
||||||
|
orders, activeTable, screen, columns, tags, dirty, currentSearch,
|
||||||
|
setActiveTable, setScreen, setDirty, setCurrentSearch,
|
||||||
|
}}>{children}</SessionContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSessionContext = (): SessionContextProps => {
|
||||||
|
const context = useContext<SessionContextProps>(SessionContext);
|
||||||
|
if (context === null) {
|
||||||
|
throw new Error(
|
||||||
|
'"useSessionContext" should be used inside a "SessionContextProvider"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
|
@ -1,10 +1,19 @@
|
||||||
import "./index.css"
|
import React from "react";
|
||||||
import React from 'react'
|
import ReactDOM from "react-dom/client";
|
||||||
import ReactDOM from 'react-dom/client'
|
import { App } from "./App";
|
||||||
import {App} from './App'
|
import AppContext from "./context/AppContext";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
|
||||||
|
import "./index.css";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("app") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App/>
|
<QueryClientProvider client={queryClient}>
|
||||||
</React.StrictMode>
|
<AppContext>
|
||||||
)
|
<App />
|
||||||
|
</AppContext>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
|
|
|
@ -64,7 +64,9 @@ export class LTOApiv0 implements LTOApi {
|
||||||
id = Number(char)
|
id = Number(char)
|
||||||
galders = val.galders
|
galders = val.galders
|
||||||
}
|
}
|
||||||
let out = {
|
let out:TricksterInventory = {
|
||||||
|
account_name: o.account.account_gid,
|
||||||
|
account_id: o.account.account_code,
|
||||||
name,
|
name,
|
||||||
id,
|
id,
|
||||||
path: char_path,
|
path: char_path,
|
||||||
|
@ -73,28 +75,30 @@ export class LTOApiv0 implements LTOApi {
|
||||||
v.unique_id = Number(k)
|
v.unique_id = Number(k)
|
||||||
return [k, v]
|
return [k, v]
|
||||||
})),
|
})),
|
||||||
} as TricksterInventory
|
}
|
||||||
return out
|
return out
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
GetAccounts = async ():Promise<TricksterAccount[]> => {
|
GetAccounts = async ():Promise<TricksterAccount[]> => {
|
||||||
return this.s.request("GET", "characters/list",undefined).then((ans:AxiosResponse)=>{
|
return this.s.request("GET", "characters/list",undefined).then((ans:AxiosResponse)=>{
|
||||||
log.debug("GetAccounts", ans.data)
|
log.debug("GetAccounts", ans.data)
|
||||||
return ans.data.map((x:any)=>{
|
return ans.data.map((x:any):TricksterAccount=>{
|
||||||
return {
|
return {
|
||||||
name: x.name,
|
name: x.name,
|
||||||
characters: [{id: x.id,account_id:x.id, path:x.name, name: x.name+'/bank', class:-8, base_job: -8, current_job: -8},...Object.values(x.characters).map((z:any)=>{
|
characters: [
|
||||||
return {
|
{account_name:x.name, id: x.id,account_id:x.id, path:x.name, name: x.name+'/bank', class:-8, base_job: -8, current_job: -8},...Object.values(x.characters).map((z:any)=>{
|
||||||
account_id: x.id,
|
return {
|
||||||
id: z.id,
|
account_name:x.name,
|
||||||
name: z.name,
|
account_id: x.id,
|
||||||
path: x.name+"/"+z.name,
|
id: z.id,
|
||||||
class: z.class,
|
name: z.name,
|
||||||
base_job: z.base_job,
|
path: x.name+"/"+z.name,
|
||||||
current_job: z.current_job,
|
class: z.class,
|
||||||
}
|
base_job: z.base_job,
|
||||||
})],
|
current_job: z.current_job,
|
||||||
} as TricksterAccount
|
}
|
||||||
|
})],
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { RefStore } from "../../state/state";
|
import { RefStore } from "../../state/state";
|
||||||
import { Serializable } from "../storage";
|
import { Serializable } from "../storage";
|
||||||
|
import { TricksterCharacter } from "../trickster";
|
||||||
import { LTOApi } from "./api";
|
import { LTOApi } from "./api";
|
||||||
import { pathIsBank, splitPath } from "./lifeto";
|
import { pathIsBank, splitPath } from "./lifeto";
|
||||||
import { BankItem, InternalXfer, InvalidOrder, MarketMove, Order,MarketMoveToChar, TxnDetails } from "./order";
|
import { BankItem, InternalXfer, InvalidOrder, MarketMove, Order,MarketMoveToChar, TxnDetails } from "./order";
|
||||||
|
@ -76,14 +77,15 @@ export class OrderTracker implements Serializable<OrderTracker> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OrderSender {
|
export class OrderSender {
|
||||||
r: RefStore
|
constructor(
|
||||||
constructor(r:RefStore) {
|
private orders: OrderTracker,
|
||||||
this.r = r
|
private chars: Map<string,TricksterCharacter>,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
send(o:OrderDetails):Order {
|
send(o:OrderDetails):Order {
|
||||||
const formed = this.form(o)
|
const formed = this.form(o)
|
||||||
this.r.orders.value.orders[formed.action_id] = formed
|
this.orders.orders[formed.action_id] = formed
|
||||||
return formed
|
return formed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,8 +109,8 @@ export class OrderSender {
|
||||||
return notSupported
|
return notSupported
|
||||||
}
|
}
|
||||||
bank_to_bank(o:OrderDetails): Order{
|
bank_to_bank(o:OrderDetails): Order{
|
||||||
const origin = this.r.chars.value.get(o.origin_path)
|
const origin = this.chars.get(o.origin_path)
|
||||||
const target = this.r.chars.value.get(o.target_path)
|
const target = this.chars.get(o.target_path)
|
||||||
if(!(origin && target)) {
|
if(!(origin && target)) {
|
||||||
return notFound
|
return notFound
|
||||||
}
|
}
|
||||||
|
@ -116,8 +118,8 @@ export class OrderSender {
|
||||||
}
|
}
|
||||||
bank_to_user(o:OrderDetails): Order{
|
bank_to_user(o:OrderDetails): Order{
|
||||||
// get the uid of the bank
|
// get the uid of the bank
|
||||||
const origin = this.r.chars.value.get(o.origin_path)
|
const origin = this.chars.get(o.origin_path)
|
||||||
const target = this.r.chars.value.get(o.target_path)
|
const target = this.chars.get(o.target_path)
|
||||||
if(!(origin && target)) {
|
if(!(origin && target)) {
|
||||||
return notFound
|
return notFound
|
||||||
}
|
}
|
||||||
|
@ -128,8 +130,8 @@ export class OrderSender {
|
||||||
return new InternalXfer(this.transformInternalOrder(o))
|
return new InternalXfer(this.transformInternalOrder(o))
|
||||||
}
|
}
|
||||||
user_to_bank(o:OrderDetails): Order{
|
user_to_bank(o:OrderDetails): Order{
|
||||||
const origin = this.r.chars.value.get(o.origin_path)
|
const origin = this.chars.get(o.origin_path)
|
||||||
const target = this.r.chars.value.get(o.target_path)
|
const target = this.chars.get(o.target_path)
|
||||||
if(!(origin && target)) {
|
if(!(origin && target)) {
|
||||||
return notFound
|
return notFound
|
||||||
}
|
}
|
||||||
|
@ -140,8 +142,8 @@ export class OrderSender {
|
||||||
return new BankItem(this.transformInternalOrder(o))
|
return new BankItem(this.transformInternalOrder(o))
|
||||||
}
|
}
|
||||||
user_to_user(o:OrderDetails): Order{
|
user_to_user(o:OrderDetails): Order{
|
||||||
const origin = this.r.chars.value.get(o.origin_path)
|
const origin = this.chars.get(o.origin_path)
|
||||||
const target = this.r.chars.value.get(o.target_path)
|
const target = this.chars.get(o.target_path)
|
||||||
if(!(origin && target)) {
|
if(!(origin && target)) {
|
||||||
return notFound
|
return notFound
|
||||||
}
|
}
|
||||||
|
@ -150,8 +152,8 @@ export class OrderSender {
|
||||||
}
|
}
|
||||||
|
|
||||||
private transformInternalOrder(o:OrderDetails):TxnDetails {
|
private transformInternalOrder(o:OrderDetails):TxnDetails {
|
||||||
const origin = this.r.chars.value.get(o.origin_path)!
|
const origin = this.chars.get(o.origin_path)!
|
||||||
const target = this.r.chars.value.get(o.target_path)!
|
const target = this.chars.get(o.target_path)!
|
||||||
return {
|
return {
|
||||||
origin: origin.id.toString(),
|
origin: origin.id.toString(),
|
||||||
target: target.id.toString(),
|
target: target.id.toString(),
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const BANK_ROOT = "api/lifeto/v2/item-manager/"
|
||||||
export const MARKET_ROOT = "marketplace-api/"
|
export const MARKET_ROOT = "marketplace-api/"
|
||||||
|
|
||||||
const login_endpoint = (name:string)=>{
|
const login_endpoint = (name:string)=>{
|
||||||
return SITE_ROOT + name
|
return SITE_ROOT + name + "?canonical=1"
|
||||||
}
|
}
|
||||||
export const api_endpoint = (name:string):string =>{
|
export const api_endpoint = (name:string):string =>{
|
||||||
return SITE_ROOT+API_ROOT + name
|
return SITE_ROOT+API_ROOT + name
|
||||||
|
@ -48,18 +48,18 @@ export class LoginHelper {
|
||||||
}
|
}
|
||||||
login = async ():Promise<TokenSession> =>{
|
login = async ():Promise<TokenSession> =>{
|
||||||
return axios.get(login_endpoint("login"),{withCredentials:false})
|
return axios.get(login_endpoint("login"),{withCredentials:false})
|
||||||
.then(async (x)=>{
|
.then(async ()=>{
|
||||||
return axios.post(login_endpoint("login"),{
|
return axios.post(login_endpoint("login"),{
|
||||||
login:this.user,
|
login:this.user,
|
||||||
password:this.pass,
|
password:this.pass,
|
||||||
redirectTo:"lifeto"
|
redirectTo:"lifeto"
|
||||||
},{withCredentials:false})
|
},{withCredentials:false})
|
||||||
.then(async (x)=>{
|
}).then(async ()=>{
|
||||||
await sleep(100)
|
await sleep(100)
|
||||||
let xsrf= getCookie("XSRF-TOKEN")
|
let xsrf= getCookie("XSRF-TOKEN")
|
||||||
return new TokenSession(this.user,this.csrf!, xsrf!)
|
return new TokenSession(this.user,this.csrf!, xsrf!)
|
||||||
})
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ export class LogoutHelper{
|
||||||
constructor(){
|
constructor(){
|
||||||
}
|
}
|
||||||
logout = async ():Promise<void> =>{
|
logout = async ():Promise<void> =>{
|
||||||
return axios.get(login_endpoint("logout"),{withCredentials:false}).catch((e)=>{})
|
return axios.get(login_endpoint("logout"),{withCredentials:false}).catch(()=>{}).then(()=>{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const sleep = async(ms:number)=> {
|
const sleep = async(ms:number)=> {
|
||||||
|
@ -89,16 +89,16 @@ export class TokenSession implements Session {
|
||||||
switch (verb.toLowerCase()){
|
switch (verb.toLowerCase()){
|
||||||
case "post":
|
case "post":
|
||||||
promise = axios.post(c(url),data,this.genHeaders())
|
promise = axios.post(c(url),data,this.genHeaders())
|
||||||
break;
|
break;
|
||||||
case "postform":
|
case "postform":
|
||||||
promise = axios.postForm(c(url),data)
|
promise = axios.postForm(c(url),data)
|
||||||
break;
|
break;
|
||||||
case "postraw":
|
case "postraw":
|
||||||
const querystring = qs.stringify(data)
|
const querystring = qs.stringify(data)
|
||||||
promise = axios.post(c(url),querystring,this.genHeaders())
|
promise = axios.post(c(url),querystring,this.genHeaders())
|
||||||
break;
|
break;
|
||||||
case "get":
|
case "get":
|
||||||
default:
|
default:
|
||||||
promise = axios.get(c(url),this.genHeaders())
|
promise = axios.get(c(url),this.genHeaders())
|
||||||
}
|
}
|
||||||
return promise.then(x=>{
|
return promise.then(x=>{
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { HotTableProps } from "@handsontable/vue3/types"
|
|
||||||
import { TricksterInventory } from "./trickster"
|
import { TricksterInventory } from "./trickster"
|
||||||
import {ColumnInfo, ColumnName, Columns, ColumnSorter, LazyColumn} from "./columns"
|
import {ColumnInfo, ColumnName, Columns, ColumnSorter, LazyColumn} from "./columns"
|
||||||
import { ColumnSettings } from "handsontable/settings"
|
|
||||||
import { PredefinedMenuItemKey } from "handsontable/plugins/contextMenu"
|
import { PredefinedMenuItemKey } from "handsontable/plugins/contextMenu"
|
||||||
import { ref } from "vue"
|
|
||||||
import Handsontable from "handsontable"
|
import Handsontable from "handsontable"
|
||||||
|
import { HotTableProps } from "@handsontable/react"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,10 +16,10 @@ export interface InventoryTableOptions {
|
||||||
export interface Mappable<T> {
|
export interface Mappable<T> {
|
||||||
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
|
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
|
||||||
}
|
}
|
||||||
export class ColumnSet implements Set<ColumnInfo>, Mappable<ColumnInfo>{
|
export class ColumnSet implements Mappable<ColumnInfo>{
|
||||||
s: Set<ColumnName> = new Set()
|
s: Set<ColumnName> = new Set()
|
||||||
size: number;
|
size: number;
|
||||||
dirty = ref(0)
|
dirty = 0
|
||||||
constructor(i?:Iterable<ColumnInfo | ColumnName>){
|
constructor(i?:Iterable<ColumnInfo | ColumnName>){
|
||||||
if(i){
|
if(i){
|
||||||
for (const a of i) {
|
for (const a of i) {
|
||||||
|
@ -59,7 +57,7 @@ export class ColumnSet implements Set<ColumnInfo>, Mappable<ColumnInfo>{
|
||||||
}).values()
|
}).values()
|
||||||
}
|
}
|
||||||
mark() {
|
mark() {
|
||||||
this.dirty.value = this.dirty.value + 1
|
this.dirty= this.dirty+ 1
|
||||||
this.size = this.s.size
|
this.size = this.s.size
|
||||||
}
|
}
|
||||||
add(value: ColumnInfo | ColumnName): this {
|
add(value: ColumnInfo | ColumnName): this {
|
||||||
|
@ -97,9 +95,9 @@ export class InventoryTable {
|
||||||
getTableColumnNames(): string[] {
|
getTableColumnNames(): string[] {
|
||||||
return this.o.columns.map(x=>x.displayName)
|
return this.o.columns.map(x=>x.displayName)
|
||||||
}
|
}
|
||||||
getTableColumnSettings(): ColumnSettings[] {
|
getTableColumnSettings(): Handsontable.ColumnSettings[] {
|
||||||
return this.o.columns.map(x=>{
|
return this.o.columns.map(x=>{
|
||||||
let out:any = {
|
let out:Handsontable.ColumnSettings = {
|
||||||
renderer: x.renderer ? x.renderer : "text",
|
renderer: x.renderer ? x.renderer : "text",
|
||||||
filters: true,
|
filters: true,
|
||||||
dropdownMenu: x.filtering ? DefaultDropdownItems() : false,
|
dropdownMenu: x.filtering ? DefaultDropdownItems() : false,
|
||||||
|
@ -200,7 +198,6 @@ export const DefaultSettings = ():HotTableProps=>{
|
||||||
// Deselect the column after clicking on input.
|
// Deselect the column after clicking on input.
|
||||||
if (coords.row === -1 && event.target.nodeName === 'INPUT') {
|
if (coords.row === -1 && event.target.nodeName === 'INPUT') {
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
this.deselectCell();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
className: 'htLeft',
|
className: 'htLeft',
|
||||||
|
|
|
@ -22,6 +22,7 @@ export interface TricksterAccount {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Identifier {
|
export interface Identifier {
|
||||||
|
account_name: string
|
||||||
account_id: number
|
account_id: number
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
export const Login = () => {
|
|
||||||
|
|
||||||
|
|
||||||
return <></>
|
|
||||||
|
|
||||||
}
|
|
|
@ -48,11 +48,10 @@ const login = () => {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}).catch((e)=>{
|
}).catch((e)=>{
|
||||||
if(e.code == "ERR_BAD_REQUEST") {
|
if(e.code == "ERR_BAD_REQUEST") {
|
||||||
alert("invalid username/password")
|
throw "invalid username/password"
|
||||||
return
|
|
||||||
}
|
}
|
||||||
alert("unknown error, please report")
|
console.warn(e)
|
||||||
console.log(e)
|
throw "unknown error, please report"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue