Added errorboundary, memoized items for performance.

This commit is contained in:
Steve White 2024-10-30 10:17:52 -05:00
parent 2e598968ac
commit efa24c3f84
3 changed files with 116 additions and 58 deletions

View File

@ -8,6 +8,7 @@ import Items from './components/Items';
import Navbar from './components/Navbar'; // Correct import here import Navbar from './components/Navbar'; // Correct import here
import Admin from './components/Admin'; // Correct import here import Admin from './components/Admin'; // Correct import here
import { createContext } from 'react'; import { createContext } from 'react';
import ErrorBoundary from './components/ErrorBoundary';
import './styles.css' import './styles.css'
export const AppContext = createContext(); export const AppContext = createContext();
@ -26,10 +27,12 @@ function App() {
return ( return (
<AppContext.Provider value={{ token, setToken }}> <AppContext.Provider value={{ token, setToken }}>
<Router> <ErrorBoundary>
<Navbar /> <Router>
<AppRoutes token={token} setToken={setToken} /> <Navbar />
</Router> <AppRoutes token={token} setToken={setToken} />
</Router>
</ErrorBoundary>
</AppContext.Provider> </AppContext.Provider>
); );
} }

View File

@ -0,0 +1,36 @@
import React from 'react';
import { Alert, Button, Container } from '@mui/material';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return (
<Container>
<Alert severity="error" sx={{ mt: 2 }}>
Something went wrong
<Button
onClick={() => window.location.reload()}
variant="outlined"
size="small"
sx={{ ml: 2 }}
>
Reload Page
</Button>
</Alert>
</Container>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { import {
Container, Container,
Button, Button,
@ -27,6 +27,30 @@ import { useApiCall } from './hooks/useApiCall';
import { api } from '../services/api'; import { api } from '../services/api';
import ItemDetails from './ItemDetails'; import ItemDetails from './ItemDetails';
const Item = React.memo(({ item, onDelete, onEdit, itemImages }) => (
<TableRow>
<TableCell>
<Avatar src={itemImages[item.ID] || '/images/default.jpg'} />
</TableCell>
<TableCell>{item.name}</TableCell>
<TableCell>{item.description}</TableCell>
<TableCell>
<Box display="flex" justifyContent="space-between" width="100%">
<Tooltip title="Edit Item">
<IconButton onClick={() => onEdit(item)} size="large" sx={{ mr: 1 }}>
<EditIcon />
</IconButton>
</Tooltip>
<Tooltip title="Delete Item">
<IconButton onClick={() => onDelete(item.ID)} size="large" color="error">
<DeleteIcon />
</IconButton>
</Tooltip>
</Box>
</TableCell>
</TableRow>
));
export default function Items({ token }) { export default function Items({ token }) {
const { id: boxId } = useParams(); const { id: boxId } = useParams();
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
@ -53,7 +77,7 @@ export default function Items({ token }) {
const getItems = async () => { const getItems = async () => {
try { try {
const response = await fetchItems(() => const response = await fetchItems(() =>
api.items.getAll(token, boxId) boxId ? api.items.getByBox(token, boxId) : api.items.getAll(token)
); );
setItems(response.data); setItems(response.data);
@ -103,7 +127,7 @@ export default function Items({ token }) {
return imageName; return imageName;
}; };
const handleSaveNewItem = async () => { const handleSaveNewItem = useCallback(async () => {
try { try {
const newItemResponse = await createItem(() => const newItemResponse = await createItem(() =>
api.items.create(token, { api.items.create(token, {
@ -112,7 +136,7 @@ export default function Items({ token }) {
box_id: parseInt(boxId, 10) box_id: parseInt(boxId, 10)
}) })
); );
if (newItemResponse.status === 200 && fileInputRef.current?.files?.[0]) { if (newItemResponse.status === 200 && fileInputRef.current?.files?.[0]) {
const imageFile = fileInputRef.current.files[0]; const imageFile = fileInputRef.current.files[0];
const newImageName = generateUniqueImageName(imageFile.name); const newImageName = generateUniqueImageName(imageFile.name);
@ -120,34 +144,49 @@ export default function Items({ token }) {
formData.append('image', new File([imageFile], newImageName, { formData.append('image', new File([imageFile], newImageName, {
type: imageFile.type, type: imageFile.type,
})); }));
await uploadImage(() => await uploadImage(() =>
api.items.uploadImage(token, newItemResponse.data.id, formData) api.items.uploadImage(token, newItemResponse.data.id, formData)
); );
if (newItemResponse.data.id) {
try {
const imageResponse = await api.items.getImage(token, newItemResponse.data.id);
const reader = new FileReader();
reader.onload = () => {
setItemImages(prev => ({
...prev,
[newItemResponse.data.id]: reader.result
}));
};
reader.readAsDataURL(imageResponse.data);
} catch (err) {
setItemImages(prev => ({
...prev,
[newItemResponse.data.id]: '/default.jpg'
}));
}
}
} }
handleCloseAddItemDialog(); handleCloseAddItemDialog();
// Refresh items list
const response = await fetchItems(() => const response = await fetchItems(() =>
api.items.getAll(token, boxId) boxId ? api.items.getByBox(token, boxId) : api.items.getAll(token)
); );
setItems(response.data); setItems(response.data);
} catch (err) {} } catch (err) {}
}; }, [token, boxId, newItemName, newItemDescription, createItem, uploadImage, fetchItems]);
const handleDeleteItem = async (itemId) => { const handleDeleteItem = useCallback(async (itemId) => {
try { try {
await deleteItem(() => await deleteItem(() => api.items.delete(token, itemId));
api.items.delete(token, itemId) setItems(prev => prev.filter(item => item.ID !== itemId));
);
setItems(items.filter(item => item.ID !== itemId));
} catch (err) {} } catch (err) {}
}; }, [token, deleteItem]);
const handleEditItem = (item) => { const handleEditItem = useCallback((item) => {
setEditingItem(item); setEditingItem(item);
}; }, []);
const handleSaveEdit = async () => { const handleSaveEdit = async () => {
setEditingItem(null); setEditingItem(null);
@ -157,9 +196,12 @@ export default function Items({ token }) {
setItems(response.data); setItems(response.data);
}; };
const filteredItems = items.filter(item => const filteredItems = useMemo(() =>
item.name.toLowerCase().includes(searchQuery.toLowerCase()) || items.filter(item =>
item.description.toLowerCase().includes(searchQuery.toLowerCase()) item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.description.toLowerCase().includes(searchQuery.toLowerCase())
),
[items, searchQuery]
); );
return ( return (
@ -251,40 +293,17 @@ export default function Items({ token }) {
<TableCell>Actions</TableCell> <TableCell>Actions</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{filteredItems.map((item) => ( {filteredItems.map((item) => (
<TableRow key={item.ID}> <Item
<TableCell> key={item.ID}
<Avatar src={itemImages[item.ID] || '/images/default.jpg'} /> item={item}
</TableCell> itemImages={itemImages}
<TableCell>{item.name}</TableCell> onDelete={handleDeleteItem}
<TableCell>{item.description}</TableCell> onEdit={handleEditItem}
<TableCell> />
<Box display="flex" justifyContent="space-between" width="100%"> ))}
<Tooltip title="Edit Item"> </TableBody>
<IconButton
onClick={() => handleEditItem(item)}
size="large"
sx={{ mr: 1 }}
>
<EditIcon />
</IconButton>
</Tooltip>
<Tooltip title="Delete Item">
<IconButton
onClick={() => handleDeleteItem(item.ID)}
size="large"
color="error"
disabled={deletingItem}
>
{deletingItem ? <CircularProgress size={20} /> : <DeleteIcon />}
</IconButton>
</Tooltip>
</Box>
</TableCell>
</TableRow>
))}
</TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
)} )}