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

View File

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

View File

@@ -35,9 +35,9 @@ function CraftingPanel() {
{(craftable || []).map((recipe, idx) => (
<div key={idx} className="craft-card">
<div className="craft-output">
<ItemIcon itemName={recipe.output} size={32} />
<ItemIcon itemName={recipe.output || ''} size={32} />
<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>
</div>
</div>
@@ -45,8 +45,8 @@ function CraftingPanel() {
<div className="craft-ingredients">
{recipe.slots && Object.entries(recipe.slots).map(([slot, item]) => (
<div key={slot} className="craft-ingredient">
<ItemIcon itemName={item} size={16} />
<span>{formatItemName(item)}</span>
<ItemIcon itemName={item || ''} size={16} />
<span>{formatItemName(item || '')}</span>
</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 && (
<div className="item-detail-panel">
<div className="detail-header">
<ItemIcon itemName={selectedItem.name} size={48} />
<ItemIcon itemName={selectedItem.name || ''} size={48} />
<div className="detail-info">
<h3>{formatItemName(selectedItem.name)}</h3>
<span className="detail-id">{selectedItem.name}</span>
<h3>{formatItemName(selectedItem.name || '')}</h3>
<span className="detail-id">{selectedItem.name || ''}</span>
</div>
<button className="detail-close" onClick={() => setSelectedItem(null)}></button>
</div>

View File

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