adds integrations with bookish-server, the books API and the google books API, adds functionality for library, searching for books, and displaying renters registered in the system

This commit is contained in:
David Lick
2020-05-28 22:08:27 -04:00
parent 888dec3402
commit a06b4d70ab
337 changed files with 17276 additions and 91 deletions
+120
View File
@@ -1641,6 +1641,14 @@
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-7.2.1.tgz",
"integrity": "sha512-oZ0Ib5I4Z2pUEcoo95cT1cr6slco9WY7yiPpG+RGNkj8YcYgJnM7pXmYmorNOReh8MIGcKSqXyeGjxnr8YiZbA=="
},
"@types/axios": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz",
"integrity": "sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=",
"requires": {
"axios": "*"
}
},
"@types/babel__core": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.7.tgz",
@@ -1777,6 +1785,14 @@
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz",
"integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug=="
},
"@types/ramda": {
"version": "0.27.6",
"resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.27.6.tgz",
"integrity": "sha512-ephagb0ZIAJSoS5I/qMS4Mqo1b/Nd50pWM+o1QO/dz8NF//GsCGPTLDVRqgXlVncy74KShfHzE5rPZXTeek4PA==",
"requires": {
"ts-toolbelt": "^6.3.3"
}
},
"@types/react": {
"version": "16.9.35",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.35.tgz",
@@ -1802,6 +1818,17 @@
"@types/react": "*"
}
},
"@types/react-redux": {
"version": "7.1.9",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.9.tgz",
"integrity": "sha512-mpC0jqxhP4mhmOl3P4ipRsgTgbNofMRXJb08Ms6gekViLj61v1hOZEKWDCyWsdONr6EjEA6ZHXC446wdywDe0w==",
"requires": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0",
"redux": "^4.0.0"
}
},
"@types/react-router": {
"version": "5.1.7",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.7.tgz",
@@ -1821,6 +1848,22 @@
"@types/react-router": "*"
}
},
"@types/redux-devtools-extension": {
"version": "2.13.2",
"resolved": "https://registry.npmjs.org/@types/redux-devtools-extension/-/redux-devtools-extension-2.13.2.tgz",
"integrity": "sha1-suCajBY7Kw9QcualrEFHQADfIRE=",
"requires": {
"redux-devtools-extension": "*"
}
},
"@types/redux-thunk": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/redux-thunk/-/redux-thunk-2.1.0.tgz",
"integrity": "sha1-vCtulylhgxr7gqm/TwZybjUflBY=",
"requires": {
"redux-thunk": "*"
}
},
"@types/stack-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
@@ -2544,6 +2587,37 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz",
"integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA=="
},
"axios": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
"requires": {
"follow-redirects": "1.5.10"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
"axobject-query": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.1.2.tgz",
@@ -10481,6 +10555,11 @@
"performance-now": "^2.1.0"
}
},
"ramda": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.0.tgz",
"integrity": "sha512-pVzZdDpWwWqEVVLshWUHjNwuVP7SfcmPraYuqocJp1yo2U1R7P+5QAfDhdItkuoGqIBnBYrtPp7rEPqDn9HlZA=="
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -10817,6 +10896,18 @@
"warning": "^4.0.2"
}
},
"react-redux": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.0.tgz",
"integrity": "sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==",
"requires": {
"@babel/runtime": "^7.5.5",
"hoist-non-react-statics": "^3.3.0",
"loose-envify": "^1.4.0",
"prop-types": "^15.7.2",
"react-is": "^16.9.0"
}
},
"react-router": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
@@ -11025,6 +11116,25 @@
"strip-indent": "^3.0.0"
}
},
"redux": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
"integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==",
"requires": {
"loose-envify": "^1.4.0",
"symbol-observable": "^1.2.0"
}
},
"redux-devtools-extension": {
"version": "2.13.8",
"resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz",
"integrity": "sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg=="
},
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
},
"regenerate": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
@@ -12526,6 +12636,11 @@
"util.promisify": "~1.0.0"
}
},
"symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
},
"symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -12865,6 +12980,11 @@
"resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.1.6.tgz",
"integrity": "sha512-CrG5GqAAzMT7144Cl+UIFP7mz/iIhiy+xQ6GGcnjTezhALT02uPMRw7tgDSESgB5MsfKt55+GPWw4ir1kVtMIQ=="
},
"ts-toolbelt": {
"version": "6.9.4",
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.9.4.tgz",
"integrity": "sha512-muRZZqfOTOVvLk5cdnp7YWm6xX+kD/WL2cS/L4zximBRcbQSuMoTbQQ2ZZBVMs1gB0EZw1qThP+HrIQB35OmEw=="
},
"tslib": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+10
View File
@@ -9,18 +9,28 @@
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"@types/axios": "^0.14.0",
"@types/jest": "^24.9.1",
"@types/node": "^12.12.42",
"@types/ramda": "^0.27.6",
"@types/react": "^16.9.35",
"@types/react-dom": "^16.9.8",
"@types/react-redux": "^7.1.9",
"@types/react-router": "^5.1.7",
"@types/react-router-dom": "^5.1.5",
"@types/redux-devtools-extension": "^2.13.2",
"@types/redux-thunk": "^2.1.0",
"@types/styled-components": "^5.1.0",
"axios": "^0.19.2",
"ramda": "^0.27.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-redux": "^7.2.0",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"redux-devtools-extension": "^2.13.8",
"redux-thunk": "^2.3.0",
"semantic-ui-react": "^0.88.2",
"styled-components": "^5.1.1",
"typescript": "^3.7.5"
+81
View File
@@ -0,0 +1,81 @@
import { actionTypes } from '../constants/books/action_types';
import booksApi from '../api/booksAPI';
import googleBooksApi from '../api/googleBooks';
import bookishApi from '../api/bookish';
import { Dispatch } from 'redux';
export function fetchBooks() {
return (dispatch: Dispatch) => {
dispatch({ type: actionTypes.FETCH_BOOKS.request });
booksApi.get('/books')
.then(res => dispatch({
type: actionTypes.FETCH_BOOKS.success,
data: res.data
}))
.catch(err => dispatch({
type: actionTypes.FETCH_BOOKS.failure,
err: err.message
}));
}
}
export function fetchBookCover(title: string, isbn: string) {
return (dispatch: Dispatch) => {
dispatch({ type: actionTypes.FETCH_COVER_IMAGE.request });
googleBooksApi.get(`/volumes?q=isbn:${isbn}`)
.then(res => {
if (res.data.totalItems === 0 ||
res.data.items[0].volumeInfo.imageLinks === undefined) {
return dispatch({
type: actionTypes.FETCH_COVER_IMAGE.failure,
err: "no image"
})
}
dispatch({
type: actionTypes.FETCH_COVER_IMAGE.success,
data: { title: title, imageLink: res.data.items[0].volumeInfo.imageLinks.thumbnail }
})
})
.catch(err => {
dispatch({
type: actionTypes.FETCH_COVER_IMAGE.failure,
err: err.message
})})
}
}
export function checkoutBook(title: string, renterId: number) {
return (dispatch: Dispatch) => {
dispatch({ type: actionTypes.CHECKOUT_BOOK.request });
bookishApi.post(`/renters/${renterId}/books/checkout`, { title })
.then(() => {
dispatch({ type: actionTypes.CHECKOUT_BOOK.success });
})
.catch(err => {
dispatch({
type: actionTypes.CHECKOUT_BOOK.failure,
err: err.message
})})
}
}
export function returnBook(title: string, renterId: number) {
return (dispatch: Dispatch) => {
dispatch({ type: actionTypes.RETURN_BOOK.request });
bookishApi.post(`/renters/${renterId}/books/return`, { title })
.then(() => {
dispatch({ type: actionTypes.RETURN_BOOK.success });
})
.catch(err => {
dispatch({
type: actionTypes.RETURN_BOOK.failure,
err: err.message
})})
}
}
+40
View File
@@ -0,0 +1,40 @@
import { actionTypes } from '../constants/renters/action_types';
import bookishApi from '../api/bookish';
import { Dispatch } from 'redux';
export function fetchRenters() {
return (dispatch: Dispatch) => {
dispatch({ type: actionTypes.FETCH_RENTERS.request });
bookishApi.get('/renters')
.then(res => {
console.log(res)
dispatch({
type: actionTypes.FETCH_RENTERS.success,
data: res.data
})})
.catch(err => {
dispatch({
type: actionTypes.FETCH_RENTERS.failure,
err: err.message
})})
}
}
export function registerRenter(name: string, address: string, email: string, phone: string) {
return (dispatch: Dispatch) => {
dispatch({ type: actionTypes.REGISTER_RENTER.request });
bookishApi.post('/renters', {name, address, email, phoneNumber: phone})
.then(res => dispatch({
type: actionTypes.REGISTER_RENTER.success,
data: res.data
}))
.catch(err => dispatch({
type: actionTypes.REGISTER_RENTER.failure,
err: err.message
}))
}
}
+5
View File
@@ -0,0 +1,5 @@
import axios from 'axios';
export default axios.create({
baseURL: 'http://localhost:3001/api/v1'
})
+5
View File
@@ -0,0 +1,5 @@
import axios from 'axios';
export default axios.create({
baseURL: 'https://servicepros-test-api.herokuapp.com/api/v1'
});
+5
View File
@@ -0,0 +1,5 @@
import axios from 'axios';
export default axios.create({
baseURL: 'https://www.googleapis.com/books/v1'
})
@@ -0,0 +1,7 @@
export function generateActionType(prefix: string) {
return {
request: `${prefix}_REQUEST`,
success: `${prefix}_SUCCESS`,
failure: `${prefix}_FAILURE`
};
}
@@ -0,0 +1,17 @@
import { generateActionType } from '../actionTypeFactory';
const FETCH_BOOKS = generateActionType('FETCH_BOOKS');
const FETCH_COVER_IMAGE = generateActionType('FETCH_COVER_IMAGE');
const SET_FILTER = 'SET_FILTER';
const ADD_TO_CART = 'ADD_TO_CART';
const CHECKOUT_BOOK = generateActionType('CHECKOUT_BOOK');
const RETURN_BOOK = generateActionType('RETURN_BOOK');
export const actionTypes = {
FETCH_BOOKS,
FETCH_COVER_IMAGE,
SET_FILTER,
ADD_TO_CART,
CHECKOUT_BOOK,
RETURN_BOOK
}
@@ -0,0 +1,9 @@
import { generateActionType } from '../actionTypeFactory';
const FETCH_RENTERS = generateActionType('FETCH_RENTERS');
const REGISTER_RENTER = generateActionType('REGISTER_RENTERS');
export const actionTypes = {
FETCH_RENTERS,
REGISTER_RENTER
}
@@ -0,0 +1,46 @@
import React from 'react';
import { useSelector, RootStateOrAny } from 'react-redux';
import styled from 'styled-components';
const BookCoverCardContainer = styled.div`
background-color: #fff;
display: flex;
border: 0.1px solid #f0f0f0;
box-shadow: 10px 5px 15px rgba(175,175,175,50);
height: 135px;
max-width: 200px;
margin: 10px 0;
`
const EmptyCart = styled.div`
display: inline-block;
margin: auto auto;
font-family: 'Helvetica';
font-size: 1.5rem;
color: #13334A;
`
const CoverImage = styled.img`
height: 100px;
object-fit: contain;
margin: auto 15px;
`
const BookCoverCard: React.FC = () => {
const cartItems = useSelector((state: RootStateOrAny) => state.books.cart)
const images = useSelector((state: RootStateOrAny) => state.books.images)
console.log(cartItems)
return (
<BookCoverCardContainer>
{cartItems.length === 0 && (<EmptyCart>Nothing here yet!</EmptyCart>)}
{cartItems && cartItems.map(cartItem => (
<CoverImage
src={images[cartItem] || 'https://college.indiana.edu/images/publications/book-cover-placeholder.jpg'} />
))}
</BookCoverCardContainer>
)
}
export default BookCoverCard;
@@ -0,0 +1,13 @@
import React from 'react';
// import styled from 'styled-components';
import Panel from '../../UIKit/Panel/Panel';
import BookCoverCard from './BookCoverCard/BookCoverCard';
const Cart: React.FC = () => (
<Panel>
<BookCoverCard />
</Panel>
)
export default Cart;
@@ -4,6 +4,7 @@ import { Route } from 'react-router';
import Header from './Header/Header';
import Library from './Library/Library';
import Cart from './Cart/Cart';
import Renters from './Renters/Renters';
const ContentContainer = styled.div`
@@ -18,6 +19,8 @@ const Content: React.FC = () => (
<Route path="/library" component={Library} />
<Route path="/renters" component={Renters} />
<Route path="/return" component={Renters} />
<Route path="/checkout" component={Cart} />
<Route path="/checkout" component={Renters} />
</ContentContainer>
)
@@ -7,7 +7,7 @@ const ButtonBayContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-left: 7.5px;
margin-left: 35px;
width: 320px;
height: 100%;
`
@@ -1,11 +1,14 @@
import React from 'react';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { actionTypes } from '../../../../constants/books/action_types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSearch } from '@fortawesome/free-solid-svg-icons';
const SearchContainer = styled.div`
width: 250px;
width: 350px;
height: 100%;
display: flex;
align-items: center;
@@ -21,22 +24,30 @@ const StyledInput = styled.input`
color: #13334A;
font-size: 1.25rem;
outline: none;
padding-left: 20px;
padding: 0 40px 0 20px;
box-shadow: 10px 5px 15px rgba(0,0,0,50)
`
const StyledIcon = styled(FontAwesomeIcon)`
color: #D97925;
position: absolute;
right: 25px;
right: 50px;
font-size: 20px;
`
const Search: React.FC = () => (
<SearchContainer>
<StyledInput />
<StyledIcon icon={faSearch} />
</SearchContainer>
)
const Search: React.FC = () => {
const dispatch = useDispatch();
return (
<SearchContainer>
<StyledInput
onChange={(e: React.FormEvent<HTMLInputElement>) => {
dispatch({ type: actionTypes.SET_FILTER, data: e.currentTarget.value });
}}
/>
<StyledIcon icon={faSearch} />
</SearchContainer>
)
}
export default Search;
@@ -1,8 +1,12 @@
import React from 'react';
import React, { useEffect } from 'react';
import { useSelector, useDispatch, RootStateOrAny } from 'react-redux';
import styled from 'styled-components';
import { fetchBookCover } from '../../../../actions/books';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCartPlus } from '@fortawesome/free-solid-svg-icons';
import { actionTypes } from '../../../../constants/books/action_types';
const BookContainer = styled.div`
display: flex;
@@ -12,8 +16,8 @@ const BookContainer = styled.div`
background-color: #fff;
border: 0.1px solid #8C8C8C;
box-shadow: 10px 5px 15px rgba(0,0,0,50);
width: 200px;
height: 350px;
width: 175px;
height: 275px;
`
const IconContainer = styled.div`
@@ -26,12 +30,13 @@ const StyledIcon = styled(FontAwesomeIcon)`
position: absolute;
top: 0;
right: 0;
transform: translate(220px, -15px);
transform: translate(195px, -15px);
font-size: 38px;
cursor: pointer;
`
const CoverImage = styled.img`
max-width: 90%;
max-height: 75%;
width: auto;
height: auto;
object-fit: contain;
@@ -54,20 +59,46 @@ const Author = styled.div`
type Props = {
title: string;
author: string;
imageLink: string;
isbn: string;
}
const Book: React.FC<Props> = ({ title, author, imageLink }) => (
<BookContainer>
<IconContainer>
<StyledIcon icon={faCartPlus} />
</IconContainer>
<CoverImage
src={imageLink}
/>
<Title>{title}</Title>
<Author>{author}</Author>
</BookContainer>
)
const Book: React.FC<Props> = ({ title, author, isbn }) => {
const imageLink = useSelector((state: RootStateOrAny) => state.books.images[title])
const cart = useSelector((state: RootStateOrAny) => state.books.cart)
const dispatch = useDispatch();
useEffect(() => {
if (imageLink === undefined) {
dispatch(fetchBookCover(title, isbn))
}
}, []);
const handleAddToCart = () => {
if (cart.length > 1) {
// TODO: Add a toast notification here when cart is full.
alert("You can only check out 2 books at a time!");
return;
}
// TODO: Add a toast notification here when item added to cart.
dispatch({ type: actionTypes.ADD_TO_CART, data: title });
}
return (
<BookContainer>
<IconContainer>
<StyledIcon
icon={faCartPlus}
onClick={handleAddToCart}
/>
</IconContainer>
<CoverImage
src={imageLink || 'https://college.indiana.edu/images/publications/book-cover-placeholder.jpg'}
/>
<Title>{title}</Title>
<Author>{author}</Author>
</BookContainer>
)
}
export default Book;
@@ -1,7 +1,10 @@
import React from 'react';
import React, { useEffect } from 'react';
import styled from 'styled-components';
import { useSelector, useDispatch, RootStateOrAny } from 'react-redux';
import { fetchBooks } from '../../../actions/books';
import Book from './Book/Book';
import { BookType } from '../../../types/book';
const LibraryContainer = styled.div`
display: flex;
@@ -9,34 +12,29 @@ const LibraryContainer = styled.div`
justify-content: center;
`
const books = [
{title: "To Kill a Mockingbird", author: "Harper Lee", imageLink: "http://www.prepressure.com/images/book-cover-to-kill-a-mocking-bird.jpg"},
{title: "To Kill a Mockingbird", author: "Harper Lee", imageLink: "http://www.prepressure.com/images/book-cover-to-kill-a-mocking-bird.jpg"},
{title: "To Kill a Mockingbird", author: "Harper Lee", imageLink: "http://www.prepressure.com/images/book-cover-to-kill-a-mocking-bird.jpg"},
{title: "To Kill a Mockingbird", author: "Harper Lee", imageLink: "http://www.prepressure.com/images/book-cover-to-kill-a-mocking-bird.jpg"},
{title: "To Kill a Mockingbird", author: "Harper Lee", imageLink: "http://www.prepressure.com/images/book-cover-to-kill-a-mocking-bird.jpg"},
{title: "To Kill a Mockingbird", author: "Harper Lee", imageLink: "http://www.prepressure.com/images/book-cover-to-kill-a-mocking-bird.jpg"},
{title: "To Kill a Mockingbird", author: "Harper Lee", imageLink: "http://www.prepressure.com/images/book-cover-to-kill-a-mocking-bird.jpg"},
{title: "To Kill a Mockingbird", author: "Harper Lee", imageLink: "http://www.prepressure.com/images/book-cover-to-kill-a-mocking-bird.jpg"},
{title: "To Kill a Mockingbird", author: "Harper Lee", imageLink: "http://www.prepressure.com/images/book-cover-to-kill-a-mocking-bird.jpg"},
{title: "To Kill a Mockingbird", author: "Harper Lee", imageLink: "http://www.prepressure.com/images/book-cover-to-kill-a-mocking-bird.jpg"},
{title: "To Kill a Mockingbird", author: "Harper Lee", imageLink: "http://www.prepressure.com/images/book-cover-to-kill-a-mocking-bird.jpg"},
{title: "To Kill a Mockingbird", author: "Harper Lee", imageLink: "http://www.prepressure.com/images/book-cover-to-kill-a-mocking-bird.jpg"},
{title: "To Kill a Mockingbird", author: "Harper Lee", imageLink: "http://www.prepressure.com/images/book-cover-to-kill-a-mocking-bird.jpg"},
{title: "To Kill a Mockingbird", author: "Harper Lee", imageLink: "http://www.prepressure.com/images/book-cover-to-kill-a-mocking-bird.jpg"},
{title: "To Kill a Mockingbird", author: "Harper Lee", imageLink: "http://www.prepressure.com/images/book-cover-to-kill-a-mocking-bird.jpg"}
]
const Library: React.FC = () => {
const books = useSelector((state: RootStateOrAny) => state.books);
const filter = useSelector((state: RootStateOrAny) => state.books.filter);
const dispatch = useDispatch();
const Library: React.FC = () => (
<LibraryContainer>
{books.map(book => (
useEffect(() => {
dispatch(fetchBooks())
}, []);
const displayBooks = books.books.filter((book: BookType) => book.title.toLowerCase().indexOf(filter) !== -1);
return (
<LibraryContainer>
{displayBooks && displayBooks.map((book: BookType) => (
<Book
title={book.title}
author={book.author}
imageLink={book.imageLink}
key={book.title}
title={book.title || ''}
author={book.author || ''}
isbn={book.isbn || ''}
/>
))}
</LibraryContainer>
)
))}
</LibraryContainer>
)
}
export default Library;
@@ -56,7 +56,7 @@ const RenterReturnContainer = styled.div`
type Props = {
name: string;
phone: string;
phoneNumber: string;
email: string;
}
@@ -65,7 +65,7 @@ const books = [
{title: "To Kill a Mockingbird", author: "Harper Lee", imageLink: "http://www.prepressure.com/images/book-cover-to-kill-a-mocking-bird.jpg"}
]
const RenterCard: React.FC<Props> = ({ name, phone, email }) => (
const RenterCard: React.FC<Props> = ({ name, phoneNumber, email }) => (
<RenterCardContainer>
<RenterImageContainer>
<RenterImagePlaceholder
@@ -76,7 +76,7 @@ const RenterCard: React.FC<Props> = ({ name, phone, email }) => (
{name}
</RenterName>
<RenterPhone>
{phone}
{phoneNumber}
</RenterPhone>
<RenterEmail>
{email}
@@ -1,37 +1,36 @@
import React from 'react';
// import styled from 'styled-components';
import React, { useEffect } from 'react';
import { useSelector, useDispatch, RootStateOrAny } from 'react-redux';
import { fetchRenters } from '../../../actions/renters';
import Panel from '../../UIKit/Panel/Panel';
import RenterCard from './RenterCard/RenterCard';
const renters = [
{name: 'John Doe', phone: '555-555-5555', email: 'john@doe.com'},
{name: 'John Doe', phone: '555-555-5555', email: 'john@doe.com'},
{name: 'John Doe', phone: '555-555-5555', email: 'john@doe.com'},
{name: 'John Doe', phone: '555-555-5555', email: 'john@doe.com'},
{name: 'John Doe', phone: '555-555-5555', email: 'john@doe.com'},
{name: 'John Doe', phone: '555-555-5555', email: 'john@doe.com'},
{name: 'John Doe', phone: '555-555-5555', email: 'john@doe.com'},
{name: 'John Doe', phone: '555-555-5555', email: 'john@doe.com'},
{name: 'John Doe', phone: '555-555-5555', email: 'john@doe.com'},
{name: 'John Doe', phone: '555-555-5555', email: 'john@doe.com'}
]
import { RenterType } from '../../../types/renter';
const Renters: React.FC = () => (
<Panel style={{
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
justifyContent: 'center'
}}>
{renters.map(renter => (
<RenterCard
name={renter.name}
phone={renter.phone}
email={renter.email}
/>
))}
</Panel>
)
const Renters: React.FC = () => {
const renters = useSelector((state: RootStateOrAny) => state.renters);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchRenters())
}, []);
return (
<Panel style={{
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
justifyContent: 'center'
}}>
{renters && renters.renters.map((renter: RenterType) => (
<RenterCard
name={renter.name || ''}
phoneNumber={renter.phoneNumber || ''}
email={renter.email || ''}
/>
))}
</Panel>
)
}
export default Renters;
@@ -13,7 +13,7 @@ const PanelContainer = styled.div`
type Props = {
children: React.ReactNode;
style: CSS.Properties;
style?: CSS.Properties;
}
const Panel: React.FC<Props> = ({ children, style }) => (
+6 -1
View File
@@ -5,10 +5,15 @@ import * as serviceWorker from './serviceWorker';
import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import { store } from './reducers/store';
ReactDOM.render(
<React.StrictMode>
<Router>
<App />
<Provider store={store}>
<App />
</Provider>
</Router>
</React.StrictMode>,
document.getElementById('root')
+122
View File
@@ -0,0 +1,122 @@
import { actionTypes } from '../constants/books/action_types';
import { mergeDeepRight } from 'ramda';
import { Action } from '../types/actions';
const initialState = {
books: [],
filter: '',
images: {},
cart: [],
loading: false,
loadError: null,
checkoutError: null,
returnError: null
}
export function books(state = initialState, action: Action) {
switch (action.type) {
// FETCH_BOOK actions
case actionTypes.FETCH_BOOKS.request:
return {
...state,
loading: true
}
case actionTypes.FETCH_BOOKS.success:
return {
...state,
loading: false,
loadError: null,
books: [...action.data]
}
case actionTypes.FETCH_BOOKS.failure:
return {
...state,
loading: false,
error: action.err
}
// FETCH_COVER_IMAGE actions
case actionTypes.FETCH_COVER_IMAGE.request:
return {
...state,
loading: true
}
case actionTypes.FETCH_COVER_IMAGE.success:
interface ImageReturn {
title: string;
imageLink: string;
}
let imageReturn: ImageReturn = {...action.data as ImageReturn};
const images = {...state.images}
images[imageReturn.title] = imageReturn.imageLink;
return mergeDeepRight(state, {
loading: false,
images: {...images}
})
case actionTypes.FETCH_COVER_IMAGE.failure:
return {
...state,
loading: false
}
// SET_FILTER action
case actionTypes.SET_FILTER:
return {
...state,
filter: action.data
}
// ADD_TO_CART action
case actionTypes.ADD_TO_CART:
return {
...state,
cart: state.cart.concat(action.data)
}
// CHECKOUT_BOOK action
case actionTypes.CHECKOUT_BOOK.request:
return {
...state,
loading: true
}
case actionTypes.CHECKOUT_BOOK.success:
return {
...state,
loading: false,
checkoutError: null
}
case actionTypes.CHECKOUT_BOOK.failure:
return {
...state,
loading: false,
checkoutError: action.err
}
// RETURN_BOOK action
case actionTypes.RETURN_BOOK.request:
return {
...state,
loading: true
}
case actionTypes.RETURN_BOOK.success:
return {
...state,
loading: false,
returnError: false
}
case actionTypes.RETURN_BOOK.failure:
return {
...state,
loading: false,
returnError: action.err
}
default:
return state;
}
}
+9
View File
@@ -0,0 +1,9 @@
import { combineReducers } from 'redux';
import { books } from './books';
import { renters } from './renters';
export const RootReducer = combineReducers({
books,
renters
})
+56
View File
@@ -0,0 +1,56 @@
import { actionTypes } from '../constants/renters/action_types';
import { Action } from '../types/actions';
const initialState = {
renters: [],
loading: false,
loadError: null,
registerError: null,
}
export function renters(state = initialState, action: Action) {
switch(action.type) {
// FETCH_RENTERS action
case actionTypes.FETCH_RENTERS.request:
return {
...state,
loading: true
}
case actionTypes.FETCH_RENTERS.success:
return {
...state,
loading: false,
loadError: null,
renters: [...action.data]
}
case actionTypes.FETCH_RENTERS.failure:
return {
...state,
loading: false,
loadError: action.err
}
// REGISTER_RENTER action
case actionTypes.REGISTER_RENTER.request:
return {
...state,
loading: true
}
case actionTypes.REGISTER_RENTER.success:
return {
...state,
loading: false,
registerError: null,
}
case actionTypes.REGISTER_RENTER.failure:
return {
...state,
loading: false,
registerError: action.err
}
default:
return state;
}
}
+13
View File
@@ -0,0 +1,13 @@
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk';
import { RootReducer } from '../';
export const store = createStore(
RootReducer,
{},
composeWithDevTools({})(
applyMiddleware(thunk)
)
);
+5
View File
@@ -0,0 +1,5 @@
export type Action = {
type: string,
data?: any,
err?: any,
}
+7
View File
@@ -0,0 +1,7 @@
export type BookType = {
title: string,
author: string,
imageLink: string,
yearPublished?: string,
isbn?: string
}
+6
View File
@@ -0,0 +1,6 @@
export type RenterType = {
name: string;
address: string;
email: string;
phoneNumber: string;
}
+2 -1
View File
@@ -19,7 +19,8 @@
"noEmit": true,
"jsx": "react",
"noUnusedLocals": true,
"noUnusedParameters": true
"noUnusedParameters": true,
"noImplicitAny": false,
},
"include": [
"src"