1
0
mirror of https://github.com/kremalicious/blog.git synced 2024-11-15 09:35:21 +01:00

Merge pull request #67 from kremalicious/feature/web3

web3 integration
This commit is contained in:
Matthias Kretschmann 2018-10-11 23:33:50 +02:00 committed by GitHub
commit ea6c0c3372
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 385 additions and 82 deletions

View File

@ -18,10 +18,12 @@
- [🎉 Features](#-features)
- [🎆 EXIF extraction](#-exif-extraction)
- [💰 Cryptocurrency donation via Web3/MetaMask](#-cryptocurrency-donation-via-web3-metamask)
- [🕸 Related Posts](#-related-posts)
- [🐝 Coinhive](#-coinhive)
- [🏆 SEO component](#-seo-component)
- [📈 Matomo (formerly Piwik) analytics tracking](#-matomo-formerly-piwik-analytics-tracking)
- [gatsby-redirect-from](#-gatsby-redirect-from)
- [gatsby-redirect-from](#gatsby-redirect-from)
- [💎 Importing SVG assets](#-importing-svg-assets)
- [🍬 Typekit component](#-typekit-component)
- [✨ Development](#-development)
@ -46,27 +48,67 @@ In the end looks like this, including location display with [pigeon-maps](https:
<img width="878" alt="screen shot 2018-10-09 at 23 59 39" src="https://user-images.githubusercontent.com/90316/46701262-6ed05680-cc1f-11e8-81c4-f4ea18b89bc0.png">
If you want to know how, have a look at the respective component under [`src/components/atoms/Exif.jsx`](src/components/atoms/Exif.jsx) and the EXIF node fields creation in [`gatsby-node.js`](gatsby-node.js).
If you want to know how this works, have a look at the respective component under
- [`src/components/atoms/Exif.jsx`](src/components/atoms/Exif.jsx)
- the EXIF node fields creation in [`gatsby-node.js`](gatsby-node.js)
### 💰 Cryptocurrency donation via Web3/MetaMask
Lets visitors say thanks with Bitcoin or Ether. Includes full Web3 client for sending Ether via MetaMask or Mist.
As a fallback, QR codes are generated with [react-qr-svg](https://github.com/no23reason/react-qr-svg) from the addresses defined in [`config.js`](config.js).
<img width="743" alt="screen shot 2018-10-11 at 21 01 37" src="https://user-images.githubusercontent.com/90316/46827443-e1187680-cd98-11e8-9daf-00a37c0ee13a.png">
If you want to know how this works, have a look at the respective components under
- [`src/components/atoms/Web3Donation.jsx`](src/components/atoms/Web3Donation.jsx)
- [`src/components/atoms/Qr.jsx`](src/components/atoms/Qr.jsx)
### 🕸 Related Posts
Under each post a list of related posts is displayed which are based on the tags of the currently viewed post. Also allows loading more related posts in place.
If you want to know how, have a look at the respective component under [`src/components/molecules/Pagination.jsx`](src/components/molecules/Pagination.jsx)
<img width="691" alt="screen shot 2018-10-11 at 21 03 03" src="https://user-images.githubusercontent.com/90316/46827531-14f39c00-cd99-11e8-84aa-0e851c32c89c.png">
If you want to know how this works, have a look at the respective component under
- [`src/components/molecules/RelatedPosts.jsx`](src/components/molecules/RelatedPosts.jsx)
### 🐝 Coinhive
Includes a component for mining Monero with JavaScript via [Coinhive](https://coinhive.com).
<img width="166" alt="screen shot 2018-10-11 at 21 09 49" src="https://user-images.githubusercontent.com/90316/46827858-03f75a80-cd9a-11e8-84f1-65b7d0027124.png">
Functionality is opt-in on a post basis. Simply add this to any post's frontmatter to activate it for this post:
```yaml
coinhive: true
```
If you want to know how this works, have a look at the respective component under
- [`src/components/atoms/Coinhive.jsx`](src/components/atoms/Coinhive.jsx)
### 🏆 SEO component
Includes a SEO component which automatically switches all required `meta` tags for search engines, Twitter Cards, and Facebook OpenGraph tags based on the browsed route/page.
If you want to know how, have a look at the respective component under [`src/components/atoms/SEO.jsx`](src/components/atoms/SEO.jsx)
If you want to know how this works, have a look at the respective component under
- [`src/components/atoms/SEO.jsx`](src/components/atoms/SEO.jsx)
### 📈 Matomo (formerly Piwik) analytics tracking
Site sends usage statistics to my own [Matomo](https://matomo.org) installation. To make this work in Gatsby, I created and open sourced a plugin, [gatsby-plugin-matomo](https://github.com/kremalicious/gatsby-plugin-matomo), which is in use on this site.
Site sends usage statistics to my own [Matomo](https://matomo.org) installation. To make this work in Gatsby, I created and open sourced a plugin which is in use on this site.
- [gatsby-plugin-matomo](https://github.com/kremalicious/gatsby-plugin-matomo)
### gatsby-redirect-from
https://github.com/kremalicious/gatsby-redirect-from
- [gatsby-redirect-from](https://github.com/kremalicious/gatsby-redirect-from)
### 💎 Importing SVG assets
@ -82,7 +124,9 @@ import { ReactComponent as Logo } from './components/svg/Logo'
Includes a component for adding the Typekit snippet.
If you want to know how, have a look at the respective component under [`src/components/atoms/Typekit.jsx`](src/components/atoms/Typekit.jsx)
If you want to know how this works, have a look at the respective component under
- [`src/components/atoms/Typekit.jsx`](src/components/atoms/Typekit.jsx)
## ✨ Development
@ -119,7 +163,7 @@ npm run format:css
npm run new "Hello"
```
...
- [`scripts/new.js`](scripts/new.js)
## 🚚 Deployment

View File

@ -212,11 +212,16 @@ module.exports = {
]
}
},
{
resolve: 'gatsby-plugin-sitemap',
options: {
exclude: ['/page/*', '/tag/*']
}
},
'gatsby-plugin-webpack-size',
'gatsby-plugin-react-helmet',
'gatsby-plugin-sharp',
'gatsby-transformer-sharp',
'gatsby-plugin-sitemap',
'gatsby-plugin-catch-links',
'gatsby-redirect-from',
'gatsby-plugin-meta-redirect',

View File

@ -31,7 +31,7 @@
"dms2dec": "^1.1.0",
"fast-exif": "^1.0.1",
"fraction.js": "^4.0.9",
"gatsby": "^2.0.20",
"gatsby": "^2.0.21",
"gatsby-image": "^2.0.13",
"gatsby-plugin-catch-links": "^2.0.4",
"gatsby-plugin-favicon": "^3.1.4",
@ -60,7 +60,7 @@
"load-script": "^1.0.0",
"node-sass": "^4.9.3",
"nord": "^0.2.1",
"pigeon-maps": "^0.11.1",
"pigeon-maps": "^0.11.2",
"pigeon-marker": "^0.3.4",
"react": "^16.5.2",
"react-clipboard.js": "^2.0.1",
@ -71,7 +71,8 @@
"react-qr-svg": "^2.1.0",
"react-time": "^4.3.0",
"react-transition-group": "^2.5.0",
"slugify": "^1.3.1"
"slugify": "^1.3.1",
"web3": "^0.20.7"
},
"devDependencies": {
"@babel/node": "^7.0.0",

View File

@ -35,6 +35,7 @@ export default class PostActions extends PureComponent {
>
@kremalicious
</a>
.
</p>
</article>
<article className={styles.action}>
@ -48,10 +49,12 @@ export default class PostActions extends PureComponent {
</p>
</article>
<ModalThanks
isOpen={this.state.showModal}
handleCloseModal={this.toggleModal}
/>
{this.state.showModal && (
<ModalThanks
isOpen={this.state.showModal}
handleCloseModal={this.toggleModal}
/>
)}
</aside>
)
}

View File

@ -1,6 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
import Image from './Image'
import Image from '../atoms/Image'
import styles from './PostImage.module.scss'
const PostImage = ({ title, fluid, fixed, alt }) => (

View File

@ -2,9 +2,9 @@ import React, { PureComponent, Fragment } from 'react'
import PropTypes from 'prop-types'
import Helmet from 'react-helmet'
import { CSSTransition } from 'react-transition-group'
import SearchInput from '../atoms/SearchInput'
import SearchButton from '../atoms/SearchButton'
import SearchResults from '../atoms/SearchResults'
import SearchInput from './SearchInput'
import SearchButton from './SearchButton'
import SearchResults from './SearchResults'
import styles from './Search.module.scss'

View File

@ -1,5 +1,5 @@
import React, { Fragment } from 'react'
import Input from './Input'
import Input from '../atoms/Input'
import styles from './SearchInput.module.scss'
const SearchInput = props => (

View File

@ -0,0 +1,31 @@
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import { QRCode } from 'react-qr-svg'
import Clipboard from 'react-clipboard.js'
import { ReactComponent as IconClipboard } from '../../images/clipboard.svg'
const Qr = ({ address, title }) => (
<Fragment>
{title && <h4>{title}</h4>}
<QRCode
bgColor="transparent"
fgColor="#6b7f88"
level="Q"
style={{ width: 120 }}
value={address}
/>
<pre>
<code>{address}</code>
<Clipboard data-clipboard-text={address} button-title="Copy to clipboard">
<IconClipboard />
</Clipboard>
</pre>
</Fragment>
)
Qr.propTypes = {
address: PropTypes.string.isRequired,
title: PropTypes.string
}
export default Qr

View File

@ -0,0 +1,188 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Web3 from 'web3'
import styles from './Web3Donation.module.scss'
const ONE_SECOND = 1000
const ONE_MINUTE = ONE_SECOND * 60
export default class Web3Donation extends PureComponent {
state = {
web3Connected: false,
networkError: null,
networkId: null,
accounts: [],
selectedAccount: null,
receipt: null,
transactionHash: null,
loading: false,
error: null
}
static propTypes = {
address: PropTypes.string
}
web3 = null
interval = null
networkInterval = null
componentDidMount() {
if (typeof window.web3 === 'undefined') {
// no web3
this.setState({ web3Connected: false })
} else {
// this.web3 = new Web3(Web3.givenProvider || 'ws://localhost:8546')
this.web3 = new Web3(window.web3.currentProvider)
this.setState({ web3Connected: true })
this.fetchAccounts()
this.fetchNetwork()
this.initPoll()
this.initNetworkPoll()
}
}
componentWillUnmount() {
clearInterval(this.interval)
clearInterval(this.networkInterval)
this.setState({ web3Connected: false })
}
initPoll() {
if (!this.interval) {
this.interval = setInterval(this.fetchAccounts, ONE_SECOND)
}
}
initNetworkPoll() {
if (!this.networkInterval) {
this.networkInterval = setInterval(this.fetchNetwork, ONE_MINUTE)
}
}
fetchNetwork = () => {
const { web3 } = this
web3 &&
web3.eth &&
//web3.eth.net.getId((err, netId) => {
web3.version.getNetwork((err, netId) => {
if (err) {
this.setState({ networkError: err })
}
if (netId != this.state.networkId) {
this.setState({
networkError: null,
networkId: netId
})
}
})
}
fetchAccounts = () => {
const { web3 } = this
web3 &&
web3.eth &&
web3.eth.getAccounts((err, accounts) => {
if (err) {
this.setState({ accountsError: err })
}
this.setState({
accounts,
selectedAccount: accounts[0]
})
})
}
handleWeb3Button = () => {
const { web3 } = this
this.setState({ loading: true })
// web3.eth
// .sendTransaction({
// from: this.state.selectedAccount,
// to: this.props.address,
// value: '10000000000000000'
// })
// .then(receipt => {
// this.setState({ receipt, loading: false })
// })
// .catch(error => {
// this.setState({ error, loading: false })
// })
web3.eth.sendTransaction(
{
from: this.state.selectedAccount,
to: this.props.address,
value: '10000000000000000'
},
(error, transactionHash) => {
if (error) this.setState({ error, loading: false })
if (!transactionHash) this.setState({ loading: true })
this.setState({ transactionHash, loading: false })
}
)
}
render() {
return (
<div className={styles.web3}>
<h4>web3</h4>
<p>Send a donation with MetaMask or Mist.</p>
{this.state.web3Connected ? (
<div>
{this.state.loading ? (
'Hang on...'
) : (
<button
className="btn btn-primary"
onClick={this.handleWeb3Button}
disabled={
!(this.state.networkId === '1') || !this.state.selectedAccount
}
>
Make it rain 0.01 Ξ
</button>
)}
{this.state.accounts.length === 0 && (
<div className={styles.alert}>
Web3 detected, but no account. Are you logged into your MetaMask
account?
</div>
)}
{this.state.networkId !== '1' && (
<div className={styles.alert}>Please connect to Main network</div>
)}
{this.state.error && (
<div className={styles.alert}>{this.state.error.message}</div>
)}
{this.state.transactionHash && (
<div className={styles.success}>
You are awesome, thanks!
<br />
<a
href={`https://etherscan.io/tx/${this.state.transactionHash}`}
>
See your transaction on etherscan.io.
</a>
</div>
)}
</div>
) : (
<div className={styles.alert}>No Web3 capable browser detected.</div>
)}
</div>
)
}
}

View File

@ -0,0 +1,39 @@
@import 'variables';
@import 'mixins';
.web3 {
@include divider;
width: 100%;
text-align: center;
margin-top: $spacer / 2;
margin-bottom: $spacer;
padding-bottom: $spacer * 1.5;
button {
margin: auto;
}
h4 {
font-size: $font-size-large;
margin-top: 0;
margin-bottom: $spacer / 4;
color: $brand-grey;
text-align: center;
}
p {
color: $brand-grey-light;
}
}
.alert {
margin-top: $spacer / 2;
font-size: $font-size-small;
color: darken($alert-error, 60%);
}
.success {
composes: alert;
color: darken($alert-success, 60%);
}

View File

@ -1,10 +1,9 @@
import React from 'react'
import React, { PureComponent } from 'react'
import { StaticQuery, graphql } from 'gatsby'
import { QRCode } from 'react-qr-svg'
import Clipboard from 'react-clipboard.js'
import Web3Donation from '../atoms/Web3Donation'
import Qr from '../atoms/Qr'
import Modal from '../atoms/Modal'
import { ReactComponent as IconClipboard } from '../../images/clipboard.svg'
import styles from './ModalThanks.module.scss'
const query = graphql`
@ -20,45 +19,35 @@ const query = graphql`
}
`
const ModalThanks = ({ ...props }) => (
<StaticQuery
query={query}
render={data => {
const { author } = data.site.siteMetadata
class ModalThanks extends PureComponent {
render() {
return (
<StaticQuery
query={query}
render={data => {
const { author } = data.site.siteMetadata
return (
<Modal
{...props}
contentLabel="Say thanks with Bitcoin or Ether"
title="Say thanks"
>
<div className={styles.modalThanks}>
{Object.keys(author).map((address, i) => (
<div key={i} className={styles.coin}>
<h4>{address}</h4>
<QRCode
bgColor="transparent"
fgColor="#6b7f88"
level="Q"
style={{ width: 150 }}
value={author[address]}
/>
<pre>
<code>{author[address]}</code>
<Clipboard
data-clipboard-text={author[address]}
button-title="Copy to clipboard"
>
<IconClipboard />
</Clipboard>
</pre>
return (
<Modal
{...this.props}
contentLabel="Say thanks with Bitcoin or Ether"
title="Say thanks"
>
<div className={styles.modalThanks}>
<Web3Donation address={author.ether} />
{Object.keys(author).map((address, i) => (
<div key={i} className={styles.coin}>
<Qr title={address} address={author[address]} />
</div>
))}
</div>
))}
</div>
</Modal>
)
}}
/>
)
</Modal>
)
}}
/>
)
}
}
export default ModalThanks

View File

@ -4,6 +4,7 @@
@media (min-width: $screen-sm) {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
}

View File

@ -74,10 +74,12 @@ export default class Footer extends PureComponent {
</button>
</p>
<ModalThanks
isOpen={this.state.showModal}
handleCloseModal={this.toggleModal}
/>
{this.state.showModal && (
<ModalThanks
isOpen={this.state.showModal}
handleCloseModal={this.toggleModal}
/>
)}
</section>
</Container>
</footer>

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react'
import { Link } from 'gatsby'
import Container from '../atoms/Container'
import Search from '../molecules/Search'
import Search from '../Search/Search'
import Menu from '../molecules/Menu'
import styles from './Header.module.scss'

View File

@ -1,7 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import { graphql, Link } from 'gatsby'
import PostImage from '../components/atoms/PostImage'
import PostImage from '../components/Post/PostImage'
import Page from '../templates/Page'
import styles from './goodies.module.scss'

View File

@ -3,15 +3,15 @@ import PropTypes from 'prop-types'
import Helmet from 'react-helmet'
import { graphql } from 'gatsby'
import Layout from '../components/Layout'
import PostImage from '../components/atoms/PostImage'
import PostTitle from '../components/atoms/PostTitle'
import PostLead from '../components/atoms/PostLead'
import PostContent from '../components/atoms/PostContent'
import PostActions from '../components/atoms/PostActions'
import PostLinkActions from '../components/atoms/PostLinkActions'
import PostImage from '../components/Post/PostImage'
import PostTitle from '../components/Post/PostTitle'
import PostLead from '../components/Post/PostLead'
import PostContent from '../components/Post/PostContent'
import PostActions from '../components/Post/PostActions'
import PostLinkActions from '../components/Post/PostLinkActions'
import SEO from '../components/atoms/SEO'
import Coinhive from '../components/atoms/Coinhive'
import PostMeta from '../components/molecules/PostMeta'
import PostMeta from '../components/Post/PostMeta'
import Exif from '../components/atoms/Exif'
import RelatedPosts from '../components/molecules/RelatedPosts'
import styles from './Post.module.scss'

View File

@ -2,12 +2,12 @@ import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import { Link, graphql } from 'gatsby'
import Layout from '../components/Layout'
import PostImage from '../components/atoms/PostImage'
import PostTitle from '../components/atoms/PostTitle'
import PostLead from '../components/atoms/PostLead'
import PostContent from '../components/atoms/PostContent'
import PostMore from '../components/atoms/PostMore'
import PostLinkActions from '../components/atoms/PostLinkActions'
import PostImage from '../components/Post/PostImage'
import PostTitle from '../components/Post/PostTitle'
import PostLead from '../components/Post/PostLead'
import PostContent from '../components/Post/PostContent'
import PostMore from '../components/Post/PostMore'
import PostLinkActions from '../components/Post/PostLinkActions'
import SEO from '../components/atoms/SEO'
import Pagination from '../components/molecules/Pagination'
import Featured from '../components/molecules/Featured'