Harden components: add ErrorBoundary, null-safe rendering

This commit is contained in:
MayaTheShy
2026-03-21 17:51:07 -04:00
parent 0ce63bacd7
commit fe6ac23329
6 changed files with 109 additions and 42 deletions

View File

@@ -4,6 +4,7 @@ import StorageOverview from './components/StorageOverview';
import SmeltingPanel from './components/SmeltingPanel'; import SmeltingPanel from './components/SmeltingPanel';
import CraftingPanel from './components/CraftingPanel'; import CraftingPanel from './components/CraftingPanel';
import AlertsPanel from './components/AlertsPanel'; import AlertsPanel from './components/AlertsPanel';
import ErrorBoundary from './components/ErrorBoundary';
import { useInventoryStore } from './store/inventoryStore'; import { useInventoryStore } from './store/inventoryStore';
import './App.css'; import './App.css';
@@ -50,7 +51,9 @@ function App() {
<div className="app-content"> <div className="app-content">
<div className="sidebar"> <div className="sidebar">
<StorageOverview /> <ErrorBoundary>
<StorageOverview />
</ErrorBoundary>
</div> </div>
<div className="main-panel"> <div className="main-panel">
<div className="panel-tabs"> <div className="panel-tabs">
@@ -80,7 +83,9 @@ function App() {
</button> </button>
</div> </div>
<div className="panel-content-wrapper"> <div className="panel-content-wrapper">
{renderPanelContent()} <ErrorBoundary>
{renderPanelContent()}
</ErrorBoundary>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -5,7 +5,7 @@ import ItemIcon from './ItemIcon';
import './AlertsPanel.css'; import './AlertsPanel.css';
function AlertsPanel() { function AlertsPanel() {
const alerts = useInventoryStore((state) => state.alerts); const alerts = useInventoryStore((state) => state.alerts) || [];
return ( return (
<div className="alerts-panel"> <div className="alerts-panel">
@@ -18,30 +18,36 @@ function AlertsPanel() {
{alerts.length > 0 ? ( {alerts.length > 0 ? (
<div className="alert-list"> <div className="alert-list">
{alerts.map((alert, idx) => ( {alerts.map((alert, idx) => {
<div key={idx} className={`alert-card ${alert.triggered ? 'triggered' : 'ok'}`}> const itemName = alert.item || alert.name || alert.label || '';
<div className="alert-icon"> const isTriggered = alert.triggered !== undefined ? alert.triggered : true;
{alert.triggered ? '🔴' : '🟢'} const current = alert.current ?? '?';
</div> const threshold = alert.threshold || alert.min || '?';
<div className="alert-info"> return (
<div className="alert-item-row"> <div key={idx} className={`alert-card ${isTriggered ? 'triggered' : 'ok'}`}>
<ItemIcon itemName={alert.item} size={20} /> <div className="alert-icon">
<span className="alert-item-name">{formatItemName(alert.item)}</span> {isTriggered ? '🔴' : '🟢'}
</div> </div>
<div className="alert-details"> <div className="alert-info">
<span className="alert-stock"> <div className="alert-item-row">
Stock: <strong>{alert.current ?? '?'}</strong> <ItemIcon itemName={itemName} size={20} />
</span> <span className="alert-item-name">{formatItemName(itemName)}</span>
<span className="alert-threshold"> </div>
Min: <strong>{alert.threshold || alert.min || '?'}</strong> <div className="alert-details">
</span> <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>
<div className={`alert-badge ${alert.triggered ? 'low' : 'ok'}`}> );
{alert.triggered ? 'LOW' : 'OK'} })}
</div>
</div>
))}
</div> </div>
) : ( ) : (
<div className="no-alerts"> <div className="no-alerts">

View File

@@ -35,9 +35,9 @@ function CraftingPanel() {
{(craftable || []).map((recipe, idx) => ( {(craftable || []).map((recipe, idx) => (
<div key={idx} className="craft-card"> <div key={idx} className="craft-card">
<div className="craft-output"> <div className="craft-output">
<ItemIcon itemName={recipe.output} size={32} /> <ItemIcon itemName={recipe.output || ''} size={32} />
<div className="craft-info"> <div className="craft-info">
<span className="craft-name">{formatItemName(recipe.output)}</span> <span className="craft-name">{formatItemName(recipe.output || '')}</span>
<span className="craft-count">Produces {recipe.count || 1}</span> <span className="craft-count">Produces {recipe.count || 1}</span>
</div> </div>
</div> </div>
@@ -45,8 +45,8 @@ function CraftingPanel() {
<div className="craft-ingredients"> <div className="craft-ingredients">
{recipe.slots && Object.entries(recipe.slots).map(([slot, item]) => ( {recipe.slots && Object.entries(recipe.slots).map(([slot, item]) => (
<div key={slot} className="craft-ingredient"> <div key={slot} className="craft-ingredient">
<ItemIcon itemName={item} size={16} /> <ItemIcon itemName={item || ''} size={16} />
<span>{formatItemName(item)}</span> <span>{formatItemName(item || '')}</span>
</div> </div>
))} ))}
</div> </div>

View File

@@ -0,0 +1,54 @@
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('ErrorBoundary caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div style={{
padding: '2rem',
background: '#1a1a2e',
color: '#ff6b6b',
borderRadius: '8px',
margin: '1rem',
fontFamily: 'monospace',
}}>
<h2> Something went wrong</h2>
<p style={{ color: '#ccc' }}>
{this.state.error?.message || 'Unknown error'}
</p>
<button
onClick={() => this.setState({ hasError: false, error: null })}
style={{
padding: '0.5rem 1rem',
background: '#4a7c59',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontFamily: 'inherit',
}}
>
🔄 Try Again
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -97,10 +97,10 @@ function InventoryGrid() {
{selectedItem && ( {selectedItem && (
<div className="item-detail-panel"> <div className="item-detail-panel">
<div className="detail-header"> <div className="detail-header">
<ItemIcon itemName={selectedItem.name} size={48} /> <ItemIcon itemName={selectedItem.name || ''} size={48} />
<div className="detail-info"> <div className="detail-info">
<h3>{formatItemName(selectedItem.name)}</h3> <h3>{formatItemName(selectedItem.name || '')}</h3>
<span className="detail-id">{selectedItem.name}</span> <span className="detail-id">{selectedItem.name || ''}</span>
</div> </div>
<button className="detail-close" onClick={() => setSelectedItem(null)}></button> <button className="detail-close" onClick={() => setSelectedItem(null)}></button>
</div> </div>

View File

@@ -17,7 +17,7 @@ function SmeltingPanel() {
const furnaceStatus = inventory.furnaceStatus || {}; const furnaceStatus = inventory.furnaceStatus || {};
const furnaceEntries = Object.entries(furnaceStatus); const furnaceEntries = Object.entries(furnaceStatus);
const smeltableEntries = Object.entries(smeltable); const smeltableEntries = Object.entries(smeltable || {});
return ( return (
<div className="smelting-panel"> <div className="smelting-panel">
@@ -44,31 +44,31 @@ function SmeltingPanel() {
{furnaceEntries.length > 0 ? ( {furnaceEntries.length > 0 ? (
<div className="furnace-grid"> <div className="furnace-grid">
{furnaceEntries.map(([name, status]) => ( {furnaceEntries.map(([name, status]) => (
<div key={name} className={`furnace-card ${status.active ? 'active' : 'idle'}`}> <div key={name} className={`furnace-card ${(status && status.active) ? 'active' : 'idle'}`}>
<div className="furnace-name">{name.replace(/^minecraft:/, '')}</div> <div className="furnace-name">{(name || '').replace(/^minecraft:/, '')}</div>
<div className="furnace-status"> <div className="furnace-status">
{status.input && ( {status && status.input && (
<div className="furnace-slot"> <div className="furnace-slot">
<span className="slot-label">In:</span> <span className="slot-label">In:</span>
<ItemIcon itemName={status.input.name} size={16} /> <ItemIcon itemName={status.input.name} size={16} />
<span>{status.input.count || 0}</span> <span>{status.input.count || 0}</span>
</div> </div>
)} )}
{status.fuel && ( {status && status.fuel && (
<div className="furnace-slot"> <div className="furnace-slot">
<span className="slot-label">Fuel:</span> <span className="slot-label">Fuel:</span>
<ItemIcon itemName={status.fuel.name} size={16} /> <ItemIcon itemName={status.fuel.name} size={16} />
<span>{status.fuel.count || 0}</span> <span>{status.fuel.count || 0}</span>
</div> </div>
)} )}
{status.output && ( {status && status.output && (
<div className="furnace-slot"> <div className="furnace-slot">
<span className="slot-label">Out:</span> <span className="slot-label">Out:</span>
<ItemIcon itemName={status.output.name} size={16} /> <ItemIcon itemName={status.output.name} size={16} />
<span>{status.output.count || 0}</span> <span>{status.output.count || 0}</span>
</div> </div>
)} )}
{!status.input && !status.fuel && !status.output && ( {(!status || (!status.input && !status.fuel && !status.output)) && (
<span className="furnace-empty">Empty</span> <span className="furnace-empty">Empty</span>
)} )}
</div> </div>
@@ -90,6 +90,8 @@ function SmeltingPanel() {
<div className="recipe-list"> <div className="recipe-list">
{smeltableEntries.map(([input, recipe]) => { {smeltableEntries.map(([input, recipe]) => {
const isDisabled = disabledRecipes[input]; const isDisabled = disabledRecipes[input];
const result = (recipe && recipe.result) || '';
const furnaces = (recipe && recipe.furnaces) || [];
return ( return (
<div <div
key={input} key={input}
@@ -105,13 +107,13 @@ function SmeltingPanel() {
</div> </div>
<span className="recipe-arrow"></span> <span className="recipe-arrow"></span>
<div className="recipe-output"> <div className="recipe-output">
<ItemIcon itemName={recipe.result} size={20} /> <ItemIcon itemName={result} size={20} />
<span>{formatItemName(recipe.result)}</span> <span>{formatItemName(result)}</span>
</div> </div>
<div className="recipe-furnaces"> <div className="recipe-furnaces">
{(recipe.furnaces || []).map((f) => ( {furnaces.map((f) => (
<span key={f} className="furnace-tag"> <span key={f} className="furnace-tag">
{f.replace(/^minecraft:/, '')} {(f || '').replace(/^minecraft:/, '')}
</span> </span>
))} ))}
</div> </div>