Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature - search #1

Merged
merged 34 commits into from
Dec 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0382f68
feat: add routing
giovanizanetti Dec 27, 2020
95e48ea
feat: connect handler to search page
giovanizanetti Dec 27, 2020
3a6e156
style: position searchbar under the top navigation
giovanizanetti Dec 27, 2020
77fc946
feat: connect search bar return button
giovanizanetti Dec 27, 2020
c633109
feat: handle search input
giovanizanetti Dec 27, 2020
9b74d73
chore: create store
giovanizanetti Dec 27, 2020
f9beb14
chore: create reducer
giovanizanetti Dec 27, 2020
ddc1e0f
chore: create BooksProvider
giovanizanetti Dec 28, 2020
9a84f54
chore: pass down bookContext to access global state
giovanizanetti Dec 28, 2020
fe795b0
refactor: App.js
giovanizanetti Dec 28, 2020
55e62a4
refactor: habdleShelf
giovanizanetti Dec 28, 2020
885435a
refactor: handleShelf
giovanizanetti Dec 28, 2020
445d04b
refactor: handleShelf
giovanizanetti Dec 28, 2020
a3a5fdb
chore: add comments
giovanizanetti Dec 28, 2020
91c315b
refactor: remove unnecessary reducer
giovanizanetti Dec 28, 2020
913ee34
feat: search
giovanizanetti Dec 28, 2020
ec996fd
feat: default shelf to none when books are not on the self
giovanizanetti Dec 28, 2020
4284eb1
feat: add selected book to the global state
giovanizanetti Dec 28, 2020
3350d53
chore: create handleSelect method and pass down through BookContext
giovanizanetti Dec 28, 2020
7de5486
refactor: handleShelf
giovanizanetti Dec 28, 2020
058740d
fix: no thumbnail
giovanizanetti Dec 28, 2020
914cc4d
fix: remove useffect dependency
giovanizanetti Dec 28, 2020
5f2e3a2
refactor: remove unused code
giovanizanetti Dec 28, 2020
780f502
refactor: create bookList component
giovanizanetti Dec 28, 2020
bdcc60c
chore: uodate comments
giovanizanetti Dec 29, 2020
da38302
fix: update my shelfs in real time when new booked is added
giovanizanetti Dec 29, 2020
c24dea8
fix: intantly update the select option on the list component
giovanizanetti Dec 29, 2020
34283ce
feat: display message when search does not find results
giovanizanetti Dec 29, 2020
b24cd19
improvement: useDebounce
giovanizanetti Dec 29, 2020
6e1b2a7
improvement: useDebounce
giovanizanetti Dec 29, 2020
4aee90b
fix: brach conflict
giovanizanetti Dec 30, 2020
f282069
chore: add comments
giovanizanetti Dec 30, 2020
69b8de1
feat: finish search functionality
giovanizanetti Dec 30, 2020
e293084
refactor: changr placeholder
giovanizanetti Dec 30, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions CONTRIBUTING.md

This file was deleted.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"lodash.debounce": "^4.0.8",
"prop-types": "^15.6.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
Expand Down
3 changes: 0 additions & 3 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,7 @@ body,
/* search page */

