Compare commits
1 Commits
master
...
coffee-bra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7188c3b26 |
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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 book.
|
||||||
- [ ] I am adding a new category
|
- [ ] I am adding a new category
|
||||||
- [ ] Removing a book
|
- [ ] Removing a book
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
@@ -1,24 +1,10 @@
|
|||||||
# Contribution Guidelines
|
# Contribution Guidelines
|
||||||
|
|
||||||
## What are the criteria for adding a new book?
|
Please ensure your pull request adheres to the following guidelines:
|
||||||
The mission of this list is to curate books that can change the lives of people for the better.
|
|
||||||
|
|
||||||
So you should add a book that has changed your life!!
|
- 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.
|
||||||
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!
|
- 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.
|
||||||
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.
|
|
||||||
|
|
||||||
Thank you!
|
Thank you!
|
||||||
|
|||||||
@@ -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
72
app/.gitignore
vendored
@@ -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
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
.cache
|
|
||||||
package.json
|
|
||||||
package-lock.json
|
|
||||||
public
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"endOfLine": "lf",
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": false,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "es5"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
22
app/LICENSE
22
app/LICENSE
@@ -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.
|
|
||||||
|
|
||||||
@@ -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.
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
@@ -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",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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
29267
app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module.exports = () => ({
|
|
||||||
plugins: [require("tailwindcss")],
|
|
||||||
})
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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 |
@@ -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't exist... the sadness.</p>
|
|
||||||
</Layout>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default NotFoundPage
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
.custom-header{
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
.footer-text {
|
|
||||||
text-align:center;
|
|
||||||
padding-bottom:1 rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-mode {
|
|
||||||
background-color: #3c4759;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
16489
app/yarn.lock
16489
app/yarn.lock
File diff suppressed because it is too large
Load Diff
3
package-lock.json
generated
3
package-lock.json
generated
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"lockfileVersion": 1
|
|
||||||
}
|
|
||||||
7
utils/.gitignore
vendored
7
utils/.gitignore
vendored
@@ -1,8 +1,3 @@
|
|||||||
.idea
|
.idea
|
||||||
__pycache__
|
__pycache__
|
||||||
config.py
|
config.py
|
||||||
bin/
|
|
||||||
include/
|
|
||||||
lib/
|
|
||||||
out.json
|
|
||||||
pip-selfcheck.json
|
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
@@ -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.
|
Simple command line interface to extend and order the Mind-Expanding-Books list.
|
||||||
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.
|
|
||||||
|
|
||||||
## 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
|
### Setup
|
||||||
data like name, year, rating etc is present in the [main README.md](../README.md). Other details
|
1) Copy ```config-sample.py``` to ```config.py```
|
||||||
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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd utils
|
cp config-sample.py config.py
|
||||||
virtualenv .
|
```
|
||||||
source bin/activate
|
2) Get a GoodReads API key [here](https://www.goodreads.com/api/keys)
|
||||||
pip install -r requirements.txt
|
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)
|
This will convert to the new format and save it to ``README-new.md``.
|
||||||
- Apply for a developer api [here](https://www.goodreads.com/api)
|
|
||||||
- Copy utils/config-sample.py as utils/config.py
|
**Note!** the first conversion can take some time, we wait 1 second between each request to GoodReads to not abuse the API.
|
||||||
- Fill in the API Key credentials
|
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
5728
utils/books.json
5728
utils/books.json
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,2 @@
|
|||||||
# save this file as 'config.py' and then, fill it with you api key
|
# 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"
|
GOODREADS_PUBLIC_API_KEY = 'write here your goodreads public API key'
|
||||||
GOOGLE_SEARCH_RAPIDAPI_HOST = ""
|
|
||||||
GOOGLE_SEARCH_RAPIDAPI_KEY = ""
|
|
||||||
GOOGLE_BOOK_API_KEY = ""
|
|
||||||
@@ -3,105 +3,51 @@ import xml.etree.ElementTree as ET
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.error
|
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):
|
def get_details(book_object):
|
||||||
|
|
||||||
url = "http://www.goodreads.com/book/title.xml?key={}&title={}".format(
|
url = "http://www.goodreads.com/book/title.xml?key={}&title={}".format(GOODREADS_PUBLIC_API_KEY,
|
||||||
GOODREADS_PUBLIC_API_KEY, urllib.parse.quote_plus(book_object["title"])
|
urllib.parse.quote_plus(book_object['title']))
|
||||||
)
|
|
||||||
print(url)
|
|
||||||
try:
|
try:
|
||||||
time_to_sleep = 1
|
tree = ET.ElementTree(file=urllib.request.urlopen(url))
|
||||||
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)
|
|
||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
book = root.find("book")
|
book = root.find('book')
|
||||||
book_object["year"] = book.find("publication_year").text or ""
|
book_object['year'] = book.find('publication_year').text or ''
|
||||||
book_object["lang"] = book.find("language_code").text
|
book_object['lang'] = book.find('language_code').text
|
||||||
book_object["rating"] = book.find("average_rating").text
|
book_object['rating'] = book.find('average_rating').text
|
||||||
book_object["pages"] = book.find("num_pages").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
|
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
print(
|
print('Error getting book details from GoodReads for book: {}. \nGot error: '.format(book_object['title']))
|
||||||
"Error getting book details from GoodReads for book: {}. \nGot error: ".format(
|
print(str(e.getcode()) + ' ' + e.msg)
|
||||||
book_object["title"]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
print(str(e.getcode()) + " " + e.msg)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_goodread_info(library, force):
|
def get_goodread_info(library):
|
||||||
import sys
|
import sys
|
||||||
|
print('')
|
||||||
print("")
|
print('Getting GoodReads data...')
|
||||||
print("Getting GoodReads data...")
|
|
||||||
|
|
||||||
processed = 0
|
processed = 0
|
||||||
total_book_count = 0
|
total_book_count = 0
|
||||||
for key in library:
|
for key in library:
|
||||||
total_book_count += len(library[key])
|
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:
|
for book in book_list:
|
||||||
# do not call the api again if we already have the infomation
|
# 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
|
processed += 1
|
||||||
continue
|
continue
|
||||||
get_details(book)
|
get_details(book)
|
||||||
processed += 1
|
processed += 1
|
||||||
|
|
||||||
print(
|
print('{}/{} records processed.'.format(processed, total_book_count), end="\b")
|
||||||
"{}/{} records processed.".format(processed, total_book_count), end="\b"
|
sys.stdout.write('\r')
|
||||||
)
|
|
||||||
sys.stdout.write("\r")
|
|
||||||
sys.stdout.flush() # <- makes python print it anyway
|
sys.stdout.flush() # <- makes python print it anyway
|
||||||
|
|
||||||
# need to wait a second between the requests, to not abuse the API
|
# need to wait a second between the requests, to not abuse the API
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import simplejson
|
|
||||||
|
|
||||||
# we assume that every line after # Books
|
# we assume that every line after # Books
|
||||||
# starting with * is a book title if file type is old
|
# 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
|
# starting with | (and not with | Name or |--) is a book if the file type is new
|
||||||
@@ -7,71 +5,51 @@ import simplejson
|
|||||||
# ARGUMENT HANDLING
|
# ARGUMENT HANDLING
|
||||||
try:
|
try:
|
||||||
import argparse
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser(description='Process file.')
|
||||||
parser = argparse.ArgumentParser(description="Process file.")
|
|
||||||
parser.add_argument("--in_file", help="File to process, defaults to ./../README.MD")
|
|
||||||
parser.add_argument(
|
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(
|
parser.add_argument(
|
||||||
"--input_file_type",
|
'--out_file',
|
||||||
choices=["old", "new"],
|
help='File to save to, defaults to ./../README-NEW.MD')
|
||||||
help="old if links are displayed in a list, new if in a table",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--sort_by",
|
'--file_type',
|
||||||
choices=["rating", "title", "author", "year"],
|
choices=['old', 'new'],
|
||||||
help="defaults to rating",
|
help='old if links are displayed in a list, new if in a table')
|
||||||
)
|
|
||||||
parser.add_argument("--force", dest="force", action="store_true", default=False)
|
|
||||||
parser.add_argument(
|
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()
|
flags = parser.parse_args()
|
||||||
except ImportError:
|
except ImportError:
|
||||||
flags = None
|
flags = None
|
||||||
|
|
||||||
|
|
||||||
def sort(library, key_to_sort_on, reverse=False):
|
def sort(library, key_to_sort_on, reverse = False):
|
||||||
new_library = {}
|
new_library = {}
|
||||||
for key in library:
|
for key in library:
|
||||||
books = library[key]
|
books = library[key]
|
||||||
new_library[key] = sorted(
|
new_library[key] = sorted(books, key=lambda k: k[key_to_sort_on], reverse=reverse)
|
||||||
books, key=lambda k: k[key_to_sort_on], reverse=reverse
|
|
||||||
)
|
|
||||||
return new_library
|
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():
|
def main():
|
||||||
from read_file import load
|
from read_file import load
|
||||||
from gooodreads import get_goodread_info
|
from gooodreads import get_goodread_info
|
||||||
from write_file import render
|
from write_file import render
|
||||||
|
|
||||||
in_file = flags.in_file or "./../README.md"
|
in_file = flags.in_file or './../README.MD'
|
||||||
out_file = flags.out_file or "./../README-new.md"
|
out_file = flags.out_file or './../README-new.md'
|
||||||
input_file_type = flags.input_file_type or "new"
|
file_type = flags.file_type or 'new'
|
||||||
sort_by = flags.sort_by or "rating"
|
sort_by = flags.sort_by or 'rating'
|
||||||
force = flags.force
|
reverse = True if sort_by == 'rating' else False
|
||||||
store_json = flags.store_json
|
|
||||||
reverse = True if sort_by == "rating" else False
|
|
||||||
|
|
||||||
library = load(in_file, input_file_type)
|
library = load(in_file, file_type)
|
||||||
get_goodread_info(library, force)
|
get_goodread_info(library)
|
||||||
library = sort(library, sort_by, reverse)
|
library = sort(library, sort_by, reverse)
|
||||||
render(in_file, out_file, library)
|
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()
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -8,24 +8,24 @@ def read_file_content(file):
|
|||||||
# old (list)
|
# old (list)
|
||||||
def parse_book_string(book_string):
|
def parse_book_string(book_string):
|
||||||
book = {}
|
book = {}
|
||||||
book["title"] = book_string.split("[")[1].split("]")[0]
|
book['title'] = book_string.split('[')[1].split(']')[0]
|
||||||
book["url"] = book_string.split("]")[1].split("(")[1].split(")")[0]
|
book['url'] = book_string.split(']')[1].split('(')[1].split(')')[0]
|
||||||
book["author"] = book_string.split(" by ")[-1]
|
book['author'] = book_string.split(' by ')[-1]
|
||||||
book["rating"] = ""
|
book['rating'] = ''
|
||||||
book["year"] = ""
|
book['year'] = ''
|
||||||
return book
|
return book
|
||||||
|
|
||||||
|
|
||||||
# new (table)
|
# new (table)
|
||||||
def parse_book_string_new(book_string):
|
def parse_book_string_new(book_string):
|
||||||
book = {}
|
book = {}
|
||||||
book_split = book_string.split("|")
|
book_split = book_string.split('|')
|
||||||
# print(book_split)
|
# print(book_split)
|
||||||
book["title"] = book_split[1].strip()
|
book['title'] = book_split[1].strip()
|
||||||
book["author"] = book_split[2].strip()
|
book['author'] = book_split[2].strip()
|
||||||
book["url"] = book_split[3].strip().split("[")[1].split("(")[1].split(")")[0]
|
book['url'] = book_split[3].strip().split('[')[1].split('(')[1].split(')')[0]
|
||||||
book["rating"] = book_split[3].strip().split("[")[1].split("]")[0]
|
book['rating'] = book_split[3].strip().split('[')[1].split(']')[0]
|
||||||
book["year"] = book_split[4].strip()
|
book['year'] = book_split[4].strip()
|
||||||
return book
|
return book
|
||||||
|
|
||||||
|
|
||||||
@@ -33,8 +33,8 @@ def load(file, file_type):
|
|||||||
file = read_file_content(file)
|
file = read_file_content(file)
|
||||||
|
|
||||||
# we start one line after tilte # Books
|
# we start one line after tilte # Books
|
||||||
line_to_start = file.index("# Books") + 1
|
line_to_start = file.index('# Books') + 1
|
||||||
current_title = ""
|
current_title = ''
|
||||||
books_under_current_title = []
|
books_under_current_title = []
|
||||||
library = {}
|
library = {}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ def load(file, file_type):
|
|||||||
line = file[i]
|
line = file[i]
|
||||||
|
|
||||||
# we have a title
|
# we have a title
|
||||||
if line.startswith("##"):
|
if line.startswith('##'):
|
||||||
if len(current_title) == 0:
|
if len(current_title) == 0:
|
||||||
current_title = line
|
current_title = line
|
||||||
else:
|
else:
|
||||||
@@ -52,16 +52,12 @@ def load(file, file_type):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# we have a book
|
# we have a book
|
||||||
if file_type == "old":
|
if file_type == 'old':
|
||||||
if line.startswith("*"):
|
if line.startswith('*'):
|
||||||
book = parse_book_string(line)
|
book = parse_book_string(line)
|
||||||
books_under_current_title.append(book)
|
books_under_current_title.append(book)
|
||||||
else:
|
else:
|
||||||
if (
|
if line.startswith('|') and not line.startswith('| Name') and not line.startswith('|---'):
|
||||||
line.startswith("|")
|
|
||||||
and not line.startswith("| Name")
|
|
||||||
and not line.startswith("|---")
|
|
||||||
):
|
|
||||||
book = parse_book_string_new(line)
|
book = parse_book_string_new(line)
|
||||||
books_under_current_title.append(book)
|
books_under_current_title.append(book)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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}")
|
|
||||||
@@ -4,12 +4,14 @@ import os
|
|||||||
|
|
||||||
def render_book_line(book_object):
|
def render_book_line(book_object):
|
||||||
book = book_object
|
book = book_object
|
||||||
book["rating"] = "?" if not "rating" in book else book["rating"]
|
book['rating'] = '?' if not 'rating' in book else book['rating']
|
||||||
book["url"] = "" if not "url" in book else book["url"]
|
book['url'] = '' if not 'url' in book else book['url']
|
||||||
book["year"] = "" if not "year" in book else book["year"]
|
book['year'] = '' if not 'year' in book else book['year']
|
||||||
return "| {} | {} | [{}]({}) | {} | \n".format(
|
return '| {} | {} | [{}]({}) | {} | \n'.format(book['title'],
|
||||||
book["title"], book["author"], book["rating"], book["url"], book["year"]
|
book['author'],
|
||||||
)
|
book['rating'],
|
||||||
|
book['url'],
|
||||||
|
book['year'])
|
||||||
|
|
||||||
|
|
||||||
# TODO: refine this logic
|
# 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
|
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
|
this is done to prevent issues if the in and the out file are the same
|
||||||
"""
|
"""
|
||||||
tmp_file = "./.tmp-file.md"
|
tmp_file = './.tmp-file.md'
|
||||||
open(tmp_file, "a").close()
|
open(tmp_file, 'a').close()
|
||||||
books_not_reached = True
|
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:
|
with open(in_file) as original_file:
|
||||||
for line in original_file:
|
for line in original_file:
|
||||||
|
|
||||||
if line.strip() in library:
|
if line.strip() in library:
|
||||||
if not books_not_reached:
|
if not books_not_reached: out_file_tmp.write('\n')
|
||||||
out_file_tmp.write("\n")
|
|
||||||
books_not_reached = False
|
books_not_reached = False
|
||||||
|
|
||||||
# render chapter and start of the table
|
# render chapter and start of the table
|
||||||
out_file_tmp.write(line)
|
out_file_tmp.write(line)
|
||||||
if len(library[line.strip()]) > 0:
|
if len(library[line.strip()]) > 0:
|
||||||
out_file_tmp.write(
|
out_file_tmp.write('| Name | Author | Goodreads Rating | Year Published | \n')
|
||||||
"| Name | Author | Goodreads Rating | Year Published | \n"
|
out_file_tmp.write('|------|--------|------------------|----------------| \n')
|
||||||
)
|
|
||||||
out_file_tmp.write(
|
|
||||||
"|------|--------|------------------|----------------| \n"
|
|
||||||
)
|
|
||||||
# render books
|
# render books
|
||||||
for book in library[line.strip()]:
|
for book in library[line.strip()]:
|
||||||
out_file_tmp.write(render_book_line(book))
|
out_file_tmp.write(render_book_line(book))
|
||||||
elif books_not_reached:
|
elif books_not_reached:
|
||||||
out_file_tmp.write(line)
|
out_file_tmp.write(line)
|
||||||
elif line.startswith("## License"):
|
elif line.startswith('## License'):
|
||||||
out_file_tmp.write("\n")
|
out_file_tmp.write('\n')
|
||||||
out_file_tmp.write("\n")
|
out_file_tmp.write('\n')
|
||||||
out_file_tmp.write(line)
|
out_file_tmp.write(line)
|
||||||
books_not_reached = True
|
books_not_reached = True
|
||||||
|
|
||||||
copyfile(tmp_file, out_file)
|
copyfile(tmp_file, out_file)
|
||||||
os.remove(tmp_file)
|
os.remove(tmp_file)
|
||||||
Reference in New Issue
Block a user