error-handling #1

Merged
stwhite merged 24 commits from error-handling into main 2024-10-30 15:19:49 +00:00
3 changed files with 116 additions and 58 deletions
Showing only changes of commit efa24c3f84 - Show all commits

View File

@ -8,6 +8,7 @@ import Items from './components/Items';
import Navbar from './components/Navbar'; // Correct import here
import Admin from './components/Admin'; // Correct import here
import { createContext } from 'react';
import ErrorBoundary from './components/ErrorBoundary';
import './styles.css'
export const AppContext = createContext();
@ -26,10 +27,12 @@ function App() {
return (
<AppContext.Provider value={{ token, setToken }}>
<Router>
<Navbar />
<AppRoutes token={token} setToken={setToken} />
</Router>
<ErrorBoundary>
<Router>
<Navbar />
<AppRoutes token={token} setToken={setToken} />
</Router>
</ErrorBoundary>
</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 {
Container,
Button,
@ -27,6 +27,30 @@ import { useApiCall } from './hooks/useApiCall';
import { api } from '../services/api';
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 }) {
const { id: boxId } = useParams();
const [items, setItems] = useState([]);
@ -53,7 +77,7 @@ export default function Items({ token }) {
const getItems = async () => {
try {
const response = await fetchItems(() =>
api.items.getAll(token, boxId)
boxId ? api.items.getByBox(token, boxId) : api.items.getAll(token)
);
setItems(response.data);
@ -103,7 +127,7 @@ export default function Items({ token }) {
return imageName;
};
const handleSaveNewItem = async () => {
const handleSaveNewItem = useCallback(async () => {
try {
const newItemResponse = await createItem(() =>
api.items.create(token, {
@ -112,7 +136,7 @@ export default function Items({ token }) {
box_id: parseInt(boxId, 10)
})
);
if (newItemResponse.status === 200 && fileInputRef.current?.files?.[0]) {
const imageFile = fileInputRef.current.files[0];
const newImageName = generateUniqueImageName(imageFile.name);
@ -120,34 +144,49 @@ export default function Items({ token }) {
formData.append('image', new File([imageFile], newImageName, {
type: imageFile.type,
}));
await uploadImage(() =>
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();
// Refresh items list
const response = await fetchItems(() =>
api.items.getAll(token, boxId)
boxId ? api.items.getByBox(token, boxId) : api.items.getAll(token)
);
setItems(response.data);
} catch (err) {}
};
}, [token, boxId, newItemName, newItemDescription, createItem, uploadImage, fetchItems]);
const handleDeleteItem = async (itemId) => {
const handleDeleteItem = useCallback(async (itemId) => {
try {
await deleteItem(() =>
api.items.delete(token, itemId)
);
setItems(items.filter(item => item.ID !== itemId));
await deleteItem(() => api.items.delete(token, itemId));
setItems(prev => prev.filter(item => item.ID !== itemId));
} catch (err) {}
};
}, [token, deleteItem]);
const handleEditItem = (item) => {
const handleEditItem = useCallback((item) => {
setEditingItem(item);
};
}, []);
const handleSaveEdit = async () => {
setEditingItem(null);
@ -157,9 +196,12 @@ export default function Items({ token }) {
setItems(response.data);
};
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.description.toLowerCase().includes(searchQuery.toLowerCase())
const filteredItems = useMemo(() =>
items.filter(item =>
item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.description.toLowerCase().includes(searchQuery.toLowerCase())
),
[items, searchQuery]
);
return (
@ -251,40 +293,17 @@ export default function Items({ token }) {
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredItems.map((item) => (
<TableRow key={item.ID}>
<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={() => 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>
<TableBody>
{filteredItems.map((item) => (
<Item
key={item.ID}
item={item}
itemImages={itemImages}
onDelete={handleDeleteItem}
onEdit={handleEditItem}
/>
))}
</TableBody>
</Table>
</TableContainer>
)}