- 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
93 lines
3.4 KiB
JavaScript
93 lines
3.4 KiB
JavaScript
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({ 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-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>
|
|
|
|
<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>
|
|
) : (
|
|
<div className="no-alerts">
|
|
<span className="no-alerts-icon">✅</span>
|
|
<span>All stock levels are OK</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default AlertsPanel;
|