This commit is contained in:
a 2025-06-20 01:18:37 -05:00
parent bd20e23b15
commit f00708e80d
No known key found for this signature in database
GPG Key ID: 2F22877AA4DFDADB
19 changed files with 165 additions and 218 deletions

View File

@ -1,4 +1,5 @@
dist
dist/**
**/vendor/**
**/locales/**
generated.*

View File

@ -1,7 +1,8 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"files": {
"ignoreUnknown": true
"ignoreUnknown": true,
"includes": ["src/**/*.{ts,tsx,js,jsx}"]
},
"linter": {
"enabled": true,

View File

@ -48,7 +48,8 @@ export const CharacterCard = ({ character }: { character: TricksterCharacter })
return (
<>
<div
<button
type="button"
onClick={() => {
setSelectedCharacter(character)
}}
@ -64,7 +65,11 @@ export const CharacterCard = ({ character }: { character: TricksterCharacter })
<div className="flex flex-col gap-2">
<div className="flex flex-row justify-center">
{character.base_job === -8 ? (
<img className="h-8" src="https://beta.lifeto.co/item_img/gel.nri.003.000.png" />
<img
className="h-8"
src="https://beta.lifeto.co/item_img/gel.nri.003.000.png"
alt="Gel character"
/>
) : (
<img
className="h-16"
@ -75,6 +80,7 @@ export const CharacterCard = ({ character }: { character: TricksterCharacter })
)
.toString()
.padStart(3, '0')}_13.png`}
alt={`Character ${character.name}`}
/>
)}
</div>
@ -94,7 +100,7 @@ export const CharacterCard = ({ character }: { character: TricksterCharacter })
</FloatingPortal>
</div>
</div>
</div>
</button>
</>
)
}
@ -157,7 +163,7 @@ export const CharacterRoulette = () => {
}}
></input>
<div className="flex flex-row flex-wrap overflow-x-scroll gap-1 h-full min-h-36 max-w-48">
{searchResults ? searchResults : <></>}
{searchResults ? searchResults : null}
</div>
</div>
</>

View File

@ -46,7 +46,8 @@ const InventoryTabs = () => {
<div className="flex flex-row gap-1">
{sections.map(x => {
return (
<div
<button
type="button"
onClick={() => {
setInventoryFilterTab(x.value)
}}
@ -55,14 +56,15 @@ const InventoryTabs = () => {
${inventoryFilter.tab === x.value ? selectedStyle : ''}`}
>
{x.name}
</div>
</button>
)
})}
</div>
<div className="flex flex-row gap-1">
{cardSections.map(x => {
return (
<div
<button
type="button"
onClick={() => {
setInventoryFilterTab(x.value)
}}
@ -71,7 +73,7 @@ ${inventoryFilter.tab === x.value ? selectedStyle : ''}`}
${inventoryFilter.tab === x.value ? selectedStyle : ''}`}
>
{x.name}
</div>
</button>
)
})}
</div>
@ -103,41 +105,45 @@ export const Inventory = () => {
<div className="flex flex-col py-2 flex-0 justify-between h-full">
<div className="flex flex-row justify-between">
<div className="flex flex-row gap-2">
<div
<button
type="button"
className="whitespace-pre bg-blue-200 px-2 py-1 rounded-xl hover:cursor-pointer hover:bg-blue-300"
onClick={() => {
addPageItemSelection()
}}
>
select filtered
</div>
<div
</button>
<button
type="button"
className="whitespace-pre bg-blue-200 px-2 py-1 rounded-xl hover:cursor-pointer hover:bg-blue-300"
onClick={() => {
addFilterItemSelection()
}}
>
select page
</div>
<div
</button>
<button
type="button"
className="whitespace-pre bg-blue-200 px-2 py-1 rounded-xl hover:cursor-pointer hover:bg-blue-300"
onClick={() => {
clearItemSelection()
}}
>
clear{' '}
</div>
</button>
</div>
<div className="flex flex-row">
<InventoryTargetSelector />
<div
<button
type="button"
onClick={_e => {
// sendOrders()
}}
className="hover:cursor-pointer whitespace-preborder border-black-1 bg-orange-200 hover:bg-orange-300 px-2 py-1"
>
Move Selected
</div>
</button>
</div>
</div>
@ -152,22 +158,26 @@ export const Inventory = () => {
setSearch(e.target.value)
}}
/>
<div
<button
type="button"
className="hover:cursor-pointer border border-black-1 bg-green-200 hover:bg-green-300 px-2 py-1 h-full flex items-center"
onClick={() => {
paginateInventory(-1)
}}
aria-label="Previous page"
>
<FaArrowLeft />
</div>
<div
</button>
<button
type="button"
className="hover:cursor-pointer border border-black-1 bg-green-200 hover:bg-green-300 px-2 py-1 h-full flex items-center"
onClick={() => {
paginateInventory(1)
}}
aria-label="Next page"
>
<FaArrowRight />
</div>
</button>
</div>
</div>
</div>

