From fe6ac233294fccd924321ba8a974a2d9abf32d26 Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Sat, 21 Mar 2026 17:51:07 -0400 Subject: [PATCH] Harden components: add ErrorBoundary, null-safe rendering --- web/client/src/App.jsx | 9 +++- web/client/src/components/AlertsPanel.jsx | 50 ++++++++++--------- web/client/src/components/CraftingPanel.jsx | 8 +-- web/client/src/components/ErrorBoundary.jsx | 54 +++++++++++++++++++++ web/client/src/components/InventoryGrid.jsx | 6 +-- web/client/src/components/SmeltingPanel.jsx | 24 ++++----- 6 files changed, 109 insertions(+), 42 deletions(-) create mode 100644 web/client/src/components/ErrorBoundary.jsx diff --git a/web/client/src/App.jsx b/web/client/src/App.jsx index bbdce76..068413d 100644 --- a/web/client/src/App.jsx +++ b/web/client/src/App.jsx @@ -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() {
- + + +
@@ -80,7 +83,9 @@ function App() {
- {renderPanelContent()} + + {renderPanelContent()} +
diff --git a/web/client/src/components/AlertsPanel.jsx b/web/client/src/components/AlertsPanel.jsx index 7a4c8e5..4febba2 100644 --- a/web/client/src/components/AlertsPanel.jsx +++ b/web/client/src/components/AlertsPanel.jsx @@ -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 (
@@ -18,30 +18,36 @@ function AlertsPanel() { {alerts.length > 0 ? (
- {alerts.map((alert, idx) => ( -
-
- {alert.triggered ? '🔴' : '🟢'} -
-
-
- - {formatItemName(alert.item)} + {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 ( +
+
+ {isTriggered ? '🔴' : '🟢'}
-
- - Stock: {alert.current ?? '?'} - - - Min: {alert.threshold || alert.min || '?'} - +
+
+ + {formatItemName(itemName)} +
+
+ + Stock: {current} + + + Min: {threshold} + +
+
+
+ {isTriggered ? 'LOW' : 'OK'}
-
- {alert.triggered ? 'LOW' : 'OK'} -
-
- ))} + ); + })}
) : (
diff --git a/web/client/src/components/CraftingPanel.jsx b/web/client/src/components/CraftingPanel.jsx index 2b40a4f..6a14f26 100644 --- a/web/client/src/components/CraftingPanel.jsx +++ b/web/client/src/components/CraftingPanel.jsx @@ -35,9 +35,9 @@ function CraftingPanel() { {(craftable || []).map((recipe, idx) => (
- +
- {formatItemName(recipe.output)} + {formatItemName(recipe.output || '')} Produces {recipe.count || 1}
@@ -45,8 +45,8 @@ function CraftingPanel() {
{recipe.slots && Object.entries(recipe.slots).map(([slot, item]) => (
- - {formatItemName(item)} + + {formatItemName(item || '')}
))}
diff --git a/web/client/src/components/ErrorBoundary.jsx b/web/client/src/components/ErrorBoundary.jsx new file mode 100644 index 0000000..fadd3ef --- /dev/null +++ b/web/client/src/components/ErrorBoundary.jsx @@ -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 ( +
+

⚠️ Something went wrong

+

+ {this.state.error?.message || 'Unknown error'} +

+ +
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/web/client/src/components/InventoryGrid.jsx b/web/client/src/components/InventoryGrid.jsx index ee66c43..3268a0f 100644 --- a/web/client/src/components/InventoryGrid.jsx +++ b/web/client/src/components/InventoryGrid.jsx @@ -97,10 +97,10 @@ function InventoryGrid() { {selectedItem && (
- +
-

{formatItemName(selectedItem.name)}

- {selectedItem.name} +

{formatItemName(selectedItem.name || '')}

+ {selectedItem.name || ''}
diff --git a/web/client/src/components/SmeltingPanel.jsx b/web/client/src/components/SmeltingPanel.jsx index 4651a26..3fa3547 100644 --- a/web/client/src/components/SmeltingPanel.jsx +++ b/web/client/src/components/SmeltingPanel.jsx @@ -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 (
@@ -44,31 +44,31 @@ function SmeltingPanel() { {furnaceEntries.length > 0 ? (
{furnaceEntries.map(([name, status]) => ( -
-
{name.replace(/^minecraft:/, '')}
+
+
{(name || '').replace(/^minecraft:/, '')}
- {status.input && ( + {status && status.input && (
In: {status.input.count || 0}
)} - {status.fuel && ( + {status && status.fuel && (
Fuel: {status.fuel.count || 0}
)} - {status.output && ( + {status && status.output && (
Out: {status.output.count || 0}
)} - {!status.input && !status.fuel && !status.output && ( + {(!status || (!status.input && !status.fuel && !status.output)) && ( Empty )}
@@ -90,6 +90,8 @@ function SmeltingPanel() {
{smeltableEntries.map(([input, recipe]) => { const isDisabled = disabledRecipes[input]; + const result = (recipe && recipe.result) || ''; + const furnaces = (recipe && recipe.furnaces) || []; return (
- - {formatItemName(recipe.result)} + + {formatItemName(result)}
- {(recipe.furnaces || []).map((f) => ( + {furnaces.map((f) => ( - {f.replace(/^minecraft:/, '')} + {(f || '').replace(/^minecraft:/, '')} ))}