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:
MayaTheShy
2026-03-21 19:07:17 -04:00
parent c25ef9f2cc
commit 29498a2f6a
15 changed files with 1244 additions and 150 deletions

View File

@@ -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>