From 02aca14e950c6623e3e31100e26e527655733981 Mon Sep 17 00:00:00 2001 From: Steve White Date: Mon, 14 Oct 2024 09:41:46 -0500 Subject: [PATCH 01/22] Trying to get the box update code to work. --- src/components/ItemDetails.js | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/components/ItemDetails.js b/src/components/ItemDetails.js index 22a43d5..36c3797 100644 --- a/src/components/ItemDetails.js +++ b/src/components/ItemDetails.js @@ -1,5 +1,5 @@ // src/components/ItemDetails.js -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import { TextField, Button, Container, Avatar, Typography } from '@mui/material'; import axios from 'axios'; import { PRIMARY_COLOR, SECONDARY_COLOR } from '../App'; @@ -31,8 +31,10 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) { fetchBoxes(); }, [token]); + const handleBoxChange = (event) => { setSelectedBoxId(event.target.value); + updateItemBoxId(); console.log('Selected box ID:', event.target.value); }; @@ -55,7 +57,7 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) { }; // Fetch the box details when the component mounts or the selectedBoxId changes - if (selectedBoxId !== null) { + if (selectedBoxId !== boxId) { getBoxDetails(selectedBoxId); } }, [selectedBoxId, token, boxId]); @@ -107,7 +109,7 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) { // navigate(`/boxes/${boxId}/items`); // Navigate back to the items list // }; - const handleImageUpload = async () => { + const handleImageUpload = useCallback(async () => { const formData = new FormData(); formData.append('image', fileInputRef.current.files[0]); @@ -125,15 +127,29 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) { // Handle upload error (e.g., show an error message) console.error('Image upload failed:', error); } + }, [item.ID, token]); + + const updateItemBoxId = async () => { + try { + const response = await axios.put(`${process.env.REACT_APP_API_URL}/items/${item.id}`, { + box_id: selectedBoxId, + }, { + headers: { Authorization: `Bearer ${token}` } + }); + // Update the item's boxId + item.box_id = selectedBoxId; + } catch (error) { + console.error('Error updating item boxId:', error); + } }; - const handleSave = async () => { + const handleSave = useCallback( async () => { let imagePath; // 1. Handle image upload first if a new image is selected if (fileInputRef.current.files[0]) { imagePath = await handleImageUpload(); } - console.log(selectedBoxId) + console.log("Selected box ID:", selectedBoxId) // 2. Update item details (name, description, etc.) try { @@ -148,7 +164,7 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) { // Handle update error console.error('Item update failed:', error); } - }; + }, [item.ID, name, description, selectedBoxId, token, onSave, handleImageUpload]); const handleImageError = (e) => { e.target.src = '/images/default.jpg'; // Fallback to default image on error @@ -216,12 +232,13 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) { +

+ + ))} + +
+ setUsername(e.target.value)} placeholder="Username" /> + setPassword(e.target.value)} placeholder="Password" /> + +
+ +
+ + +
+ + ); +} \ No newline at end of file diff --git a/src/components/Navbar.js b/src/components/Navbar.js index 5ddb1af..2a40079 100644 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -36,6 +36,7 @@ export default function Navbar() { + From cd394ca1ee031cd5a001720529286f56a7d18f5f Mon Sep 17 00:00:00 2001 From: Steve White Date: Thu, 17 Oct 2024 08:11:58 -0500 Subject: [PATCH 06/22] Admin page added - create/delete users, backup and restore database --- src/components/Admin.css | 15 ++++++++++ src/components/Admin.js | 60 ++++++++++++++++++++++++++++++---------- 2 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 src/components/Admin.css diff --git a/src/components/Admin.css b/src/components/Admin.css new file mode 100644 index 0000000..9ec3156 --- /dev/null +++ b/src/components/Admin.css @@ -0,0 +1,15 @@ +/* Admin.css */ +table { + border-collapse: collapse; + width: 100%; +} + +th, td { + border: 1px solid #ddd; + padding: 10px; + text-align: left; +} + +th { + background-color: #f0f0f0; +} \ No newline at end of file diff --git a/src/components/Admin.js b/src/components/Admin.js index 5f4f578..4c464e4 100644 --- a/src/components/Admin.js +++ b/src/components/Admin.js @@ -3,6 +3,8 @@ import React, { useState, useEffect, useRef } from 'react'; import axios from 'axios'; import { Link, useNavigate } from 'react-router-dom'; import { PRIMARY_COLOR, SECONDARY_COLOR } from '../App'; +import './Admin.css'; // Import the CSS file + export default function Admin() { const [users, setUsers] = useState([]); @@ -103,24 +105,52 @@ export default function Admin() { return (

Admin

-
    - {users.map(user => ( -
  • - {user.username} - -
  • - ))} -
+ + + + + + + + + {users.map(user => ( + + + + + ))} + +
UsernameActions
{user.username} + +
setUsername(e.target.value)} placeholder="Username" /> setPassword(e.target.value)} placeholder="Password" />
- -
- - -
+

Database

+ + + + + + + + + + + + + + + + + +
Action
Backup + +
Restore + + +
- ); -} \ No newline at end of file + )}; \ No newline at end of file From 88a90e39b7372c0b7052ffa687fd57cd55e7c440 Mon Sep 17 00:00:00 2001 From: Steve White Date: Thu, 17 Oct 2024 08:47:27 -0500 Subject: [PATCH 07/22] UI cleanup with tables and chairs and tooltips :D --- src/components/Boxes.js | 46 ++++++++++------- src/components/ItemDetails.js | 19 ++++--- src/components/Items.js | 95 +++++++++++++++++++++-------------- 3 files changed, 96 insertions(+), 64 deletions(-) diff --git a/src/components/Boxes.js b/src/components/Boxes.js index 888ceda..b8fbc12 100644 --- a/src/components/Boxes.js +++ b/src/components/Boxes.js @@ -1,6 +1,6 @@ // src/components/Boxes.js import React, { useEffect, useState } from 'react'; -import { Container, Button, TextField, List, ListItem, ListItemText, IconButton } from '@mui/material'; +import { Container, Button, TextField, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material'; import { Delete as DeleteIcon } from '@mui/icons-material'; import { Link as RouterLink } from 'react-router-dom'; // Import Link from react-router-dom import axios from 'axios'; @@ -51,6 +51,33 @@ export default function Boxes({ token }) { return ( + + + + + + Box Name + Actions + + + + {boxes.map((box) => ( + + + + {box.name} + + + + + + + ))} + +
+
Add Box - - {boxes.map((box) => ( - handleDeleteBox(box.ID)}> - - - }> - {/* Use Link component */} - {box.name} - - } - /> - - ))} -
); } \ No newline at end of file diff --git a/src/components/ItemDetails.js b/src/components/ItemDetails.js index 98bf967..2815ade 100644 --- a/src/components/ItemDetails.js +++ b/src/components/ItemDetails.js @@ -1,6 +1,6 @@ // src/components/ItemDetails.js import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { TextField, Button, Container, Avatar } from '@mui/material'; +import { TextField, Button, Container, Avatar, Tooltip } from '@mui/material'; import axios from 'axios'; import { PRIMARY_COLOR, SECONDARY_COLOR } from '../App'; //import { useNavigate } from 'react-router-dom'; // Import useNavigate @@ -195,13 +195,15 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) {

Edit Item: {item.name}

