1 Commits

Author SHA1 Message Date
Vishnu Ks
a7188c3b26 readme: Add buy me a coffee badge. 2018-03-04 14:51:56 +05:30
61 changed files with 512 additions and 65793 deletions

View File

@@ -1,7 +1,5 @@
(todo:please remove this todo and the sections you did not check below before you make your pull request.
If you are unsure, please check other PRs like this one: https://github.com/hackerkid/Mind-Expanding-Books/pull/207#issue-377268434)
## In this pull request
## In this pull request
- [ ] I am adding a new book.
- [ ] I am adding a new category
- [ ] Removing a book

1
.gitignore vendored
View File

@@ -1 +0,0 @@
.DS_Store

View File

@@ -1,24 +1,10 @@
# Contribution Guidelines
## What are the criteria for adding a new book?
The mission of this list is to curate books that can change the lives of people for the better.
Please ensure your pull request adheres to the following guidelines:
So you should add a book that has changed your life!!
Here are some questions that you can potentially ask yourself to help make this decision. If the answer is yes for most of them, feel free to create a pull request!
1. If you had the money to gift a book to every college graduate this year, would you gift this book?
2. Would you gift this book to your children at any point in their lives?
3. If there are only 3 books that you can keep a physical copy of in your life, would this book be one of them?
4. Would your life have been better off had you read this book 10 years back?
5. Would this book be relevant 1000 years from now?
## How to create a pull request?
- Search for existing books in [README.md](README.md) and make sure that you are not adding a duplicate.
- Insert the book in the following format in [README.md](README.md). Don't change any other files.
`| Book name | Author | [Goodreads rating](Goodreads url) | Year published |`
- Make sure that the book is inserted in the correct order according to the Goodreads rating.
- Mention in pull request clearly why you think the book deserves to be on the list.
- Search previous suggestions before making a new one, as yours may be a duplicate.
- if you want to add a new book you should have read the book.
- Use the following format: `| Book name | Author | [Goodreads rating](Goodreads url) | Year published |`
- Mention in pull request clearly why you think the book deserve to be in the list.
Thank you!

1028
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +0,0 @@
module.exports = {
env: {
browser: true,
es2020: true,
},
extends: [
'plugin:react/recommended',
'airbnb',
],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 11,
sourceType: 'module',
},
plugins: [
'react',
],
rules: {
},
};

72
app/.gitignore vendored
View File

@@ -1,72 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# dotenv environment variables file
.env
# gatsby files
.cache/
public
# Mac files
.DS_Store
# Yarn
yarn-error.log
.pnp/
.pnp.js
# Yarn Integrity file
.yarn-integrity
# ignore package-lock as it complicates merging
package-lock.json

View File

@@ -1,4 +0,0 @@
.cache
package.json
package-lock.json
public

View File

@@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 gatsbyjs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,39 +0,0 @@
`app/` directory contains the source code of the Mind Expanding Books [website](https://books.vishnuks.com)
## How to setup development environment
#### Verify Node Install
```
node -v
npm -v
```
#### Install Gatsby CLI
```
npm install -g gatsby-cli
```
```bash
git clone https://github.com/hackerkid/Mind-Expanding-Books
cd app/
npm install
gatsby develop
```
Once this is done, the development server should be accessible at http://localhost:8000
## High level overview of the website
- The website is made using Gatsby, which is a React based static site generator.
- The website is deployed in Netlify automatically whenever a commit is pushed to GitHub.
- When you create a pull request with changes to the source code, Netlify will automatically
create a website for previewing the changes. You can click on "Details" in the "Deploy preview ready!"
message in the pull request page for seeing the website.
## From where does the website fetches the data of the books?
Website fetches the data of the books from `app/src/data/books.json` file. See [README in utils directory](../utils/README.MD)
for details on how this file is generated.

View File

@@ -1,14 +0,0 @@
/**
* Implement Gatsby's Browser APIs in this file.
*
* See: https://www.gatsbyjs.org/docs/browser-apis/
*/
// You can delete this file if you're not using it
import React from "react"
import GlobalState from "./src/context/globalState"
export const wrapRootElement = ({ element }) => (
<GlobalState>{element}</GlobalState>
)

View File

@@ -1,46 +0,0 @@
module.exports = {
siteMetadata: {
title: `Mind Expanding Books`,
description: `Kick off your next, great Gatsby project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need.`,
author: `@gatsbyjs`,
},
plugins: [
`gatsby-plugin-react-helmet`,
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/images`,
},
},
`gatsby-transformer-sharp`,
`gatsby-plugin-sharp`,
{
resolve: `gatsby-plugin-manifest`,
options: {
name: `gatsby-starter-default`,
short_name: `starter`,
start_url: `/`,
background_color: `#663399`,
theme_color: `#663399`,
display: `minimal-ui`,
icon: `src/images/gatsby-icon.png`, // This path is relative to the root of the site.
},
},
`gatsby-plugin-offline`,
`gatsby-transformer-json`,
{
resolve: `gatsby-source-filesystem`,
options: {
path: `./src/data/`,
},
},
`gatsby-plugin-postcss`,
{
resolve: `gatsby-plugin-google-analytics`,
options: {
trackingId: "UA-139957969-2",
}
}
],
}

View File

@@ -1,49 +0,0 @@
const fs = require('fs');
const categories = JSON.parse(fs.readFileSync('src/data/categories.json'));
const slugify = require('slugify');
exports.createPages = async function ({ actions, graphql }) {
const { createPage } = actions;
await Promise.all(
categories.map(async (category) => {
const data = await graphql(
`
query categoryBooksQuery($categoryName: String) {
allBooksJson(
filter: { category: { eq: $categoryName } }
sort: { fields: [rating], order: DESC }
) {
edges {
node {
id
title
url
rating
author
year
category
image_url
description
amazon_url
}
}
}
}
`,
{ categoryName: category.name },
);
console.log(category.name, data.data);
createPage({
path: slugify(category.name),
component: require.resolve('./src/templates/categoryTemplate.js'),
context: {
categoryName: category.name,
data: data.data,
image: category.emoji,
limit: null,
},
});
}),
);
};

View File

@@ -1,14 +0,0 @@
/**
* Implement Gatsby's SSR (Server Side Rendering) APIs in this file.
*
* See: https://www.gatsbyjs.org/docs/ssr-apis/
*/
// You can delete this file if you're not using it
import React from "react"
import GlobalState from "./src/context/globalState"
export const wrapRootElement = ({ element }) => (
<GlobalState>{element}</GlobalState>
)

