├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SEARCH_TERMS.md ├── package.json ├── public ├── favicon.ico └── index.html └── src ├── AlertDialog.js ├── App.css ├── App.js ├── App.test.js ├── Authentication.js ├── Book.js ├── BookInfo.js ├── BookRating.js ├── BookUtils.js ├── BooksAPI.js ├── Bookshelf.js ├── ConfirmDialog.js ├── Crypto.js ├── EntropyInput.js ├── Loader.js ├── Login.js ├── PrivateRoute.js ├── QRCodeBox.js ├── Register.js ├── Search.js ├── Share.js ├── UnauthenticatedRoute.js ├── icons ├── add.svg ├── arrow-back.svg ├── arrow-drop-down.svg ├── loaders │ ├── circle.svg │ ├── clock.svg │ └── dots.svg ├── myreads.jpg └── shelves │ ├── currently-reading.svg │ ├── none.svg │ ├── read.svg │ └── want-to-read.svg ├── index.css └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | .DS_Store 4 | .idea/ 5 | build/ 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | The files in this repository are used in the course videos and are the starting point for all students. Because we want all students to have the same experience going through course, if your pull request alters any of the core files, then it (most likely) will _not_ be merged into the project. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Vin Busquet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MyReads Project 2 | 3 | This repository contains my implementation of the MyReads app. This is the final assessment project for the 4 | Udacity's React Fundamentals course, part of the [React Nanodegree Program](https://udacity.com/course/react-nanodegree--nd019). 5 | MyReads is a bookshelf app that allows the user to select and categorize books they have read, are currently reading, or 6 | want to read. 7 | 8 | ![](https://raw.githubusercontent.com/computationalcore/myreads/gh-pages/myreads.gif) 9 | 10 | ## Demo 11 | 12 | [computationalcore.github.io/myreads](https://computationalcore.github.io/myreads) 13 | 14 | ## Getting Started 15 | 16 | These instructions will get you a copy of the project up and running on your local machine for development and testing 17 | purposes. 18 | 19 | ### Prerequisites 20 | 21 | The project can be built with npm or yarn, so choose one of the approach bellow in case you don't 22 | have any installed on your system. 23 | 24 | * npm is distributed with Node.js which means that when you download Node.js, 25 | you automatically get npm installed on your computer. [Download Node.js](https://github.com/facebookincubator/create-react-app) 26 | 27 | or 28 | 29 | * Yarn is a package manager built by Facebook Team and seems to be faster than npm in general. [Download Yarn](https://yarnpkg.com/en/docs/install) 30 | 31 | ### Installing 32 | 33 | To download the project follow the instructions bellow 34 | 35 | ``` 36 | git clone https://github.com/computationalcore/myreads 37 | cd myreads 38 | ``` 39 | 40 | Install dependencies and run with: 41 | 42 | npm 43 | ``` 44 | npm install 45 | npm start 46 | ``` 47 | or 48 | 49 | yarn 50 | ``` 51 | yarn install 52 | yarn start 53 | ``` 54 | 55 | ## Backend Server 56 | 57 | To simplify development process, Udacity provides a backend server for you to develop against. 58 | The provided file [`BooksAPI.js`](src/BooksAPI.js) contains the methods you will need to perform necessary operations 59 | on the backend: 60 | 61 | * [`getAll`](#getall) 62 | * [`update`](#update) 63 | * [`search`](#search) 64 | 65 | ### `getAll` 66 | 67 | Method Signature: 68 | 69 | ```js 70 | getAll() 71 | ``` 72 | 73 | * Returns a Promise which resolves to a JSON object containing a collection of book objects. 74 | * This collection represents the books currently in the bookshelves in your app. 75 | 76 | ### `update` 77 | 78 | Method Signature: 79 | 80 | ```js 81 | update(book, shelf) 82 | ``` 83 | 84 | * book: `` containing at minimum an `id` attribute 85 | * shelf: `` contains one of ["wantToRead", "currentlyReading", "read"] 86 | * Returns a Promise which resolves to a JSON object containing the response data of the POST request 87 | 88 | ### `search` 89 | 90 | Method Signature: 91 | 92 | ```js 93 | search(query, maxResults) 94 | ``` 95 | 96 | * query: `` 97 | * maxResults: `` Due to the nature of the backend server, search results are capped at 20, even if this is set higher. 98 | * Returns a Promise which resolves to a JSON object containing a collection of book objects. 99 | * These books do not know which shelf they are on. They are raw results only. You'll need to make sure that books have the correct state while on the search page. 100 | 101 | ### Important 102 | The backend API uses a fixed set of cached search results and is limited to a particular set of search terms, which can 103 | be found in [SEARCH_TERMS.md](SEARCH_TERMS.md). That list of terms are the _only_ terms that will work with the backend, 104 | so don't be surprised if your searches for Basket Weaving or Bubble Wrap don't come back with any results. 105 | 106 | ## To the Infinity and beyond 107 | 108 | All the features added beyond the basic project specs were developed with the intention to extract as much as I 109 | could from the lectures so far and as much as I could from the API data, without the need to extend any other server-side 110 | functionality. For example, bookmark books functionality would require to implement a backend API call to save this data 111 | for the account/token, so I considered it out of the scope, focusing only on react related features and its interactions 112 | with the provided API. Also note that the authentication architecture is based on similar concepts used in 113 | bitcoin paper wallets, using a bitcoin address as the token the server side expect on the requests, and the private key 114 | that generates this address as the only credential needed to login into the system. 115 | 116 | ![](https://raw.githubusercontent.com/computationalcore/myreads/gh-pages/myreads_authentication.gif) 117 | 118 | ## Versions 119 | 120 | v1.0 121 | * Default project implementation 122 | 123 | v1.1 124 | * Change to material UI based interface 125 | * Book transitions animations 126 | * Shelf category ribbon on each book at search page 127 | * Bulk move books 128 | * Clear shelf capabilities 129 | 130 | v1.2 131 | * Connection error handlers 132 | * Star ratings on each book 133 | * Individual book info page 134 | 135 | v1.3 136 | * Authenticated account support 137 | * Book sharing functionality 138 | * Book search functionality for each bookshelf 139 | 140 | ## Authors 141 | Vin Busquet 142 | * [https://github.com/computationalcore](https://github.com/computationalcore) 143 | 144 | ## License 145 | 146 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 147 | 148 | 149 | ## Acknowledgments 150 | * [Udacity](https://www.udacity.com/) 151 | * [Tyler McGinnis](https://twitter.com/tylermcginnis33) 152 | * [Ryan Florence](https://twitter.com/ryanflorence) 153 | * [Michael Jackson](https://twitter.com/mjackson) 154 | * [Alfredo Hernandez](https://alfredocreates.com) 155 | -------------------------------------------------------------------------------- /SEARCH_TERMS.md: -------------------------------------------------------------------------------- 1 | 'Android', 'Art', 'Artificial Intelligence', 'Astronomy', 'Austen', 'Baseball', 'Basketball', 'Bhagat', 'Biography', 'Brief', 'Business', 'Camus', 'Cervantes', 'Christie', 'Classics', 'Comics', 'Cook', 'Cricket', 'Cycling', 'Desai', 'Design', 'Development', 'Digital Marketing', 'Drama', 'Drawing', 'Dumas', 'Education', 'Everything', 'Fantasy', 'Film', 'Finance', 'First', 'Fitness', 'Football', 'Future', 'Games', 'Gandhi', 'Homer', 'Horror', 'Hugo', 'Ibsen', 'Journey', 'Kafka', 'King', 'Lahiri', 'Larsson', 'Learn', 'Literary Fiction', 'Make', 'Manage', 'Marquez', 'Money', 'Mystery', 'Negotiate', 'Painting', 'Philosophy', 'Photography', 'Poetry', 'Production', 'Programming', 'React', 'Redux', 'River', 'Robotics', 'Rowling', 'Satire', 'Science Fiction', 'Shakespeare', 'Singh', 'Swimming', 'Tale', 'Thrun', 'Time', 'Tolstoy', 'Travel', 'Ultimate', 'Virtual Reality', 'Web Development', 'iOS' 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MyReads", 3 | "version": "1.0.0", 4 | "description": "A bookshelf app that allows the user to select and categorize books they have read, are currently reading, or want to read", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/computationalcore/myreads" 8 | }, 9 | "keywords": [ 10 | "myreads", 11 | "bookshelf", 12 | "books", 13 | "udacity", 14 | "google books" 15 | ], 16 | "author": "Vin Busquet", 17 | "license": "SEE LICENSE IN LICENSE.txt", 18 | "bugs": { 19 | "url": "https://github.com/computationalcore/myreads" 20 | }, 21 | "homepage": "https://github.com/computationalcore/myreads#readme", 22 | "dependencies": { 23 | "bigi": "^1.4.2", 24 | "bs58check": "^2.1.0", 25 | "create-hash": "^1.1.3", 26 | "ecurve": "^1.0.5", 27 | "material-ui": "^0.19.4", 28 | "prop-types": "^15.5.8", 29 | "qrcode.react": "^0.7.2", 30 | "react": "^15.5.4", 31 | "react-copy-to-clipboard": "^5.0.1", 32 | "react-cursor-position": "^2.2.2", 33 | "react-dom": "^15.5.4", 34 | "react-loader-advanced": "^1.7.1", 35 | "react-qr-reader": "^2.0.0", 36 | "react-router-dom": "^4.2.2", 37 | "react-scroll-to-component": "^1.0.1", 38 | "react-share": "^1.17.0", 39 | "react-star-rating-component": "^1.3.0", 40 | "react-swipeable-views": "^0.12.10", 41 | "react-transition-group": "^1.2.1", 42 | "safe-buffer": "^5.1.1", 43 | "sort-by": "^1.2.0", 44 | "throttle-debounce": "^1.0.1", 45 | "wif": "^2.0.6", 46 | "window-resize-listener-react": "^1.0.3" 47 | }, 48 | "devDependencies": { 49 | "react-scripts": "0.9.5" 50 | }, 51 | "scripts": { 52 | "start": "react-scripts start", 53 | "build": "react-scripts build", 54 | "test": "react-scripts test --env=jsdom", 55 | "eject": "react-scripts eject" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computationalcore/myreads/8cafefed17b2b5b8b4804ed1f6ccf8199ad4f539/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 19 | MyReads 20 | 21 | 22 |
23 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/AlertDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Dialog from 'material-ui/Dialog'; 4 | import FlatButton from 'material-ui/FlatButton'; 5 | import './App.css'; 6 | 7 | /** 8 | * This object is used for type checking the props of the component. 9 | */ 10 | const propTypes = { 11 | message: PropTypes.string.isRequired, 12 | open: PropTypes.bool.isRequired, 13 | onClick: PropTypes.func, 14 | }; 15 | 16 | /** 17 | * This object sets default values to the optional props. 18 | */ 19 | const defaultProps = { 20 | onClick: () => { 21 | }, 22 | }; 23 | 24 | /** 25 | * This callback type is called `clickCallback` and is displayed as a global symbol. 26 | * 27 | * @callback clickCallback 28 | */ 29 | 30 | /** 31 | * @description Represents a material UI based Alert Dialog with an OK button. 32 | * @constructor 33 | * @param {Object} props - The props that were defined by the caller of this component. 34 | * @param {string} props.message - The message to be displayed by the dialog. 35 | * @param {boolean} props.open - The dialog visibility. 36 | * @param {clickCallback} [props.function] - The callback to be executed when user clicks the OK button. 37 | */ 38 | function AlertDialog(props) { 39 | return ( 40 |
41 | 47 | } 48 | modal={true} 49 | open={props.open} 50 | > 51 | {props.message} 52 | 53 |
54 | ); 55 | } 56 | 57 | // Type checking the props of the component 58 | AlertDialog.propTypes = propTypes; 59 | // Assign default values to the optional props 60 | AlertDialog.defaultProps = defaultProps; 61 | 62 | export default AlertDialog; -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | html, body, .root { 2 | height: 100%; 3 | } 4 | body { 5 | line-height: 1.5; 6 | } 7 | body, .app { 8 | background: white; 9 | } 10 | 11 | /* general app */ 12 | 13 | .app-bar { 14 | position: fixed; 15 | width: 100%; 16 | top: 0; 17 | left: 0; 18 | z-index: 10; 19 | display: flex; 20 | box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 0 6px rgba(0,0,0,0.23); 21 | } 22 | 23 | .app-bar-title { 24 | text-align: center; 25 | font-size: 28px; 26 | } 27 | 28 | .app-bar-icon { 29 | padding-right: 54px; 30 | } 31 | 32 | .app-menu-shelf-icon { 33 | width: 18px; 34 | margin-right: 10px; 35 | vertical-align: middle; 36 | } 37 | 38 | .app-content { 39 | padding-top: 64px; 40 | } 41 | 42 | /* Entropy Component */ 43 | 44 | .entropy-text { 45 | font-family: 'Dancing Script', Georgia, Times, serif; 46 | font-size: 50px; 47 | line-height: 60px; 48 | position: absolute; 49 | margin-left:-100px; 50 | margin-top:60px; 51 | top: 50%; 52 | left: 50%; 53 | transform: translateY(-50%); 54 | } 55 | 56 | .entropy-paper { 57 | max-height: 400px; 58 | height: 90%; 59 | width: 90%; 60 | margin: 0 auto; 61 | text-align: center; 62 | } 63 | 64 | .entropy-swipe { 65 | height: 400px; 66 | width: 100%; 67 | } 68 | 69 | .entropy-progress-bar { 70 | background-color: #4CAF50!important; 71 | height: 400px; 72 | } 73 | 74 | /* authentication */ 75 | 76 | .app-authentication { 77 | text-align: center; 78 | margin-top: 80px; 79 | } 80 | 81 | .app-authentication-internal { 82 | width: 90%; 83 | margin: 0 auto; 84 | } 85 | .important-note { 86 | font-size: small; 87 | font-weight: 600; 88 | color: red; 89 | } 90 | 91 | /* register */ 92 | 93 | .account-backup { 94 | border-radius: 25px; 95 | border: 1px solid; 96 | padding: 10px; 97 | width: fit-content; 98 | margin: 0 auto; 99 | } 100 | 101 | .account-address { 102 | color: #323266; 103 | margin: 0; 104 | font-size: calc(0.8vmax + 1vmin); 105 | } 106 | 107 | .app-register-text { 108 | font-size: small; 109 | text-align: center; 110 | } 111 | .account-note { 112 | font-weight: 600; 113 | color: red; 114 | } 115 | 116 | /* login */ 117 | .qr-reader { 118 | max-width: 500px; 119 | width: 75%; 120 | height: auto; 121 | text-align: center; 122 | margin: 0 auto; 123 | } 124 | 125 | .login-header { 126 | font-size: small; 127 | } 128 | 129 | /* main page */ 130 | 131 | .list-books-title { 132 | padding: 10px 0; 133 | background: #2e7c31; 134 | text-align: center; 135 | } 136 | .list-books-title h1 { 137 | font-weight: 400; 138 | margin: 0; 139 | color: white; 140 | } 141 | 142 | .list-books-content { 143 | margin-top: 90px; 144 | padding: 0 0 80px; 145 | flex: 1; 146 | } 147 | 148 | .bookshelf { 149 | padding: 0 10px 20px; 150 | } 151 | 152 | @media (min-width: 600px) { 153 | .bookshelf { 154 | padding: 0 20px 40px; 155 | } 156 | } 157 | 158 | .bookshelf-title { 159 | border-bottom: 1px solid #dedede; 160 | } 161 | .bookshelf-books { 162 | text-align: center; 163 | } 164 | 165 | .open-search { 166 | position: fixed; 167 | right: 25px; 168 | bottom: 25px; 169 | } 170 | .open-search a { 171 | display: block; 172 | width: 50px; 173 | height: 50px; 174 | border-radius: 50%; 175 | /*background: #2e7d32; 176 | background-image: url('./icons/add.svg');*/ 177 | background-repeat: no-repeat; 178 | background-position: center; 179 | background-size: 28px; 180 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 181 | font-size: 0; 182 | } 183 | 184 | .shelf-bar-icon { 185 | width: 25px; 186 | margin-right: 10px; 187 | padding-top: 5px; 188 | } 189 | 190 | /* search page */ 191 | 192 | .search-books-bar { 193 | position: fixed; 194 | width: 100%; 195 | left: 0; 196 | z-index: 5; 197 | display: flex; 198 | box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 0 6px rgba(0,0,0,0.23); 199 | } 200 | .search-books-input-wrapper { 201 | flex: 1; 202 | background: #e9e; 203 | } 204 | .search-books-bar input { 205 | width: 100%; 206 | padding: 15px 10px; 207 | font-size: 1.25em; 208 | border: none; 209 | outline: none; 210 | text-align: center; 211 | } 212 | 213 | .search-books-results { 214 | padding: 20px 10px 20px; 215 | } 216 | 217 | .search-books-results-internal { 218 | margin-top: 60px; 219 | } 220 | 221 | .search-note { 222 | text-align: center; 223 | } 224 | 225 | /* books grid */ 226 | 227 | .shelf-book-list{ 228 | display: block; 229 | list-style-type: decimal; 230 | -webkit-margin-before: 1em; 231 | -webkit-margin-after: 1em; 232 | -webkit-margin-start: 0; 233 | -webkit-margin-end: 0; 234 | -webkit-padding-start: 0; 235 | } 236 | 237 | .books-grid { 238 | list-style-type: none; 239 | padding: 0; 240 | margin: 0; 241 | 242 | display: flex; 243 | justify-content: center; 244 | flex-wrap: wrap; 245 | } 246 | .books-grid li { 247 | padding: 10px 15px; 248 | text-align: left; 249 | } 250 | 251 | .book { 252 | width: 140px; 253 | } 254 | .book-title, 255 | .book-authors { 256 | font-size: 0.8em; 257 | } 258 | .book-title { 259 | margin-top: 10px; 260 | } 261 | .book-authors { 262 | color: #999; 263 | } 264 | 265 | .book-top { 266 | position: relative; 267 | height: 200px; 268 | display: flex; 269 | align-items: flex-end; 270 | } 271 | 272 | .book-shelf-changer { 273 | position: absolute; 274 | right: 0; 275 | bottom: -10px; 276 | width: 40px; 277 | height: 40px; 278 | border-radius: 50%; 279 | background: #60ac5d; 280 | background-image: url('./icons/arrow-drop-down.svg'); 281 | background-repeat: no-repeat; 282 | background-position: center; 283 | background-size: 20px; 284 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 285 | } 286 | .book-shelf-changer select { 287 | width: 100%; 288 | height: 100%; 289 | opacity: 0; 290 | cursor: pointer; 291 | } 292 | 293 | .book-shelf-changer { 294 | position: absolute; 295 | right: 0; 296 | bottom: -10px; 297 | width: 40px; 298 | height: 40px; 299 | border-radius: 50%; 300 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 301 | } 302 | 303 | .book-select-box { 304 | position: absolute; 305 | top: 5px; 306 | left: 0; 307 | } 308 | 309 | .book-rating-container { 310 | margin-top: 20px; 311 | } 312 | 313 | .book-rating { 314 | list-style-type: none; 315 | padding: 0; 316 | margin: 0; 317 | 318 | display: flex; 319 | justify-content: center; 320 | flex-wrap: wrap; 321 | } 322 | .book-rating-star { 323 | margin-right: 5px; 324 | } 325 | .book-rating-counter { 326 | font-size: 12px; 327 | /*display: inline-block;*/ 328 | line-height: normal; 329 | padding-top: 5px; 330 | /*color: #D46D00;*/ 331 | } 332 | 333 | .app-book-menu-shelf-icon { 334 | width: 18px; 335 | margin-right: 10px; 336 | vertical-align: middle; 337 | } 338 | 339 | .app-book-menu-remove-icon { 340 | width: 25px; 341 | margin-right: 7px; 342 | margin-left: -3px; 343 | vertical-align: middle; 344 | } 345 | 346 | /* book cover */ 347 | 348 | .book-cover { 349 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 350 | background: #eee; 351 | } 352 | .book-cover-title { 353 | padding: 20px 10px 0; 354 | text-align: center; 355 | font-size: 0.8em; 356 | } 357 | 358 | html, body { 359 | height: 100%; 360 | width: 100%; 361 | margin: 0; 362 | } 363 | 364 | .book-cover-title li { 365 | float: left; 366 | box-sizing: border-box; 367 | } 368 | 369 | /* book corner ribbon */ 370 | 371 | .book-checkbox { 372 | margin-bottom: 16px; 373 | background-color: white; 374 | } 375 | 376 | .ribbon { 377 | width: 130px; 378 | height: 130px; 379 | overflow: hidden; 380 | position: absolute; 381 | } 382 | 383 | .ribbon::before, 384 | .ribbon::after { 385 | position: absolute; 386 | z-index: -1; 387 | content: ''; 388 | display: block; 389 | border: 5px solid #2980b9; 390 | } 391 | 392 | .ribbon span { 393 | position: absolute; 394 | display: block; 395 | width: 225px; 396 | padding: 5px 0; 397 | box-shadow: 0 3px 3px rgba(0,0,0,.1); 398 | color: #fff; 399 | font: 700 12px/1 'Lato', sans-serif; 400 | text-shadow: 0 1px 1px rgba(0,0,0,.2); 401 | text-transform: uppercase; 402 | text-align: center; 403 | } 404 | 405 | .ribbon-top-right { 406 | top: -10px; 407 | right: -10px; 408 | } 409 | .ribbon-top-right::before, 410 | .ribbon-top-right::after { 411 | border-top-color: transparent; 412 | border-right-color: transparent; 413 | } 414 | .ribbon-top-right::before { 415 | top: 0; 416 | left: 0; 417 | } 418 | .ribbon-top-right::after { 419 | bottom: 0; 420 | right: 0; 421 | } 422 | .ribbon-top-right span { 423 | left: -25px; 424 | top: 30px; 425 | transform: rotate(45deg); 426 | } 427 | 428 | .ribbon-currentlyreading span { 429 | background-color: #2980b9; 430 | } 431 | 432 | .ribbon-wanttoread span{ 433 | background-color: #9932CC; 434 | } 435 | 436 | .ribbon-read span{ 437 | background-color: #006400; 438 | } 439 | 440 | /* book transition animations */ 441 | 442 | .move-book-animation-enter { 443 | opacity: 0.01; 444 | width: 0; 445 | } 446 | 447 | .move-book-animation-enter.move-book-animation-enter-active { 448 | opacity: 1; 449 | width: 170px; 450 | transition: 700ms; 451 | } 452 | 453 | .move-book-animation-leave { 454 | opacity: 1; 455 | width: 170px; 456 | } 457 | 458 | .move-book-animation-leave.move-book-animation-leave-active { 459 | opacity: 0.01; 460 | width: 0; 461 | transition: 700ms; 462 | } 463 | 464 | /* book select control mode and animations */ 465 | 466 | .select-mode-button { 467 | margin-right: 12px; 468 | } 469 | 470 | .select-mode-controls { 471 | list-style-type: none; 472 | padding: 0; 473 | margin: 0; 474 | 475 | display: flex; 476 | justify-content: center; 477 | flex-wrap: wrap; 478 | } 479 | 480 | .book-select-mode-enter { 481 | opacity: 0.01; 482 | height: 0; 483 | } 484 | 485 | .book-select-mode-enter.book-select-mode-enter-active { 486 | opacity: 1; 487 | height: 36px; 488 | transition: 500ms; 489 | } 490 | 491 | .book-select-mode-leave { 492 | opacity: 1; 493 | height: 36px; 494 | } 495 | 496 | .book-select-mode-leave.book-select-mode-leave-active { 497 | opacity: 0.01; 498 | height: 0; 499 | transition: 300ms; 500 | } 501 | 502 | 503 | .book-select-box-move-enter { 504 | opacity: 0.01; 505 | height: 0; 506 | } 507 | 508 | .book-select-box-move-enter.book-select-box-move-enter-active { 509 | opacity: 1; 510 | height: 77px; 511 | transition: 500ms; 512 | } 513 | 514 | .book-select-box-move-leave { 515 | opacity: 1; 516 | height: 77px; 517 | } 518 | 519 | .book-select-box-move-leave.book-select-box-move-leave-active { 520 | opacity: 0.01; 521 | height: 0; 522 | transition: 300ms; 523 | } 524 | 525 | 526 | .search-shelf { 527 | text-align: center; 528 | } 529 | 530 | .search-shelf-input { 531 | height: 72px; 532 | } 533 | 534 | .search-shelf-mode-enter { 535 | opacity: 0.01; 536 | height: 0; 537 | } 538 | 539 | .search-shelf-mode-enter.search-shelf-mode-enter-active { 540 | opacity: 1; 541 | height: 72px; 542 | transition: 300ms; 543 | } 544 | 545 | .search-shelf-mode-leave { 546 | opacity: 1; 547 | height: 72px; 548 | } 549 | 550 | .search-shelf-mode-leave.search-shelf-mode-leave-active { 551 | opacity: 0.01; 552 | height: 0; 553 | transition: 300ms; 554 | } 555 | 556 | /* Loader */ 557 | 558 | .shelf-loader-box{ 559 | text-align: center; 560 | margin-top: 20px; 561 | } 562 | 563 | .loader-shelf-menu{ 564 | margin-left: 10px; 565 | } 566 | 567 | .loader-box-svg{ 568 | color: #2980b9; 569 | width: 100px; 570 | height: 100px; 571 | display:inline-block; 572 | } 573 | 574 | /* Shelf message (No books, No internet) */ 575 | .shelf-message-text{ 576 | text-align: center; 577 | margin-top: 100px; 578 | height: 80px; 579 | } 580 | 581 | .fade-enter { 582 | opacity: 0; 583 | z-index: 1; 584 | } 585 | 586 | .fade-enter.fade-enter-active { 587 | opacity: 1; 588 | transition: opacity 250ms ease-in; 589 | } 590 | 591 | /* Book Info */ 592 | 593 | .info-action-button { 594 | margin: 10px; 595 | } 596 | 597 | .info-prop-content{ 598 | color: rgba(0, 0, 0, 0.87); 599 | display: block; 600 | font-size: 15px; 601 | } 602 | 603 | .info-prop{ 604 | margin-bottom: 10px; 605 | } 606 | 607 | .info-prop-title{ 608 | color: rgba(0, 0, 0, 0.54); 609 | display: block; 610 | font-size: 12px; 611 | } 612 | 613 | .info-grid { 614 | display: grid; 615 | grid-template-columns: repeat(2, auto); 616 | grid-gap: 20px; 617 | justify-content: center; 618 | align-content: center; 619 | } 620 | 621 | .info-share { 622 | margin: 10px; 623 | } 624 | 625 | /* dialog */ 626 | 627 | .confirm-dialog { 628 | margin-right: 12px; 629 | } 630 | 631 | /* share */ 632 | 633 | .share-network { 634 | vertical-align: top; 635 | display: inline-block; 636 | margin-right: 30px; 637 | text-align: center; 638 | } 639 | .share-network-share-count { 640 | margin-top: 3px; 641 | font-size: 12px; 642 | } 643 | .share-network-share-button { 644 | cursor: pointer; 645 | } 646 | .share-network-share-button:hover:not(:active) { 647 | opacity: 0.75; 648 | } 649 | 650 | .grid2 .info-item{ 651 | max-width: 300px; 652 | } 653 | 654 | @media (max-width: 400px) { 655 | /* For mobile phones: */ 656 | .info-grid { 657 | display: grid; 658 | grid-template-columns: repeat(1, auto); 659 | grid-gap: 10px; 660 | justify-content: center; 661 | align-content: center; 662 | } 663 | 664 | .grid2 .info-item{ 665 | max-width: 300px; 666 | } 667 | 668 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, Route } from 'react-router-dom'; 3 | import debounce from 'throttle-debounce/debounce'; 4 | import sortBy from 'sort-by'; 5 | import scrollToComponent from 'react-scroll-to-component'; 6 | import AppBar from 'material-ui/AppBar'; 7 | import ArrowBack from 'material-ui/svg-icons/navigation/arrow-back'; 8 | import ContentAdd from 'material-ui/svg-icons/content/add'; 9 | import Divider from 'material-ui/Divider'; 10 | import Drawer from 'material-ui/Drawer'; 11 | import FloatingActionButton from 'material-ui/FloatingActionButton'; 12 | import IconButton from 'material-ui/IconButton'; 13 | import Logout from 'material-ui/svg-icons/action/exit-to-app'; 14 | import Menu from 'material-ui/Menu'; 15 | import MenuItem from 'material-ui/MenuItem'; 16 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 17 | import Subheader from 'material-ui/Subheader'; 18 | import SvgIcon from 'material-ui/SvgIcon'; 19 | import * as BooksAPI from './BooksAPI'; 20 | import * as BookUtils from './BookUtils'; 21 | import Authentication from './Authentication'; 22 | import BookInfo from './BookInfo'; 23 | import Bookshelf from './Bookshelf'; 24 | import Login from './Login'; 25 | import PrivateRoute from './PrivateRoute'; 26 | import Register from './Register'; 27 | import Search from './Search'; 28 | import UnauthenticatedRoute from './UnauthenticatedRoute'; 29 | import './App.css'; 30 | 31 | /** 32 | * @description Main App component. 33 | * @constructor 34 | * @param {Object} props - The props that were defined by the caller of this component. 35 | */ 36 | class BooksApp extends React.Component { 37 | 38 | constructor(props){ 39 | super(props); 40 | 41 | // Add debounce to search requests 42 | this.search = debounce(400, this.search); 43 | /** 44 | * @typedef {Object} ComponentState 45 | * @property {Object[]} books - All books from the logged account. 46 | * @property {number} request - App request state used to represent the API request/response. 47 | * @property {boolean} menuOpen - Main Screen menu state. 48 | * @property {Object[]} searchResults - All books returned from the search. 49 | * @property {string} query - Search term input. 50 | */ 51 | 52 | /** @type {ComponentState} */ 53 | this.state = { 54 | books: [], 55 | request: BookUtils.request.OK, 56 | menuOpen: false, 57 | searchResults: [], 58 | query: '', 59 | }; 60 | } 61 | 62 | /** 63 | * Lifecycle event handler called just after the App loads into the DOM. 64 | * Call the API to get all books if the user is logged. 65 | */ 66 | componentDidMount() { 67 | // Execute get books only if user is logged 68 | if(BookUtils.isLogged()) { 69 | this.getAllBooks(); 70 | } 71 | } 72 | 73 | /** 74 | * @description: Get all books from the logged user. 75 | */ 76 | getAllBooks = () => { 77 | // Inside catch block the context change so assign like this to reference the app context not the catch context 78 | const app = this; 79 | this.setState({request: BookUtils.request.LOADING}); 80 | // Update the Shelves 81 | BooksAPI.getAll().then((books) => { 82 | app.setState({books: books, request: BookUtils.request.OK}); 83 | }).catch(function () { 84 | app.setState({request: BookUtils.request.ERROR}); 85 | }); 86 | }; 87 | 88 | /** 89 | * @description Change shelf value for a book element from the server data. 90 | * @param {Object} book - The book to be updated. 91 | * @param {string} shelf - The category ID. 92 | */ 93 | updateBook = (book, shelf) => { 94 | // If books state array is not empty 95 | if (this.state.books) { 96 | // Update book state to include updating variable used at book updating animation 97 | book.updating = true; 98 | this.setState(state => ({ 99 | books: state.books.filter(b => b.id !== book.id).concat([book]).sort(sortBy('title')) 100 | })); 101 | 102 | // Inside catch block the context change so assign like this to reference the app context not the catch 103 | // context 104 | const app = this; 105 | // Update book reference at remote server, if successful update local state reference also 106 | BooksAPI.update(book, shelf).then(() => { 107 | book.shelf = shelf; 108 | book.updating = false; 109 | // This will update all Bookshelf components since it will force call render and the book will move 110 | // to the correct shelf. 111 | this.setState(state => ({ 112 | books: state.books.filter(b => b.id !== book.id).concat([book]).sort(sortBy('title')) 113 | })); 114 | }).catch(function () { 115 | // If will remove load animations in case of failure also 116 | book.updating = false; 117 | app.setState(state => ({ 118 | books: state.books.filter(b => b.id !== book.id).concat([book]).sort(sortBy('title')), 119 | request: BookUtils.request.BOOK_ERROR, 120 | })); 121 | }); 122 | } 123 | }; 124 | 125 | /** 126 | * @description Update the query state and call the search. 127 | * @param {string} query - The search term. 128 | */ 129 | updateQuery = (query) => { 130 | // If query is empty or undefined 131 | if (!query) { 132 | this.setState({query: '', searchResults: [], request: BookUtils.request.OK}); 133 | return; 134 | } 135 | // Update the search field as soon as the character is typed 136 | this.setState({ 137 | query: query, 138 | request: BookUtils.request.LOADING, 139 | searchResults: [] 140 | }, function stateUpdateComplete() { 141 | this.search(); 142 | }.bind(this)); 143 | }; 144 | 145 | /** 146 | * @description Search books for the query state. 147 | */ 148 | search = () => { 149 | // Inside catch block the context change so assign like this to reference the app context not the catch 150 | // context 151 | const app = this; 152 | const query = this.state.query; 153 | if (query.trim() === '') { 154 | this.setState({query: '', searchResults: [], request: BookUtils.request.OK}); 155 | return; 156 | } 157 | 158 | BooksAPI.search(query).then((books) => { 159 | 160 | // If the query state (the search input) changed while the request was in process not show the books 161 | // of a previous query state 162 | if (query !== this.state.query) return; 163 | 164 | //If the query is empty no need to request to server just clean the books array 165 | if ('error' in books) { 166 | books = [] 167 | } 168 | else { 169 | /* 170 | * Since the search API didn't return the shelf property for a book this will compare with 171 | * memory mapped books from the shelves to include the correct shelf attribute. 172 | */ 173 | books.map(book => (this.state.books.filter((b) => b.id === book.id).map(b => book.shelf = b.shelf))); 174 | } 175 | this.setState({ 176 | searchResults: books.sort(sortBy('title')), 177 | request: BookUtils.request.OK, 178 | }); 179 | }).catch(function () { 180 | // If will remove load animations in case of failure also 181 | app.setState(state => ({ 182 | searchResults: [], 183 | request: BookUtils.request.ERROR, 184 | })); 185 | }); 186 | }; 187 | 188 | /** 189 | * @description Save account address into local storage and proceed to main app page. 190 | * @param {string} address - The account address. 191 | * @param {Object} history - The react router history object. 192 | */ 193 | handleLogin = (address, history) => { 194 | // Reset any previous stored state data in memory 195 | this.setState({ 196 | books: [], 197 | menuOpen: false, 198 | searchResults: [], 199 | query: '', 200 | }); 201 | BookUtils.saveAccountAddress(address); 202 | history.push('/'); 203 | this.getAllBooks(); 204 | }; 205 | 206 | /** 207 | * @description Clean the address from local storage and redirect to authentication page. 208 | * @param {Object} history - The react router history object. 209 | */ 210 | handleLogout = (history) => { 211 | BookUtils.cleanAccountAddress(); 212 | history.push('/authentication'); 213 | }; 214 | 215 | /** 216 | * @description Back to previous page and clean some previous page state. 217 | * @param {Object} history - The react router history object. 218 | */ 219 | goToPrevious = (history) => { 220 | history.push('/'); 221 | // Clear previous search if go back to home 222 | this.setState({query: '', searchResults: []}); 223 | }; 224 | 225 | /** 226 | * @description Change request state to OK in case of pressing OK at dialog after an individual book update failure. 227 | */ 228 | handleUpdateBookError = () => { 229 | this.setState({request: BookUtils.request.OK}); 230 | }; 231 | 232 | /** 233 | * @description Toggle app menu open/close state. 234 | */ 235 | handleMenuToggle = () => { 236 | this.setState(state => ({ 237 | menuOpen: !state.menuOpen, 238 | })); 239 | }; 240 | 241 | /** 242 | * @description Animated scroll the main page area top to the informed shelf. 243 | * @param {string} shelf - The id of the category. 244 | */ 245 | goToShelf = (shelf) => { 246 | this.setState({menuOpen: false}, function stateUpdateComplete() { 247 | scrollToComponent(this[shelf], {offset: -90, align: 'top', duration: 500, ease: 'inCirc'}); 248 | }.bind(this)); 249 | }; 250 | 251 | render() { 252 | return ( 253 | 254 |
255 | {/* Main app screen (Bookshelf) */} 256 | ( 257 |
258 |
259 | MyReads
} 261 | iconClassNameRight="muidocs-icon-navigation-expand-more" 262 | onLeftIconButtonTouchTap={this.handleMenuToggle} 263 | /> 264 |
265 | this.setState({'menuOpen': open})}> 267 | 268 | Go to Shelf 269 | {BookUtils.getBookshelfCategories().map((shelf) => ( 270 | this.goToShelf(shelf)}> 271 | {this.props.category}/ 274 | {BookUtils.getBookshelfCategoryName(shelf)} 275 | 276 | ))} 277 | 278 | (this.handleLogout(history))}> 279 | 280 | Logout 281 | 282 | 283 | 284 |
285 |
286 | {BookUtils.getBookshelfCategories().map((shelf) => ( 287 |
{ 289 | this[shelf] = section; 290 | }}> 291 | book.shelf === shelf).sort( 293 | sortBy('title'))} 294 | category={shelf} 295 | request={this.state.request} 296 | onUpdateBook={this.updateBook} 297 | onUpdateBookError={this.handleUpdateBookError} 298 | onConnectionError={this.getAllBooks} 299 | /> 300 |
301 | ))} 302 |
303 |
304 |
305 | 309 | 310 | 311 | 312 | 313 |
314 |
315 | )}/> 316 | {/* Search */} 317 | ( 318 |
319 |
320 | Search
} 322 | iconElementLeft={ 323 | 324 | 325 | 326 | } 327 | onLeftIconButtonTouchTap={() => (this.goToPrevious(history))} 328 | /> 329 |
330 |
331 | 340 |
341 | 342 | )}/> 343 | {/* BookInfo page */} 344 | ( 346 |
347 |
348 | Book Details
} 350 | iconElementLeft={ 351 | 352 | {state && 353 | 354 | } 355 | {!state && 356 | 357 | 358 | 359 | } 360 | 361 | } 362 | onLeftIconButtonTouchTap={ 363 | () => { 364 | // Go Back 365 | if (state) 366 | history.goBack(); 367 | // Go to Home 368 | else { 369 | history.push("/"); 370 | } 371 | } 372 | } 373 | /> 374 |
375 |
376 | 377 |
378 | 379 | )}/> 380 | {/* Authentication Page */} 381 | ( 382 |
383 |
384 | MyReads
} 386 | showMenuIconButton={false} 387 | /> 388 |
389 |
390 | 391 |
392 | 393 | )}/> 394 | {/* Login page */} 395 | ( 396 |
397 |
398 | Login
} 400 | iconElementLeft={ 401 | 402 | 403 | 404 | } 405 | onLeftIconButtonTouchTap={() => (history.push('/authentication'))} 406 | /> 407 |
408 |
409 | 410 |
411 | 412 | )}/> 413 | {/* Register Page */} 414 | ( 415 |
416 |
417 | Register
} 419 | iconElementLeft={ 420 | 421 | 422 | 423 | } 424 | onLeftIconButtonTouchTap={() => (history.push('/authentication'))} 425 | /> 426 |
427 |
428 | 429 |
430 | 431 | )}/> 432 | 433 | 434 |
435 | ); 436 | } 437 | } 438 | 439 | export default BooksApp; -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | /** 6 | This course is not designed to teach Test Driven Development. 7 | Feel free to use this file to test your application, but it 8 | is not required. 9 | **/ 10 | 11 | it('renders without crashing', () => { 12 | const div = document.createElement('div'); 13 | ReactDOM.render(, div); 14 | }); -------------------------------------------------------------------------------- /src/Authentication.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import RaisedButton from 'material-ui/RaisedButton'; 4 | 5 | /** 6 | * @description The Authentication Page. 7 | * @constructor 8 | * @param {Object} props - The props that were defined by the caller of this component. 9 | */ 10 | function Authentication(props) { 11 | return ( 12 |
13 | 14 | Fork me on GitHub 19 | 20 |
21 |

Welcome to MyReads!

22 |

This is a bookshelf app that allow users to select and categorize books they had read, are currently 23 | reading, or want to read.

24 |

This is the final assessment project for the Udacity's React Fundamentals course, part of the 26 | React Nanodegree Program

27 |
28 | 29 | 33 | 34 |
35 |
36 | 37 | 41 | 42 |
43 |

44 | Important: The backend API provided by Udacity eventually clean internal data which consequently resets 45 | the account data. 46 | This is just an evaluation project not intended to be use in daily basis. 47 |

48 |
49 |
50 | ); 51 | } 52 | 53 | export default Authentication; -------------------------------------------------------------------------------- /src/Book.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {CSSTransitionGroup} from 'react-transition-group'; 4 | import { Link } from 'react-router-dom'; 5 | import Loader from 'react-loader-advanced'; 6 | import Checkbox from 'material-ui/Checkbox'; 7 | import Divider from 'material-ui/Divider'; 8 | import FloatingActionButton from 'material-ui/FloatingActionButton'; 9 | import IconMenu from 'material-ui/IconMenu'; 10 | import Info from 'material-ui/svg-icons/action/info'; 11 | import MenuItem from 'material-ui/MenuItem'; 12 | import NavigationExpandMoreIcon from 'material-ui/svg-icons/navigation/expand-more'; 13 | import Subheader from 'material-ui/Subheader'; 14 | import DotLoader from './icons/loaders/dots.svg'; 15 | import RemoveIcon from './icons/shelves/none.svg'; 16 | import * as BookUtils from './BookUtils'; 17 | import BookRating from './BookRating'; 18 | import './App.css'; 19 | 20 | /** 21 | * This object is used for type checking the props of the component. 22 | */ 23 | const propTypes = { 24 | id: PropTypes.string.isRequired, 25 | image: PropTypes.string.isRequired, 26 | title: PropTypes.string.isRequired, 27 | updating: PropTypes.bool.isRequired, 28 | selected: PropTypes.bool.isRequired, 29 | selectMode: PropTypes.bool.isRequired, 30 | withRibbon: PropTypes.bool.isRequired, 31 | onUpdate: PropTypes.func.isRequired, 32 | authors: PropTypes.array, 33 | averageRating: PropTypes.number, 34 | ratingsCount: PropTypes.number, 35 | shelf: PropTypes.string, 36 | onSelectBook: PropTypes.func, 37 | onDeselectBook: PropTypes.func, 38 | }; 39 | 40 | /** 41 | * This object sets default values to the optional props. 42 | */ 43 | const defaultProps = { 44 | authors: [], 45 | averageRating: 0, 46 | ratingsCount: 0, 47 | shelf: 'none', 48 | onSelectBook: () => {}, 49 | onDeselectBook: () => {}, 50 | }; 51 | 52 | /** 53 | * This callback type is called `selectBoxCallback` and is displayed as a global symbol. 54 | * 55 | * @callback selectBoxCallback 56 | */ 57 | 58 | /** 59 | * This callback type is called `updateCallback` and is displayed as a global symbol. 60 | * 61 | * @callback updateCallback 62 | * @param {string} shelf - The id of the shelf. 63 | */ 64 | 65 | /** 66 | * @description Represents a Book. 67 | * @constructor 68 | * @param {Object} props - The props that were defined by the caller of this component. 69 | * @param {string} props.id - The id of the book. 70 | * @param {string} props.image - The url of the book cover image. 71 | * @param {string} props.title - The title of the book. 72 | * @param {boolean} props.updating - Indicates whether the book is updating. Shows the loader layer if true. 73 | * @param {boolean} props.selected - Indicates whether the book is selected. 74 | * @param {boolean} props.selectMode - Indicates whether the book is in select mode. 75 | * @param {boolean} props.withRibbon - Indicates whether the shelf category ribbon is present. 76 | * @param {updateCallback} props.onUpdate - The callback to be executed when user clicks the OK button. 77 | * @param {Object[]} [props.authors=[]] - The authors of the book. 78 | * @param {number} [props.averageRating=0] - The average value of the book ratings. 79 | * @param {number} [props.ratingsCount=0] - The total number of the book ratings. 80 | * @param {string} [props.shelf=none] - The shelf category id of which the book belongs. 81 | * @param {selectBoxCallback} [props.onSelectBook] - The callback to be executed when a book is selected. 82 | * @param {selectBoxCallback} [props.onDeselectBook] - The callback to be executed when a book is deselected. 83 | */ 84 | function Book(props) { 85 | return ( 86 |
87 |
88 | {/* Show Loader if book is updating */} 89 |
Updating
}> 91 |
99 | {props.withRibbon && (props.shelf !== 'none') && 100 |
101 |
102 |
103 | 104 | {BookUtils.getBookshelfCategoryName(props.shelf).split(" ", 2)[0]} 105 | 106 |
107 |
108 |
} 109 |
110 |
111 |
112 | 116 | {props.selectMode && { 120 | if (isInputChecked) { 121 | props.onSelectBook(); 122 | } 123 | else { 124 | props.onDeselectBook(); 125 | } 126 | }} 127 | />} 128 | 129 |
130 | 134 | {!props.selectMode && !props.updating && 135 |
136 |
137 | 140 | 141 | 142 | }> 143 | {/* Link state is used to control if app menu should show home button or back arrow */} 144 | 145 | 146 | 147 | Show Book Details 148 | 149 | 150 | 151 | Move Book to... 152 | {BookUtils.getBookshelfCategories().filter(shelf => shelf !== (props.shelf)).map( 153 | (shelf) => ( 154 | { 156 | // Call informed function with the shelf value to be updated 157 | props.onUpdate(shelf); 158 | }}> 159 | 163 | {BookUtils.getBookshelfCategoryName(shelf)} 164 | 165 | ))} 166 | {/* Show only if belong to any shelf other than none */} 167 | {( !('shelf' in props) || (props.shelf !== 'none')) && 168 | { 170 | // Call informed function with the shelf value to be updated 171 | props.onUpdate('none'); 172 | }}> 173 | 174 | None 175 | 176 | } 177 | 178 |
179 |
180 | } 181 |
182 |
183 |
184 | 188 |
189 |
{props.title}
190 |
191 | {('authors' in props) ? props.authors.join(', ') : ''} 192 |
193 |
194 | ); 195 | } 196 | 197 | // Type checking the props of the component 198 | Book.propTypes = propTypes; 199 | // Assign default values to the optional props 200 | Book.defaultProps = defaultProps; 201 | 202 | export default Book; -------------------------------------------------------------------------------- /src/BookInfo.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {Card, CardActions, CardTitle, CardText} from 'material-ui/Card'; 4 | import RaisedButton from 'material-ui/RaisedButton'; 5 | import ActionGrade from 'material-ui/svg-icons/action/grade'; 6 | import RemoveRedEye from 'material-ui/svg-icons/image/remove-red-eye'; 7 | import LoaderBox from './Loader'; 8 | import * as BooksAPI from './BooksAPI'; 9 | import BookRating from './BookRating'; 10 | import Share from './Share'; 11 | 12 | const rateItURL = 'https://books.google.com.br/books?op=lookup&id='; 13 | 14 | /** 15 | * This object is used for type checking the props of the component. 16 | */ 17 | const propTypes = { 18 | id: PropTypes.string.isRequired, 19 | }; 20 | 21 | /** 22 | * @description BookInfo page 23 | * @constructor 24 | * @param {Object} props - The props that were defined by the caller of this component. 25 | * @param {string} props.id - The id of the book. 26 | */ 27 | class BookInfo extends Component { 28 | 29 | constructor(props){ 30 | super(props); 31 | /** 32 | * @typedef {Object} ComponentState 33 | * @property {Object} book - The book object. 34 | * @property {boolean} loading - Indicates whether the page is updating. 35 | */ 36 | 37 | /** @type {ComponentState} */ 38 | this.state = { 39 | book: {}, 40 | status: 'loading', 41 | }; 42 | } 43 | 44 | /** 45 | * Lifecycle event handler called just after the App loads into the DOM. 46 | * Call the API to get all books and update books state variable when the callback returns. 47 | */ 48 | componentDidMount() { 49 | this.getBookInfo(); 50 | } 51 | 52 | /** 53 | * @description Call the API to get book data and update book state variable when the callback returns. 54 | */ 55 | getBookInfo = () => { 56 | // Inside catch block the context change so assign like this to reference the app context not the catch 57 | // context 58 | const app = this; 59 | this.setState({status: 'loading'}); 60 | // Get the book data using API request 61 | BooksAPI.get(this.props.id).then((book) => { 62 | // No error, book data is available 63 | if(!book.errorCode) { 64 | app.setState({book: book, status: 'ok'}); 65 | } 66 | // 500 happens when bookID is not found (it can happen for any other server internal error, but since 67 | // API response is limited I must assume that the 500 means no book available for the informed book ID 68 | else { 69 | if(book.errorCode === 500){ 70 | app.setState({status: 'not_found'}); 71 | } 72 | // Any other error code 73 | else { 74 | app.setState({status: 'error'}); 75 | } 76 | } 77 | }).catch(function() { 78 | app.setState({status: 'error'}); 79 | }); 80 | }; 81 | 82 | render() { 83 | const book = this.state.book; 84 | 85 | return ( 86 |
87 | {/* Show when app is getting data from server */} 88 |
89 | 90 |
91 | {/* Show when app have problems getting book data from server */} 92 | {(this.state.status === 'error') && 93 |
94 |
Unable to fetch data from server
95 |
(Maybe internet connection or server instability)
96 | 97 |
98 | } 99 | {/* Show when book id is not found */} 100 | {(this.state.status === 'not_found') && 101 |
102 | No Book available for the informed ID. 103 |
104 | } 105 | {/* Book */} 106 | {(this.state.status === 'ok') && 107 | 108 |
109 | 110 |
111 |
112 | book.title 113 |
114 | 118 |
119 |
120 |
121 | {('authors' in book) && 122 |
123 | {(book.authors.length > 1) ? 'Authors' : 'Author'} 125 | 126 | {book.authors.map((author) => ( 127 | 128 | {author} 129 | 130 | ))} 131 |
132 | } 133 | {book.publisher && 134 |
135 | Publisher 136 | {book.publisher} 137 |
138 | } 139 | {book.publishedDate && 140 |
141 | Published Date 142 | {book.publishedDate} 143 |
144 | } 145 | {('pageCount' in book) && 146 |
147 | Pages 148 | {book.pageCount} 149 |
150 | } 151 | {book.categories && 152 |
153 | 155 | {(book.categories.length > 1) ? 'Categories' : 'Category'} 156 | 157 | {book.categories.map((category) => ( 158 | 159 | {category} 160 | 161 | ))} 162 |
163 | } 164 |
165 |
166 |
167 | 168 | {book.description} 169 | 170 | 171 |
172 | } 179 | /> 180 | } 187 | /> 188 |
189 |
190 |

