├── .eslintrc.js
├── .gitignore
├── DOCUMENTS
└── images
│ ├── Book.jpg
│ ├── BookTyping.jpg
│ ├── Shelf.jpg
│ ├── Shelves.jpg
│ └── Signin.jpg
├── README.md
├── assets
└── img
│ ├── arrow.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ ├── icon.png
│ ├── logo.png
│ └── shelf.png
├── client
├── components
│ ├── About.js
│ ├── App.js
│ ├── Book.js
│ ├── BookNotesHeader.js
│ ├── BookRow.js
│ ├── Error.js
│ ├── Login.js
│ ├── NoteRow.js
│ ├── Search.js
│ ├── Shelf.js
│ ├── ShelfRow.js
│ └── Shelves.js
├── index.html
├── index.js
└── styles.css
├── package-lock.json
├── package.json
├── server
├── controllers
│ ├── BookController.js
│ ├── grShelfController.js
│ ├── grShelvesController.js
│ ├── oauthController.js
│ └── xmlController.js
├── models
│ └── BookNoteModel.js
├── routes
│ ├── goodreads.js
│ └── oauth.js
└── server.js
└── webpack.config.js
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['airbnb-base'],
3 | env: {
4 | browser: true,
5 | node: true,
6 | es6: true,
7 | jest: true,
8 | },
9 | rules: {
10 | 'consistent-return': 'off',
11 | 'func-names': 'off',
12 | 'no-console': 'off',
13 | curly: 'off',
14 | 'react/destructuring-assignment': 'off',
15 | 'react/jsx-filename-extension': 'off',
16 | 'react/prop-types': 'off',
17 | 'react/jsx-wrap-multilines': 'off',
18 | 'react/jsx-one-expression-per-line': 'off',
19 | 'react/jsx-closing-tag-location': 'off',
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # General
2 | .DS_Store
3 |
4 | # dependencies
5 | node_modules/
6 |
7 | # logs
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | *.log
12 |
13 | # output from webpack
14 | dist/
15 |
16 | # zip file for initial aws deployment
17 | *.zip
18 |
19 | # jest coverage
20 | coverage/
21 |
22 | .vscode/
23 |
24 | # environment variables
25 | .env
--------------------------------------------------------------------------------
/DOCUMENTS/images/Book.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rosedamasco/ReadingNotes/2b97adb77cbcc61b31743f4be7df9f962412d63c/DOCUMENTS/images/Book.jpg
--------------------------------------------------------------------------------
/DOCUMENTS/images/BookTyping.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rosedamasco/ReadingNotes/2b97adb77cbcc61b31743f4be7df9f962412d63c/DOCUMENTS/images/BookTyping.jpg
--------------------------------------------------------------------------------
/DOCUMENTS/images/Shelf.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rosedamasco/ReadingNotes/2b97adb77cbcc61b31743f4be7df9f962412d63c/DOCUMENTS/images/Shelf.jpg
--------------------------------------------------------------------------------
/DOCUMENTS/images/Shelves.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rosedamasco/ReadingNotes/2b97adb77cbcc61b31743f4be7df9f962412d63c/DOCUMENTS/images/Shelves.jpg
--------------------------------------------------------------------------------
/DOCUMENTS/images/Signin.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rosedamasco/ReadingNotes/2b97adb77cbcc61b31743f4be7df9f962412d63c/DOCUMENTS/images/Signin.jpg
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ReadingNotes
2 |
3 | A GoodReads companion application for you to jot down private notes about the books you're reading.
4 |
5 | Sign in with your GoodReads account and view all of your shelves. Choose a shelf, then choose a book, and write your personal notes. Write about how you love this author and how they describe the setting so well. Or write about how much it irritates you that -. Write all your thought because these notes are just for you. You can look back on all your books and recall if you should pick up another book by that author.
6 |
7 | # Views
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/assets/img/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rosedamasco/ReadingNotes/2b97adb77cbcc61b31743f4be7df9f962412d63c/assets/img/arrow.png
--------------------------------------------------------------------------------
/assets/img/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rosedamasco/ReadingNotes/2b97adb77cbcc61b31743f4be7df9f962412d63c/assets/img/favicon-32x32.png
--------------------------------------------------------------------------------
/assets/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rosedamasco/ReadingNotes/2b97adb77cbcc61b31743f4be7df9f962412d63c/assets/img/favicon.ico
--------------------------------------------------------------------------------
/assets/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rosedamasco/ReadingNotes/2b97adb77cbcc61b31743f4be7df9f962412d63c/assets/img/icon.png
--------------------------------------------------------------------------------
/assets/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rosedamasco/ReadingNotes/2b97adb77cbcc61b31743f4be7df9f962412d63c/assets/img/logo.png
--------------------------------------------------------------------------------
/assets/img/shelf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rosedamasco/ReadingNotes/2b97adb77cbcc61b31743f4be7df9f962412d63c/assets/img/shelf.png
--------------------------------------------------------------------------------
/client/components/About.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const About = () => {
4 | return (
5 |
6 |
About Reading Notes
7 |
8 | Hello there! Thank you for coming to Reading Notes, a GoodReads companion application to jot
9 | down private notes about the books you're reading.
10 |
11 |
12 |
13 | GoodReads does allow you to add private notes for each book on your shelf, but sometimes 512
14 | characters just isn't enough. I read so many books, so I personally like to jot down notes
15 | as I'm reading to remind myself later on if I loved a book by an author or not. This way, if
16 | I come across a fun synopsis, I can check real quick if I've enjoyed the writing style by
17 | that same author before.
18 |
19 |
20 |
21 | Sign in via your GoodReads account and see all of your shelves and books. Pick a book and
22 | start noting your thoughts. Are you loving how the author describes the room so vividly that
23 | you can actually see it? Is the main character TSTL? Note any and all things you want to
24 | remember for later.
25 |
26 |
27 |
Future features:
28 |
29 | Delete notes from a book
30 | Search for books on your bookshelf
31 | Search for authors on your bookshelf
32 | Rate the book from the book notes page
33 | Change the book's shelf
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default About;
41 |
--------------------------------------------------------------------------------
/client/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Switch, Route, Link } from 'react-router-dom';
3 | import Login from './Login';
4 | import Shelves from './Shelves';
5 | import Shelf from './Shelf';
6 | import Book from './Book';
7 | import About from './About';
8 | import Search from './Search';
9 | import Error from './Error';
10 |
11 | const App = () => {
12 | const [isSignedIn, setSignedIn] = useState(false);
13 |
14 | useEffect(() => {
15 | const hasUserIdCookie = document.cookie.includes('userid');
16 | setSignedIn(hasUserIdCookie);
17 | }, []);
18 |
19 | const signout = () => {
20 | // delete cookie by setting expire date to past date
21 | document.cookie = 'userid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
22 | setSignedIn(false);
23 | };
24 |
25 | const links = (
26 |
27 | {!isSignedIn && (
28 |
29 | Sign In
30 |
31 | )}
32 | {isSignedIn && (
33 |
34 | Shelves
35 |
36 | )}
37 |
38 | Search
39 |
40 |
41 | About
42 |
43 | {isSignedIn && (
44 |
45 |
46 | Sign Out
47 |
48 |
49 | )}
50 |
51 | );
52 |
53 | return (
54 |
55 |
59 |
70 |
71 | );
72 | };
73 |
74 | export default App;
75 |
--------------------------------------------------------------------------------
/client/components/Book.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import NoteRow from './NoteRow';
3 | import BookNotesHeader from './BookNotesHeader';
4 |
5 | const Book = () => {
6 | const id = window.location.pathname.split('/')[2];
7 | const [header, setHeaders] = useState([Loading Title... ]);
8 | const [bookNotes, setBookNotes] = useState([Loading Notes... ]);
9 |
10 | const [location, setLocation] = useState('');
11 | const [note, setNote] = useState('');
12 |
13 | const [locClass, setLocClass] = useState('');
14 | const [noteClass, setNoteClass] = useState('');
15 |
16 | useEffect(() => {
17 | fetch(`/gr/book/${id}`)
18 | .then((response) => response.json())
19 | .then(({ book, notes }) => {
20 | // add book header with image and title
21 | // and notes header (loc, note, date)
22 | setHeaders([
23 | ,
24 | ]);
25 | // add notes
26 | const tempNotes = [];
27 | notes.forEach((oldNote) => {
28 | tempNotes.push(
29 |
30 | );
31 | });
32 | setBookNotes(tempNotes);
33 | });
34 | }, []);
35 |
36 | const updateLocation = (e) => {
37 | const tempLocation = e.target.value;
38 | setLocation(tempLocation);
39 | };
40 |
41 | const updateNote = (e) => {
42 | const tempNote = e.target.value;
43 | setNote(tempNote);
44 | };
45 |
46 | const saveNote = () => {
47 | // require location and note fields
48 | if (location === '') {
49 | setLocClass('missing-input');
50 | return;
51 | }
52 | if (note === '') {
53 | setNoteClass('missing-input');
54 | setLocClass('');
55 | return;
56 | }
57 |
58 | // update input fields to be clear and no red borders
59 | setLocClass('');
60 | setNoteClass('');
61 | setLocation('');
62 | setNote('');
63 |
64 | // add input to view
65 | const today = new Date();
66 | const date = `${today.getMonth() + 1}/${today.getDate()}/${today.getFullYear()}`;
67 | const newNote = ;
68 | setBookNotes([...bookNotes, newNote]);
69 |
70 | // post new note to db
71 | fetch(`/gr/book`, {
72 | method: 'POST',
73 | headers: {
74 | 'Content-Type': 'application/json',
75 | },
76 | body: JSON.stringify({ id, location, note, date }),
77 | });
78 | };
79 |
80 | return (
81 |
82 | {header}
83 |
{bookNotes}
84 |
85 | {
92 | updateLocation(e);
93 | }}
94 | />
95 |
106 |
107 | );
108 | };
109 |
110 | export default Book;
111 |
--------------------------------------------------------------------------------
/client/components/BookNotesHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const NotesHeader = (props) => {
4 | return (
5 |
6 |
13 |
14 |
Loc
15 |
Note
16 |
Date
17 |
18 |
19 | );
20 | };
21 |
22 | export default NotesHeader;
23 |
--------------------------------------------------------------------------------
/client/components/BookRow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const BookRow = (props) => {
4 | return (
5 |
6 |
7 |
8 |
9 | {props.title}
10 |
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default BookRow;
18 |
--------------------------------------------------------------------------------
/client/components/Error.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Error = () => {
4 | return (
5 |
6 |
Uh oh... an error occured. Please try again.
7 |
8 | );
9 | };
10 |
11 | export default Error;
12 |
--------------------------------------------------------------------------------
/client/components/Login.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | const Login = () => {
4 | const [buttonClicked, setButtonClicked] = useState(false);
5 |
6 | const login = () => {
7 | setButtonClicked(true);
8 | fetch('/oauth/login')
9 | .then((response) => response.json())
10 | .then((url) => {
11 | window.location = url;
12 | });
13 | };
14 |
15 | return (
16 |
17 |
18 |
READING NOTES IS UNDER CONSTRUCTION
19 |
20 | **Goodreads API is being retired, and developer keys are no longer being issued. Steps are
21 | being taken to refactor this application with this new information. Thank you for your
22 | patience.**
23 |
24 |
25 | {!buttonClicked && (
26 |
27 |
31 |
32 | )}
33 | {buttonClicked &&
Loading... }
34 |
35 | );
36 | };
37 |
38 | export default Login;
39 |
--------------------------------------------------------------------------------
/client/components/NoteRow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const NoteRow = (props) => {
4 | return (
5 |
6 |
{props.location}
7 |
{props.note}
8 |
{props.date}
9 |
10 | );
11 | };
12 |
13 | export default NoteRow;
14 |
--------------------------------------------------------------------------------
/client/components/Search.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Search = () => {
4 | return (
5 |
6 |
Under Construction
7 |
8 | );
9 | };
10 |
11 | export default Search;
12 |
--------------------------------------------------------------------------------
/client/components/Shelf.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import BookRow from './BookRow';
3 |
4 | const Shelf = () => {
5 | const shelfName = window.location.pathname.split('/')[2];
6 | const [shelfPage, setShelfPage] = useState(1);
7 | const [bookRows, setBookRows] = useState([Loading... ]);
8 |
9 | const tempBook = [];
10 |
11 | const fetchBooks = (newShelfPage) => {
12 | fetch(`/gr/shelf/${shelfName}/${newShelfPage}`)
13 | .then((response) => response.json())
14 | .then(({ shelfBooks }) => {
15 | shelfBooks.forEach((book) => {
16 | tempBook.push(
17 |
18 | );
19 | });
20 | setBookRows(tempBook);
21 | });
22 | };
23 |
24 | useEffect(() => fetchBooks(shelfPage), []);
25 |
26 | const prevPage = () => {
27 | if (shelfPage <= 1) return;
28 | setBookRows([Loading... ]);
29 | fetchBooks(shelfPage - 1);
30 | setShelfPage(shelfPage - 1);
31 | window.scrollTo(0, 0);
32 | return;
33 | };
34 |
35 | const nextPage = () => {
36 | if (bookRows.length < 10) return;
37 | setBookRows([Loading... ]);
38 | fetchBooks(shelfPage + 1);
39 | setShelfPage(shelfPage + 1);
40 | window.scrollTo(0, 0);
41 | return;
42 | };
43 |
44 | return (
45 |
46 |
Shelf: {shelfName}
47 | {bookRows}
48 |
49 | {'<< Prev Page '}
50 | {' NextPage >>'}
51 |
52 |
53 | );
54 | };
55 |
56 | export default Shelf;
57 |
--------------------------------------------------------------------------------
/client/components/ShelfRow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const ShelfRow = (props) => {
4 | return (
5 |
6 |
7 |
8 |
9 | {props.name}
10 | ({props.bookCount})
11 |
12 | );
13 | };
14 |
15 | export default ShelfRow;
16 |
--------------------------------------------------------------------------------
/client/components/Shelves.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import ShelfRow from './ShelfRow';
3 |
4 | const Shelves = () => {
5 | const [shelfRows, setShelfRows] = useState([Loading... ]);
6 |
7 | const tempShelf = [];
8 | useEffect(() => {
9 | fetch('/gr/shelves')
10 | .then((response) => response.json())
11 | .then(({ shelves }) => {
12 | shelves.forEach((shelf) => {
13 | tempShelf.push(
14 |
20 | );
21 | });
22 | setShelfRows(tempShelf);
23 | });
24 | }, []);
25 |
26 | return {shelfRows}
;
27 | };
28 |
29 | export default Shelves;
30 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ReadingNotes
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import App from './components/App';
5 | import './styles.css';
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('app')
12 | );
13 |
--------------------------------------------------------------------------------
/client/styles.css:
--------------------------------------------------------------------------------
1 | html {
2 | background-image: url('https://images.unsplash.com/photo-1457369804613-52c61a468e7d?ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80');
3 | background-attachment: fixed;
4 | background-size: cover;
5 | background-color: gainsboro;
6 | min-width: 375px;
7 | font-size: 0.9em;
8 | }
9 |
10 | a {
11 | color: black;
12 | text-decoration: none;
13 | }
14 |
15 | /********
16 | ********
17 | HEADER: Logo and Navigation Bar
18 | ********
19 | ********/
20 | #header {
21 | position: fixed;
22 | top: 0;
23 | right: 0;
24 | width: 100%;
25 | min-width: 375px;
26 | height: 4em;
27 | display: flex;
28 | flex-direction: column;
29 | justify-content: center;
30 | align-items: center;
31 | background-color: #6a868d;
32 | padding: 0.5em 0;
33 | }
34 |
35 | nav ul {
36 | margin: 0;
37 | padding: 0;
38 | }
39 |
40 | nav li {
41 | display: inline;
42 | padding: 0 1em;
43 | }
44 |
45 | /********
46 | ********
47 | Container for all content below header
48 | ********
49 | ********/
50 | #below-header {
51 | border-radius: 5px;
52 | background-color: #bfbbb0;
53 | margin: 5.5em auto auto auto;
54 | padding: 5px 5px;
55 | max-width: 375px;
56 | }
57 |
58 | #below-header > div {
59 | text-align: center;
60 | }
61 |
62 | /********
63 | ********
64 | Container for each shelf in Shelves view and each book in Shelf view
65 | ********
66 | ********/
67 | .row {
68 | display: grid;
69 | grid-template-columns: 1fr 1fr 1fr;
70 | justify-items: center;
71 | align-items: center;
72 | }
73 |
74 | .bookImg {
75 | height: 85px;
76 | }
77 |
78 | /********
79 | ********
80 | Goodreads login logo
81 | ********
82 | ********/
83 | #goodreadsLogo {
84 | height: 20px;
85 | }
86 |
87 | /********
88 | ********
89 | Container for book header Book view
90 | ********
91 | ********/
92 | #book-header {
93 | display: flex;
94 | justify-content: center;
95 | align-items: center;
96 | margin-bottom: 6px;
97 | }
98 |
99 | /********
100 | ********
101 | Container for notes header and each note on Book view
102 | ********
103 | ********/
104 | .bold {
105 | font-weight: bold;
106 | }
107 |
108 | .note-row {
109 | display: grid;
110 | grid-template-columns: 1fr 5fr 2fr;
111 | justify-items: center;
112 | align-items: center;
113 | gap: 6px;
114 | margin-bottom: 5px;
115 | border: 1px solid #6a868d;
116 | }
117 |
118 | .wrap-note {
119 | word-break: break-word;
120 | }
121 |
122 | /********
123 | ********
124 | Container for input fields on Book view
125 | ********
126 | ********/
127 | #input-row {
128 | position: relative;
129 | bottom: 0;
130 | display: flex;
131 | justify-content: center;
132 | align-content: center;
133 | background-color: #6a868d;
134 | padding: 5px;
135 | border-radius: 5px;
136 | }
137 |
138 | #input-row > * {
139 | width: 100%;
140 | margin: 0 2px;
141 | }
142 |
143 | #loc-input {
144 | width: 40px;
145 | }
146 | #input-row > button {
147 | width: 100px;
148 | }
149 |
150 | .missing-input {
151 | border: red 2px solid;
152 | }
153 |
154 | /********
155 | ********
156 | paragraphs (only in About)
157 | ********
158 | ********/
159 | #about p,
160 | #about ul {
161 | width: 80%;
162 | margin: auto;
163 | text-align: left;
164 | }
165 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "readingnotes",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "engines": {
7 | "node": "10.22.0",
8 | "npm": "6.14.5"
9 | },
10 | "scripts": {
11 | "start": "nodemon server/server.js",
12 | "build": "webpack --env.NODE_ENV=production",
13 | "dev": "webpack-dev-server --open --env.NODE_ENV=development & nodemon server/server.js",
14 | "test": "jest"
15 | },
16 | "keywords": [],
17 | "author": "",
18 | "license": "ISC",
19 | "dependencies": {
20 | "cookie-parser": "^1.4.5",
21 | "dotenv": "^8.2.0",
22 | "express": "^4.17.1",
23 | "oauth": "^0.9.15",
24 | "pg": "^8.3.3",
25 | "react": "^16.13.1",
26 | "react-dom": "^16.13.1",
27 | "react-router-dom": "^5.2.0",
28 | "superagent": "^6.0.0",
29 | "xml2js": "^0.4.23"
30 | },
31 | "devDependencies": {
32 | "@babel/core": "^7.11.4",
33 | "@babel/preset-env": "^7.11.0",
34 | "@babel/preset-react": "^7.10.4",
35 | "babel-loader": "^8.1.0",
36 | "css-loader": "^4.2.2",
37 | "enzyme": "^3.11.0",
38 | "enzyme-adapter-react-16": "^1.15.3",
39 | "html-webpack-plugin": "^4.3.0",
40 | "jest": "^26.4.2",
41 | "style-loader": "^1.2.1",
42 | "webpack": "^4.44.1",
43 | "webpack-cli": "^3.3.12",
44 | "webpack-dev-server": "^3.11.0"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/server/controllers/BookController.js:
--------------------------------------------------------------------------------
1 | const db = require('../models/BookNoteModel');
2 |
3 | const BookController = {};
4 |
5 | BookController.getBook = (req, res, next) => {
6 | const { id } = req.params;
7 | const bookQuery = `SELECT title, author, imgURL FROM books
8 | WHERE id=$1
9 | LIMIT 1;`;
10 | db.query(bookQuery, [id])
11 | .then((results) => {
12 | const book = results.rows[0];
13 | res.locals.book = {
14 | title: book.title,
15 | author: book.author,
16 | imgURL: book.imgurl,
17 | };
18 | return next();
19 | })
20 | .catch((err) =>
21 | next({
22 | log: `Error in BookController.getBook: ${err}`,
23 | message: { err: `Error in BookController.getBook: ${err}` },
24 | })
25 | );
26 | };
27 |
28 | BookController.getNotes = (req, res, next) => {
29 | const { userid } = req.cookies;
30 | const { id } = req.params;
31 | const tableName = `user${userid}`;
32 | const notesQuery = `SELECT location, note, date FROM ${tableName}
33 | WHERE book_id=$1;`;
34 | db.query(notesQuery, [id])
35 | .then((results) => {
36 | const notes = results.rows;
37 | res.locals.notes = notes;
38 | return next();
39 | })
40 | .catch((err) =>
41 | next({
42 | log: `Error in BookController.getNotes: ${err}`,
43 | message: { err: `Error in BookController.getNotes: ${err}` },
44 | })
45 | );
46 | };
47 |
48 | BookController.addNote = (req, res, next) => {
49 | const { userid } = req.cookies;
50 | const { id, location, note, date } = req.body;
51 | const tableName = `user${userid}`;
52 | const insertNoteQuery = `INSERT INTO ${tableName}(book_id, location, note, date)
53 | VALUES($1, $2, $3, $4);`;
54 | db.query(insertNoteQuery, [id, location, note, date])
55 | .then(() => res.status(200).end())
56 | .catch((err) =>
57 | next({
58 | log: `Error in BookController.addNote: ${err}`,
59 | message: { err: `Error in BookController.addNote: ${err}` },
60 | })
61 | );
62 | };
63 |
64 | module.exports = BookController;
65 |
--------------------------------------------------------------------------------
/server/controllers/grShelfController.js:
--------------------------------------------------------------------------------
1 | const { OAUTH } = require('./oauthController');
2 | const db = require('../models/BookNoteModel');
3 |
4 | const { API_KEY } = process.env;
5 | const GR_URL = 'https://goodreads.com';
6 |
7 | const grShelfController = {};
8 |
9 | grShelfController.getShelfBooks = (req, res, next) => {
10 | const { userid, accessToken, accessSecret } = req.cookies;
11 | if (!userid || !accessToken || !accessSecret) return res.redirect('/');
12 | const { name, page } = req.params;
13 | const useridParam = `id=${userid}`;
14 | const shelfParam = `shelf=${name}`;
15 | const keyParam = `key=${API_KEY}`;
16 | const pageParam = `page=${page}`;
17 | const perPageParam = 'per_page=10';
18 | const shelfURL = `${GR_URL}/review/list?${useridParam}&${shelfParam}&${keyParam}&${pageParam}&${perPageParam}&format=xml`;
19 | OAUTH.get(shelfURL, accessToken, accessSecret, (err, xmlResponse) => {
20 | if (err) {
21 | return next({
22 | log: `Error in grShelfController.getShelfBooks: ${err}`,
23 | message: { err: `Error in grShelfController.getShelfBooks: ${err}` },
24 | });
25 | }
26 | res.locals.xmlData = xmlResponse;
27 | return next();
28 | });
29 | };
30 |
31 | grShelfController.parseShelfBooks = (req, res, next) => {
32 | const { xmlObj } = res.locals;
33 | const shelfBooksObj = xmlObj.GoodreadsResponse.books[0].book;
34 | console.log('shelfBooksObj', JSON.stringify(shelfBooksObj, null, 1));
35 | const parsedShelfBooks = [];
36 | shelfBooksObj.forEach((book) => {
37 | console.log('getting book info');
38 | const bookInfo = {
39 | id: book.id[0]._,
40 | title: book.title[0],
41 | author: book.authors[0].author[0].name[0],
42 | imgURL: book.image_url[0],
43 | };
44 | console.log(bookInfo);
45 | parsedShelfBooks.push(bookInfo);
46 | });
47 | res.locals.shelfBooks = parsedShelfBooks;
48 | return next();
49 | };
50 |
51 | grShelfController.addBooksToDB = (req, res, next) => {
52 | const { shelfBooks } = res.locals;
53 | let bookColumnValues = '';
54 | shelfBooks.forEach((book, index) => {
55 | const simpleBookTitle = book.title.replace(/'/g, "''");
56 | const simpleAuthor = book.author.replace(/'/g, "''");
57 | bookColumnValues += `('${book.id}', '${simpleBookTitle}', '${simpleAuthor}', '${book.imgURL}')`;
58 | if (index < shelfBooks.length - 1) bookColumnValues += ',';
59 | });
60 | console.log(bookColumnValues);
61 | const bookInsertQuery = `INSERT INTO books(id, title, author, imgurl)
62 | VALUES ${bookColumnValues}
63 | ON CONFLICT (id)
64 | DO NOTHING;`;
65 | db.query(bookInsertQuery)
66 | .then(() => next())
67 | .catch((err) =>
68 | next({
69 | log: `Error in grShelfController.addBooksToDB: ${err}`,
70 | message: { err: `grShelfController.addBooksToDB: ${err}` },
71 | })
72 | );
73 | };
74 |
75 | module.exports = grShelfController;
76 |
--------------------------------------------------------------------------------
/server/controllers/grShelvesController.js:
--------------------------------------------------------------------------------
1 | const superagent = require('superagent');
2 |
3 | const { API_KEY } = process.env;
4 |
5 | const grShelvesController = {};
6 |
7 | grShelvesController.getShelves = (req, res, next) => {
8 | const { userid } = req.cookies;
9 | if (!userid) return res.redirect('/');
10 | superagent
11 | .get('https://www.goodreads.com/shelf/list.xml')
12 | .query({ user_id: userid })
13 | .query({ key: API_KEY })
14 | .buffer()
15 | .type('xml')
16 | .end((err, xmlResponse) => {
17 | if (err) {
18 | return next({
19 | log: `Error in grShelvesController.getShelves: ${err}`,
20 | message: { err: `Error in grShelvesController.getShelves: ${err}` },
21 | });
22 | }
23 | res.locals.xmlData = xmlResponse.text;
24 | return next();
25 | });
26 | };
27 |
28 | grShelvesController.parseShelves = (req, res, next) => {
29 | const { xmlObj } = res.locals;
30 | const shelvesObj = xmlObj.GoodreadsResponse.shelves[0].user_shelf;
31 | const parsedShelves = [];
32 | shelvesObj.forEach((shelf) => {
33 | const shelfInfo = {
34 | id: shelf.id[0]._,
35 | name: shelf.name[0],
36 | bookCount: shelf.book_count[0]._,
37 | };
38 | parsedShelves.push(shelfInfo);
39 | });
40 | res.locals.shelves = parsedShelves;
41 | return next();
42 | };
43 |
44 | module.exports = grShelvesController;
45 |
--------------------------------------------------------------------------------
/server/controllers/oauthController.js:
--------------------------------------------------------------------------------
1 | const { OAuth } = require('oauth');
2 | const db = require('../models/BookNoteModel');
3 |
4 | const { API_KEY, API_SECRET, CALLBACK_URL } = process.env;
5 | const GR_URL = 'https://goodreads.com';
6 |
7 | const oauthController = {};
8 |
9 | const requestURL = `${GR_URL}/oauth/request_token`;
10 | const accessURL = `${GR_URL}/oauth/access_token`;
11 | const version = '1.0';
12 | const encryption = 'HMAC-SHA1';
13 | const OAUTH = new OAuth(
14 | requestURL,
15 | accessURL,
16 | API_KEY,
17 | API_SECRET,
18 | version,
19 | CALLBACK_URL,
20 | encryption
21 | );
22 | const REQUEST_TOKEN = {};
23 | const ACCESS_TOKEN = {};
24 |
25 | oauthController.getRequestToken = (req, res, next) => {
26 | OAUTH.getOAuthRequestToken((err, reqToken, reqTokenSecret, results) => {
27 | if (err) {
28 | return next({
29 | log: `Error in oauthController.getRequestToken: ${err}`,
30 | message: { err: `Error in oauthController.getRequestToken: ${err}` },
31 | });
32 | }
33 |
34 | const url = `${GR_URL}/oauth/authorize?oauth_token=${reqToken}&oauth_callback=${CALLBACK_URL}`;
35 | REQUEST_TOKEN.token = reqToken;
36 | REQUEST_TOKEN.secret = reqTokenSecret;
37 |
38 | return res.json(url);
39 | });
40 | };
41 |
42 | oauthController.getAccessToken = (req, res, next) => {
43 | if (req.query.authorize === '0') return res.redirect('/');
44 |
45 | OAUTH.getOAuthAccessToken(
46 | REQUEST_TOKEN.token,
47 | REQUEST_TOKEN.secret,
48 | 1,
49 | (err, accessToken, accessTokenSecret, results) => {
50 | if (err) {
51 | return next({
52 | log: `Error in oauthController.getAccessToken: ${err}`,
53 | message: { err: `Error in oauthController.getAccessToken: ${err}` },
54 | });
55 | }
56 | ACCESS_TOKEN.token = accessToken;
57 | ACCESS_TOKEN.secret = accessTokenSecret;
58 | return next();
59 | }
60 | );
61 | };
62 |
63 | oauthController.getUserInfo = (req, res, next) => {
64 | OAUTH.get(
65 | `${GR_URL}/api/auth_user`,
66 | ACCESS_TOKEN.token,
67 | ACCESS_TOKEN.secret,
68 | (err, data, response) => {
69 | if (err) {
70 | return next({
71 | log: `Error in oauthController.getUserInfo: ${err}`,
72 | message: { err: `Error in oauthController.getUserInfo: ${err}` },
73 | });
74 | }
75 | res.locals.xmlData = data;
76 | return next();
77 | }
78 | );
79 | };
80 |
81 | oauthController.parseUserInfo = (req, res, next) => {
82 | const userObj = res.locals.xmlObj;
83 | const user = {
84 | id: userObj.GoodreadsResponse.user[0].$.id,
85 | name: userObj.GoodreadsResponse.user[0].name[0],
86 | };
87 | res.locals.user = user;
88 | return next();
89 | };
90 |
91 | oauthController.setUserCookies = (req, res, next) => {
92 | res.cookie('userid', res.locals.user.id, { maxAge: 86400000 });
93 | res.cookie('username', res.locals.user.name, { httpOnly: true, maxAge: 86400000 });
94 | res.cookie('accessToken', ACCESS_TOKEN.token, { httpOnly: true, maxAge: 86400000 });
95 | res.cookie('accessSecret', ACCESS_TOKEN.secret, { httpOnly: true, maxAge: 86400000 });
96 | return next();
97 | };
98 |
99 | oauthController.addUserToDB = (req, res, next) => {
100 | const tableName = `user${res.locals.user.id}`;
101 | const userTableExistQuery = `SELECT EXISTS (
102 | SELECT FROM pg_tables
103 | WHERE tablename = $1
104 | );`;
105 | db.query(userTableExistQuery, [tableName])
106 | .then((results) => Promise.resolve(results.rows[0].exists))
107 | .then((tableExists) => {
108 | // if user table exists, move onto next step
109 | if (tableExists) {
110 | return next();
111 | }
112 | // if table does not exist, create table for user
113 | const createUserTableQuery = `CREATE TABLE ${tableName} (
114 | id SERIAL PRIMARY KEY,
115 | book_id VARCHAR,
116 | location INT,
117 | note VARCHAR,
118 | date VARCHAR,
119 | FOREIGN KEY (book_id) REFERENCES books(id)
120 | );`;
121 | db.query(createUserTableQuery).then(() => next());
122 | })
123 | .catch((err) =>
124 | next({
125 | log: `Error in oauthController.addUserToDB: ${err}`,
126 | message: { err: `Error in oauthController.addUserToDB: ${err}` },
127 | })
128 | );
129 | };
130 |
131 | module.exports = { oauthController, OAUTH };
132 |
--------------------------------------------------------------------------------
/server/controllers/xmlController.js:
--------------------------------------------------------------------------------
1 | const { parseString } = require('xml2js');
2 |
3 | const xmlController = {};
4 |
5 | xmlController.parseXML = (req, res, next) => {
6 | parseString(res.locals.xmlData, (err, object) => {
7 | if (err) {
8 | return next({
9 | log: `Error in xmlController.parseXML, parseString: ${err}`,
10 | message: { err: `Error in xmlController.parseXML, parseString: ${err}` },
11 | });
12 | }
13 | res.locals.xmlObj = object;
14 | return next();
15 | });
16 | };
17 |
18 | module.exports = xmlController;
19 |
--------------------------------------------------------------------------------
/server/models/BookNoteModel.js:
--------------------------------------------------------------------------------
1 | const { Pool } = require('pg');
2 |
3 | // On ElephantSQL created table to store book info:
4 | // CREATE TABLE books (
5 | // id VARCHAR PRIMARY KEY,
6 | // title VARCHAR,
7 | // author VARCHAR,
8 | // imgurl VARCHAR
9 | // );
10 |
11 | const URI = process.env.PG_URI;
12 |
13 | const pool = new Pool({
14 | connectionString: URI,
15 | });
16 |
17 | module.exports = {
18 | query: (text, params, callback) => {
19 | console.log('executed query: ', text);
20 | return pool.query(text, params, callback);
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/server/routes/goodreads.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const xmlController = require('../controllers/xmlController');
3 | const grShelvesController = require('../controllers/grShelvesController');
4 | const grShelfController = require('../controllers/grShelfController');
5 | const BookController = require('../controllers/BookController');
6 |
7 | const router = express.Router();
8 |
9 | router.get(
10 | '/shelves',
11 | grShelvesController.getShelves,
12 | xmlController.parseXML,
13 | grShelvesController.parseShelves,
14 | (req, res) => res.status(200).json({ shelves: [...res.locals.shelves] })
15 | );
16 |
17 | router.get(
18 | '/shelf/:name/:page',
19 | grShelfController.getShelfBooks,
20 | xmlController.parseXML,
21 | grShelfController.parseShelfBooks,
22 | grShelfController.addBooksToDB,
23 | (req, res) => res.status(200).json({ shelfBooks: [...res.locals.shelfBooks] })
24 | );
25 |
26 | router.get('/book/:id', BookController.getBook, BookController.getNotes, (req, res) => {
27 | res.status(200).json({ book: { ...res.locals.book }, notes: [...res.locals.notes] });
28 | });
29 |
30 | router.post('/book', BookController.addNote);
31 |
32 | module.exports = router;
33 |
--------------------------------------------------------------------------------
/server/routes/oauth.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const { oauthController } = require('../controllers/oauthController');
3 | const xmlController = require('../controllers/xmlController');
4 |
5 | const router = express.Router();
6 |
7 | // OAuth login with goodreads
8 | router.get('/login', oauthController.getRequestToken);
9 | router.get(
10 | '/callback',
11 | oauthController.getAccessToken,
12 | oauthController.getUserInfo,
13 | xmlController.parseXML,
14 | oauthController.parseUserInfo,
15 | oauthController.setUserCookies,
16 | oauthController.addUserToDB,
17 | (req, res) => res.redirect('/shelves')
18 | );
19 |
20 | module.exports = router;
21 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const express = require('express');
3 | const cookieParser = require('cookie-parser');
4 | require('dotenv').config();
5 |
6 | // initialize server and port
7 | const app = express();
8 | const PORT = 3434;
9 |
10 | // require routers
11 | const oauthRouter = require('./routes/oauth');
12 | const goodreadsRouter = require('./routes/goodreads');
13 |
14 | // handle parsing request body
15 | app.use(express.json()); // for parsing application/json
16 | app.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
17 | app.use(cookieParser());
18 |
19 | // FLOW TEST
20 | app.use((req, res, next) => {
21 | console.log(`
22 | ********* FLOW TEST *********\n
23 | METHOD: ${req.method}\n
24 | URL: ${req.url}\n
25 | BODY: ${JSON.stringify(req.body)}\n`);
26 | return next();
27 | });
28 |
29 | // define route handlers
30 | app.use('/oauth/', oauthRouter);
31 | app.use('/gr/', goodreadsRouter);
32 |
33 | // handle requests for static files
34 | app.use('/assets', express.static(path.join(__dirname, '../assets')));
35 |
36 | // route handler to respond with main app
37 | app.use('/bundle.js', express.static(path.join(__dirname, '../dist/bundle.js')));
38 | app.get('/*', (req, res) => {
39 | res.sendFile(path.join(__dirname, '../dist', 'index.html'));
40 | });
41 |
42 | // catch-all route handler for any requests to an unknown route
43 | app.use((req, res) => res.status(404).send('Wake up Neo... Knock, knock.'));
44 |
45 | /**
46 | * configire express global error handler
47 | * @see https://expressjs.com/en/guide/error-handling.html#writing-error-handlers
48 | */
49 | app.use((err, req, res, next) => {
50 | const defaultErr = {
51 | log: 'Express error handler caught unknown middleware error',
52 | status: 400,
53 | message: { err: 'An error occurred' },
54 | };
55 | const errorObj = Object.assign(defaultErr, err);
56 | console.log(errorObj.log);
57 | res.status(errorObj.status).json(errorObj.message);
58 | });
59 |
60 | app.listen(PORT, () => {
61 | console.log(`Listening server on ${PORT}`);
62 | });
63 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = (env) => {
5 | return {
6 | entry: [
7 | // entry point of our app
8 | './client/index.js',
9 | ],
10 | output: {
11 | path: path.resolve(__dirname, 'dist'),
12 | publicPath: '/',
13 | filename: 'bundle.js',
14 | },
15 | mode: env.NODE_ENV,
16 | devServer: {
17 | host: 'localhost',
18 | port: 8080,
19 | // match the output path
20 | contentBase: path.resolve(__dirname, 'dist'),
21 | // enable HMR on the devServer
22 | hot: true,
23 | // match the output 'publicPath'
24 | publicPath: '/',
25 | // fallback to root for other urls
26 | historyApiFallback: true,
27 |
28 | inline: true,
29 |
30 | headers: { 'Access-Control-Allow-Origin': '*' },
31 | /**
32 | * proxy is required in order to make api calls to
33 | * express server while using hot-reload webpack server
34 | * routes api fetch requests from localhost:8080/* (webpack dev server)
35 | * to localhost:3434/* (where our Express server is running)
36 | */
37 | proxy: {
38 | '/': {
39 | target: 'http://localhost:3434/',
40 | secure: false,
41 | },
42 | },
43 | },
44 | module: {
45 | rules: [
46 | {
47 | test: /\.jsx?/,
48 | exclude: /node_modules/,
49 | use: {
50 | loader: 'babel-loader',
51 | options: {
52 | presets: ['@babel/preset-env', '@babel/preset-react'],
53 | },
54 | },
55 | },
56 | {
57 | test: /\.s?css/,
58 | exclude: /node_modules/,
59 | use: ['style-loader', 'css-loader'],
60 | },
61 | ],
62 | },
63 | plugins: [
64 | new HtmlWebpackPlugin({
65 | template: './client/index.html',
66 | }),
67 | ],
68 | resolve: {
69 | // Enable importing JS / JSX files without specifying their extension
70 | extensions: ['.js', '.jsx'],
71 | },
72 | };
73 | };
74 |
--------------------------------------------------------------------------------