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:
Generated
+120
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
})})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}))
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export default axios.create({
|
||||
baseURL: 'http://localhost:3001/api/v1'
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export default axios.create({
|
||||
baseURL: 'https://servicepros-test-api.herokuapp.com/api/v1'
|
||||
});
|
||||
@@ -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 }) => (
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import { books } from './books';
|
||||
import { renters } from './renters';
|
||||
|
||||
export const RootReducer = combineReducers({
|
||||
books,
|
||||
renters
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
export type Action = {
|
||||
type: string,
|
||||
data?: any,
|
||||
err?: any,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export type BookType = {
|
||||
title: string,
|
||||
author: string,
|
||||
imageLink: string,
|
||||
yearPublished?: string,
|
||||
isbn?: string
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export type RenterType = {
|
||||
name: string;
|
||||
address: string;
|
||||
email: string;
|
||||
phoneNumber: string;
|
||||
}
|
||||
@@ -19,7 +19,8 @@
|
||||
"noEmit": true,
|
||||
"jsx": "react",
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitAny": false,
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
||||
Reference in New Issue
Block a user