Share

191 | 192 |
193 |
194 |
195 | } 196 |
197 | ); 198 | } 199 | } 200 | // Type checking the props of the component 201 | BookInfo.propTypes = propTypes; 202 | 203 | export default BookInfo; -------------------------------------------------------------------------------- /src/BookRating.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import StarRatingComponent from 'react-star-rating-component'; 4 | 5 | /** 6 | * This object is used for type checking the props of the component. 7 | */ 8 | const propTypes = { 9 | value: PropTypes.number.isRequired, 10 | count: PropTypes.number.isRequired, 11 | }; 12 | 13 | /** 14 | * @description Represents a star rating component that support full and half star (0.5 unity values). 15 | * @constructor 16 | * @param {Object} props - The props that were defined by the caller of this component. 17 | * @param {number} props.value - The average value of the ratings. 18 | * @param {number} props.count - The total number of ratings. 19 | */ 20 | function BookRating(props) { 21 | return ( 22 |
23 | {/* The name props is the radio input value, it required but only is important if the editing is true, since 24 | the application will not allow this component to have editable value it will be set as a random value */} 25 | { 34 | return ; 35 | }} 36 | renderStarIconHalf={() => } 37 | /> 38 | ({props.count}) 39 |
40 | ); 41 | } 42 | 43 | // Type checking the props of the component 44 | BookRating.propTypes = propTypes; 45 | 46 | export default BookRating; -------------------------------------------------------------------------------- /src/BookUtils.js: -------------------------------------------------------------------------------- 1 | import CurrentlyReading from './icons/shelves/currently-reading.svg'; 2 | import WantToRead from './icons/shelves/want-to-read.svg'; 3 | import Read from './icons/shelves/read.svg'; 4 | import Bigi from 'bigi'; 5 | import Wif from 'wif'; 6 | import Buffer from 'safe-buffer'; 7 | import Hash from 'create-hash'; 8 | import Ecurve from 'ecurve'; 9 | import Bs58check from 'bs58check'; 10 | 11 | const secp256k1 = Ecurve.getCurveByName('secp256k1'); 12 | 13 | /** 14 | * @description Encode hash in base58 format. 15 | * @param {string} hash - The hash to be converted to base58. 16 | * @returns {string} Base58 encoded hash. 17 | */ 18 | const toBase58Check = (hash) => { 19 | const payload = Buffer.Buffer.allocUnsafe(21); 20 | payload.writeUInt8(0x00, 0); 21 | hash.copy(payload, 1); 22 | return Bs58check.encode(payload); 23 | }; 24 | 25 | /** 26 | * @description Get the address account. This address use same bitcoin. 27 | * @param {string} wif - Private key in WIF format 28 | * @returns {string} Returns the address. 29 | */ 30 | export const getAddress = (wif) => { 31 | const key = secp256k1.G.multiply(Bigi.fromBuffer(Wif.decode(wif).privateKey)).getEncoded(true); 32 | return toBase58Check(Hash('rmd160').update(Hash('sha256').update(key).digest()).digest()); 33 | }; 34 | 35 | /** 36 | * @description Get a private key in WIF format. 37 | * @param {string} value - The value to be used into the hash that generate the key. 38 | * @returns {string} The key in WIF format. 39 | */ 40 | export const getWif = (value) => { 41 | const hash = Hash('sha256').update(value).digest(); 42 | return Wif.encode(128, hash, true); 43 | }; 44 | 45 | 46 | /** 47 | * @description Return true if user is logged. 48 | * @returns {boolean} Indicates whether client is logged. 49 | */ 50 | export const isLogged = () => { 51 | const address = localStorage.account_address; 52 | return !!(address); 53 | }; 54 | 55 | /** 56 | * @description Save the address to local storage. This account address is used as a unique token for storing the user 57 | * bookshelf data on the backend server. 58 | * @param {string} accountAddress - The account address. 59 | */ 60 | export const saveAccountAddress = (accountAddress) => { 61 | localStorage.account_address = accountAddress; 62 | }; 63 | 64 | /** 65 | * @description Remove account address from the local storage. 66 | */ 67 | export const cleanAccountAddress = () => { 68 | localStorage.removeItem('account_address'); 69 | }; 70 | 71 | /** 72 | * @description Get the request headers with the account address inside. 73 | */ 74 | export const getAccountHeaders = () => ( 75 | { 76 | 'Accept': 'application/json', 77 | 'Authorization': localStorage.account_address 78 | } 79 | ); 80 | 81 | 82 | /** 83 | * 84 | * @type {{OK: number, LOADING: number, ERROR: number, BOOK_ERROR: number}} 85 | */ 86 | export const request = { 87 | 'OK': 1, 88 | 'LOADING': 2, 89 | 'ERROR': 3, 90 | 'BOOK_ERROR': 4, 91 | }; 92 | 93 | // Category Utils functions 94 | 95 | /** 96 | * Array of the available bookshelf categories IDs. 97 | * @type {[string,string,string]} 98 | */ 99 | const BOOKSHELF_CATEGORY_IDS = [ 100 | 'currentlyReading', 101 | 'wantToRead', 102 | 'read', 103 | ]; 104 | 105 | /** 106 | * Array of the available bookshelf categories names. 107 | * The index matches the BOOKSHELF_CATEGORY_IDS. 108 | * @type {[string,string,string]} 109 | */ 110 | const BOOKSHELF_CATEGORY_NAMES = [ 111 | 'Currently Reading', 112 | 'Want to Read', 113 | 'Read', 114 | ]; 115 | 116 | /** 117 | * Array of svg icons for each bookshelf categories. 118 | * @type {[object,object,object]} 119 | */ 120 | const BOOKSHELF_CATEGORY_ICONS = [ 121 | CurrentlyReading, 122 | WantToRead, 123 | Read, 124 | ]; 125 | 126 | 127 | /** 128 | * @description Get the array of all available bookshelf categories IDs. 129 | */ 130 | export const getBookshelfCategories = () => BOOKSHELF_CATEGORY_IDS; 131 | 132 | /** 133 | * @description Return the bookshelf category name of the informed id or '' if the id doesn't belong to any category. 134 | * @param {string} categoryId - The id of the category. 135 | * @returns {string} The category name. 136 | */ 137 | export const getBookshelfCategoryName = (categoryId) => { 138 | const categoryInternalIndex = BOOKSHELF_CATEGORY_IDS.indexOf(categoryId); 139 | 140 | if (categoryInternalIndex === -1) { 141 | // If Category doesn't exists returns '' 142 | return ''; 143 | } 144 | 145 | return BOOKSHELF_CATEGORY_NAMES[categoryInternalIndex]; 146 | }; 147 | 148 | /** 149 | * @description Return the bookshelf category svg icon reference of the informed id or '' if the id doesn't belong to 150 | * any category. 151 | * @param {string} categoryId - The id of the category. 152 | * @returns {string} The path to the svg image 153 | */ 154 | export const getBookshelfCategoryIcon = (categoryId) => { 155 | const categoryInternalIndex = BOOKSHELF_CATEGORY_IDS.indexOf(categoryId); 156 | 157 | if (categoryInternalIndex === -1) { 158 | // If Category doesn't exists returns '' 159 | return ''; 160 | } 161 | 162 | return BOOKSHELF_CATEGORY_ICONS[categoryInternalIndex]; 163 | }; -------------------------------------------------------------------------------- /src/BooksAPI.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contain some functions to interact with the Book Lender API provided by Udacity. 3 | * 4 | * From the URL https://reactnd-books-api.udacity.com follows the API calls specs: 5 | * 6 | * Use an Authorization header to work with your own data: 7 | * 8 | * fetch(url, { headers: { 'Authorization': 'whatever-you-want' }}) 9 | * 10 | * The following endpoints are available: 11 | * 12 | * GET /status 13 | * GET /books 14 | * GET /books/:id 15 | * PUT /books/:id { shelf } 16 | * POST /search { query, maxResults } 17 | */ 18 | import * as BookUtils from './BookUtils'; 19 | 20 | const api = "https://reactnd-books-api.udacity.com"; 21 | 22 | // Generate a unique token for retrieving get one book API call since it doesn't need the any user related data 23 | // a not logged user could access it. 24 | let token = localStorage.token; 25 | if (!token) 26 | token = Math.random().toString(36).substr(-8); 27 | 28 | const headers = { 29 | 'Accept': 'application/json', 30 | 'Authorization': token, 31 | }; 32 | 33 | /** 34 | * @description Returns the details of a book. Implement the API call GET /books/:id. Note: this is the only function 35 | * that use token randomly generated by this script instead of the user address because the BookInfo components 36 | * that uses this function needs to request data for logged and not logged users. 37 | * @param {string} bookId - The id of the book. 38 | * @returns {Promise.object} The book object if successfully or an object with errorCode key with the http status error. 39 | */ 40 | export const get = (bookId) => 41 | fetch(`${api}/books/${bookId}`, {headers}) 42 | .then(res => { 43 | if (res.status !== 200) { 44 | return res; 45 | } 46 | return res.json(); 47 | }) 48 | .then(data => { 49 | // Only happen if status is 200 50 | if (data.book) { 51 | return data.book; 52 | } 53 | // Return the object with with http status (error code) 54 | return {errorCode: data.status}; 55 | }); 56 | 57 | /** 58 | * @description Get all classified books (books with shelf data) available for a particular address. Implements the call 59 | * to API GET /books. 60 | * @returns {Promise.array} Array of book objects (same book object returned by get()). 61 | */ 62 | export const getAll = () => { 63 | const headers = BookUtils.getAccountHeaders(); 64 | return fetch(`${api}/books`, {headers}) 65 | .then(res => res.json()) 66 | .then(data => data.books); 67 | }; 68 | 69 | /** 70 | * @description Update a book with the informed shelf. Implements the call PUT /books/:id { shelf }. 71 | * @param {object} book - The book object (same object returned at get() and getAll()). 72 | * @param {string} shelf - The shelf ID (only available. 73 | */ 74 | export const update = (book, shelf) => 75 | fetch(`${api}/books/${book.id}`, { 76 | method: 'PUT', 77 | headers: { 78 | ...BookUtils.getAccountHeaders(), 79 | 'Content-Type': 'application/json' 80 | }, 81 | body: JSON.stringify({shelf}) 82 | }).then(res => res.json()); 83 | 84 | /** 85 | * @description Search books that contains a particular query term. Implements the call POST /search {query, maxResults} 86 | * Note: The backend API uses a fixed set of cached search results and is limited to a particular set of search terms, 87 | * which can be found in ./SEARCH_TERMS.md. 88 | * @param {string} query - The search terms. 89 | * @param {number} maxResults - Due to the nature of the backend server, search results are capped at 20, even if this 90 | * is set higher. 91 | */ 92 | export const search = (query, maxResults) => 93 | fetch(`${api}/search`, { 94 | method: 'POST', 95 | headers: { 96 | ...BookUtils.getAccountHeaders(), 97 | 'Content-Type': 'application/json' 98 | }, 99 | body: JSON.stringify({query, maxResults}) 100 | }).then(res => res.json()) 101 | .then(data => data.books); 102 | -------------------------------------------------------------------------------- /src/Bookshelf.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {CSSTransitionGroup} from 'react-transition-group'; 4 | import escapeRegExp from 'escape-string-regexp'; 5 | import sortBy from 'sort-by'; 6 | import Checkbox from 'material-ui/Checkbox'; 7 | import IconMenu from 'material-ui/IconMenu'; 8 | import IconButton from 'material-ui/IconButton'; 9 | import MenuItem from 'material-ui/MenuItem'; 10 | import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert'; 11 | import NavigationClose from 'material-ui/svg-icons/navigation/close'; 12 | import RaisedButton from 'material-ui/RaisedButton'; 13 | import SelectField from 'material-ui/SelectField'; 14 | import Subheader from 'material-ui/Subheader'; 15 | import TextField from 'material-ui/TextField'; 16 | import RemoveIcon from './icons/shelves/none.svg'; 17 | import SearchIcon from 'material-ui/svg-icons/action/search'; 18 | import {Toolbar, ToolbarGroup, ToolbarSeparator, ToolbarTitle} from 'material-ui/Toolbar'; 19 | import ConfirmDialog from './ConfirmDialog'; 20 | import AlertDialog from './AlertDialog'; 21 | import LoaderBox from './Loader'; 22 | import Book from './Book'; 23 | import * as BookUtils from './BookUtils'; 24 | import './App.css'; 25 | 26 | /** 27 | * This object is used for type checking the props of the component. 28 | */ 29 | const propTypes = { 30 | books: PropTypes.array.isRequired, 31 | category: PropTypes.oneOf(BookUtils.getBookshelfCategories()), 32 | request: PropTypes.oneOf(Object.values(BookUtils.request)), 33 | title: PropTypes.string, 34 | withRibbon: PropTypes.bool, 35 | withShelfMenu: PropTypes.bool, 36 | onUpdateBook: PropTypes.func.isRequired, 37 | onUpdateBookError: PropTypes.func, 38 | onConnectionError: PropTypes.func, 39 | }; 40 | 41 | /** 42 | * This object sets default values to the optional props. 43 | */ 44 | const defaultProps = { 45 | category: null, 46 | request: BookUtils.request.OK, 47 | title: null, 48 | withRibbon: false, 49 | withShelfMenu: true, 50 | onUpdateBookError: () => {}, 51 | onConnectionError: () => {}, 52 | }; 53 | 54 | /** 55 | * This callback type is called `updateBook` and is displayed as a global symbol. 56 | * 57 | * @callback updateBook 58 | * @param {Object} book - The book object. 59 | * @param {string} shelf - The id of the shelf. 60 | */ 61 | 62 | /** 63 | * This callback type is called `updateBookError` and is displayed as a global symbol. 64 | * 65 | * @callback updateBookError 66 | */ 67 | 68 | /** 69 | * This callback type is called `connectionError` and is displayed as a global symbol. 70 | * 71 | * @callback connectionError 72 | * @param {string} shelf - The id of the shelf. 73 | */ 74 | 75 | /** 76 | * @description Represents the Bookshelf element, with the books from the shelf category 77 | * @constructor 78 | * @param {Object} props - The props that were defined by the caller of this component. 79 | * @param {Object[]} props.books - List of books that belongs to the shelf. 80 | * @param {number} [props.category=null] - The id of the category of the shelf. 81 | * @param {number} [props.request=OK] - The API request state. 82 | * @param {string} [props.title=null] - The title of the shelf. 83 | * @param {boolean} [props.withRibbon=false] - Indicates whether the ribbon is present on top of each book. 84 | * @param {boolean} [props.withShelfMenu=true] - Indicates whether the shelf menu is present. 85 | * @param {updateBook} props.onUpdateBook - The callback executed when the update book is triggered. 86 | * @param {updateBookError} [props.onUpdateBookError] - The callback executed if the update fails. 87 | * @param {connectionError} [props.onConnectionError] - The callback executed if the a connection error happens. 88 | */ 89 | class Bookshelf extends Component { 90 | 91 | constructor(props){ 92 | super(props); 93 | 94 | /** 95 | * @typedef {Object} ComponentState 96 | * @property {boolean} selectMode - Indicates if the select mode is active. 97 | * @property {Object[]} selectedBooks - Array of selected books. 98 | * @property {boolean} dialogOpen - Visibility of the clear shelf dialog. 99 | * @property {boolean} alertDialogOpen - Visibility of the error alert dialog. 100 | * @property {string} query - Search query input of the shelf internal search. 101 | * @property {boolean} queryMode - Indicates if the shelf's books search is active. 102 | */ 103 | 104 | /** @type {ComponentState} */ 105 | 106 | this.state = { 107 | selectMode: false, 108 | selectedBooks: [], 109 | dialogOpen: false, 110 | alertDialogOpen: false, 111 | query: '', 112 | queryMode: false, 113 | }; 114 | } 115 | 116 | /** 117 | * @description Enable the select books mode. 118 | */ 119 | enableSelectMode = () => { 120 | if (!this.state.selectMode) { 121 | this.setState({selectMode: true, query: '', queryMode: false}); 122 | } 123 | }; 124 | 125 | /** 126 | * @description Disable the select books mode. 127 | */ 128 | disableSelectMode = () => { 129 | if (this.state.selectMode) { 130 | this.setState({selectMode: false, selectedBooks: []}); 131 | } 132 | }; 133 | 134 | /** 135 | * @description Insert the book into the selectedBooks state array. 136 | * @param {object} book - The Book object. 137 | */ 138 | selectBook = (book) => { 139 | // If books state array is not empty 140 | if (this.state.selectMode) { 141 | this.setState(state => ({ 142 | selectedBooks: state.selectedBooks.filter(b => b.id !== book.id).concat([book]) 143 | })); 144 | } 145 | }; 146 | 147 | /** 148 | * @description Copy all books from the shelf to the selectedBooks state array. 149 | */ 150 | selectAllBooks = () => this.setState({selectedBooks: this.props.books}); 151 | 152 | /** 153 | * @description Clear selectedBooks state array. 154 | */ 155 | deselectAllBooks = () => this.setState({selectedBooks: []}); 156 | 157 | /** 158 | * @description Remove book from the selectedBooks state array. 159 | * @param {object} book - The book object. 160 | */ 161 | deselectBook = (book) => { 162 | // If books state array is not empty 163 | if (this.state.selectMode) { 164 | this.setState(state => ({ 165 | selectedBooks: state.selectedBooks.filter(b => b.id !== book.id) 166 | })); 167 | } 168 | }; 169 | 170 | /** 171 | * @description Update shelf value of the books from selectedBooks state array to the informed value. 172 | * @param {string} shelf - The id of the shelf. 173 | */ 174 | updateBooks = (shelf) => { 175 | const onUpdateBook = this.props.onUpdateBook; 176 | const selectedBooks = this.state.selectedBooks; 177 | this.setState({selectMode: false, selectedBooks: []}); 178 | selectedBooks.forEach(function (book) { 179 | onUpdateBook(book, shelf); 180 | }); 181 | }; 182 | 183 | /** 184 | * @description Open the Clear shelf dialog. 185 | */ 186 | handleDialogOpen = () => { 187 | this.setState({dialogOpen: true}); 188 | }; 189 | 190 | /** 191 | * @description Close the Clear shelf dialog. 192 | */ 193 | handleDialogClose = () => { 194 | this.setState({dialogOpen: false}); 195 | }; 196 | 197 | /** 198 | * @description Remove all books from the shelf. 199 | */ 200 | clearShelf = () => { 201 | let onUpdateBook = this.props.onUpdateBook; 202 | this.props.books.forEach(function (book) { 203 | onUpdateBook(book, 'none'); 204 | }); 205 | // Close the Clear dialog 206 | this.handleDialogClose(); 207 | this.setState({selectMode: false, query: '', queryMode: false}); 208 | }; 209 | 210 | 211 | /** 212 | * @description Update search query input. 213 | * @param {string} query - The search term. 214 | */ 215 | updateSearchQuery = (query) => { 216 | this.setState({ query: query.trim() }) 217 | }; 218 | 219 | /** 220 | * @description Toggle the search mode activation. 221 | */ 222 | handleSearchCheck = () => { 223 | this.setState((state) => { 224 | return { 225 | queryMode: !state.queryMode, 226 | query: '', 227 | }; 228 | }, function stateUpdateComplete() { 229 | this.searchInput.focus(); 230 | }.bind(this)); 231 | }; 232 | 233 | render() { 234 | const { 235 | books, 236 | category, 237 | onUpdateBook, 238 | onConnectionError, 239 | onUpdateBookError, 240 | request, 241 | title, 242 | withRibbon, 243 | withShelfMenu 244 | } = this.props; 245 | 246 | const selectMode = this.state.selectMode && (request === BookUtils.request.OK); 247 | const { query, queryMode } = this.state; 248 | let showingBooks; 249 | if (query && queryMode) { 250 | // Escape regex elements converting to literal strings, and 'í' to ignore case 251 | const match = new RegExp(escapeRegExp(query), 'i'); 252 | showingBooks = books.filter((book) => match.test(book.title)); 253 | showingBooks.sort(sortBy('title')); 254 | } else { 255 | showingBooks = books; 256 | } 257 | 258 | return ( 259 |
260 | {/* Shelf Toolbar */} 261 | 262 | 263 | 264 | 267 | 268 | {/* Only show shelf menu if this props is true */} 269 | {withShelfMenu && (books.length > 0) && 270 | 271 | {!selectMode && 272 |
273 | } 275 | uncheckedIcon={} 276 | checked={this.state.queryMode} 277 | onCheck={this.handleSearchCheck} 278 | /> 279 |
280 | } 281 | 282 | {/* Shelf Menu Options */} 283 | {!selectMode && 284 | 287 | 288 | 289 | } 290 | > 291 | 292 | 293 | 294 | } 295 | {/* Close Select Mode Button */} 296 | {selectMode && 297 | 298 | 299 | 300 | } 301 |
302 | } 303 |
304 | {/* Clear Books Confirmation Dialog */} 305 | 313 | {/* Alert Dialog when have problems with book update server response */} 314 | 319 | {/* Shelf Loader */} 320 |
321 | 322 |
323 | 324 |
    325 | {/* Select Book Controls */} 326 | 330 | {selectMode && 331 |
    332 |
    333 | 338 | 343 |
    344 |
    345 | 349 | {(this.state.selectedBooks.length > 0) && 350 |
    351 | 1 ? 'Books': 353 | 'Book') + " to"} 354 | value={this.state.value} 355 | onChange={(event, index, value) => (this.updateBooks(value))} 356 | > 357 | Move Book to... 358 | {BookUtils.getBookshelfCategories().filter(shelf => shelf !== category).map( 359 | (shelf) => ( 360 | 361 | 365 | {BookUtils.getBookshelfCategoryName(shelf)} 366 | 367 | ) 368 | )} 369 | 370 | 372 | None 373 | 374 | 375 |
    } 376 |
    377 |
    378 |
    } 379 |
    380 | 381 | {/* Show when shelf is empty */} 382 | {(books.length === 0) && (request === BookUtils.request.OK) && 383 |
    384 | No Books available 385 |
    386 | } 387 | {/* Show when shelf have problems getting data from server */} 388 | {(books.length === 0) && (request === BookUtils.request.ERROR) && 389 |
    390 |
    Unable to fetch data from server
    391 |
    (Maybe internet connection or server instability)
    392 | 393 |
    394 | } 395 | {/* Search Books Input */} 396 | 400 | {(this.state.queryMode) && 401 |
    402 | { 404 | this.searchInput = input; 405 | }} 406 | className="search-shelf-input" 407 | hintText="Enter the search term" 408 | floatingLabelText="Search" 409 | onChange={(event) => this.updateSearchQuery(event.target.value)} 410 | /> 411 |
    412 | } 413 |
    414 | {/* Show when search results are empty */} 415 | {(books.length > 0) && (showingBooks.length === 0) && 416 |
    417 | No Results 418 |
    419 | } 420 | {/* Book List */} 421 | 426 | {showingBooks.map((book) => ( 427 |
  1. 428 | b.id === book.id).length > 0)} 439 | withRibbon={withRibbon} 440 | onSelectBook={() => this.selectBook(book)} 441 | onDeselectBook={() => this.deselectBook(book)} 442 | onUpdate={(shelf) => (onUpdateBook(book, shelf))} 443 | /> 444 |
  2. 445 | ))} 446 |
    447 |
