From 3e0c3067db5af3541e36d8e607b8e514a036fcb4 Mon Sep 17 00:00:00 2001
From: Kot Anton <707myemail@gmail.com>
Date: Mon, 23 Dec 2024 20:58:52 -0500
Subject: [PATCH] implemented bookList modal dialog with book details
---
src/App.js | 2 +-
src/{ => api}/services/firebaseConfig.js | 0
.../BookListSection/BookListSection.jsx | 39 ++++++-
src/components/BookListSection/book/Book.css | 40 +++++--
src/components/BookListSection/book/Book.jsx | 19 +++-
.../bookListModal/BookListModal.css | 107 ++++++++++++++++++
.../bookListModal/BookListModal.jsx | 77 +++++++++++++
src/components/common/button/Button.css | 13 ++-
src/components/common/button/Button.jsx | 14 ++-
src/components/common/modal/Modal.css | 1 +
src/components/common/modal/Modal.jsx | 6 +-
src/components/header/Header.jsx | 2 +-
.../searchBookSection/SearchBookSection.jsx | 1 +
.../searchBookModal/SearchBookModal.css | 1 +
src/components/user/login/Login.jsx | 2 +-
src/components/user/signup/Signup.jsx | 2 +-
src/redux/slices/booksSlice.js | 2 +-
17 files changed, 299 insertions(+), 29 deletions(-)
rename src/{ => api}/services/firebaseConfig.js (100%)
create mode 100644 src/components/BookListSection/bookListModal/BookListModal.css
create mode 100644 src/components/BookListSection/bookListModal/BookListModal.jsx
diff --git a/src/App.js b/src/App.js
index 5b4693a..502ea78 100644
--- a/src/App.js
+++ b/src/App.js
@@ -12,7 +12,7 @@ import Login from './components/user/login/Login'
import Signup from './components/user/signup/Signup'
import Error from './components/error/Error'
import { syncLoadBook } from './redux/slices/booksSlice'
-import { auth } from './services/firebaseConfig'
+import { auth } from './api/services/firebaseConfig'
import './App.css'
function App() {
diff --git a/src/services/firebaseConfig.js b/src/api/services/firebaseConfig.js
similarity index 100%
rename from src/services/firebaseConfig.js
rename to src/api/services/firebaseConfig.js
diff --git a/src/components/BookListSection/BookListSection.jsx b/src/components/BookListSection/BookListSection.jsx
index 1589b4e..fe330e1 100644
--- a/src/components/BookListSection/BookListSection.jsx
+++ b/src/components/BookListSection/BookListSection.jsx
@@ -1,4 +1,4 @@
-import { useEffect } from 'react'
+import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { setError } from '../../redux/slices/errorSlice'
import Book from './book/Book'
@@ -13,12 +13,15 @@ import {
selectTitleFilter,
selectAuthorsFilter,
} from '../../redux/slices/filterSlice'
+import BookListModal from './bookListModal/BookListModal'
const BookListSection = ({ 'data-testid': testId }) => {
const dispatch = useDispatch()
const books = useSelector(selectBook)
const titleFilter = useSelector(selectTitleFilter)
const authorsFilter = useSelector(selectAuthorsFilter)
+ const [isModalOpen, setIsModalOpen] = useState(false)
+ const [modalBook, setModalBook] = useState(null)
useEffect(() => {
if (process.env.NODE_ENV !== 'test') {
@@ -26,11 +29,22 @@ const BookListSection = ({ 'data-testid': testId }) => {
}
}, [dispatch])
+ useEffect(() => {
+ if (isModalOpen && modalBook) {
+ const updatedBookState = books.find((book) => book.id === modalBook.id)
+ if (updatedBookState) {
+ setModalBook(updatedBookState)
+ }
+ }
+ }, [books, isModalOpen, modalBook])
+
const handleDeleteBook = (id) => {
const book = books.find((book) => book.id === id)
if (book) {
if (!book.isFavorite) {
dispatch(syncDeleteBook(id))
+ setIsModalOpen(false)
+ setModalBook(null)
} else {
dispatch(setError("Can't Delete Favorite Book!"))
}
@@ -41,6 +55,19 @@ const BookListSection = ({ 'data-testid': testId }) => {
dispatch(syncToggleFavorite(id))
}
+ const handleOpenBookModal = (id) => {
+ const book = books.find((book) => book.id === id)
+ if (book) {
+ setIsModalOpen(true)
+ setModalBook(book)
+ }
+ }
+
+ const handleCloseBookModal = () => {
+ setIsModalOpen(false)
+ setModalBook(null)
+ }
+
const filteredBooksArr = books.filter((book) => {
return (
book.title.toLowerCase().includes(titleFilter.toLowerCase()) &&
@@ -81,11 +108,21 @@ const BookListSection = ({ 'data-testid': testId }) => {
bookAuthor={highlightFilterMatch(book.authors, authorsFilter)}
onHandleDeleteBook={handleDeleteBook}
onToggleFavoriteBook={toggleFavoriteBook}
+ onClick={() => handleOpenBookModal(book.id)}
data-testid={`bookList_item id=${book.id}`}
/>
))
)}
+ {isModalOpen && (
+
+ )}
)
}
diff --git a/src/components/BookListSection/book/Book.css b/src/components/BookListSection/book/Book.css
index d560585..6dbc9ce 100644
--- a/src/components/BookListSection/book/Book.css
+++ b/src/components/BookListSection/book/Book.css
@@ -5,20 +5,22 @@ li[data-testid^='bookList_item'] {
gap: 1rem;
align-items: center;
padding: 0.5rem 1rem;
+ cursor: pointer;
background-color: #fff;
border-bottom: 1px solid #ccc;
+ transition: transform 0.3s, background-color 0.3s ease;
}
li[data-testid*='bookList_item']:nth-child(even) {
background-color: #f2f2f2;
}
-li[data-testid*='bookList_item']:hover {
- background-color: #bcd0fcfe;
+li[data-testid^='bookList_item']:hover {
+ background-color: #eaf3ff;
+ transform: scale(1.005);
}
/* ----- */
-
span[data-testid='book_index'] {
text-align: center;
min-width: 2rem;
@@ -35,8 +37,7 @@ div[data-testid='book_title_and_author_wrapper'] {
overflow: hidden;
}
-/* -----title */
-
+/* Title */
div[data-testid='book_title_wrapper'] {
white-space: nowrap;
overflow: hidden;
@@ -54,8 +55,7 @@ span[data-testid='book_title']::after {
content: '"';
}
-/* -----author */
-
+/* Author */
div[data-testid='book_author_wrapper'] {
white-space: nowrap;
overflow: hidden;
@@ -65,31 +65,49 @@ div[data-testid='book_author_wrapper'] {
span[data-testid='book_author'] {
padding-left: 0.5rem;
font-weight: bold;
+ transition: color 0.2s ease;
}
-/* -----action buttons */
+span[data-testid='book_author']:hover {
+ color: #007bff;
+}
+/* Action Buttons */
div[data-testid='book_actions'] {
display: flex;
justify-content: center;
align-items: center;
}
+button[data-testid^='book_favorite_toggle_'] {
+ min-width: 0;
+ padding: 0.5rem;
+ border: 1px solid #fca510;
+ background-color: #fff;
+ transition: background-color 0.3s ease;
+}
+
+button[data-testid='book_favorite_toggle_false']:hover,
+button[data-testid='book_favorite_toggle_true']:hover {
+ background-color: #f6dbabfd;
+}
+
[data-testid='book_favorite_icon'],
[data-testid='book_nonFavorite_icon'] {
width: 2.5rem;
height: 2.5rem;
- margin: 0rem 1rem;
cursor: pointer;
- transition: color 0.3s, transform 0.3s ease-in-out;
color: #fca510;
+ transition: transform 0.2s, color 0.2s ease;
}
[data-testid='book_favorite_icon']:hover,
[data-testid='book_nonFavorite_icon']:hover {
- transform: scale(1.3);
+ transform: scale(1.1);
+ color: #e69303;
}
+/* Delete Button */
button[data-testid='delete_book_btn'] {
margin: 0 0.5rem;
background-color: #fff;
diff --git a/src/components/BookListSection/book/Book.jsx b/src/components/BookListSection/book/Book.jsx
index 1f571d8..d39f91d 100644
--- a/src/components/BookListSection/book/Book.jsx
+++ b/src/components/BookListSection/book/Book.jsx
@@ -1,7 +1,7 @@
import { TbStar } from 'react-icons/tb'
import { TbStarFilled } from 'react-icons/tb'
-import './Book.css'
import Button from '../../common/button/Button'
+import './Book.css'
const Book = ({
index,
@@ -10,10 +10,11 @@ const Book = ({
bookAuthor,
onHandleDeleteBook,
onToggleFavoriteBook,
+ onClick,
'data-testid': testId,
}) => {
return (
-
+
{++index}.
@@ -25,8 +26,11 @@ const Book = ({
- onToggleFavoriteBook(book.id)}
+
+
diff --git a/src/components/BookListSection/bookListModal/BookListModal.css b/src/components/BookListSection/bookListModal/BookListModal.css
new file mode 100644
index 0000000..94e850e
--- /dev/null
+++ b/src/components/BookListSection/bookListModal/BookListModal.css
@@ -0,0 +1,107 @@
+div[data-testid='bookListModal_body'] {
+ display: flex;
+ flex-direction: column;
+ padding: 1rem;
+ padding-bottom: 5rem;
+ overflow-y: auto;
+ gap: 1rem;
+}
+
+/* Book Item */
+div[data-testid^='bookListModal_book_item'] {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ border-bottom: 1px solid #ddd;
+ gap: 1rem;
+ padding: 1rem;
+}
+
+div[data-testid='bookListModal_book_content'] {
+ width: 100%;
+ display: flex;
+ gap: 1rem;
+}
+
+div[data-testid='bookListModal_left_content'] {
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ width: 30%;
+}
+
+img[data-testid='bookListModal_book_img'] {
+ min-width: 70%;
+ max-height: 100%;
+ padding-top: 3rem;
+}
+
+div[data-testid='bookListModal_right_content'] {
+ width: 70%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 2rem;
+ padding: 2rem;
+}
+
+div[data-testid='bookListModal_add_book_wrapper'] {
+ flex-grow: 1;
+ display: flex;
+ align-items: flex-end;
+ justify-content: flex-start;
+}
+
+/* Actions Wrapper */
+div[data-testid='bookListModal_actions_wrapper'] {
+ display: flex;
+ align-items: center;
+ column-gap: 1rem;
+}
+
+/* Favorite Toggle Button */
+button[data-testid^='bookListModal_favorite_toggle_'] {
+ min-width: 0;
+ padding: 0.5rem;
+ border: 1px solid #fca510;
+ background-color: #fff;
+ /* transition: background-color 0.2s, transform 0.3s ease; */
+}
+
+button[data-testid^='bookListModal_favorite_toggle_false']:hover,
+button[data-testid^='bookListModal_favorite_toggle_true']:hover {
+ background-color: #f6dbab;
+}
+
+/* Favorite Icon */
+[data-testid='bookListModal_favorite_icon'],
+[data-testid='bookListModal_nonFavorite_icon'] {
+ width: 2.5rem;
+ height: 2.5rem;
+ cursor: pointer;
+ transition: transform 0.2s, color 0.3s, ease;
+ color: #fca510;
+}
+
+[data-testid='bookListModal_favorite_icon']:hover,
+[data-testid='bookListModal_nonFavorite_icon']:hover {
+ transform: scale(1.1);
+ color: #e69303;
+}
+
+button[data-testid='bookListModal_delete_btn'] {
+ margin: 0;
+ background-color: #fff;
+ color: red;
+ border: 1px solid red;
+}
+
+button[data-testid='bookListModal_delete_btn']:hover {
+ background-color: rgb(255, 204, 204);
+}
+
+button[data-testid='bookListModal_delete_btn']:disabled {
+ border: 1px solid darkgray;
+ color: fff;
+ background-color: darkgray;
+}
diff --git a/src/components/BookListSection/bookListModal/BookListModal.jsx b/src/components/BookListSection/bookListModal/BookListModal.jsx
new file mode 100644
index 0000000..aa3073c
--- /dev/null
+++ b/src/components/BookListSection/bookListModal/BookListModal.jsx
@@ -0,0 +1,77 @@
+import { TbStar } from 'react-icons/tb'
+import { TbStarFilled } from 'react-icons/tb'
+import Button from '../../common/button/Button'
+import Modal from '../../common/modal/Modal'
+import './BookListModal.css'
+
+const BookListModal = ({
+ isOpen,
+ onClose,
+ modalBook,
+ onHandleDeleteBook,
+ onToggleFavoriteBook,
+}) => {
+ return (
+
+ {modalBook && (
+
+
+
+ {modalBook.image ? (
+
+ ) : (
+ 'No image available'
+ )}
+
+
+
{modalBook.title}
+
+
+ {modalBook.authors && modalBook.authors.includes(', ')
+ ? 'Authors:'
+ : 'Author:'}
+ {' '}
+ {modalBook.authors || 'Unknown Authors'}
+
+
+ Published Year:{' '}
+ {modalBook.publishedDate || 'N/A'}
+
+
+ Description:{' '}
+ {modalBook.description || 'No description available'}
+
+
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+export default BookListModal
diff --git a/src/components/common/button/Button.css b/src/components/common/button/Button.css
index 0935d6f..c512ea5 100644
--- a/src/components/common/button/Button.css
+++ b/src/components/common/button/Button.css
@@ -20,6 +20,14 @@
background-color: #0056b3;
}
+span[data-testid='button_text'] {
+ transition: transform 0.2s ease;
+}
+
+span[data-testid='button_text']:hover {
+ transform: scale(1.05);
+}
+
.componentButton:active {
background-color: #016adb;
transform: scale(0.95);
@@ -27,8 +35,9 @@
}
.componentButton:disabled {
- background-color: #0056b3;
- color: darkgray;
+ background-color: darkgray;
+ border: 1px solid darkgray;
+ color: #fff;
cursor: not-allowed;
}
diff --git a/src/components/common/button/Button.jsx b/src/components/common/button/Button.jsx
index 62b1ec9..6817510 100644
--- a/src/components/common/button/Button.jsx
+++ b/src/components/common/button/Button.jsx
@@ -5,9 +5,11 @@ const Button = ({
text = 'Submit',
type = 'button',
onClick = () => {},
- className = '',
disabled = false,
+ isLoading = false,
+ children,
'data-testid': testId,
+ className = '',
}) => {
return (
)
}
diff --git a/src/components/common/modal/Modal.css b/src/components/common/modal/Modal.css
index 99320a7..b265957 100644
--- a/src/components/common/modal/Modal.css
+++ b/src/components/common/modal/Modal.css
@@ -18,6 +18,7 @@ div[data-testid='modal_component'] {
display: flex;
flex-direction: column;
width: 80%;
+ /* min-height: 60%; */
max-height: 90%;
background-color: #f2f2f2;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
diff --git a/src/components/common/modal/Modal.jsx b/src/components/common/modal/Modal.jsx
index 109f43c..c7d5b81 100644
--- a/src/components/common/modal/Modal.jsx
+++ b/src/components/common/modal/Modal.jsx
@@ -8,7 +8,11 @@ const Modal = ({ isOpen, onClose, 'data-testid': testId, children }) => {
return (
-
+
e.stopPropagation()}
+ data-testid="modal_component"
+ >
diff --git a/src/components/searchBookSection/searchBookModal/SearchBookModal.css b/src/components/searchBookSection/searchBookModal/SearchBookModal.css
index f7726ff..0344044 100644
--- a/src/components/searchBookSection/searchBookModal/SearchBookModal.css
+++ b/src/components/searchBookSection/searchBookModal/SearchBookModal.css
@@ -2,6 +2,7 @@ div[data-testid='modal_body'] {
display: flex;
flex-direction: column;
padding: 1rem;
+ padding-bottom: 5rem;
overflow-y: auto;
gap: 1rem;
}
diff --git a/src/components/user/login/Login.jsx b/src/components/user/login/Login.jsx
index e21eba9..1f39139 100644
--- a/src/components/user/login/Login.jsx
+++ b/src/components/user/login/Login.jsx
@@ -5,7 +5,7 @@ import { IoArrowBackCircleOutline } from 'react-icons/io5'
import { signInWithEmailAndPassword, onAuthStateChanged } from 'firebase/auth'
import Input from '../../common/input/Input'
import Button from '../../common/button/Button'
-import { auth } from '../../../services/firebaseConfig'
+import { auth } from '../../../api/services/firebaseConfig'
import { setError } from '../../../redux/slices/errorSlice'
import './Login.css'
diff --git a/src/components/user/signup/Signup.jsx b/src/components/user/signup/Signup.jsx
index 7f77c56..6b1f233 100644
--- a/src/components/user/signup/Signup.jsx
+++ b/src/components/user/signup/Signup.jsx
@@ -8,7 +8,7 @@ import {
} from 'firebase/auth'
import Input from '../../common/input/Input'
import Button from '../../common/button/Button'
-import { auth } from '../../../services/firebaseConfig'
+import { auth } from '../../../api/services/firebaseConfig'
import { setError } from '../../../redux/slices/errorSlice'
import './Signup.css'
diff --git a/src/redux/slices/booksSlice.js b/src/redux/slices/booksSlice.js
index 0da301d..85fcabe 100644
--- a/src/redux/slices/booksSlice.js
+++ b/src/redux/slices/booksSlice.js
@@ -7,7 +7,7 @@ import {
getDocs,
setDoc,
} from 'firebase/firestore'
-import { auth, db } from '../../services/firebaseConfig'
+import { auth, db } from '../../api/services/firebaseConfig'
import { setError } from './errorSlice'
const initialState = []