29267
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,69 +0,0 @@
{
"name": "gatsby-starter-default",
"private": true,
"description": "A simple starter to get up and developing quickly with Gatsby",
"version": "0.1.0",
"author": "Kyle Mathews <mathews.kyle@gmail.com>",
"dependencies": {
"bootstrap": "^4.5.2",
"gatsby": "^2.24.91",
"gatsby-image": "^2.2.27",
"gatsby-plugin-google-analytics": "^2.3.14",
"gatsby-plugin-manifest": "^2.4.37",
"gatsby-plugin-offline": "^3.0.14",
"gatsby-plugin-postcss": "^2.1.11",
"gatsby-plugin-react-helmet": "^3.1.11",
"gatsby-plugin-sharp": "^2.6.43",
"gatsby-source-filesystem": "^2.1.31",
"gatsby-transformer-json": "^2.2.13",
"gatsby-transformer-sharp": "^2.2.21",
"prop-types": "^15.7.2",
"react": "^16.10.2",
"react-bootstrap": "^1.3.0",
"react-dom": "^16.10.2",
"react-helmet": "^5.2.1",
"react-star-rating-component": "^1.4.1",
"react-star-ratings": "^2.3.0",
"slugify": "^1.4.5"
},
"devDependencies": {
"eslint": "^7.7.0",
"eslint-config-airbnb": "^18.2.0",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-react": "^7.20.6",
"eslint-plugin-react-hooks": "^4.1.0",
"husky": "^4.3.0",
"lint-staged": ">=10",
"prettier": "1.19.1",
"tailwindcss": "^1.1.2"
},
"keywords": [
"gatsby"
],
"license": "MIT",
"scripts": {
"build": "gatsby build",
"develop": "gatsby develop",
"format": "prettier --write \"**/*.{js,jsx,json,md}\"",
"start": "npm run develop",
"serve": "gatsby serve",
"test": "echo \"Write tests! -> https://gatsby.dev/unit-testing \""
},
"repository": {
"type": "git",
"url": "https://github.com/gatsbyjs/gatsby-starter-default"
},
"bugs": {
"url": "https://github.com/gatsbyjs/gatsby/issues"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,jsx,json,md}": "prettier --write",
"*.js": "eslint --cache --fix"
}
}

View File

@@ -1,3 +0,0 @@
module.exports = () => ({
plugins: [require("tailwindcss")],
})

View File

@@ -1,14 +0,0 @@
import React from "react";
import { OutboundLink } from "gatsby-plugin-google-analytics"
const getTargetURL = (book) => {
return book.amazon_url + "?tag=vishnuks-20";
}
export default ({ book }) => {
return (
<OutboundLink href={getTargetURL(book)} target="_blank" rel="noreferrer">
<img alt="Amazon link" style={{ marginBottom: "-8px" }} src="https://img.icons8.com/color/48/000000/amazon.png"/>
</OutboundLink>
)
}

View File

@@ -1,95 +0,0 @@
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import StarRatings from 'react-star-ratings';
import { Card, Row, Col } from 'react-bootstrap';
import AmazonURL from '../components/amazonurl';
import Bookmark from '../components/bookmark';
import GoodReadsImage from '../components/goodreadsimage';
const truncateContent = (content) => {
if (!content) {
return '';
}
return content.length > 350 ? content.substring(0, 350) + '...' : content;
};
const showFullText = (content) => {
if (!content) {
return '';
}
return content;
};
const BookCard = ({ book }) => {
const [ show, toggleShow ] = useState(false);
return (
<Card style={{ marginBottom: '15px' }}>
<Row>
<Col xs={6} sm={6} md={4} xl={2}>
<Card.Img
style={{
paddingLeft: '15px',
paddingRight: '15px',
paddingTop: '30px'
}}
src={book.image_url}
alt={book.title}
/>
</Col>
<Col xs={12} sm={6} md={8} xl={10}>
<Card.Body>
<Card.Title>{book.title}</Card.Title>
<Card.Subtitle className="text-muted">
<Card.Text style={{ paddingTop: '2px' }}>
{book.author} <b>{book.year ? book.year : null}</b>
</Card.Text>
<StarRatings
rating={parseFloat(book.rating)}
numberOfStars={5}
starDimension="18px"
starSpacing="1px"
starRatedColor="#fa604a"
/>
<div style={{ display: 'flex', alignItems: 'center', paddingTop: '.75rem' }}>
<div style={{ width: '30px', height: '30px', marginRight: '5px' }}>
{book.amazon_url ? <AmazonURL book={book} /> : null}
</div>
<div style={{ width: '30px', height: '30px' }}>
<a href={book.url} target="_blank">
<GoodReadsImage />
</a>
</div>
<Bookmark book={book} />
</div>
</Card.Subtitle>
<p style={{ color: 'gray', fontSize: '0.8rem', paddingTop: '1rem' }}>
{!show && truncateContent(book.description)}
{show && showFullText(book.description)}
</p>
{!show && book.description.length>350 &&(
<button className="btn btn-sm btn-primary " onClick={() => toggleShow(true)}>
Show More
</button>
)}
{show && (
<button className="btn btn-sm btn-primary " onClick={() => toggleShow(false)}>
Show Less
</button>
)}
</Card.Body>
</Col>
</Row>
</Card>
);
};
BookCard.propTypes = {
siteTitle: PropTypes.object
};
BookCard.defaultProps = {
book: {}
};
export default BookCard;

View File

@@ -1,26 +0,0 @@
import React, { useContext } from 'react'
import { Button } from "react-bootstrap"
import { BookmarkContext } from '../context/globalState'
export default ({ book }) => {
const { updateReadingList, readingList } = useContext(BookmarkContext)
const readingListIds = readingList.bookIds
return (
<div onClick={() => updateReadingList({ type: 'bookmark', retrievedBook: book })}>
<Button style={{
height: "30px",
width: "30px",
marginLeft: "0.25rem",
display: "grid",
justifyContent: "center",
alignContent: "center" }}
variant={ readingListIds.includes(book.id) ? "success" : "light"
}>
<span>
🔖
</span>
</Button>
</div>
)
}

View File

@@ -1,12 +0,0 @@
import React from "react";
export default ({categoryName, categoryImage}) => {
return (
<div className="my-2 mx-2" aria-labelledby="category-description">
<h4 id="category-description">
{categoryImage} {categoryName}
</h4>
</div>
)
}

View File

@@ -1,28 +0,0 @@
import React from 'react';
import '../styles/sidebar.css';
import BookCard from './bookcard';
import SortByDropdown, { FIELDS_TO_SORT_BY, compareFunctions } from './sortByDropdown';
export default ({ data, limit }) => {
const [sortBy, setSortBy] = React.useState(FIELDS_TO_SORT_BY[0]);
const sortedBooks = React.useMemo(() => [...data.allBooksJson.edges]
.sort(compareFunctions[sortBy.value]),
[sortBy]);
return (
<>
<SortByDropdown sortBy={sortBy.label} onSortByItemClick={setSortBy} />
{sortedBooks.map((x, index) => {
const book = x.node;
if (!limit || index < limit) {
if (!book.description || book.description.length < 10) {
return null;
}
return <BookCard book={book} key={book.id} />;
}
return null;
})}
</>
);
};

View File

