Major UI overhaul: bigger icons, creative inventory tabs, grouped smelting, alerts popup, analytics, mod icons
- 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
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useInventoryStore } from '../store/inventoryStore';
|
||||
import { formatItemName } from '../utils/itemUtils';
|
||||
import ItemIcon from './ItemIcon';
|
||||
@@ -14,11 +14,36 @@ function SmeltingPanel() {
|
||||
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 */}
|
||||
@@ -80,47 +105,74 @@ function SmeltingPanel() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recipe list */}
|
||||
{/* 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-list">
|
||||
{smeltableEntries.map(([input, recipe]) => {
|
||||
const isDisabled = disabledRecipes[input];
|
||||
const result = (recipe && recipe.result) || '';
|
||||
const furnaces = (recipe && recipe.furnaces) || [];
|
||||
|
||||
<div className="recipe-groups">
|
||||
{groupedRecipes.map(([outputName, recipes]) => {
|
||||
const isCollapsed = collapsedGroups[outputName];
|
||||
const enabledCount = recipes.filter((r) => !disabledRecipes[r.input]).length;
|
||||
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={result} size={20} />
|
||||
<span>{formatItemName(result)}</span>
|
||||
</div>
|
||||
<div className="recipe-furnaces">
|
||||
{furnaces.map((f) => (
|
||||
<span key={f} className="furnace-tag">
|
||||
{(f || '').replace(/^minecraft:/, '')}
|
||||
</span>
|
||||
))}
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
{smeltableEntries.length === 0 && (
|
||||
{groupedRecipes.length === 0 && (
|
||||
<div className="no-data">No smelting recipes loaded</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user