Compare commits
291 Commits
coffee-bra
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2844c5ad5e | ||
|
|
40024c5945 | ||
|
|
4a44d35985 | ||
|
|
dd9807a929 | ||
|
|
11822842f0 | ||
|
|
dafefcf6b3 | ||
|
|
07f1f66360 | ||
|
|
56ef6eecd0 | ||
|
|
650d9ba116 | ||
|
|
af2c3309fc | ||
|
|
5a1a11d461 | ||
|
|
240aa60cbe | ||
|
|
35e0e76220 | ||
|
|
962d13ba02 | ||
|
|
897e916f62 | ||
|
|
76b4feef0d | ||
|
|
cb0f09f359 | ||
|
|
bf88bcfaeb | ||
|
|
7a7505ac5e | ||
|
|
a30a93569c | ||
|
|
41b4e47494 | ||
|
|
a18f7a23cf | ||
|
|
a5ecb7dab3 | ||
|
|
32deebdcf6 | ||
|
|
73efa4443c | ||
|
|
9a55f9ab1e | ||
|
|
357e72a887 | ||
|
|
d29ce7d24c | ||
|
|
76858735ed | ||
|
|
90959ff6c4 | ||
|
|
0c4450a4a5 | ||
|
|
436a3e149a | ||
|
|
046d0330be | ||
|
|
66737e2aab | ||
|
|
42a1065c48 | ||
|
|
483e534fcd | ||
|
|
98f39c0ecf | ||
|
|
402c8ce7ed | ||
|
|
febd71338f | ||
|
|
bc5075dc80 | ||
|
|
9eb334bf85 | ||
|
|
8d02dbfd60 | ||
|
|
e2d431d67d | ||
|
|
6c42389d72 | ||
|
|
a7f12ac7e5 | ||
|
|
52747a1b76 | ||
|
|
90f9bc88f5 | ||
|
|
0566905e42 | ||
|
|
13bbce624f | ||
|
|
a9a7440ed3 | ||
|
|
3a41378d29 | ||
|
|
751a16ca3e | ||
|
|
ab70c2764b | ||
|
|
5ddeafb2fb | ||
|
|
53e2941d4a | ||
|
|
fe916127e2 | ||
|
|
bef88206ae | ||
|
|
d898b99562 | ||
|
|
e86b26a72e | ||
|
|
48b16e1c8d | ||
|
|
ba8bd2f045 | ||
|
|
569741d47c | ||
|
|
923ef1ee9d | ||
|
|
7a162b1f04 | ||
|
|
a81ac25651 | ||
|
|
33efad1281 | ||
|
|
52918bae4d | ||
|
|
a989ca1b6a | ||
|
|
e6382aa88a | ||
|
|
3890f96456 | ||
|
|
add51b7e82 | ||
|
|
cc0515d66c | ||
|
|
1632af7b67 | ||
|
|
b405c7a806 | ||
|
|
26656bafa9 | ||
|
|
9c966cc21e | ||
|
|
a39e38c580 | ||
|
|
2d71afbf5e | ||
|
|
6c43a5f56d | ||
|
|
8c80de361c | ||
|
|
a893ef1171 | ||
|
|
b125c6e705 | ||
|
|
7fc9dca26e | ||
|
|
de3b3db21e | ||
|
|
9e415f4c73 | ||
|
|
0e9f96be27 | ||
|
|
73058d651f | ||
|
|
fb0cba56bb | ||
|
|
5214f0cbba | ||
|
|
9d944378c8 | ||
|
|
7af707a571 | ||
|
|
9a821212bd | ||
|
|
f78852620c | ||
|
|
e0c1230289 | ||
|
|
283ddf3ced | ||
|
|
bf4fda7318 | ||
|
|
2ac25fdbfb | ||
|
|
f8fe3e8953 | ||
|
|
10aa58a3ed | ||
|
|
faa40a2130 | ||
|
|
3e9c89c4bc | ||
|
|
7a9b648e58 | ||
|
|
fee2000ed2 | ||
|
|
3e1fd761e8 | ||
|
|
a284140772 | ||
|
|
4b192c8108 | ||
|
|
2bea8f1e1d | ||
|
|
3c998f94fc | ||
|
|
48ea125eba | ||
|
|
9816ab055c | ||
|
|
55387bc554 | ||
|
|
6648b56181 | ||
|
|
a3fa4726d7 | ||
|
|
5d5d90a20a | ||
|
|
700bfb7c9f | ||
|
|
275b1bcc97 | ||
|
|
c13a65aeb2 | ||
|
|
ee180c8068 | ||
|
|
0deea97ec6 | ||
|
|
cf43ea3086 | ||
|
|
5dcc780fc8 | ||
|
|
0793edee1d | ||
|
|
075a221392 | ||
|
|
4ec82e5448 | ||
|
|
40eafd8623 | ||
|
|
8615477f0c | ||
|
|
4a8b11a5cc | ||
|
|
5117828a9d | ||
|
|
e74a6e1ec0 | ||
|
|
c9b56c3a4f | ||
|
|
5c56e8540f | ||
|
|
33d545a26b | ||
|
|
3489f3d5c3 | ||
|
|
28c6f9eaf8 | ||
|
|
ce30d8b8b4 | ||
|
|
2656b43f51 | ||
|
|
50b542189c | ||
|
|
3a0b94c846 | ||
|
|
2412ec36f9 | ||
|
|
c2aae6dbd5 | ||
|
|
c03eab472b | ||
|
|
4bde8e6f0f | ||
|
|
6bac7895ac | ||
|
|
3bbc985ea3 | ||
|
|
a226450410 | ||
|
|
d5c6cd0be6 | ||
|
|
97522a0332 | ||
|
|
d656841cb3 | ||
|
|
f62088d411 | ||
|
|
c85124cc55 | ||
|
|
1489b1359e | ||
|
|
e9e9b6755c | ||
|
|
18cc21b60d | ||
|
|
050ad48cd8 | ||
|
|
eedf0ba7f1 | ||
|
|
819ce9ba7c | ||
|
|
fe5aefd2c6 | ||
|
|
30141d31f3 | ||
|
|
1bfb3321a3 | ||
|
|
69d0848ed5 | ||
|
|
43345db743 | ||
|
|
4daab218a0 | ||
|
|
b3982bd426 | ||
|
|
f258bea951 | ||
|
|
5163baa13b | ||
|
|
cd2c9ee334 | ||
|
|
8dbfeb1059 | ||
|
|
9f2a822a54 | ||
|
|
ef9fc03843 | ||
|
|
454ada1ee6 | ||
|
|
e2264c0eeb | ||
|
|
695d92f465 | ||
|
|
549b560748 | ||
|
|
7bd8e6682b | ||
|
|
f4446aff07 | ||
|
|
fbec5ca04a | ||
|
|
c8634462fc | ||
|
|
eee53f2f60 | ||
|
|
c527547008 | ||
|
|
44c779acb5 | ||
|
|
96d806ed6d | ||
|
|
872b09fe52 | ||
|
|
fb36c7cde8 | ||
|
|
5df6370439 | ||
|
|
f36a9615a7 | ||
|
|
8cd6ba59d7 | ||
|
|
e1962883f6 | ||
|
|
193f06bb21 | ||
|
|
68e0dbe1cb | ||
|
|
dd7b9b6efb | ||
|
|
a1fa7011cd | ||
|
|
e50712e5bc | ||
|
|
c94bd9f3b3 | ||
|
|
de1bba855f | ||
|
|
fbfd6d8384 | ||
|
|
08ba2f9cbd | ||
|
|
6bc3393293 | ||
|
|
04cc460bb5 | ||
|
|
3abe7db364 | ||
|
|
1a70bf665e | ||
|
|
5dc79eeca6 | ||
|
|
52e63d7c1d | ||
|
|
c6197e9646 | ||
|
|
18ef998136 | ||
|
|
64d9be9221 | ||
|
|
6e251e0699 | ||
|
|
de9a3d4040 | ||
|
|
02148006ae | ||
|
|
76f2f431c1 | ||
|
|
5bb6865ba6 | ||
|
|
9fe8505ea4 | ||
|
|
52285301d1 | ||
|
|
843ed75db7 | ||
|
|
d723ec905b | ||
|
|
3bd0ce9bd6 | ||
|
|
828a68fa75 | ||
|
|
1593c00620 | ||
|
|
4d81bf3fe1 | ||
|
|
f985c157d9 | ||
|
|
defa5cb196 | ||
|
|
b4cde4c777 | ||
|
|
b8b957805e | ||
|
|
d620d9d08b | ||
|
|
0f3d13c58a | ||
|
|
63633fcf57 | ||
|
|
98013de897 | ||
|
|
40d8e36880 | ||
|
|
302806920c | ||
|
|
629c6ef9d2 | ||
|
|
6da704d06a | ||
|
|
7cbabf11c3 | ||
|
|
ad6e84654e | ||
|
|
edad683fac | ||
|
|
a821f2a23d | ||
|
|
4e2f55598f | ||
|
|
f50bdf9006 | ||
|
|
edd2eef7f4 | ||
|
|
3d24c54a6c | ||
|
|
b4b005460f | ||
|
|
1e00c3442c | ||
|
|
fb6e2e8b5d | ||
|
|
89cdab6ad2 | ||
|
|
bba3cfe578 | ||
|
|
b9e8ae5bbf | ||
|
|
1acc7c9007 | ||
|
|
8022843544 | ||
|
|
15c5cd8505 | ||
|
|
c2e244f784 | ||
|
|
49763eecd2 | ||
|
|
63350a60a3 | ||
|
|
5e02e59123 | ||
|
|
08b11eb478 | ||
|
|
c619ae91f6 | ||
|
|
a65474a397 | ||
|
|
3eb0b5b129 | ||
|
|
e8a127c823 | ||
|
|
b1b22e5a1f | ||
|
|
98ebe2ffd9 | ||
|
|
45d98c7e39 | ||
|
|
dcc0c64aea | ||
|
|
e66fb6c054 | ||
|
|
2989f26919 | ||
|
|
5029549e2c | ||
|
|
bbb331f0fa | ||
|
|
c581cc1dfb | ||
|
|
708e20ba85 | ||
|
|
d34d8d49af | ||
|
|
810c497558 | ||
|
|
834854a36c | ||
|
|
066d05c0af | ||
|
|
9bf3627795 | ||
|
|
2b92156cc1 | ||
|
|
4d0e21a999 | ||
|
|
7de464fa23 | ||
|
|
46feb27687 | ||
|
|
9a2318c00c | ||
|
|
8b982eb061 | ||
|
|
4e8712a154 | ||
|
|
f741582866 | ||
|
|
32e21b16ad | ||
|
|
37f4220178 | ||
|
|
75f0f09275 | ||
|
|
3c92a18525 | ||
|
|
71974a19fb | ||
|
|
7ba90048f7 | ||
|
|
896c165222 | ||
|
|
91d197c82e | ||
|
|
d056b55fc9 | ||
|
|
87198913ea | ||
|
|
3b70e71567 | ||
|
|
e865b1f12b |
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,5 +1,7 @@
|
|||||||
|
(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
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.DS_Store
|
||||||
@@ -1,10 +1,24 @@
|
|||||||
# Contribution Guidelines
|
# Contribution Guidelines
|
||||||
|
|
||||||
Please ensure your pull request adheres to the following 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.
|
||||||
|
|
||||||
- Search previous suggestions before making a new one, as yours may be a duplicate.
|
So you should add a book that has changed your life!!
|
||||||
- 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 |`
|
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!
|
||||||
- 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!
|
||||||
|
|||||||
22
app/.eslintrc.js
Normal file
22
app/.eslintrc.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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
Normal file
72
app/.gitignore
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# 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
|
||||||
4
app/.prettierignore
Normal file
4
app/.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.cache
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
public
|
||||||
7
app/.prettierrc
Normal file
7
app/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
1
app/.prettierrc.json
Normal file
1
app/.prettierrc.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
22
app/LICENSE
Normal file
22
app/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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.
|
||||||
|
|
||||||
39
app/README.md
Normal file
39
app/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
`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.
|
||||||
14
app/gatsby-browser.js
Normal file
14
app/gatsby-browser.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
)
|
||||||
46
app/gatsby-config.js
Normal file
46
app/gatsby-config.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
49
app/gatsby-node.js
Normal file
49
app/gatsby-node.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
14
app/gatsby-ssr.js
Normal file
14
app/gatsby-ssr.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* 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
Normal file
29267
app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
69
app/package.json
Normal file
69
app/package.json
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
app/postcss.config.js
Normal file
3
app/postcss.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = () => ({
|
||||||
|
plugins: [require("tailwindcss")],
|
||||||
|
})
|
||||||
14
app/src/components/amazonurl.js
Normal file
14
app/src/components/amazonurl.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
95
app/src/components/bookcard.js
Normal file
95
app/src/components/bookcard.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
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;
|
||||||
26
app/src/components/bookmark.js
Normal file
26
app/src/components/bookmark.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
12
app/src/components/categorydescription.js
Normal file
12
app/src/components/categorydescription.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
28
app/src/components/feed.js
Normal file
28
app/src/components/feed.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
32
app/src/components/goodreadsimage.js
Normal file
32
app/src/components/goodreadsimage.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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
|
||||||
28
app/src/components/header.js
Normal file
28
app/src/components/header.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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
|
||||||
32
app/src/components/image.js
Normal file
32
app/src/components/image.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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
|
||||||
622
app/src/components/layout.css
Normal file
622
app/src/components/layout.css
Normal file
@@ -0,0 +1,622 @@
|
|||||||
|
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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
47
app/src/components/layout.js
Normal file
47
app/src/components/layout.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
88
app/src/components/seo.js
Normal file
88
app/src/components/seo.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
49
app/src/components/sidebar.js
Normal file
49
app/src/components/sidebar.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
40
app/src/components/sortByDropdown.js
Normal file
40
app/src/components/sortByDropdown.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
33
app/src/context/bookReducer.js
Normal file
33
app/src/context/bookReducer.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/src/context/globalState.js
Normal file
25
app/src/context/globalState.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
5728
app/src/data/books.json
Normal file
5728
app/src/data/books.json
Normal file
File diff suppressed because it is too large
Load Diff
74
app/src/data/categories.json
Normal file
74
app/src/data/categories.json
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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": "🖌️"
|
||||||
|
}
|
||||||
|
]
|
||||||
BIN
app/src/images/amazon.png
Normal file
BIN
app/src/images/amazon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/images/gatsby-astronaut.png
Normal file
BIN
app/src/images/gatsby-astronaut.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
BIN
app/src/images/gatsby-icon.png
Normal file
BIN
app/src/images/gatsby-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
app/src/images/goodreads.png
Normal file
BIN
app/src/images/goodreads.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
14
app/src/pages/404.js
Normal file
14
app/src/pages/404.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
79
app/src/pages/index.js
Normal file
79
app/src/pages/index.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
16
app/src/pages/page-2.js
Normal file
16
app/src/pages/page-2.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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
|
||||||
36
app/src/pages/readingList.js
Normal file
36
app/src/pages/readingList.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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
|
||||||
11
app/src/styles/global.css
Normal file
11
app/src/styles/global.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.custom-header{
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
.footer-text {
|
||||||
|
text-align:center;
|
||||||
|
padding-bottom:1 rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode {
|
||||||
|
background-color: #3c4759;
|
||||||
|
}
|
||||||
38
app/src/styles/sidebar.css
Normal file
38
app/src/styles/sidebar.css
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
32
app/src/templates/categoryTemplate.js
Normal file
32
app/src/templates/categoryTemplate.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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
Normal file
16489
app/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
3
package-lock.json
generated
Normal file
3
package-lock.json
generated
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1
|
||||||
|
}
|
||||||
7
utils/.gitignore
vendored
7
utils/.gitignore
vendored
@@ -1,3 +1,8 @@
|
|||||||
.idea
|
.idea
|
||||||
__pycache__
|
__pycache__
|
||||||
config.py
|
config.py
|
||||||
|
bin/
|
||||||
|
include/
|
||||||
|
lib/
|
||||||
|
out.json
|
||||||
|
pip-selfcheck.json
|
||||||
|
|||||||
34
utils/HOUSEKEEPING.md
Normal file
34
utils/HOUSEKEEPING.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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,34 +1,37 @@
|
|||||||
# Utils for Mind-Expanding-Books
|
`utils/` mainly contains scripts for generating `app/src/data/books.json` file from `README.md`
|
||||||
|
|
||||||
Simple command line interface to extend and order the Mind-Expanding-Books list.
|
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.
|
||||||
|
|
||||||
## Getting started
|
## Generating `app/src/data/books.json`
|
||||||
|
|
||||||
### Setup
|
The website shows name of the book, year, rating, cover, amazon link, etc in book card. Some of the
|
||||||
1) Copy ```config-sample.py``` to ```config.py```
|
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
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp config-sample.py config.py
|
cd utils
|
||||||
```
|
virtualenv .
|
||||||
2) Get a GoodReads API key [here](https://www.goodreads.com/api/keys)
|
source bin/activate
|
||||||
3) Copy your public key to the ```config.py``` file
|
pip install -r requirements.txt
|
||||||
|
|
||||||
### 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).
|
## Executing the script
|
||||||
|
|
||||||
This will convert to the new format and save it to ``README-new.md``.
|
- Register at [goodreads](https://www.goodreads.com)
|
||||||
|
- Apply for a developer api [here](https://www.goodreads.com/api)
|
||||||
**Note!** the first conversion can take some time, we wait 1 second between each request to GoodReads to not abuse the API.
|
- Copy utils/config-sample.py as utils/config.py
|
||||||
After the first generation only missing records will be tried to be retrieved.
|
- Fill in the API Key credentials
|
||||||
|
|
||||||
Run to see the available arguments
|
|
||||||
```bash
|
|
||||||
python3 housekeep.py --help
|
|
||||||
```
|
|
||||||
|
|||||||
5728
utils/book_name_to_details.json
Normal file
5728
utils/book_name_to_details.json
Normal file
File diff suppressed because it is too large
Load Diff
5728
utils/books.json
Normal file
5728
utils/books.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,5 @@
|
|||||||
# 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,51 +3,105 @@ import xml.etree.ElementTree as ET
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.error
|
import urllib.error
|
||||||
|
|
||||||
from config import GOODREADS_PUBLIC_API_KEY
|
import requests
|
||||||
|
|
||||||
|
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(GOODREADS_PUBLIC_API_KEY,
|
url = "http://www.goodreads.com/book/title.xml?key={}&title={}".format(
|
||||||
urllib.parse.quote_plus(book_object['title']))
|
GOODREADS_PUBLIC_API_KEY, urllib.parse.quote_plus(book_object["title"])
|
||||||
|
)
|
||||||
|
print(url)
|
||||||
try:
|
try:
|
||||||
tree = ET.ElementTree(file=urllib.request.urlopen(url))
|
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)
|
||||||
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('Error getting book details from GoodReads for book: {}. \nGot error: '.format(book_object['title']))
|
print(
|
||||||
print(str(e.getcode()) + ' ' + e.msg)
|
"Error getting book details from GoodReads for book: {}. \nGot error: ".format(
|
||||||
|
book_object["title"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print(str(e.getcode()) + " " + e.msg)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_goodread_info(library):
|
def get_goodread_info(library, force):
|
||||||
import sys
|
import sys
|
||||||
print('')
|
|
||||||
print('Getting GoodReads data...')
|
print("")
|
||||||
|
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:
|
||||||
for chapter in library:
|
book_list = library[category]
|
||||||
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 'rating' in book and book['rating']:
|
if not force and "rating" in book and book["rating"]:
|
||||||
processed += 1
|
processed += 1
|
||||||
continue
|
continue
|
||||||
get_details(book)
|
get_details(book)
|
||||||
processed += 1
|
processed += 1
|
||||||
|
|
||||||
print('{}/{} records processed.'.format(processed, total_book_count), end="\b")
|
print(
|
||||||
sys.stdout.write('\r')
|
"{}/{} records processed.".format(processed, total_book_count), end="\b"
|
||||||
|
)
|
||||||
|
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,3 +1,5 @@
|
|||||||
|
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
|
||||||
@@ -5,51 +7,71 @@
|
|||||||
# 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(
|
||||||
'--in_file',
|
"--out_file", help="File to save to, defaults to ./../README-NEW.MD"
|
||||||
help='File to process, defaults to ./../README.MD')
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--out_file',
|
"--input_file_type",
|
||||||
help='File to save to, defaults to ./../README-NEW.MD')
|
choices=["old", "new"],
|
||||||
|
help="old if links are displayed in a list, new if in a table",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--file_type',
|
"--sort_by",
|
||||||
choices=['old', 'new'],
|
choices=["rating", "title", "author", "year"],
|
||||||
help='old if links are displayed in a list, new if in a table')
|
help="defaults to rating",
|
||||||
|
)
|
||||||
|
parser.add_argument("--force", dest="force", action="store_true", default=False)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--sort_by',
|
"--store-json", dest="store_json", action="store_true", default=False
|
||||||
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(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
|
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"
|
||||||
file_type = flags.file_type or 'new'
|
input_file_type = flags.input_file_type or "new"
|
||||||
sort_by = flags.sort_by or 'rating'
|
sort_by = flags.sort_by or "rating"
|
||||||
reverse = True if sort_by == 'rating' else False
|
force = flags.force
|
||||||
|
store_json = flags.store_json
|
||||||
|
reverse = True if sort_by == "rating" else False
|
||||||
|
|
||||||
library = load(in_file, file_type)
|
library = load(in_file, input_file_type)
|
||||||
get_goodread_info(library)
|
get_goodread_info(library, force)
|
||||||
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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
8
utils/pyvenv.cfg
Normal file
8
utils/pyvenv.cfg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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,12 +52,16 @@ 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 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)
|
book = parse_book_string_new(line)
|
||||||
books_under_current_title.append(book)
|
books_under_current_title.append(book)
|
||||||
|
|
||||||
|
|||||||
26
utils/requirements.txt
Normal file
26
utils/requirements.txt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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
|
||||||
94
utils/update_json_files.py
Normal file
94
utils/update_json_files.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
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,14 +4,12 @@ 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(book['title'],
|
return "| {} | {} | [{}]({}) | {} | \n".format(
|
||||||
book['author'],
|
book["title"], book["author"], book["rating"], book["url"], book["year"]
|
||||||
book['rating'],
|
)
|
||||||
book['url'],
|
|
||||||
book['year'])
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: refine this logic
|
# TODO: refine this logic
|
||||||
@@ -21,32 +19,37 @@ 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: out_file_tmp.write('\n')
|
if not books_not_reached:
|
||||||
|
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('| Name | Author | Goodreads Rating | Year Published | \n')
|
out_file_tmp.write(
|
||||||
out_file_tmp.write('|------|--------|------------------|----------------| \n')
|
"| Name | Author | Goodreads Rating | Year Published | \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