.search-books-bar {
position: fixed;
width: 100%;
top: 0;
left: 0;
z-index: 5;
display: flex;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 0 6px rgba(0, 0, 0, 0.23);
Expand Down
79 changes: 59 additions & 20 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,71 @@
import { useState, useEffect } from 'react'
import './App.css'
import Navigation from './components/Navigation/Navigation'
import Main from './components/Main/Main'
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'
import { BooksProvider } from './BooksProvider'
import { getAll, update } from './BooksAPI'

const BooksApp = () => {
// state = {
// /**
// * TODO: Instead of using this state variable to keep track of which page
// * we're on, use the URL in the browser's address bar. This will ensure that
// * users can use the browser's back and forward buttons to navigate between
// * pages, as well as provide a good URL they can bookmark and share.
// */
// showSearchPage: false,
// }
const [searchResults, setSearchResults] = useState([])
const [books, setBooks] = useState([])
const [shouldUpdate, setShouldUpdate] = useState(true)

const currentlyReading = books && books.filter((book) => book.shelf === 'currentlyReading')
const wantToRead = books && books.filter((book) => book.shelf === 'wantToRead')
const read = books && books.filter((book) => book.shelf === 'read')

useEffect(() => {
if (shouldUpdate) {
getAll()
.then((data) => {
setBooks(data)
})
.then(() => setShouldUpdate(false))
.catch((err) => console.log(err))
}
setShouldUpdate(false)
}, [shouldUpdate])

const handleShelf = (shelf, book) => {
const { id } = book

// update server
update(id, shelf)
// check if book is already on the shelf
const isBook = books.find((book) => book.id === id)

// when book already exists, change it to the selected shelf
if (isBook !== undefined) {
const myBooks = [...books]
const bookIndex = myBooks.findIndex((book) => book.id === id)
myBooks[bookIndex].shelf = shelf
setBooks(myBooks)

// When book does not exists, add it to the selected shelf and update the state
} else {
// force update to update the books
setShouldUpdate(true)
}
}

return (
<div className='app'>
<Navigation />
<Router>
<Switch>
<div className='list-books'>
<Main />
<div className='open-search'>
<button onClick={() => this.setState({ showSearchPage: true })}>Add a book</button>
</div>
</div>
</Switch>
</Router>

{/* Pass books down to make it available for any consumer component within the provider */}
<BooksProvider
value={{
books,
handleShelf,
searchResults,
setSearchResults,
currentlyReading,
read,
wantToRead,
}}
>
<Main />
</BooksProvider>
</div>
)
}
Expand Down
7 changes: 7 additions & 0 deletions src/BooksProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createContext } from 'react'

export const BooksContext = createContext([])

export const BooksProvider = BooksContext.Provider

export default BooksContext
Binary file added src/assets/book-placeholder.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 17 additions & 9 deletions src/components/Book/Book.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { useContext } from 'react'
import BooksProvider from '../../BooksProvider'
import BookShelfChanger from '../BookShelfChanger/BookShelfChanger'
import placeholder from '../../assets/book-placeholder.jpeg'

