231 lines
9.4 KiB
JavaScript
231 lines
9.4 KiB
JavaScript
import React, { useState, useCallback, useMemo } from 'react';
|
|
import { useInventoryStore } from '../store/inventoryStore';
|
|
import { formatItemName, getItemEmoji, formatCount, ITEM_CATEGORIES, getItemCategory } from '../utils/itemUtils';
|
|
import ItemIcon from './ItemIcon';
|
|
import './InventoryGrid.css';
|
|
|
|
function InventoryGrid() {
|
|
const inventory = useInventoryStore((state) => state.inventory);
|
|
const searchQuery = useInventoryStore((state) => state.searchQuery);
|
|
const setSearchQuery = useInventoryStore((state) => state.setSearchQuery);
|
|
const getFilteredItems = useInventoryStore((state) => state.getFilteredItems);
|
|
const orderItem = useInventoryStore((state) => state.orderItem);
|
|
const dropperNicknames = useInventoryStore((state) => state.dropperNicknames) || {};
|
|
|
|
const [selectedItemName, setSelectedItemName] = useState(null);
|
|
const [orderAmount, setOrderAmount] = useState(1);
|
|
const [sortBy, setSortBy] = useState('count');
|
|
const [activeCategory, setActiveCategory] = useState('all');
|
|
const [selectedDropper, setSelectedDropper] = useState('');
|
|
|
|
// Derive selectedItem from live inventory so count always reflects real-time data
|
|
const selectedItem = useMemo(() => {
|
|
if (!selectedItemName) return null;
|
|
const items = inventory.itemList || [];
|
|
return items.find((i) => i.name === selectedItemName) || null;
|
|
}, [selectedItemName, inventory.itemList]);
|
|
|
|
const droppers = inventory.droppers || [];
|
|
|
|
const items = getFilteredItems();
|
|
|
|
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;
|
|
await orderItem(selectedItem.name, orderAmount, selectedDropper || undefined);
|
|
}, [selectedItem, orderAmount, orderItem, selectedDropper]);
|
|
|
|
const handleItemClick = (item) => {
|
|
setSelectedItemName(item.name);
|
|
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 */}
|
|
<div className="inventory-toolbar">
|
|
<div className="search-box">
|
|
<span className="search-icon">🔍</span>
|
|
<input
|
|
type="text"
|
|
placeholder="Search items..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="search-input"
|
|
/>
|
|
{searchQuery && (
|
|
<button className="search-clear" onClick={() => setSearchQuery('')}>✕</button>
|
|
)}
|
|
</div>
|
|
<div className="sort-controls">
|
|
<button
|
|
className={`mc-btn ${sortBy === 'count' ? 'green' : ''}`}
|
|
onClick={() => setSortBy('count')}
|
|
>
|
|
# Count
|
|
</button>
|
|
<button
|
|
className={`mc-btn ${sortBy === 'name' ? 'green' : ''}`}
|
|
onClick={() => setSortBy('name')}
|
|
>
|
|
A Name
|
|
</button>
|
|
</div>
|
|
<div className="item-count-label">
|
|
{sortedItems.length} types · {(inventory.grandTotal || 0).toLocaleString()} items
|
|
</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">
|
|
<div className="item-grid">
|
|
{sortedItems.map((item) => (
|
|
<div
|
|
key={item.name}
|
|
className={`item-slot ${selectedItem?.name === item.name ? 'selected' : ''}`}
|
|
onClick={() => handleItemClick(item)}
|
|
>
|
|
<ItemIcon itemName={item.name} size={48} />
|
|
<span className="item-slot-count">{formatCount(item.count)}</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}"` : activeCategory !== 'all' ? 'No items in this category' : 'No items in storage'}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Item detail / order panel */}
|
|
{selectedItem && (
|
|
<div className="item-detail-panel">
|
|
<div className="detail-header">
|
|
<ItemIcon itemName={selectedItem.name || ''} size={48} />
|
|
<div className="detail-info">
|
|
<h3>{formatItemName(selectedItem.name || '')}</h3>
|
|
<span className="detail-id">{selectedItem.name || ''}</span>
|
|
</div>
|
|
<button className="detail-close" onClick={() => setSelectedItemName(null)}>✕</button>
|
|
</div>
|
|
|
|
<div className="detail-stats">
|
|
<div className="detail-stat">
|
|
<span className="label">Total</span>
|
|
<span className="value">{(selectedItem.count || 0).toLocaleString()}</span>
|
|
</div>
|
|
<div className="detail-stat">
|
|
<span className="label">Stacks</span>
|
|
<span className="value">
|
|
{Math.ceil((selectedItem.count || 0) / 64)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="order-section">
|
|
<h4>📤 Order Items</h4>
|
|
<div className="order-controls">
|
|
{droppers.length >= 1 && (
|
|
<div className="dropper-select-wrapper">
|
|
<label className="dropper-label">📍 Dispense to:</label>
|
|
<select
|
|
className="dropper-select"
|
|
value={selectedDropper}
|
|
onChange={(e) => setSelectedDropper(e.target.value)}
|
|
>
|
|
{droppers.map((d) => {
|
|
const nick = dropperNicknames[d.name];
|
|
const shortName = d.name.replace(/^minecraft:/, '');
|
|
const label = nick
|
|
? `${nick} (${shortName})`
|
|
: `${shortName}${d.isDefault ? ' (default)' : d.clientId ? ` (client ${d.clientId})` : ''}`;
|
|
return (
|
|
<option key={d.name} value={d.name}>
|
|
{label}
|
|
</option>
|
|
);
|
|
})}
|
|
</select>
|
|
</div>
|
|
)}
|
|
<div className="order-amount-controls">
|
|
<button className="mc-btn" onClick={() => setOrderAmount(Math.max(1, orderAmount - 1))}>-</button>
|
|
<input
|
|
type="number"
|
|
className="order-input"
|
|
value={orderAmount}
|
|
onChange={(e) => setOrderAmount(Math.max(1, parseInt(e.target.value) || 1))}
|
|
min="1"
|
|
max={selectedItem.count || 1}
|
|
/>
|
|
<button className="mc-btn" onClick={() => setOrderAmount(Math.min(selectedItem.count || 1, orderAmount + 1))}>+</button>
|
|
</div>
|
|
<div className="order-presets">
|
|
<button className="mc-btn" onClick={() => setOrderAmount(1)}>1</button>
|
|
<button className="mc-btn" onClick={() => setOrderAmount(16)}>16</button>
|
|
<button className="mc-btn" onClick={() => setOrderAmount(32)}>32</button>
|
|
<button className="mc-btn" onClick={() => setOrderAmount(64)}>64</button>
|
|
<button className="mc-btn" onClick={() => setOrderAmount(selectedItem.count || 1)}>All</button>
|
|
</div>
|
|
<button className="mc-btn green order-btn" onClick={handleOrder}>
|
|
📤 Dispense x{orderAmount}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default InventoryGrid;
|