- Inventory: Bigger item icons (48px) filling their slots, MC-style hover tooltips - Creative inventory category tabs (All/Blocks/Tools/Combat/Food/Redstone/Materials/Misc) - Smelting recipes grouped by output item with collapsible sections - Alerts moved from full tab to popup overlay triggered from header bell icon - New Analytics tab with SVG charts (storage over time, top items, item trend lookup) - Mod icon support for ComputerCraft (CC:Tweaked) via GitHub CDN fallback - Server: new /api/history-summary endpoint for aggregate storage history - Store: fetchHistorySummary and fetchItemHistory for analytics data
185 lines
7.3 KiB
JavaScript
185 lines
7.3 KiB
JavaScript
import React, { useState, useMemo } from 'react';
|
||
import { useInventoryStore } from '../store/inventoryStore';
|
||
import { formatItemName } from '../utils/itemUtils';
|
||
import ItemIcon from './ItemIcon';
|
||
import './SmeltingPanel.css';
|
||
|
||
function SmeltingPanel() {
|
||
const inventory = useInventoryStore((state) => state.inventory);
|
||
const smeltingPaused = useInventoryStore((state) => state.smeltingPaused);
|
||
const disabledRecipes = useInventoryStore((state) => state.disabledRecipes);
|
||
const smeltable = useInventoryStore((state) => state.smeltable);
|
||
const toggleSmelting = useInventoryStore((state) => state.toggleSmelting);
|
||
const toggleRecipe = useInventoryStore((state) => state.toggleRecipe);
|
||
const enableAllRecipes = useInventoryStore((state) => state.enableAllRecipes);
|
||
const disableAllRecipes = useInventoryStore((state) => state.disableAllRecipes);
|
||
|
||
const [collapsedGroups, setCollapsedGroups] = useState({});
|
||
|
||
const furnaceStatus = inventory.furnaceStatus || {};
|
||
const furnaceEntries = Object.entries(furnaceStatus);
|
||
|
||
const smeltableEntries = Object.entries(smeltable || {});
|
||
|
||
// Group recipes by output item
|
||
const groupedRecipes = useMemo(() => {
|
||
const groups = {};
|
||
for (const [input, recipe] of smeltableEntries) {
|
||
const result = (recipe && recipe.result) || 'unknown';
|
||
if (!groups[result]) {
|
||
groups[result] = [];
|
||
}
|
||
groups[result].push({ input, recipe });
|
||
}
|
||
// Sort groups by output name
|
||
return Object.entries(groups).sort(([a], [b]) =>
|
||
formatItemName(a).localeCompare(formatItemName(b))
|
||
);
|
||
}, [smeltableEntries]);
|
||
|
||
const toggleGroup = (groupKey) => {
|
||
setCollapsedGroups((prev) => ({
|
||
...prev,
|
||
[groupKey]: !prev[groupKey],
|
||
}));
|
||
};
|
||
|
||
return (
|
||
<div className="smelting-panel">
|
||
{/* Header */}
|
||
<div className="smelting-header">
|
||
<h2>🔥 Smelting Dashboard</h2>
|
||
<div className="smelting-controls">
|
||
<button
|
||
className={`mc-btn ${smeltingPaused ? 'green' : 'red'}`}
|
||
onClick={toggleSmelting}
|
||
>
|
||
{smeltingPaused ? '▶ Resume' : '⏸ Pause'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{smeltingPaused && (
|
||
<div className="smelting-paused-banner">⚠️ Smelting is PAUSED</div>
|
||
)}
|
||
|
||
{/* Furnace status */}
|
||
<div className="furnace-section detail-section">
|
||
<h3>🔥 Furnaces ({inventory.furnaceCount || 0})</h3>
|
||
{furnaceEntries.length > 0 ? (
|
||
<div className="furnace-grid">
|
||
{furnaceEntries.map(([name, status]) => (
|
||
<div key={name} className={`furnace-card ${(status && status.active) ? 'active' : 'idle'}`}>
|
||
<div className="furnace-name">{(name || '').replace(/^minecraft:/, '')}</div>
|
||
<div className="furnace-status">
|
||
{status && status.input && (
|
||
<div className="furnace-slot">
|
||
<span className="slot-label">In:</span>
|
||
<ItemIcon itemName={status.input.name} size={16} />
|
||
<span>{status.input.count || 0}</span>
|
||
</div>
|
||
)}
|
||
{status && status.fuel && (
|
||
<div className="furnace-slot">
|
||
<span className="slot-label">Fuel:</span>
|
||
<ItemIcon itemName={status.fuel.name} size={16} />
|
||
<span>{status.fuel.count || 0}</span>
|
||
</div>
|
||
)}
|
||
{status && status.output && (
|
||
<div className="furnace-slot">
|
||
<span className="slot-label">Out:</span>
|
||
<ItemIcon itemName={status.output.name} size={16} />
|
||
<span>{status.output.count || 0}</span>
|
||
</div>
|
||
)}
|
||
{(!status || (!status.input && !status.fuel && !status.output)) && (
|
||
<span className="furnace-empty">Empty</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="no-data">No furnace data available</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Recipe list - Grouped by output */}
|
||
<div className="recipe-section detail-section">
|
||
<h3>📋 Smelting Recipes ({smeltableEntries.length})</h3>
|
||
<div className="recipe-bulk-controls">
|
||
<button className="mc-btn green" onClick={enableAllRecipes}>✅ Enable All</button>
|
||
<button className="mc-btn red" onClick={disableAllRecipes}>❌ Disable All</button>
|
||
</div>
|
||
|
||
<div className="recipe-groups">
|
||
{groupedRecipes.map(([outputName, recipes]) => {
|
||
const isCollapsed = collapsedGroups[outputName];
|
||
const enabledCount = recipes.filter((r) => !disabledRecipes[r.input]).length;
|
||
return (
|
||
<div key={outputName} className="recipe-group">
|
||
<div
|
||
className="recipe-group-header"
|
||
onClick={() => toggleGroup(outputName)}
|
||
>
|
||
<span className="recipe-group-toggle">
|
||
{isCollapsed ? '▶' : '▼'}
|
||
</span>
|
||
<ItemIcon itemName={outputName} size={24} />
|
||
<span className="recipe-group-name">
|
||
{formatItemName(outputName)}
|
||
</span>
|
||
<span className="recipe-group-count">
|
||
{enabledCount}/{recipes.length} enabled
|
||
</span>
|
||
</div>
|
||
|
||
{!isCollapsed && (
|
||
<div className="recipe-group-items">
|
||
{recipes.map(({ input, recipe }) => {
|
||
const isDisabled = disabledRecipes[input];
|
||
const furnaces = (recipe && recipe.furnaces) || [];
|
||
return (
|
||
<div
|
||
key={input}
|
||
className={`recipe-item ${isDisabled ? 'disabled' : 'enabled'}`}
|
||
onClick={() => toggleRecipe(input)}
|
||
>
|
||
<div className="recipe-toggle">
|
||
{isDisabled ? '⬜' : '✅'}
|
||
</div>
|
||
<div className="recipe-input">
|
||
<ItemIcon itemName={input} size={20} />
|
||
<span>{formatItemName(input)}</span>
|
||
</div>
|
||
<span className="recipe-arrow">→</span>
|
||
<div className="recipe-output">
|
||
<ItemIcon itemName={outputName} size={20} />
|
||
</div>
|
||
<div className="recipe-furnaces">
|
||
{furnaces.map((f) => (
|
||
<span key={f} className="furnace-tag">
|
||
{(f || '').replace(/^minecraft:/, '')}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
{groupedRecipes.length === 0 && (
|
||
<div className="no-data">No smelting recipes loaded</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default SmeltingPanel;
|