finishes functionality for cart, checkout, return and renter registration

This commit is contained in:
David Lick
2020-05-31 23:25:49 -04:00
parent 28e52a4e08
commit 5cd92132b3
326 changed files with 495 additions and 16570 deletions
+15 -3
View File
@@ -1,8 +1,11 @@
import React from 'react';
import React, { useEffect } from 'react';
import './App.css';
import styled from 'styled-components';
import Sidebar from './containers/Sidebar/Sidebar';
import Content from './containers/Content/Content';
import { useDispatch } from 'react-redux';
import { fetchBooks } from './actions/books';
import { fetchRenters } from './actions/renters';
const AppContainer = styled.div`
background: #13334A;
@@ -10,11 +13,20 @@ const AppContainer = styled.div`
display: flex;
`
const App: React.FC = () => (
const App: React.FC = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchBooks());
dispatch(fetchRenters());
}, []);
return (
<AppContainer>
<Sidebar/>
<Content />
</AppContainer>
)
);
};
export default App;
+27 -12
View File
@@ -5,15 +5,19 @@ import bookishApi from '../api/bookish';
import { Dispatch } from 'redux';
import { fetchRenters } from './renters';
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
}))
.then(res => {
dispatch({
type: actionTypes.FETCH_BOOKS.success,
data: res.data
})
})
.catch(err => dispatch({
type: actionTypes.FETCH_BOOKS.failure,
err: err.message
@@ -29,12 +33,11 @@ export function fetchBookCover(title: string, isbn: string) {
.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"
})
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 }
@@ -48,7 +51,7 @@ export function fetchBookCover(title: string, isbn: string) {
}
}
export function checkoutBook(title: string, renterId: number) {
export function checkoutBook(title: string, renterId: string) {
return (dispatch: Dispatch) => {
dispatch({ type: actionTypes.CHECKOUT_BOOK.request });
@@ -56,7 +59,12 @@ export function checkoutBook(title: string, renterId: number) {
.then(() => {
dispatch({ type: actionTypes.CHECKOUT_BOOK.success });
})
// After a book is checked out successfully, refetch renters.
.then(() => {
dispatch<any>(fetchRenters());
})
.catch(err => {
alert(`Couldn't check out ${title}`);
dispatch({
type: actionTypes.CHECKOUT_BOOK.failure,
err: err.message
@@ -64,18 +72,25 @@ export function checkoutBook(title: string, renterId: number) {
}
}
export function returnBook(title: string, renterId: number) {
export function returnBook(title: string, renterId: string) {
return (dispatch: Dispatch) => {
dispatch({ type: actionTypes.RETURN_BOOK.request });
bookishApi.post(`/renters/${renterId}/books/return`, { title })
.then(() => {
dispatch({ type: actionTypes.RETURN_BOOK.success });
alert(`${title} was returned`);
})
// After a book is returned successfully, refetch renters.
.then(() => {
dispatch<any>(fetchRenters());
})
.catch(err => {
dispatch({
type: actionTypes.RETURN_BOOK.failure,
err: err.message
})})
})
alert(`Failed to return ${title}`);
})
}
}
+11 -5
View File
@@ -9,7 +9,6 @@ export function fetchRenters() {
bookishApi.get('/renters')
.then(res => {
console.log(res)
dispatch({
type: actionTypes.FETCH_RENTERS.success,
data: res.data
@@ -31,10 +30,17 @@ export function registerRenter(name: string, address: string, email: string, pho
type: actionTypes.REGISTER_RENTER.success,
data: res.data
}))
.catch(err => dispatch({
type: actionTypes.REGISTER_RENTER.failure,
err: err.message
}))
// After a renter is registered successfully, refetch renters.
.then(() => {
dispatch<any>(fetchRenters());
})
.catch(err => {
alert('Could not register renter');
dispatch({
type: actionTypes.REGISTER_RENTER.failure,
err: err.message
})
})
}
}
@@ -4,6 +4,8 @@ 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 REMOVE_FROM_CART = 'REMOVE_FROM_CART';
const CLEAR_CART = 'CLEAR_CART';
const CHECKOUT_BOOK = generateActionType('CHECKOUT_BOOK');
const RETURN_BOOK = generateActionType('RETURN_BOOK');
@@ -12,6 +14,8 @@ export const actionTypes = {
FETCH_COVER_IMAGE,
SET_FILTER,
ADD_TO_CART,
REMOVE_FROM_CART,
CLEAR_CART,
CHECKOUT_BOOK,
RETURN_BOOK
}
@@ -2,13 +2,16 @@ import React from 'react';
import { useSelector, RootStateOrAny } from 'react-redux';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { actionTypes } from '../../../../constants/books/action_types';
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;
height: 225px;
min-width: 325px;
margin: 10px 0;
`
@@ -21,23 +24,27 @@ const EmptyCart = styled.div`
`
const CoverImage = styled.img`
height: 100px;
height: 200px;
object-fit: contain;
margin: auto 15px;
cursor: pointer;
`
const BookCoverCard: React.FC = () => {
const cartItems = useSelector((state: RootStateOrAny) => state.books.cart)
const images = useSelector((state: RootStateOrAny) => state.books.images)
console.log(cartItems)
const dispatch = useDispatch();
const cartItems = useSelector((state: RootStateOrAny) => state.books.cart);
const images = useSelector((state: RootStateOrAny) => state.books.images);
return (
<BookCoverCardContainer>
{cartItems.length === 0 && (<EmptyCart>Nothing here yet!</EmptyCart>)}
{cartItems.length === 0 && (<EmptyCart>Nothing here yet. Pick out a book!</EmptyCart>)}
{cartItems && cartItems.map(cartItem => (
<CoverImage
src={images[cartItem] || 'https://college.indiana.edu/images/publications/book-cover-placeholder.jpg'} />
src={images[cartItem] || 'https://college.indiana.edu/images/publications/book-cover-placeholder.jpg'}
onClick={() => {
dispatch({ type: actionTypes.REMOVE_FROM_CART, data: cartItem })
}}
/>
))}
</BookCoverCardContainer>
)
@@ -1,13 +1,53 @@
import React from 'react';
// import styled from 'styled-components';
import Panel from '../../UIKit/Panel/Panel';
import BookCoverCard from './BookCoverCard/BookCoverCard';
import CartDetail from './CartDetail/CartDetail';
import Renters from '../Renters/Renters';
import { useDispatch, useSelector, RootStateOrAny } from 'react-redux';
import { RouteComponentProps } from 'react-router-dom';
import { checkoutBook } from '../../../actions/books';
import { actionTypes } from '../../../constants/books/action_types';
const Cart: React.FC = () => (
<Panel>
<BookCoverCard />
</Panel>
)
const Cart: React.FC<RouteComponentProps> = ({ history }) => {
const dispatch = useDispatch();
const cart = useSelector((state: RootStateOrAny) => state.books.cart);
let panelStyle = {};
if (cart.length !== 0) {
panelStyle = {
display: 'flex', justifyContent: 'space-between'
}
}
const checkoutHandler = (renterId: string) => {
if (cart.length === 0) {
alert("Add items to your cart before checking out");
return
}
// Checkout each cart item.
cart.map(cartItem => {
dispatch(checkoutBook(cartItem, renterId))
})
// After each item has been checked out clear the cart.
dispatch({ type: actionTypes.CLEAR_CART });
// Redirect to the homepage.
history.push('/');
}
return (
<>
<Panel style={panelStyle}>
<BookCoverCard />
{cart.length !== 0 && <CartDetail />}
</Panel>
<Renters
checkout={checkoutHandler}
/>
</>
);
}
export default Cart;
@@ -0,0 +1,63 @@
import React from 'react';
import styled from 'styled-components';
import { useSelector, RootStateOrAny } from 'react-redux';
const CartDetailContainer = styled.div`
background-color: #fff;
display: flex;
flex-direction: column;
border: 0.1px solid #f0f0f0;
box-shadow: 10px 5px 15px rgba(175,175,175,50);
height: 225px;
min-width: 300px;
width: 100%;
margin: 10px 0 0 20px;
font-family: 'Helvetica';
font-size: 1.5rem;
font-weight: 100;
color: #13334A;
justify-content: center;
align-items: center;
`
const BreakdownContainer = styled.div`
background-color: #fff;
width: 100%;
height: 200px;
font-size: 1.25rem;
padding: 0;
margin: 0;
`
const BreakdownP = styled.p`
margin: 0;
margin-left: 20px;
padding: 0;
line-height: 1;
`
const CartDetail: React.FC = () => {
const cart = useSelector((state: RootStateOrAny) => state.books.cart);
// 12096e5 is a magic number for 14 days.
const dueDate = new Date(Date.now() + 12096e5);
return (
<CartDetailContainer>
Cart
<BreakdownContainer>
<p/>
<BreakdownP>Books in cart:</BreakdownP>
<ul>
{cart.length !== 0 && cart.map(cartItem => (
<li>{cartItem}</li>
))}
</ul>
<BreakdownP>Due date:</BreakdownP>
<BreakdownP>{dueDate.toDateString()}</BreakdownP>
</BreakdownContainer>
</CartDetailContainer>
);
};
export default CartDetail;
@@ -6,6 +6,7 @@ import Header from './Header/Header';
import Library from './Library/Library';
import Cart from './Cart/Cart';
import Renters from './Renters/Renters';
import Register from './Register/Register';
const ContentContainer = styled.div`
width: 100%;
@@ -18,9 +19,9 @@ const Content: React.FC = () => (
<Route path="/" exact component={Library} />
<Route path="/library" component={Library} />
<Route path="/renters" component={Renters} />
<Route path="/register" component={Register} />
<Route path="/return" component={Renters} />
<Route path="/checkout" component={Cart} />
<Route path="/checkout" component={Renters} />
</ContentContainer>
)
@@ -16,7 +16,7 @@ const ButtonBay: React.FC = () => (
<ButtonBayContainer>
<Button
text="Register new renter"
destination="/renters"
destination="/register"
/>
<Button
text="Checkout"
@@ -1,6 +1,8 @@
import React from 'react';
import styled from 'styled-components';
import { useLocation } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { actionTypes } from '../../../../constants/books/action_types';
@@ -37,16 +39,21 @@ const StyledIcon = styled(FontAwesomeIcon)`
const Search: React.FC = () => {
const dispatch = useDispatch();
const location = useLocation();
const displaySearch = location.pathname === '/library' || location.pathname === '/';
return (
<SearchContainer>
<StyledInput
onChange={(e: React.FormEvent<HTMLInputElement>) => {
dispatch({ type: actionTypes.SET_FILTER, data: e.currentTarget.value });
}}
/>
<StyledIcon icon={faSearch} />
</SearchContainer>
<>
{displaySearch && <SearchContainer>
<StyledInput
onChange={(e: React.FormEvent<HTMLInputElement>) => {
dispatch({ type: actionTypes.SET_FILTER, data: e.currentTarget.value });
}}
/>
<StyledIcon icon={faSearch} />
</SearchContainer>}
</>
)
}
@@ -63,8 +63,8 @@ type Props = {
}
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 imageLink = useSelector((state: RootStateOrAny) => state.books.images[title]);
const cart = useSelector((state: RootStateOrAny) => state.books.cart);
const dispatch = useDispatch();
useEffect(() => {
@@ -75,12 +75,10 @@ const Book: React.FC<Props> = ({ title, author, 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 });
}
@@ -18,7 +18,7 @@ const Library: React.FC = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchBooks())
dispatch(fetchBooks());
}, []);
const displayBooks = books.books.filter((book: BookType) => book.title.toLowerCase().indexOf(filter) !== -1);
@@ -0,0 +1,126 @@
import React, { useState } from 'react';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { registerRenter } from '../../../actions/renters';
import { RouteComponentProps } from 'react-router-dom';
import Panel from '../../UIKit/Panel/Panel';
const RegistrationContainer = styled.div`
font-family: 'Helvetica';
width: 80%;
max-width: 600px;
margin: auto;
`
const RegistrationForm = styled.form`
width: 100%;
`
const RegistrationRow = styled.div`
width: 100%;
display: block;
`
const RegistrationColumn = styled.div`
width: 100%;
display: inline-block;
`
const RegistrationLabel = styled.label`
width: 30%;
max-width: 100px;
float: left;
color: #13334A;
height: 38px;
line-height: 38px;
text-align: right;
`
const RegistrationInput = styled.input`
width: 60%;
float: right;
height: 38px;
border: 1px solid #ccc;
color: #13334A;
font-size: 1.25rem;
outline: none;
box-shadow: 10px 5px 15px rgba(175,175,175,50)
`
const RegistrationButton = styled.button`
display: block;
height: 38px;
width: 200px;
margin: 15px auto 0 auto;
cursor: pointer;
`
const Register: React.FC<RouteComponentProps> = ({ history }) => {
const dispatch = useDispatch();
const [name, setName] = useState('');
const [address, setAddress] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
return (
<Panel>
<RegistrationContainer>
<RegistrationForm>
<RegistrationRow>
<RegistrationColumn>
<RegistrationLabel>
Name:
</RegistrationLabel>
<RegistrationInput
onChange={e => {
setName(e.target.value);
}}
/>
</RegistrationColumn>
<RegistrationColumn>
<RegistrationLabel>
Addresss:
</RegistrationLabel>
<RegistrationInput
onChange={e => {
setAddress(e.target.value);
}}
/>
</RegistrationColumn>
<RegistrationColumn>
<RegistrationLabel>
Email:
</RegistrationLabel>
<RegistrationInput
onChange={e => {
setEmail(e.target.value);
}}
/>
</RegistrationColumn>
<RegistrationColumn>
<RegistrationLabel>
Phone:
</RegistrationLabel>
<RegistrationInput
onChange={e => {
setPhone(e.target.value);
}}
/>
</RegistrationColumn>
</RegistrationRow>
</RegistrationForm>
</RegistrationContainer>
<RegistrationButton
onClick={() => {
dispatch(registerRenter(name, address, email, phone));
history.push('/renters');
}}
>Submit</RegistrationButton>
</Panel>
);
};
export default Register;
@@ -1,5 +1,9 @@
import React from 'react';
import React, { useEffect } from 'react';
import styled from 'styled-components';
import { BookType } from '../../../../../types/book';
import { useDispatch, useSelector, RootStateOrAny } from 'react-redux';
import { fetchBookCover, returnBook } from '../../../../../actions/books';
const BookReturnContainer = styled.div`
display: inline-flex;
@@ -11,16 +15,43 @@ const CoverImage = styled.img`
height: 100px;
object-fit: contain;
margin: auto 15px;
cursor: pointer;
`
type Props = {
imageLink: string;
renterId: string;
book: BookType;
}
const BookReturn: React.FC<Props> = ({ imageLink }) => (
<BookReturnContainer>
<CoverImage src={imageLink} />
</BookReturnContainer>
);
const BookReturn: React.FC<Props> = ({ renterId, book }) => {
let title = '';
let isbn = '';
if (book !== undefined) {
title = book.title;
if (book.isbn) {
isbn = book.isbn;
}
}
const dispatch = useDispatch();
const images = useSelector((state: RootStateOrAny) => state.books.images);
useEffect(() => {
if (images[title] === undefined) {
dispatch(fetchBookCover(title, isbn));
}
}, []);
return (
<BookReturnContainer>
<CoverImage
src={images[title] || 'https://college.indiana.edu/images/publications/book-cover-placeholder.jpg'}
onClick={() => {
dispatch(returnBook(title, renterId));
}}
/>
</BookReturnContainer>
);
}
export default BookReturn;
@@ -0,0 +1,31 @@
import React from 'react';
import styled from'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faShoppingBasket } from '@fortawesome/free-solid-svg-icons';
const CheckoutButtonContainer = styled.div`
height: 100%;
width: 200px;
float: right;
text-align: center;
line-height: 135px;
cursor: pointer;
font-family: 'Helvetica';
font-size: 24px;
font-weight: 100;
color: #777;
`
type Props = {
clickHandler: Function;
}
const CheckoutButton: React.FC<Props> = ({ clickHandler }) => (
<CheckoutButtonContainer
onClick={() => clickHandler()}
>
Checkout <FontAwesomeIcon icon={faShoppingBasket} />
</CheckoutButtonContainer>
);
export default CheckoutButton;
@@ -1,7 +1,10 @@
import React from 'react';
import styled from 'styled-components';
import { useSelector, RootStateOrAny } from 'react-redux';
import BookReturn from './BookReturn/BookReturn';
import CheckoutButton from './CheckoutButton/CheckoutButton';
const RenterCardContainer = styled.div`
background-color: #fff;
@@ -55,41 +58,62 @@ const RenterReturnContainer = styled.div`
`
type Props = {
id: string;
name: string;
phoneNumber: string;
email: string;
rentals: [string];
displayRentals: boolean;
displayCheckout: boolean;
handleCheckoutClicked: Function;
}
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"}
]
const RenterCard: React.FC<Props> = ({
id,
name,
phoneNumber,
email,
rentals,
displayRentals,
displayCheckout,
handleCheckoutClicked
}) => {
const books = useSelector((state: RootStateOrAny) => state.books.books);
const RenterCard: React.FC<Props> = ({ name, phoneNumber, email }) => (
<RenterCardContainer>
<RenterImageContainer>
<RenterImagePlaceholder
src="https://avatars3.githubusercontent.com/u/47925772?s=400&v=4" />
</RenterImageContainer>
<RenterInfoContainer>
<RenterName>
{name}
</RenterName>
<RenterPhone>
{phoneNumber}
</RenterPhone>
<RenterEmail>
{email}
</RenterEmail>
</RenterInfoContainer>
<RenterReturnContainer>
{books.map(book => (
<BookReturn
imageLink={book.imageLink}
/>
))}
</RenterReturnContainer>
</RenterCardContainer>
)
return (
<RenterCardContainer>
<RenterImageContainer>
<RenterImagePlaceholder
src="https://avatars3.githubusercontent.com/u/47925772?s=400&v=4" />
</RenterImageContainer>
<RenterInfoContainer>
<RenterName>
{name}
</RenterName>
<RenterPhone>
{phoneNumber}
</RenterPhone>
<RenterEmail>
{email}
</RenterEmail>
</RenterInfoContainer>
<RenterReturnContainer>
{books && displayRentals && rentals && rentals.map(rental => {
return (
<BookReturn
key={id}
renterId={id}
book={books.find(book => book.title === rental)}
/>
);
})}
{displayCheckout &&
<CheckoutButton
clickHandler={() => handleCheckoutClicked(id)}
/>}
</RenterReturnContainer>
</RenterCardContainer>
);
};
export default RenterCard;
@@ -7,9 +7,19 @@ import RenterCard from './RenterCard/RenterCard';
import { RenterType } from '../../../types/renter';
const Renters: React.FC = () => {
const renters = useSelector((state: RootStateOrAny) => state.renters);
import { useLocation } from 'react-router-dom';
type Props = {
checkout: Function;
}
const Renters: React.FC<Props> = ({ checkout }) => {
const renters = useSelector((state: RootStateOrAny) => state.renters.renters);
const dispatch = useDispatch();
const location = useLocation();
const displayRentals = location.pathname === '/return';
const displayCheckout = location.pathname === '/checkout';
useEffect(() => {
dispatch(fetchRenters())
@@ -22,11 +32,17 @@ const Renters: React.FC = () => {
flexWrap: 'wrap',
justifyContent: 'center'
}}>
{renters && renters.renters.map((renter: RenterType) => (
{renters && renters.map((renter: RenterType) => (
<RenterCard
name={renter.name || ''}
phoneNumber={renter.phoneNumber || ''}
email={renter.email || ''}
key={renter.id}
id={renter.id}
name={renter.name}
phoneNumber={renter.phoneNumber}
email={renter.email}
rentals={renter.rentals}
displayRentals={displayRentals}
displayCheckout={displayCheckout}
handleCheckoutClicked={checkout}
/>
))}
</Panel>
@@ -6,7 +6,7 @@ import Menu from './Menu/Menu'
const SidebarContainer = styled.div`
background: #011126;
box-shadow: 0px 0px 5px rgba(0,0,0,50);
box-shadow: 0px 0px 12px rgba(0,0,0,100);
padding-top: 2.5%;
min-width: 275px;
flex: 1;
+16
View File
@@ -73,11 +73,20 @@ export function books(state = initialState, action: Action) {
// ADD_TO_CART action
case actionTypes.ADD_TO_CART:
alert(`Added ${action.data} to the cart`);
return {
...state,
cart: state.cart.concat(action.data)
}
// REMOVE_FROM_CART action
case actionTypes.REMOVE_FROM_CART:
alert(`Removed ${action.data} from the cart`);
return {
...state,
cart: state.cart.filter(item => item !== action.data)
}
// CHECKOUT_BOOK action
case actionTypes.CHECKOUT_BOOK.request:
return {
@@ -115,6 +124,13 @@ export function books(state = initialState, action: Action) {
loading: false,
returnError: action.err
}
// CLEAR_CART action
case actionTypes.CLEAR_CART:
return {
...state,
cart: [],
}
default:
return state;
+2
View File
@@ -1,6 +1,8 @@
export type RenterType = {
id: string;
name: string;
address: string;
email: string;
phoneNumber: string;
rentals: [string];
}