From 8fbfea2089de18e1b806a208b13837ed8b3dcad7 Mon Sep 17 00:00:00 2001 From: Greg Bowne Date: Sat, 1 Feb 2025 16:50:09 -0800 Subject: [PATCH] refactoring Library.jsx into more manageable smaller components --- src/components/BooksTable.jsx | 77 ++ src/components/Library/Library.jsx | 1221 ++----------------- src/components/Library/hooks/useLibrary.jsx | 128 ++ src/components/Library/hooks/useModals.jsx | 57 + 4 files changed, 343 insertions(+), 1140 deletions(-) create mode 100644 src/components/BooksTable.jsx create mode 100644 src/components/Library/hooks/useLibrary.jsx create mode 100644 src/components/Library/hooks/useModals.jsx diff --git a/src/components/BooksTable.jsx b/src/components/BooksTable.jsx new file mode 100644 index 0000000..0c1660d --- /dev/null +++ b/src/components/BooksTable.jsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { + Table, + TableHead, + TableBody, + TableRow, + TableCell, + TableContainer, + TableSortLabel, + Paper +} from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import BookTableRow from './BookTableRow'; + +const BooksTable = ({ + rows, + loading, + sortOrder, + sortingColumn, + onSort, + onRemoveBook, + onAddToLibrary, + onOpenReviewModal, + onOpenReadReviewModal, + children +}) => { + const { t } = useTranslation(); + const columns = [ + { id: 'title', label: 'title' }, + { id: 'author', label: 'author' }, + { id: 'category', label: 'category' }, + { id: 'publisher', label: 'publisher' }, + { id: 'ISBN', label: 'isbn' }, + { id: 'year', label: 'year' }, + { id: 'edition', label: 'edition' } + ]; + + return ( + + + + + {t('home.headings.action')} + {columns.map((column) => ( + + onSort(column.id, sortOrder ? 'asc' : 'desc')} + > + {t(`home.headings.${column.label}`)} + + + ))} + {t('home.headings.review')} + + + + {children} + {rows.map((row, idx) => ( + + ))} + +
+
+ ); +}; + +export default BooksTable; \ No newline at end of file diff --git a/src/components/Library/Library.jsx b/src/components/Library/Library.jsx index a290803..fbc5459 100644 --- a/src/components/Library/Library.jsx +++ b/src/components/Library/Library.jsx @@ -1,1155 +1,96 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import { orderBy } from 'lodash'; -import Box from '@mui/material/Box'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import CircularProgress from '@mui/material/CircularProgress'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; -import StarIcon from '@mui/icons-material/Star'; -import Paper from '@mui/material/Paper'; -import IconButton from '@mui/material/IconButton'; -import TableSortLabel from '@mui/material/TableSortLabel'; -import Typography from '@mui/material/Typography'; -import AddIcon from '@mui/icons-material/Add'; -import DeleteIcon from '@mui/icons-material/Delete'; -// import RemoveIcon from '@mui/icons-material/Remove'; -import Dialog from '@mui/material/Dialog'; -import DialogActions from '@mui/material/DialogActions'; -import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; -import DialogTitle from '@mui/material/DialogTitle'; -import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; -import EditIcon from '@mui/icons-material/Edit'; -import ShareIcon from '@mui/icons-material/Share'; import { useTranslation } from 'react-i18next'; -/* import { TableVirtuoso } from 'react-virtuoso'; */ -import './Library.css'; -import Classes from './Library.module.css'; -// import rows from './data.json'; -import { Button, Modal, TextField } from '@mui/material'; -import CloseIcon from '@mui/icons-material/Close'; -import Snackbar from '@mui/material/Snackbar'; -import ISBN from 'isbn-validate'; -import { Rating } from 'react-simple-star-rating'; -import axios from 'axios'; -export default function Library({ filter, setFilter }) { - const [myRows, setMyRows] = useState([]); - const [visibleContent, setVisibleContent] = useState(-1); - const [showSnackBar, handleSnackBar] = useState(false); - const [isDeleted, setIsDeleted] = useState(false); - const [removedItemName, setRemovedItemName] = useState(''); - - //table sorting states - const [sortOrder, setSortOrder] = useState(false); - const [sortingColumn, setSortingColumn] = useState(''); - - //Modal states - const [showModal, handleModalBox] = useState(false); - const [loading, setLoading] = useState(false); - const [bookUploadError, setBookUploadError] = useState(''); - const [name, setName] = useState(''); - const [author, setAuthor] = useState(''); - const [category, setCategory] = useState(''); - const [publisher, setPublisher] = useState(''); - const [isbn, setIsbn] = useState(''); - const [year, setYear] = useState(''); - const [edition, setEdition] = useState(''); - const [error, setError] = useState(false); - const [blankEntry, setBlankEntry] = useState(false); +import { CircularProgress } from '@mui/material'; +import AddBookButton from './components/AddBookButton'; +import BooksTable from './components/BooksTable'; +import { useLibrary } from './hooks/useLibrary'; +import { useModals } from './hooks/useModals'; +import { + AddBookModal, + ReviewModal, + ReadReviewModal, + LibraryAddModal +} from './components/Modals'; +import NotificationSnackbar from './components/NotificationSnackbar'; - //Book review modal states - const [showReviewModal, handleReviewModal] = useState(false); - const [isReviewAdded, handleReviewAdded] = useState(false); - const [bookName, setBookName] = useState(''); - const [bookReview, setBookReview] = useState(''); - const [rating, setRating] = useState(0); - - //Book read review modal states - const [enableReviewModal, setEnableReviewModal] = useState(false); - const [book, setBook] = useState({}); - //get loggedIn user email to send with bookObj for userId on new book entry - const userEmail = JSON.parse(localStorage.getItem('user')).email; - //get loggedIn user id to match with book userId - const userId = JSON.parse(localStorage.getItem('user'))._id; - //This state helps us for two way in input filed - const [isAdded, handleIsAdded] = useState(false); +export default function Library({ filter, setFilter }) { const { t } = useTranslation(); - //Add to library modal - const [isLibraryModalOpen, setIsLibraryModalOpen] = useState(false); - // prettier-ignore - const [addToPersonalLibraryMessage, setAddToPersonalLibraryMessage] = useState(null); - const removeBookByName = async (row) => { - try { - setLoading(true); - const response = await axios.delete( - `http://localhost:3001/api/books/${row._id}`, - { - data: { - bookId: row._id, - userId: row.userId, - userEmail, - }, - } - ); - if (response.status === 200) { - setRemovedItemName('removed ' + row.title + ' successfully'); - setVisibleContent(-1); - fetchBooksFromDB(); - setIsDeleted(true); - } - // You can also update your state here to reflect the changes - } catch (err) { - setLoading(false); - const errorMsg = err.response.data.message; - setBookUploadError(errorMsg); - handleSnackBar(true); - } - - setLoading(false); - }; - - const addItemToTable = async (e) => { - e.preventDefault(); - try { - setLoading(true); - if ( - name && - author && - category && - publisher && - isbn && - year && - edition - ) { - let bookObj = { - name, - author, - category, - publisher, - isbn, - year, - edition, - reviews: [], - userEmail, - }; - - const response = await axios.post( - 'http://localhost:3001/api/books/newbook', - { bookObj } - ); - - if (response.status === 200) { - handleIsAdded(true); - resetBookState(); - handleModalBox(false); - fetchBooksFromDB(); - } - } else { - setBlankEntry(true); - } - } catch (err) { - const errorMsg = err.response.data.message; - setBookUploadError(errorMsg); - handleSnackBar(true); - } - setLoading(false); - }; - - const addBookToPersonalLibrary = async (book) => { - try { - const response = await axios.post( - 'http://localhost:3001/api/books/add-book-to-personal-library', - { book } - ); - - setAddToPersonalLibraryMessage(response.data.payload); - setIsLibraryModalOpen(true); - } catch (error) { - console.log(error); - } - }; - - const addBooksToDB = async (fileBooks) => { - try { - const userData = JSON.parse(localStorage.getItem('user')); - - // Check if userData is not null before accessing its properties - if (userData) { - // Iterate through each book in the file - for (const bookObj of fileBooks) { - const userId = userData._id; - const userEmail = userData.email; - - const { - title, - author, - category, - publisher, - ISBN, - year, - edition, - } = bookObj; - console.log('Adding book', bookObj); - - // Send a request to add the book to the backend - await axios.post( - 'http://localhost:3001/api/books/newbook', - { - bookObj: { - userId, - userEmail, - name: title, - author, - category, - publisher, - isbn: ISBN, - year, - edition, - reviews: [], - }, - } - ); - } - - // After adding all books, fetch books from the backend - fetchBooksFromDB(); - } else { - console.error('Error adding books from file:', error); - } - } catch (error) { - // Handle error - console.error('Error adding books from file:', error); - } - }; - - const fetchBooksFromDB = async () => { - try { - setLoading(true); - - // Fetch data from the database - const response = await axios.get( - 'http://localhost:3001/api/books/getall' - ); - const books = response.data; - console.log(books); - // If the database has no books, fetch data from the file - if (books.length === 0) { - const fileResponse = await axios.get( - 'http://localhost:3001/api/books/filedata' - ); - const fileBooks = fileResponse.data; - - // Assuming fileBooks is an array of books from the file - setMyRows([...fileBooks]); - - // Add books to the database - await addBooksToDB(fileBooks); - } else { - // Database has books, use them - setMyRows([...books]); - } - } catch (error) { - // Handle error - console.error('Error fetching books:', error); - } finally { - setLoading(false); - } - }; - - const resetBookState = () => { - setName(''); - setAuthor(''); - setCategory(''); - setPublisher(''); - setIsbn(''); - setYear(''); - setEdition(''); - }; - - const closeSnackBar = () => { - handleSnackBar(false); - handleIsAdded(false); - handleReviewAdded(false); - setBlankEntry(false); - setIsDeleted(false); - }; - const action = ( - - - - - - ); - - const style = { - position: 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: 400, - bgcolor: 'background.paper', - border: '2px solid #000', - boxShadow: 24, - p: 4, - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', - alignItems: 'center', - }; - - //Check if publishing year is a four digit number - const handleYearChange = (event) => { - const value = event.target.value; - setYear(value); - - if (value.length === 4 && !isNaN(value)) { - setError(false); - } else { - setError(true); - } - }; - - // books table sort - const handleSort = (columnName, order) => { - if (columnName === 'title') { - const sortedRows = orderBy(myRows, ['title'], [order]); - setMyRows(sortedRows); - } else if (columnName === 'author') { - const sortedRows = orderBy(myRows, ['author'], [order]); - setMyRows(sortedRows); - } else if (columnName === 'category') { - const sortedRows = orderBy(myRows, ['category'], [order]); - setMyRows(sortedRows); - } else if (columnName === 'publisher') { - const sortedRows = orderBy(myRows, ['publisher'], [order]); - setMyRows(sortedRows); - } else if (columnName === 'year') { - const sortedRows = orderBy(myRows, ['year'], [order]); - setMyRows(sortedRows); - } else if (columnName === 'ISBN') { - const sortedRows = orderBy(myRows, ['ISBN'], [order]); - setMyRows(sortedRows); - } else if (columnName === 'edition') { - const sortedRows = orderBy(myRows, ['edition'], [order]); - setMyRows(sortedRows); - } - }; - - //Check if ISBN is valid - const handleISBNChange = (event) => { - const value = event.target.value; - setIsbn(value); - - if (ISBN.Validate(value)) { - setError(false); - } else { - setError(true); - } - }; - - //adds the review for each book - const handleBookReview = () => { - const updatedRows = myRows.map((book) => { - if (book.name === bookName) { - return { - ...book, - reviews: [ - ...book.reviews, - { starRating: rating, textReview: bookReview }, - ], - }; - } else { - return { ...book }; - } - }); - setMyRows([...updatedRows]); - handleReviewAdded(true); - setBookReview(''); - setRating(0); - }; - - const onPointerMove = (value, index) => { - setRating(value); - }; - - const starGenerator = (count) => { - const stars = []; - for (let i = 1; i <= count; i++) { - stars.push(); - } - return stars; - }; - - React.useEffect(() => { - const filteredRow = () => { - if (filter) { - //searching can be improved - const newRow = myRows.filter( - (row) => - (filter && row.title.includes(filter)) || - (row.category && row.category.includes(filter)) || - row.author.includes(filter) - ); - - if (newRow.length > 0) { - setMyRows(newRow); - } else { - setMyRows(myRows); - } - } else { - setMyRows(myRows); - } - }; - filteredRow(); - return () => { - setMyRows([]); - setFilter(''); - }; - }, [filter, setFilter, myRows]); - //remove the empty dependency array - useEffect(() => { - fetchBooksFromDB(); - }); - - // open hidden content when ellipsis icon is clicked - const handleShowMore = (index) => { - setVisibleContent((prevIndex) => (prevIndex === index ? -1 : index)); - }; + const { + myRows, + loading, + handleSort, + sortOrder, + sortingColumn, + setSortOrder, + setSortingColumn, + removeBookByName, + addItemToTable, + addBookToPersonalLibrary + } = useLibrary(filter, setFilter); + + const { + notifications, + modalsState, + modalActions, + reviewState, + handleBookReview, + onPointerMove + } = useModals(); + + if (!myRows.length) { + return ( +
+ +
+ ); + } return ( <> - - - + + - - {/* //book review modal start// */} - { - handleReviewModal(false); - }} - > - - - {t('modal.review.title')} {bookName} - - + - { - setBookReview(e.target.value); - }} - /> + - {isReviewAdded ? ( - <> - - - ) : ( - <> - - - )} - - - {/* //book review modal end// //book read review modal start// */} -
- { - setEnableReviewModal(false); - }} - aria-labelledby='alert-dialog-title' - aria-describedby='alert-dialog-description' - > - - {t('modal.review.readReview')} {book?.title} - + - - - {book?.reviews?.length > 0 ? ( - book?.reviews?.map((item, index) => { - return ( -
- {starGenerator(item?.starRating)} - - {item?.textReview} - -
- ); - }) - ) : ( - - {t('modal.review.noReview')}{' '} - {book?.title} - - )} -
-
- - - -
-
- {/* //book read reviews modal end// */} - { - handleModalBox(false); - }} + - - - {t('home.headings.item')} - -
{ - addItemToTable(e); - }} - > - { - setName(e.target.value); - }} - defaultValue='' - /> - { - setAuthor(e.target.value); - }} - fullWidth - style={{ marginTop: '10px' }} - /> - { - setCategory(e.target.value); - }} - fullWidth - style={{ marginTop: '10px' }} - /> - { - setPublisher(e.target.value); - }} - fullWidth - style={{ marginTop: '10px' }} - /> - - - { - setEdition(e.target.value); - }} - fullWidth - style={{ marginTop: '10px' }} - /> - {isAdded ? ( - <> - - - ) : ( - <> - - - )} - -
-
- {/* Add to Library modal */} - { - setIsLibraryModalOpen(false); - }} - > - {/* prettier-ignore */} - - - { addToPersonalLibraryMessage? 'Book successfully added to personal library':'Book already exist in personal library' } - - - - - - - {myRows.length > 0 ? ( - - - - - - {t('home.headings.action')} - - - { - setSortOrder(!sortOrder); - setSortingColumn('title'); - handleSort( - 'title', - sortOrder ? 'asc' : 'desc' - ); - }} - > - {t('home.headings.title')} - - - - { - setSortOrder(!sortOrder); - setSortingColumn('author'); - handleSort( - 'author', - sortOrder ? 'asc' : 'desc' - ); - }} - > - {t('home.headings.author')} - {' '} - - - {' '} - { - setSortOrder(!sortOrder); - setSortingColumn('category'); - handleSort( - 'category', - sortOrder ? 'asc' : 'desc' - ); - }} - > - {t('home.headings.category')} - {' '} - - - {' '} - { - setSortOrder(!sortOrder); - setSortingColumn('publisher'); - handleSort( - 'publisher', - sortOrder ? 'asc' : 'desc' - ); - }} - > - {t('home.headings.publisher')} - {' '} - - - {' '} - { - setSortOrder(!sortOrder); - setSortingColumn('ISBN'); - handleSort( - 'ISBN', - sortOrder ? 'asc' : 'desc' - ); - }} - > - {t('home.headings.isbn')} - - - - {' '} - { - setSortOrder(!sortOrder); - setSortingColumn('year'); - handleSort( - 'year', - sortOrder ? 'asc' : 'desc' - ); - }} - > - {t('home.headings.year')} - {' '} - - - { - setSortOrder(!sortOrder); - setSortingColumn('edition'); - handleSort( - 'edition', - sortOrder ? 'asc' : 'desc' - ); - }} - > - {t('home.headings.edition')} - - - - {t('home.headings.review')} - - - - - - - - - - {myRows.map((row, idx) => ( - - {userId === row.userId ? ( - -
- handleShowMore(idx) - } - > - - - - - {t('home.more')} - -
- - - setVisibleContent(-1) - } - > - {t('home.buttons.close')} - - - { - removeBookByName(row); - }} - > - {t('home.buttons.delete')} - {loading ? ( - - ) : ( - - )} - - - {t('home.buttons.edit')} - - - - {t('home.buttons.share')} - - - -
- ) : ( - - )} - - {row.title} - - - {row.author} - - - {row.category} - - - {row.publisher} - - - {row.ISBN} - - - {row.year} - - - {row.edition + ' Edition'} - - - - - - -
- ))} -
-
-
- ) : ( -
- -
- )} + modalActions.handleModalBox(true)} /> + ); } @@ -1157,4 +98,4 @@ export default function Library({ filter, setFilter }) { Library.propTypes = { filter: PropTypes.string, setFilter: PropTypes.func, -}; +}; \ No newline at end of file diff --git a/src/components/Library/hooks/useLibrary.jsx b/src/components/Library/hooks/useLibrary.jsx new file mode 100644 index 0000000..1a3b382 --- /dev/null +++ b/src/components/Library/hooks/useLibrary.jsx @@ -0,0 +1,128 @@ +import { useState, useEffect } from 'react'; +import { orderBy } from 'lodash'; +import axios from 'axios'; + +export const useLibrary = (filter, setFilter) => { + const [myRows, setMyRows] = useState([]); + const [loading, setLoading] = useState(false); + const [sortOrder, setSortOrder] = useState(false); + const [sortingColumn, setSortingColumn] = useState(''); + + const handleSort = (columnName, order) => { + const sortConfig = { + title: 'title', + author: 'author', + category: 'category', + publisher: 'publisher', + year: 'year', + ISBN: 'ISBN', + edition: 'edition' + }; + + if (sortConfig[columnName]) { + const sortedRows = orderBy(myRows, [sortConfig[columnName]], [order]); + setMyRows(sortedRows); + } + }; + + const removeBookByName = async (row) => { + try { + setLoading(true); + const response = await axios.delete( + `http://localhost:3001/api/books/${row._id}`, + { + data: { + bookId: row._id, + userId: row.userId, + userEmail: JSON.parse(localStorage.getItem('user')).email, + }, + } + ); + if (response.status === 200) { + fetchBooksFromDB(); + } + } catch (err) { + console.error('Error removing book:', err); + } finally { + setLoading(false); + } + }; + + const addItemToTable = async (bookData) => { + try { + setLoading(true); + const response = await axios.post( + 'http://localhost:3001/api/books/newbook', + { bookObj: bookData } + ); + if (response.status === 200) { + fetchBooksFromDB(); + return true; + } + } catch (err) { + console.error('Error adding book:', err); + return false; + } finally { + setLoading(false); + } + }; + + const addBookToPersonalLibrary = async (book) => { + try { + const response = await axios.post( + 'http://localhost:3001/api/books/add-book-to-personal-library', + { book } + ); + return response.data.payload; + } catch (error) { + console.error('Error adding to personal library:', error); + return null; + } + }; + + const fetchBooksFromDB = async () => { + try { + setLoading(true); + const response = await axios.get('http://localhost:3001/api/books/getall'); + setMyRows(response.data); + } catch (error) { + console.error('Error fetching books:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchBooksFromDB(); + }, []); + + useEffect(() => { + if (filter) { + const filteredRows = myRows.filter( + (row) => + row.title.includes(filter) || + (row.category && row.category.includes(filter)) || + row.author.includes(filter) + ); + setMyRows(filteredRows.length > 0 ? filteredRows : myRows); + } + + return () => { + setMyRows([]); + setFilter(''); + }; + }, [filter, setFilter]); + + return { + myRows, + loading, + handleSort, + sortOrder, + sortingColumn, + setSortOrder, + setSortingColumn, + removeBookByName, + addItemToTable, + addBookToPersonalLibrary + }; +}; \ No newline at end of file diff --git a/src/components/Library/hooks/useModals.jsx b/src/components/Library/hooks/useModals.jsx new file mode 100644 index 0000000..0348998 --- /dev/null +++ b/src/components/Library/hooks/useModals.jsx @@ -0,0 +1,57 @@ +import { useState } from 'react'; + +export const useModals = () => { + const [notifications, setNotifications] = useState({ + isDeleted: false, + isAdded: false, + blankEntry: false, + showSnackBar: false, + removedItemName: '', + bookUploadError: '' + }); + + const [modalsState, setModalsState] = useState({ + showModal: false, + showReviewModal: false, + enableReviewModal: false, + isLibraryModalOpen: false, + addToPersonalLibraryMessage: null + }); + + const [reviewState, setReviewState] = useState({ + bookName: '', + bookReview: '', + rating: 0, + book: {} + }); + + const modalActions = { + handleModalBox: (show) => setModalsState(prev => ({ ...prev, showModal: show })), + handleReviewModal: (show) => setModalsState(prev => ({ ...prev, showReviewModal: show })), + setEnableReviewModal: (show) => setModalsState(prev => ({ ...prev, enableReviewModal: show })), + setIsLibraryModalOpen: (show) => setModalsState(prev => ({ ...prev, isLibraryModalOpen: show })) + }; + + const handleBookReview = (review) => { + setReviewState(prev => ({ + ...prev, + bookReview: review + })); + }; + + const onPointerMove = (value) => { + setReviewState(prev => ({ + ...prev, + rating: value + })); + }; + + return { + notifications, + modalsState, + modalActions, + reviewState, + handleBookReview, + onPointerMove + }; +}; \ No newline at end of file