Compare commits
1 Commits
master
...
coffee-bra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7188c3b26 |
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,7 +1,5 @@
|
||||
(todo:please remove this todo and the sections you did not check below before you make your pull request.
|
||||
If you are unsure, please check other PRs like this one: https://github.com/hackerkid/Mind-Expanding-Books/pull/207#issue-377268434)
|
||||
|
||||
## In this pull request
|
||||
## In this pull request
|
||||
- [ ] I am adding a new book.
|
||||
- [ ] I am adding a new category
|
||||
- [ ] Removing a book
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.DS_Store
|
||||
@@ -1,24 +1,10 @@
|
||||
# Contribution Guidelines
|
||||
|
||||
## What are the criteria for adding a new book?
|
||||
The mission of this list is to curate books that can change the lives of people for the better.
|
||||
Please ensure your pull request adheres to the following guidelines:
|
||||
|
||||
So you should add a book that has changed your life!!
|
||||
|
||||
Here are some questions that you can potentially ask yourself to help make this decision. If the answer is yes for most of them, feel free to create a pull request!
|
||||
|
||||
1. If you had the money to gift a book to every college graduate this year, would you gift this book?
|
||||
2. Would you gift this book to your children at any point in their lives?
|
||||
3. If there are only 3 books that you can keep a physical copy of in your life, would this book be one of them?
|
||||
4. Would your life have been better off had you read this book 10 years back?
|
||||
5. Would this book be relevant 1000 years from now?
|
||||
|
||||
|
||||
## How to create a pull request?
|
||||
- Search for existing books in [README.md](README.md) and make sure that you are not adding a duplicate.
|
||||
- Insert the book in the following format in [README.md](README.md). Don't change any other files.
|
||||
`| Book name | Author | [Goodreads rating](Goodreads url) | Year published |`
|
||||
- Make sure that the book is inserted in the correct order according to the Goodreads rating.
|
||||
- Mention in pull request clearly why you think the book deserves to be on the list.
|
||||
- Search previous suggestions before making a new one, as yours may be a duplicate.
|
||||
- if you want to add a new book you should have read the book.
|
||||
- Use the following format: `| Book name | Author | [Goodreads rating](Goodreads url) | Year published |`
|
||||
- Mention in pull request clearly why you think the book deserve to be in the list.
|
||||
|
||||
Thank you!
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2020: true,
|
||||
},
|
||||
extends: [
|
||||
'plugin:react/recommended',
|
||||
'airbnb',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 11,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: [
|
||||
'react',
|
||||
],
|
||||
rules: {
|
||||
},
|
||||
};
|
||||
72
app/.gitignore
vendored
72
app/.gitignore
vendored
@@ -1,72 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# gatsby files
|
||||
.cache/
|
||||
public
|
||||
|
||||
# Mac files
|
||||
.DS_Store
|
||||
|
||||
# Yarn
|
||||
yarn-error.log
|
||||
.pnp/
|
||||
.pnp.js
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# ignore package-lock as it complicates merging
|
||||
package-lock.json
|
||||
@@ -1,4 +0,0 @@
|
||||
.cache
|
||||
package.json
|
||||
package-lock.json
|
||||
public
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
22
app/LICENSE
22
app/LICENSE
@@ -1,22 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 gatsbyjs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
`app/` directory contains the source code of the Mind Expanding Books [website](https://books.vishnuks.com)
|
||||
|
||||
## How to setup development environment
|
||||
|
||||
#### Verify Node Install
|
||||
|
||||
```
|
||||
node -v
|
||||
npm -v
|
||||
```
|
||||
|
||||
#### Install Gatsby CLI
|
||||
|
||||
```
|
||||
npm install -g gatsby-cli
|
||||
```
|
||||
|
||||
```bash
|
||||
|
||||
git clone https://github.com/hackerkid/Mind-Expanding-Books
|
||||
cd app/
|
||||
npm install
|
||||
gatsby develop
|
||||
```
|
||||
|
||||
Once this is done, the development server should be accessible at http://localhost:8000
|
||||
|
||||
## High level overview of the website
|
||||
|
||||
- The website is made using Gatsby, which is a React based static site generator.
|
||||
- The website is deployed in Netlify automatically whenever a commit is pushed to GitHub.
|
||||
- When you create a pull request with changes to the source code, Netlify will automatically
|
||||
create a website for previewing the changes. You can click on "Details" in the "Deploy preview ready!"
|
||||
message in the pull request page for seeing the website.
|
||||
|
||||
## From where does the website fetches the data of the books?
|
||||
|
||||
Website fetches the data of the books from `app/src/data/books.json` file. See [README in utils directory](../utils/README.MD)
|
||||
for details on how this file is generated.
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Implement Gatsby's Browser APIs in this file.
|
||||
*
|
||||
* See: https://www.gatsbyjs.org/docs/browser-apis/
|
||||
*/
|
||||
|
||||
// You can delete this file if you're not using it
|
||||
import React from "react"
|
||||
|
||||
import GlobalState from "./src/context/globalState"
|
||||
|
||||
export const wrapRootElement = ({ element }) => (
|
||||
<GlobalState>{element}</GlobalState>
|
||||
)
|
||||
@@ -1,46 +0,0 @@
|
||||
module.exports = {
|
||||
siteMetadata: {
|
||||
title: `Mind Expanding Books`,
|
||||
description: `Kick off your next, great Gatsby project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need.`,
|
||||
author: `@gatsbyjs`,
|
||||
},
|
||||
plugins: [
|
||||
`gatsby-plugin-react-helmet`,
|
||||
{
|
||||
resolve: `gatsby-source-filesystem`,
|
||||
options: {
|
||||
name: `images`,
|
||||
path: `${__dirname}/src/images`,
|
||||
},
|
||||
},
|
||||
`gatsby-transformer-sharp`,
|
||||
`gatsby-plugin-sharp`,
|
||||
{
|
||||
resolve: `gatsby-plugin-manifest`,
|
||||
options: {
|
||||
name: `gatsby-starter-default`,
|
||||
short_name: `starter`,
|
||||
start_url: `/`,
|
||||
background_color: `#663399`,
|
||||
theme_color: `#663399`,
|
||||
display: `minimal-ui`,
|
||||
icon: `src/images/gatsby-icon.png`, // This path is relative to the root of the site.
|
||||
},
|
||||
},
|
||||
`gatsby-plugin-offline`,
|
||||
`gatsby-transformer-json`,
|
||||
{
|
||||
resolve: `gatsby-source-filesystem`,
|
||||
options: {
|
||||
path: `./src/data/`,
|
||||
},
|
||||
},
|
||||
`gatsby-plugin-postcss`,
|
||||
{
|
||||
resolve: `gatsby-plugin-google-analytics`,
|
||||
options: {
|
||||
trackingId: "UA-139957969-2",
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const categories = JSON.parse(fs.readFileSync('src/data/categories.json'));
|
||||
const slugify = require('slugify');
|
||||
|
||||
exports.createPages = async function ({ actions, graphql }) {
|
||||
const { createPage } = actions;
|
||||
await Promise.all(
|
||||
categories.map(async (category) => {
|
||||
const data = await graphql(
|
||||
`
|
||||
query categoryBooksQuery($categoryName: String) {
|
||||
allBooksJson(
|
||||
filter: { category: { eq: $categoryName } }
|
||||
sort: { fields: [rating], order: DESC }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
url
|
||||
rating
|
||||
author
|
||||
year
|
||||
category
|
||||
image_url
|
||||
description
|
||||
amazon_url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ categoryName: category.name },
|
||||
);
|
||||
console.log(category.name, data.data);
|
||||
createPage({
|
||||
path: slugify(category.name),
|
||||
component: require.resolve('./src/templates/categoryTemplate.js'),
|
||||
context: {
|
||||
categoryName: category.name,
|
||||
data: data.data,
|
||||
image: category.emoji,
|
||||
limit: null,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Implement Gatsby's SSR (Server Side Rendering) APIs in this file.
|
||||
*
|
||||
* See: https://www.gatsbyjs.org/docs/ssr-apis/
|
||||
*/
|
||||
|
||||
// You can delete this file if you're not using it
|
||||
import React from "react"
|
||||
|
||||
import GlobalState from "./src/context/globalState"
|
||||
|
||||
export const wrapRootElement = ({ element }) => (
|
||||
<GlobalState>{element}</GlobalState>
|
||||
)
|
||||
29267
app/package-lock.json
generated
29267
app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,69 +0,0 @@
|
||||
{
|
||||
"name": "gatsby-starter-default",
|
||||
"private": true,
|
||||
"description": "A simple starter to get up and developing quickly with Gatsby",
|
||||
"version": "0.1.0",
|
||||
"author": "Kyle Mathews <mathews.kyle@gmail.com>",
|
||||
"dependencies": {
|
||||
"bootstrap": "^4.5.2",
|
||||
"gatsby": "^2.24.91",
|
||||
"gatsby-image": "^2.2.27",
|
||||
"gatsby-plugin-google-analytics": "^2.3.14",
|
||||
"gatsby-plugin-manifest": "^2.4.37",
|
||||
"gatsby-plugin-offline": "^3.0.14",
|
||||
"gatsby-plugin-postcss": "^2.1.11",
|
||||
"gatsby-plugin-react-helmet": "^3.1.11",
|
||||
"gatsby-plugin-sharp": "^2.6.43",
|
||||
"gatsby-source-filesystem": "^2.1.31",
|
||||
"gatsby-transformer-json": "^2.2.13",
|
||||
"gatsby-transformer-sharp": "^2.2.21",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.10.2",
|
||||
"react-bootstrap": "^1.3.0",
|
||||
"react-dom": "^16.10.2",
|
||||
"react-helmet": "^5.2.1",
|
||||
"react-star-rating-component": "^1.4.1",
|
||||
"react-star-ratings": "^2.3.0",
|
||||
"slugify": "^1.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.7.0",
|
||||
"eslint-config-airbnb": "^18.2.0",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||
"eslint-plugin-react": "^7.20.6",
|
||||
"eslint-plugin-react-hooks": "^4.1.0",
|
||||
"husky": "^4.3.0",
|
||||
"lint-staged": ">=10",
|
||||
"prettier": "1.19.1",
|
||||
"tailwindcss": "^1.1.2"
|
||||
},
|
||||
"keywords": [
|
||||
"gatsby"
|
||||
],
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "gatsby build",
|
||||
"develop": "gatsby develop",
|
||||
"format": "prettier --write \"**/*.{js,jsx,json,md}\"",
|
||||
"start": "npm run develop",
|
||||
"serve": "gatsby serve",
|
||||
"test": "echo \"Write tests! -> https://gatsby.dev/unit-testing \""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/gatsbyjs/gatsby-starter-default"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/gatsbyjs/gatsby/issues"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,json,md}": "prettier --write",
|
||||
"*.js": "eslint --cache --fix"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = () => ({
|
||||
plugins: [require("tailwindcss")],
|
||||
})
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from "react";
|
||||
import { OutboundLink } from "gatsby-plugin-google-analytics"
|
||||
|
||||
const getTargetURL = (book) => {
|
||||
return book.amazon_url + "?tag=vishnuks-20";
|
||||
}
|
||||
|
||||
export default ({ book }) => {
|
||||
return (
|
||||
<OutboundLink href={getTargetURL(book)} target="_blank" rel="noreferrer">
|
||||
<img alt="Amazon link" style={{ marginBottom: "-8px" }} src="https://img.icons8.com/color/48/000000/amazon.png"/>
|
||||
</OutboundLink>
|
||||
)
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useState } from 'react';
|
||||
import StarRatings from 'react-star-ratings';
|
||||
import { Card, Row, Col } from 'react-bootstrap';
|
||||
|
||||
import AmazonURL from '../components/amazonurl';
|
||||
import Bookmark from '../components/bookmark';
|
||||
import GoodReadsImage from '../components/goodreadsimage';
|
||||
|
||||
const truncateContent = (content) => {
|
||||
if (!content) {
|
||||
return '';
|
||||
}
|
||||
return content.length > 350 ? content.substring(0, 350) + '...' : content;
|
||||
};
|
||||
|
||||
const showFullText = (content) => {
|
||||
if (!content) {
|
||||
return '';
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
const BookCard = ({ book }) => {
|
||||
const [ show, toggleShow ] = useState(false);
|
||||
return (
|
||||
<Card style={{ marginBottom: '15px' }}>
|
||||
<Row>
|
||||
<Col xs={6} sm={6} md={4} xl={2}>
|
||||
<Card.Img
|
||||
style={{
|
||||
paddingLeft: '15px',
|
||||
paddingRight: '15px',
|
||||
paddingTop: '30px'
|
||||
}}
|
||||
src={book.image_url}
|
||||
alt={book.title}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={6} md={8} xl={10}>
|
||||
<Card.Body>
|
||||
<Card.Title>{book.title}</Card.Title>
|
||||
<Card.Subtitle className="text-muted">
|
||||
<Card.Text style={{ paddingTop: '2px' }}>
|
||||
{book.author} <b>{book.year ? book.year : null}</b>
|
||||
</Card.Text>
|
||||
<StarRatings
|
||||
rating={parseFloat(book.rating)}
|
||||
numberOfStars={5}
|
||||
starDimension="18px"
|
||||
starSpacing="1px"
|
||||
starRatedColor="#fa604a"
|
||||
/>
|
||||
<div style={{ display: 'flex', alignItems: 'center', paddingTop: '.75rem' }}>
|
||||
<div style={{ width: '30px', height: '30px', marginRight: '5px' }}>
|
||||
{book.amazon_url ? <AmazonURL book={book} /> : null}
|
||||
</div>
|
||||
<div style={{ width: '30px', height: '30px' }}>
|
||||
<a href={book.url} target="_blank">
|
||||
<GoodReadsImage />
|
||||
</a>
|
||||
</div>
|
||||
<Bookmark book={book} />
|
||||
</div>
|
||||
</Card.Subtitle>
|
||||
<p style={{ color: 'gray', fontSize: '0.8rem', paddingTop: '1rem' }}>
|
||||
{!show && truncateContent(book.description)}
|
||||
{show && showFullText(book.description)}
|
||||
</p>
|
||||
{!show && book.description.length>350 &&(
|
||||
<button className="btn btn-sm btn-primary " onClick={() => toggleShow(true)}>
|
||||
Show More
|
||||
</button>
|
||||
)}
|
||||
{show && (
|
||||
<button className="btn btn-sm btn-primary " onClick={() => toggleShow(false)}>
|
||||
Show Less
|
||||
</button>
|
||||
)}
|
||||
</Card.Body>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
BookCard.propTypes = {
|
||||
siteTitle: PropTypes.object
|
||||
};
|
||||
|
||||
BookCard.defaultProps = {
|
||||
book: {}
|
||||
};
|
||||
|
||||
export default BookCard;
|
||||
@@ -1,26 +0,0 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { Button } from "react-bootstrap"
|
||||
import { BookmarkContext } from '../context/globalState'
|
||||
|
||||
export default ({ book }) => {
|
||||
const { updateReadingList, readingList } = useContext(BookmarkContext)
|
||||
const readingListIds = readingList.bookIds
|
||||
|
||||
return (
|
||||
<div onClick={() => updateReadingList({ type: 'bookmark', retrievedBook: book })}>
|
||||
<Button style={{
|
||||
height: "30px",
|
||||
width: "30px",
|
||||
marginLeft: "0.25rem",
|
||||
display: "grid",
|
||||
justifyContent: "center",
|
||||
alignContent: "center" }}
|
||||
variant={ readingListIds.includes(book.id) ? "success" : "light"
|
||||
}>
|
||||
<span>
|
||||
🔖
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
|
||||
export default ({categoryName, categoryImage}) => {
|
||||
return (
|
||||
<div className="my-2 mx-2" aria-labelledby="category-description">
|
||||
<h4 id="category-description">
|
||||
{categoryImage} {categoryName}
|
||||
</h4>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
import '../styles/sidebar.css';
|
||||
import BookCard from './bookcard';
|
||||
import SortByDropdown, { FIELDS_TO_SORT_BY, compareFunctions } from './sortByDropdown';
|
||||
|
||||
export default ({ data, limit }) => {
|
||||
const [sortBy, setSortBy] = React.useState(FIELDS_TO_SORT_BY[0]);
|
||||
|
||||
const sortedBooks = React.useMemo(() => [...data.allBooksJson.edges]
|
||||
.sort(compareFunctions[sortBy.value]),
|
||||
[sortBy]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SortByDropdown sortBy={sortBy.label} onSortByItemClick={setSortBy} />
|
||||
{sortedBooks.map((x, index) => {
|
||||
const book = x.node;
|
||||
if (!limit || index < limit) {
|
||||
if (!book.description || book.description.length < 10) {
|
||||
return null;
|
||||
}
|
||||
return <BookCard book={book} key={book.id} />;
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from "react"
|
||||
import { useStaticQuery, graphql } from "gatsby"
|
||||
import Img from "gatsby-image"
|
||||
|
||||
/*
|
||||
* This component is built using `gatsby-image` to automatically serve optimized
|
||||
* images with lazy loading and reduced file sizes. The image is loaded using a
|
||||
* `useStaticQuery`, which allows us to load the image from directly within this
|
||||
* component, rather than having to pass the image data down from pages.
|
||||
*
|
||||
* For more information, see the docs:
|
||||
* - `gatsby-image`: https://gatsby.dev/gatsby-image
|
||||
* - `useStaticQuery`: https://www.gatsbyjs.org/docs/use-static-query/
|
||||
*/
|
||||
|
||||
const Image = () => {
|
||||
const data = useStaticQuery(graphql`
|
||||
query {
|
||||
placeholderImage: file(relativePath: { eq: "goodreads.png" }) {
|
||||
childImageSharp {
|
||||
fluid(maxWidth: 30) {
|
||||
...GatsbyImageSharpFluid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
return <Img fluid={data.placeholderImage.childImageSharp.fluid} />
|
||||
}
|
||||
|
||||
export default Image
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Link } from "gatsby"
|
||||
import PropTypes from "prop-types"
|
||||
import React from "react"
|
||||
|
||||
const Header = ({ siteTitle }) => (
|
||||
<header className="mx-2 bg-red d-none d-lg-block custom-header" aria-labelledby='main-title'>
|
||||
<h4 className="d-flex justify-content-end" id="main-title" style={{ margin: 16 }}>
|
||||
<Link
|
||||
to="/"
|
||||
style={{
|
||||
textDecorationColor: `none`,
|
||||
}}
|
||||
>
|
||||
{siteTitle}
|
||||
</Link>
|
||||
</h4>
|
||||
</header>
|
||||
)
|
||||
|
||||
Header.propTypes = {
|
||||
siteTitle: PropTypes.string,
|
||||
}
|
||||
|
||||
Header.defaultProps = {
|
||||
siteTitle: ``,
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from "react"
|
||||
import { useStaticQuery, graphql } from "gatsby"
|
||||
import Img from "gatsby-image"
|
||||
|
||||
/*
|
||||
* This component is built using `gatsby-image` to automatically serve optimized
|
||||
* images with lazy loading and reduced file sizes. The image is loaded using a
|
||||
* `useStaticQuery`, which allows us to load the image from directly within this
|
||||
* component, rather than having to pass the image data down from pages.
|
||||
*
|
||||
* For more information, see the docs:
|
||||
* - `gatsby-image`: https://gatsby.dev/gatsby-image
|
||||
* - `useStaticQuery`: https://www.gatsbyjs.org/docs/use-static-query/
|
||||
*/
|
||||
|
||||
const Image = () => {
|
||||
const data = useStaticQuery(graphql`
|
||||
query {
|
||||
placeholderImage: file(relativePath: { eq: "gatsby-astronaut.png" }) {
|
||||
childImageSharp {
|
||||
fluid(maxWidth: 300) {
|
||||
...GatsbyImageSharpFluid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
return <Img fluid={data.placeholderImage.childImageSharp.fluid} />
|
||||
}
|
||||
|
||||
export default Image
|
||||
@@ -1,622 +0,0 @@
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
main,
|
||||
menu,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
audio,
|
||||
canvas,
|
||||
progress,
|
||||
video {
|
||||
display: inline-block;
|
||||
}
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
[hidden],
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
a {
|
||||
background-color: transparent;
|
||||
-webkit-text-decoration-skip: objects;
|
||||
}
|
||||
a:active,
|
||||
a:hover {
|
||||
outline-width: 0;
|
||||
}
|
||||
abbr[title] {
|
||||
border-bottom: none;
|
||||
text-decoration: underline;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
b,
|
||||
strong {
|
||||
font-weight: inherit;
|
||||
font-weight: bolder;
|
||||
}
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
mark {
|
||||
background-color: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
optgroup {
|
||||
font-weight: 700;
|
||||
}
|
||||
button,
|
||||
input {
|
||||
overflow: visible;
|
||||
}
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
[type="reset"],
|
||||
[type="submit"],
|
||||
button,
|
||||
html [type="button"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner,
|
||||
button::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring,
|
||||
button:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
fieldset {
|
||||
border: 1px solid silver;
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em;
|
||||
}
|
||||
legend {
|
||||
box-sizing: border-box;
|
||||
color: inherit;
|
||||
display: table;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
[type="search"]::-webkit-search-cancel-button,
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
::-webkit-input-placeholder {
|
||||
color: inherit;
|
||||
opacity: 0.54;
|
||||
}
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
font: inherit;
|
||||
}
|
||||
html {
|
||||
font: 112.5%/1.45em georgia, serif;
|
||||
box-sizing: border-box;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
* {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
*:before {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
body {
|
||||
color: hsla(0, 0%, 0%, 0.8);
|
||||
font-family: georgia, serif;
|
||||
font-weight: normal;
|
||||
word-wrap: break-word;
|
||||
font-kerning: normal;
|
||||
-moz-font-feature-settings: "kern", "liga", "clig", "calt";
|
||||
-ms-font-feature-settings: "kern", "liga", "clig", "calt";
|
||||
-webkit-font-feature-settings: "kern", "liga", "clig", "calt";
|
||||
font-feature-settings: "kern", "liga", "clig", "calt";
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
}
|
||||
h1 {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
color: inherit;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
font-weight: bold;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-size: 2.25rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
h2 {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
color: inherit;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
font-weight: bold;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-size: 1.62671rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
h3 {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
color: inherit;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
font-weight: bold;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-size: 1.38316rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
h4 {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
color: inherit;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
font-weight: bold;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-size: 1rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
h5 {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
color: inherit;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
font-weight: bold;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-size: 0.85028rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
h6 {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
color: inherit;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
font-weight: bold;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-size: 0.78405rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
hgroup {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
}
|
||||
ul {
|
||||
margin-left: 1.45rem;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
list-style-position: outside;
|
||||
list-style-image: none;
|
||||
}
|
||||
ol {
|
||||
margin-left: 1.45rem;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
list-style-position: outside;
|
||||
list-style-image: none;
|
||||
}
|
||||
dl {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
}
|
||||
dd {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
}
|
||||
p {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
}
|
||||
figure {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
}
|
||||
pre {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.42;
|
||||
background: hsla(0, 0%, 0%, 0.04);
|
||||
border-radius: 3px;
|
||||
overflow: auto;
|
||||
word-wrap: normal;
|
||||
padding: 1.45rem;
|
||||
}
|
||||
table {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.45rem;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
fieldset {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
}
|
||||
blockquote {
|
||||
margin-left: 1.45rem;
|
||||
margin-right: 1.45rem;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
}
|
||||
form {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
}
|
||||
noscript {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
}
|
||||
iframe {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
}
|
||||
hr {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: calc(1.45rem - 1px);
|
||||
background: hsla(0, 0%, 0%, 0.2);
|
||||
border: none;
|
||||
height: 1px;
|
||||
}
|
||||
address {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
margin-bottom: 1.45rem;
|
||||
}
|
||||
b {
|
||||
font-weight: bold;
|
||||
}
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
}
|
||||
li {
|
||||
margin-bottom: calc(1.45rem / 2);
|
||||
}
|
||||
ol li {
|
||||
padding-left: 0;
|
||||
}
|
||||
ul li {
|
||||
padding-left: 0;
|
||||
}
|
||||
li > ol {
|
||||
margin-left: 1.45rem;
|
||||
margin-bottom: calc(1.45rem / 2);
|
||||
margin-top: calc(1.45rem / 2);
|
||||
}
|
||||
li > ul {
|
||||
margin-left: 1.45rem;
|
||||
margin-bottom: calc(1.45rem / 2);
|
||||
margin-top: calc(1.45rem / 2);
|
||||
}
|
||||
blockquote *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
li *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
p *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
li > p {
|
||||
margin-bottom: calc(1.45rem / 2);
|
||||
}
|
||||
code {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.45rem;
|
||||
}
|
||||
kbd {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.45rem;
|
||||
}
|
||||
samp {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.45rem;
|
||||
}
|
||||
abbr {
|
||||
border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
|
||||
cursor: help;
|
||||
}
|
||||
acronym {
|
||||
border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
|
||||
cursor: help;
|
||||
}
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
|
||||
cursor: help;
|
||||
text-decoration: none;
|
||||
}
|
||||
thead {
|
||||
text-align: left;
|
||||
}
|
||||
td,
|
||||
th {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid hsla(0, 0%, 0%, 0.12);
|
||||
font-feature-settings: "tnum";
|
||||
-moz-font-feature-settings: "tnum";
|
||||
-ms-font-feature-settings: "tnum";
|
||||
-webkit-font-feature-settings: "tnum";
|
||||
padding-left: 0.96667rem;
|
||||
padding-right: 0.96667rem;
|
||||
padding-top: 0.725rem;
|
||||
padding-bottom: calc(0.725rem - 1px);
|
||||
}
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
tt,
|
||||
code {
|
||||
background-color: hsla(0, 0%, 0%, 0.04);
|
||||
border-radius: 3px;
|
||||
font-family: "SFMono-Regular", Consolas, "Roboto Mono", "Droid Sans Mono",
|
||||
"Liberation Mono", Menlo, Courier, monospace;
|
||||
padding: 0;
|
||||
padding-top: 0.2em;
|
||||
padding-bottom: 0.2em;
|
||||
}
|
||||
pre code {
|
||||
background: none;
|
||||
line-height: 1.42;
|
||||
}
|
||||
code:before,
|
||||
code:after,
|
||||
tt:before,
|
||||
tt:after {
|
||||
letter-spacing: -0.2em;
|
||||
content: " ";
|
||||
}
|
||||
pre code:before,
|
||||
pre code:after,
|
||||
pre tt:before,
|
||||
pre tt:after {
|
||||
content: "";
|
||||
}
|
||||
@media only screen and (max-width: 480px) {
|
||||
html {
|
||||
font-size: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* Layout component that queries for data
|
||||
* with Gatsby's useStaticQuery component
|
||||
*
|
||||
* See: https://www.gatsbyjs.org/docs/use-static-query/
|
||||
*/
|
||||
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { useStaticQuery, graphql } from "gatsby"
|
||||
|
||||
import Header from "./header"
|
||||
import "./layout.css"
|
||||
import "bootstrap/dist/css/bootstrap.min.css"
|
||||
import { Container } from "react-bootstrap"
|
||||
|
||||
const Layout = ({ children }) => {
|
||||
const data = useStaticQuery(graphql`
|
||||
query SiteTitleQuery {
|
||||
site {
|
||||
siteMetadata {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header siteTitle={data.site.siteMetadata.title} />
|
||||
<Container fluid>
|
||||
<main>{children}</main>
|
||||
<footer class ="footer-text">
|
||||
© {new Date().getFullYear()}, Built with
|
||||
{` `}
|
||||
<a href="https://www.gatsbyjs.org">Gatsby</a>
|
||||
</footer>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Layout.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
export default Layout
|
||||
@@ -1,88 +0,0 @@
|
||||
/**
|
||||
* SEO component that queries for data with
|
||||
* Gatsby's useStaticQuery React hook
|
||||
*
|
||||
* See: https://www.gatsbyjs.org/docs/use-static-query/
|
||||
*/
|
||||
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import Helmet from "react-helmet"
|
||||
import { useStaticQuery, graphql } from "gatsby"
|
||||
|
||||
function SEO({ description, lang, meta, title }) {
|
||||
const { site } = useStaticQuery(
|
||||
graphql`
|
||||
query {
|
||||
site {
|
||||
siteMetadata {
|
||||
title
|
||||
description
|
||||
author
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
const metaDescription = description || site.siteMetadata.description
|
||||
|
||||
return (
|
||||
<Helmet
|
||||
htmlAttributes={{
|
||||
lang,
|
||||
}}
|
||||
title={title}
|
||||
titleTemplate={`%s | ${site.siteMetadata.title}`}
|
||||
meta={[
|
||||
{
|
||||
name: `description`,
|
||||
content: metaDescription,
|
||||
},
|
||||
{
|
||||
property: `og:title`,
|
||||
content: title,
|
||||
},
|
||||
{
|
||||
property: `og:description`,
|
||||
content: metaDescription,
|
||||
},
|
||||
{
|
||||
property: `og:type`,
|
||||
content: `website`,
|
||||
},
|
||||
{
|
||||
name: `twitter:card`,
|
||||
content: `summary`,
|
||||
},
|
||||
{
|
||||
name: `twitter:creator`,
|
||||
content: site.siteMetadata.author,
|
||||
},
|
||||
{
|
||||
name: `twitter:title`,
|
||||
content: title,
|
||||
},
|
||||
{
|
||||
name: `twitter:description`,
|
||||
content: metaDescription,
|
||||
},
|
||||
].concat(meta)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
SEO.defaultProps = {
|
||||
lang: `en`,
|
||||
meta: [],
|
||||
description: ``,
|
||||
}
|
||||
|
||||
SEO.propTypes = {
|
||||
description: PropTypes.string,
|
||||
lang: PropTypes.string,
|
||||
meta: PropTypes.arrayOf(PropTypes.object),
|
||||
title: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default SEO
|
||||
@@ -1,49 +0,0 @@
|
||||
import React, { useContext } from "react"
|
||||
import { Navbar, Nav } from "react-bootstrap"
|
||||
import { StaticQuery, graphql, Link } from "gatsby"
|
||||
import "../styles/sidebar.css"
|
||||
import { BookmarkContext } from '../context/globalState'
|
||||
var slugify = require('slugify')
|
||||
|
||||
export default () => {
|
||||
const { readingList } = useContext(BookmarkContext)
|
||||
|
||||
return (
|
||||
<StaticQuery
|
||||
query={graphql`
|
||||
query CategoryQuery {
|
||||
allCategoriesJson {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
emoji
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
render={data => (
|
||||
<Navbar className="sidebar-sticky" collapseOnSelect expand="lg" bg="ligt" variant="light">
|
||||
<Navbar.Toggle aria-controls="responsive-navbar-nav" />
|
||||
<Navbar.Collapse>
|
||||
<div>
|
||||
<div style={{position: "relative", left: "0.9rem", paddingBottom: "0.2rem"}}>
|
||||
<Link to="/readingList">🔖 Reading List ({readingList.bookIds.length})</Link>
|
||||
</div>
|
||||
{data.allCategoriesJson.edges.map(function(x, index) {
|
||||
return (
|
||||
<Nav.Item key={index}>
|
||||
<Nav.Link href={slugify(x.node.name)}>
|
||||
{x.node.emoji} {x.node.name}
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Navbar.Collapse>
|
||||
</Navbar>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Dropdown } from 'react-bootstrap';
|
||||
|
||||
export const compareFunctions = {
|
||||
title_a_z: ({ node: bookOne }, { node: bookTwo }) => bookOne.title.localeCompare(bookTwo.title),
|
||||
title_z_a: ({ node: bookOne }, { node: bookTwo }) => bookTwo.title.localeCompare(bookOne.title),
|
||||
year_descending: ({ node: bookOne }, { node: bookTwo }) => Number(bookTwo.year) - Number(bookOne.year),
|
||||
year_ascending: ({ node: bookOne }, { node: bookTwo }) => Number(bookOne.year) - Number(bookTwo.year),
|
||||
rating_descending: ({ node: bookOne }, { node: bookTwo }) => Number(bookTwo.rating) - Number(bookOne.rating),
|
||||
rating_ascending: ({ node: bookOne }, { node: bookTwo }) => Number(bookOne.rating) - Number(bookTwo.rating),
|
||||
};
|
||||
|
||||
export const FIELDS_TO_SORT_BY = [
|
||||
{ label: 'Rating, high to low', value: 'rating_descending' },
|
||||
{ label: 'Rating, low to high', value: 'rating_ascending' },
|
||||
{ label: 'Publication year, new to old', value: 'year_descending' },
|
||||
{ label: 'Publication year, old to new', value: 'year_ascending' },
|
||||
{ label: 'Title, A-Z', value: 'title_a_z' },
|
||||
{ label: 'Title, Z-A', value: 'title_z_a' },
|
||||
];
|
||||
|
||||
export default ({ sortBy, onSortByItemClick }) => (
|
||||
<div className="mb-2">
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle variant="outline">
|
||||
Sort By:
|
||||
{' '}
|
||||
{sortBy}
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
{FIELDS_TO_SORT_BY.map((field, index) => (
|
||||
<Dropdown.Item key={index} onClick={() => onSortByItemClick(field)}>
|
||||
{field.label}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
@@ -1,33 +0,0 @@
|
||||
export default function bookReducer(state, action) {
|
||||
let readingListCopy = {...state}
|
||||
|
||||
switch (action.type) {
|
||||
case 'init': {
|
||||
if (action.content) {
|
||||
return action.content
|
||||
}
|
||||
return readingListCopy
|
||||
}
|
||||
case 'bookmark': {
|
||||
let { bookIds, books } = readingListCopy
|
||||
const { retrievedBook } = action
|
||||
const retrievedBookId = retrievedBook.id
|
||||
// Delete existing bookmark
|
||||
if (bookIds.includes(retrievedBookId)) {
|
||||
readingListCopy.bookIds = bookIds.filter(id => id !== retrievedBookId)
|
||||
delete books[retrievedBookId]
|
||||
if (typeof window !== undefined) {
|
||||
localStorage.setItem('Bookmarks', JSON.stringify(readingListCopy))
|
||||
}
|
||||
// Add new bookmark
|
||||
} else {
|
||||
books[retrievedBookId] = retrievedBook
|
||||
bookIds.push(retrievedBookId)
|
||||
if (typeof window !== undefined) {
|
||||
localStorage.setItem('Bookmarks', JSON.stringify(readingListCopy))
|
||||
}
|
||||
}
|
||||
return readingListCopy
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import React, { useReducer, useEffect } from 'react'
|
||||
import bookReducer from './bookReducer'
|
||||
|
||||
export const BookmarkContext = React.createContext()
|
||||
|
||||
export default function GlobalState({children}) {
|
||||
let [readingList, updateReadingList] = useReducer(bookReducer, {
|
||||
books: {},
|
||||
bookIds: []
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== undefined) {
|
||||
const retrievedBooks = JSON.parse(localStorage.getItem('Bookmarks'))
|
||||
console.log(retrievedBooks)
|
||||
updateReadingList({type: 'init', content: retrievedBooks})
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<BookmarkContext.Provider value={{readingList, updateReadingList}}>
|
||||
{children}
|
||||
</BookmarkContext.Provider>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,74 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "Startups and Business",
|
||||
"emoji": "🚀"
|
||||
},
|
||||
{
|
||||
"name": "Philosophy And Psychology",
|
||||
"emoji": "☯️"
|
||||
},
|
||||
{
|
||||
"name": "Autobiographies and Biographies",
|
||||
"emoji": "👩🏾"
|
||||
},
|
||||
{
|
||||
"name": "History",
|
||||
"emoji": "📜"
|
||||
},
|
||||
{
|
||||
"name": "Science and Medicine",
|
||||
"emoji": "🔬"
|
||||
},
|
||||
{
|
||||
"name": "Logic and Problem Solving",
|
||||
"emoji": "🧩"
|
||||
},
|
||||
{
|
||||
"name": "Politics",
|
||||
"emoji": "🗳️"
|
||||
},
|
||||
{
|
||||
"name": "Economics",
|
||||
"emoji": "📈"
|
||||
},
|
||||
{
|
||||
"name": "Gender",
|
||||
"emoji": "🏳️🌈"
|
||||
},
|
||||
{
|
||||
"name": "Sexuality",
|
||||
"emoji": "😘"
|
||||
},
|
||||
{
|
||||
"name": "Education",
|
||||
"emoji": "🏫"
|
||||
},
|
||||
{
|
||||
"name": "Writing",
|
||||
"emoji": "📝"
|
||||
},
|
||||
{
|
||||
"name": "Theater and Film",
|
||||
"emoji": "🎬"
|
||||
},
|
||||
{
|
||||
"name": "Health",
|
||||
"emoji": "👩⚕️"
|
||||
},
|
||||
{
|
||||
"name": "Travel",
|
||||
"emoji": "🛩️"
|
||||
},
|
||||
{
|
||||
"name": "Language",
|
||||
"emoji": "🉐"
|
||||
},
|
||||
{
|
||||
"name": "Nature",
|
||||
"emoji": "🌲"
|
||||
},
|
||||
{
|
||||
"name": "Art",
|
||||
"emoji": "🖌️"
|
||||
}
|
||||
]
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 163 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 KiB |
@@ -1,14 +0,0 @@
|
||||
import React from "react"
|
||||
|
||||
import Layout from "../components/layout"
|
||||
import SEO from "../components/seo"
|
||||
|
||||
const NotFoundPage = () => (
|
||||
<Layout>
|
||||
<SEO title="404: Not found" />
|
||||
<h1 id="title">NOT FOUND</h1>
|
||||
<p>You just hit a route that doesn't exist... the sadness.</p>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
export default NotFoundPage
|
||||
@@ -1,79 +0,0 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { graphql } from "gatsby"
|
||||
|
||||
import Layout from "../components/layout"
|
||||
import SEO from "../components/seo"
|
||||
import SideBar from "../components/sidebar"
|
||||
import { Container, Row, Col, Navbar } from "react-bootstrap"
|
||||
import BookFeed from "../components/feed"
|
||||
|
||||
function myFunction(setMaximumBooksToShow, maximumBooksToShow) {
|
||||
if (
|
||||
document.documentElement.clientHeight +
|
||||
document.documentElement.scrollTop >=
|
||||
document.documentElement.scrollHeight
|
||||
) {
|
||||
setMaximumBooksToShow(maximumBooksToShow + 12)
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ data }) => {
|
||||
let [maximumBooksToShow, setMaximumBooksToShow] = useState(12)
|
||||
|
||||
useEffect(() => {
|
||||
window.document.onscroll = () =>
|
||||
myFunction(setMaximumBooksToShow, maximumBooksToShow)
|
||||
})
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<SEO title="Home" />
|
||||
<Container fluid>
|
||||
<Row>
|
||||
<Col lg={2}>
|
||||
<SideBar />
|
||||
</Col>
|
||||
<Col lg={10}>
|
||||
<BookFeed data={data} limit={maximumBooksToShow} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
{/* <footer style={{marginLeft: 150,
|
||||
width: `100%`,
|
||||
position: `fixed`,
|
||||
bottom: 0}}>
|
||||
© {new Date().getFullYear()}, Built with
|
||||
{` `}
|
||||
<a href="https://www.gatsbyjs.org">Gatsby</a>
|
||||
</footer> */}
|
||||
</Row>
|
||||
</Container>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export const query = graphql`
|
||||
query MyQuery {
|
||||
allBooksJson(
|
||||
sort: {
|
||||
fields: [rating]
|
||||
order: DESC
|
||||
}
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
url
|
||||
rating
|
||||
author
|
||||
year
|
||||
category
|
||||
description
|
||||
image_url
|
||||
amazon_url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from "react"
|
||||
import { Link } from "gatsby"
|
||||
|
||||
import Layout from "../components/layout"
|
||||
import SEO from "../components/seo"
|
||||
|
||||
const SecondPage = () => (
|
||||
<Layout>
|
||||
<SEO title="Page two" />
|
||||
<h1>Hi from the second page</h1>
|
||||
<p>Welcome to page 2</p>
|
||||
<Link to="/">Go back to the homepage</Link>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
export default SecondPage
|
||||
@@ -1,36 +0,0 @@
|
||||
import React, { useContext } from "react"
|
||||
import { Link } from "gatsby"
|
||||
import { Container, Row, Col } from "react-bootstrap"
|
||||
import SideBar from "../components/sidebar"
|
||||
import Layout from "../components/layout"
|
||||
import SEO from "../components/seo"
|
||||
import Bookcard from "../components/bookcard"
|
||||
import { BookmarkContext } from "../context/globalState"
|
||||
|
||||
const ReadingList = () => {
|
||||
const { readingList } = useContext(BookmarkContext)
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<SEO title="Reading list" />
|
||||
<Container fluid>
|
||||
<Row>
|
||||
<Col xs={2}>
|
||||
<SideBar />
|
||||
</Col>
|
||||
<Col>
|
||||
<h2>Your reading list</h2>
|
||||
<Link to="/">Go back to the homepage</Link>
|
||||
{
|
||||
readingList.bookIds.map(bookId => {
|
||||
return <Bookcard book={readingList.books[bookId]} key={bookId} />
|
||||
})
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<p>Reading List</p>
|
||||
</Layout>
|
||||
)}
|
||||
|
||||
export default ReadingList
|
||||
@@ -1,11 +0,0 @@
|
||||
.custom-header{
|
||||
position: fixed;
|
||||
}
|
||||
.footer-text {
|
||||
text-align:center;
|
||||
padding-bottom:1 rem;
|
||||
}
|
||||
|
||||
.dark-mode {
|
||||
background-color: #3c4759;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
min-height: 100vh !important;
|
||||
margin: 70px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#sidebar-wrapper {
|
||||
min-height: 100vh !important;
|
||||
width: 100vw;
|
||||
margin-left: -1rem;
|
||||
-webkit-transition: margin 0.25s ease-out;
|
||||
-moz-transition: margin 0.25s ease-out;
|
||||
-o-transition: margin 0.25s ease-out;
|
||||
transition: margin 0.25s ease-out;
|
||||
}
|
||||
#sidebar-wrapper .sidebar-heading {
|
||||
padding: 0.875rem 1.25rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
#page-content-wrapper {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.navbar {
|
||||
padding: 10px 0px;
|
||||
}
|
||||
.navbar-toggler {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { graphql } from "gatsby"
|
||||
|
||||
import Layout from "../components/layout"
|
||||
import SEO from "../components/seo"
|
||||
import SideBar from "../components/sidebar"
|
||||
import CategoryDescription from "../components/categorydescription"
|
||||
import { Container, Row, Col } from "react-bootstrap"
|
||||
import BookFeed from "../components/feed"
|
||||
|
||||
const basicTemplate = props => {
|
||||
const { pageContext } = props
|
||||
const { categoryName, data, image } = pageContext
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<SEO title="Home" />
|
||||
<Container fluid>
|
||||
<Row>
|
||||
<Col lg={2}>
|
||||
<SideBar />
|
||||
</Col>
|
||||
<Col lg={10}>
|
||||
<CategoryDescription categoryName={categoryName} categoryImage={image} />
|
||||
<BookFeed data={data} categoryName={categoryName} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
export default basicTemplate
|
||||
16489
app/yarn.lock
16489
app/yarn.lock
File diff suppressed because it is too large
Load Diff
3
package-lock.json
generated
3
package-lock.json
generated
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1
|
||||
}
|
||||
7
utils/.gitignore
vendored
7
utils/.gitignore
vendored
@@ -1,8 +1,3 @@
|
||||
.idea
|
||||
__pycache__
|
||||
config.py
|
||||
bin/
|
||||
include/
|
||||
lib/
|
||||
out.json
|
||||
pip-selfcheck.json
|
||||
config.py
|
||||
@@ -1,34 +0,0 @@
|
||||
This contains documentation on `housekeep.py` which was a command developed for converting from legacy README
|
||||
format to the new format. This is not used anymore.
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
### Setup
|
||||
1) Copy ```config-sample.py``` to ```config.py```
|
||||
|
||||
```bash
|
||||
cp config-sample.py config.py
|
||||
```
|
||||
2) Get a GoodReads API key [here](https://www.goodreads.com/api/keys)
|
||||
3) Copy your public key to the ```config.py``` file
|
||||
|
||||
### Converting
|
||||
|
||||
Run
|
||||
|
||||
```bash
|
||||
python3 housekeep.py --file_type='old'
|
||||
```
|
||||
|
||||
The ``--file_type='old'`` flag is needed if the books are displayed in the old format (in a list). New format is the default (when the records are displayed in tables).
|
||||
|
||||
This will convert to the new format and save it to ``README-new.md``.
|
||||
|
||||
**Note!** the first conversion can take some time, we wait 1 second between each request to GoodReads to not abuse the API.
|
||||
After the first generation only missing records will be tried to be retrieved.
|
||||
|
||||
Run to see the available arguments
|
||||
```bash
|
||||
python3 housekeep.py --help
|
||||
```
|
||||
@@ -1,37 +1,34 @@
|
||||
`utils/` mainly contains scripts for generating `app/src/data/books.json` file from `README.md`
|
||||
# Utils for Mind-Expanding-Books
|
||||
|
||||
In most cases, you don't want to run the scripts in this directory or make changes to it.
|
||||
The only time you want to mess with the scripts is when you want to fetch some extra data from `API` or
|
||||
want to update the `app/src/data/books.json` file to include a newly added book or category.
|
||||
Simple command line interface to extend and order the Mind-Expanding-Books list.
|
||||
|
||||
## Generating `app/src/data/books.json`
|
||||
## Getting started
|
||||
|
||||
The website shows name of the book, year, rating, cover, amazon link, etc in book card. Some of the
|
||||
data like name, year, rating etc is present in the [main README.md](../README.md). Other details
|
||||
like cover photo, amazon link etc is fetched from various APIs.
|
||||
|
||||
The script that fetches all these extra data lives in `utils/update_json_files.py`
|
||||
|
||||
The script goes through all the books in [main README.md](../README.md) and starts fetching the extra details
|
||||
from `Goodreads` and `Google Search` API. And the result is stored in `utils/books.json` and `utils/book_name_to_details.json`
|
||||
(used for caching only).
|
||||
|
||||
Once the script completes the run, the `utils/books.json` file is copied to `app/src/data/books.json` **manually**.
|
||||
|
||||
And the website uses the data from `app/src/data/books.json` for generating the pages.
|
||||
|
||||
## Install packages for script
|
||||
### Setup
|
||||
1) Copy ```config-sample.py``` to ```config.py```
|
||||
|
||||
```bash
|
||||
cd utils
|
||||
virtualenv .
|
||||
source bin/activate
|
||||
pip install -r requirements.txt
|
||||
cp config-sample.py config.py
|
||||
```
|
||||
2) Get a GoodReads API key [here](https://www.goodreads.com/api/keys)
|
||||
3) Copy your public key to the ```config.py``` file
|
||||
|
||||
### Converting
|
||||
|
||||
Run
|
||||
|
||||
```bash
|
||||
python3 housekeep.py --file_type='old'
|
||||
```
|
||||
|
||||
## Executing the script
|
||||
The ``--file_type='old'`` flag is needed if the books are displayed in the old format (in a list). New format is the default (when the records are displayed in tables).
|
||||
|
||||
- Register at [goodreads](https://www.goodreads.com)
|
||||
- Apply for a developer api [here](https://www.goodreads.com/api)
|
||||
- Copy utils/config-sample.py as utils/config.py
|
||||
- Fill in the API Key credentials
|
||||
This will convert to the new format and save it to ``README-new.md``.
|
||||
|
||||
**Note!** the first conversion can take some time, we wait 1 second between each request to GoodReads to not abuse the API.
|
||||
After the first generation only missing records will be tried to be retrieved.
|
||||
|
||||
Run to see the available arguments
|
||||
```bash
|
||||
python3 housekeep.py --help
|
||||
```
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
5728
utils/books.json
5728
utils/books.json
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,2 @@
|
||||
# save this file as 'config.py' and then, fill it with you api key
|
||||
GOODREADS_PUBLIC_API_KEY = "write here your goodreads public API key"
|
||||
GOOGLE_SEARCH_RAPIDAPI_HOST = ""
|
||||
GOOGLE_SEARCH_RAPIDAPI_KEY = ""
|
||||
GOOGLE_BOOK_API_KEY = ""
|
||||
GOODREADS_PUBLIC_API_KEY = 'write here your goodreads public API key'
|
||||
@@ -3,105 +3,51 @@ import xml.etree.ElementTree as ET
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
import requests
|
||||
from config import GOODREADS_PUBLIC_API_KEY
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from config import GOODREADS_PUBLIC_API_KEY, GOOGLE_SEARCH_RAPIDAPI_HOST, GOOGLE_SEARCH_RAPIDAPI_KEY, GOOGLE_BOOK_API_KEY
|
||||
from googlesearch import search
|
||||
|
||||
def get_details(book_object):
|
||||
|
||||
url = "http://www.goodreads.com/book/title.xml?key={}&title={}".format(
|
||||
GOODREADS_PUBLIC_API_KEY, urllib.parse.quote_plus(book_object["title"])
|
||||
)
|
||||
print(url)
|
||||
url = "http://www.goodreads.com/book/title.xml?key={}&title={}".format(GOODREADS_PUBLIC_API_KEY,
|
||||
urllib.parse.quote_plus(book_object['title']))
|
||||
|
||||
try:
|
||||
time_to_sleep = 1
|
||||
while True:
|
||||
response = urllib.request.urlopen(url)
|
||||
print(response.getcode())
|
||||
if response.getcode() == 429:
|
||||
time_to_sleep = time_to_sleep * 2
|
||||
print("Sleeping for {}".format(time_to_sleep))
|
||||
time.sleep(time_to_sleep)
|
||||
else:
|
||||
break
|
||||
tree = ET.ElementTree(file=response)
|
||||
tree = ET.ElementTree(file=urllib.request.urlopen(url))
|
||||
root = tree.getroot()
|
||||
book = root.find("book")
|
||||
book_object["year"] = book.find("publication_year").text or ""
|
||||
book_object["lang"] = book.find("language_code").text
|
||||
book_object["rating"] = book.find("average_rating").text
|
||||
book_object["pages"] = book.find("num_pages").text
|
||||
book_object["image_url"] = book.find("image_url").text
|
||||
book_object["isbn"] = book.find("isbn").text
|
||||
|
||||
description = book.find("description").text
|
||||
if description:
|
||||
book_object["description"] = BeautifulSoup(description).text
|
||||
else:
|
||||
book_object["description"] = ""
|
||||
if GOOGLE_BOOK_API_KEY.strip(" "):
|
||||
# Attempt to use Google Book API
|
||||
url = "https://www.googleapis.com/books/v1/volumes?q={}+inauthor:{}&key={}".format(
|
||||
book_object["title"], book_object["author"], GOOGLE_BOOK_API_KEY,
|
||||
)
|
||||
response = requests.request("GET", url)
|
||||
|
||||
for item in response.json()["items"]:
|
||||
if "description" in item["volumeInfo"]:
|
||||
book_object["description"] = item["volumeInfo"]["description"]
|
||||
break
|
||||
|
||||
print("Fetching amazon link")
|
||||
|
||||
url = "https://google-search3.p.rapidapi.com/api/v1/search/q=site:amazon.com {} {}".format(book_object["title"], book_object["author"])
|
||||
|
||||
headers = {
|
||||
'x-rapidapi-host': GOOGLE_SEARCH_RAPIDAPI_HOST,
|
||||
'x-rapidapi-key': GOOGLE_SEARCH_RAPIDAPI_KEY,
|
||||
}
|
||||
|
||||
response = requests.request("GET", url, headers=headers)
|
||||
book_object["amazon_url"] = response.json()["results"][0]["link"]
|
||||
return True
|
||||
book = root.find('book')
|
||||
book_object['year'] = book.find('publication_year').text or ''
|
||||
book_object['lang'] = book.find('language_code').text
|
||||
book_object['rating'] = book.find('average_rating').text
|
||||
book_object['pages'] = book.find('num_pages').text
|
||||
except urllib.error.HTTPError as e:
|
||||
print(
|
||||
"Error getting book details from GoodReads for book: {}. \nGot error: ".format(
|
||||
book_object["title"]
|
||||
)
|
||||
)
|
||||
print(str(e.getcode()) + " " + e.msg)
|
||||
return False
|
||||
print('Error getting book details from GoodReads for book: {}. \nGot error: '.format(book_object['title']))
|
||||
print(str(e.getcode()) + ' ' + e.msg)
|
||||
|
||||
|
||||
def get_goodread_info(library, force):
|
||||
def get_goodread_info(library):
|
||||
import sys
|
||||
|
||||
print("")
|
||||
print("Getting GoodReads data...")
|
||||
print('')
|
||||
print('Getting GoodReads data...')
|
||||
|
||||
processed = 0
|
||||
total_book_count = 0
|
||||
for key in library:
|
||||
total_book_count += len(library[key])
|
||||
|
||||
for category in library:
|
||||
book_list = library[category]
|
||||
|
||||
for chapter in library:
|
||||
book_list = library[chapter]
|
||||
for book in book_list:
|
||||
# do not call the api again if we already have the infomation
|
||||
if not force and "rating" in book and book["rating"]:
|
||||
if 'rating' in book and book['rating']:
|
||||
processed += 1
|
||||
continue
|
||||
get_details(book)
|
||||
processed += 1
|
||||
|
||||
print(
|
||||
"{}/{} records processed.".format(processed, total_book_count), end="\b"
|
||||
)
|
||||
sys.stdout.write("\r")
|
||||
print('{}/{} records processed.'.format(processed, total_book_count), end="\b")
|
||||
sys.stdout.write('\r')
|
||||
sys.stdout.flush() # <- makes python print it anyway
|
||||
|
||||
# need to wait a second between the requests, to not abuse the API
|
||||
time.sleep(1)
|
||||
time.sleep(1)
|
||||
@@ -1,5 +1,3 @@
|
||||
import simplejson
|
||||
|
||||
# we assume that every line after # Books
|
||||
# starting with * is a book title if file type is old
|
||||
# starting with | (and not with | Name or |--) is a book if the file type is new
|
||||
@@ -7,71 +5,51 @@ import simplejson
|
||||
# ARGUMENT HANDLING
|
||||
try:
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Process file.")
|
||||
parser.add_argument("--in_file", help="File to process, defaults to ./../README.MD")
|
||||
parser = argparse.ArgumentParser(description='Process file.')
|
||||
parser.add_argument(
|
||||
"--out_file", help="File to save to, defaults to ./../README-NEW.MD"
|
||||
)
|
||||
'--in_file',
|
||||
help='File to process, defaults to ./../README.MD')
|
||||
parser.add_argument(
|
||||
"--input_file_type",
|
||||
choices=["old", "new"],
|
||||
help="old if links are displayed in a list, new if in a table",
|
||||
)
|
||||
'--out_file',
|
||||
help='File to save to, defaults to ./../README-NEW.MD')
|
||||
parser.add_argument(
|
||||
"--sort_by",
|
||||
choices=["rating", "title", "author", "year"],
|
||||
help="defaults to rating",
|
||||
)
|
||||
parser.add_argument("--force", dest="force", action="store_true", default=False)
|
||||
'--file_type',
|
||||
choices=['old', 'new'],
|
||||
help='old if links are displayed in a list, new if in a table')
|
||||
parser.add_argument(
|
||||
"--store-json", dest="store_json", action="store_true", default=False
|
||||
)
|
||||
'--sort_by',
|
||||
choices = ['rating', 'title', 'author', 'year'],
|
||||
help='defaults to rating')
|
||||
flags = parser.parse_args()
|
||||
except ImportError:
|
||||
flags = None
|
||||
|
||||
|
||||
def sort(library, key_to_sort_on, reverse=False):
|
||||
def sort(library, key_to_sort_on, reverse = False):
|
||||
new_library = {}
|
||||
for key in library:
|
||||
books = library[key]
|
||||
new_library[key] = sorted(
|
||||
books, key=lambda k: k[key_to_sort_on], reverse=reverse
|
||||
)
|
||||
new_library[key] = sorted(books, key=lambda k: k[key_to_sort_on], reverse=reverse)
|
||||
return new_library
|
||||
|
||||
|
||||
def format_library(library):
|
||||
formated_library = []
|
||||
for category in library:
|
||||
for book in library[category]:
|
||||
book["category"] = category[len("## ") :]
|
||||
formated_library.append(book)
|
||||
return formated_library
|
||||
|
||||
|
||||
def main():
|
||||
from read_file import load
|
||||
from gooodreads import get_goodread_info
|
||||
from write_file import render
|
||||
|
||||
in_file = flags.in_file or "./../README.md"
|
||||
out_file = flags.out_file or "./../README-new.md"
|
||||
input_file_type = flags.input_file_type or "new"
|
||||
sort_by = flags.sort_by or "rating"
|
||||
force = flags.force
|
||||
store_json = flags.store_json
|
||||
reverse = True if sort_by == "rating" else False
|
||||
in_file = flags.in_file or './../README.MD'
|
||||
out_file = flags.out_file or './../README-new.md'
|
||||
file_type = flags.file_type or 'new'
|
||||
sort_by = flags.sort_by or 'rating'
|
||||
reverse = True if sort_by == 'rating' else False
|
||||
|
||||
library = load(in_file, input_file_type)
|
||||
get_goodread_info(library, force)
|
||||
library = load(in_file, file_type)
|
||||
get_goodread_info(library)
|
||||
library = sort(library, sort_by, reverse)
|
||||
render(in_file, out_file, library)
|
||||
if store_json:
|
||||
with open("out.json", "w") as f:
|
||||
f.write(simplejson.dumps(format_library(library), indent=4, sort_keys=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
home = /usr
|
||||
implementation = CPython
|
||||
version_info = 3.8.5.final.0
|
||||
virtualenv = 20.0.20
|
||||
include-system-site-packages = false
|
||||
base-prefix = /usr
|
||||
base-exec-prefix = /usr
|
||||
base-executable = /usr/bin/python3
|
||||
@@ -8,24 +8,24 @@ def read_file_content(file):
|
||||
# old (list)
|
||||
def parse_book_string(book_string):
|
||||
book = {}
|
||||
book["title"] = book_string.split("[")[1].split("]")[0]
|
||||
book["url"] = book_string.split("]")[1].split("(")[1].split(")")[0]
|
||||
book["author"] = book_string.split(" by ")[-1]
|
||||
book["rating"] = ""
|
||||
book["year"] = ""
|
||||
book['title'] = book_string.split('[')[1].split(']')[0]
|
||||
book['url'] = book_string.split(']')[1].split('(')[1].split(')')[0]
|
||||
book['author'] = book_string.split(' by ')[-1]
|
||||
book['rating'] = ''
|
||||
book['year'] = ''
|
||||
return book
|
||||
|
||||
|
||||
# new (table)
|
||||
def parse_book_string_new(book_string):
|
||||
book = {}
|
||||
book_split = book_string.split("|")
|
||||
book_split = book_string.split('|')
|
||||
# print(book_split)
|
||||
book["title"] = book_split[1].strip()
|
||||
book["author"] = book_split[2].strip()
|
||||
book["url"] = book_split[3].strip().split("[")[1].split("(")[1].split(")")[0]
|
||||
book["rating"] = book_split[3].strip().split("[")[1].split("]")[0]
|
||||
book["year"] = book_split[4].strip()
|
||||
book['title'] = book_split[1].strip()
|
||||
book['author'] = book_split[2].strip()
|
||||
book['url'] = book_split[3].strip().split('[')[1].split('(')[1].split(')')[0]
|
||||
book['rating'] = book_split[3].strip().split('[')[1].split(']')[0]
|
||||
book['year'] = book_split[4].strip()
|
||||
return book
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ def load(file, file_type):
|
||||
file = read_file_content(file)
|
||||
|
||||
# we start one line after tilte # Books
|
||||
line_to_start = file.index("# Books") + 1
|
||||
current_title = ""
|
||||
line_to_start = file.index('# Books') + 1
|
||||
current_title = ''
|
||||
books_under_current_title = []
|
||||
library = {}
|
||||
|
||||
@@ -42,7 +42,7 @@ def load(file, file_type):
|
||||
line = file[i]
|
||||
|
||||
# we have a title
|
||||
if line.startswith("##"):
|
||||
if line.startswith('##'):
|
||||
if len(current_title) == 0:
|
||||
current_title = line
|
||||
else:
|
||||
@@ -52,16 +52,12 @@ def load(file, file_type):
|
||||
continue
|
||||
|
||||
# we have a book
|
||||
if file_type == "old":
|
||||
if line.startswith("*"):
|
||||
if file_type == 'old':
|
||||
if line.startswith('*'):
|
||||
book = parse_book_string(line)
|
||||
books_under_current_title.append(book)
|
||||
else:
|
||||
if (
|
||||
line.startswith("|")
|
||||
and not line.startswith("| Name")
|
||||
and not line.startswith("|---")
|
||||
):
|
||||
if line.startswith('|') and not line.startswith('| Name') and not line.startswith('|---'):
|
||||
book = parse_book_string_new(line)
|
||||
books_under_current_title.append(book)
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
appdirs==1.4.4
|
||||
attrs==20.1.0
|
||||
black==19.10b0
|
||||
click==7.1.2
|
||||
pathspec==0.8.0
|
||||
regex==2020.7.14
|
||||
toml==0.10.1
|
||||
typed-ast==1.4.1
|
||||
amzsear==2.0.1
|
||||
appdirs==1.4.4
|
||||
attrs==20.1.0
|
||||
beautifulsoup4==4.9.1
|
||||
black==19.10b0
|
||||
bs4==0.0.1
|
||||
certifi==2020.6.20
|
||||
chardet==3.0.4
|
||||
click==7.1.2
|
||||
google==3.0.0
|
||||
idna==2.10
|
||||
pathspec==0.8.0
|
||||
regex==2020.7.14
|
||||
requests==2.24.0
|
||||
soupsieve==2.0.1
|
||||
toml==0.10.1
|
||||
typed-ast==1.4.1
|
||||
urllib3==1.25.10
|
||||
@@ -1,94 +0,0 @@
|
||||
import json
|
||||
import time
|
||||
|
||||
from read_file import load
|
||||
from gooodreads import get_details
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
required_fields = [
|
||||
"title",
|
||||
"author",
|
||||
"url",
|
||||
"rating",
|
||||
"year",
|
||||
"pages",
|
||||
"image_url",
|
||||
"description",
|
||||
"category",
|
||||
"amazon_url",
|
||||
]
|
||||
|
||||
|
||||
def book_has_all_fields(book):
|
||||
for required_field in required_fields:
|
||||
if required_field not in existing_book:
|
||||
print(f"Missing {required_field}")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def clean_category(category_raw):
|
||||
if "### " in category_raw:
|
||||
return category_raw[4:]
|
||||
if "## " in category_raw:
|
||||
return category_raw[3:]
|
||||
|
||||
def validate_bookcover(book_details):
|
||||
"""
|
||||
Check if goodreads returns a nophoto
|
||||
Use open library to fetch the book cover
|
||||
based on ISBN
|
||||
|
||||
Args:
|
||||
book_details: Book info returned as json by goodreads API
|
||||
|
||||
Returns:
|
||||
This API checks for book cover, and returns with a valid
|
||||
bookcover if nophoto found on goodreads, using openlibrary
|
||||
"""
|
||||
no_photo_url='https://s.gr-assets.com/assets/nophoto/book/'
|
||||
open_library_url='http://covers.openlibrary.org/b/isbn/{isbn}-M.jpg'
|
||||
|
||||
if (book_details['image_url'].__contains__(no_photo_url)):
|
||||
book_details['image_url'] = open_library_url.format(isbn=book_details['isbn'])
|
||||
return book_details
|
||||
|
||||
if __name__ == "__main__":
|
||||
library = load("../README.md", "new")
|
||||
existing_book_names_to_details = json.load(open("book_name_to_details.json"))
|
||||
|
||||
for category in library:
|
||||
category_name = clean_category(category)
|
||||
for book in library[category]:
|
||||
if (title := book["title"]) in existing_book_names_to_details:
|
||||
existing_book = existing_book_names_to_details[title]
|
||||
if book_has_all_fields(existing_book):
|
||||
print(f"🆗 {title}")
|
||||
continue
|
||||
new_book = {
|
||||
"title": title,
|
||||
"author": book["author"],
|
||||
"url": book["url"],
|
||||
"category": category_name,
|
||||
}
|
||||
fetched = get_details(new_book)
|
||||
if fetched:
|
||||
print(f"✅ {title}")
|
||||
new_book = validate_bookcover(new_book)
|
||||
existing_book_names_to_details[title] = new_book
|
||||
with open("book_name_to_details.json", "w") as f:
|
||||
json.dump(
|
||||
existing_book_names_to_details,
|
||||
f,
|
||||
sort_keys=True,
|
||||
indent=4,
|
||||
separators=(",", ": "),
|
||||
)
|
||||
|
||||
book_list = []
|
||||
for _, book in existing_book_names_to_details.items():
|
||||
book_list.append(book)
|
||||
with open("books.json", "w") as f:
|
||||
json.dump(book_list, f, sort_keys=True, indent=4, separators=(",", ": "))
|
||||
else:
|
||||
print(f"❌ Error while fetching {title}")
|
||||
@@ -4,12 +4,14 @@ import os
|
||||
|
||||
def render_book_line(book_object):
|
||||
book = book_object
|
||||
book["rating"] = "?" if not "rating" in book else book["rating"]
|
||||
book["url"] = "" if not "url" in book else book["url"]
|
||||
book["year"] = "" if not "year" in book else book["year"]
|
||||
return "| {} | {} | [{}]({}) | {} | \n".format(
|
||||
book["title"], book["author"], book["rating"], book["url"], book["year"]
|
||||
)
|
||||
book['rating'] = '?' if not 'rating' in book else book['rating']
|
||||
book['url'] = '' if not 'url' in book else book['url']
|
||||
book['year'] = '' if not 'year' in book else book['year']
|
||||
return '| {} | {} | [{}]({}) | {} | \n'.format(book['title'],
|
||||
book['author'],
|
||||
book['rating'],
|
||||
book['url'],
|
||||
book['year'])
|
||||
|
||||
|
||||
# TODO: refine this logic
|
||||
@@ -19,37 +21,32 @@ def render(in_file, out_file, library):
|
||||
savig the new file to tmp_file location, the copying it to out-file and deleting tmp_file
|
||||
this is done to prevent issues if the in and the out file are the same
|
||||
"""
|
||||
tmp_file = "./.tmp-file.md"
|
||||
open(tmp_file, "a").close()
|
||||
tmp_file = './.tmp-file.md'
|
||||
open(tmp_file, 'a').close()
|
||||
books_not_reached = True
|
||||
with open(tmp_file, "w") as out_file_tmp:
|
||||
with open(tmp_file, 'w') as out_file_tmp:
|
||||
with open(in_file) as original_file:
|
||||
for line in original_file:
|
||||
|
||||
if line.strip() in library:
|
||||
if not books_not_reached:
|
||||
out_file_tmp.write("\n")
|
||||
if not books_not_reached: out_file_tmp.write('\n')
|
||||
books_not_reached = False
|
||||
|
||||
# render chapter and start of the table
|
||||
out_file_tmp.write(line)
|
||||
if len(library[line.strip()]) > 0:
|
||||
out_file_tmp.write(
|
||||
"| Name | Author | Goodreads Rating | Year Published | \n"
|
||||
)
|
||||
out_file_tmp.write(
|
||||
"|------|--------|------------------|----------------| \n"
|
||||
)
|
||||
out_file_tmp.write('| Name | Author | Goodreads Rating | Year Published | \n')
|
||||
out_file_tmp.write('|------|--------|------------------|----------------| \n')
|
||||
# render books
|
||||
for book in library[line.strip()]:
|
||||
out_file_tmp.write(render_book_line(book))
|
||||
elif books_not_reached:
|
||||
out_file_tmp.write(line)
|
||||
elif line.startswith("## License"):
|
||||
out_file_tmp.write("\n")
|
||||
out_file_tmp.write("\n")
|
||||
elif line.startswith('## License'):
|
||||
out_file_tmp.write('\n')
|
||||
out_file_tmp.write('\n')
|
||||
out_file_tmp.write(line)
|
||||
books_not_reached = True
|
||||
|
||||
copyfile(tmp_file, out_file)
|
||||
os.remove(tmp_file)
|
||||
os.remove(tmp_file)
|
||||
Reference in New Issue
Block a user