View File

@ -28,9 +28,11 @@ const AccountInventorySelectorItem = forwardRef<
return (
<div
ref={ref}
// biome-ignore lint/a11y/useSemanticElements: Custom autocomplete component needs role="option"
role="option"
id={id}
aria-selected={active}
tabIndex={-1}
{...rest}
style={{
background: active ? 'lightblue' : 'none',
@ -149,8 +151,8 @@ export const InventoryTargetSelector = () => {
>
{items.map((item, index) => (
<AccountInventorySelectorItem
key={item.path}
{...getItemProps({
key: item.path,
ref(node) {
listRef.current[index] = node
},

View File

@ -1,7 +1,7 @@
import { useAtom } from 'jotai'
import { useState } from 'react'
import useLocalStorage from 'use-local-storage'
import { LoginHelper } from '../lib/session'
import { login, logout } from '../lib/session'
import { loginStatusAtom } from '../state/atoms'
export const LoginWidget = () => {
@ -19,8 +19,9 @@ export const LoginWidget = () => {
<div>{loginState.community_name}</div>
<div className="flex flex-row gap-2">
<button
type="button"
onClick={() => {
LoginHelper.logout().finally(() => {
logout().finally(() => {
refetchLoginState()
})
return
@ -40,7 +41,7 @@ export const LoginWidget = () => {
<div className="flex flex-col">
<form
action={() => {
LoginHelper.login(username, password)
login(username, password)
.catch(e => {
setLoginError(e.message)
})
@ -58,7 +59,6 @@ export const LoginWidget = () => {
setUsername(e.target.value)
}}
value={username}
id="username"
placeholder="username"
className="w-32 pl-2 pb-1 border-b border-gray-600 placeholder-gray-500"
/>

View File

@ -1,14 +0,0 @@
import { SessionContextProvider } from './SessionContext'
interface IContext {
children: React.ReactNode
}
function AppContext(props: IContext): any {
const { children } = props
const providers = [SessionContextProvider]
const res = providers.reduceRight((acc, CurrVal) => <CurrVal>{acc as any}</CurrVal>, children)
return res as any
}
export default AppContext

View File

@ -1,80 +0,0 @@
import { createContext, useContext, useState } from 'react'
type Setter<T> = React.Dispatch<React.SetStateAction<T | undefined>>
type MustSetter<T> = React.Dispatch<React.SetStateAction<T>>
import useLocalStorage from 'use-local-storage'
import { BasicColumns, ColumnInfo, ColumnName, DetailsColumns, MoveColumns } from '../lib/columns'
import { OrderTracker } from '../lib/lifeto/order_manager'
import { StoreColSet } from '../lib/storage'
import { ColumnSet } from '../lib/table'
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
}

View File

@ -1,7 +1,6 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { App } from './App'
import AppContext from './context/AppContext'
import './lib/superjson'
import './index.css'
@ -14,9 +13,7 @@ ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
<React.StrictMode>
<Provider>
<QueryClientProvider client={queryClient}>
<AppContext>
<App />
</AppContext>
</QueryClientProvider>
</Provider>
</React.StrictMode>,

View File

@ -88,15 +88,6 @@ class Count implements ColumnInfo {
}
const spacer = '-----------'
class Move implements ColumnInfo {
name: ColumnName = 'Move'
displayName = 'Target'
writable = true
options = getMoveTargets
getter(_item: TricksterItem): string | number {
return spacer
}
}
const getMoveTargets = (invs: string[]): string[] => {
const out: string[] = []
@ -110,6 +101,16 @@ const getMoveTargets = (invs: string[]): string[] => {
return out
}
class Move implements ColumnInfo {
name: ColumnName = 'Move'
displayName = 'Target'
writable = true
options = getMoveTargets
getter(_item: TricksterItem): string | number {
return spacer
}
}
class MoveCount implements ColumnInfo {
name: ColumnName = 'MoveCount'
displayName = 'Move #'

View File

@ -34,9 +34,12 @@ export class LTOApiv0 implements LTOApi {
case 'buy-from-order':
case 'cancel-order':
endpoint = market_endpoint
break
case 'sell-item':
VERB = 'POSTFORM'
break
default:
break
}
return this.s.request(VERB as any, e, t, endpoint).then(x => {
return x.data

View File

@ -166,10 +166,13 @@ export class InternalXfer extends BasicOrder {
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!
if (this.details?.origin_path && this.details?.item_uid && this.details?.count) {
const inventory = r.invs.value.get(this.details.origin_path)
const origin_item = inventory?.items[this.details.item_uid]
if (origin_item) {
origin_item.item_count = origin_item.item_count - this.details.count
}
}
} else {
throw x.message
}
@ -223,10 +226,13 @@ export class BankItem extends BasicOrder {
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!
if (this.details?.origin_path && this.details?.item_uid && this.details?.count) {
const inventory = r.invs.value.get(this.details.origin_path)
const origin_item = inventory?.items[this.details.item_uid]
if (origin_item) {
origin_item.item_count = origin_item.item_count - this.details.count
}
}
} else {
throw x.message ? x.message : 'unknown error'
}
@ -292,12 +298,13 @@ export class PrivateMarket extends BasicOrder {
this.mark('SUCCESS')
this.listingId = x.data.listing_id
this.listingHash = x.data.hash
try {
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!
} catch (_e) {}
if (this.details?.origin_path && this.details?.item_uid && this.details?.count) {
const inventory = r.invs.value.get(this.details.origin_path)
const origin_item = inventory?.items[this.details.item_uid]
if (origin_item) {
origin_item.item_count = origin_item.item_count - this.details.count
}
}
} else {
throw x.message ? x.message : 'unknown error'
}

View File

@ -65,6 +65,7 @@ export class OrderTracker implements Serializable<OrderTracker> {
break
case 'MarketMove':
newOrder = new MarketMove(o.details).parse(o)
break
case 'MarketMoveToChar':
newOrder = new MarketMoveToChar(o.details).parse(o)
break
@ -156,8 +157,11 @@ export class OrderSender {
}
private transformInternalOrder(o: OrderDetails): TxnDetails {
const origin = this.chars.get(o.origin_path)!
const target = this.chars.get(o.target_path)!
const origin = this.chars.get(o.origin_path)
const target = this.chars.get(o.target_path)
if (!origin || !target) {
throw new Error(`Character not found: origin=${o.origin_path}, target=${o.target_path}`)
}
return {
origin: origin.id.toString(),
target: target.id.toString(),

View File

@ -26,14 +26,15 @@ export class StatefulLTOApi implements LTOApi {
}
GetInventory = async (path: string): Promise<TricksterInventory> => {
const inv = await this.u.GetInventory(path)
if (this.r.invs.value.get(inv.path)) {
this.r.invs.value.get(inv.path)!.items = inv.items
const existingInv = this.r.invs.value.get(inv.path)
if (existingInv) {
existingInv.items = inv.items
if (inv.galders) {
existingInv.galders = inv.galders
}
} else {
this.r.invs.value.set(inv.path, inv)
}
if (inv.galders) {
this.r.invs.value.get(inv.path)!.galders = inv.galders
}
this.r.dirty.value = this.r.dirty.value + 1
return inv
}

View File

@ -150,16 +150,6 @@ class Count implements ColumnInfo {
}
}
class Move implements ColumnInfo {
name: ColumnName = 'Move'
displayName = 'Target'
writable = true
options = getMoveTargets
getter(_item: TricksterItem): string | number {
return '---------------------------------------------'
}
}
const getMoveTargets = (invs: string[]): string[] => {
const out: string[] = []
for (const k of invs) {
@ -171,6 +161,16 @@ const getMoveTargets = (invs: string[]): string[] => {
return out
}
class Move implements ColumnInfo {
name: ColumnName = 'Move'
displayName = 'Target'
writable = true
options = getMoveTargets
getter(_item: TricksterItem): string | number {
return '---------------------------------------------'
}
}
class MoveCount implements ColumnInfo {
name: ColumnName = 'MoveCount'
displayName = 'Move #'

View File

@ -33,8 +33,7 @@ export interface Session {
request: (verb: Method, url: string, data: any, c?: EndpointCreator) => Promise<any>
}
export class LoginHelper {
static login = async (user: string, pass: string): Promise<TokenSession> => {
export const login = async (user: string, pass: string): Promise<TokenSession> => {
return axios
.get(login_endpoint('login'), {
withCredentials: false,
@ -62,26 +61,34 @@ export class LoginHelper {
.catch(e => {
if (e instanceof AxiosError) {
if (e.code === 'ERR_BAD_REQUEST') {
throw 'invalid username/password'
throw new Error('invalid username/password')
}
throw e.message
throw new Error(e.message)
}
throw e
})
}
static info = async (): Promise<TricksterAccountInfo> => {
export const getAccountInfo = async (): Promise<TricksterAccountInfo> => {
return axios
.get(raw_endpoint('settings/info'), { withCredentials: false })
.then((ans: AxiosResponse) => {
return ans.data
})
}
static logout = async (): Promise<void> => {
export const logout = async (): Promise<void> => {
return axios
.get(login_endpoint('logout'), { withCredentials: false })
.catch(() => {})
.then(() => {})
}
// Keep LoginHelper for backwards compatibility
export const LoginHelper = {
login,
info: getAccountInfo,
logout,
}
export class TokenSession implements Session {
@ -91,7 +98,7 @@ export class TokenSession implements Session {
data: any,
c: EndpointCreator = api_endpoint,
): Promise<AxiosResponse> => {
let promise
let promise: Promise<AxiosResponse>
switch (verb.toLowerCase()) {
case 'post':
promise = axios.post(c(url), data, this.genHeaders())

View File

@ -20,7 +20,8 @@ const columns = {
return c[0].has(row.original.item.id)
}, [c])
return (
<div
<button
type="button"
className={`no-select flex flex-row ${row.original.status?.selected ? 'animate-pulse' : ''}`}
onClick={_e => {
setItemSelection({
@ -35,7 +36,7 @@ const columns = {
className="select-none object-contain select-none"
/>
</div>
</div>
</button>
)
},
}),

View File

@ -187,7 +187,7 @@ export const currentItemSelectionAtom = atom<[Map<string, number>, number]>([
export const currentInventorySearchQueryAtom = atom('')
export const filteredCharacterItemsAtom = atom(get => {
const { items, searcher } = get(currentCharacterItemsAtom)
const { items } = get(currentCharacterItemsAtom)
const [selection] = get(currentItemSelectionAtom)
const filter = get(inventoryFilterAtom)
const out: ItemWithSelection[] = []
@ -202,7 +202,7 @@ export const filteredCharacterItemsAtom = atom(get => {
continue
}
}
let status
let status: { selected: boolean } | undefined
if (selection.has(value.id)) {
status = {
selected: true,

View File

@ -68,7 +68,7 @@ export const loadStore = () => {
export const saveStore = () => {
const store = useStoreRef()
for (const [k, v] of Object.entries(StoreReviver)) {
let coke
let coke: string | undefined
if (store[k as keyof RefStore] !== undefined) {
coke = v.Murder(store[k as keyof RefStore].value as any)
}