diff --git a/package.json b/package.json
index a5860af..07469ce 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"isomorphic-fetch": "^2.2.1",
"path": "^0.12.7",
"prop-types": "^15.5.8",
+ "query-string": "^4.3.4",
"react": "^15.5.4",
"react-dom": "^15.5.4",
"react-redux": "^5.0.5",
diff --git a/src/actions/actionTypes.js b/src/actions/actionTypes.js
new file mode 100644
index 0000000..1ceea4b
--- /dev/null
+++ b/src/actions/actionTypes.js
@@ -0,0 +1,3 @@
+// Library actions
+export const LIBRARY_LIST_BOOKS = 'LIBRARY_LIST_BOOKS';
+export const LIBRARY_SHOW_SINGLE_BOOK = 'LIBRARY_SHOW_SINGLE_BOOK';
diff --git a/src/config/index.js b/src/config/index.js
new file mode 100644
index 0000000..999d405
--- /dev/null
+++ b/src/config/index.js
@@ -0,0 +1,12 @@
+export default {
+ serverPort: 3000,
+ baseUrl: 'http://localhost:3000',
+ api: {
+ url: '/api/'
+ },
+ views: {
+ engine: '.hbs',
+ extension: '.hbs',
+ path: './views'
+ }
+}
diff --git a/src/constants/api.js b/src/constants/api.js
new file mode 100644
index 0000000..7380d02
--- /dev/null
+++ b/src/constants/api.js
@@ -0,0 +1,6 @@
+export const API = Object.freeze({
+ LIBRARY: {
+ BOOK: 'library/book',
+ BOOKS: 'library/books'
+ }
+});
diff --git a/src/containers/Library/actions.js b/src/containers/Library/actions.js
new file mode 100644
index 0000000..938629f
--- /dev/null
+++ b/src/containers/Library/actions.js
@@ -0,0 +1,19 @@
+// Actions Types
+import * as types from '../../actions/actionTypes';
+
+// Api
+import libraryApi from './api';
+
+export function loadBooks() {
+ return {
+ type: types.LIBRARY_LIST_BOOKS,
+ payload: libraryApi.getAllBooks()
+ };
+}
+
+export function loadSingleBook(query) {
+ return {
+ type: types.LIBRARY_SHOW_SINGLE_BOOK,
+ payload: libraryApi.getSingleBook(query)
+ };
+}
diff --git a/src/containers/Library/api.js b/src/containers/Library/api.js
new file mode 100644
index 0000000..5bc62c2
--- /dev/null
+++ b/src/containers/Library/api.js
@@ -0,0 +1,17 @@
+// Constants
+import { API } from '../../constants/api';
+
+// Utils
+import { apiFetch } from '../../lib/utils/api';
+
+class LibraryApi {
+ static getAllBooks(query) {
+ return apiFetch(API.LIBRARY.BOOKS, {}, query);
+ }
+
+ static getSingleBook(query) {
+ return apiFetch(API.LIBRARY.BOOK, {}, query);
+ }
+}
+
+export default LibraryApi;
diff --git a/src/containers/Library/index.js b/src/containers/Library/index.js
new file mode 100644
index 0000000..70844bf
--- /dev/null
+++ b/src/containers/Library/index.js
@@ -0,0 +1,133 @@
+// Dependencies
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import { Link } from 'react-router-dom';
+
+// Actions
+import * as actions from '../../containers/Library/actions';
+
+// Utils
+import { isFirstRender } from '../../lib/utils/frontend';
+
+class Library extends Component {
+ static propTypes = {
+ loadBooks: PropTypes.func.isRequired,
+ books: PropTypes.array.isRequired,
+ book: PropTypes.array
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ displaySingleBook: false
+ };
+ }
+
+ componentWillMount() {
+ const {
+ match: {
+ params: {
+ id = 0
+ }
+ }
+ } = this.props;
+
+ if (id > 0) {
+ this.setState({
+ displaySingleBook: true
+ });
+
+ this.props.loadSingleBook({ id });
+ } else {
+ this.setState({
+ displaySingleBook: false
+ });
+
+ this.props.loadBooks();
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const {
+ match: {
+ params: {
+ id = 0
+ }
+ }
+ } = nextProps;
+
+ if (nextProps.match.params !== this.props.match.params) {
+ if (id > 0) {
+ this.setState({
+ displaySingleBook: true
+ });
+
+ this.props.loadSingleBook({ id });
+ } else {
+ this.setState({
+ displaySingleBook: false
+ });
+ }
+ }
+ }
+
+ renderSingleBook(book) {
+ return (
+
+
{book.title}
+
Autor: {book.author}
+

+
Go back
+
+ );
+ }
+
+ renderBooksList(books) {
+ return (
+
+
Library
+
+ {
+ books.map((book, key) => {
+ return (
+ -
+ {book.title} - {book.author}
+
+ )
+ })
+ }
+
+
+ );
+ }
+
+ render() {
+ const {
+ books,
+ book
+ } = this.props;
+
+ if (isFirstRender(books) && book.length === 0) {
+ return null;
+ }
+
+ let show = this.renderBooksList(books);
+
+ if (this.state.displaySingleBook && book.length > 0) {
+ show = this.renderSingleBook(book[0]);
+ }
+
+ return (
+
+ {show}
+
+ );
+ }
+}
+
+export default connect(state => ({
+ books: state.library.books,
+ book: state.library.book
+}), actions)(Library);
diff --git a/src/containers/Library/reducer.js b/src/containers/Library/reducer.js
new file mode 100644
index 0000000..cdb7ad4
--- /dev/null
+++ b/src/containers/Library/reducer.js
@@ -0,0 +1,30 @@
+// Utils
+import { getNewState } from '../../lib/utils/frontend';
+
+const initialState = {
+ books: [],
+ book: []
+};
+
+export default function libraryReducer(state = initialState, action) {
+ switch (action.type) {
+ case 'LIBRARY_LIST_BOOKS_SUCCESS': {
+ const { payload: { response = [] } } = action;
+
+ return getNewState(state, {
+ books: response
+ });
+ }
+
+ case 'LIBRARY_SHOW_SINGLE_BOOK_SUCCESS': {
+ const { payload: { response = [] } } = action;
+
+ return getNewState(state, {
+ book: response
+ });
+ }
+
+ default:
+ return state;
+ }
+}
diff --git a/src/data/books.json b/src/data/books.json
new file mode 100644
index 0000000..9c84958
--- /dev/null
+++ b/src/data/books.json
@@ -0,0 +1,34 @@
+{
+ "response": [
+ {
+ "id": 1,
+ "title": "El SeƱor de los Anillos",
+ "author": "J. R. R. Tolkien",
+ "image": "https://imagessl7.casadellibro.com/a/l/t0/87/9788445000687.jpg"
+ },
+ {
+ "id": 2,
+ "title": "Padre Rico Padre Pobre",
+ "author": "Robert Kiyosaki",
+ "image": "https://imagessl2.casadellibro.com/a/l/t0/02/9788466317702.jpg"
+ },
+ {
+ "id": 3,
+ "title": "El Tao de Warren Buffett",
+ "author": "Mary Buffett",
+ "image": "https://imagessl6.casadellibro.com/a/l/t0/56/9788493562656.jpg"
+ },
+ {
+ "id": 4,
+ "title": "Burlar al Diablo. Secretos desde la Cripta",
+ "author": "Napoleon Hill",
+ "image": "https://images-na.ssl-images-amazon.com/images/I/41DcYrw4upL._SX311_BO1,204,203,200_.jpg"
+ },
+ {
+ "id": 5,
+ "title": "El Alquimista",
+ "author": "Paulo Coelho",
+ "image": "http://libros-gratis.com/wp-content/uploads/2015/12/el-alquimista.jpg"
+ }
+ ]
+}
diff --git a/src/data/menu.js b/src/data/menu.js
index 33c2e95..915f9d4 100644
--- a/src/data/menu.js
+++ b/src/data/menu.js
@@ -10,5 +10,9 @@ export default [
{
title: 'Contact Us',
url: '/contact'
+ },
+ {
+ title: 'Library',
+ url: '/library'
}
];
diff --git a/src/data/post.json b/src/data/post.json
deleted file mode 100644
index 4187a5f..0000000
--- a/src/data/post.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "response": [
- {
- "id": 1,
- "title": "Test 1",
- "slug": "test-1",
- "content": "Test 1 - Content
",
- "author": "Codejobs",
- "day": "16",
- "month": "06",
- "year": "2017",
- "language": "es",
- "state": "Active"
- }
- ]
-}
diff --git a/src/index.js b/src/index.js
index c70a541..e06579d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,4 +1,5 @@
// Dependencies
+import 'babel-polyfill';
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
diff --git a/src/lib/utils/api.js b/src/lib/utils/api.js
new file mode 100644
index 0000000..becffee
--- /dev/null
+++ b/src/lib/utils/api.js
@@ -0,0 +1,59 @@
+// Dependencies
+import queryString from 'query-string';
+
+// Config
+import config from '../../config';
+
+export function apiEndpoint(endpoint, qs) {
+ let query = '';
+
+ if (qs) {
+ query = `?${qs}`;
+ }
+
+ return `${config.api.url}${endpoint}${query}`;
+}
+
+export function apiFetch(endpoint, options = {}, query = false) {
+ let qs;
+
+ if (query) {
+ qs = queryString.stringify(query);
+ }
+
+ const getPromise = async () => {
+ try {
+ const fetchOptions = apiOptions(options);
+ const fetchEndpoint = apiEndpoint(endpoint, qs);
+ const response = await fetch(fetchEndpoint, fetchOptions);
+
+ return response.json();
+ } catch (e) {
+ throw e;
+ }
+ };
+
+ return getPromise();
+}
+
+export function apiOptions(options = {}) {
+ const {
+ method = 'GET',
+ headers = {
+ 'Content-Type': 'application/json'
+ },
+ body = false
+ } = options;
+
+ const newOptions = {
+ method,
+ headers,
+ credentials: 'include'
+ };
+
+ if (body) {
+ newOptions.body = body;
+ }
+
+ return newOptions;
+}
diff --git a/src/lib/utils/frontend.js b/src/lib/utils/frontend.js
new file mode 100644
index 0000000..b10483f
--- /dev/null
+++ b/src/lib/utils/frontend.js
@@ -0,0 +1,9 @@
+import { isDefined } from './is';
+
+export function getNewState(state, newState) {
+ return Object.assign({}, state, newState);
+}
+
+export function isFirstRender(items) {
+ return items && items.length === 0 || !isDefined(items);
+}
diff --git a/src/lib/utils/is.js b/src/lib/utils/is.js
new file mode 100644
index 0000000..bde4006
--- /dev/null
+++ b/src/lib/utils/is.js
@@ -0,0 +1,15 @@
+export function isArray(variable) {
+ return variable instanceof Array;
+}
+
+export function isDefined(variable) {
+ return typeof variable !== 'undefined' && variable !== null;
+}
+
+export function isFunction(variable) {
+ return typeof variable === 'function';
+}
+
+export function isObject(variable) {
+ return isDefined(variable) && typeof variable === 'object';
+}
diff --git a/src/reducers/index.js b/src/reducers/index.js
index bf7efbf..3888552 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -1,11 +1,15 @@
// Dependencies
import { combineReducers } from 'redux';
+// App Reducers
+import library from '../containers/Library/reducer';
+
// Shared Reducers
import device from './deviceReducer';
const rootReducer = combineReducers({
- device
+ device,
+ library
});
export default rootReducer;
diff --git a/src/routes.js b/src/routes.js
index 1b9b933..22380dc 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -10,12 +10,15 @@ import Page404 from './components/Page404';
// Container
import Home from './containers/Home';
+import Library from './containers/Library';
const AppRoutes = () =>
+
+
diff --git a/src/server/api/blog.js b/src/server/api/blog.js
index d716ff5..185b62e 100644
--- a/src/server/api/blog.js
+++ b/src/server/api/blog.js
@@ -3,7 +3,6 @@ import express from 'express';
// Data
import posts from '../../data/posts.json';
-import post from '../../data/post.json';
// Express Router
const Router = express.Router();
@@ -13,7 +12,17 @@ Router.get('/posts', (req, res, next) => {
});
Router.get('/post', (req, res, next) => {
- res.json(post);
+ const {
+ query: {
+ slug = ''
+ }
+ } = req;
+
+ const selectedPost = posts.response.filter(book => book.slug === slug);
+
+ res.json({
+ response: selectedPost
+ });
});
export default Router;
diff --git a/src/server/api/library.js b/src/server/api/library.js
new file mode 100644
index 0000000..01828f6
--- /dev/null
+++ b/src/server/api/library.js
@@ -0,0 +1,28 @@
+// Dependencies
+import express from 'express';
+
+// Data
+import books from '../../data/books.json';
+
+// Express Router
+const Router = express.Router();
+
+Router.get('/books', (req, res, next) => {
+ res.json(books);
+});
+
+Router.get('/book', (req, res, next) => {
+ const {
+ query: {
+ id = 0
+ }
+ } = req;
+
+ const selectedBook = books.response.filter(book => book.id === Number(id));
+
+ res.json({
+ response: selectedBook
+ });
+});
+
+export default Router;
diff --git a/src/server/index.js b/src/server/index.js
index f25df20..83836f7 100644
--- a/src/server/index.js
+++ b/src/server/index.js
@@ -7,11 +7,15 @@ import webpackHotMiddleware from 'webpack-hot-middleware';
import open from 'open';
import exphbs from 'express-handlebars';
+// Config
+import config from '../config';
+
// Webpack Configuration
import webpackConfig from '../../webpack.config.babel';
// API
import blogApi from './api/blog';
+import libraryApi from './api/library';
// Helpers
import * as hbsHelper from '../lib/handlebars';
@@ -19,9 +23,6 @@ import * as hbsHelper from '../lib/handlebars';
// Utils
import { isMobile } from '../lib/utils/device';
-// Server Port
-const port = 3000;
-
// Environment
const isDevelopment = process.env.NODE_ENV !== 'production';
@@ -32,14 +33,14 @@ const app = express();
app.use(express.static(path.join(__dirname, '../public')));
// Handlebars setup
-app.engine('.hbs', exphbs({
- extname: '.hbs',
+app.engine(config.views.engine, exphbs({
+ extname: config.views.extension,
helpers: hbsHelper
}));
// View Engine Setup
-app.set('views', path.join(__dirname, './views'));
-app.set('view engine', '.hbs');
+app.set('views', path.join(__dirname, config.views.path));
+app.set('view engine', config.views.engine);
// Webpack Compiler
const webpackCompiler = webpack(webpackConfig);
@@ -58,6 +59,7 @@ app.use((req, res, next) => {
// API dispatch
app.use('/api/blog', blogApi);
+app.use('/api/library', libraryApi);
// Sending all the traffic to React
app.get('*', (req, res) => {
@@ -67,8 +69,8 @@ app.get('*', (req, res) => {
});
// Listen port 3000
-app.listen(port, err => {
+app.listen(config.serverPort, err => {
if (!err) {
- open(`http://localhost:${port}`);
+ open(`${config.baseUrl}`);
}
});
diff --git a/yarn.lock b/yarn.lock
index ce0c053..666ab45 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1948,10 +1948,6 @@ hoek@2.x.x:
version "2.16.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
-hoist-non-react-statics@1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.0.5.tgz#0e36d2c130c8511f267a0d4ceb45ec7d7e4f0c70"
-
hoist-non-react-statics@^1.0.3, hoist-non-react-statics@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
@@ -2058,12 +2054,6 @@ interpret@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90"
-invariant@2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.0.tgz#c8d7e847366a49cc18b622f058a689d481e895f2"
- dependencies:
- loose-envify "^1.0.0"
-
invariant@^2.0.0, invariant@^2.1.0, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
@@ -2209,10 +2199,6 @@ js-base64@^2.1.9:
version "2.1.9"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.1.9.tgz#f0e80ae039a4bd654b5f281fc93f04a914a7fcce"
-js-tokens@^1.0.1:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-1.0.3.tgz#14e56eb68c8f1a92c43d59f5014ec29dc20f2ae1"
-
js-tokens@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"
@@ -2342,12 +2328,6 @@ longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
-loose-envify@1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.1.0.tgz#527582d62cff4e04da3f9976c7110d3392ec7e0c"
- dependencies:
- js-tokens "^1.0.1"
-
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
@@ -3102,7 +3082,7 @@ qs@6.4.0, qs@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
-query-string@^4.1.0:
+query-string@^4.1.0, query-string@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
dependencies:
@@ -3286,14 +3266,6 @@ redux-promise-middleware@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/redux-promise-middleware/-/redux-promise-middleware-4.3.0.tgz#38997574b6f150ab8ff1aa72ca5563fbc82c7cef"
-redux-state@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/redux-state/-/redux-state-1.1.0.tgz#04ed8304f902282d34cf9317eb29bb830f1088f7"
- dependencies:
- hoist-non-react-statics "1.0.5"
- invariant "2.2.0"
- loose-envify "1.1.0"
-
redux@^3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.0.tgz#07a623cafd92eee8abe309d13d16538f6707926f"