448 |
449 | ); 450 | } 451 | } 452 | 453 | // Type checking the props of the component 454 | Bookshelf.propTypes = propTypes; 455 | // Assign default values to the optional props 456 | Bookshelf.defaultProps = defaultProps; 457 | 458 | export default Bookshelf; -------------------------------------------------------------------------------- /src/ConfirmDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Dialog from 'material-ui/Dialog'; 4 | import FlatButton from 'material-ui/FlatButton'; 5 | import './App.css'; 6 | 7 | /** 8 | * This object is used for type checking the props of the component. 9 | */ 10 | const propTypes = { 11 | title: PropTypes.string.isRequired, 12 | message: PropTypes.string.isRequired, 13 | open: PropTypes.bool.isRequired, 14 | onCancel: PropTypes.func.isRequired, 15 | onConfirm: PropTypes.func.isRequired, 16 | }; 17 | 18 | /** 19 | * This callback type is called `cancelCallback` and is displayed as a global symbol. 20 | * 21 | * @callback cancelCallback 22 | */ 23 | 24 | /** 25 | * This callback type is called `confirmCallback` and is displayed as a global symbol. 26 | * 27 | * @callback confirmCallback 28 | */ 29 | 30 | /** 31 | * @description Represents a material UI based Confirm Dialog. 32 | * @constructor 33 | * @param {Object} props - The props that were defined by the caller of this component. 34 | * @param {string} props.title - The message to be displayed by the dialog. 35 | * @param {boolean} props.open - The visibility of the dialog. 36 | * @param {cancelCallback} props.onCancel - The callback to execute when the user hits the cancel button. 37 | * @param {confirmCallback} props.onConfirm - The callback to execute when the user hits the confirm button. 38 | */ 39 | function ConfirmDialog(props) { 40 | 41 | // Confirm Dialog buttons 42 | const dialogActions = [ 43 | , 48 | , 55 | ]; 56 | 57 | return ( 58 |
59 | 65 | {props.message} 66 | 67 |
68 | ); 69 | } 70 | 71 | // Type checking the props of the component 72 | ConfirmDialog.propTypes = propTypes; 73 | 74 | export default ConfirmDialog; -------------------------------------------------------------------------------- /src/Crypto.js: -------------------------------------------------------------------------------- 1 | import CurrentlyReading from './icons/shelves/currently-reading.svg'; 2 | import WantToRead from './icons/shelves/want-to-read.svg'; 3 | import Read from './icons/shelves/read.svg'; 4 | 5 | /** 6 | * @description Return true if user is logged. 7 | * @returns {boolean} Indicates whether client is logged. 8 | */ 9 | export const isLogged = () => { 10 | const address = localStorage.account_address; 11 | return !!(address); 12 | }; 13 | 14 | /** 15 | * @description Save the address to local storage. This account address is used as a unique token for storing the user 16 | * bookshelf data on the backend server. 17 | * @param {string} accountAddress - The account address. 18 | */ 19 | export const saveAccountAddress = (accountAddress) => { 20 | localStorage.account_address = accountAddress; 21 | }; 22 | 23 | /** 24 | * @description Remove account address from the local storage. 25 | */ 26 | export const cleanAccountAddress = () => { 27 | localStorage.removeItem('account_address'); 28 | }; 29 | 30 | /** 31 | * @description Get the request headers with the account address inside. 32 | */ 33 | export const getAccountHeaders = () => ( 34 | { 35 | 'Accept': 'application/json', 36 | 'Authorization': localStorage.account_address 37 | } 38 | ); 39 | 40 | 41 | /** 42 | * 43 | * @type {{OK: number, LOADING: number, ERROR: number, BOOK_ERROR: number}} 44 | */ 45 | export const request = { 46 | 'OK': 1, 47 | 'LOADING': 2, 48 | 'ERROR': 3, 49 | 'BOOK_ERROR': 4, 50 | }; 51 | 52 | // Category Utils functions 53 | 54 | /** 55 | * Array of the available bookshelf categories IDs. 56 | * @type {[string,string,string]} 57 | */ 58 | const BOOKSHELF_CATEGORY_IDS = [ 59 | 'currentlyReading', 60 | 'wantToRead', 61 | 'read', 62 | ]; 63 | 64 | /** 65 | * Array of the available bookshelf categories names. 66 | * The index matches the BOOKSHELF_CATEGORY_IDS. 67 | * @type {[string,string,string]} 68 | */ 69 | const BOOKSHELF_CATEGORY_NAMES = [ 70 | 'Currently Reading', 71 | 'Want to Read', 72 | 'Read', 73 | ]; 74 | 75 | /** 76 | * Array of svg icons for each bookshelf categories. 77 | * @type {[object,object,object]} 78 | */ 79 | const BOOKSHELF_CATEGORY_ICONS = [ 80 | CurrentlyReading, 81 | WantToRead, 82 | Read, 83 | ]; 84 | 85 | 86 | /** 87 | * @description Get the array of all available bookshelf categories IDs. 88 | */ 89 | export const getBookshelfCategories = () => BOOKSHELF_CATEGORY_IDS; 90 | 91 | /** 92 | * @description Return the bookshelf category name of the informed id or '' if the id doesn't belong to any category. 93 | * @param {string} categoryId - The id of the category. 94 | * @returns {string} The category name. 95 | */ 96 | export const getBookshelfCategoryName = (categoryId) => { 97 | const categoryInternalIndex = BOOKSHELF_CATEGORY_IDS.indexOf(categoryId); 98 | 99 | if (categoryInternalIndex === -1) { 100 | // If Category doesn't exists returns '' 101 | return ''; 102 | } 103 | 104 | return BOOKSHELF_CATEGORY_NAMES[categoryInternalIndex]; 105 | }; 106 | 107 | /** 108 | * @description Return the bookshelf category svg icon reference of the informed id or '' if the id doesn't belong to 109 | * any category. 110 | * @param {string} categoryId - The id of the category. 111 | * @returns {string} The path to the svg image 112 | */ 113 | export const getBookshelfCategoryIcon = (categoryId) => { 114 | const categoryInternalIndex = BOOKSHELF_CATEGORY_IDS.indexOf(categoryId); 115 | 116 | if (categoryInternalIndex === -1) { 117 | // If Category doesn't exists returns '' 118 | return ''; 119 | } 120 | 121 | return BOOKSHELF_CATEGORY_ICONS[categoryInternalIndex]; 122 | }; -------------------------------------------------------------------------------- /src/EntropyInput.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Paper from 'material-ui/Paper'; 4 | import ReactCursorPosition from 'react-cursor-position'; 5 | import * as BookUtils from './BookUtils'; 6 | 7 | /** 8 | * This object is used for type checking the props of the component. 9 | */ 10 | const propTypes = { 11 | value: PropTypes.string, 12 | onComplete: PropTypes.func, 13 | }; 14 | 15 | /** 16 | * This object sets default values to the optional props. 17 | */ 18 | const defaultProps = { 19 | value: Math.random().toString(36), 20 | }; 21 | 22 | /** 23 | * This callback type is called `entropyCallback` and is displayed as a global symbol. 24 | * 25 | * @callback entropyCallback 26 | * @param {Object} accountData - The object with account information. 27 | * @param {Object} accountData.wif - The key in WIF format. 28 | * @param {Object} accountData.address - The account address. 29 | */ 30 | 31 | /** 32 | * @description Represents an entropy input element that get users mouse/finger movements to get extra entropy and 33 | * generate a unique key (WIF format) and address (bitcoin address format) 34 | * @see {@link https://bitcoin.org/en/developer-guide#distributing-only-wallets} 35 | * @constructor 36 | * @param {Object} props - The props that were defined by the caller of this component. 37 | * @param {string} [props.value] - The string value to be used with the random entry. 38 | * @param {entropyCallback} props.onComplete - The callback executed when the entropy is complete (100%). 39 | */ 40 | class EntropyInput extends Component { 41 | 42 | constructor(props){ 43 | super(props); 44 | 45 | /** 46 | * @typedef {Object} ComponentState 47 | * @property {Object[]} entropy - Extra entropy array of cursor position object. 48 | * @property {number} progress - Percent of the progress based on the entropy array size. 49 | */ 50 | 51 | /** @type {ComponentState} */ 52 | this.state = { 53 | entropy: [], 54 | progress: 0, 55 | }; 56 | } 57 | 58 | /** 59 | * @description Run every time the cursor position change, passing the new position to this function. 60 | * @param {Object} position - The position pair (x,y) of the cursor. 61 | */ 62 | handlePositionChange = ({position}) => { 63 | this.setState(state => { 64 | if( (this.state.entropy.length < 500) ) { 65 | const entropy = state.entropy; 66 | entropy.push(position.y.toString() + position.x.toString()); 67 | return {entropy: entropy, progress: Math.round(entropy.length/2)} 68 | } 69 | },function stateUpdateComplete() { 70 | if(this.state.progress === 100) { 71 | const wif = BookUtils.getWif(this.props.value+this.state.entropy); 72 | const address = BookUtils.getAddress(wif); 73 | this.props.onComplete({wif: wif,address: address}); 74 | } 75 | }.bind(this)); 76 | }; 77 | 78 | render() { 79 | const progress = (this.state.progress < 100) ? this.state.progress: 100; 80 | return ( 81 |
82 | 83 | 90 |
92 |
93 |