{/* Display the item image as an avatar */} - + + + {imageOverlayVisible && (
{name} @@ -233,6 +235,7 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) { margin="normal" value={imagePath} onChange={(e) => setImagePath(e.target.value)} + sx={{ display: 'none' }} /> ) : ( - - {items - .filter(item => - item.name.toLowerCase().includes(searchQuery.toLowerCase()) || - item.description.toLowerCase().includes(searchQuery.toLowerCase()) - ) - .map((item) => ( - - handleEditItem(item)}> - - - handleDeleteItem(item.ID)}> - - - - }> - - - - - {item.description} - {item.image_path && ( - Image: {item.image_path} - )} - - } - /> - - ))} - + + + + + Image + Name + Description + Actions + + + + {items + .filter(item => + item.name.toLowerCase().includes(searchQuery.toLowerCase()) || + item.description.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .map((item) => ( + + + + + {item.name} + {item.description} + + + handleEditItem(item)} + size="large" + sx={{ mr: 1 }} + > + + + + + handleDeleteItem(item.ID)} + size="large" + color="error" + > + + + + + + ))} + +
+
)} ); From 578d38f187b65f3b817d22bfe441997fbe38e547 Mon Sep 17 00:00:00 2001 From: Steve White Date: Thu, 17 Oct 2024 09:04:28 -0500 Subject: [PATCH 08/22] More UI fixes to Admin.js --- src/components/Admin.js | 166 ++++++++++++++++++++++++++++------------ 1 file changed, 115 insertions(+), 51 deletions(-) diff --git a/src/components/Admin.js b/src/components/Admin.js index 4c464e4..3a2e68e 100644 --- a/src/components/Admin.js +++ b/src/components/Admin.js @@ -4,6 +4,21 @@ import axios from 'axios'; import { Link, useNavigate } from 'react-router-dom'; import { PRIMARY_COLOR, SECONDARY_COLOR } from '../App'; import './Admin.css'; // Import the CSS file +import { + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Button, + Alert, + Container, + Box, + TextField + } from '@mui/material'; export default function Admin() { @@ -26,6 +41,19 @@ export default function Admin() { }); }, []); + const fetchUsers = async () => { + try { + const response = await axios.get(`${process.env.REACT_APP_API_URL}/admin/user`, { + headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } + }); + setUsers(response.data); + } catch (error) { + console.error('Error fetching users:', error); + // Optionally, set an error message + // setErrorMessage('Failed to fetch users. Please try again.'); + } + }; + const handleCreateUser = async (e) => { e.preventDefault(); try { @@ -49,6 +77,9 @@ export default function Admin() { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }); setUsers(users.filter(user => user.id !== id)); + //setUsers(prevUsers => prevUsers.filter(user => user.id !== id)); + fetchUsers(); + } catch (error) { console.error(error); } @@ -103,54 +134,87 @@ export default function Admin() { }; return ( -
-

Admin

- - - - - - - - - {users.map(user => ( - - - - - ))} - -
UsernameActions
{user.username} - -
-
- setUsername(e.target.value)} placeholder="Username" /> - setPassword(e.target.value)} placeholder="Password" /> - -
-

Database

