├── .gitignore ├── LICENSE.txt ├── README.md ├── logo.xcf ├── package-lock.json ├── package.json ├── public ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico └── index.html ├── src ├── App.js ├── App.test.js ├── AuthorStats.js ├── PlotlyComponent.js ├── RatingStats.js ├── ReadingStats.js ├── Section.js ├── SortableTable.js ├── Spinner.css ├── Spinner.js ├── Statistics.js ├── StatsComponent.js ├── css │ ├── App.css │ ├── index.css │ └── tables.css ├── export_enhanced_full.csv ├── img │ ├── error_smiley.png │ └── logo.png ├── index.js ├── parseExport.js ├── registerServiceWorker.js ├── shared_plots.js └── util.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # IDE files 4 | /.idea 5 | 6 | # dependencies 7 | /node_modules 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Paul Klinger 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bookstats 2 | 3 | Shows detailed statistics about users reading habits by analyzing their exported database from [goodreads](https://goodreads.com). 4 | 5 | Additional information (e.g. re-reading dates) can be added to the export file [using an external tool](https://github.com/PaulKlinger/Enhance-GoodReads-Export) and will be used in the statistics. 6 | If the "private notes" field contains a line of the form "words: 543" (i.e. matching the regex "^words: (\d+)$") this will be used instead of the default estimate of multiplying the page count by 270. 7 | 8 | Available at https://almoturg.com/bookstats -------------------------------------------------------------------------------- /logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaulKlinger/Bookstats/8c945dca784f33201fa722de24db301db96f5c1d/logo.xcf -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bookstats", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://almoturg.com/bookstats", 6 | "dependencies": { 7 | "moment": "^2.27.0", 8 | "papaparse": "^5.3.0", 9 | "plotly.js": "^1.55.1", 10 | "react": "^15.5.4", 11 | "react-dom": "^15.5.4" 12 | }, 13 | "devDependencies": { 14 | "react-scripts": "1.0.1" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaulKlinger/Bookstats/8c945dca784f33201fa722de24db301db96f5c1d/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaulKlinger/Bookstats/8c945dca784f33201fa722de24db301db96f5c1d/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaulKlinger/Bookstats/8c945dca784f33201fa722de24db301db96f5c1d/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | Bookstats 21 | 22 | 23 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import logo from './img/logo.png'; 3 | import error_smiley from './img/error_smiley.png' 4 | import './css/App.css'; 5 | import demo_library from './export_enhanced_full.csv' 6 | 7 | import StatsComponent from "./StatsComponent"; 8 | import parseExport from "./parseExport" 9 | import Spinner from "./Spinner"; 10 | 11 | 12 | class App extends Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = {file: null, distribute_year: true, statistics: null,processing: false, error: false}; 16 | this.show_demo = this.show_demo.bind(this); 17 | this.handle_options = this.handle_options.bind(this); 18 | this.calc_statistics = this.calc_statistics.bind(this); 19 | } 20 | 21 | show_demo(e) { 22 | e.preventDefault(); 23 | this.setState({file: demo_library, statistics: null}, this.calc_statistics); 24 | } 25 | 26 | handle_options(e) { 27 | const target = e.target; 28 | if (target.name === "file") { 29 | this.setState({file: this.fileUpload.files[0], statistics: null}, this.calc_statistics); 30 | } else { 31 | const value = target.type === 'checkbox' ? target.checked : target.value; 32 | this.setState({ 33 | [target.name]: value, 34 | statistics: null 35 | }, this.calc_statistics); 36 | } 37 | } 38 | 39 | calc_statistics() { 40 | let self = this; 41 | 42 | if (this.state.file !== undefined && this.state.file !== null) { 43 | this.setState({processing: true, error: false, statistics: null}, () => { 44 | parseExport(this.state.file, {distribute_year: this.state.distribute_year}).then((statistics) => { 45 | self.setState({statistics: statistics, processing: false}); 46 | }, (reason) => { 47 | console.log(reason); 48 | self.setState({statistics: null, processing: false, error: true}) 49 | }); 50 | }); 51 | } 52 | } 53 | 54 | render() { 55 | return ( 56 |
57 |