Swipe Here

94 |

{progress}%

95 |
96 | 97 | 98 |
99 | ); 100 | } 101 | } 102 | 103 | // Type checking the props of the component 104 | EntropyInput.propTypes = propTypes; 105 | // Assign default values to the optional props 106 | EntropyInput.defaultProps = defaultProps; 107 | 108 | export default EntropyInput; -------------------------------------------------------------------------------- /src/Loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import CircleLoader from './icons/loaders/circle.svg'; 4 | import ClockLoader from './icons/loaders/clock.svg'; 5 | import DotsLoader from './icons/loaders/dots.svg'; 6 | import './App.css'; 7 | 8 | const type = ['clock','dots','circle']; 9 | const loaders = [ClockLoader, DotsLoader, CircleLoader]; 10 | 11 | /** 12 | * This object is used for type checking the props of the component. 13 | */ 14 | const propTypes = { 15 | loading: PropTypes.bool, 16 | size: PropTypes.number, 17 | type: PropTypes.oneOf(type), 18 | message: PropTypes.string, 19 | }; 20 | 21 | /** 22 | * This object sets default values to the optional props. 23 | */ 24 | const defaultProps = { 25 | loading: false, 26 | size: 36, 27 | type: type[0], 28 | message: '', 29 | }; 30 | 31 | /** 32 | * @description Represents a loader box, a square box with loading image and loading text. 33 | * @constructor 34 | * @param {Object} props - The props that were defined by the caller of this component. 35 | * @param {boolean} props.loading - Visibility of the component. 36 | * @param {number} props.size - The size in pixel of the box. 37 | * @param {type} props.type - The type of the loader. 38 | * @param {string} props.message - The message to be displayed with the box. 39 | */ 40 | function LoaderBox(props) { 41 | if (!props.loading) { 42 | return null; 43 | } 44 | let style = {}; 45 | if (props.size) { 46 | style = {width: props.size, height: props.size}; 47 | } 48 | let loaderType; 49 | if (!props.type) { 50 | loaderType = type[0]; 51 | } 52 | else{ 53 | loaderType = props.type; 54 | } 55 | return ( 56 |
57 |
58 | Loader 59 |
60 | {props.message && 61 |
62 | {props.message} 63 |
64 | } 65 |
66 | ); 67 | } 68 | 69 | // Type checking the props of the component 70 | LoaderBox.propTypes = propTypes; 71 | // Assign default values to the optional props 72 | LoaderBox.defaultProps = defaultProps; 73 | 74 | export default LoaderBox; -------------------------------------------------------------------------------- /src/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | import QrReader from 'react-qr-reader'; 4 | import SwipeableViews from 'react-swipeable-views'; 5 | import Checkbox from 'material-ui/Checkbox'; 6 | import MenuItem from 'material-ui/MenuItem'; 7 | import RaisedButton from 'material-ui/RaisedButton'; 8 | import SelectField from 'material-ui/SelectField'; 9 | import {Tabs, Tab} from 'material-ui/Tabs'; 10 | import TextField from 'material-ui/TextField'; 11 | import Visibility from 'material-ui/svg-icons/action/visibility'; 12 | import VisibilityOff from 'material-ui/svg-icons/action/visibility-off'; 13 | import * as BookUtils from './BookUtils'; 14 | import AlertDialog from './AlertDialog'; 15 | 16 | /** 17 | * @description The Login page component. 18 | * @constructor 19 | * @param {Object} props - The props that were defined by the caller of this component. 20 | */ 21 | class Login extends Component { 22 | 23 | constructor(props){ 24 | super(props); 25 | 26 | /** 27 | * @typedef {Object} ComponentState 28 | * @property {number} delay - The delay between each scan in milliseconds. 29 | * @property {string} facingMode - If the device have multiple cameras this sets which camera is selected. 30 | * @property {boolean} legacyMode - Enable the scan based on uploaded image only. 31 | * @property {boolean} cameraError - Indicates whether an error camera happened. 32 | * @property {string} dialogMessage - The alert dialog message. 33 | * @property {number} slideIndex - The index of the tab. 34 | * @property {boolean} submitDisabled - Indicates whether submit is disabled. 35 | * @property {boolean} inputVisible - The visibility of the account key input. 36 | */ 37 | 38 | /** @type {ComponentState} */ 39 | this.state = { 40 | delay: 300, 41 | facingMode: 'user', 42 | legacyMode: false, 43 | cameraError: false, 44 | dialogMessage: '', 45 | slideIndex: 0, 46 | submitDisabled: true, 47 | inputVisible: false, 48 | }; 49 | this.handleScan = this.handleScan.bind(this); 50 | } 51 | 52 | /** 53 | * @description Handle the scan from the QR code reader. 54 | * @param {string} data - The data from the QR code reader after a scan. 55 | */ 56 | handleScan = (data) => { 57 | const errorMessage = 'Not found any valid QRCode format for the Access Key. Please try a valid image.'; 58 | if(data){ 59 | this.saveAddress(data, errorMessage); 60 | } 61 | else if (this.state.legacyMode) { 62 | this.saveAddress('', errorMessage); 63 | } 64 | }; 65 | 66 | /** 67 | * @description If the app couldn't read the camera. Errors cause can vary from user not giving permission to access 68 | * camera, by the absence of camera or WebRTC is not supported by the browser. 69 | * @param {Object} err - The QR code reader error object. 70 | */ 71 | handleError = (err) => { 72 | this.setState({ 73 | cameraError: true 74 | }); 75 | console.error(err); 76 | }; 77 | 78 | /** 79 | * @description Enable QrCode code reader legacy mode to be able to upload images and open upload dialog. 80 | */ 81 | handleImageLoad = () => { 82 | // Set the QrCode reader to legacy mode for the upload to be available 83 | this.setState({legacyMode: true},function stateUpdateComplete() { 84 | // Show the upload dialog to choose the file 85 | this.qrReader.openImageDialog(); 86 | }.bind(this)); 87 | }; 88 | 89 | /** 90 | * @description If the user environment have two cameras (like mobile phones with frontal and rear camera) it allows 91 | * to switch. The user can also disable the camera and it will set the state to legacy mode (upload file mode). 92 | * between them. 93 | * @param event 94 | * @param index 95 | * @param value 96 | */ 97 | handleCameraChange = (event, index, value) => { 98 | if(value !== 'disabled') { 99 | this.setState({facingMode: value, legacyMode: false}); 100 | } 101 | else { 102 | this.setState({facingMode: value, legacyMode: true}); 103 | } 104 | 105 | }; 106 | 107 | /** 108 | * @description Hide the alert dialog. 109 | */ 110 | hideDialog = () => { 111 | this.setState({ 112 | dialogMessage: '', 113 | }); 114 | }; 115 | 116 | 117 | /** 118 | * @description Validate the the WIF, generate the address and proceed to main app screen. If the fails the dialog 119 | * with the errorMessage will be displayed. 120 | * @see {@link https://en.bitcoin.it/wiki/Wallet_import_format} 121 | * @param {string} wif - The key in WIF format. 122 | * @param {string} errorMessage - Error message to be displayed if the WIF key is not correct. 123 | */ 124 | saveAddress = (wif, errorMessage) => { 125 | try { 126 | // It will work if the WIF is correct 127 | const address = BookUtils.getAddress(wif); 128 | this.props.onComplete(address, this.props.history); 129 | } 130 | catch (e) { 131 | // If WIF is invalid format 132 | this.setState({ 133 | dialogMessage: errorMessage, 134 | }); 135 | } 136 | }; 137 | 138 | /** 139 | * @description Handle account key submit button. 140 | */ 141 | handleKeySubmit = () => { 142 | this.saveAddress(this.keyInput.input.value, 'Not a valid access key. Please enter a valid value.'); 143 | }; 144 | 145 | /** 146 | * @description Handle tab switch. 147 | * @param {number} value - The index of the tab slide. 148 | */ 149 | handleChange = (value) => { 150 | this.setState({ 151 | slideIndex: value, 152 | }); 153 | }; 154 | 155 | /** 156 | * @description Handle the Account Key TextField input change. It enable the submit button if field is not empty or 157 | * disable it otherwise. 158 | * @param event 159 | * @param value 160 | */ 161 | handleTextFieldChange = (event, value) => { 162 | if((value.length > 0) && this.state.submitDisabled){ 163 | this.setState({submitDisabled: false}); 164 | } 165 | else if((value.length === 0) && !this.state.submitDisabled){ 166 | this.setState({submitDisabled: true}); 167 | } 168 | }; 169 | 170 | /** 171 | * @description Change the visibility of the access key text input. 172 | */ 173 | handleVisibilityCheck = () => { 174 | this.setState((state) => { 175 | return { 176 | inputVisible: !state.inputVisible, 177 | }; 178 | }); 179 | }; 180 | 181 | render() { 182 | return ( 183 |
184 | 0)} 186 | message={this.state.dialogMessage} 187 | onClick={this.hideDialog} 188 | /> 189 |