- - - - - - - - - - - - - - - - - -
Action
Backup - -
Restore - - -
-
- )}; \ No newline at end of file + + + Admin + + + + + Add New User + + setUsername(e.target.value)} + sx={{ mr: 2 }} + /> + setPassword(e.target.value)} + sx={{ mr: 2 }} + /> + + + + + + + + Username + Actions + + + + {users.map(user => ( + + {user.username} + + + + + ))} + +
+
+ + + + + + +
+ ); +} \ No newline at end of file From 4998f3a2d53964ad23465fd0950c4278affaa2a8 Mon Sep 17 00:00:00 2001 From: Steve White Date: Thu, 17 Oct 2024 10:43:58 -0500 Subject: [PATCH 09/22] Fixing MaterialUI stuff --- src/components/Admin.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/components/Admin.js b/src/components/Admin.js index 3a2e68e..ed13972 100644 --- a/src/components/Admin.js +++ b/src/components/Admin.js @@ -17,13 +17,15 @@ import { Alert, Container, Box, - TextField + TextField, + Tab } from '@mui/material'; export default function Admin() { const [users, setUsers] = useState([]); const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const navigate = useNavigate(); const fileInputRef = useRef(null); @@ -59,13 +61,15 @@ export default function Admin() { try { const response = await axios.post(`${process.env.REACT_APP_API_URL}/admin/user`, { username, - password + password, + email }, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }); setUsers([...users, response.data]); setUsername(''); setPassword(''); + setEmail(''); } catch (error) { console.error(error); } @@ -158,6 +162,13 @@ export default function Admin() { onChange={(e) => setPassword(e.target.value)} sx={{ mr: 2 }} /> + setEmail(e.target.value)} + sx={{ mr: 2 }} + /> @@ -167,14 +178,18 @@ export default function Admin() { + ID Username + Email Actions {users.map(user => ( - {user.username} + {user.ID} + {user.username} + {user.email}
+
Box Name From 3f33d5fda4869cac5b3649361ea0bfedcb2ef2e9 Mon Sep 17 00:00:00 2001 From: Steve White Date: Mon, 21 Oct 2024 11:27:57 -0500 Subject: [PATCH 14/22] Changed api endpoints to include /api/v1 --- src/App.js | 20 ++++++++++---------- src/components/Admin.js | 12 ++++++------ src/components/Boxes.js | 8 ++++---- src/components/ItemDetails.js | 12 ++++++------ src/components/Items.js | 10 +++++----- src/components/Login.js | 2 +- src/components/Navbar.js | 8 ++++---- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/App.js b/src/App.js index 2393a5e..d8b0484 100644 --- a/src/App.js +++ b/src/App.js @@ -40,24 +40,24 @@ function AppRoutes({ token, setToken }) { return ( <> - } /> + } /> : } + path="/api/v1/boxes" + element={token ? : } /> : } + path="/api/v1/items" + element={token ? : } /> : } + path="/api/v1/boxes/:id/items" + element={token ? : } /> : } + path="/api/v1/admin" + element={token ? : } /> - } /> + } /> ); diff --git a/src/components/Admin.js b/src/components/Admin.js index ed13972..58b82d7 100644 --- a/src/components/Admin.js +++ b/src/components/Admin.js @@ -32,7 +32,7 @@ export default function Admin() { useEffect(() => { - axios.get(`${process.env.REACT_APP_API_URL}/admin/user`, { + axios.get(`${process.env.REACT_APP_API_URL}/api/v1/admin/user`, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }) .then(response => { @@ -45,7 +45,7 @@ export default function Admin() { const fetchUsers = async () => { try { - const response = await axios.get(`${process.env.REACT_APP_API_URL}/admin/user`, { + const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/v1/admin/user`, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }); setUsers(response.data); @@ -59,7 +59,7 @@ export default function Admin() { const handleCreateUser = async (e) => { e.preventDefault(); try { - const response = await axios.post(`${process.env.REACT_APP_API_URL}/admin/user`, { + const response = await axios.post(`${process.env.REACT_APP_API_URL}/api/v1/admin/user`, { username, password, email @@ -77,7 +77,7 @@ export default function Admin() { const handleDeleteUser = async (id) => { try { - await axios.delete(`${process.env.REACT_APP_API_URL}/admin/user/${id}`, { + await axios.delete(`${process.env.REACT_APP_API_URL}/api/v1/admin/user/${id}`, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }); setUsers(users.filter(user => user.id !== id)); @@ -91,7 +91,7 @@ export default function Admin() { const handleBackupDatabase = async () => { try { - const response = await axios.get(`${process.env.REACT_APP_API_URL}/admin/db`, { + const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/v1/admin/db`, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }, responseType: 'blob' }); @@ -119,7 +119,7 @@ export default function Admin() { throw new Error('No token found in local storage'); } - const response = await axios.post(`${process.env.REACT_APP_API_URL}/admin/db`, formData, { + const response = await axios.post(`${process.env.REACT_APP_API_URL}/api/v1/admin/db`, formData, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'multipart/form-data' diff --git a/src/components/Boxes.js b/src/components/Boxes.js index 48b65f8..f3dc573 100644 --- a/src/components/Boxes.js +++ b/src/components/Boxes.js @@ -20,7 +20,7 @@ export default function Boxes({ token }) { useEffect(() => { //console.log('Token:' + token); - axios.get(`${process.env.REACT_APP_API_URL}/boxes`, { + axios.get(`${process.env.REACT_APP_API_URL}/api/v1/boxes`, { headers: { Authorization: `Bearer ${token}` } }).then(response => { setBoxes(response.data); @@ -33,7 +33,7 @@ export default function Boxes({ token }) { }, [boxes]); const handleCreateBox = () => { - axios.post(`${process.env.REACT_APP_API_URL}/boxes`, { name: newBoxName }, { + axios.post(`${process.env.REACT_APP_API_URL}/api/v1/boxes`, { name: newBoxName }, { headers: { Authorization: `Bearer ${token}` } }).then(response => { setBoxes([...boxes, response.data]); @@ -42,7 +42,7 @@ export default function Boxes({ token }) { }; const handleDeleteBox = (id) => { - axios.delete(`${process.env.REACT_APP_API_URL}/boxes/${id}`, { + axios.delete(`${process.env.REACT_APP_API_URL}/api/v1/boxes/${id}`, { headers: { Authorization: `Bearer ${token}` } }).then(() => { setBoxes(boxes.filter(box => box.ID !== id)); @@ -64,7 +64,7 @@ export default function Boxes({ token }) { {boxes.map((box) => ( - + {box.name} diff --git a/src/components/ItemDetails.js b/src/components/ItemDetails.js index 2815ade..713d8cb 100644 --- a/src/components/ItemDetails.js +++ b/src/components/ItemDetails.js @@ -25,7 +25,7 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) { useEffect(() => { const fetchBoxes = async () => { try { - const response = await axios.get(`${process.env.REACT_APP_API_URL}/boxes`, { + const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/v1/boxes`, { headers: { Authorization: `Bearer ${token}` } }); setBoxes(response.data); @@ -52,7 +52,7 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) { console.error('Invalid boxId:', boxId); return; } - const response = await axios.get(`${process.env.REACT_APP_API_URL}/boxes/${boxIdNumber}`, { + const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/v1/boxes/${boxIdNumber}`, { headers: { Authorization: `Bearer ${token}` } }); setBoxName(response.data.name); @@ -73,7 +73,7 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) { useEffect(() => { // Function to fetch image similar to getImageSrc in Items.js const getImageSrc = (itemId) => { - return axios.get(`${process.env.REACT_APP_API_URL}/items/${itemId}/image`, { + return axios.get(`${process.env.REACT_APP_API_URL}/api/v1/items/${itemId}/image`, { headers: { Authorization: `Bearer ${token}` }, responseType: 'blob' }) @@ -122,7 +122,7 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) { formData.append('image', fileInputRef.current.files[0]); try { - const response = await axios.post(`${process.env.REACT_APP_API_URL}/items/${item.ID}/upload`, formData, { + const response = await axios.post(`${process.env.REACT_APP_API_URL}/api/v1/items/${item.ID}/upload`, formData, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'multipart/form-data' @@ -141,7 +141,7 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) { const updateItemBoxId = async () => { try { // eslint-disable-next-line - const response = await axios.put(`${process.env.REACT_APP_API_URL}/items/${item.id}`, { + const response = await axios.put(`${process.env.REACT_APP_API_URL}/api/v1/items/${item.id}`, { box_id: selectedBoxId, }, { headers: { Authorization: `Bearer ${token}` } @@ -164,7 +164,7 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) { // 2. Update item details (name, description, etc.) try { - await axios.put(`${process.env.REACT_APP_API_URL}/items/${item.ID}`, { + await axios.put(`${process.env.REACT_APP_API_URL}/api/v1/items/${item.ID}`, { name, description, box_id: +selectedBoxId, // Ensure the updated selected box is saved diff --git a/src/components/Items.js b/src/components/Items.js index 14cdafb..fed9ac1 100644 --- a/src/components/Items.js +++ b/src/components/Items.js @@ -45,7 +45,7 @@ export default function Items({ token }) { 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}/items` : `${process.env.REACT_APP_API_URL}/boxes/${boxId}/items`; + 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) => { @@ -109,7 +109,7 @@ export default function Items({ token }) { }); try { - const response = await axios.post(`${process.env.REACT_APP_API_URL}/items/${itemId}/upload`, formData, { + 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' @@ -137,7 +137,7 @@ export default function Items({ token }) { // 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}/items`, { + 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 @@ -218,7 +218,7 @@ export default function Items({ token }) { }; const getImageSrc = useCallback((itemId) => { - return axios.get(`${process.env.REACT_APP_API_URL}/items/${itemId}/image`, { + return axios.get(`${process.env.REACT_APP_API_URL}/api/v1/items/${itemId}/image`, { headers: { Authorization: `Bearer ${token}` }, responseType: 'blob' }) @@ -302,7 +302,7 @@ export default function Items({ token }) { // }; const handleDeleteItem = (itemId) => { - axios.delete(`${process.env.REACT_APP_API_URL}/items/${itemId}`, { + axios.delete(`${process.env.REACT_APP_API_URL}/api/v1/items/${itemId}`, { headers: { Authorization: `Bearer ${token}` } }).then(() => { fetchItems(); diff --git a/src/components/Login.js b/src/components/Login.js index fdce747..3a1ca21 100644 --- a/src/components/Login.js +++ b/src/components/Login.js @@ -16,7 +16,7 @@ export default function Login({ setToken }) { setLoginError(false); // Reset error state on each login attempt try { // eslint-disable-next-line no-template-curly-in-string - const response = await axios.post(`${process.env.REACT_APP_API_URL}/login`, { username, password }); + const response = await axios.post(`${process.env.REACT_APP_API_URL}/api/v1/login`, { username, password }); setToken(response.data.token); navigate('/boxes'); } catch (error) { diff --git a/src/components/Navbar.js b/src/components/Navbar.js index 2a40079..8fcefab 100644 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -33,10 +33,10 @@ export default function Navbar() { Boxes - - - - + + + + From 5f3b592724e046f9d5e79e8b679431e32bc87c1c Mon Sep 17 00:00:00 2001 From: stwhite Date: Wed, 23 Oct 2024 12:49:43 -0500 Subject: [PATCH 15/22] Added a bunch of Docker crap but I think Dockerfile.build is all that matters --- .dockerignore | 3 +++ .env | 5 +---- Dockerfile | 7 +++++++ Dockerfile.build | 23 +++++++++++++++++++++++ Dockerfile.create | 20 ++++++++++++++++++++ Dockerfile.nginx | 4 ++++ Dockerfile.update | 18 ++++++++++++++++++ build.bash | 7 +++++++ src/Dockerfile | 29 ----------------------------- test.bash | 18 ++++++++++++++++++ 10 files changed, 101 insertions(+), 33 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 Dockerfile.build create mode 100644 Dockerfile.create create mode 100644 Dockerfile.nginx create mode 100644 Dockerfile.update create mode 100755 build.bash delete mode 100644 src/Dockerfile create mode 100644 test.bash diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3d5ed1b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +.git +*.log diff --git a/.env b/.env index a376ace..2d3f4c6 100644 --- a/.env +++ b/.env @@ -1,5 +1,2 @@ -# URL of the API -REACT_APP_API_URL=http://localhost:8080 +REACT_APP_API_URL=http://zbox.local:8080 -# Base URL of the webapp -REACT_APP_BASE_URL="/" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dc24a16 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM node:18-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY .env . +COPY . . +CMD ["npm", "run", "build"] diff --git a/Dockerfile.build b/Dockerfile.build new file mode 100644 index 0000000..1f65d4f --- /dev/null +++ b/Dockerfile.build @@ -0,0 +1,23 @@ +# Stage 1: Install dependencies +FROM node:14 AS build-stage + +WORKDIR /app + +# Copy package.json and package-lock.json first to leverage caching +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy the rest of your application code +COPY .env . +COPY . . + +# Build the application +RUN npm run build + +# Final stage: nothing needed, just to complete the multi-stage +FROM scratch + +# Copy the build output from the previous stage to a directory +COPY --from=build-stage /app/build ./build \ No newline at end of file diff --git a/Dockerfile.create b/Dockerfile.create new file mode 100644 index 0000000..2be1d90 --- /dev/null +++ b/Dockerfile.create @@ -0,0 +1,20 @@ +# Dockerfile.create +FROM node:14 AS builder + +# Set working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install dependencies +RUN npm install +RUN npm install -g serve + +# Copy the rest of the application code +COPY . . +COPY .env . + +RUN npm build . + +CMD ["serve", "-s", "build"] diff --git a/Dockerfile.nginx b/Dockerfile.nginx new file mode 100644 index 0000000..ea26fc5 --- /dev/null +++ b/Dockerfile.nginx @@ -0,0 +1,4 @@ +FROM nginx:alpine +COPY build /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/Dockerfile.update b/Dockerfile.update new file mode 100644 index 0000000..f7f009d --- /dev/null +++ b/Dockerfile.update @@ -0,0 +1,18 @@ +# Dockerfile.update +FROM node:14 + +# Set working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package*.json ./ +ENV REACT_APP_API_URL=http://zbox.local:8080 + +# Install dependencies +RUN npm install + +# Copy the rest of the application code +COPY . . +COPY .env . + +CMD ["npm", "run", "build"] diff --git a/build.bash b/build.bash new file mode 100755 index 0000000..9f0369a --- /dev/null +++ b/build.bash @@ -0,0 +1,7 @@ +#!/bin/bash +# Create teh react-builder image +docker build -f Dockerfile.create -t react-builder:latest . +# Run the react-builder image with local build/ dir mounted +docker run -v $(pwd)/build:/app/build react-builder:latest +# create the nginx container: +docker build --no-cache -f Dockerfile.nginx -t boxes-fe:latest . diff --git a/src/Dockerfile b/src/Dockerfile deleted file mode 100644 index 042b2ca..0000000 --- a/src/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -# Use an official Node runtime as the base image -FROM node:14 as build - -# Set the working directory in the container -WORKDIR /app - -# Copy package.json and package-lock.json -COPY package*.json ./ - -# Install dependencies -RUN npm install - -# Copy the rest of the application code -COPY . . - -# Build the app -RUN npm run build - -# Use nginx to serve the static files -FROM nginx:alpine - -# Copy the build output to replace the default nginx contents -COPY --from=build /app/build /usr/share/nginx/html - -# Expose port 80 -EXPOSE 80 - -# Start nginx -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/test.bash b/test.bash new file mode 100644 index 0000000..b551d03 --- /dev/null +++ b/test.bash @@ -0,0 +1,18 @@ +#!/bin/bash + +# Set the username and password +USERNAME="boxuser" +PASSWORD="boxuser" +EMAIL="boxuser@r8z.us" + +# Set the database file and table name +DB_FILE="../boxes-api/data/boxes.db" +TABLE_NAME="users" + +# Generate the bcrypt encrypted password using htpasswd +ENCRYPTED_PASSWORD=$(htpasswd -bnBC 10 "" "$PASSWORD" | tr -d ':\n') + +# Insert the username and encrypted password into the database +sqlite3 "$DB_FILE" "INSERT INTO $TABLE_NAME (username, password, email) VALUES ('$USERNAME', '$ENCRYPTED_PASSWORD', '$EMAIL')" + +echo "Password encrypted and stored in the database." From ca301c31083c4367a981c811377af4b9d01fe614 Mon Sep 17 00:00:00 2001 From: stwhite Date: Wed, 23 Oct 2024 13:03:34 -0500 Subject: [PATCH 16/22] Updated README.md with Docker instrauciotns --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 6def6f0..b10df65 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,21 @@ This is the frontend application for the Boxes App, built with React. It allows 1. **Clone the repository:** ```bash git clone git@gitea.r8z.us:stwhite/boxes-fe.git + +2. create the docker builder + ```bash + docker build -f Dockerfile.build -t boxes-fe-builder . + ``` + +3. build the application using the docker builder + ```bash + docker run box-builder:latest + docker container create --name tmp-con boxes-fe-builder -- sleep 1200 + docker cp tmp-con:/app/build ./boxes-fe/build + docker rm tmp-con + ``` + +4. copy the appliction to the boxes-api /build directory + ```bash + cp -r ./boxes-fe/build/* ../boxes-api/build/ + ``` \ No newline at end of file From 158d52dab4a633ddd972de5cc63b5f62564b4877 Mon Sep 17 00:00:00 2001 From: stwhite Date: Thu, 24 Oct 2024 15:51:36 -0500 Subject: [PATCH 17/22] Changes app name to Boxes instead of React App --- public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index aa069f2..64d1a54 100644 --- a/public/index.html +++ b/public/index.html @@ -24,7 +24,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + Boxes From 31eb7c04391188c9a2e80b6783efb172aedaa2ee Mon Sep 17 00:00:00 2001 From: Steve White Date: Tue, 29 Oct 2024 13:20:42 -0500 Subject: [PATCH 18/22] attempting to reload /index with any user-generated reload --- .env | 2 +- Dockerfile.build | 2 +- src/index.js | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.env b/.env index 2d3f4c6..2aa934b 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ -REACT_APP_API_URL=http://zbox.local:8080 +REACT_APP_API_URL=http://nebula1:8080 diff --git a/Dockerfile.build b/Dockerfile.build index 1f65d4f..df11716 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -20,4 +20,4 @@ RUN npm run build FROM scratch # Copy the build output from the previous stage to a directory -COPY --from=build-stage /app/build ./build \ No newline at end of file +COPY --from=build-stage /app/build ./build diff --git a/src/index.js b/src/index.js index db46643..ce25047 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,11 @@ import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; +const path = window.location.pathname; +if (path !== '/' && !path.startsWith('/api/v1/')) { + window.history.replaceState(null, '', '/'); +} + // Log the environment variables console.log('REACT_APP_API_URL:', process.env.REACT_APP_API_URL); From bd32bfaae5dcb9931633b3c2e290a1285f5aa244 Mon Sep 17 00:00:00 2001 From: Steve White Date: Tue, 29 Oct 2024 19:41:34 -0500 Subject: [PATCH 19/22] Fixing build system --- .dockerignore | 2 ++ Dockerfile.build | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.dockerignore b/.dockerignore index 3d5ed1b..faccad0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,5 @@ node_modules .git *.log +build +.DS_Store diff --git a/Dockerfile.build b/Dockerfile.build index df11716..48475aa 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -16,8 +16,8 @@ COPY . . # Build the application RUN npm run build -# Final stage: nothing needed, just to complete the multi-stage -FROM scratch +# Final stage: Use a minimal base image +FROM alpine:latest -# Copy the build output from the previous stage to a directory -COPY --from=build-stage /app/build ./build +# Copy the build output from the previous stage +COPY --from=build-stage /app/build ./build \ No newline at end of file From 165e4965df7378f8ced1b293d177aba5dce9b231 Mon Sep 17 00:00:00 2001 From: Steve White Date: Wed, 30 Oct 2024 08:57:41 -0500 Subject: [PATCH 20/22] refactored to use consistent api access via api.js --- src/components/Admin.js | 184 ++++++++++++++-------------------- src/components/Boxes.js | 57 +++++------ src/components/ItemDetails.js | 148 +++++++-------------------- src/components/Items.js | 101 +++++++------------ src/components/Login.js | 18 ++-- src/services/api.js | 72 +++++++++++++ 6 files changed, 253 insertions(+), 327 deletions(-) create mode 100644 src/services/api.js diff --git a/src/components/Admin.js b/src/components/Admin.js index 58b82d7..101f15a 100644 --- a/src/components/Admin.js +++ b/src/components/Admin.js @@ -1,8 +1,8 @@ // src/components/Admin.js import React, { useState, useEffect, useRef } from 'react'; -import axios from 'axios'; import { Link, useNavigate } from 'react-router-dom'; import { PRIMARY_COLOR, SECONDARY_COLOR } from '../App'; +import { api } from '../services/api'; import './Admin.css'; // Import the CSS file import { Typography, @@ -22,120 +22,86 @@ import { } from '@mui/material'; -export default function Admin() { - const [users, setUsers] = useState([]); - const [username, setUsername] = useState(''); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const navigate = useNavigate(); - const fileInputRef = useRef(null); - - - useEffect(() => { - axios.get(`${process.env.REACT_APP_API_URL}/api/v1/admin/user`, { - headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } - }) - .then(response => { - setUsers(response.data); - }) - .catch(error => { - console.error(error); - }); - }, []); - - const fetchUsers = async () => { - try { - const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/v1/admin/user`, { - headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } - }); - setUsers(response.data); - } catch (error) { - console.error('Error fetching users:', error); - // Optionally, set an error message - // setErrorMessage('Failed to fetch users. Please try again.'); - } - }; - - const handleCreateUser = async (e) => { - e.preventDefault(); - try { - const response = await axios.post(`${process.env.REACT_APP_API_URL}/api/v1/admin/user`, { - username, - password, - email - }, { - headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } - }); - setUsers([...users, response.data]); - setUsername(''); - setPassword(''); - setEmail(''); - } catch (error) { - console.error(error); - } - }; - - const handleDeleteUser = async (id) => { - try { - await axios.delete(`${process.env.REACT_APP_API_URL}/api/v1/admin/user/${id}`, { - headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } - }); - setUsers(users.filter(user => user.id !== id)); - //setUsers(prevUsers => prevUsers.filter(user => user.id !== id)); + export default function Admin() { + const [users, setUsers] = useState([]); + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const navigate = useNavigate(); + const fileInputRef = useRef(null); + + useEffect(() => { fetchUsers(); - - } catch (error) { - console.error(error); - } - }; - - const handleBackupDatabase = async () => { - try { - const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/v1/admin/db`, { - headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }, - responseType: 'blob' - }); - const blob = new Blob([response.data], { type: 'application/x-sqlite3' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'database.db'; - a.click(); - } catch (error) { - console.error(error); - } - }; - - const handleRestoreDatabase = async (e) => { - e.preventDefault(); - try { - const file = fileInputRef.current.files[0]; - const formData = new FormData(); - formData.append('database', file); - console.log("sending request to restore db") + }, []); - const token = localStorage.getItem('token'); - if (!token) { - throw new Error('No token found in local storage'); + const fetchUsers = async () => { + try { + const response = await api.admin.getUsers(localStorage.getItem('token')); + setUsers(response.data); + } catch (error) { + console.error('Error fetching users:', error); } + }; - const response = await axios.post(`${process.env.REACT_APP_API_URL}/api/v1/admin/db`, formData, { - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'multipart/form-data' + const handleCreateUser = async (e) => { + e.preventDefault(); + try { + const response = await api.admin.createUser(localStorage.getItem('token'), { + username, + password, + email + }); + setUsers([...users, response.data]); + setUsername(''); + setPassword(''); + setEmail(''); + } catch (error) { + console.error(error); + } + }; + + const handleDeleteUser = async (id) => { + try { + await api.admin.deleteUser(localStorage.getItem('token'), id); + setUsers(users.filter(user => user.id !== id)); + fetchUsers(); + } catch (error) { + console.error(error); + } + }; + + const handleBackupDatabase = async () => { + try { + const response = await api.admin.backupDb(localStorage.getItem('token')); + const blob = new Blob([response.data], { type: 'application/x-sqlite3' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'database.db'; + a.click(); + } catch (error) { + console.error(error); + } + }; + + const handleRestoreDatabase = async (e) => { + e.preventDefault(); + try { + const file = fileInputRef.current.files[0]; + const formData = new FormData(); + formData.append('database', file); + + const response = await api.admin.restoreDb(localStorage.getItem('token'), formData); + + if (response.status === 200) { + alert('Database restored successfully'); + navigate('/admin'); } - }); - - if (response.status === 200) { - alert('Database restored successfully'); - navigate('/admin'); - } else { - throw new Error(`Failed to restore database: ${response.statusText}`); + } catch (error) { + console.error(error); } - } catch (error) { - console.error(error); - } - }; + }; + return ( diff --git a/src/components/Boxes.js b/src/components/Boxes.js index f3dc573..0ae3ea1 100644 --- a/src/components/Boxes.js +++ b/src/components/Boxes.js @@ -3,50 +3,43 @@ import React, { useEffect, useState } from 'react'; import { Container, Button, TextField, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material'; import { Delete as DeleteIcon } from '@mui/icons-material'; import { Link as RouterLink } from 'react-router-dom'; // Import Link from react-router-dom -import axios from 'axios'; import { PRIMARY_COLOR, SECONDARY_COLOR } from '../App'; +import { api } from '../services/api'; export default function Boxes({ token }) { const [boxes, setBoxes] = useState([]); const [newBoxName, setNewBoxName] = useState(''); - const apiUrl = `${process.env.REACT_APP_API_URL}/boxes`; - - const debugApi = () => { - if (process.env.DEBUG_API) { - console.log("URL is " + apiUrl); - } - }; - debugApi(); useEffect(() => { - //console.log('Token:' + token); - axios.get(`${process.env.REACT_APP_API_URL}/api/v1/boxes`, { - headers: { Authorization: `Bearer ${token}` } - }).then(response => { - setBoxes(response.data); - }); + fetchBoxes(); }, [token]); - // Log boxes state changes outside the useEffect - useEffect(() => { - //console.log('Boxes updated:', boxes); - }, [boxes]); - - const handleCreateBox = () => { - axios.post(`${process.env.REACT_APP_API_URL}/api/v1/boxes`, { name: newBoxName }, { - headers: { Authorization: `Bearer ${token}` } - }).then(response => { - setBoxes([...boxes, response.data]); - setNewBoxName(''); - }); + const fetchBoxes = async () => { + try { + const response = await api.boxes.getAll(token); + setBoxes(response.data); + } catch (error) { + console.error('Error fetching boxes:', error); + } }; - const handleDeleteBox = (id) => { - axios.delete(`${process.env.REACT_APP_API_URL}/api/v1/boxes/${id}`, { - headers: { Authorization: `Bearer ${token}` } - }).then(() => { + const handleCreateBox = async () => { + try { + const response = await api.boxes.create(token, { name: newBoxName }); + setBoxes([...boxes, response.data]); + setNewBoxName(''); + } catch (error) { + console.error('Error creating box:', error); + } + }; + + const handleDeleteBox = async (id) => { + try { + await api.boxes.delete(token, id); setBoxes(boxes.filter(box => box.ID !== id)); - }); + } catch (error) { + console.error('Error deleting box:', error); + } }; return ( diff --git a/src/components/ItemDetails.js b/src/components/ItemDetails.js index 713d8cb..2659a19 100644 --- a/src/components/ItemDetails.js +++ b/src/components/ItemDetails.js @@ -1,33 +1,24 @@ // src/components/ItemDetails.js import React, { useState, useEffect, useRef, useCallback } from 'react'; import { TextField, Button, Container, Avatar, Tooltip } from '@mui/material'; -import axios from 'axios'; import { PRIMARY_COLOR, SECONDARY_COLOR } from '../App'; -//import { useNavigate } from 'react-router-dom'; // Import useNavigate +import { api } from '../services/api'; export default function ItemDetails({ item, token, onSave, onClose, boxId }) { const [name, setName] = useState(item.name); const [description, setDescription] = useState(item.description); const [imagePath, setImagePath] = useState(item.image_path || ''); - const [imageSrc, setImageSrc] = useState('/images/default.jpg'); // Initial default image - const fileInputRef = useRef(null); // Add this line to define fileInputRef + const [imageSrc, setImageSrc] = useState('/images/default.jpg'); + const fileInputRef = useRef(null); const [imageOverlayVisible, setImageOverlayVisible] = useState(false); - // const navigate = useNavigate(); // Initialize useNavigate - // eslint says boxName is defined but never used, but when I remove it it fails to compile - // because boxName is undefined - // eslint-disable-next-line const [boxName, setBoxName] = useState(''); const [boxes, setBoxes] = useState([]); const [selectedBoxId, setSelectedBoxId] = useState(item.box_id); - //console.log("item.box_id: " + item.box_id); - useEffect(() => { const fetchBoxes = async () => { try { - const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/v1/boxes`, { - headers: { Authorization: `Bearer ${token}` } - }); + const response = await api.boxes.getAll(token); setBoxes(response.data); } catch (error) { console.error('Error fetching boxes:', error); @@ -36,15 +27,12 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) { fetchBoxes(); }, [token]); - const handleBoxChange = (event) => { const newBoxId = event.target.value; - setSelectedBoxId(newBoxId); // Update only this state - //console.log('Selected box ID:', newBoxId); + setSelectedBoxId(newBoxId); }; useEffect(() => { - // Fetch box details only when the selectedBoxId changes const getBoxDetails = async (boxId) => { try { const boxIdNumber = +boxId; @@ -52,134 +40,71 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) { console.error('Invalid boxId:', boxId); return; } - const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/v1/boxes/${boxIdNumber}`, { - headers: { Authorization: `Bearer ${token}` } - }); - setBoxName(response.data.name); + const response = await api.boxes.getAll(token); + const box = response.data.find(b => b.ID === boxIdNumber); + if (box) { + setBoxName(box.name); + } } catch (error) { console.error('Error fetching box details:', error); } }; if (selectedBoxId !== item.box_id) { - getBoxDetails(selectedBoxId); // Fetch when selected box changes - //console.log("selectedBoxId:", selectedBoxId); + getBoxDetails(selectedBoxId); } else if (item.box_id) { - getBoxDetails(item.box_id); // Fetch when boxId exists and selectedBoxId is empty - //console.log("item.box_id:", item.box_id); + getBoxDetails(item.box_id); } - }, [selectedBoxId, token, item.box_id]); // Removed `boxId` from dependencies + }, [selectedBoxId, token, item.box_id]); useEffect(() => { - // Function to fetch image similar to getImageSrc in Items.js - const getImageSrc = (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 the data URL of the default image if image fetch fails - 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); - const dataURL = canvas.toDataURL(); - resolve(dataURL); - }; - img.onerror = reject; - }); - }); + const fetchItemImage = async () => { + try { + const response = await api.items.getImage(token, item.ID); + const reader = new FileReader(); + reader.onload = () => setImageSrc(reader.result); + reader.readAsDataURL(response.data); + } catch (error) { + setImageSrc('/default.jpg'); + } }; - - // Fetch the image when the component mounts or the item changes - getImageSrc(item.ID).then(dataUrl => setImageSrc(dataUrl)); + fetchItemImage(); }, [item.ID, token]); - // const handleCloseItemDetails = () => { - // onClose(); // Call the onClose prop to close the modal - // navigate(`/boxes/${boxId}/items`); // Navigate back to the items list - // }; - const handleImageUpload = useCallback(async () => { + if (!fileInputRef.current.files[0]) return; + const formData = new FormData(); formData.append('image', fileInputRef.current.files[0]); try { - const response = await axios.post(`${process.env.REACT_APP_API_URL}/api/v1/items/${item.ID}/upload`, formData, { - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'multipart/form-data' - } - }); - // Handle successful upload (e.g., show a success message) - //console.log('Image uploaded successfully!', response.data.imagePath); - return response.data.imagePath; // Indicate successful upload + const response = await api.items.uploadImage(token, item.ID, formData); + return response.data.imagePath; } catch (error) { - // Handle upload error (e.g., show an error message) console.error('Image upload failed:', error); } }, [item.ID, token]); - // eslint-disable-next-line - const updateItemBoxId = async () => { - try { - // eslint-disable-next-line - const response = await axios.put(`${process.env.REACT_APP_API_URL}/api/v1/items/${item.id}`, { - box_id: selectedBoxId, - }, { - headers: { Authorization: `Bearer ${token}` } - }); - // Update the item's boxId - item.box_id = selectedBoxId; - } catch (error) { - console.error('Error updating item boxId:', error); - } - }; - - const handleSave = useCallback( async () => { + const handleSave = useCallback(async () => { let imagePath; - // 1. Handle image upload first if a new image is selected if (fileInputRef.current.files[0]) { - // eslint-disable-next-line imagePath = await handleImageUpload(); } - //console.log("Selected box ID:", selectedBoxId) - // 2. Update item details (name, description, etc.) try { - await axios.put(`${process.env.REACT_APP_API_URL}/api/v1/items/${item.ID}`, { + await api.items.update(token, item.ID, { name, description, - box_id: +selectedBoxId, // Ensure the updated selected box is saved - }, { - headers: { Authorization: `Bearer ${token}` } + box_id: +selectedBoxId, }); - onSave(); // Notify parent to refresh items + onSave(); } catch (error) { - // Handle update error console.error('Item update failed:', error); } }, [item.ID, name, description, selectedBoxId, token, onSave, handleImageUpload]); const handleImageError = (e) => { - e.target.src = '/images/default.jpg'; // Fallback to default image on error + e.target.src = '/images/default.jpg'; }; const handleAvatarClick = () => { @@ -194,13 +119,12 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) {

Edit Item: {item.name}

- {/* Display the item image as an avatar */} @@ -242,14 +166,11 @@ export default function ItemDetails({ item, token, onSave, onClose, boxId }) { accept="image/*" ref={fileInputRef} style={{ display: 'none' }} - id="editItemImageUpload" // Unique ID + id="editItemImageUpload" />
- - - ID - Username - Email - Actions - - - - {users.map(user => ( - - {user.ID} - {user.username} - {user.email} - - - + {loadingUsers ? ( + + ) : ( + +
+ + + ID + Username + Email + Actions - ))} - -
- + + + {users.map(user => ( + + {user.ID} + {user.username} + {user.email} + + + + + ))} + + + + )} + + + ))} + + + + )} + setNewBoxName(e.target.value)} + disabled={creatingBox} /> - ); diff --git a/src/components/ItemDetails.js b/src/components/ItemDetails.js index 2659a19..2c37f5f 100644 --- a/src/components/ItemDetails.js +++ b/src/components/ItemDetails.js @@ -1,141 +1,123 @@ -// src/components/ItemDetails.js -import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { TextField, Button, Container, Avatar, Tooltip } from '@mui/material'; +import React, { useState, useEffect, useRef } from 'react'; +import { TextField, Button, Container, Avatar, Tooltip, Alert, CircularProgress } from '@mui/material'; import { PRIMARY_COLOR, SECONDARY_COLOR } from '../App'; +import { useApiCall } from './hooks/useApiCall'; import { api } from '../services/api'; -export default function ItemDetails({ item, token, onSave, onClose, boxId }) { +export default function ItemDetails({ item, token, onSave, onClose }) { const [name, setName] = useState(item.name); const [description, setDescription] = useState(item.description); const [imagePath, setImagePath] = useState(item.image_path || ''); const [imageSrc, setImageSrc] = useState('/images/default.jpg'); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); const fileInputRef = useRef(null); const [imageOverlayVisible, setImageOverlayVisible] = useState(false); - const [boxName, setBoxName] = useState(''); const [boxes, setBoxes] = useState([]); const [selectedBoxId, setSelectedBoxId] = useState(item.box_id); + const { execute: fetchBoxes, loading: loadingBoxes, error: boxesError } = useApiCall(); + const { execute: updateItem, loading: savingItem, error: saveError } = useApiCall(); + const { execute: uploadImage, loading: uploadingImage, error: uploadError } = useApiCall(); + useEffect(() => { - const fetchBoxes = async () => { + const getBoxes = async () => { try { - const response = await api.boxes.getAll(token); + const response = await fetchBoxes(() => api.boxes.getAll(token)); setBoxes(response.data); - } catch (error) { - console.error('Error fetching boxes:', error); - } + } catch (err) {} }; - fetchBoxes(); + getBoxes(); }, [token]); - const handleBoxChange = (event) => { - const newBoxId = event.target.value; - setSelectedBoxId(newBoxId); - }; - useEffect(() => { - const getBoxDetails = async (boxId) => { - try { - const boxIdNumber = +boxId; - if (isNaN(boxIdNumber)) { - console.error('Invalid boxId:', boxId); - return; - } - const response = await api.boxes.getAll(token); - const box = response.data.find(b => b.ID === boxIdNumber); - if (box) { - setBoxName(box.name); - } - } catch (error) { - console.error('Error fetching box details:', error); - } - }; - - if (selectedBoxId !== item.box_id) { - getBoxDetails(selectedBoxId); - } else if (item.box_id) { - getBoxDetails(item.box_id); - } - }, [selectedBoxId, token, item.box_id]); - - useEffect(() => { - const fetchItemImage = async () => { + const loadImage = async () => { + setLoading(true); + setError(null); try { const response = await api.items.getImage(token, item.ID); const reader = new FileReader(); - reader.onload = () => setImageSrc(reader.result); + reader.onload = () => { + setImageSrc(reader.result); + setLoading(false); + }; reader.readAsDataURL(response.data); - } catch (error) { + } catch (err) { setImageSrc('/default.jpg'); + setError(err); + setLoading(false); } }; - fetchItemImage(); + loadImage(); }, [item.ID, token]); - const handleImageUpload = useCallback(async () => { - if (!fileInputRef.current.files[0]) return; + const handleImageUpload = async () => { + if (!fileInputRef.current?.files?.[0]) return null; const formData = new FormData(); formData.append('image', fileInputRef.current.files[0]); try { - const response = await api.items.uploadImage(token, item.ID, formData); + const response = await uploadImage(() => + api.items.uploadImage(token, item.ID, formData) + ); return response.data.imagePath; - } catch (error) { - console.error('Image upload failed:', error); + } catch (err) { + return null; } - }, [item.ID, token]); + }; - const handleSave = useCallback(async () => { - let imagePath; - if (fileInputRef.current.files[0]) { - imagePath = await handleImageUpload(); + const handleSave = async () => { + if (fileInputRef.current?.files?.[0]) { + await handleImageUpload(); } try { - await api.items.update(token, item.ID, { + await updateItem(() => api.items.update(token, item.ID, { name, description, box_id: +selectedBoxId, - }); + })); onSave(); - } catch (error) { - console.error('Item update failed:', error); - } - }, [item.ID, name, description, selectedBoxId, token, onSave, handleImageUpload]); - - const handleImageError = (e) => { - e.target.src = '/images/default.jpg'; + } catch (err) {} }; - const handleAvatarClick = () => { - setImageOverlayVisible(true); - }; - - const handleCloseOverlay = () => { - setImageOverlayVisible(false); + const handleBoxChange = (event) => { + setSelectedBoxId(event.target.value); }; return (

Edit Item: {item.name}

+ + {(error || boxesError || saveError || uploadError) && ( + + {error?.message || boxesError?.message || saveError?.message || uploadError?.message} + + )} + + {loading ? ( + + ) : ( + + setImageOverlayVisible(true)} + /> + + )} - - - {imageOverlayVisible && (
{name} -
)} + setName(e.target.value)} + disabled={savingItem} /> setDescription(e.target.value)} + disabled={savingItem} /> - setImagePath(e.target.value)} - sx={{ display: 'none' }} - /> - - -
-
- - + + -
); } \ No newline at end of file diff --git a/src/components/Items.js b/src/components/Items.js index 461d825..74a6025 100644 --- a/src/components/Items.js +++ b/src/components/Items.js @@ -1,19 +1,9 @@ -import React, { useEffect, useState, useCallback, useRef } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { Container, - List, - ListItem, - ListItemText, - TextField, Button, - IconButton, - Typography, - Avatar, - ListItemAvatar, - Dialog, - DialogTitle, - DialogContent, - DialogActions, + IconButton, + TextField, TableContainer, Table, TableHead, @@ -21,213 +11,52 @@ import { TableCell, TableBody, Box, - Tooltip + Tooltip, + Avatar, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Alert, + CircularProgress } 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 { useParams, useLocation } from 'react-router-dom'; import { PRIMARY_COLOR, SECONDARY_COLOR } from '../App'; +import { useApiCall } from './hooks/useApiCall'; import { api } from '../services/api'; +import ItemDetails from './ItemDetails'; export default function Items({ token }) { - const { id: boxId } = useParams(); + 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 [openAddItemDialog, setOpenAddItemDialog] = useState(false); 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 { execute: fetchItems, loading: loadingItems, error: itemsError } = useApiCall(); + const { execute: createItem, loading: creatingItem, error: createError } = useApiCall(); + const { execute: deleteItem, loading: deletingItem, error: deleteError } = useApiCall(); + const { execute: uploadImage, loading: uploadingImage, error: uploadError } = useApiCall(); - 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, - })); + const url = boxId ? + `${process.env.REACT_APP_API_URL}/api/v1/boxes/${boxId}/items` : + `${process.env.REACT_APP_API_URL}/api/v1/items`; - // 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} - */ - - const handleSaveNewItem = async () => { - try { - const newItemResponse = await api.items.create(token, { - name: newItemName, - description: newItemDescription, - box_id: parseInt(boxId, 10) - }); - - if (newItemResponse.status === 200 && fileInputRef.current.files[0]) { - const newItemId = newItemResponse.data.id; - const imageFile = fileInputRef.current.files[0]; - const newImageName = generateUniqueImageName(imageFile.name); - const formData = new FormData(); - formData.append('image', new File([imageFile], newImageName, { - type: imageFile.type, - })); - - await api.items.uploadImage(token, newItemId, formData); - } - - handleCloseAddItemDialog(); - fetchItems(); - } catch (error) { - 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(() => { - const fetchData = boxId ? - api.items.getByBox(token, boxId) : - api.items.getAll(token); - - fetchData - .then(response => { + useEffect(() => { + const getItems = async () => { + try { + const response = await fetchItems(() => + api.items.getAll(token, boxId) + ); setItems(response.data); + // Fetch images for each item response.data.forEach(item => { api.items.getImage(token, item.ID) @@ -240,61 +69,107 @@ export default function Items({ token }) { })); }; reader.readAsDataURL(response.data); + }) + .catch(() => { + setItemImages(prev => ({ + ...prev, + [item.ID]: '/default.jpg' + })); }); }); - }); + } catch (err) {} + }; + getItems(); }, [token, boxId]); - // lint says I don't need boxId here - useEffect(() => { - fetchItems(); - }, [boxId, token, fetchItems]); + const handleAddItem = () => { + setOpenAddItemDialog(true); + }; - // 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]); - // } + const handleCloseAddItemDialog = () => { + setOpenAddItemDialog(false); + setNewItemName(''); + setNewItemDescription(''); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; - // 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 generateUniqueImageName = (imageName) => { + if (imageName.toLowerCase() === 'image.jpg') { + const randomString = Math.random().toString(36).substr(2, 9); + return `image_${randomString}.jpg`; + } + return imageName; + }; - const handleDeleteItem = (itemId) => { - api.items.delete(token, itemId) - .then(() => { - fetchItems(); - }); + const handleSaveNewItem = async () => { + try { + const newItemResponse = await createItem(() => + api.items.create(token, { + name: newItemName, + description: newItemDescription, + box_id: parseInt(boxId, 10) + }) + ); + + if (newItemResponse.status === 200 && fileInputRef.current?.files?.[0]) { + const imageFile = fileInputRef.current.files[0]; + const newImageName = generateUniqueImageName(imageFile.name); + const formData = new FormData(); + formData.append('image', new File([imageFile], newImageName, { + type: imageFile.type, + })); + + await uploadImage(() => + api.items.uploadImage(token, newItemResponse.data.id, formData) + ); + } + + handleCloseAddItemDialog(); + + // Refresh items list + const response = await fetchItems(() => + api.items.getAll(token, boxId) + ); + setItems(response.data); + } catch (err) {} + }; + + const handleDeleteItem = async (itemId) => { + try { + await deleteItem(() => + api.items.delete(token, itemId) + ); + setItems(items.filter(item => item.ID !== itemId)); + } catch (err) {} }; const handleEditItem = (item) => { setEditingItem(item); }; - const handleSaveEdit = () => { + const handleSaveEdit = async () => { setEditingItem(null); - fetchItems(); + const response = await fetchItems(() => + api.items.getAll(token, boxId) + ); + setItems(response.data); }; + const filteredItems = items.filter(item => + item.name.toLowerCase().includes(searchQuery.toLowerCase()) || + item.description.toLowerCase().includes(searchQuery.toLowerCase()) + ); + return ( + {(itemsError || createError || deleteError || uploadError) && ( + + {itemsError?.message || createError?.message || deleteError?.message || uploadError?.message} + + )} + setSearchQuery(e.target.value)} /> -

Items in Box: {boxName === "Unknown Box" ? "All Boxes" : `${boxName} (${items.length} items)`}

- - - {/* Dialog for adding new item */} Add New Item @@ -340,75 +213,80 @@ export default function Items({ token }) { 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" + style={{ display: 'block', margin: '10px 0' }} /> - - {editingItem ? ( + + {editingItem && ( setEditingItem(null)} boxId={boxId} - /> + /> + )} + + {loadingItems ? ( + ) : ( - - - - Image - Name - Description - Actions - - - - {items - .filter(item => - item.name.toLowerCase().includes(searchQuery.toLowerCase()) || - item.description.toLowerCase().includes(searchQuery.toLowerCase()) - ) - .map((item) => ( +
+ + + Image + Name + Description + Actions + + + + {filteredItems.map((item) => ( {item.name} {item.description} - - - handleEditItem(item)} - size="large" - sx={{ mr: 1 }} - > - - - - - handleDeleteItem(item.ID)} - size="large" - color="error" - > - - - - + + + + handleEditItem(item)} + size="large" + sx={{ mr: 1 }} + > + + + + + handleDeleteItem(item.ID)} + size="large" + color="error" + disabled={deletingItem} + > + {deletingItem ? : } + + + + ))} - -
-
+ + + )}
); diff --git a/src/components/Login.js b/src/components/Login.js index e94e171..e5a8b45 100644 --- a/src/components/Login.js +++ b/src/components/Login.js @@ -1,27 +1,26 @@ // src/components/Login.js import React, { useState } from 'react'; -import { Button, TextField, Container, Typography, Alert } from '@mui/material'; -import { useNavigate } from 'react-router-dom'; // Import useNavigate +import { Button, TextField, Container, Typography, Alert, CircularProgress } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; import { PRIMARY_COLOR, SECONDARY_COLOR } from '../App'; import { api } from '../services/api'; - +import { useApiCall } from './hooks/useApiCall'; export default function Login({ setToken }) { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); - const [loginError, setLoginError] = useState(false); const navigate = useNavigate(); + const { execute, loading, error } = useApiCall(); const handleLogin = async (e) => { e.preventDefault(); - setLoginError(false); try { - const response = await api.login({ username, password }); + const response = await execute(() => api.login({ username, password })); setToken(response.data.token); navigate('/boxes'); - } catch (error) { - console.error('Login failed', error); - setLoginError(true); + } catch (err) { + // Error handling is now managed by useApiCall + console.error('Login attempt failed'); } }; @@ -29,9 +28,10 @@ export default function Login({ setToken }) { Login - {/* Display error message if loginError is true */} - {loginError && ( - Login Failed + {error && ( + + {error.status === 401 ? 'Invalid username or password' : error.message} + )}
@@ -52,10 +52,17 @@ export default function Login({ setToken }) { value={password} onChange={(e) => setPassword(e.target.value)} /> -
); -} +} \ No newline at end of file diff --git a/src/components/hooks/useApiCall.js b/src/components/hooks/useApiCall.js new file mode 100644 index 0000000..eaa1bc7 --- /dev/null +++ b/src/components/hooks/useApiCall.js @@ -0,0 +1,43 @@ +import { useState } from 'react'; + +/** + * useApiCall hook. + * + * This hook helps to wrap API calls in a way that makes it easier to manage + * loading state and errors. + * + * @returns {Object} An object with `execute`, `loading`, and `error` + * properties. `execute` is a function that wraps the API call and sets the + * `loading` and `error` states accordingly. `loading` is a boolean that is + * `true` while the API call is in progress and `false` otherwise. `error` is + * `null` if the API call was successful and an error object if the API call + * failed. + * + * @example + * const { execute, loading, error } = useApiCall(); + * + * const fetchData = async () => { + * const response = await execute(api.items.getAll()); + * // Do something with the response + * }; + */ +export const useApiCall = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const execute = async (apiCall) => { + setLoading(true); + setError(null); + try { + const result = await apiCall(); + return result; + } catch (err) { + setError(err); + throw err; + } finally { + setLoading(false); + } + }; + + return { execute, loading, error }; + }; \ No newline at end of file diff --git a/src/services/errorHandler.js b/src/services/errorHandler.js new file mode 100644 index 0000000..96cd27f --- /dev/null +++ b/src/services/errorHandler.js @@ -0,0 +1,36 @@ +// src/utils/errorHandler.js +import axios from 'axios'; + +export class ApiError extends Error { + constructor(message, status, details = {}) { + super(message); + this.status = status; + this.details = details; + } + } + + // Enhanced API client with error handling + export const createApiClient = () => { + const client = axios.create({ + baseURL: process.env.REACT_APP_API_URL + }); + + client.interceptors.response.use( + response => response, + error => { + if (error.response?.status === 401) { + localStorage.removeItem('token'); + window.location.href = '/login'; + return Promise.reject(new ApiError('Session expired', 401)); + } + + return Promise.reject(new ApiError( + error.response?.data?.message || 'An error occurred', + error.response?.status, + error.response?.data + )); + } + ); + + return client; + }; From efa24c3f8457bf08cf0e246380be10e2460dd160 Mon Sep 17 00:00:00 2001 From: Steve White Date: Wed, 30 Oct 2024 10:17:52 -0500 Subject: [PATCH 22/22] Added errorboundary, memoized items for performance. --- src/App.js | 11 ++- src/components/ErrorBoundary.js | 36 +++++++++ src/components/Items.js | 127 ++++++++++++++++++-------------- 3 files changed, 116 insertions(+), 58 deletions(-) create mode 100644 src/components/ErrorBoundary.js diff --git a/src/App.js b/src/App.js index d8b0484..52848b8 100644 --- a/src/App.js +++ b/src/App.js @@ -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 ( - - - - + + + + + + ); } diff --git a/src/components/ErrorBoundary.js b/src/components/ErrorBoundary.js new file mode 100644 index 0000000..09bb2d1 --- /dev/null +++ b/src/components/ErrorBoundary.js @@ -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 ( + + + Something went wrong + + + + ); + } + return this.props.children; + } +} + +export default ErrorBoundary; \ No newline at end of file diff --git a/src/components/Items.js b/src/components/Items.js index 74a6025..8af44ee 100644 --- a/src/components/Items.js +++ b/src/components/Items.js @@ -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 }) => ( + + + + + {item.name} + {item.description} + + + + onEdit(item)} size="large" sx={{ mr: 1 }}> + + + + + onDelete(item.ID)} size="large" color="error"> + + + + + + +)); + 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 }) { Actions - - {filteredItems.map((item) => ( - - - - - {item.name} - {item.description} - - - - handleEditItem(item)} - size="large" - sx={{ mr: 1 }} - > - - - - - handleDeleteItem(item.ID)} - size="large" - color="error" - disabled={deletingItem} - > - {deletingItem ? : } - - - - - - ))} - + + {filteredItems.map((item) => ( + + ))} + )}