1
0
forked from a/lifeto-shop

Compare commits

...

10 Commits

Author SHA1 Message Date
a
6e99ef1501
noot 2024-08-11 20:13:42 -05:00
a
e39ba4d052
noot 2024-08-05 16:22:28 -05:00
a
140c604502
noot 2024-08-05 16:08:21 -05:00
a
e913f85a0a
noot 2024-08-05 15:58:47 -05:00
a
cdbb83d6af
a 2024-04-08 22:56:47 -05:00
a
182bcfc13e
Merge branch 'raymonf-v2-api' 2024-04-08 22:53:11 -05:00
a
9869512d3c Merge branch 'master' into v2-api 2024-04-09 03:52:57 +00:00
a
a7ecda47d0
a 2024-04-08 22:49:29 -05:00
Raymonf
dfbb8ea7af the 2024-04-08 23:46:51 -04:00
Raymonf
09b4dcbea9 v2 2024-04-08 23:42:34 -04:00
31 changed files with 5245 additions and 307 deletions

View File

@ -1,6 +1,9 @@
VERSION=v0.0.2
build:
docker build -t cr.aaaaa.news/lto:latest .
docker build -t tuxpa.in/a/lto:${VERSION} .
push:
docker push cr.aaaaa.news/lto:latest
docker push tuxpa.in/a/lto:${VERSION}

54
eslint.config.cjs Normal file
View File

@ -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,
},
}

View File

@ -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>

View File

@ -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"
}

6
postcss.config.cjs Normal file
View File

@ -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

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

30
src/App.tsx Normal file
View File

@ -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>
</>
);
};

View File

@ -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
}

View File

@ -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>
</>
}

View File

@ -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>
}

62
src/components/login.tsx Normal file
View File

@ -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>
</>
}

View File

@ -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;

View File

@ -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;
};

View File

@ -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;
};

3
src/index.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

19
src/index.tsx Normal file
View File

@ -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>,
);

View File

@ -17,7 +17,7 @@ class Image implements ColumnInfo {
displayName = " "
renderer = coverRenderer
getter(item:TricksterItem):(string|number) {
return item.image ? item.image : ""
return item.item_image ? item.item_image : ""
}
}
@ -207,7 +207,7 @@ class Compound implements ColumnInfo {
}
const compFilter= (item:TricksterItem): boolean => {
return (item.item_desc.toLowerCase().includes("compound item"))
return (item.item_comment.toLowerCase().includes("compound item"))
}
@ -379,7 +379,7 @@ class Desc implements ColumnInfo {
displayName = "desc"
renderer = descRenderer
getter(item:TricksterItem):(string|number){
return item.item_desc
return item.item_comment
}
}
function descRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) {

View File

@ -46,7 +46,8 @@ export class LTOApiv0 implements LTOApi {
if(char_path.startsWith(":")) {
char_path = char_path.replace(":","")
}
return this.s.request("GET", `item-manager/items/account/${char_path}`,undefined).then((ans:AxiosResponse)=>{
let type = char_path.includes("/") ? "char" : "account"
return this.s.request("GET", `v2/item-manager/items/${type}/${char_path}`,undefined).then((ans:AxiosResponse)=>{
const o = ans.data
log.debug("GetInventory", o)
let name = "bank"
@ -63,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,
@ -72,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,
}
})],
}
})
})
}

View File