@@ -1,32 +0,0 @@
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Img from "gatsby-image"
/*
* This component is built using `gatsby-image` to automatically serve optimized
* images with lazy loading and reduced file sizes. The image is loaded using a
* `useStaticQuery`, which allows us to load the image from directly within this
* component, rather than having to pass the image data down from pages.
*
* For more information, see the docs:
* - `gatsby-image`: https://gatsby.dev/gatsby-image
* - `useStaticQuery`: https://www.gatsbyjs.org/docs/use-static-query/
*/
const Image = () => {
const data = useStaticQuery(graphql`
query {
placeholderImage: file(relativePath: { eq: "goodreads.png" }) {
childImageSharp {
fluid(maxWidth: 30) {
...GatsbyImageSharpFluid
}
}
}
}
`)
return <Img fluid={data.placeholderImage.childImageSharp.fluid} />
}
export default Image

View File

@@ -1,28 +0,0 @@
import { Link } from "gatsby"
import PropTypes from "prop-types"
import React from "react"
const Header = ({ siteTitle }) => (
<header className="mx-2 bg-red d-none d-lg-block custom-header" aria-labelledby='main-title'>
<h4 className="d-flex justify-content-end" id="main-title" style={{ margin: 16 }}>
<Link
to="/"
style={{
textDecorationColor: `none`,
}}
>
{siteTitle}
</Link>
</h4>
</header>
)
Header.propTypes = {
siteTitle: PropTypes.string,
}
Header.defaultProps = {
siteTitle: ``,
}
export default Header

View File

@@ -1,32 +0,0 @@
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Img from "gatsby-image"
/*
* This component is built using `gatsby-image` to automatically serve optimized
* images with lazy loading and reduced file sizes. The image is loaded using a
* `useStaticQuery`, which allows us to load the image from directly within this
* component, rather than having to pass the image data down from pages.
*
* For more information, see the docs:
* - `gatsby-image`: https://gatsby.dev/gatsby-image
* - `useStaticQuery`: https://www.gatsbyjs.org/docs/use-static-query/
*/
const Image = () => {
const data = useStaticQuery(graphql`
query {
placeholderImage: file(relativePath: { eq: "gatsby-astronaut.png" }) {
childImageSharp {
fluid(maxWidth: 300) {
...GatsbyImageSharpFluid
}
}
}
}
`)
return <Img fluid={data.placeholderImage.childImageSharp.fluid} />
}
export default Image

View File

