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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user