@ -117,9 +117,9 @@ export class InvalidOrder extends Order{
}
export interface BasicResponse {
status: number
status: string
data: any
msg?: string
message?: string
}
@ -153,14 +153,14 @@ export class InternalXfer extends BasicOrder{
this.mark("WORKING")
return api.BankAction<InternalXferRequest, InternalXferResponse>("internal-xfer-item",this.originalRequest)
.then((x:InternalXferResponse)=>{
if(x.status == 200){
if(x.status === 'success'){
this.originalResponse = x
this.stage = 1
this.mark("SUCCESS")
const origin_item = r.invs.value.get(this.details?.origin_path!)!.items[this.details?.item_uid!]!
origin_item.item_count = origin_item.item_count - this.details?.count!
}else{
throw x.msg
throw x.message
}
})
.catch((e)=>{
@ -207,14 +207,14 @@ export class BankItem extends BasicOrder{
return api.BankAction<BankItemRequest, BankItemResponse>("bank-item",this.originalRequest)
.then((x)=>{
debug("BankItem",x)
if(x.status == 200){
if(x.status === 'success'){
this.stage = 1
this.originalResponse = x
this.mark("SUCCESS")
const origin_item = r.invs.value.get(this.details?.origin_path!)!.items[this.details?.item_uid!]!
origin_item.item_count = origin_item.item_count - this.details?.count!
}else {
throw x.msg ? x.msg : "unknown error"
throw x.message ? x.message : "unknown error"
}
})
.catch((e)=>{
@ -272,7 +272,7 @@ export class PrivateMarket extends BasicOrder{
return api.BankAction<PrivateMarketRequest, PrivateMarketResponse>("sell-item",this.originalRequest)
.then((x)=>{
debug("PrivateMarket",x)
if(x.status == 200){
if(x.status === 'success'){
this.stage = 1
this.originalResponse = x
this.mark("SUCCESS")
@ -284,7 +284,7 @@ export class PrivateMarket extends BasicOrder{
}catch(e){
}
}else {
throw x.msg ? x.msg : "unknown error"
throw x.message ? x.message : "unknown error"
}
})
.catch((e)=>{
@ -364,7 +364,7 @@ export class MarketMove extends PrivateMarket {
.then((x)=>{
debug("MarketMove",x)
this.moveResponse = x
if(x.status == 200){
if(x.status === 'success'){
this.moveStage = 1
this.moveState = "SUCCESS"
this.newUid = x.item_uid
@ -440,7 +440,7 @@ export class MarketMoveToChar extends MarketMove {
.then((x)=>{
debug("MarketMoveToChar",x)
this.charResponse = x
if(x.status == 200){
if(x.status === 'success'){
this.charStage = 1
this.charState = "SUCCESS"
}else {

View File

@ -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,41 +118,42 @@ 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
}
const [account, name] = splitPath(target.path)
if(account != origin.path) {
/*if(account != origin.path) {
return new MarketMoveToChar(this.transformInternalOrder(o))
}
}*/
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
}
const [account, name] = splitPath(origin.path)
if(account != target.path) {
/*if(account != target.path) {
return new MarketMove(this.transformInternalOrder(o))
}
}*/
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
}
return new MarketMoveToChar(this.transformInternalOrder(o))
// return new MarketMoveToChar(this.transformInternalOrder(o))
return new InternalXfer(this.transformInternalOrder(o))
}
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(),

View File

@ -80,7 +80,7 @@ class Image implements ColumnInfo {
displayName = " "
renderer = coverRenderer
getter(item:TricksterItem):(string|number) {
return item.image ? item.image : ""
return item.item_image ? item.item_image : ""
}
}
@ -255,7 +255,7 @@ class Compound implements ColumnInfo {
}
const compFilter= (item:TricksterItem): boolean => {
return (item.item_desc.toLowerCase().includes("compound item"))
return (item.item_comment.toLowerCase().includes("compound item"))
}
@ -427,7 +427,7 @@ class Desc implements ColumnInfo {
displayName = "desc"
renderer = descRenderer
getter(item:TricksterItem):(string|number){
return item.item_desc
return item.item_comment
}
}
function descRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) {

View File

@ -6,11 +6,11 @@ import { getCookie, removeCookie } from "typescript-cookie";
export const SITE_ROOT = "/lifeto/"
export const API_ROOT = "api/lifeto/"
export const BANK_ROOT = "item-manager-action/"
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=>{

View File

@ -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',

View File

@ -1,24 +1,18 @@
export interface ItemExpireTime {
text: string
us: string
}
export interface TricksterItem {
unique_id: number;
item_name: string;
item_id: number;
item_count: number;
item_desc: string;
item_comment: string;
item_use: string;
item_slots?: number;
item_min_level?: number;
is_equip?: boolean;
is_drill?: boolean;
item_expire_time?: ItemExpireTime;
item_expire_time?: string;
refine_level?: number;
refine_type?: number;
refine_state?: number;
image?: string;
item_image?: string;
stats?: {[key: string]:any}
}
@ -28,6 +22,7 @@ export interface TricksterAccount {
}
export interface Identifier {
account_name: string
account_id: number
id: number
name: string

View File

@ -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"
})
}

10
tailwind.config.js Normal file
View File

@ -0,0 +1,10 @@
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

View File

@ -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" }]
}

View File

@ -1,8 +1,10 @@
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node"
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -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/, ''),
// },
// }
// }
//})

4688
yarn.lock

File diff suppressed because it is too large Load Diff