165 lines
5.7 KiB
JavaScript
165 lines
5.7 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
||
import InventoryGrid from './components/InventoryGrid';
|
||
import StorageOverview from './components/StorageOverview';
|
||
import SmeltingPanel from './components/SmeltingPanel';
|
||
import CraftingPanel from './components/CraftingPanel';
|
||
import AlertsPanel from './components/AlertsPanel';
|
||
import AnalyticsPanel from './components/AnalyticsPanel';
|
||
import SettingsPanel from './components/SettingsPanel';
|
||
import ErrorBoundary from './components/ErrorBoundary';
|
||
import { useInventoryStore } from './store/inventoryStore';
|
||
import './App.css';
|
||
|
||
function App() {
|
||
const connect = useInventoryStore((state) => state.connect);
|
||
const connected = useInventoryStore((state) => state.connected);
|
||
const bridgeConnected = useInventoryStore((state) => state.bridgeConnected);
|
||
const lastUpdate = useInventoryStore((state) => state.lastUpdate);
|
||
const commandResult = useInventoryStore((state) => state.commandResult);
|
||
const alerts = useInventoryStore((state) => state.alerts) || [];
|
||
const [panelTab, setPanelTab] = useState('inventory');
|
||
const [showAlerts, setShowAlerts] = useState(false);
|
||
const [showSettings, setShowSettings] = useState(false);
|
||
const [, forceRender] = useState(0);
|
||
|
||
const turtleDashboardUrl = import.meta.env.VITE_TURTLE_DASHBOARD_URL || 'https://turtles.spatulaa.com';
|
||
|
||
useEffect(() => {
|
||
connect();
|
||
}, [connect]);
|
||
|
||
// Re-render every 5s so the "last updated" text stays fresh
|
||
useEffect(() => {
|
||
const id = setInterval(() => forceRender((n) => n + 1), 5000);
|
||
return () => clearInterval(id);
|
||
}, []);
|
||
|
||
const staleSecs = lastUpdate ? Math.floor((Date.now() - lastUpdate) / 1000) : null;
|
||
|
||
const renderPanelContent = () => {
|
||
switch (panelTab) {
|
||
case 'inventory':
|
||
return <InventoryGrid />;
|
||
case 'smelting':
|
||
return <SmeltingPanel />;
|
||
case 'crafting':
|
||
return <CraftingPanel />;
|
||
case 'analytics':
|
||
return <AnalyticsPanel />;
|
||
default:
|
||
return <InventoryGrid />;
|
||
}
|
||
};
|
||
|
||
const triggeredAlerts = alerts.filter((a) => a.triggered !== false);
|
||
|
||
return (
|
||
<div className="app">
|
||
<div className="app-header">
|
||
<h1>⛏️ Inventory Manager</h1>
|
||
<div className="header-right">
|
||
{/* Cross-link to Turtle Dashboard */}
|
||
<a
|
||
href={turtleDashboardUrl}
|
||
className="cross-link-btn"
|
||
title="Open Turtle Control Dashboard"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
>
|
||
🐢 Turtles
|
||
</a>
|
||
{/* Settings gear button */}
|
||
<button
|
||
className="settings-gear"
|
||
onClick={() => setShowSettings(!showSettings)}
|
||
title="Settings"
|
||
>
|
||
⚙️
|
||
</button>
|
||
{/* Alerts bell button */}
|
||
<button
|
||
className={`alerts-bell ${triggeredAlerts.length > 0 ? 'has-alerts' : ''}`}
|
||
onClick={() => setShowAlerts(!showAlerts)}
|
||
title="Low-stock alerts"
|
||
>
|
||
🔔
|
||
{triggeredAlerts.length > 0 && (
|
||
<span className="alerts-bell-badge">{triggeredAlerts.length}</span>
|
||
)}
|
||
</button>
|
||
<div className={`connection-status ${connected ? 'connected' : 'disconnected'}`}>
|
||
<span className="status-dot"></span>
|
||
{connected ? 'Connected' : 'Disconnected'}
|
||
</div>
|
||
{connected && (
|
||
<div className={`connection-status ${bridgeConnected ? 'connected' : 'disconnected'}`} title="Minecraft CC:Tweaked bridge">
|
||
<span className="status-dot"></span>
|
||
{bridgeConnected ? 'Bridge OK' : 'Bridge Off'}
|
||
</div>
|
||
)}
|
||
{staleSecs !== null && staleSecs > 0 && (
|
||
<span className="last-update-text" title="Time since last data update">
|
||
{staleSecs < 60 ? `${staleSecs}s ago` : `${Math.floor(staleSecs / 60)}m ago`}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{commandResult && (
|
||
<div className={`command-toast ${commandResult.success ? 'success' : 'error'}`}>
|
||
{commandResult.message || commandResult.error || (commandResult.success ? 'OK' : 'Failed')}
|
||
</div>
|
||
)}
|
||
|
||
{/* Alerts popup overlay */}
|
||
<AlertsPanel isOpen={showAlerts} onClose={() => setShowAlerts(false)} />
|
||
|
||
{/* Settings overlay */}
|
||
<SettingsPanel isOpen={showSettings} onClose={() => setShowSettings(false)} />
|
||
|
||
<div className="app-content">
|
||
<div className="sidebar">
|
||
<ErrorBoundary>
|
||
<StorageOverview />
|
||
</ErrorBoundary>
|
||
</div>
|
||
<div className="main-panel">
|
||
<div className="panel-tabs">
|
||
<button
|
||
className={panelTab === 'inventory' ? 'active' : ''}
|
||
onClick={() => setPanelTab('inventory')}
|
||
>
|
||
📦 Inventory
|
||
</button>
|
||
<button
|
||
className={panelTab === 'smelting' ? 'active' : ''}
|
||
onClick={() => setPanelTab('smelting')}
|
||
>
|
||
🔥 Smelting
|
||
</button>
|
||
<button
|
||
className={panelTab === 'crafting' ? 'active' : ''}
|
||
onClick={() => setPanelTab('crafting')}
|
||
>
|
||
🔨 Crafting
|
||
</button>
|
||
<button
|
||
className={panelTab === 'analytics' ? 'active' : ''}
|
||
onClick={() => setPanelTab('analytics')}
|
||
>
|
||
📊 Analytics
|
||
</button>
|
||
</div>
|
||
<div className="panel-content-wrapper">
|
||
<ErrorBoundary>
|
||
{renderPanelContent()}
|
||
</ErrorBoundary>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default App;
|