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,60 +1,90 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useInventoryStore } from '../store/inventoryStore';
|
||||
import { formatItemName } from '../utils/itemUtils';
|
||||
import ItemIcon from './ItemIcon';
|
||||
import './AlertsPanel.css';
|
||||
|
||||
function AlertsPanel() {
|
||||
function AlertsPanel({ isOpen, onClose }) {
|
||||
const alerts = useInventoryStore((state) => state.alerts) || [];
|
||||
const panelRef = useRef(null);
|
||||
|
||||
// Close on click outside
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const handleClickOutside = (e) => {
|
||||
if (panelRef.current && !panelRef.current.contains(e.target)) {
|
||||
// Check if the click was on the bell button (has alerts-bell class)
|
||||
if (e.target.closest('.alerts-bell')) return;
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// Close on Escape
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const handleEsc = (e) => { if (e.key === 'Escape') onClose(); };
|
||||
document.addEventListener('keydown', handleEsc);
|
||||
return () => document.removeEventListener('keydown', handleEsc);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="alerts-panel">
|
||||
<div className="alerts-header">
|
||||
<h2>🔔 Low-Stock Alerts</h2>
|
||||
<span className="alert-count">
|
||||
{alerts.length} alert{alerts.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="alerts-overlay" ref={panelRef}>
|
||||
<div className="alerts-popup">
|
||||
<div className="alerts-popup-header">
|
||||
<h3>🔔 Low-Stock Alerts</h3>
|
||||
<span className="alert-count">
|
||||
{alerts.length} alert{alerts.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<button className="alerts-close" onClick={onClose}>✕</button>
|
||||
</div>
|
||||
|
||||
{alerts.length > 0 ? (
|
||||
<div className="alert-list">
|
||||
{alerts.map((alert, idx) => {
|
||||
const itemName = alert.item || alert.name || alert.label || '';
|
||||
const isTriggered = alert.triggered !== undefined ? alert.triggered : true;
|
||||
const current = alert.current ?? '?';
|
||||
const threshold = alert.threshold || alert.min || '?';
|
||||
return (
|
||||
<div key={idx} className={`alert-card ${isTriggered ? 'triggered' : 'ok'}`}>
|
||||
<div className="alert-icon">
|
||||
{isTriggered ? '🔴' : '🟢'}
|
||||
</div>
|
||||
<div className="alert-info">
|
||||
<div className="alert-item-row">
|
||||
<ItemIcon itemName={itemName} size={20} />
|
||||
<span className="alert-item-name">{formatItemName(itemName)}</span>
|
||||
<div className="alerts-popup-body">
|
||||
{alerts.length > 0 ? (
|
||||
<div className="alert-list">
|
||||
{alerts.map((alert, idx) => {
|
||||
const itemName = alert.item || alert.name || alert.label || '';
|
||||
const isTriggered = alert.triggered !== undefined ? alert.triggered : true;
|
||||
const current = alert.current ?? '?';
|
||||
const threshold = alert.threshold || alert.min || '?';
|
||||
return (
|
||||
<div key={idx} className={`alert-card ${isTriggered ? 'triggered' : 'ok'}`}>
|
||||
<div className="alert-icon">
|
||||
{isTriggered ? '🔴' : '🟢'}
|
||||
</div>
|
||||
<div className="alert-info">
|
||||
<div className="alert-item-row">
|
||||
<ItemIcon itemName={itemName} size={20} />
|
||||
<span className="alert-item-name">{formatItemName(itemName)}</span>
|
||||
</div>
|
||||
<div className="alert-details">
|
||||
<span className="alert-stock">
|
||||
Stock: <strong>{current}</strong>
|
||||
</span>
|
||||
<span className="alert-threshold">
|
||||
Min: <strong>{threshold}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`alert-badge ${isTriggered ? 'low' : 'ok'}`}>
|
||||
{isTriggered ? 'LOW' : 'OK'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="alert-details">
|
||||
<span className="alert-stock">
|
||||
Stock: <strong>{current}</strong>
|
||||
</span>
|
||||
<span className="alert-threshold">
|
||||
Min: <strong>{threshold}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`alert-badge ${isTriggered ? 'low' : 'ok'}`}>
|
||||
{isTriggered ? 'LOW' : 'OK'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="no-alerts">
|
||||
<span className="no-alerts-icon">✅</span>
|
||||
<span>All stock levels are OK</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="no-alerts">
|
||||
<span className="no-alerts-icon">✅</span>
|
||||
<span>No alerts configured or all stock levels are OK</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user