diff --git a/src/App.jsx b/src/App.jsx index 490be93..1706442 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -8,23 +8,22 @@ import Search from './components/Search' const App = () => { const [books, setBooks] = useState([]) - const [filteredBooks, setFilteredBooks] = useState([]) - const [selectedBook, setSelectedBook] = useState(null) const [showPanel, setShowPanel] = useState(false) + const [showFaves, setShowFaves] = useState(false) + const faveBookIds = JSON.parse(localStorage.getItem('faveBookIds') || '[]') useEffect(() => { const fetchData = async () => { const response = await fetch('https://book-club-json.herokuapp.com/books') const books = await response.json() - setBooks(books) - setFilteredBooks(books) + setBooks(books.map((book) => ({...book, isFaved: faveBookIds.includes(book.id)}))) } fetchData() }, []) - const pickBook = (book) => { - setSelectedBook(book) + const pickBook = (bookId) => { + setBooks((books) => books.map((book) => ({...book, isPicked: book.id === bookId}))) setShowPanel(true) } @@ -32,37 +31,76 @@ const App = () => { setShowPanel(false) } + const toggleShowFaves = () => { + setShowFaves((showFaves) => !showFaves) + } + + const toggleFave = (bookId) => { + setBooks((books) => { + const updatedBooks = books.map((book) => + book.id === bookId ? {...book, isFaved: !book.isFaved} : book + ) + + localStorage.setItem( + 'faveBookIds', + JSON.stringify(updatedBooks.filter(({isFaved}) => isFaved).map(({id}) => id)) + ) + return updatedBooks + }) + } + const filterBooks = (searchTerm) => { const stringSearch = (bookAttribute, searchTerm) => bookAttribute.toLowerCase().includes(searchTerm.toLowerCase()) - if (!searchTerm) { - setFilteredBooks(books) - } else { - setFilteredBooks( - books.filter( - (book) => stringSearch(book.title, searchTerm) || stringSearch(book.author, searchTerm) - ) - ) - } + setBooks((books) => + books.map((book) => { + const isFiltered = !searchTerm + ? false + : stringSearch(book.title, searchTerm) || stringSearch(book.author, searchTerm) + ? false + : true + return {...book, isFiltered: isFiltered} + }) + ) } - const hasFiltered = filteredBooks.length !== books.length + const hasFiltered = books.some((book) => book.isFiltered) + + const displayBooks = hasFiltered + ? books.filter((book) => !book.isFiltered) + : showFaves + ? books.filter((book) => book.isFaved) + : books + + const selectedBook = books.find((book) => book.isPicked) return ( <>
- +
- {(state) => } + {(state) => ( + + )} ) diff --git a/src/assets/sad-face.svg b/src/assets/sad-face.svg new file mode 100644 index 0000000..7dbeec9 --- /dev/null +++ b/src/assets/sad-face.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/Book/index.jsx b/src/components/Book/index.jsx index 93e80a6..4239b1a 100644 --- a/src/components/Book/index.jsx +++ b/src/components/Book/index.jsx @@ -2,7 +2,7 @@ import React from 'react' import {Container, Cover, Title, Author} from './styles' const Book = ({book, pickBook, isLarge}) => ( - pickBook && pickBook(book)}> + pickBook && pickBook(book.id)}>
{book.title} diff --git a/src/components/BooksContainer/index.jsx b/src/components/BooksContainer/index.jsx index ab5040e..da1af88 100644 --- a/src/components/BooksContainer/index.jsx +++ b/src/components/BooksContainer/index.jsx @@ -1,7 +1,15 @@ import React, {useRef, useEffect, useState} from 'react' import {debounce} from 'lodash-es' import Book from '../Book' -import {Container, H2, BookList} from './styles' +import {Container, H2, BookList, NoBooksContainer, H3, SadFace, H4} from './styles' + +const NoBooksMessage = () => ( + +

Oh dear!

+ +

There are no books to see here.

+
+) const BooksContainer = ({books, pickBook, title, isPanelOpen}) => { const prevPanelState = useRef(false) @@ -31,11 +39,15 @@ const BooksContainer = ({books, pickBook, title, isPanelOpen}) => { return (

{title}

- - {books.map((book) => ( - - ))} - + {books.length > 0 ? ( + + {books.map((book) => ( + + ))} + + ) : ( + + )}
) } diff --git a/src/components/BooksContainer/styles.js b/src/components/BooksContainer/styles.js index 5b3a276..ac64c47 100644 --- a/src/components/BooksContainer/styles.js +++ b/src/components/BooksContainer/styles.js @@ -1,4 +1,5 @@ import styled from 'styled-components' +import {ReactComponent as SadFaceSVG} from '../../assets/sad-face.svg' export const Container = styled.div` background-color: #a7e1f8; @@ -39,3 +40,32 @@ export const BookList = styled.div` grid-column-gap: 20px; } ` + +export const NoBooksContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + text-align: center; + height: 50vh; +` + +export const H3 = styled.h3` + font-size: 34px; + margin: 0; + @media (max-width: 1000px) { + font-size: 30px; + } +` + +export const H4 = styled.h4` + font-size: 24px; + margin: 0; + @media (max-width: 1000px) { + font-size: 20px; + } +` + +export const SadFace = styled(SadFaceSVG)` + margin: 20px 0; +` diff --git a/src/components/DetailPanel/index.jsx b/src/components/DetailPanel/index.jsx index 121cd59..e1319c6 100644 --- a/src/components/DetailPanel/index.jsx +++ b/src/components/DetailPanel/index.jsx @@ -1,9 +1,9 @@ import React, {useRef, useEffect} from 'react' import Book from '../Book' import {CloseWrapper, Panel, BG, P, Em} from './styles' -import {Close} from '../../styles' +import {Close, Button} from '../../styles' -const DetailPanel = ({book, closePanel, state}) => { +const DetailPanel = ({book, closePanel, state, toggleFave}) => { const panelEl = useRef(null) const prevBook = useRef(null) @@ -23,6 +23,9 @@ const DetailPanel = ({book, closePanel, state}) => { {book && ( <> +

{book.description}

diff --git a/src/components/DetailPanel/styles.js b/src/components/DetailPanel/styles.js index ce2dca9..da7f366 100644 --- a/src/components/DetailPanel/styles.js +++ b/src/components/DetailPanel/styles.js @@ -18,7 +18,7 @@ export const Panel = styled.article` background-color: #ffe581; border-left: 2px solid #000; box-sizing: border-box; - height: calc(100vh - 82px); + height: calc(100vh - 83px); width: 660px; position: fixed; z-index: 2; @@ -29,10 +29,10 @@ export const Panel = styled.article` transition: 300ms; right: ${({$state}) => ($state === 'entering' || $state === 'entered' ? 0 : '-660px')}; - @media (max-width: 800px) { + @media (max-width: 1000px) { + height: calc(100vh - 75px); border-left: none; width: 100vw; - height: calc(100vh - 75px); padding: 40px 86px 20px 20px; bottom: ${({$state}) => ($state === 'entering' || $state === 'entered' ? 0 : '-100vh')}; right: unset; @@ -42,7 +42,7 @@ export const Panel = styled.article` export const CloseWrapper = styled(Pill)` position: fixed; cursor: pointer; - top: 120px; + top: 130px; right: 40px; z-index: 4; display: ${({$state}) => ($state === 'entered' ? 'flex' : 'none')}; @@ -51,7 +51,7 @@ export const CloseWrapper = styled(Pill)` margin-left: -3px; } - @media (max-width: 800px) { + @media (max-width: 1000px) { top: unset; bottom: 20px; right: 20px; diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index b5ee704..1e446a5 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -1,12 +1,12 @@ import React from 'react' -import {HeaderContainer, Logo} from './styles' +import {HeaderContainer, Logo, RightContainer} from './styles' const Header = ({children}) => ( - {children} + {children} ) diff --git a/src/components/Header/styles.js b/src/components/Header/styles.js index 00fc352..df82719 100644 --- a/src/components/Header/styles.js +++ b/src/components/Header/styles.js @@ -23,8 +23,14 @@ export const Logo = styled(LogoAsset)` width: 270px; display: block; - @media (max-width: 800px) { + @media (max-width: 1000px) { height: 33px; - width: 222px; + width: 200px; } ` + +export const RightContainer = styled.div` + right: 0; + display: flex; + justify-content: space-between; +` diff --git a/src/components/Search/index.jsx b/src/components/Search/index.jsx index 0e59e90..10b31e3 100644 --- a/src/components/Search/index.jsx +++ b/src/components/Search/index.jsx @@ -1,8 +1,17 @@ import React, {useState, useRef} from 'react' -import {Input, SearchContainer, Icon, Wrapper} from './styles' -import {Close} from '../../styles' +import {Input, SearchContainer, Icon, Wrapper, FaveButtonContainer, Counter} from './styles' +import {Close, Button} from '../../styles' -const Search = ({filterBooks}) => { +const FaveButton = ({faveBooksLength, toggleShowFaves, showFaves}) => ( + + {faveBooksLength} + + +) + +const Search = ({filterBooks, faveBooksLength, toggleShowFaves, showFaves}) => { const inputEl = useRef(null) const [showOnDesktop, setShowOnDesktop] = useState(false) @@ -22,6 +31,11 @@ const Search = ({filterBooks}) => { return ( + diff --git a/src/components/Search/styles.js b/src/components/Search/styles.js index daefeae..caf756b 100644 --- a/src/components/Search/styles.js +++ b/src/components/Search/styles.js @@ -3,9 +3,11 @@ import {ReactComponent as MagnifyingIcon} from '../../assets/search.svg' import {Pill} from '../../styles' export const Wrapper = styled.div` - @media (max-width: 800px) { + display: flex; + gap: 20px; + + @media (max-width: 1000px) { border-top: 2px solid black; - display: flex; align-items: center; justify-content: center; background: #ffbccc; @@ -15,6 +17,7 @@ export const Wrapper = styled.div` bottom: 0; position: fixed; z-index: 1; + gap: unset; } ` @@ -26,13 +29,14 @@ export const SearchContainer = styled(Pill)` button { display: ${({$showOnDesktop}) => ($showOnDesktop ? 'block' : 'none')}; - @media (max-width: 800px) { + @media (max-width: 1000px) { display: block; } } - @media (max-width: 800px) { - width: 85%; + @media (max-width: 1000px) { + width: 50%; + margin-right: 10px; } ` @@ -44,9 +48,31 @@ export const Input = styled.input` background: inherit; border: none; padding: 6px; + width: 100%; ` export const Icon = styled(MagnifyingIcon)` - width: 20px; + width: 30px; cursor: pointer; ` + +export const FaveButtonContainer = styled.div` + display: flex; + + @media (max-width: 1000px) { + position: relative; + left: -15px; + } +` + +export const Counter = styled(Pill)` + position: relative; + right: -150px; + bottom: 10px; + padding: 4px; + + @media (max-width: 1000px) { + right: -120px; + padding: 2px; + } +` diff --git a/src/styles.js b/src/styles.js index 328c849..631e12f 100644 --- a/src/styles.js +++ b/src/styles.js @@ -29,7 +29,7 @@ export const Close = styled.button` border: 0; cursor: pointer; height: 24px; - padding: 0; + padding: 0 12px; position: relative; width: 24px; @@ -50,3 +50,21 @@ export const Close = styled.button` transform: rotate(-45deg); } ` + +export const Button = styled.button` + display: block; + border-radius: 30px; + padding: ${({$hasEmoji}) => ($hasEmoji ? '5px 14px' : '8px')}; + border: 2px solid #000; + background: transparent; + font-family: 'Work Sans', sans-serif; + font-size: 18px; + margin-bottom: ${({$isHeader}) => ($isHeader ? '0' : '14px')}; + cursor: pointer; + width: ${({$isHeader}) => ($isHeader ? '140px' : 'unset')}; + + @media (max-width: 1000px) { + font-size: 14px; + width: ${({$isHeader}) => ($isHeader ? '110px' : 'unset')}; + } +`