Repository for frontend final project web dev.
- Extra Credit of completing a project that is not the employee task is fulfilled.
- Extra Credit having at least one child component, within a parent component, within a parent component that you create.
- Within the Search component, we have a BookCard component that is shown as search results, and within the BookCard component, there is the BookModel component that is displayed by clicking on a search result and a pop-up showing up.
- UI (React)
- Navbar/sidebar component:
web-dev-final-frontend/book-lib/src/components/Navbar/Navbar.js
Lines 1 to 68 in 4e7664a
import { useRef } from "react"; import LibraryBooksIcon from "@mui/icons-material/LibraryBooks"; import MenuBookIcon from "@mui/icons-material/MenuBook"; import ManageSearchIcon from "@mui/icons-material/ManageSearch"; import Button from "@mui/material/Button"; import { Link } from "react-router-dom"; import { FaBars, FaTimes } from "react-icons/fa"; import "./Navbar.css"; import GitHubIcon from "@mui/icons-material/GitHub"; function Navbar() { const navRef = useRef(); const showNavbar = () => { navRef.current.classList.toggle("responsive_nav"); }; return ( <header> <div> <Link to="/"> <MenuBookIcon fontSize="large" sx={{ color: "black" }} /> </Link> </div> <div> <nav ref={navRef}> <Link to="/forums"> <Button variant="text" sx={{ color: "black" }} startIcon={<LibraryBooksIcon />} > Forums </Button> </Link> <Link to="/search"> <Button variant="text" sx={{ color: "black" }} startIcon={<ManageSearchIcon />} > Search </Button> </Link> <button className="nav-btn nav-close-btn" onClick={showNavbar}> <FaTimes /> </button> </nav> <button className="nav-btn" onClick={showNavbar}> <FaBars /> </button> </div> <div> <Link to="https://github.com/JoshuaRamc/web-dev-final-frontend" target="_blank" > <GitHubIcon fontSize="large" style={{ color: "black", marginRight: "25px" }} ></GitHubIcon> </Link> </div> </header> ); } export default Navbar; - 3 or more additional components (only listing three random because we have over 8 components):
import React, { useState } from "react"; import BookModel from "../Search/BookModel"; const BookCard = ({ book }) => { const [show, setShow] = useState(false); const [bookItem, setItem] = useState(); console.log(book); return ( <> {book.map((item) => { let thumbnail = item.volumeInfo.imageLinks && item.volumeInfo.imageLinks.smallThumbnail; let amount = item.saleInfo.listPrice && item.saleInfo.listPrice.amount; if (thumbnail !== undefined && amount !== undefined) { return ( <> <div className='card' onClick={() => { setShow(true); setItem(item); }} > <img src={thumbnail} alt='' /> <div className='bottom'> <h3 className='title'> {item.volumeInfo.title} </h3> <p className='amount'>₹{amount}</p> </div> </div> <BookModel show={show} item={bookItem} onClose={() => setShow(false)} /> </> ); } })} </> ); }; export default BookCard; const BookList = (props) => { const { books, setBooks } = useContext(BooksContext); const navigate = useNavigate(); const fetchData = async () => { try { const res = await BookLookup.get("/findall"); setBooks(res.data); } catch (e) { console.log(e); } }; const handleBookSelect = (isbn) => { navigate(`/book/${isbn}`); }; useEffect(() => { fetchData(); // eslint-disable-next-line }, []); const handleDelete = async (isbn) => { try { const res = await BookLookup.delete(`/${isbn}`); console.log(res); window.location.reload(false); } catch (e) { console.log(e); } }; return ( <> <div className="list-group"> <table className="table table-hover table-light"> <thead> <tr style={{ backgroundColor: "pink" }}> <th scope="col">Title</th> <th scope="col">Author(s)</th> <th scope="col">Publication Date</th> <th scope="col">Page Count</th> <th scope="col">ISBN</th> <th scope="col">Conversation</th> <th scope="col">Remove Forum</th> </tr> </thead> <tbody> {books.map((book) => { return ( <tr key={book.isbn}> <td>{book.title}</td> <td>{book.authors}</td> <td>{book.publication_date}</td> <td>{book.page_count}</td> <td>{book.isbn}</td> <td> <button className="btn btn-warning" style={{ backgroundColor: "PaleGreen" }} onClick={() => handleBookSelect(book.isbn)} > Conversation Page </button> </td> <td> {" "} <Button variant="text" sx={{ color: "black" }} startIcon={<RemoveCircleOutlineIcon />} onClick={() => handleDelete(book.isbn)} ></Button> </td> </tr> ); })} </tbody> </table> </div> </> ); }; export default BookList; const AddThread = () => { const { isbn } = useParams(); const [name, setName] = useState(""); const [comment, setComment] = useState(); const [rating, setRating] = useState("1"); const handleThreadAdd = async (e) => { e.preventDefault(); try { const newThread = await BookLookup.post(`/thread/create`, { isbn_num: isbn, name, review: comment, rating, }); console.log(newThread); window.location.reload(false); } catch (err) { console.log(err); } }; return ( <div className="mb-2" style={{ marginTop: "1rem" }}> <form action=""> <div className="form-row"> <div className="form-group col-8"> <label htmlFor="name">Name</label> <input value={name} onChange={(e) => setName(e.target.value)} id="name" placeholder="name" type="text" className="form-control" /> </div> <div className="form-group col-4"> <label htmlFor="rating">Rating</label> <select value={rating} onChange={(e) => setRating(e.target.value)} id="rating" className="custom-select" > <option disabled>Rating</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> <option value="5">5</option> </select> </div> </div> <div className="form-group"> <label htmlFor="Comment">Comment</label> <div style={{ display: "flex" }}> <textarea id="Comment" className="form-control" value={comment} onChange={(e) => setComment(e.target.value)} style={{ maxWidth: "75%" }} ></textarea> <Button type="submit" onClick={handleThreadAdd} variant="contained" style={{ backgroundColor: "#7B68EE", color: "white", marginLeft: "10px", }} > Add Thread </Button> </div> </div> </form> </div> ); }; export default AddThread;
- 1 or more components components should display data representing a single instance from a model and Clicking on one of these components should show additional information related to that instance
const BookList = (props) => { const { books, setBooks } = useContext(BooksContext); const navigate = useNavigate(); const fetchData = async () => { try { const res = await BookLookup.get("/findall"); setBooks(res.data); } catch (e) { console.log(e); } }; const handleBookSelect = (isbn) => { navigate(`/book/${isbn}`); }; useEffect(() => { fetchData(); // eslint-disable-next-line }, []); const handleDelete = async (isbn) => { try { const res = await BookLookup.delete(`/${isbn}`); console.log(res); window.location.reload(false); } catch (e) { console.log(e); } }; return ( <> <div className="list-group"> <table className="table table-hover table-light"> <thead> <tr style={{ backgroundColor: "pink" }}> <th scope="col">Title</th> <th scope="col">Author(s)</th> <th scope="col">Publication Date</th> <th scope="col">Page Count</th> <th scope="col">ISBN</th> <th scope="col">Conversation</th> <th scope="col">Remove Forum</th> </tr> </thead> <tbody> {books.map((book) => { return ( <tr key={book.isbn}> <td>{book.title}</td> <td>{book.authors}</td> <td>{book.publication_date}</td> <td>{book.page_count}</td> <td>{book.isbn}</td> <td> <button className="btn btn-warning" style={{ backgroundColor: "PaleGreen" }} onClick={() => handleBookSelect(book.isbn)} > Conversation Page </button> </td> <td> {" "} <Button variant="text" sx={{ color: "black" }} startIcon={<RemoveCircleOutlineIcon />} onClick={() => handleDelete(book.isbn)} ></Button> </td> </tr> ); })} </tbody> </table> </div> </> ); }; export default BookList;
- 1 or more components should display data based on store state
web-dev-final-frontend/book-lib/src/components/Search/BookCard.js
Lines 13 to 56 in 4e7664a
const isShown = useSelector((state) => state.index); const handleClose = async () => { setShow(false); dispatch(toggleShown(false)); }; return ( <> {book.map((item) => { let thumbnail = item.volumeInfo.imageLinks && item.volumeInfo.imageLinks.smallThumbnail; if (thumbnail !== undefined) { return ( <> <div className="BookCard" onClick={() => { dispatch(toggleShown(true)); setShow(true); setItem(item); }} > <img src={thumbnail} alt="" /> <div className="bottom"> <h3 className="title">{item.volumeInfo.title}</h3> </div> </div> <BookModel show={show} item={bookItem} onClose={() => handleClose()} /> </> ); } else { return null; } })} </> ); }; export default BookCard;
- 1 or more components should take text-based user input
- Text based user input component:
const AddThread = () => { const { isbn } = useParams(); const [name, setName] = useState(""); const [comment, setComment] = useState(); const [rating, setRating] = useState("1"); const handleThreadAdd = async (e) => { e.preventDefault(); try { const newThread = await BookLookup.post(`/thread/create`, { isbn_num: isbn, name, review: comment, rating, }); console.log(newThread); window.location.reload(false); } catch (err) { console.log(err); } }; return ( <div className="mb-2" style={{ marginTop: "1rem" }}> <form action=""> <div className="form-row"> <div className="form-group col-8"> <label htmlFor="name">Name</label> <input value={name} onChange={(e) => setName(e.target.value)} id="name" placeholder="name" type="text" className="form-control" /> </div> <div className="form-group col-4"> <label htmlFor="rating">Rating</label> <select value={rating} onChange={(e) => setRating(e.target.value)} id="rating" className="custom-select" > <option disabled>Rating</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> <option value="5">5</option> </select> </div> </div> <div className="form-group"> <label htmlFor="Comment">Comment</label> <div style={{ display: "flex" }}> <textarea id="Comment" className="form-control" value={comment} onChange={(e) => setComment(e.target.value)} style={{ maxWidth: "75%" }} ></textarea> <Button type="submit" onClick={handleThreadAdd} variant="contained" style={{ backgroundColor: "#7B68EE", color: "white", marginLeft: "10px", }} > Add Thread </Button> </div> </div> </form> </div> ); }; export default AddThread;
- Text based user input component:
- Components should enable to user to perform CRUD operations on the backend models (listed snippets below are just a few examples and are not all instances of CRUD OPS)
- For CREATE:
const BookModel = ({ show, item, onClose }) => { const navigate = useNavigate(); if (!show) { return null; } let thumbnail = item.volumeInfo.imageLinks && item.volumeInfo.imageLinks.smallThumbnail; const handleBookAdd = async ( isbnNum, bookTitle, bookAuthors, bookPublication, bookDescription, bookPages, bookGenre ) => { try { const newBook = await BookLookup.post(`/createbook`, { isbn: isbnNum, title: bookTitle, authors: bookAuthors, publication_date: bookPublication, description: bookDescription, page_count: bookPages, genre: bookGenre, }); console.log(newBook); navigate(`/forums`); } catch (err) { console.log(err); } }; return ( <> <div className="overlay"> <div className="overlay-inner"> <button className="close" onClick={onClose}> X </button> <div className="inner-box"> <img src={thumbnail} alt="" /> <div className="info"> <h1>{item.volumeInfo.title}</h1> <h3>{item.volumeInfo.authors}</h3> <h4> <div style={{ display: "flex", flexDirection: "column" }}> {item.volumeInfo.categories[0]} <span>{item.volumeInfo.publishedDate}</span> <span> {item.volumeInfo.industryIdentifiers[0].identifier} </span> </div> </h4> <br /> <div style={{ display: "flex" }}> <a href={item.volumeInfo.previewLink}> <button>More</button> </a> <Button style={{ marginLeft: "5px", backgroundColor: "OliveDrab" }} variant="contained" onClick={() => handleBookAdd( item.volumeInfo.industryIdentifiers[0].identifier, item.volumeInfo.title, item.volumeInfo.authors[0], item.volumeInfo.publishedDate, item.volumeInfo.description, item.volumeInfo.pageCount, item.volumeInfo.categories[0] ) } > Thread! </Button> </div> </div> </div> <h4 className="description">{item.volumeInfo.description}</h4> </div> </div> </> ); }; export default BookModel; - For READ:
const BookList = (props) => { const { books, setBooks } = useContext(BooksContext); const navigate = useNavigate(); const fetchData = async () => { try { const res = await BookLookup.get("/findall"); setBooks(res.data); } catch (e) { console.log(e); } }; const handleBookSelect = (isbn) => { navigate(`/book/${isbn}`); }; useEffect(() => { fetchData(); // eslint-disable-next-line }, []); const handleDelete = async (isbn) => { try { const res = await BookLookup.delete(`/${isbn}`); console.log(res); window.location.reload(false); } catch (e) { console.log(e); } }; return ( <> <div className="list-group"> <table className="table table-hover table-light"> <thead> <tr style={{ backgroundColor: "pink" }}> <th scope="col">Title</th> <th scope="col">Author(s)</th> <th scope="col">Publication Date</th> <th scope="col">Page Count</th> <th scope="col">ISBN</th> <th scope="col">Conversation</th> <th scope="col">Remove Forum</th> </tr> </thead> <tbody> {books.map((book) => { return ( <tr key={book.isbn}> <td>{book.title}</td> <td>{book.authors}</td> <td>{book.publication_date}</td> <td>{book.page_count}</td> <td>{book.isbn}</td> <td> <button className="btn btn-warning" style={{ backgroundColor: "PaleGreen" }} onClick={() => handleBookSelect(book.isbn)} > Conversation Page </button> </td> <td> {" "} <Button variant="text" sx={{ color: "black" }} startIcon={<RemoveCircleOutlineIcon />} onClick={() => handleDelete(book.isbn)} ></Button> </td> </tr> ); })} </tbody> </table> </div> </> ); }; export default BookList; - For UPDATE:
web-dev-final-frontend/book-lib/src/pages/Edit/Edit.js
Lines 6 to 74 in 4e7664a
function Edit() { const { id } = useParams(); const handleEdit = async (e) => { try { const res = await BookLookup.put(`/thread/${id}`, { review: comment, rating: rating, }); console.log(res); } catch (e) { console.log(e); } }; const [rating, setRating] = useState("1"); const [comment, setComment] = useState(""); return ( <div className="mb-2" style={{ marginTop: "1rem" }}> <form action=""> <div className="form-row"> <div className="form-group col-4"> <label htmlFor="rating">Rating</label> <select value={rating} onChange={(e) => setRating(e.target.value)} id="rating" className="custom-select" > <option disabled>Rating</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> <option value="5">5</option> </select> </div> </div> <div className="form-group"> <label htmlFor="Comment">Comment</label> <div style={{ display: "flex" }}> <textarea id="Comment" className="form-control" value={comment} onChange={(e) => setComment(e.target.value)} style={{ maxWidth: "75%" }} ></textarea> <Button type="submit" // onClick={handleThreadAdd} variant="contained" style={{ backgroundColor: "#7B68EE", color: "white", marginLeft: "10px", }} onClick={() => handleEdit()} > Confirm Edit </Button> </div> </div> </form> </div> ); } export default Edit; - For DELETE:
web-dev-final-frontend/book-lib/src/pages/Conversation/Conversation.js
Lines 32 to 121 in 44a4b50
const handleDelete = async (id) => { try { const res = await BookLookup.delete(`/thread/${id}`); setThreads( threads.filter((thread) => { return thread.id !== id; }) ); console.log(res); } catch (e) { console.log(e); } }; const handleEdit = async (id) => { try { navigate(`/edit/${id}`); } catch (e) { console.log(e); } }; useEffect(() => { fetchData(); console.log(selectedBook); console.log(threads); // eslint-disable-next-line }, []); return ( <div> <div style={{ display: "flex", justifyContent: "center", marginTop: "10px" }} > <h1>{selectedBook.title}</h1> </div> <div style={{ display: "flex", justifyContent: "center", marginTop: "10px", fontSize: 14, textAlign: "center", }} > "{selectedBook.description}" </div> <AddThread /> {selectedBook && ( <> <div className="mt-3"> <div className="row row-cols-3 mb-2" style={{ display: "flex", justifyContent: "space-evenly" }} > {threads.map((thread) => { return ( <div className="card text-white mb-3 mr-4" style={{ maxWidth: "30%", minWidth: "30%", backgroundColor: "MediumPurple", }} > <div className="card-header d-flex justify-content-between"> <span>{thread.name}</span> <span>{thread.rating}/5</span> </div> <div className="card-body"> <p className="card-text">{thread.review}</p> </div> <div style={{ display: "flex", justifyContent: "space-between", }} > <Button variant="text" sx={{ color: "black" }} startIcon={<EditIcon />} onClick={() => handleEdit(thread.id)} ></Button> <Button variant="text" sx={{ color: "black" }} startIcon={<HighlightOffIcon />} onClick={() => handleDelete(thread.id)}
- For CREATE:
- Navbar/sidebar component:
- Client-Side Routing (React Router)
- Create 2 or more routes that display different componetns based on the URL that are accessible from the navbar/topbar
- Route Creation:
web-dev-final-frontend/book-lib/src/App.js
Lines 2 to 26 in 972d9ef
import { Route, Routes } from "react-router-dom"; import SearchLibrary from "./components/Search/SearchLibrary"; import Navbar from "./components/Navbar/Navbar"; import Forum from "./pages/Forum/Forum"; import Conversation from "./pages/Conversation/Conversation"; import { BooksContextProvider } from "./apis/BooksContext"; import Home from "./pages/Home/Home"; import Edit from "./pages/Edit/Edit"; function App() { return ( <BooksContextProvider> <Navbar /> <Routes> <Route path="/" element={<Home />} /> <Route path="/forums" element={<Forum />} /> <Route path="/search" element={<SearchLibrary />} /> <Route path="/book/:isbn" element={<Conversation />} /> <Route path="/edit/:id" element={<Edit />} /> </Routes> </BooksContextProvider> ); } export default App; - Navbar Accessiblity:
web-dev-final-frontend/book-lib/src/components/Navbar/Navbar.js
Lines 18 to 68 in 4e7664a
return ( <header> <div> <Link to="/"> <MenuBookIcon fontSize="large" sx={{ color: "black" }} /> </Link> </div> <div> <nav ref={navRef}> <Link to="/forums"> <Button variant="text" sx={{ color: "black" }} startIcon={<LibraryBooksIcon />} > Forums </Button> </Link> <Link to="/search"> <Button variant="text" sx={{ color: "black" }} startIcon={<ManageSearchIcon />} > Search </Button> </Link> <button className="nav-btn nav-close-btn" onClick={showNavbar}> <FaTimes /> </button> </nav> <button className="nav-btn" onClick={showNavbar}> <FaBars /> </button> </div> <div> <Link to="https://github.com/JoshuaRamc/web-dev-final-frontend" target="_blank" > <GitHubIcon fontSize="large" style={{ color: "black", marginRight: "25px" }} ></GitHubIcon> </Link> </div> </header> ); } export default Navbar;
- Route Creation:
- Use dynamic segments to display appropriate info based on the segment info (Examples contain additional embedded dynamic segments)
- Navigate to dynamic segment:
web-dev-final-frontend/book-lib/src/components/BookList/BookList.js
Lines 21 to 71 in 4e7664a
const handleBookSelect = (isbn) => { navigate(`/book/${isbn}`); }; useEffect(() => { fetchData(); // eslint-disable-next-line }, []); const handleDelete = async (isbn) => { try { const res = await BookLookup.delete(`/${isbn}`); console.log(res); window.location.reload(false); } catch (e) { console.log(e); } }; return ( <> <div className="list-group"> <table className="table table-hover table-light"> <thead> <tr style={{ backgroundColor: "pink" }}> <th scope="col">Title</th> <th scope="col">Author(s)</th> <th scope="col">Publication Date</th> <th scope="col">Page Count</th> <th scope="col">ISBN</th> <th scope="col">Conversation</th> <th scope="col">Remove Forum</th> </tr> </thead> <tbody> {books.map((book) => { return ( <tr key={book.isbn}> <td>{book.title}</td> <td>{book.authors}</td> <td>{book.publication_date}</td> <td>{book.page_count}</td> <td>{book.isbn}</td> <td> <button className="btn btn-warning" style={{ backgroundColor: "PaleGreen" }} onClick={() => handleBookSelect(book.isbn)} > Conversation Page </button> - Rendered component when navigating to dynamic segment:
web-dev-final-frontend/book-lib/src/pages/Conversation/Conversation.js
Lines 11 to 135 in 4e7664a
function Conversation() { const { isbn } = useParams(); const { selectedBook, setSelectedBook } = useContext(BooksContext); const { threads, setThreads } = useContext(BooksContext); const navigate = useNavigate(); const fetchData = async () => { try { const res = await BookLookup.get(`/${isbn}`, { params: { limit: 1, }, }); const threadRes = await BookLookup.get(`/thread/isbn/${isbn}`); setSelectedBook(res.data); setThreads(threadRes.data); } catch (e) { console.log(e); } }; const handleDelete = async (id) => { try { const res = await BookLookup.delete(`/thread/${id}`); setThreads( threads.filter((thread) => { return thread.id !== id; }) ); console.log(res); } catch (e) { console.log(e); } }; const handleEdit = async (id) => { try { navigate(`/edit/${id}`); } catch (e) { console.log(e); } }; useEffect(() => { fetchData(); console.log(selectedBook); console.log(threads); // eslint-disable-next-line }, []); return ( <div> <div style={{ display: "flex", justifyContent: "center", marginTop: "10px" }} > <h1>{selectedBook.title}</h1> </div> <div style={{ display: "flex", justifyContent: "center", marginTop: "10px", fontSize: 14, textAlign: "center", }} > "{selectedBook.description}" </div> <AddThread /> {selectedBook && ( <> <div className="mt-3"> <div className="row row-cols-3 mb-2" style={{ display: "flex", justifyContent: "space-evenly" }} > {threads.map((thread) => { return ( <div className="card text-white mb-3 mr-4" style={{ maxWidth: "30%", minWidth: "30%", backgroundColor: "MediumPurple", }} > <div className="card-header d-flex justify-content-between"> <span>{thread.name}</span> <span>{thread.rating}/5</span> </div> <div className="card-body"> <p className="card-text">{thread.review}</p> </div> <div style={{ display: "flex", justifyContent: "space-between", }} > <Button variant="text" sx={{ color: "black" }} startIcon={<EditIcon />} onClick={() => handleEdit(thread.id)} ></Button> <Button variant="text" sx={{ color: "black" }} startIcon={<HighlightOffIcon />} onClick={() => handleDelete(thread.id)} ></Button> </div> </div> ); })} </div> </div> </> )} </div> ); } export default Conversation;
- Navigate to dynamic segment:
- Create 2 or more routes that display different componetns based on the URL that are accessible from the navbar/topbar
- Stae Management (Redux)
- Create a store and a reducer to handle incoming actions:
- Store and reducer:
web-dev-final-frontend/book-lib/src/store.js
Lines 1 to 6 in 4e7664a
import {configureStore} from '@reduxjs/toolkit'; import indexReducer from './indexSlice'; export const store = configureStore({ reducer: { index: indexReducer
- Store and reducer:
- Create 1 or more action creators to create actions based on input:
- Action Creator:
web-dev-final-frontend/book-lib/src/indexSlice.js
Lines 3 to 13 in 4e7664a
const initialState = { isShown: false, }; export const indexSlice = createSlice({ name: "index", initialState, reducers: { toggleShown: (state) => { state.isShown = !state.isShown; },
- Action Creator:
- Update store using dispatch and actions
- Update store:
web-dev-final-frontend/book-lib/src/components/Search/BookCard.js
Lines 13 to 45 in 4e7664a
const isShown = useSelector((state) => state.index); const handleClose = async () => { setShow(false); dispatch(toggleShown(false)); }; return ( <> {book.map((item) => { let thumbnail = item.volumeInfo.imageLinks && item.volumeInfo.imageLinks.smallThumbnail; if (thumbnail !== undefined) { return ( <> <div className="BookCard" onClick={() => { dispatch(toggleShown(true)); setShow(true); setItem(item); }} > <img src={thumbnail} alt="" /> <div className="bottom"> <h3 className="title">{item.volumeInfo.title}</h3> </div> </div> <BookModel show={show} item={bookItem} onClose={() => handleClose()}
- Update store:
- Reflect updates to the state in the frontend UI:
- Reflected updates show the card:
const BookModel = ({ show, item, onClose }) => { const navigate = useNavigate(); if (!show) { return null; } let thumbnail = item.volumeInfo.imageLinks && item.volumeInfo.imageLinks.smallThumbnail; const handleBookAdd = async ( isbnNum, bookTitle, bookAuthors, bookPublication, bookDescription, bookPages, bookGenre ) => { try { const newBook = await BookLookup.post(`/createbook`, { isbn: isbnNum, title: bookTitle, authors: bookAuthors, publication_date: bookPublication, description: bookDescription, page_count: bookPages, genre: bookGenre, }); console.log(newBook); navigate(`/forums`); } catch (err) { console.log(err); } };
- Reflected updates show the card:
- Create a store and a reducer to handle incoming actions:
- API CALLS:
- Backend: Using backend routes, should be able to perform CRUD on database models ( The following examples are not the only examples in code.)
- CREATE:
const navigate = useNavigate(); if (!show) { return null; } let thumbnail = item.volumeInfo.imageLinks && item.volumeInfo.imageLinks.smallThumbnail; const handleBookAdd = async ( isbnNum, bookTitle, bookAuthors, bookPublication, bookDescription, bookPages, bookGenre ) => { try { const newBook = await BookLookup.post(`/createbook`, { isbn: isbnNum, title: bookTitle, authors: bookAuthors, publication_date: bookPublication, description: bookDescription, page_count: bookPages, genre: bookGenre, }); console.log(newBook); navigate(`/forums`); } catch (err) { console.log(err); } }; - READ:
const BookList = (props) => { const { books, setBooks } = useContext(BooksContext); const navigate = useNavigate(); const fetchData = async () => { try { const res = await BookLookup.get("/findall"); setBooks(res.data); } catch (e) { console.log(e); } }; const handleBookSelect = (isbn) => { navigate(`/book/${isbn}`); }; useEffect(() => { fetchData(); // eslint-disable-next-line }, []); - UPDATE:
web-dev-final-frontend/book-lib/src/pages/Edit/Edit.js
Lines 6 to 74 in 4e7664a
function Edit() { const { id } = useParams(); const handleEdit = async (e) => { try { const res = await BookLookup.put(`/thread/${id}`, { review: comment, rating: rating, }); console.log(res); } catch (e) { console.log(e); } }; const [rating, setRating] = useState("1"); const [comment, setComment] = useState(""); return ( <div className="mb-2" style={{ marginTop: "1rem" }}> <form action=""> <div className="form-row"> <div className="form-group col-4"> <label htmlFor="rating">Rating</label> <select value={rating} onChange={(e) => setRating(e.target.value)} id="rating" className="custom-select" > <option disabled>Rating</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> <option value="5">5</option> </select> </div> </div> <div className="form-group"> <label htmlFor="Comment">Comment</label> <div style={{ display: "flex" }}> <textarea id="Comment" className="form-control" value={comment} onChange={(e) => setComment(e.target.value)} style={{ maxWidth: "75%" }} ></textarea> <Button type="submit" // onClick={handleThreadAdd} variant="contained" style={{ backgroundColor: "#7B68EE", color: "white", marginLeft: "10px", }} onClick={() => handleEdit()} > Confirm Edit </Button> </div> </div> </form> </div> ); } export default Edit; - DELETE:
const handleDelete = async (id) => { try { const res = await BookLookup.delete(`/thread/${id}`); setThreads( threads.filter((thread) => { return thread.id !== id; }) ); console.log(res); } catch (e) { console.log(e); } }; const handleEdit = async (id) => { try { navigate(`/edit/${id}`); } catch (e) { console.log(e); } }; useEffect(() => { fetchData(); console.log(selectedBook); console.log(threads); // eslint-disable-next-line }, []);
- CREATE:
- External: Should make 2 or more External API Calls
- External API Calls:
const SearchLibrary = () => { const [search, setSearch] = useState(""); const [bookData, setData] = useState([]); const searchBook = (evt) => { axios .get( "https://www.googleapis.com/books/v1/volumes?q=" + search + "&key=AIzaSyAGX87cdFiNL82Y2_Ez1zuDXnrm-y53Cto" + "&maxResults=40" ) .then((res) => setData(res.data.items)) .catch((err) => console.log(err)); }; return ( <> <div className="header-search"> <div className="row1"> <h1>Book Finder</h1> </div> <div className="row2"> <div className="search"> <input type="text" placeholder="Enter Your Book Name" value={search} onChange={(e) => setSearch(e.target.value)} /> <Button style={{ background: "#7B68EE" }} onClick={searchBook} variant="contained" > Search Google Books </Button> </div> <img src="./images/bg2.png" alt="" /> </div> </div> <div className="container">{<Card book={bookData} />}</div> </> ); }; export default SearchLibrary; - External API Calls:
const BookModel = ({ show, item, onClose }) => { const navigate = useNavigate(); if (!show) { return null; } let thumbnail = item.volumeInfo.imageLinks && item.volumeInfo.imageLinks.smallThumbnail; const handleBookAdd = async ( isbnNum, bookTitle, bookAuthors, bookPublication, bookDescription, bookPages, bookGenre ) => { try { const newBook = await BookLookup.post(`/createbook`, { isbn: isbnNum, title: bookTitle, authors: bookAuthors, publication_date: bookPublication, description: bookDescription, page_count: bookPages, genre: bookGenre, }); console.log(newBook); navigate(`/forums`); } catch (err) { console.log(err); } }; return ( <> <div className="overlay"> <div className="overlay-inner"> <button className="close" onClick={onClose}> X </button> <div className="inner-box"> <img src={thumbnail} alt="" /> <div className="info"> <h1>{item.volumeInfo.title}</h1> <h3>{item.volumeInfo.authors}</h3> <h4> <div style={{ display: "flex", flexDirection: "column" }}> {item.volumeInfo.categories[0]} <span>{item.volumeInfo.publishedDate}</span> <span> {item.volumeInfo.industryIdentifiers[0].identifier} </span> </div> </h4> <br /> <div style={{ display: "flex" }}> <a href={item.volumeInfo.previewLink}> <button>More</button> </a> <Button style={{ marginLeft: "5px", backgroundColor: "OliveDrab" }} variant="contained" onClick={() => handleBookAdd( item.volumeInfo.industryIdentifiers[0].identifier, item.volumeInfo.title, item.volumeInfo.authors[0], item.volumeInfo.publishedDate, item.volumeInfo.description, item.volumeInfo.pageCount, item.volumeInfo.categories[0] ) }
- External API Calls:
- Backend: Using backend routes, should be able to perform CRUD on database models ( The following examples are not the only examples in code.)