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,6 +1,6 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { useInventoryStore } from '../store/inventoryStore';
|
||||
import { formatItemName, getItemEmoji, formatCount } from '../utils/itemUtils';
|
||||
import { formatItemName, getItemEmoji, formatCount, ITEM_CATEGORIES, getItemCategory } from '../utils/itemUtils';
|
||||
import ItemIcon from './ItemIcon';
|
||||
import './InventoryGrid.css';
|
||||
|
||||
@@ -13,15 +13,23 @@ function InventoryGrid() {
|
||||
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [orderAmount, setOrderAmount] = useState(1);
|
||||
const [sortBy, setSortBy] = useState('count'); // 'count', 'name'
|
||||
const [sortBy, setSortBy] = useState('count');
|
||||
const [activeCategory, setActiveCategory] = useState('all');
|
||||
|
||||
const items = getFilteredItems();
|
||||
|
||||
const sortedItems = [...items].sort((a, b) => {
|
||||
if (sortBy === 'count') return (b.count || 0) - (a.count || 0);
|
||||
if (sortBy === 'name') return (a.displayName || a.name || '').localeCompare(b.displayName || b.name || '');
|
||||
return 0;
|
||||
});
|
||||
const filteredByCategory = useMemo(() => {
|
||||
if (activeCategory === 'all') return items;
|
||||
return items.filter((item) => getItemCategory(item.name) === activeCategory);
|
||||
}, [items, activeCategory]);
|
||||
|
||||
const sortedItems = useMemo(() => {
|
||||
return [...filteredByCategory].sort((a, b) => {
|
||||
if (sortBy === 'count') return (b.count || 0) - (a.count || 0);
|
||||
if (sortBy === 'name') return (a.displayName || a.name || '').localeCompare(b.displayName || b.name || '');
|
||||
return 0;
|
||||
});
|
||||
}, [filteredByCategory, sortBy]);
|
||||
|
||||
const handleOrder = useCallback(async () => {
|
||||
if (!selectedItem || orderAmount <= 0) return;
|
||||
@@ -33,6 +41,17 @@ function InventoryGrid() {
|
||||
setOrderAmount(1);
|
||||
};
|
||||
|
||||
// Count items per category for badges
|
||||
const categoryCounts = useMemo(() => {
|
||||
const counts = { all: items.length };
|
||||
for (const cat of ITEM_CATEGORIES) {
|
||||
if (cat.id !== 'all') {
|
||||
counts[cat.id] = items.filter((item) => getItemCategory(item.name) === cat.id).length;
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}, [items]);
|
||||
|
||||
return (
|
||||
<div className="inventory-panel">
|
||||
{/* Search & controls bar */}
|
||||
@@ -69,6 +88,24 @@ function InventoryGrid() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Creative inventory category tabs */}
|
||||
<div className="category-tabs">
|
||||
{ITEM_CATEGORIES.map((cat) => (
|
||||
<button
|
||||
key={cat.id}
|
||||
className={`category-tab ${activeCategory === cat.id ? 'active' : ''}`}
|
||||
onClick={() => setActiveCategory(cat.id)}
|
||||
title={cat.label}
|
||||
>
|
||||
<ItemIcon itemName={`minecraft:${cat.icon}`} size={20} />
|
||||
<span className="category-label">{cat.label}</span>
|
||||
{categoryCounts[cat.id] > 0 && (
|
||||
<span className="category-count">{categoryCounts[cat.id]}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="inventory-body">
|
||||
{/* Item grid */}
|
||||
<div className="item-grid-wrapper">
|
||||
@@ -78,16 +115,20 @@ function InventoryGrid() {
|
||||
key={item.name}
|
||||
className={`item-slot ${selectedItem?.name === item.name ? 'selected' : ''}`}
|
||||
onClick={() => handleItemClick(item)}
|
||||
title={`${formatItemName(item.name)}\n${item.count} total`}
|
||||
>
|
||||
<ItemIcon itemName={item.name} size={32} />
|
||||
<ItemIcon itemName={item.name} size={48} />
|
||||
<span className="item-slot-count">{formatCount(item.count)}</span>
|
||||
<span className="item-slot-name">{formatItemName(item.name)}</span>
|
||||
{/* Minecraft-style tooltip */}
|
||||
<div className="mc-tooltip">
|
||||
<div className="mc-tooltip-name">{formatItemName(item.name)}</div>
|
||||
<div className="mc-tooltip-count">{(item.count || 0).toLocaleString()} items</div>
|
||||
<div className="mc-tooltip-id">{item.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{sortedItems.length === 0 && (
|
||||
<div className="empty-grid">
|
||||
{searchQuery ? `No items matching "${searchQuery}"` : 'No items in storage'}
|
||||
{searchQuery ? `No items matching "${searchQuery}"` : activeCategory !== 'all' ? 'No items in this category' : 'No items in storage'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user