diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..525e668
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+/package-lock.json
+/client/dist/bundle.js
+/client/.DS_Store
+/node_modules/
+Archive.zip
+.env
+.DS_Store
+
+# Elastic Beanstalk Files
+.elasticbeanstalk/*
+!.elasticbeanstalk/*.cfg.yml
+!.elasticbeanstalk/*.global.yml
diff --git a/app.js b/app.js
new file mode 100644
index 0000000..b5a7d68
--- /dev/null
+++ b/app.js
@@ -0,0 +1,41 @@
+// SERVER
+
+const express = require('express')
+const parser = require('body-parser')
+const axios = require('axios');
+const app = express()
+
+const db = require('./database/index.js')
+
+if (process.env.NODE_ENV !== 'production') {
+ require('dotenv').config();
+}
+
+const port = process.env.PORT || 8000;
+const apiKey = process.env.API_KEY
+
+app.use(express.static(__dirname + '/client/dist'));
+app.use(parser.json());
+
+app.get('/books/top', (req, res) => {
+ let url = `https://api.nytimes.com/svc/books/v3/lists/current/hardcover-fiction.json?api-key=${apiKey}`
+ axios.get(url)
+ .then(response => res.end(JSON.stringify(response.data)))
+ .catch(err => res.end(err))
+})
+
+app.get('/books/wishlist', (req, res) => {
+ db.getWishlistBooks((err, response) => res.end(JSON.stringify(response)))
+})
+
+app.post('/books/wishlist', (req, res) => {
+ db.addToWishlist(req.body.currentBook, (err) => res.end(err))
+})
+
+app.delete('/books/wishlist', (req, res) => {
+ db.removeFromWishlist(req.body.currentBook, (err) => res.end(err))
+})
+
+app.listen(port, () => {
+ console.log(`listening on port ${port}`)
+})
\ No newline at end of file
diff --git a/client/dist/index.html b/client/dist/index.html
new file mode 100644
index 0000000..e309e01
--- /dev/null
+++ b/client/dist/index.html
@@ -0,0 +1,12 @@
+
+
+
+ OkayReads
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/dist/styles.css b/client/dist/styles.css
new file mode 100644
index 0000000..00e24e3
--- /dev/null
+++ b/client/dist/styles.css
@@ -0,0 +1,173 @@
+html, body, #app {
+ margin: 0;
+ padding: 0;
+ font-family: "Roboto", sans-serif;
+ font-weight: 400;
+ height: 100%;
+ width: 100%;
+}
+
+#app {
+ height: 100%;
+ width: 100%;
+ background-color: rgba(231, 76, 60, 0.25);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.main {
+ flex-grow: 1;
+ display: flex;
+}
+
+.wishlist-empty{
+ color: white;
+}
+
+.books {
+ flex-basis:80%;
+ display: flex;
+ flex-wrap: wrap;
+ min-height: 20rem;
+ margin: 0;
+ padding: 20px;
+ overflow-y: scroll;
+ background-color: #2c3e50;
+}
+
+.book_item {
+ flex-basis: 22%;
+ box-sizing: border-box;
+ margin: 1.5%;
+ display: flex;
+ flex-direction: column;
+ list-style: none;
+ background-color: #fff;
+ border: 1px solid #eee;
+ box-shadow: 0 10px 28px -7px rgba(0,0,0,0.1);
+ cursor: pointer;
+}
+
+.book_item > div > img {
+ width: 100%;
+}
+
+.book_description {
+ display: flex;
+ flex-grow: 1;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: 10px;
+}
+
+.book_description h2 {
+ color: #555;
+ font-weight: bold;
+ margin-bottom: 20px;
+}
+
+.book_details {
+ display: flex;
+ justify-content: space-between;
+}
+
+.book_details span {
+ color: #555;
+ font-size: 0.8rem;
+ font-weight: bold;
+}
+
+.book_year, .book_rating {
+ display: flex;
+ flex-direction: column;
+}
+
+.book_year, .book_rating{
+ color: #aaa;
+ margin-bottom: 5px;
+ font-size: 0.65rem;
+ font-weight: normal;
+}
+
+.book_rating {
+ align-items: flex-end;
+}
+
+.app {
+ height: 10em;
+ width: 10em;
+ background: lightblue;
+ overflow: hidden;
+}
+
+#modal-root {
+ position: relative;
+ z-index: 999;
+}
+
+.modal-right {
+ padding: 2rem;
+}
+
+.modal {
+ background-color: rgba(248, 247, 247, 0.8);
+ position: fixed;
+ height: 100%;
+ width: 100%;
+ top: 0;
+ left: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.modal-left{
+ padding-bottom: 1rem;
+}
+
+#modal-x{
+ float: right;
+ cursor: pointer;
+}
+
+.preview {
+ background-color: white;
+ padding: 10px;
+ max-width: 70%;
+}
+
+.right-align {
+ float: right;
+ padding: 5px;
+}
+
+.header {
+ padding: 10px;
+}
+
+button{
+ border: 0;
+ background: #21a8ca;
+ border-radius: 5px;
+ padding: 0.5rem 1rem;
+ font-size: 0.8rem;
+ line-height: 1;
+ color: white;
+ cursor: pointer;
+}
+
+.close-btn {
+ width: 100%;
+ border: 0;
+ background: #21a8ca;
+ border-radius: 5px;
+ padding: 0.5rem 1rem;
+ font-size: 0.8rem;
+ line-height: 1;
+ color: white;
+}
+
+#wishlist-notice{
+ visibility: hidden;
+}
\ No newline at end of file
diff --git a/client/src/components/books.jsx b/client/src/components/books.jsx
new file mode 100644
index 0000000..8bb6ff7
--- /dev/null
+++ b/client/src/components/books.jsx
@@ -0,0 +1,44 @@
+
+import React, { Component } from 'react';
+
+class Books extends Component {
+
+ render() {
+
+ if (!this.props.books) {
+ return (
+
+ Oh no! You haven't added any books to your wishlist. Check out the NYT bestsellers and start adding now!
+
+ )
+ }
+
+
+ return (
+
+ {
+ this.props.books.map((book, idx) => {
+ return -
+
+

+
+
{book.title}
+
+
+
+ {book.author}
+
+
+ Rank:
+ {book.rank}
+
+
+
+
+ })
+ }
+
)
+ }
+}
+
+export default Books
\ No newline at end of file
diff --git a/client/src/components/modal.jsx b/client/src/components/modal.jsx
new file mode 100644
index 0000000..485fab3
--- /dev/null
+++ b/client/src/components/modal.jsx
@@ -0,0 +1,31 @@
+
+import React, { Component } from 'react';
+import ReactDOM, { createPortal } from 'react-dom';
+
+const modalRoot = document.getElementById('modal-root');
+
+class Modal extends Component {
+
+ constructor(props) {
+ super(props)
+ this.el = document.createElement('div');
+ }
+
+ componentDidMount() {
+ modalRoot.appendChild(this.el);
+ }
+
+ componentWillUnmount() {
+ modalRoot.removeChild(this.el);
+ }
+
+ render() {
+ return createPortal(
+ this.props.children,
+ this.el,
+ );
+
+ }
+}
+
+export default Modal
\ No newline at end of file
diff --git a/client/src/components/selectedBook.jsx b/client/src/components/selectedBook.jsx
new file mode 100644
index 0000000..7592879
--- /dev/null
+++ b/client/src/components/selectedBook.jsx
@@ -0,0 +1,46 @@
+import React, { Fragment } from 'react'
+
+const SelectedBook = ({ currentBook, handleHideModal, showWishlist, showMsg, addToWishlist, deleteFromWishlist }) => (
+
+
+
+
{currentBook.title} X
+
+ {/* div 1 */}
+
+