const Book = ({ book, handleShelf }) => {
const {
imageLinks: { thumbnail },
title,
authors,
shelf,
id,
} = book
const Book = ({ book, search }) => {
const { title, authors, shelf, id, imageLinks } = book

const { books } = useContext(BooksProvider)

// if book list coming from search results, check if book is already is in any shelf mybooks
const isBook = () => {
if (search) {
return books && books.find((book) => book.id === id)
}
}

const thumbnail = imageLinks ? book.imageLinks.thumbnail : placeholder

const bookAuthors = authors && authors.map((author) => <p key={author}>{author}</p>)

Expand All @@ -23,7 +31,7 @@ const Book = ({ book, handleShelf }) => {
backgroundImage: `url(${thumbnail})`,
}}
>
<BookShelfChanger shelf={shelf} id={id} handleShelf={handleShelf} />
<BookShelfChanger shelf={shelf} book={isBook() !== undefined ? isBook() : book} search={search} />
</div>
</div>
<div className='book-title'>{title}</div>
Expand Down
9 changes: 9 additions & 0 deletions src/components/BookList/BookList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Book from '../Book/Book'

const BookList = ({ books, search }) => {
return (
<ol className='books-grid'>{books && books.map((book) => <Book key={book.id} book={book} search={search} />)}</ol>
)
}

export default BookList
10 changes: 4 additions & 6 deletions src/components/BookShelf/BookShelf.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import Book from '../Book/Book'
import BookList from '../BookList/BookList'

const BookShelf = ({ name, books, handleShelf }) => {
const BookShelf = ({ name, books }) => {
return (
<section className='bookshelf'>
<h2 className='bookshelf-title'>{name}</h2>
<h2 className='bookshelf-title'>{name && name}</h2>
<div className='bookshelf-books'>
<ol className='books-grid'>
{books && books.map((book) => <Book key={book.id} book={book} handleShelf={handleShelf} />)}
</ol>
<BookList books={books} />
</div>
</section>
)
Expand Down
16 changes: 14 additions & 2 deletions src/components/BookShelfChanger/BookShelfChanger.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
const BookShelfChanger = ({ shelf, handleShelf, id }) => {
import { useContext, useState } from 'react'
import BooksContext from '../../BooksProvider'

const BookShelfChanger = ({ book }) => {
const bookShelf = book.shelf ? book.shelf : 'none'
const { handleShelf } = useContext(BooksContext)
const [value, setValue] = useState(bookShelf)

const handleChange = (e) => {
handleShelf(e.target.value, book)
setValue(e.target.value)
}

return (
<div className='book-shelf-changer'>
<select value={shelf} onChange={(e) => handleShelf(e.target.value, id)}>
<select value={value} onChange={handleChange}>
<option value='move' disabled>
Move to...
</option>
Expand Down
51 changes: 26 additions & 25 deletions src/components/Main/Main.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
import { useState, useEffect } from 'react'
import { getAll, update } from '../../BooksAPI'
import { useContext } from 'react'
import BookShelf from '../BookShelf/BookShelf'
import BooksContext from '../../BooksProvider'
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'
import Search from '../Search/Search'

const Main = () => {
const [books, setBooks] = useState([])
const currentlyReading = books && books.filter((book) => book.shelf === 'currentlyReading')
const wantToRead = books && books.filter((book) => book.shelf === 'wantToRead')
const readBooks = books && books.filter((book) => book.shelf === 'read')

const handleShelf = (shelf, id) => {
const myBooks = [...books]
const bookIndex = myBooks.findIndex((book) => book.id === id)
myBooks[bookIndex].shelf = shelf
setBooks(myBooks)
update(id, shelf)
}

useEffect(() => {
getAll().then((data) => setBooks(data))
}, [])
const { currentlyReading, wantToRead, read } = useContext(BooksContext)

return (
<div className='list-books-content'>
<div>
<BookShelf name='Currently Reading' books={currentlyReading} handleShelf={handleShelf} />
<BookShelf name='Want to Read' books={wantToRead} handleShelf={handleShelf} />
<BookShelf name='Read' books={readBooks} handleShelf={handleShelf} />
</div>
</div>
<Router>
<Switch>
<Route exact path='/'>
<div className='list-books'>
<div className='list-books-content'>
<div>
<BookShelf name='Currently Reading' books={currentlyReading} />
<BookShelf name='Want to Read' books={wantToRead} />
<BookShelf name='Read' books={read} />
</div>
</div>
<Link to='/search' className='open-search'>
<button>Add a book</button>
</Link>
</div>
</Route>
<Route path='/search'>
<Search />
</Route>
</Switch>
</Router>
)
}

Expand Down
64 changes: 52 additions & 12 deletions src/components/Search/search.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,65 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import BookList from '../BookList/BookList'
import { search } from '../../BooksAPI'
import { useDebounce } from '../../hooks/useDebounce'

const Search = () => {
// APi query value (delayed)
const [query, setQuery] = useState('')
const [searchResults, setSearchResults] = useState([])
const [message, setMessage] = useState('')
// Inpute value
const [value, setValue] = useState('')

// fetch api
useEffect(() => {
if (!query) {
setSearchResults([])
setMessage('')
}
query && search(query).then((data) => setSearchResults(data))
}, [query])

// display message
useEffect(() => {
if (query.length && !searchResults.length) {
setTimeout(() => {
setMessage('Not found! Try another search term!')
}, 800)
}
}, [query, searchResults])

const debouncedSave = useDebounce((value) => setQuery(value), 500)

const handleChange = (e) => {
const text = e.target.value.toLowerCase()
// input value to display immediately
setValue(text)
// delay query to deminish api calls
debouncedSave(text)
}

return (
<div className='search-books'>
<div className='search-books-bar'>
<button className='close-search' onClick={() => this.setState({ showSearchPage: false })}>
<Link className='close-search' to='/'>
Close
</button>
</Link>
<div className='search-books-input-wrapper'>
{/*
NOTES: The search from BooksAPI is limited to a particular set of search terms.
You can find these search terms here:
https://github.com/udacity/reactnd-project-myreads-starter/blob/master/SEARCH_TERMS.md

However, remember that the BooksAPI.search method DOES search by title or author. So, don't worry if
you don't find a specific author or title. Every search is limited by search terms.
*/}
<input type='text' placeholder='Search by title or author' />
<input
type='text'
placeholder='Search by title or author and add to your reads'
onChange={handleChange}
value={value}
/>
</div>
</div>
<div className='search-books-results'>
<ol className='books-grid'></ol>
{searchResults.length ? <BookList search={true} books={searchResults} /> : <strong>{message}</strong>}
</div>
</div>
)
}

export default Search
8 changes: 8 additions & 0 deletions src/hooks/useDebounce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import _debounce from 'lodash.debounce'
import { useCallback } from 'react'

export const useDebounce = (callback, delay) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedCallback = useCallback(_debounce(callback, delay), [delay])
return debouncedCallback
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6786,6 +6786,11 @@ lodash._reinterpolate@^3.0.0:
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=

lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=

lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
Expand Down