Use the QrCode backup OR the Access Key. If you don't have an account Register Here.

191 | 195 | 196 | 197 | 198 | 202 |
203 | {!this.state.cameraError && 204 | 209 | 210 | 211 | 212 | 213 | } 214 | { 217 | this.qrReader = qrReader; 218 | }} 219 | delay={this.state.delay} 220 | onError={this.handleError} 221 | onScan={this.handleScan} 222 | legacyMode={this.state.legacyMode} 223 | facingMode={this.state.facingMode} 224 | /> 225 | {this.state.cameraError && 226 |
Error reading the camera, try to upload the QRCode Image with the 227 | button bellow
228 | } 229 |
OR
230 | 235 |
236 |
237 |

Enter the Access Key

238 | { 242 | this.keyInput = keyInput; 243 | }} 244 | hintText="Enter the access key" 245 | floatingLabelText="Access Key" 246 | onChange={this.handleTextFieldChange} 247 | /> 248 | } 253 | uncheckedIcon={} 254 | /> 255 |
256 | 262 |
263 |
264 |
265 |
266 | ) 267 | } 268 | } 269 | 270 | export default Login; 271 | -------------------------------------------------------------------------------- /src/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as BookUtils from './BookUtils'; 3 | import { Route, Redirect } from 'react-router-dom'; 4 | 5 | /** 6 | * @description Private routes let the component available only if the user is logged. If not redirect to the 7 | * authentication page. 8 | * @constructor 9 | * @param {Object} props - The props that were defined by the caller of this component. 10 | * @param {function} props.render The render function that must run if the user is not logged. 11 | * @param {Object} props.rest The rest of props available to be included at the route. 12 | */ 13 | const PrivateRoute = ({ render, ...rest }) => { 14 | 15 | let privateRender; 16 | 17 | if (BookUtils.isLogged()) { 18 | privateRender = render; 19 | } 20 | else { 21 | privateRender = (props) => ( 22 | 26 | ) 27 | } 28 | 29 | return( 30 | 31 | ); 32 | }; 33 | 34 | export default PrivateRoute; -------------------------------------------------------------------------------- /src/QRCodeBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import QRCode from 'qrcode.react'; 4 | import RaisedButton from 'material-ui/RaisedButton'; 5 | import FileFileDownload from 'material-ui/svg-icons/file/file-download'; 6 | 7 | /** 8 | * This object is used for type checking the props of the component. 9 | */ 10 | const propTypes = { 11 | value: PropTypes.string.isRequired, 12 | }; 13 | 14 | /** 15 | * @description Represent the QR code component. It generate a QR Code image from a string passed via props 16 | * with a download button. 17 | * @constructor 18 | * @param {Object} props - The props that were defined by the caller of this component. 19 | * @param {string} props.value - The string to be converted to the QR code image box. 20 | */ 21 | class QRCodeBox extends React.Component { 22 | 23 | /** 24 | * Lifecycle event handler called just after the App loads into the DOM. 25 | * Setup the download QR code link converting svg to the image downloadable data. 26 | */ 27 | componentDidMount() { 28 | const app = this; 29 | const link = this.downloadButton; 30 | 31 | link.addEventListener('click', function (e) { 32 | // Convert the svg canvas to image data 33 | link.href = app.qrCanvas._canvas.toDataURL(); 34 | }, false); 35 | } 36 | 37 | render() { 38 | const {value} = this.props; 39 | 40 | return ( 41 |
42 | { 44 | this.qrCanvas = qrCode; 45 | }} 46 | fgColor="#323266" size={240} 47 | value={value} 48 | logoWidth={48} 49 | /> 50 | 63 |
64 | ); 65 | } 66 | } 67 | 68 | QRCodeBox.propTypes = propTypes; 69 | 70 | export default QRCodeBox; -------------------------------------------------------------------------------- /src/Register.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {CopyToClipboard} from 'react-copy-to-clipboard'; 3 | import ReactWindowResizeListener from 'window-resize-listener-react'; 4 | import { 5 | Step, 6 | Stepper, 7 | StepLabel, 8 | StepContent, 9 | } from 'material-ui/Stepper'; 10 | import ExpandTransition from 'material-ui/internal/ExpandTransition'; 11 | import FlatButton from 'material-ui/FlatButton'; 12 | import RaisedButton from 'material-ui/RaisedButton'; 13 | import EntropyInput from './EntropyInput'; 14 | import QRCodeBox from './QRCodeBox'; 15 | 16 | /** 17 | * @description The Register page component 18 | * @constructor 19 | * @param {Object} props - The props that were defined by the caller of this component. 20 | */ 21 | class Register extends React.Component { 22 | 23 | constructor(props){ 24 | super(props); 25 | 26 | /** 27 | * @typedef {Object} ComponentState 28 | * @property {boolean} loading - Indicates whether the stepper component is loading. 29 | * @property {number} stepIndex - The index of the stepper. 30 | * @property {string} accountKey - The private key that generates the account address. 31 | * @property {string} accountAddress - The address of the account. 32 | * @property {string} stepperOrientation - Indicates whether the stepper is horizontal or vertical. 33 | */ 34 | 35 | /** @type {ComponentState} */ 36 | this.state = { 37 | loading: false, 38 | stepIndex: 0, 39 | accountKey: '', 40 | accountAddress: '', 41 | stepperOrientation: 'vertical', 42 | }; 43 | } 44 | 45 | /** 46 | * @description Lifecycle event handler called just after the App loads into the DOM. 47 | * Check the screen width to decide if stepper should be vertical or horizontal. 48 | */ 49 | componentWillMount(){ 50 | this.setState({stepperOrientation: ((innerWidth < 570) ? 'vertical':'horizontal') }); 51 | } 52 | 53 | /** 54 | * @description Handle next step of the stepper. 55 | */ 56 | handleNext = () => { 57 | const {stepIndex} = this.state; 58 | 59 | // Account Creation Finished 60 | if (stepIndex >= 2){ 61 | this.props.onComplete(this.state.accountAddress, this.props.history); 62 | } 63 | if (!this.state.loading) { 64 | this.dummyAsync(() => this.setState({ 65 | loading: false, 66 | stepIndex: stepIndex + 1, 67 | })); 68 | } 69 | }; 70 | 71 | /** 72 | * @description Handle previous step of the stepper. 73 | */ 74 | handlePrev = () => { 75 | const {stepIndex} = this.state; 76 | if (!this.state.loading) { 77 | this.dummyAsync(() => this.setState({ 78 | loading: false, 79 | stepIndex: stepIndex - 1, 80 | })); 81 | } 82 | }; 83 | 84 | /** 85 | * @description Execute when the Entropy input is complete. The parameter is an object with the account address and 86 | * the WIF key. 87 | * @see {@link https://en.bitcoin.it/wiki/Wallet_import_format} 88 | * @param {object} accountData - The object with the wif and address keys generated by entropy input. 89 | */ 90 | handleEntropyComplete = (accountData) => { 91 | this.setState({accountKey: accountData.wif, accountAddress: accountData.address},function stateUpdateComplete() { 92 | this.handleNext(); 93 | }.bind(this)); 94 | }; 95 | 96 | /** 97 | * @description Async callback for stepper transitions. 98 | * @param {function} cb - The callback that should be executed after 500ms. 99 | */ 100 | dummyAsync = (cb) => { 101 | this.setState({loading: true}, () => { 102 | this.asyncTimer = setTimeout(cb, 500); 103 | }); 104 | }; 105 | 106 | /** 107 | * @description Set the stepper orientation state based on the the viewport width. 108 | */ 109 | checkStepper = () => { 110 | this.setState({stepperOrientation: ((innerWidth < 570) ? 'vertical':'horizontal') }); 111 | }; 112 | 113 | /** 114 | * @description Resize handler that runs anytime the width/height change. 115 | */ 116 | resizeHandler = () => { 117 | this.checkStepper(); 118 | }; 119 | 120 | /** 121 | * @description Render the step content based on index. 122 | * @param {number} stepIndex - The index of the stepper. 123 | */ 124 | getStepContent(stepIndex) { 125 | switch (stepIndex) { 126 | case 0: 127 | return ( 128 |
129 |

To create your account move the cursor randomly inside box 130 | bellow for a while, until 131 | it is completely green

132 |
133 | 134 |
135 |
136 | ); 137 | case 1: 138 | return ( 139 |
140 |

Please Download and Save this QR code that contains the access 141 | key or copy the key from the box 142 | it before proceed.

143 |

This Access Key is the only login credential you need.

144 |
145 | 146 |
147 |

You can also Copy/Paste the key somewhere or send it to your 148 | email

149 |
150 |
{this.state.accountKey}
151 | 152 | 153 | 154 | 160 |
161 |

Note: If you lost this key will not be able to recover your 162 | account.

163 |
164 | ); 165 | case 2: 166 | return ( 167 |
168 |

Congratulations!

169 |

170 | If you already saved your access key feel free to continue.

171 |

I hope you enjoy the app!

172 |
173 | ); 174 | default: 175 | return 'You\'re a long way from home sonny jim!'; 176 | } 177 | } 178 | 179 | /** 180 | * @description Render the step actions (buttons) based on index. 181 | * @param {number} stepIndex - The index of the stepper. 182 | */ 183 | renderStepActions(stepIndex) { 184 | return ( 185 |
186 | {/* Show stepper control only after the first step */} 187 | {(stepIndex >= 1) && 188 |
189 | 195 | 200 |
201 | } 202 |
203 | ); 204 | } 205 | 206 | /** 207 | * @description Render the actions and content for the horizontal stepper. 208 | */ 209 | renderHorizontalContent() { 210 | const {stepIndex} = this.state; 211 | const contentStyle = {margin: '0 16px', overflow: 'hidden'}; 212 | 213 | return ( 214 |
215 |
{this.getStepContent(stepIndex)}
216 | {this.renderStepActions(stepIndex)} 217 |
218 | ); 219 | } 220 | 221 | render() { 222 | const {loading, stepIndex} = this.state; 223 | 224 | return ( 225 |
226 | 227 | 228 |
229 | 230 | {(this.state.stepperOrientation === 'horizontal') && 231 |
232 | 233 | 234 | Swipe to create account 235 | 236 | 237 | Save the Access Key 238 | 239 | 240 | Congratulations 241 | 242 | 243 | 244 | {this.renderHorizontalContent()} 245 | 246 |
247 | } 248 | {(this.state.stepperOrientation === 'vertical') && 249 |
250 | 251 | 252 | Swipe to create account 253 | 254 | {this.getStepContent(0)} 255 | {this.renderStepActions(0)} 256 | 257 | 258 | 259 | Save the Access Key 260 | 261 | {this.getStepContent(1)} 262 | {this.renderStepActions(1)} 263 | 264 | 265 | 266 | Congratulations 267 | 268 | {this.getStepContent(2)} 269 | {this.renderStepActions(2)} 270 | 271 | 272 | 273 |
274 | } 275 |
276 |
277 | ); 278 | } 279 | } 280 | 281 | export default Register; 282 | -------------------------------------------------------------------------------- /src/Search.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Bookshelf from './Bookshelf'; 4 | import * as BookUtils from './BookUtils'; 5 | 6 | /** 7 | * This object is used for type checking the props of the component. 8 | */ 9 | const propTypes = { 10 | // Books from the Shelves 11 | books: PropTypes.array.isRequired, 12 | query: PropTypes.string.isRequired, 13 | request: PropTypes.oneOf(Object.values(BookUtils.request)), 14 | onSearch: PropTypes.func.isRequired, 15 | onUpdateQuery: PropTypes.func.isRequired, 16 | onUpdateBook: PropTypes.func.isRequired, 17 | onUpdateBookError: PropTypes.func, 18 | }; 19 | 20 | /** 21 | * @description The Search page component. 22 | * @constructor 23 | * @param {Object} props - The props that were defined by the caller of this component. 24 | */ 25 | class Search extends Component { 26 | 27 | /** 28 | * Lifecycle event handler called just after the App loads into the DOM. 29 | * Focus the search input when component load. 30 | */ 31 | componentDidMount(){ 32 | this.searchInput.focus(); 33 | } 34 | 35 | render() { 36 | const {books, request, query, onUpdateQuery, onSearch, onUpdateBook, onUpdateBookError} = this.props; 37 | 38 | return ( 39 |
40 |
41 |
42 | { 44 | this.searchInput = input; 45 | }} 46 | value={query} 47 | className="search-books" 48 | type="text" 49 | placeholder="Search by title or author" 50 | onChange={(event) => onUpdateQuery(event.target.value)} 51 | /> 52 |
53 |
54 |
55 |
56 |