+
+ {/* div 2 */}
+
+
+ Author: {currentBook.author}
+
+
+ Rank: {currentBook.rank}
+
+
+ {currentBook.description}
+
+
+
+ {showWishlist ?
+ :
+ }
+
{showMsg}
+
+
+
+
+
+
+
+
+
+);
+
+const handleOnClick = function (method) {
+ method()
+
+}
+
+export default SelectedBook
\ No newline at end of file
diff --git a/client/src/index.jsx b/client/src/index.jsx
new file mode 100644
index 0000000..f07bcb4
--- /dev/null
+++ b/client/src/index.jsx
@@ -0,0 +1,133 @@
+// CLIENT
+
+import React, { Component, Fragment } from 'react';
+import ReactDOM from 'react-dom';
+import axios from 'axios';
+
+import Books from './components/books.jsx'
+import Modal from './components/modal.jsx'
+import SelectedBook from './components/selectedBook.jsx'
+
+const errorMessage = "Oh no! An error occurred. Please try again later."
+
+class App extends Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ showWishlist: false,
+ wishlist: [],
+ books: [],
+ currentBook: {},
+ showError: false,
+ showModal: false,
+ showMsg: false,
+ }
+ this.getTopBooks = this.getTopBooks.bind(this)
+ this.handleBookClick = this.handleBookClick.bind(this)
+ this.handleHideModal = this.handleHideModal.bind(this)
+ this.addToWishlist = this.addToWishlist.bind(this)
+ this.getWishlistBooks = this.getWishlistBooks.bind(this)
+ this.deleteFromWishlist = this.deleteFromWishlist.bind(this)
+ this.hideBannerAfterDelay = this.hideBannerAfterDelay.bind(this)
+ }
+
+ componentDidMount() {
+ this.getTopBooks()
+ this.getWishlistBooks()
+ }
+
+ getTopBooks() {
+ axios.get('/books/top')
+ .then(({ data }) => this.setState({ books: data.results.books }))
+ .catch((err) => console.log('An error occurred: ', err))
+ }
+
+ getWishlistBooks() {
+ axios.get('/books/wishlist')
+ .then(({ data }) => this.setState({ wishlist: data }))
+ .catch((err) => console.err('An error occurred:', err))
+ }
+
+ handleBookClick(e) {
+ let rank = e.currentTarget.dataset.rank
+ let book = this.state.books[rank - 1]
+ this.setState({ showModal: true, currentBook: book })
+ }
+
+ handleHideModal() {
+ this.setState({ showModal: false })
+ }
+
+ addToWishlist() {
+ axios.post('/books/wishlist', { currentBook: this.state.currentBook })
+ .then((response) => {
+ let message = "Good choice! Successfully added to your wishlist :)"
+
+ if (response && response.data) {
+ if (response.data === 'ER_DUP_ENTRY') message = "This title is already in your wishlist!"
+ else message = errorMessage
+ }
+
+ this.setState({ showMsg: message }, () => {
+ this.hideBannerAfterDelay()
+ this.getWishlistBooks()
+ })
+ })
+ .catch((err) => console.err('An error occurred:', err))
+ }
+
+ deleteFromWishlist() {
+ let { currentBook } = this.state
+ axios.delete('/books/wishlist', { data: { currentBook, } })
+ .then((response) => {
+ let message = "Successfully deleted from your wishlist.";
+ if (response.data) message = errorMessage
+ this.setState({ showMsg: message }, () => {
+ this.hideBannerAfterDelay()
+ this.getWishlistBooks()
+ })
+ })
+ }
+
+ hideBannerAfterDelay(delay = 2000) {
+ let self = this;
+ setTimeout(() => {
+ self.setState({ showMsg: false })
+ }, delay)
+ }
+
+
+ render() {
+ let modal = this.state.showModal ?
+
+
+
+ :
+
+ let navButton = this.state.showWishlist ? "See NYT bestsellers" : "See your wishlist";
+ return (
+
+
+
Okay Reads
+ A barely okay clone of Good Reads. Click on a book to learn more about that NYT bestseller.
+
+
+
+
+ {modal}
+
+
+ )
+ }
+}
+
+ReactDOM.render(, document.getElementById('root'))
\ No newline at end of file
diff --git a/database/index.js b/database/index.js
new file mode 100644
index 0000000..09853b9
--- /dev/null
+++ b/database/index.js
@@ -0,0 +1,52 @@
+// DATABASE
+
+const mysql = require('mysql');
+
+if (process.env.NODE_ENV !== 'production') {
+ require('dotenv').config();
+}
+
+const connection = mysql.createConnection({
+ host: process.env.RDS_HOSTNAME,
+ user: process.env.RDS_USERNAME,
+ password: process.env.RDS_PASSWORD,
+ port: process.env.RDS_PORT,
+ database: process.env.RDS_DATABASE,
+});
+
+connection.connect(function (err) {
+ if (err) {
+ console.error('Database connection failed: ' + err.stack);
+ return;
+ }
+ console.log('Connected to database.');
+});
+
+// connection.end();
+
+const getWishlistBooks = (cb) => {
+ let query = `SELECT * FROM wishlist`;
+ connection.query(query, (err, results) => {
+ cb(err, results);
+ })
+}
+
+const addToWishlist = ({ title, book_image, amazon_product_url, author, rank, description, primary_isbn10 }, cb) => {
+ var query = `INSERT INTO wishlist (title, book_image, amazon_product_url, author, rank, description, primary_isbn10) VALUE (?, ?, ?, ?, ?, ?, ?);`;
+ connection.query(query, [title, book_image, amazon_product_url, author, rank, description, primary_isbn10], (err) => {
+ if(err) cb(err.code)
+ else cb(null)
+ });
+}
+
+const removeFromWishlist = ({primary_isbn10}, cb) => {
+ var query = `DELETE FROM wishlist WHERE primary_isbn10 = ?;`;
+ connection.query(query, [primary_isbn10], (err) => {
+ cb(err);
+ });
+}
+
+// exports.connection = connection;
+exports.getWishlistBooks = getWishlistBooks;
+exports.addToWishlist = addToWishlist;
+exports.removeFromWishlist = removeFromWishlist;
diff --git a/database/schema.sql b/database/schema.sql
new file mode 100644
index 0000000..15bb28e
--- /dev/null
+++ b/database/schema.sql
@@ -0,0 +1,17 @@
+CREATE DATABASE IF NOT EXISTS okayreads;
+
+USE okayreads;
+
+DROP TABLE IF EXISTS wishlist;
+
+CREATE TABLE IF NOT EXISTS wishlist (
+ id INT NOT NULL AUTO_INCREMENT,
+ title VARCHAR(50) NOT NULL UNIQUE,
+ book_image VARCHAR(100),
+ amazon_product_url VARCHAR(250),
+ author VARCHAR(50) NOT NULL,
+ rank INT NOT NULL,
+ description TINYTEXT NOT NULL,
+ primary_isbn10 VARCHAR(20) NOT NULL UNIQUE,
+ PRIMARY KEY(id)
+)
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..3c0db0c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "fullstack-developer-challenge",
+ "version": "1.0.0",
+ "description": "Bellese technologies coding take home challenge",
+ "main": "index.js",
+ "scripts": {
+ "start": "node app.js",
+ "build": "webpack --config webpack.config.js",
+ "react-dev": "webpack -d --watch",
+ "server-dev": "nodemon app.js"
+ },
+ "author": "WG",
+ "homepage": "https://github.com/weigao10/fullstack-developer-challenge",
+ "dependencies": {
+ "aws-sdk": "^2.562.0",
+ "axios": "^0.18.0",
+ "body-parser": "^1.18.2",
+ "dotenv": "^8.2.0",
+ "ejs": "^2.7.1",
+ "express": "^4.16.3",
+ "mysql": "^2.17.1",
+ "nodemon": "^1.19.4",
+ "react": "^16.3.1",
+ "react-dom": "^16.3.1"
+ },
+ "devDependencies": {
+ "babel-core": "^6.26.0",
+ "babel-loader": "^7.1.4",
+ "babel-preset-es2015": "^6.24.1",
+ "babel-preset-react": "^6.24.1",
+ "webpack": "^2.7.0",
+ "webpack-cli": "^3.3.9"
+ }
+}
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..b2942e2
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,23 @@
+const path = require('path');
+const SRC_DIR = path.join(__dirname, '/client/src');
+const DIST_DIR = path.join(__dirname, '/client/dist');
+
+module.exports = {
+ entry: `${SRC_DIR}/index.jsx`,
+ output: {
+ filename: 'bundle.js',
+ path: DIST_DIR
+ },
+ module : {
+ loaders : [
+ {
+ test : /\.jsx?/,
+ include : SRC_DIR,
+ loader : 'babel-loader',
+ query: {
+ presets: ['react', 'es2015']
+ }
+ }
+ ]
+ }
+};
\ No newline at end of file