boxes-fe/src/components/Items.js

440 lines
14 KiB
JavaScript

import React, { useEffect, useState, useCallback, useRef } from 'react';
import {
Container,
List,
ListItem,
ListItemText,
TextField,
Button,
IconButton,
Typography,
Avatar,
ListItemAvatar,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TableContainer,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
Box,
Tooltip
} from '@mui/material';
import { Delete as DeleteIcon, Edit as EditIcon } from '@mui/icons-material';
import axios from 'axios';
import { useParams, useLocation } from 'react-router-dom';
import ItemDetails from './ItemDetails';
import { PRIMARY_COLOR, SECONDARY_COLOR } from '../App';
export default function Items({ token }) {
const { id: boxId } = useParams();
const [items, setItems] = useState([]);
const [newItemName, setNewItemName] = useState('');
const [newItemDescription, setNewItemDescription] = useState('');
// const [newItemImagePath, setNewItemImagePath] = useState('/images/default.jpg');
const [editingItem, setEditingItem] = useState(null);
const location = useLocation();
const boxName = location.state?.boxName || 'Unknown Box';
// const boxID = location.state?.boxId; // used in handleClose function
const [itemImages, setItemImages] = useState({});
const fileInputRef = useRef(null);
const [openAddItemDialog, setOpenAddItemDialog] = useState(false); // For Add Item dialog
const { id } = useParams();
const boxID = id;
const url = boxId === undefined ? `${process.env.REACT_APP_API_URL}/api/v1/items` : `${process.env.REACT_APP_API_URL}/api/v1/boxes/${boxId}/items`;
const [searchQuery, setSearchQuery] = useState('');
const debugLog = (message) => {
if (process.env.DEBUG_API) {
console.log(message);
}
};
debugLog("Box ID: " + boxID);
// const handleSelectItem = (item) => {
// setSelectedItem(item);
// };
const handleAddItem = () => {
setOpenAddItemDialog(true);
};
const handleCloseAddItemDialog = () => {
setOpenAddItemDialog(false);
setNewItemName('');
setNewItemDescription('');
// setNewItemImagePath('');
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
/**
* This function takes an image name and returns a unique image name.
* The purpose of this function is to prevent overwriting of images with the same name.
* If the image name is 'image.jpg', a random string is generated and appended to the image name.
* This is used to prevent overwriting of images with the same name.
* For example, if an image named 'image.jpg' is uploaded, the function will return 'image_8xgu6hcu.jpg'
* This ensures that the image will not overwrite any existing image with the same name.
* @param {string} imageName - The name of the image
* @return {string} - The unique image name
*/
const generateUniqueImageName = (imageName) => {
if (imageName.toLowerCase() === 'image.jpg') {
// Generate a random string
const randomString = Math.random().toString(36).substr(2, 9);
// Append the random string to the image name
return `image_${randomString}.jpg`;
}
// Return the original image name if it's not 'image.jpg'
return imageName;
};
const handleImageUpload = async (itemId, imageFile, newImageName) => {
const formData = new FormData();
//const imageFile = fileInputRef.current.files[0];
//const newImageName = generateUniqueImageName(imageFile.name);
formData.append('image', new File([imageFile], newImageName, {
type: imageFile.type,
}));
// Create a new file with the unique name
// eslint-disable-next-line
const newImageFile = new File([imageFile], newImageName, {
type: imageFile.type,
});
try {
const response = await axios.post(`${process.env.REACT_APP_API_URL}/api/v1/items/${itemId}/upload`, formData, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'multipart/form-data'
}
});
// console.log('Image uploaded successfully!');
return response.data.imagePath; // Indicate successful upload
} catch (error) {
console.error('Image upload failed:', error);
return null; // Indicate upload failure
}
};
/**
* Handle saving a new item.
*
* This function first creates the item without the image, then if the item creation is successful,
* it uploads the image associated with the item.
*
* @return {Promise<void>}
*/
const handleSaveNewItem = async () => {
try {
// Step 1: Create the item first
// This sends a request to the API to create a new item with the
// name and description provided. The box_id is set to the selected
// box ID.
const newItemResponse = await axios.post(`${process.env.REACT_APP_API_URL}/api/v1/items`, {
name: newItemName,
description: newItemDescription,
box_id: parseInt(boxId, 10) // Ensure boxId is converted to an integer
}, {
headers: {
Authorization: `Bearer ${token}`
}
});
console.log('New item created:', newItemResponse.status);
// Step 2: If item creation is successful, upload the image
if (newItemResponse.status === 200 && fileInputRef.current.files[0]) {
// Get the ID of the newly created item
const newItemId = newItemResponse.data.id;
// Get the image file that was uploaded
const imageFile = fileInputRef.current.files[0];
// Generate a unique image name by appending a random string
// to the image file name. This is used to prevent overwriting
// of images with the same name.
const newImageName = generateUniqueImageName(imageFile.name);
// Upload the image file with the unique name to the server
const uploadedImagePath = await handleImageUpload(newItemId, fileInputRef.current.files[0], newImageName);
if (uploadedImagePath) {
// The image was uploaded successfully. Log the uploaded image path
console.log("Image path to save:", uploadedImagePath);
// You might want to update your item in the backend with the image path
// For example:
// await axios.put(...);
} else {
// The image upload failed. Log an error message
console.error('Failed to upload image for the new item.');
}
}
// Close the add item dialog
handleCloseAddItemDialog();
// Fetch the items again to get the latest data
fetchItems();
} catch (error) {
// Catch any errors that may occur during the item creation
// and image upload process. Log the error message
console.error('Error adding item:', error);
}
};
//const [selectedItem, setSelectedItem] = React.useState(null);
const handleCloseItemDetails = () => {
setEditingItem(null); // Close the ItemDetails modal
};
const handleImageError = (e) => {
if (e.target.src.startsWith('data:image/')) {
console.error("Default image failed to load. Check the file path.");
return;
}
const reader = new FileReader();
reader.onload = () => {
e.target.onerror = null;
e.target.src = reader.result;
};
fetch('/default.jpg')
.then(res => res.blob())
.then(blob => reader.readAsDataURL(blob))
.catch(error => console.error("Error loading default image:", error));
};
const getImageSrc = useCallback((itemId) => {
return axios.get(`${process.env.REACT_APP_API_URL}/api/v1/items/${itemId}/image`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob'
})
.then(response => {
if (response.status === 200) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(response.data);
});
} else {
throw new Error('Image fetch failed');
}
})
.catch(() => {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = '/default.jpg';
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
resolve(canvas.toDataURL());
};
img.onerror = reject;
});
});
}, [token]);
const fetchItems = useCallback(() => {
axios.get( url, {
headers: { Authorization: `Bearer ${token}` }
}).then(response => {
setItems(response.data);
// Fetch images for each item
response.data.forEach(item => {
getImageSrc(item.ID).then(imageDataUrl => {
setItemImages(prevItemImages => ({
...prevItemImages,
[item.ID]: imageDataUrl
}));
});
});
});
}, [token, getImageSrc, url]);
// lint says I don't need boxId here
useEffect(() => {
fetchItems();
}, [boxId, token, fetchItems]);
// const handleAddItem = () => {
// const formData = new FormData();
// formData.append('name', newItemName);
// formData.append('description', newItemDescription);
// formData.append('box_id', parseInt(boxId, 10));
// // Append image only if a new one is selected
// if (fileInputRef.current.files[0]) {
// formData.append('image', fileInputRef.current.files[0]);
// }
// axios.post(`${process.env.REACT_APP_API_URL}/items`, formData, {
// headers: {
// Authorization: `Bearer ${token}`,
// 'Content-Type': 'multipart/form-data' // Important for file uploads
// }
// }).then(() => {
// setNewItemName('');
// setNewItemDescription('');
// setNewItemImagePath('');
// // Clear the file input
// if (fileInputRef.current) {
// fileInputRef.current.value = '';
// }
// fetchItems();
// });
// };
const handleDeleteItem = (itemId) => {
axios.delete(`${process.env.REACT_APP_API_URL}/api/v1/items/${itemId}`, {
headers: { Authorization: `Bearer ${token}` }
}).then(() => {
fetchItems();
});
};
const handleEditItem = (item) => {
setEditingItem(item);
};
const handleSaveEdit = () => {
setEditingItem(null);
fetchItems();
};
return (
<Container>
<TextField
label="Search"
variant="outlined"
fullWidth
margin="normal"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<h2>Items in Box: {boxName === "Unknown Box" ? "All Boxes" : `${boxName} (${items.length} items)`}</h2>
<input
type="file"
accept="image/*"
ref={fileInputRef}
style={{ display: 'none' }}
id="newItemImageUpload" // Unique ID
/>
<Button sx={{ backgroundColor: PRIMARY_COLOR, borderBottom: '1px solid', borderColor: '#444', color: SECONDARY_COLOR }}variant="contained" color="primary" onClick={handleAddItem}>
Add Item
</Button>
{/* Dialog for adding new item */}
<Dialog open={openAddItemDialog} onClose={handleCloseAddItemDialog}>
<DialogTitle>Add New Item</DialogTitle>
<DialogContent>
<TextField
label="Item Name"
variant="outlined"
fullWidth
margin="normal"
value={newItemName}
onChange={(e) => setNewItemName(e.target.value)}
/>
<TextField
label="Item Description"
variant="outlined"
fullWidth
margin="normal"
value={newItemDescription}
onChange={(e) => setNewItemDescription(e.target.value)}
/>
<input
type="file"
accept="image/*"
ref={fileInputRef}
// capture="environment" // Capture the image from the user's camera
style={{ display: 'block', margin: '10px 0' }} // Style as needed
id="newItemImageUpload"
/>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseAddItemDialog}>Cancel</Button>
<Button onClick={handleSaveNewItem} color="primary">
Save
</Button>
</DialogActions>
</Dialog>
{editingItem ? (
<ItemDetails
item={editingItem}
token={token}
onSave={handleSaveEdit}
onClose={handleCloseItemDetails}
boxId={boxId}
/>
) : (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell style={{ width: '40px' }}>Image</TableCell>
<TableCell style={{ width: '100px' }}>Name</TableCell>
<TableCell>Description</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items
.filter(item =>
item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.description.toLowerCase().includes(searchQuery.toLowerCase())
)
.map((item) => (
<TableRow key={item.ID}>
<TableCell>
<Avatar src={itemImages[item.ID] || '/images/default.jpg'} />
</TableCell>
<TableCell>{item.name}</TableCell>
<TableCell>{item.description}</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"
>
<DeleteIcon />
</IconButton>
</Tooltip>
</Box>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Container>
);
}