forked from a/lifeto-shop
Compare commits
6 Commits
Author | SHA1 | Date |
---|---|---|
a | 6e99ef1501 | |
a | e39ba4d052 | |
a | 140c604502 | |
a | e913f85a0a | |
a | cdbb83d6af | |
a | 182bcfc13e |
2
Makefile
2
Makefile
|
@ -1,4 +1,4 @@
|
|||
VERSION=v0.0.1
|
||||
VERSION=v0.0.2
|
||||
|
||||
build:
|
||||
docker build -t tuxpa.in/a/lto:${VERSION} .
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -4,10 +4,10 @@
|
|||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
<title>lto inventory</title>
|
||||
</head>
|
||||
<body style="overflow-y: hidden;">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<body style="overflow-y: hidden;" class="w-screen h-screen">
|
||||
<div id="app" class="w-full h-full"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
37
package.json
37
package.json
|
@ -4,27 +4,48 @@
|
|||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@handsontable/vue3": "^12.0.1",
|
||||
"@handsontable/react": "^14.5.0",
|
||||
"@tanstack/react-query": "^5.51.21",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||
"@typescript-eslint/parser": "^8.0.1",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"@vueuse/core": "^8.7.5",
|
||||
"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",
|
||||
"pinia": "^2.0.14",
|
||||
"prettier": "^3.3.3",
|
||||
"qs": "^6.10.5",
|
||||
"typescript-cookie": "^1.0.4",
|
||||
"uuid": "^8.3.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-spinners": "^0.14.1",
|
||||
"typescript-cookie": "^1.0.6",
|
||||
"use-local-storage": "^3.0.0",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"uuid": "^10.0.0",
|
||||
"vue": "^3.2.25"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^2.3.3",
|
||||
"typescript": "^4.5.4",
|
||||
"vite": "^2.9.9",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.40",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.3.5",
|
||||
"vue-tsc": "^0.34.7"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -0,0 +1,30 @@
|
|||
import { FC } from "react";
|
||||
import { LoginWidget } from "./components/login";
|
||||
import { CharacterRoulette } from "./components/characters";
|
||||
import { Inventory } from "./components/inventory";
|
||||
|
||||
export const App: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -36,7 +36,7 @@ watch(invs.value,()=>{
|
|||
const currentInv = invs.value.get(props.character)
|
||||
if(currentInv){
|
||||
if(currentInv.galders){
|
||||
galders.value = currentInv.galders
|
||||
galders.value = currentInv.galders || currentInv.money
|
||||
}
|
||||
items.value = Object.values(currentInv.items).length
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
|
@ -0,0 +1,19 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { App } from "./App";
|
||||
import AppContext from "./context/AppContext";
|
||||
|
||||
|
||||
import "./index.css";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("app") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppContext>
|
||||
<App />
|
||||
</AppContext>
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
|
@ -64,7 +64,9 @@ export class LTOApiv0 implements LTOApi {
|
|||
id = Number(char)
|
||||
galders = val.galders
|
||||
}
|
||||
let out = {
|
||||
let out:TricksterInventory = {
|
||||
account_name: o.account.account_gid,
|
||||
account_id: o.account.account_code,
|
||||
name,
|
||||
id,
|
||||
path: char_path,
|
||||
|
@ -73,28 +75,30 @@ export class LTOApiv0 implements LTOApi {
|
|||
v.unique_id = Number(k)
|
||||
return [k, v]
|
||||
})),
|
||||
} as TricksterInventory
|
||||
}
|
||||
return out
|
||||
})
|
||||
}
|
||||
GetAccounts = async ():Promise<TricksterAccount[]> => {
|
||||
return this.s.request("GET", "characters/list",undefined).then((ans:AxiosResponse)=>{
|
||||
log.debug("GetAccounts", ans.data)
|
||||
return ans.data.map((x:any)=>{
|
||||
return ans.data.map((x:any):TricksterAccount=>{
|
||||
return {
|
||||
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)=>{
|
||||
return {
|
||||
account_id: x.id,
|
||||
id: z.id,
|
||||
name: z.name,
|
||||
path: x.name+"/"+z.name,
|
||||
class: z.class,
|
||||
base_job: z.base_job,
|
||||
current_job: z.current_job,
|
||||
}
|
||||
})],
|
||||
} as TricksterAccount
|
||||
characters: [
|
||||
{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)=>{
|
||||
return {
|
||||
account_name:x.name,
|
||||
account_id: x.id,
|
||||
id: z.id,
|
||||
name: z.name,
|
||||
path: x.name+"/"+z.name,
|
||||
class: z.class,
|
||||
base_job: z.base_job,
|
||||
current_job: z.current_job,
|
||||
}
|
||||
})],
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { RefStore } from "../../state/state";
|
||||
import { Serializable } from "../storage";
|
||||
import { TricksterCharacter } from "../trickster";
|
||||
import { LTOApi } from "./api";
|
||||
import { pathIsBank, splitPath } from "./lifeto";
|
||||
import { BankItem, InternalXfer, InvalidOrder, MarketMove, Order,MarketMoveToChar, TxnDetails } from "./order";
|
||||
|
@ -76,14 +77,15 @@ export class OrderTracker implements Serializable<OrderTracker> {
|
|||
}
|
||||
|
||||
export class OrderSender {
|
||||
r: RefStore
|
||||
constructor(r:RefStore) {
|
||||
this.r = r
|
||||
constructor(
|
||||
private orders: OrderTracker,
|
||||
private chars: Map<string,TricksterCharacter>,
|
||||
) {
|
||||
}
|
||||
|
||||
send(o:OrderDetails):Order {
|
||||
const formed = this.form(o)
|
||||
this.r.orders.value.orders[formed.action_id] = formed
|
||||
this.orders.orders[formed.action_id] = formed
|
||||
return formed
|
||||
}
|
||||
|
||||
|
@ -107,8 +109,8 @@ export class OrderSender {
|
|||
return notSupported
|
||||
}
|
||||
bank_to_bank(o:OrderDetails): Order{
|
||||
const origin = this.r.chars.value.get(o.origin_path)
|
||||
const target = this.r.chars.value.get(o.target_path)
|
||||
const origin = this.chars.get(o.origin_path)
|
||||
const target = this.chars.get(o.target_path)
|
||||
if(!(origin && target)) {
|
||||
return notFound
|
||||
}
|
||||
|
@ -116,8 +118,8 @@ export class OrderSender {
|
|||
}
|
||||
bank_to_user(o:OrderDetails): Order{
|
||||
// get the uid of the bank
|
||||
const origin = this.r.chars.value.get(o.origin_path)
|
||||
const target = this.r.chars.value.get(o.target_path)
|
||||
const origin = this.chars.get(o.origin_path)
|
||||
const target = this.chars.get(o.target_path)
|
||||
if(!(origin && target)) {
|
||||
return notFound
|
||||
}
|
||||
|
@ -128,8 +130,8 @@ export class OrderSender {
|
|||
return new InternalXfer(this.transformInternalOrder(o))
|
||||
}
|
||||
user_to_bank(o:OrderDetails): Order{
|
||||
const origin = this.r.chars.value.get(o.origin_path)
|
||||
const target = this.r.chars.value.get(o.target_path)
|
||||
const origin = this.chars.get(o.origin_path)
|
||||
const target = this.chars.get(o.target_path)
|
||||
if(!(origin && target)) {
|
||||
return notFound
|
||||
}
|
||||
|
@ -140,8 +142,8 @@ export class OrderSender {
|
|||
return new BankItem(this.transformInternalOrder(o))
|
||||
}
|
||||
user_to_user(o:OrderDetails): Order{
|
||||
const origin = this.r.chars.value.get(o.origin_path)
|
||||
const target = this.r.chars.value.get(o.target_path)
|
||||
const origin = this.chars.get(o.origin_path)
|
||||
const target = this.chars.get(o.target_path)
|
||||
if(!(origin && target)) {
|
||||
return notFound
|
||||
}
|
||||
|
@ -150,8 +152,8 @@ export class OrderSender {
|
|||
}
|
||||
|
||||
private transformInternalOrder(o:OrderDetails):TxnDetails {
|
||||
const origin = this.r.chars.value.get(o.origin_path)!
|
||||
const target = this.r.chars.value.get(o.target_path)!
|
||||
const origin = this.chars.get(o.origin_path)!
|
||||
const target = this.chars.get(o.target_path)!
|
||||
return {
|
||||
origin: origin.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/"
|
||||
|
||||
const login_endpoint = (name:string)=>{
|
||||
return SITE_ROOT + name
|
||||
return SITE_ROOT + name + "?canonical=1"
|
||||
}
|
||||
export const api_endpoint = (name:string):string =>{
|
||||
return SITE_ROOT+API_ROOT + name
|
||||
|
@ -48,18 +48,18 @@ export class LoginHelper {
|
|||
}
|
||||
login = async ():Promise<TokenSession> =>{
|
||||
return axios.get(login_endpoint("login"),{withCredentials:false})
|
||||
.then(async (x)=>{
|
||||
return axios.post(login_endpoint("login"),{
|
||||
login:this.user,
|
||||
password:this.pass,
|
||||
redirectTo:"lifeto"
|
||||
},{withCredentials:false})
|
||||
.then(async (x)=>{
|
||||
.then(async ()=>{
|
||||
return axios.post(login_endpoint("login"),{
|
||||
login:this.user,
|
||||
password:this.pass,
|
||||
redirectTo:"lifeto"
|
||||
},{withCredentials:false})
|
||||
}).then(async ()=>{
|
||||
await sleep(100)
|
||||
let xsrf= getCookie("XSRF-TOKEN")
|
||||
return new TokenSession(this.user,this.csrf!, xsrf!)
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ export class LogoutHelper{
|
|||
constructor(){
|
||||
}
|
||||
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)=> {
|
||||
|
@ -89,16 +89,16 @@ export class TokenSession implements Session {
|
|||
switch (verb.toLowerCase()){
|
||||
case "post":
|
||||
promise = axios.post(c(url),data,this.genHeaders())
|
||||
break;
|
||||
break;
|
||||
case "postform":
|
||||
promise = axios.postForm(c(url),data)
|
||||
break;
|
||||
promise = axios.postForm(c(url),data)
|
||||
break;
|
||||
case "postraw":
|
||||
const querystring = qs.stringify(data)
|
||||
promise = axios.post(c(url),querystring,this.genHeaders())
|
||||
break;
|
||||
promise = axios.post(c(url),querystring,this.genHeaders())
|
||||
break;
|
||||
case "get":
|
||||
default:
|
||||
default:
|
||||
promise = axios.get(c(url),this.genHeaders())
|
||||
}
|
||||
return promise.then(x=>{
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import { HotTableProps } from "@handsontable/vue3/types"
|
||||
import { TricksterInventory } from "./trickster"
|
||||
import {ColumnInfo, ColumnName, Columns, ColumnSorter, LazyColumn} from "./columns"
|
||||
import { ColumnSettings } from "handsontable/settings"
|
||||
import { PredefinedMenuItemKey } from "handsontable/plugins/contextMenu"
|
||||
import { ref } from "vue"
|
||||
import Handsontable from "handsontable"
|
||||
import { HotTableProps } from "@handsontable/react"
|
||||
|
||||
|
||||
|
||||
|
@ -18,10 +16,10 @@ export interface InventoryTableOptions {
|
|||
export interface Mappable<T> {
|
||||
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()
|
||||
size: number;
|
||||
dirty = ref(0)
|
||||
dirty = 0
|
||||
constructor(i?:Iterable<ColumnInfo | ColumnName>){
|
||||
if(i){
|
||||
for (const a of i) {
|
||||
|
@ -59,7 +57,7 @@ export class ColumnSet implements Set<ColumnInfo>, Mappable<ColumnInfo>{
|
|||
}).values()
|
||||
}
|
||||
mark() {
|
||||
this.dirty.value = this.dirty.value + 1
|
||||
this.dirty= this.dirty+ 1
|
||||
this.size = this.s.size
|
||||
}
|
||||
add(value: ColumnInfo | ColumnName): this {
|
||||
|
@ -97,9 +95,9 @@ export class InventoryTable {
|
|||
getTableColumnNames(): string[] {
|
||||
return this.o.columns.map(x=>x.displayName)
|
||||
}
|
||||
getTableColumnSettings(): ColumnSettings[] {
|
||||
getTableColumnSettings(): Handsontable.ColumnSettings[] {
|
||||
return this.o.columns.map(x=>{
|
||||
let out:any = {
|
||||
let out:Handsontable.ColumnSettings = {
|
||||
renderer: x.renderer ? x.renderer : "text",
|
||||
filters: true,
|
||||
dropdownMenu: x.filtering ? DefaultDropdownItems() : false,
|
||||
|
@ -200,7 +198,6 @@ export const DefaultSettings = ():HotTableProps=>{
|
|||
// Deselect the column after clicking on input.
|
||||
if (coords.row === -1 && event.target.nodeName === 'INPUT') {
|
||||
event.stopImmediatePropagation();
|
||||
this.deselectCell();
|
||||
}
|
||||
},
|
||||
className: 'htLeft',
|
||||
|
|
|
@ -22,6 +22,7 @@ export interface TricksterAccount {
|
|||
}
|
||||
|
||||
export interface Identifier {
|
||||
account_name: string
|
||||
account_id: number
|
||||
id: number
|
||||
name: string
|
||||
|
|
|
@ -48,11 +48,10 @@ const login = () => {
|
|||
window.location.reload()
|
||||
}).catch((e)=>{
|
||||
if(e.code == "ERR_BAD_REQUEST") {
|
||||
alert("invalid username/password")
|
||||
return
|
||||
throw "invalid username/password"
|
||||
}
|
||||
alert("unknown error, please report")
|
||||
console.log(e)
|
||||
console.warn(e)
|
||||
throw "unknown error, please report"
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
|
@ -1,18 +1,30 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"target": "esnext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"types": [ "node" ],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["esnext", "dom"],
|
||||
"skipLibCheck": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": false,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"include": [
|
||||
"src",
|
||||
"app",
|
||||
"index",
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node"
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
plugins: [react()],
|
||||
server: {
|
||||
proxy: {
|
||||
// with options
|
||||
|
@ -15,3 +16,18 @@ export default defineConfig({
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
//export default defineConfig({
|
||||
// plugins: [vue()],
|
||||
// server: {
|
||||
// proxy: {
|
||||
// // with options
|
||||
// '/lifeto': {
|
||||
// target: "https://beta.lifeto.co/",
|
||||
// changeOrigin: true,
|
||||
// rewrite: (path) => path.replace(/^\/lifeto/, ''),
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
//})
|
||||
|
|
Loading…
Reference in New Issue