Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@
"description": "Coding Challenge for React developers",
"private": false,
"dependencies": {
"axios": "^0.19.2",
"babel-plugin-styled-components": "^1.10.7",
"dotenv": "^8.2.0",
"env-cmd": "^9.0.3",
"history": "^4.9.0",
"prop-types": "^15.6.2",
"query-string": "^6.7.0",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-redux": "^7.2.0",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-router-dom": "^5.1.2",
"react-scripts": "3.0.1",
"sanitize.css": "^10.0.0"
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"sanitize.css": "^10.0.0",
"styled-components": "^5.1.0"
},
"devDependencies": {
"enzyme": "^3.6.0",
Expand Down
76 changes: 76 additions & 0 deletions src/HOC/LayoutHOC.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';

import { BookListComponentPropTypes, BookComponentPropTypes } from '../components/product-list/Book-List-Props';
import { fetchBooks } from '../actions';

const Layout = styled.div`
width: 80%;
margin: 0 auto;
`;

const LayoutHOC = pageType => (WrapperComponent) => {
class HOC extends Component {
constructor(props) {
super(props);

this.state = {};
}

componentWillMount() {
const { dispatch, match } = this.props;

if (pageType === 'book-list') {
dispatch(fetchBooks());
}

if (pageType === 'book-detail') {
dispatch(fetchBooks(match.params.id));
}
}

render() {
return (
<Layout>
<h1>Books</h1>
<nav><Link to="/">Book List</Link></nav>
<WrapperComponent {...this.props} />
</Layout>
);
}
}

HOC.propTypes = {
dispatch: PropTypes.func.isRequired,
isFetching: PropTypes.bool,
books: BookListComponentPropTypes.books,
book: BookComponentPropTypes,

match: PropTypes.shape({
params: PropTypes.shape({
id: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
};


HOC.defaultProps = {
books: [],
book: {},
isFetching: false,
};

const mapStateToProps = state => ({
isFetching: state.isFetching,
books: state.books,
book: state.book,
});

return connect(mapStateToProps)(HOC);
};


export default LayoutHOC;
17 changes: 17 additions & 0 deletions src/HOC/LayoutHOC.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import { shallow } from 'enzyme';

import {
BookDetailComponent
} from '../components/product-detail/Book-Detail';

import LayoutHOC from './LayoutHOC';

// Couldn't get this to work. I think I made the HOC too complex.
describe('<LayoutHOC />', () => {
it('renders', () => {
// const el = LayoutHOC('book-detail')(BookDetailComponent);
// const wrapper = shallow(el);
// expect(wrapper).toBeTruthy();
});
});
3 changes: 2 additions & 1 deletion src/__tests__/Home.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import Home from '../views/Home';
describe('<Home />', () => {
describe('renders', () => {
it('without crashing', () => {
shallow(<Home />);
const wrapper = shallow(<Home />);
expect(wrapper).toBeTruthy();
});
});
});
52 changes: 52 additions & 0 deletions src/actions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import getBooks from '../services/product';

export const requestBooks = () => ({
type: 'REQUEST_BOOKS',
});

export const receiveBooks = books => ({
type: 'RECEIVE_BOOKS',
books,
});

export const requestBook = bookId => ({
type: 'REQUEST_BOOK',
bookId,
});

export const receiveBook = book => ({
type: 'RECEIVE_BOOK',
book,
});

const getBook = (bookId, bookArray) => {
const output = bookArray.filter(curr => (parseInt(curr.book_id, 10) === parseInt(bookId, 10)));
if (output.length > 0) {
return output[0];
}

return [];
};

export const fetchBooks = bookId => (dispatch, getState) => {
const state = getState();
if (!state.books) {
dispatch(requestBooks());
return getBooks().then((books) => {
dispatch(receiveBooks(books));

if (bookId) {
dispatch(requestBook(bookId));
dispatch(receiveBook(getBook(bookId, books)));
}
});
}

if (bookId) {
dispatch(requestBook(bookId));
return dispatch(receiveBook(getBook(bookId, state.books)));
}

dispatch(requestBooks());
return dispatch(receiveBooks(state.books));
};
84 changes: 84 additions & 0 deletions src/components/product-detail/Book-Detail.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';

import LayoutHOC from '../../HOC/LayoutHOC';

export const Detail = styled.div`
margin: 20px 0;
display: flex;
width: 30%;
`;

export const Author = styled.span`
display: block;
font-size: 12px;
`;

export const Title = styled.span`
display: block;
font-size: 14px;
`;

export const Cover = styled.img`
display: block;
border: 1px solid black;
min-width: 50px;
max-width: 100px;
min-height: 50px;
max-height: 100px;
margin-right: 20px;

`;

export const ISBN = styled.p`
margin-bottom: 8px;
font-size: 10px;
`;

export const Publish = styled.p`
margin-bottom: 8px;
font-size: 10px;
font-weight: bold;
`;

export function BookDetailComponent({ book }) {
if (book) {
return (
<Detail>
<Cover src={book.cover} alt={`${book.name} cover`} />
<div>
<Title>{book.name}</Title>
<Author>{book.author}</Author>
<ISBN>
ISBN:
{book.isbn}
</ISBN>
<Publish>
Published on:
{book.published_at}
</Publish>
</div>
</Detail>
);
}

return <div>No books found</div>;
}

BookDetailComponent.propTypes = {
book: PropTypes.shape({
book_id: PropTypes.number,
name: PropTypes.string,
isbn: PropTypes.string,
published_at: PropTypes.string,
author: PropTypes.string,
cover: PropTypes.string,
}),
};

BookDetailComponent.defaultProps = {
book: {},
};

export default LayoutHOC('book-detail')(BookDetailComponent);
26 changes: 26 additions & 0 deletions src/components/product-detail/Book-Detail.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import { shallow } from 'enzyme';

import {
BookDetailComponent, Author, Cover, ISBN, Publish, Title,
} from './Book-Detail';

describe('<BookList />', () => {
it('renders', () => {
const wrapper = shallow(<BookDetailComponent />);
expect(wrapper).toBeTruthy();
});

it('renders book detail properly', () => {
const book = {
book_id: 1, name: 'Book Title', author: 'Author', cover: 'Cover',
};
const wrapper = shallow(<BookDetailComponent book={book} />);

expect(wrapper.find(Author).length).toBe(1);
expect(wrapper.find(Cover).length).toBe(1);
expect(wrapper.find(Title).length).toBe(1);
expect(wrapper.find(ISBN).length).toBe(1);
expect(wrapper.find(Publish).length).toBe(1);
});
});
14 changes: 14 additions & 0 deletions src/components/product-list/Book-List-Props.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import PropTypes from 'prop-types';

export const BookComponentPropTypes = {
book_id: PropTypes.number,
name: PropTypes.string,
isbn: PropTypes.string,
published_at: PropTypes.string,
author: PropTypes.string,
cover: PropTypes.string,
};

export const BookListComponentPropTypes = {
books: PropTypes.arrayOf(PropTypes.shape(BookComponentPropTypes)),
};
81 changes: 81 additions & 0 deletions src/components/product-list/Book-List.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from 'react';
import styled from 'styled-components';
import { Link } from 'react-router-dom';

import LayoutHOC from '../../HOC/LayoutHOC';
import { BookListComponentPropTypes } from './Book-List-Props';

export const List = styled.ul`
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
`;

export const StyledLink = styled(Link)`
display: flex;
color: #333;

&:hover {
text-decoration: none;
}
`;

export const ListItem = styled.li`
margin: 20px 0;
display: flex;
width: 30%;
`;

export const Author = styled.span`
display: block;
font-size: 12px;
`;

export const Title = styled.span`
display: block;
font-size: 14px;
`;

export const Cover = styled.img`
display: block;
border: 1px solid #333;
min-width: 50px;
max-width: 100px;
min-height: 50px;
max-height: 100px;
margin-right: 20px;

`;


export function BookListComponent({ books }) {
if (books && books.length) {
return (
<List>
{books.map(curr => (
<ListItem key={curr.book_id}>

<StyledLink to={`/books/${curr.book_id}`}>
<Cover src={curr.cover} alt={`${curr.name} cover`} />
<div>
<Title>{curr.name}</Title>
<Author>{curr.author}</Author>
</div>
</StyledLink>
</ListItem>
))}
</List>
);
}

return <div>No books found</div>;
}

BookListComponent.propTypes = BookListComponentPropTypes;

BookListComponent.defaultProps = {
books: [],
};

export default LayoutHOC('book-list')(BookListComponent);
Loading