@@ -1,622 +0,0 @@
html {
font-family: sans-serif;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
main,
menu,
nav,
section,
summary {
display: block;
}
audio,
canvas,
progress,
video {
display: inline-block;
}
audio:not([controls]) {
display: none;
height: 0;
}
progress {
vertical-align: baseline;
}
[hidden],
template {
display: none;
}
a {
background-color: transparent;
-webkit-text-decoration-skip: objects;
}
a:active,
a:hover {
outline-width: 0;
}
abbr[title] {
border-bottom: none;
text-decoration: underline;
text-decoration: underline dotted;
}
b,
strong {
font-weight: inherit;
font-weight: bolder;
}
dfn {
font-style: italic;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
mark {
background-color: #ff0;
color: #000;
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
img {
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
figure {
margin: 1em 40px;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
button,
input,
optgroup,
select,
textarea {
font: inherit;
margin: 0;
}
optgroup {
font-weight: 700;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
[type="reset"],
[type="submit"],
button,
html [type="button"] {
-webkit-appearance: button;
}
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner,
button::-moz-focus-inner {
border-style: none;
padding: 0;
}
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring,
button:-moz-focusring {
outline: 1px dotted ButtonText;
}
fieldset {
border: 1px solid silver;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
legend {
box-sizing: border-box;
color: inherit;
display: table;
max-width: 100%;
padding: 0;
white-space: normal;
}
textarea {
overflow: auto;
}
[type="checkbox"],
[type="radio"] {
box-sizing: border-box;
padding: 0;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-input-placeholder {
color: inherit;
opacity: 0.54;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
html {
font: 112.5%/1.45em georgia, serif;
box-sizing: border-box;
overflow-y: scroll;
}
* {
box-sizing: inherit;
}
*:before {
box-sizing: inherit;
}
*:after {
box-sizing: inherit;
}
body {
color: hsla(0, 0%, 0%, 0.8);
font-family: georgia, serif;
font-weight: normal;
word-wrap: break-word;
font-kerning: normal;
-moz-font-feature-settings: "kern", "liga", "clig", "calt";
-ms-font-feature-settings: "kern", "liga", "clig", "calt";
-webkit-font-feature-settings: "kern", "liga", "clig", "calt";
font-feature-settings: "kern", "liga", "clig", "calt";
}
img {
max-width: 100%;
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
h1 {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
color: inherit;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-weight: bold;
text-rendering: optimizeLegibility;
font-size: 2.25rem;
line-height: 1.1;
}
h2 {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
color: inherit;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-weight: bold;
text-rendering: optimizeLegibility;
font-size: 1.62671rem;
line-height: 1.1;
}
h3 {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
color: inherit;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-weight: bold;
text-rendering: optimizeLegibility;
font-size: 1.38316rem;
line-height: 1.1;
}
h4 {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
color: inherit;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-weight: bold;
text-rendering: optimizeLegibility;
font-size: 1rem;
line-height: 1.1;
}
h5 {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
color: inherit;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-weight: bold;
text-rendering: optimizeLegibility;
font-size: 0.85028rem;
line-height: 1.1;
}
h6 {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
color: inherit;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-weight: bold;
text-rendering: optimizeLegibility;
font-size: 0.78405rem;
line-height: 1.1;
}
hgroup {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
ul {
margin-left: 1.45rem;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
list-style-position: outside;
list-style-image: none;
}
ol {
margin-left: 1.45rem;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
list-style-position: outside;
list-style-image: none;
}
dl {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
dd {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
p {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
figure {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
pre {
margin-left: 0;
margin-right: 0;
margin-top: 0;
margin-bottom: 1.45rem;
font-size: 0.85rem;
line-height: 1.42;
background: hsla(0, 0%, 0%, 0.04);
border-radius: 3px;
overflow: auto;
word-wrap: normal;
padding: 1.45rem;
}
table {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
font-size: 1rem;
line-height: 1.45rem;
border-collapse: collapse;
width: 100%;
}
fieldset {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
blockquote {
margin-left: 1.45rem;
margin-right: 1.45rem;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
form {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
noscript {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
iframe {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
hr {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: calc(1.45rem - 1px);
background: hsla(0, 0%, 0%, 0.2);
border: none;
height: 1px;
}
address {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
b {
font-weight: bold;
}
strong {
font-weight: bold;
}
dt {
font-weight: bold;
}
th {
font-weight: bold;
}
li {
margin-bottom: calc(1.45rem / 2);
}
ol li {
padding-left: 0;
}
ul li {
padding-left: 0;
}
li > ol {
margin-left: 1.45rem;
margin-bottom: calc(1.45rem / 2);
margin-top: calc(1.45rem / 2);
}
li > ul {
margin-left: 1.45rem;
margin-bottom: calc(1.45rem / 2);
margin-top: calc(1.45rem / 2);
}
blockquote *:last-child {
margin-bottom: 0;
}
li *:last-child {
margin-bottom: 0;
}
p *:last-child {
margin-bottom: 0;
}
li > p {
margin-bottom: calc(1.45rem / 2);
}
code {
font-size: 0.85rem;
line-height: 1.45rem;
}
kbd {
font-size: 0.85rem;
line-height: 1.45rem;
}
samp {
font-size: 0.85rem;
line-height: 1.45rem;
}
abbr {
border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
cursor: help;
}
acronym {
border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
cursor: help;
}
abbr[title] {
border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
cursor: help;
text-decoration: none;
}
thead {
text-align: left;
}
td,
th {
text-align: left;
border-bottom: 1px solid hsla(0, 0%, 0%, 0.12);
font-feature-settings: "tnum";
-moz-font-feature-settings: "tnum";
-ms-font-feature-settings: "tnum";
-webkit-font-feature-settings: "tnum";
padding-left: 0.96667rem;
padding-right: 0.96667rem;
padding-top: 0.725rem;
padding-bottom: calc(0.725rem - 1px);
}
th:first-child,
td:first-child {
padding-left: 0;
}
th:last-child,
td:last-child {
padding-right: 0;
}
tt,
code {
background-color: hsla(0, 0%, 0%, 0.04);
border-radius: 3px;
font-family: "SFMono-Regular", Consolas, "Roboto Mono", "Droid Sans Mono",
"Liberation Mono", Menlo, Courier, monospace;
padding: 0;
padding-top: 0.2em;
padding-bottom: 0.2em;
}
pre code {
background: none;
line-height: 1.42;
}
code:before,
code:after,
tt:before,
tt:after {
letter-spacing: -0.2em;
content: " ";
}
pre code:before,
pre code:after,
pre tt:before,
pre tt:after {
content: "";
}
@media only screen and (max-width: 480px) {
html {
font-size: 100%;
}
}

View File

@@ -1,47 +0,0 @@
/**
* Layout component that queries for data
* with Gatsby's useStaticQuery component
*
* See: https://www.gatsbyjs.org/docs/use-static-query/
*/
import React from "react"
import PropTypes from "prop-types"
import { useStaticQuery, graphql } from "gatsby"
import Header from "./header"
import "./layout.css"
import "bootstrap/dist/css/bootstrap.min.css"
import { Container } from "react-bootstrap"
const Layout = ({ children }) => {
const data = useStaticQuery(graphql`
query SiteTitleQuery {
site {
siteMetadata {
title
}
}
}
`)
return (
<>
<Header siteTitle={data.site.siteMetadata.title} />
<Container fluid>
<main>{children}</main>
<footer class ="footer-text">
© {new Date().getFullYear()}, Built with
{` `}
<a href="https://www.gatsbyjs.org">Gatsby</a>
</footer>
</Container>
</>
)
}
Layout.propTypes = {
children: PropTypes.node.isRequired,
}
export default Layout

View File

@@ -1,88 +0,0 @@
/**
* SEO component that queries for data with
* Gatsby's useStaticQuery React hook
*
* See: https://www.gatsbyjs.org/docs/use-static-query/
*/
import React from "react"
import PropTypes from "prop-types"
import Helmet from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"
function SEO({ description, lang, meta, title }) {
const { site } = useStaticQuery(
graphql`
query {
site {
siteMetadata {
title
description
author
}
}
}
`
)
const metaDescription = description || site.siteMetadata.description
return (
<Helmet
htmlAttributes={{
lang,
}}
title={title}
titleTemplate={`%s | ${site.siteMetadata.title}`}
meta={[
{
name: `description`,
content: metaDescription,
},
{
property: `og:title`,
content: title,
},
{
property: `og:description`,
content: metaDescription,
},
{
property: `og:type`,
content: `website`,
},
{
name: `twitter:card`,
content: `summary`,
},
{
name: `twitter:creator`,
content: site.siteMetadata.author,
},
{
name: `twitter:title`,
content: title,
},
{
name: `twitter:description`,
content: metaDescription,
},
].concat(meta)}
/>
)
}
SEO.defaultProps = {
lang: `en`,
meta: [],
description: ``,
}
SEO.propTypes = {
description: PropTypes.string,
lang: PropTypes.string,
meta: PropTypes.arrayOf(PropTypes.object),
title: PropTypes.string.isRequired,
}
export default SEO

View File

@@ -1,49 +0,0 @@
import React, { useContext } from "react"
import { Navbar, Nav } from "react-bootstrap"
import { StaticQuery, graphql, Link } from "gatsby"
import "../styles/sidebar.css"
import { BookmarkContext } from '../context/globalState'
var slugify = require('slugify')
export default () => {
const { readingList } = useContext(BookmarkContext)
return (
<StaticQuery
query={graphql`
query CategoryQuery {
allCategoriesJson {
edges {
node {
id
name
emoji
}
}
}
}
`}
render={data => (
<Navbar className="sidebar-sticky" collapseOnSelect expand="lg" bg="ligt" variant="light">
<Navbar.Toggle aria-controls="responsive-navbar-nav" />
<Navbar.Collapse>
<div>
<div style={{position: "relative", left: "0.9rem", paddingBottom: "0.2rem"}}>
<Link to="/readingList">🔖 Reading List ({readingList.bookIds.length})</Link>
</div>
{data.allCategoriesJson.edges.map(function(x, index) {
return (
<Nav.Item key={index}>
<Nav.Link href={slugify(x.node.name)}>
{x.node.emoji} {x.node.name}
</Nav.Link>
</Nav.Item>
)
})}
</div>
</Navbar.Collapse>
</Navbar>
)}
/>
)
}

View File

@@ -1,40 +0,0 @@
import React from 'react';
import { Dropdown } from 'react-bootstrap';
export const compareFunctions = {
title_a_z: ({ node: bookOne }, { node: bookTwo }) => bookOne.title.localeCompare(bookTwo.title),
title_z_a: ({ node: bookOne }, { node: bookTwo }) => bookTwo.title.localeCompare(bookOne.title),
year_descending: ({ node: bookOne }, { node: bookTwo }) => Number(bookTwo.year) - Number(bookOne.year),
year_ascending: ({ node: bookOne }, { node: bookTwo }) => Number(bookOne.year) - Number(bookTwo.year),
rating_descending: ({ node: bookOne }, { node: bookTwo }) => Number(bookTwo.rating) - Number(bookOne.rating),
rating_ascending: ({ node: bookOne }, { node: bookTwo }) => Number(bookOne.rating) - Number(bookTwo.rating),
};
export const FIELDS_TO_SORT_BY = [
{ label: 'Rating, high to low', value: 'rating_descending' },
{ label: 'Rating, low to high', value: 'rating_ascending' },
{ label: 'Publication year, new to old', value: 'year_descending' },
{ label: 'Publication year, old to new', value: 'year_ascending' },
{ label: 'Title, A-Z', value: 'title_a_z' },
{ label: 'Title, Z-A', value: 'title_z_a' },
];
export default ({ sortBy, onSortByItemClick }) => (
<div className="mb-2">
<Dropdown>
<Dropdown.Toggle variant="outline">
Sort By:
{' '}
{sortBy}
</Dropdown.Toggle>
<Dropdown.Menu>
{FIELDS_TO_SORT_BY.map((field, index) => (
<Dropdown.Item key={index} onClick={() => onSortByItemClick(field)}>
{field.label}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
</div>
);

View File

@@ -1,33 +0,0 @@
export default function bookReducer(state, action) {
let readingListCopy = {...state}
switch (action.type) {
case 'init': {
if (action.content) {
return action.content
}
return readingListCopy
}
case 'bookmark': {
let { bookIds, books } = readingListCopy
const { retrievedBook } = action
const retrievedBookId = retrievedBook.id
// Delete existing bookmark
if (bookIds.includes(retrievedBookId)) {
readingListCopy.bookIds = bookIds.filter(id => id !== retrievedBookId)
delete books[retrievedBookId]
if (typeof window !== undefined) {
localStorage.setItem('Bookmarks', JSON.stringify(readingListCopy))
}
// Add new bookmark
} else {
books[retrievedBookId] = retrievedBook
bookIds.push(retrievedBookId)
if (typeof window !== undefined) {
localStorage.setItem('Bookmarks', JSON.stringify(readingListCopy))
}
}
return readingListCopy
}
}
}

View File

@@ -1,25 +0,0 @@
import React, { useReducer, useEffect } from 'react'
import bookReducer from './bookReducer'
export const BookmarkContext = React.createContext()
export default function GlobalState({children}) {
let [readingList, updateReadingList] = useReducer(bookReducer, {
books: {},
bookIds: []
})
useEffect(() => {
if (typeof window !== undefined) {
const retrievedBooks = JSON.parse(localStorage.getItem('Bookmarks'))
console.log(retrievedBooks)
updateReadingList({type: 'init', content: retrievedBooks})
}
}, [])
return (
<BookmarkContext.Provider value={{readingList, updateReadingList}}>
{children}
</BookmarkContext.Provider>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,74 +0,0 @@
[
{
"name": "Startups and Business",
"emoji": "🚀"
},
{
"name": "Philosophy And Psychology",
"emoji": "☯️"
},
{
"name": "Autobiographies and Biographies",
"emoji": "👩🏾"
},
{
"name": "History",
"emoji": "📜"
},
{
"name": "Science and Medicine",
"emoji": "🔬"
},
{
"name": "Logic and Problem Solving",
"emoji": "🧩"
},
{
"name": "Politics",
"emoji": "🗳️"
},
{
"name": "Economics",
"emoji": "📈"
},
{
"name": "Gender",
"emoji": "🏳️‍🌈"
},
{
"name": "Sexuality",
"emoji": "😘"
},
{
"name": "Education",
"emoji": "🏫"
},
{
"name": "Writing",
"emoji": "📝"
},
{
"name": "Theater and Film",
"emoji": "🎬"
},
{
"name": "Health",
"emoji": "👩‍⚕️"
},
{
"name": "Travel",
"emoji": "🛩️"
},
{
"name": "Language",
"emoji": "🉐"
},
{
"name": "Nature",
"emoji": "🌲"
},
{
"name": "Art",
"emoji": "🖌️"
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,14 +0,0 @@
import React from "react"
import Layout from "../components/layout"
import SEO from "../components/seo"
const NotFoundPage = () => (
<Layout>
<SEO title="404: Not found" />
<h1 id="title">NOT FOUND</h1>
<p>You just hit a route that doesn&#39;t exist... the sadness.</p>
</Layout>
)
export default NotFoundPage

View File

@@ -1,79 +0,0 @@
import React, { useState, useEffect } from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"
import SEO from "../components/seo"
import SideBar from "../components/sidebar"
import { Container, Row, Col, Navbar } from "react-bootstrap"
import BookFeed from "../components/feed"
function myFunction(setMaximumBooksToShow, maximumBooksToShow) {
if (
document.documentElement.clientHeight +
document.documentElement.scrollTop >=
document.documentElement.scrollHeight
) {
setMaximumBooksToShow(maximumBooksToShow + 12)
}
}
export default ({ data }) => {
let [maximumBooksToShow, setMaximumBooksToShow] = useState(12)
useEffect(() => {
window.document.onscroll = () =>
myFunction(setMaximumBooksToShow, maximumBooksToShow)
})
return (
<Layout>
<SEO title="Home" />
<Container fluid>
<Row>
<Col lg={2}>
<SideBar />
</Col>
<Col lg={10}>
<BookFeed data={data} limit={maximumBooksToShow} />
</Col>
</Row>
<Row>
{/* <footer style={{marginLeft: 150,
width: `100%`,
position: `fixed`,
bottom: 0}}>
© {new Date().getFullYear()}, Built with
{` `}
<a href="https://www.gatsbyjs.org">Gatsby</a>
</footer> */}
</Row>
</Container>
</Layout>
)
}
export const query = graphql`
query MyQuery {
allBooksJson(
sort: {
fields: [rating]
order: DESC
}
) {
edges {
node {
id
title
url
rating
author
year
category
description
image_url
amazon_url
}
}
}
}
`

View File

@@ -1,16 +0,0 @@
import React from "react"
import { Link } from "gatsby"
import Layout from "../components/layout"
import SEO from "../components/seo"
const SecondPage = () => (
<Layout>
<SEO title="Page two" />
<h1>Hi from the second page</h1>
<p>Welcome to page 2</p>
<Link to="/">Go back to the homepage</Link>
</Layout>
)
export default SecondPage

View File

@@ -1,36 +0,0 @@
import React, { useContext } from "react"
import { Link } from "gatsby"
import { Container, Row, Col } from "react-bootstrap"
import SideBar from "../components/sidebar"
import Layout from "../components/layout"
import SEO from "../components/seo"
import Bookcard from "../components/bookcard"
import { BookmarkContext } from "../context/globalState"
const ReadingList = () => {
const { readingList } = useContext(BookmarkContext)
return (
<Layout>
<SEO title="Reading list" />
<Container fluid>
<Row>
<Col xs={2}>
<SideBar />
</Col>
<Col>
<h2>Your reading list</h2>
<Link to="/">Go back to the homepage</Link>
{
readingList.bookIds.map(bookId => {
return <Bookcard book={readingList.books[bookId]} key={bookId} />
})
}
</Col>
</Row>
</Container>
<p>Reading List</p>
</Layout>
)}
export default ReadingList

View File

@@ -1,11 +0,0 @@
.custom-header{
position: fixed;
}
.footer-text {
text-align:center;
padding-bottom:1 rem;
}
.dark-mode {
background-color: #3c4759;
}

View File

@@ -1,38 +0,0 @@
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
min-height: 100vh !important;
margin: 70px 0 0;
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
width: max-content;
}
.nav-link {
font-size: 0.9rem;
}
#sidebar-wrapper {
min-height: 100vh !important;
width: 100vw;
margin-left: -1rem;
-webkit-transition: margin 0.25s ease-out;
-moz-transition: margin 0.25s ease-out;
-o-transition: margin 0.25s ease-out;
transition: margin 0.25s ease-out;
}
#sidebar-wrapper .sidebar-heading {
padding: 0.875rem 1.25rem;
font-size: 1.2rem;
}
#page-content-wrapper {
min-width: 0;
width: 100%;
}
.navbar {
padding: 10px 0px;
}
.navbar-toggler {
margin-bottom: 1.2rem;
}

View File

@@ -1,32 +0,0 @@
import React, { useState, useEffect } from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"
import SEO from "../components/seo"
import SideBar from "../components/sidebar"
import CategoryDescription from "../components/categorydescription"
import { Container, Row, Col } from "react-bootstrap"
import BookFeed from "../components/feed"
const basicTemplate = props => {
const { pageContext } = props
const { categoryName, data, image } = pageContext
return (
<Layout>
<SEO title="Home" />
<Container fluid>
<Row>
<Col lg={2}>
<SideBar />
</Col>
<Col lg={10}>
<CategoryDescription categoryName={categoryName} categoryImage={image} />
<BookFeed data={data} categoryName={categoryName} />
</Col>
</Row>
</Container>
</Layout>
)
}
export default basicTemplate

File diff suppressed because it is too large Load Diff

3
package-lock.json generated
View File

@@ -1,3 +0,0 @@
{
"lockfileVersion": 1
}

7
utils/.gitignore vendored
View File

@@ -1,8 +1,3 @@
.idea
__pycache__
config.py
bin/
include/
lib/
out.json
pip-selfcheck.json
config.py

View File

@@ -1,34 +0,0 @@
This contains documentation on `housekeep.py` which was a command developed for converting from legacy README
format to the new format. This is not used anymore.
## Getting started
### Setup
1) Copy ```config-sample.py``` to ```config.py```
```bash
cp config-sample.py config.py
```
2) Get a GoodReads API key [here](https://www.goodreads.com/api/keys)
3) Copy your public key to the ```config.py``` file
### Converting
Run
```bash
python3 housekeep.py --file_type='old'
```
The ``--file_type='old'`` flag is needed if the books are displayed in the old format (in a list). New format is the default (when the records are displayed in tables).
This will convert to the new format and save it to ``README-new.md``.
**Note!** the first conversion can take some time, we wait 1 second between each request to GoodReads to not abuse the API.
After the first generation only missing records will be tried to be retrieved.
Run to see the available arguments
```bash
python3 housekeep.py --help
```

View File

@@ -1,37 +1,34 @@
`utils/` mainly contains scripts for generating `app/src/data/books.json` file from `README.md`
# Utils for Mind-Expanding-Books
In most cases, you don't want to run the scripts in this directory or make changes to it.
The only time you want to mess with the scripts is when you want to fetch some extra data from `API` or
want to update the `app/src/data/books.json` file to include a newly added book or category.
Simple command line interface to extend and order the Mind-Expanding-Books list.
## Generating `app/src/data/books.json`
## Getting started
The website shows name of the book, year, rating, cover, amazon link, etc in book card. Some of the
data like name, year, rating etc is present in the [main README.md](../README.md). Other details
like cover photo, amazon link etc is fetched from various APIs.
The script that fetches all these extra data lives in `utils/update_json_files.py`
The script goes through all the books in [main README.md](../README.md) and starts fetching the extra details
from `Goodreads` and `Google Search` API. And the result is stored in `utils/books.json` and `utils/book_name_to_details.json`
(used for caching only).
Once the script completes the run, the `utils/books.json` file is copied to `app/src/data/books.json` **manually**.
And the website uses the data from `app/src/data/books.json` for generating the pages.
## Install packages for script
### Setup
1) Copy ```config-sample.py``` to ```config.py```
```bash
cd utils
virtualenv .
source bin/activate
pip install -r requirements.txt
cp config-sample.py config.py
```
2) Get a GoodReads API key [here](https://www.goodreads.com/api/keys)
3) Copy your public key to the ```config.py``` file
### Converting
Run
```bash
python3 housekeep.py --file_type='old'
```
## Executing the script
The ``--file_type='old'`` flag is needed if the books are displayed in the old format (in a list). New format is the default (when the records are displayed in tables).
- Register at [goodreads](https://www.goodreads.com)
- Apply for a developer api [here](https://www.goodreads.com/api)
- Copy utils/config-sample.py as utils/config.py
- Fill in the API Key credentials
This will convert to the new format and save it to ``README-new.md``.
**Note!** the first conversion can take some time, we wait 1 second between each request to GoodReads to not abuse the API.
After the first generation only missing records will be tried to be retrieved.
Run to see the available arguments
```bash
python3 housekeep.py --help
```

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,2 @@
# save this file as 'config.py' and then, fill it with you api key
GOODREADS_PUBLIC_API_KEY = "write here your goodreads public API key"
GOOGLE_SEARCH_RAPIDAPI_HOST = ""
GOOGLE_SEARCH_RAPIDAPI_KEY = ""
GOOGLE_BOOK_API_KEY = ""
GOODREADS_PUBLIC_API_KEY = 'write here your goodreads public API key'

View File

@@ -3,105 +3,51 @@ import xml.etree.ElementTree as ET
import urllib.request
import urllib.error
import requests
from config import GOODREADS_PUBLIC_API_KEY
from bs4 import BeautifulSoup
from config import GOODREADS_PUBLIC_API_KEY, GOOGLE_SEARCH_RAPIDAPI_HOST, GOOGLE_SEARCH_RAPIDAPI_KEY, GOOGLE_BOOK_API_KEY
from googlesearch import search
def get_details(book_object):
url = "http://www.goodreads.com/book/title.xml?key={}&title={}".format(
GOODREADS_PUBLIC_API_KEY, urllib.parse.quote_plus(book_object["title"])
)
print(url)
url = "http://www.goodreads.com/book/title.xml?key={}&title={}".format(GOODREADS_PUBLIC_API_KEY,
urllib.parse.quote_plus(book_object['title']))
try:
time_to_sleep = 1
while True:
response = urllib.request.urlopen(url)
print(response.getcode())
if response.getcode() == 429:
time_to_sleep = time_to_sleep * 2
print("Sleeping for {}".format(time_to_sleep))
time.sleep(time_to_sleep)
else:
break
tree = ET.ElementTree(file=response)
tree = ET.ElementTree(file=urllib.request.urlopen(url))
root = tree.getroot()
book = root.find("book")
book_object["year"] = book.find("publication_year").text or ""
book_object["lang"] = book.find("language_code").text
book_object["rating"] = book.find("average_rating").text
book_object["pages"] = book.find("num_pages").text
book_object["image_url"] = book.find("image_url").text
book_object["isbn"] = book.find("isbn").text
description = book.find("description").text
if description:
book_object["description"] = BeautifulSoup(description).text
else:
book_object["description"] = ""
if GOOGLE_BOOK_API_KEY.strip(" "):
# Attempt to use Google Book API
url = "https://www.googleapis.com/books/v1/volumes?q={}+inauthor:{}&key={}".format(
book_object["title"], book_object["author"], GOOGLE_BOOK_API_KEY,
)
response = requests.request("GET", url)
for item in response.json()["items"]:
if "description" in item["volumeInfo"]:
book_object["description"] = item["volumeInfo"]["description"]
break
print("Fetching amazon link")
url = "https://google-search3.p.rapidapi.com/api/v1/search/q=site:amazon.com {} {}".format(book_object["title"], book_object["author"])
headers = {
'x-rapidapi-host': GOOGLE_SEARCH_RAPIDAPI_HOST,
'x-rapidapi-key': GOOGLE_SEARCH_RAPIDAPI_KEY,
}
response = requests.request("GET", url, headers=headers)
book_object["amazon_url"] = response.json()["results"][0]["link"]
return True
book = root.find('book')
book_object['year'] = book.find('publication_year').text or ''
book_object['lang'] = book.find('language_code').text
book_object['rating'] = book.find('average_rating').text
book_object['pages'] = book.find('num_pages').text
except urllib.error.HTTPError as e:
print(
"Error getting book details from GoodReads for book: {}. \nGot error: ".format(
book_object["title"]
)
)
print(str(e.getcode()) + " " + e.msg)
return False
print('Error getting book details from GoodReads for book: {}. \nGot error: '.format(book_object['title']))
print(str(e.getcode()) + ' ' + e.msg)
def get_goodread_info(library, force):
def get_goodread_info(library):
import sys
print("")
print("Getting GoodReads data...")
print('')
print('Getting GoodReads data...')
processed = 0
total_book_count = 0
for key in library:
total_book_count += len(library[key])
for category in library:
book_list = library[category]
for chapter in library:
book_list = library[chapter]
for book in book_list:
# do not call the api again if we already have the infomation
if not force and "rating" in book and book["rating"]:
if 'rating' in book and book['rating']:
processed += 1
continue
get_details(book)
processed += 1
print(
"{}/{} records processed.".format(processed, total_book_count), end="\b"
)
sys.stdout.write("\r")
print('{}/{} records processed.'.format(processed, total_book_count), end="\b")
sys.stdout.write('\r')
sys.stdout.flush() # <- makes python print it anyway
# need to wait a second between the requests, to not abuse the API
time.sleep(1)
time.sleep(1)

View File

@@ -1,5 +1,3 @@
import simplejson
# we assume that every line after # Books
# starting with * is a book title if file type is old
# starting with | (and not with | Name or |--) is a book if the file type is new
@@ -7,71 +5,51 @@ import simplejson
# ARGUMENT HANDLING
try:
import argparse
parser = argparse.ArgumentParser(description="Process file.")
parser.add_argument("--in_file", help="File to process, defaults to ./../README.MD")
parser = argparse.ArgumentParser(description='Process file.')
parser.add_argument(
"--out_file", help="File to save to, defaults to ./../README-NEW.MD"
)
'--in_file',
help='File to process, defaults to ./../README.MD')
parser.add_argument(
"--input_file_type",
choices=["old", "new"],
help="old if links are displayed in a list, new if in a table",
)
'--out_file',
help='File to save to, defaults to ./../README-NEW.MD')
parser.add_argument(
"--sort_by",
choices=["rating", "title", "author", "year"],
help="defaults to rating",
)
parser.add_argument("--force", dest="force", action="store_true", default=False)
'--file_type',
choices=['old', 'new'],
help='old if links are displayed in a list, new if in a table')
parser.add_argument(
"--store-json", dest="store_json", action="store_true", default=False
)
'--sort_by',
choices = ['rating', 'title', 'author', 'year'],
help='defaults to rating')
flags = parser.parse_args()
except ImportError:
flags = None
def sort(library, key_to_sort_on, reverse=False):
def sort(library, key_to_sort_on, reverse = False):
new_library = {}
for key in library:
books = library[key]
new_library[key] = sorted(
books, key=lambda k: k[key_to_sort_on], reverse=reverse
)
new_library[key] = sorted(books, key=lambda k: k[key_to_sort_on], reverse=reverse)
return new_library
def format_library(library):
formated_library = []
for category in library:
for book in library[category]:
book["category"] = category[len("## ") :]
formated_library.append(book)
return formated_library
def main():
from read_file import load
from gooodreads import get_goodread_info
from write_file import render
in_file = flags.in_file or "./../README.md"
out_file = flags.out_file or "./../README-new.md"
input_file_type = flags.input_file_type or "new"
sort_by = flags.sort_by or "rating"
force = flags.force
store_json = flags.store_json
reverse = True if sort_by == "rating" else False
in_file = flags.in_file or './../README.MD'
out_file = flags.out_file or './../README-new.md'
file_type = flags.file_type or 'new'
sort_by = flags.sort_by or 'rating'
reverse = True if sort_by == 'rating' else False
library = load(in_file, input_file_type)
get_goodread_info(library, force)
library = load(in_file, file_type)
get_goodread_info(library)
library = sort(library, sort_by, reverse)
render(in_file, out_file, library)
if store_json:
with open("out.json", "w") as f:
f.write(simplejson.dumps(format_library(library), indent=4, sort_keys=True))
if __name__ == "__main__":
if __name__ == '__main__':
main()

View File

@@ -1,8 +0,0 @@
home = /usr
implementation = CPython
version_info = 3.8.5.final.0
virtualenv = 20.0.20
include-system-site-packages = false
base-prefix = /usr
base-exec-prefix = /usr
base-executable = /usr/bin/python3

View File

@@ -8,24 +8,24 @@ def read_file_content(file):
# old (list)
def parse_book_string(book_string):
book = {}
book["title"] = book_string.split("[")[1].split("]")[0]
book["url"] = book_string.split("]")[1].split("(")[1].split(")")[0]
book["author"] = book_string.split(" by ")[-1]
book["rating"] = ""
book["year"] = ""
book['title'] = book_string.split('[')[1].split(']')[0]
book['url'] = book_string.split(']')[1].split('(')[1].split(')')[0]
book['author'] = book_string.split(' by ')[-1]
book['rating'] = ''
book['year'] = ''
return book
# new (table)
def parse_book_string_new(book_string):
book = {}
book_split = book_string.split("|")
book_split = book_string.split('|')
# print(book_split)
book["title"] = book_split[1].strip()
book["author"] = book_split[2].strip()
book["url"] = book_split[3].strip().split("[")[1].split("(")[1].split(")")[0]
book["rating"] = book_split[3].strip().split("[")[1].split("]")[0]
book["year"] = book_split[4].strip()
book['title'] = book_split[1].strip()
book['author'] = book_split[2].strip()
book['url'] = book_split[3].strip().split('[')[1].split('(')[1].split(')')[0]
book['rating'] = book_split[3].strip().split('[')[1].split(']')[0]
book['year'] = book_split[4].strip()
return book
@@ -33,8 +33,8 @@ def load(file, file_type):
file = read_file_content(file)
# we start one line after tilte # Books
line_to_start = file.index("# Books") + 1
current_title = ""
line_to_start = file.index('# Books') + 1
current_title = ''
books_under_current_title = []
library = {}
@@ -42,7 +42,7 @@ def load(file, file_type):
line = file[i]
# we have a title
if line.startswith("##"):
if line.startswith('##'):
if len(current_title) == 0:
current_title = line
else:
@@ -52,16 +52,12 @@ def load(file, file_type):
continue
# we have a book
if file_type == "old":
if line.startswith("*"):
if file_type == 'old':
if line.startswith('*'):
book = parse_book_string(line)
books_under_current_title.append(book)
else:
if (
line.startswith("|")
and not line.startswith("| Name")
and not line.startswith("|---")
):
if line.startswith('|') and not line.startswith('| Name') and not line.startswith('|---'):
book = parse_book_string_new(line)
books_under_current_title.append(book)

View File

@@ -1,26 +0,0 @@
appdirs==1.4.4
attrs==20.1.0
black==19.10b0
click==7.1.2
pathspec==0.8.0
regex==2020.7.14
toml==0.10.1
typed-ast==1.4.1
amzsear==2.0.1
appdirs==1.4.4
attrs==20.1.0
beautifulsoup4==4.9.1
black==19.10b0
bs4==0.0.1
certifi==2020.6.20
chardet==3.0.4
click==7.1.2
google==3.0.0
idna==2.10
pathspec==0.8.0
regex==2020.7.14
requests==2.24.0
soupsieve==2.0.1
toml==0.10.1
typed-ast==1.4.1
urllib3==1.25.10

View File

@@ -1,94 +0,0 @@
import json
import time
from read_file import load
from gooodreads import get_details
from bs4 import BeautifulSoup
required_fields = [
"title",
"author",
"url",
"rating",
"year",
"pages",
"image_url",
"description",
"category",
"amazon_url",
]
def book_has_all_fields(book):
for required_field in required_fields:
if required_field not in existing_book:
print(f"Missing {required_field}")
return False
return True
def clean_category(category_raw):
if "### " in category_raw:
return category_raw[4:]
if "## " in category_raw:
return category_raw[3:]
def validate_bookcover(book_details):
"""
Check if goodreads returns a nophoto
Use open library to fetch the book cover
based on ISBN
Args:
book_details: Book info returned as json by goodreads API
Returns:
This API checks for book cover, and returns with a valid
bookcover if nophoto found on goodreads, using openlibrary
"""
no_photo_url='https://s.gr-assets.com/assets/nophoto/book/'
open_library_url='http://covers.openlibrary.org/b/isbn/{isbn}-M.jpg'
if (book_details['image_url'].__contains__(no_photo_url)):
book_details['image_url'] = open_library_url.format(isbn=book_details['isbn'])
return book_details
if __name__ == "__main__":
library = load("../README.md", "new")
existing_book_names_to_details = json.load(open("book_name_to_details.json"))
for category in library:
category_name = clean_category(category)
for book in library[category]:
if (title := book["title"]) in existing_book_names_to_details:
existing_book = existing_book_names_to_details[title]
if book_has_all_fields(existing_book):
print(f"🆗 {title}")
continue
new_book = {
"title": title,
"author": book["author"],
"url": book["url"],
"category": category_name,
}
fetched = get_details(new_book)
if fetched:
print(f"{title}")
new_book = validate_bookcover(new_book)
existing_book_names_to_details[title] = new_book
with open("book_name_to_details.json", "w") as f:
json.dump(
existing_book_names_to_details,
f,
sort_keys=True,
indent=4,
separators=(",", ": "),
)
book_list = []
for _, book in existing_book_names_to_details.items():
book_list.append(book)
with open("books.json", "w") as f:
json.dump(book_list, f, sort_keys=True, indent=4, separators=(",", ": "))
else:
print(f"❌ Error while fetching {title}")

View File

@@ -4,12 +4,14 @@ import os
def render_book_line(book_object):
book = book_object
book["rating"] = "?" if not "rating" in book else book["rating"]
book["url"] = "" if not "url" in book else book["url"]
book["year"] = "" if not "year" in book else book["year"]
return "| {} | {} | [{}]({}) | {} | \n".format(
book["title"], book["author"], book["rating"], book["url"], book["year"]
)
book['rating'] = '?' if not 'rating' in book else book['rating']
book['url'] = '' if not 'url' in book else book['url']
book['year'] = '' if not 'year' in book else book['year']
return '| {} | {} | [{}]({}) | {} | \n'.format(book['title'],
book['author'],
book['rating'],
book['url'],
book['year'])
# TODO: refine this logic
@@ -19,37 +21,32 @@ def render(in_file, out_file, library):
savig the new file to tmp_file location, the copying it to out-file and deleting tmp_file
this is done to prevent issues if the in and the out file are the same
"""
tmp_file = "./.tmp-file.md"
open(tmp_file, "a").close()
tmp_file = './.tmp-file.md'
open(tmp_file, 'a').close()
books_not_reached = True
with open(tmp_file, "w") as out_file_tmp:
with open(tmp_file, 'w') as out_file_tmp:
with open(in_file) as original_file:
for line in original_file:
if line.strip() in library:
if not books_not_reached:
out_file_tmp.write("\n")
if not books_not_reached: out_file_tmp.write('\n')
books_not_reached = False
# render chapter and start of the table
out_file_tmp.write(line)
if len(library[line.strip()]) > 0:
out_file_tmp.write(
"| Name | Author | Goodreads Rating | Year Published | \n"
)
out_file_tmp.write(
"|------|--------|------------------|----------------| \n"
)
out_file_tmp.write('| Name | Author | Goodreads Rating | Year Published | \n')
out_file_tmp.write('|------|--------|------------------|----------------| \n')
# render books
for book in library[line.strip()]:
out_file_tmp.write(render_book_line(book))
elif books_not_reached:
out_file_tmp.write(line)
elif line.startswith("## License"):
out_file_tmp.write("\n")
out_file_tmp.write("\n")
elif line.startswith('## License'):
out_file_tmp.write('\n')
out_file_tmp.write('\n')
out_file_tmp.write(line)
books_not_reached = True
copyfile(tmp_file, out_file)
os.remove(tmp_file)
os.remove(tmp_file)