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

@@ -5,6 +5,12 @@ import './ItemIcon.css';
// Minecraft assets CDN - actual game textures (16x16 pixel art)
const MC_ASSETS_BASE = 'https://cdn.jsdelivr.net/gh/InventivetalentDev/minecraft-assets@1.21.4/assets/minecraft/textures';
// Mod texture CDN bases (GitHub raw URLs for known mod repos)
const MOD_TEXTURE_BASES = {
computercraft: 'https://raw.githubusercontent.com/cc-tweaked/CC-Tweaked/mc-1.20.x/projects/common/src/main/resources/assets/computercraft/textures',
create: 'https://raw.githubusercontent.com/Creators-of-Create/Create/mc1.20.1/dev/src/main/resources/assets/create/textures',
};
// Some items have texture names that differ from their registry name
const TEXTURE_ALIASES = {
// Crops / seeds
@@ -102,26 +108,37 @@ const BLOCK_TEXTURE_SUFFIXES = {
/**
* Attempt multiple texture URLs in order.
* 1. item/{name}.png
* 2. block/{name}.png (with optional suffix)
* 3. emoji fallback
* For vanilla: item/{name}.png → block/{name}.png
* For mods: mod repo item/ → mod repo block/ → vanilla fallback
*/
function getTextureUrls(shortName) {
function getTextureUrls(fullItemName) {
// Parse namespace and short name
const colonIdx = (fullItemName || '').indexOf(':');
const namespace = colonIdx >= 0 ? fullItemName.substring(0, colonIdx) : 'minecraft';
const shortName = colonIdx >= 0 ? fullItemName.substring(colonIdx + 1) : fullItemName;
const alias = TEXTURE_ALIASES[shortName] || shortName;
const urls = [];
// For non-minecraft mods, try mod-specific URLs first
if (namespace !== 'minecraft') {
const modBase = MOD_TEXTURE_BASES[namespace];
if (modBase) {
urls.push(`${modBase}/item/${shortName}.png`);
urls.push(`${modBase}/block/${shortName}.png`);
urls.push(`${modBase}/block/${shortName}_front.png`);
urls.push(`${modBase}/block/${shortName}_side.png`);
}
}
// Vanilla texture URLs
if (BLOCK_TEXTURES.has(shortName)) {
// Known block — try block texture first
const suffix = BLOCK_TEXTURE_SUFFIXES[shortName] || '';
urls.push(`${MC_ASSETS_BASE}/block/${alias}${suffix}.png`);
// Also try without suffix
if (suffix) urls.push(`${MC_ASSETS_BASE}/block/${alias}.png`);
}
// Always try item texture
urls.push(`${MC_ASSETS_BASE}/item/${alias}.png`);
// If not a known block, also try block as fallback
if (!BLOCK_TEXTURES.has(shortName)) {
urls.push(`${MC_ASSETS_BASE}/block/${alias}.png`);
}
@@ -134,7 +151,7 @@ const iconCache = new Map();
/**
* Renders a Minecraft item icon using the official game textures.
* Cascading fallback: item texture → block texture → emoji
* Cascading fallback: mod texture → item texture → block texture → emoji
*/
function ItemIcon({ itemName, size = 32 }) {
const [urlIndex, setUrlIndex] = useState(0);
@@ -144,11 +161,11 @@ function ItemIcon({ itemName, size = 32 }) {
return <span className="item-icon-emoji" style={{ fontSize: size * 0.7 }}>📦</span>;
}
// Strip namespace (minecraft:diamond → diamond)
const shortName = itemName.replace(/^[a-z0-9_.-]+:/, '');
// Use full item name as cache key (handles mod namespaces correctly)
const cacheKey = itemName.replace(/^minecraft:/, '');
// Check if we already know this item has no texture
if (iconCache.get(shortName) === 'none' || allFailed) {
if (iconCache.get(cacheKey) === 'none' || allFailed) {
return (
<span className="item-icon-emoji" style={{ fontSize: size * 0.7 }}>
{getItemEmoji(itemName)}
@@ -157,13 +174,13 @@ function ItemIcon({ itemName, size = 32 }) {
}
// If we have a cached URL, use it directly
const cachedUrl = iconCache.get(shortName);
const cachedUrl = iconCache.get(cacheKey);
if (cachedUrl && cachedUrl !== 'none') {
return (
<img
className="item-icon-img"
src={cachedUrl}
alt={shortName}
alt={cacheKey}
width={size}
height={size}
loading="lazy"
@@ -171,11 +188,11 @@ function ItemIcon({ itemName, size = 32 }) {
);
}
const urls = getTextureUrls(shortName);
const urls = getTextureUrls(itemName);
const currentUrl = urls[urlIndex];
if (!currentUrl) {
iconCache.set(shortName, 'none');
iconCache.set(cacheKey, 'none');
return (
<span className="item-icon-emoji" style={{ fontSize: size * 0.7 }}>
{getItemEmoji(itemName)}
@@ -187,18 +204,18 @@ function ItemIcon({ itemName, size = 32 }) {
<img
className="item-icon-img"
src={currentUrl}
alt={shortName}
alt={cacheKey}
width={size}
height={size}
loading="lazy"
onLoad={() => {
iconCache.set(shortName, currentUrl);
iconCache.set(cacheKey, currentUrl);
}}
onError={() => {
if (urlIndex + 1 < urls.length) {
setUrlIndex(urlIndex + 1);
} else {
iconCache.set(shortName, 'none');
iconCache.set(cacheKey, 'none');
setAllFailed(true);
}
}}