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 ? ( + {`${modalBook.title} + ) : ( + '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 = []