Add inventory store with WebSocket and REST API integration
This commit is contained in:
210
web/client/src/store/inventoryStore.js
Normal file
210
web/client/src/store/inventoryStore.js
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
const WS_URL = import.meta.env.VITE_WS_URL ||
|
||||||
|
`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`;
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL ||
|
||||||
|
`${window.location.protocol}//${window.location.host}/api`;
|
||||||
|
|
||||||
|
console.log('🔌 WebSocket URL:', WS_URL);
|
||||||
|
console.log('📡 API URL:', API_URL);
|
||||||
|
|
||||||
|
export const useInventoryStore = create((set, get) => ({
|
||||||
|
// State
|
||||||
|
inventory: {
|
||||||
|
itemList: [],
|
||||||
|
grandTotal: 0,
|
||||||
|
chestCount: 0,
|
||||||
|
totalSlots: 0,
|
||||||
|
usedSlots: 0,
|
||||||
|
freeSlots: 0,
|
||||||
|
usedRatio: 0,
|
||||||
|
dropperOk: false,
|
||||||
|
barrelOk: false,
|
||||||
|
furnaceCount: 0,
|
||||||
|
furnaceStatus: {},
|
||||||
|
},
|
||||||
|
activity: {},
|
||||||
|
alerts: [],
|
||||||
|
smeltingPaused: false,
|
||||||
|
disabledRecipes: {},
|
||||||
|
smeltable: {},
|
||||||
|
craftable: [],
|
||||||
|
craftTurtleOk: false,
|
||||||
|
connected: false,
|
||||||
|
ws: null,
|
||||||
|
lastUpdate: 0,
|
||||||
|
searchQuery: '',
|
||||||
|
commandResult: null,
|
||||||
|
|
||||||
|
// WebSocket connection
|
||||||
|
connect: () => {
|
||||||
|
const ws = new WebSocket(WS_URL);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('✅ Connected to server');
|
||||||
|
set({ connected: true, ws });
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data.type === 'initial_state') {
|
||||||
|
set({
|
||||||
|
inventory: data.inventory || get().inventory,
|
||||||
|
activity: data.activity || {},
|
||||||
|
alerts: data.alerts || [],
|
||||||
|
smeltingPaused: data.smeltingPaused || false,
|
||||||
|
disabledRecipes: data.disabledRecipes || {},
|
||||||
|
smeltable: data.smeltable || {},
|
||||||
|
craftable: data.craftable || [],
|
||||||
|
craftTurtleOk: data.craftTurtleOk || false,
|
||||||
|
lastUpdate: data.lastUpdate || Date.now(),
|
||||||
|
});
|
||||||
|
} else if (data.type === 'state_update') {
|
||||||
|
set({
|
||||||
|
inventory: data.inventory || get().inventory,
|
||||||
|
activity: data.activity || get().activity,
|
||||||
|
alerts: data.alerts || get().alerts,
|
||||||
|
smeltingPaused: data.smeltingPaused !== undefined ? data.smeltingPaused : get().smeltingPaused,
|
||||||
|
disabledRecipes: data.disabledRecipes || get().disabledRecipes,
|
||||||
|
craftTurtleOk: data.craftTurtleOk !== undefined ? data.craftTurtleOk : get().craftTurtleOk,
|
||||||
|
lastUpdate: data.lastUpdate || Date.now(),
|
||||||
|
});
|
||||||
|
} else if (data.type === 'command_result') {
|
||||||
|
set({ commandResult: data });
|
||||||
|
// Clear after 5s
|
||||||
|
setTimeout(() => {
|
||||||
|
set((state) => {
|
||||||
|
if (state.commandResult === data) {
|
||||||
|
return { commandResult: null };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing message:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('❌ Disconnected from server');
|
||||||
|
set({ connected: false, ws: null });
|
||||||
|
setTimeout(() => {
|
||||||
|
get().connect();
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Search
|
||||||
|
setSearchQuery: (query) => set({ searchQuery: query }),
|
||||||
|
|
||||||
|
getFilteredItems: () => {
|
||||||
|
const { inventory, searchQuery } = get();
|
||||||
|
const items = inventory.itemList || [];
|
||||||
|
if (!searchQuery) return items;
|
||||||
|
const q = searchQuery.toLowerCase();
|
||||||
|
return items.filter((item) => {
|
||||||
|
const name = (item.displayName || item.name || '').toLowerCase();
|
||||||
|
return name.includes(q);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Actions via REST API
|
||||||
|
orderItem: async (itemName, amount, dropperName) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/order`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ itemName, amount, dropperName }),
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error ordering item:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
requestScan: async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/scan`, { method: 'POST' });
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error requesting scan:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleSmelting: async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/smelting/toggle`, { method: 'POST' });
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error toggling smelting:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleRecipe: async (recipe) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/recipes/toggle`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ recipe }),
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error toggling recipe:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
enableAllRecipes: async () => {
|
||||||
|
try {
|
||||||
|
await fetch(`${API_URL}/recipes/enable-all`, { method: 'POST' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error enabling all recipes:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
disableAllRecipes: async () => {
|
||||||
|
try {
|
||||||
|
await fetch(`${API_URL}/recipes/disable-all`, { method: 'POST' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error disabling all recipes:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
craftItem: async (recipeIdx) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/craft`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ recipeIdx }),
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error crafting item:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sortBarrel: async (barrelName) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/sort-barrel`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ barrelName }),
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error sorting barrel:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user