57 | Important: The backend API uses a fixed set of cached search results and is limited to a 58 | particular set of search terms, which can be found in  59 | 62 | SEARCH_TERMS.md 63 | . 64 |

65 | 75 |
76 |
77 |
78 | ); 79 | }; 80 | } 81 | 82 | // Type checking the props of the component 83 | Search.propTypes = propTypes; 84 | 85 | export default Search; -------------------------------------------------------------------------------- /src/Share.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | ShareButtons, 5 | ShareCounts, 6 | generateShareIcon 7 | } from 'react-share'; 8 | import MyReadsImage from './icons/myreads.jpg'; 9 | import './App.css'; 10 | 11 | const { 12 | FacebookShareButton, 13 | GooglePlusShareButton, 14 | LinkedinShareButton, 15 | TwitterShareButton, 16 | PinterestShareButton, 17 | VKShareButton, 18 | OKShareButton, 19 | TelegramShareButton, 20 | WhatsappShareButton, 21 | RedditShareButton, 22 | EmailShareButton, 23 | TumblrShareButton, 24 | } = ShareButtons; 25 | 26 | const { 27 | FacebookShareCount, 28 | GooglePlusShareCount, 29 | LinkedinShareCount, 30 | PinterestShareCount, 31 | VKShareCount, 32 | OKShareCount, 33 | RedditShareCount, 34 | TumblrShareCount, 35 | } = ShareCounts; 36 | 37 | const FacebookIcon = generateShareIcon('facebook'); 38 | const TwitterIcon = generateShareIcon('twitter'); 39 | const GooglePlusIcon = generateShareIcon('google'); 40 | const LinkedinIcon = generateShareIcon('linkedin'); 41 | const PinterestIcon = generateShareIcon('pinterest'); 42 | const VKIcon = generateShareIcon('vk'); 43 | const OKIcon = generateShareIcon('ok'); 44 | const TelegramIcon = generateShareIcon('telegram'); 45 | const WhatsappIcon = generateShareIcon('whatsapp'); 46 | const RedditIcon = generateShareIcon('reddit'); 47 | const TumblrIcon = generateShareIcon('tumblr'); 48 | const EmailIcon = generateShareIcon('email'); 49 | 50 | /** 51 | * This object is used for type checking the props of the component. 52 | */ 53 | const propTypes = { 54 | title: PropTypes.string.isRequired, 55 | url: PropTypes.string.isRequired, 56 | }; 57 | 58 | /** 59 | * @description Represents the share are with several buttons and share counts. 60 | * @constructor 61 | * @param {Object} props - The props that were defined by the caller of this component. 62 | * @param {string} props.value The share string to be show with the url. 63 | * @param {string} props.url The url of the page which should be shared. 64 | */ 65 | function Share(props) { 66 | return ( 67 |
68 |
69 | 73 | 76 | 77 | 78 | 81 | {count => count} 82 | 83 |
84 | 85 |
86 | 90 | 93 | 94 | 95 |
96 |   97 |
98 |
99 | 100 |
101 | 105 | 106 | 107 | 108 |
109 |   110 |
111 |
112 | 113 |
114 | 119 | 120 | 121 | 122 |
123 |   124 |
125 |
126 | 127 |
128 | 131 | 134 | 135 | 136 | 139 | {count => count} 140 | 141 |
142 | 143 |
144 | 150 | 153 | 154 | 155 | 158 | {count => count} 159 | 160 |
161 | 162 |
163 | 169 | 170 | 171 | 172 | 174 |
175 | 176 |
177 | 183 | 186 | 187 | 188 | 190 |
191 | 192 |
193 | 199 | 202 | 203 | 204 | 206 |
207 | 208 |
209 | 215 | 218 | 219 | 220 | 222 |
223 | 224 |
225 | 231 | 234 | 235 | 236 | 238 |
239 | 240 |
241 | 246 | 249 | 250 |
251 |
252 | ); 253 | } 254 | 255 | // Type checking the props of the component 256 | Share.propTypes = propTypes; 257 | 258 | export default Share; -------------------------------------------------------------------------------- /src/UnauthenticatedRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as BookUtils from './BookUtils'; 3 | import { Route, Redirect } from 'react-router-dom'; 4 | 5 | /** 6 | * @description Private routes let the component available only if the user is NOT logged. If the user is logged 7 | * it will redirect to the main page. 8 | * @constructor 9 | * @param {Object} props - The props that were defined by the caller of this component. 10 | * @param {function} props.render The render function that must run if the user is not logged. 11 | * @param {Object} props.rest The rest of props available to be included at the route. 12 | */ 13 | const UnauthenticatedRoute = ({ render, ...rest }) => { 14 | 15 | let privateRender; 16 | 17 | if (!BookUtils.isLogged()) { 18 | privateRender = render; 19 | } 20 | else { 21 | privateRender = (props) => ( 22 | 26 | ) 27 | } 28 | 29 | return( 30 | 31 | ); 32 | }; 33 | 34 | export default UnauthenticatedRoute; -------------------------------------------------------------------------------- /src/icons/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/arrow-back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/arrow-drop-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/loaders/circle.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 12 | 13 | -------------------------------------------------------------------------------- /src/icons/loaders/clock.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 13 | 14 | 15 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/icons/loaders/dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 12 | 13 | 14 | 18 | 22 | 23 | 24 | 28 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/icons/myreads.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computationalcore/myreads/8cafefed17b2b5b8b4804ed1f6ccf8199ad4f539/src/icons/myreads.jpg -------------------------------------------------------------------------------- /src/icons/shelves/currently-reading.svg: -------------------------------------------------------------------------------- 1 | ABABABCreated by AlfredoCreates.comfrom the Noun Project -------------------------------------------------------------------------------- /src/icons/shelves/none.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/shelves/read.svg: -------------------------------------------------------------------------------- 1 | Created by AlfredoCreates.comfrom the Noun Project -------------------------------------------------------------------------------- /src/icons/shelves/want-to-read.svg: -------------------------------------------------------------------------------- 1 | Created by AlfredoCreates.comfrom the Noun Project -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | *, *:before, *:after { 5 | box-sizing: inherit; 6 | } 7 | body { 8 | margin: 0; 9 | padding: 0; 10 | font-family: 'Roboto', sans-serif; 11 | } 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './App'; 5 | import './index.css'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); --------------------------------------------------------------------------------