1
0
mirror of https://github.com/kremalicious/blog.git synced 2024-06-30 21:52:05 +02:00

Merge pull request #73 from kremalicious/feature/metamask-provider

Web3/MetaMask changes
This commit is contained in:
Matthias Kretschmann 2018-10-26 01:14:47 +02:00 committed by GitHub
commit 0064fa0f9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 82 additions and 528 deletions

View File

@ -8,8 +8,9 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "gatsby develop", "start": "gatsby develop",
"build": "gatsby build", "build": "npm run rename:scrypt && gatsby build",
"ssr": "npm run build && serve -s public/", "ssr": "npm run build && serve -s public/",
"rename:scrypt": "sed -i -e 's|./build/Release/scrypt|scrypt|g' node_modules/scrypt/index.js",
"format": "run-p format:js format:css format:md format:yaml", "format": "run-p format:js format:css format:md format:yaml",
"format:js": "prettier-eslint --write 'src/**/*.{js,jsx}'", "format:js": "prettier-eslint --write 'src/**/*.{js,jsx}'",
"format:css": "prettier-stylelint --write", "format:css": "prettier-stylelint --write",
@ -25,7 +26,7 @@
"new": "babel-node ./scripts/new.js" "new": "babel-node ./scripts/new.js"
}, },
"browserslist": [ "browserslist": [
"last 3 versions" "last 2 versions"
], ],
"dependencies": { "dependencies": {
"dms2dec": "^1.1.0", "dms2dec": "^1.1.0",
@ -33,7 +34,7 @@
"fraction.js": "^4.0.10", "fraction.js": "^4.0.10",
"gatsby": "^2.0.31", "gatsby": "^2.0.31",
"gatsby-image": "^2.0.17", "gatsby-image": "^2.0.17",
"gatsby-plugin-catch-links": "^2.0.4", "gatsby-plugin-catch-links": "^2.0.5",
"gatsby-plugin-favicon": "^3.1.4", "gatsby-plugin-favicon": "^3.1.4",
"gatsby-plugin-feed": "^2.0.8", "gatsby-plugin-feed": "^2.0.8",
"gatsby-plugin-lunr": "^1.2.0", "gatsby-plugin-lunr": "^1.2.0",
@ -72,8 +73,8 @@
"react-qr-svg": "^2.1.0", "react-qr-svg": "^2.1.0",
"react-time": "^4.3.0", "react-time": "^4.3.0",
"react-transition-group": "^2.5.0", "react-transition-group": "^2.5.0",
"slugify": "^1.3.1", "slugify": "^1.3.2",
"web3": "^0.20.7" "web3": "^1.0.0-beta.36"
}, },
"devDependencies": { "devDependencies": {
"@babel/node": "^7.0.0", "@babel/node": "^7.0.0",

View File

@ -13,6 +13,8 @@ export default class Alerts extends PureComponent {
networkName: PropTypes.string, networkName: PropTypes.string,
error: PropTypes.object, error: PropTypes.object,
transactionHash: PropTypes.string, transactionHash: PropTypes.string,
confirmationNumber: PropTypes.number,
receipt: PropTypes.object,
web3Connected: PropTypes.bool.isRequired web3Connected: PropTypes.bool.isRequired
} }
@ -22,10 +24,7 @@ export default class Alerts extends PureComponent {
noCorrectNetwork: `Please connect to <strong>Main</strong> network. You are on <strong>${networkName}</strong> right now.`, noCorrectNetwork: `Please connect to <strong>Main</strong> network. You are on <strong>${networkName}</strong> right now.`,
noWeb3: noWeb3:
'No Web3 detected. Install <a href="https://metamask.io">MetaMask</a>, <a href="https://brave.com">Brave</a>, or <a href="https://github.com/ethereum/mist">Mist</a>.', 'No Web3 detected. Install <a href="https://metamask.io">MetaMask</a>, <a href="https://brave.com">Brave</a>, or <a href="https://github.com/ethereum/mist">Mist</a>.',
success: `You are awesome, thanks!<br /> transaction: `<a href="https://etherscan.io/tx/${transactionHash}" target="_blank">See your transaction on etherscan.io.</a>`
<a href="https://etherscan.io/tx/${transactionHash}">
See your transaction on etherscan.io.
</a>`
}) })
render() { render() {
@ -56,8 +55,7 @@ export default class Alerts extends PureComponent {
{transactionHash && ( {transactionHash && (
<Message <Message
className={styles.success} message={this.alertMessages(null, transactionHash).transaction}
message={this.alertMessages(transactionHash).success}
/> />
)} )}
</Fragment> </Fragment>

View File

@ -4,7 +4,7 @@ import Web3 from 'web3'
import InputGroup from './InputGroup' import InputGroup from './InputGroup'
import Alerts from './Alerts' import Alerts from './Alerts'
import styles from './index.module.scss' import styles from './index.module.scss'
import { getNetworkName } from './utils' import { getNetworkName, Logger } from './utils'
const ONE_SECOND = 1000 const ONE_SECOND = 1000
const ONE_MINUTE = ONE_SECOND * 60 const ONE_MINUTE = ONE_SECOND * 60
@ -18,8 +18,10 @@ export default class Web3Donation extends PureComponent {
selectedAccount: null, selectedAccount: null,
amount: '0.01', amount: '0.01',
transactionHash: null, transactionHash: null,
receipt: null,
loading: false, loading: false,
error: null error: null,
message: 'Hang on...'
} }
static propTypes = { static propTypes = {
@ -31,41 +33,36 @@ export default class Web3Donation extends PureComponent {
networkInterval = null networkInterval = null
componentDidMount() { componentDidMount() {
this.initAllTheTings() this.initWeb3()
} }
componentWillUnmount() { componentWillUnmount() {
this.resetAllTheThings() this.resetAllTheThings()
} }
// getPermissions = async ethereum => { async initWeb3() {
// try {
// // Request account access if needed
// await ethereum.enable()
// } catch (error) {
// // User denied account access...
// Logger.error(error)
// }
// }
initAllTheTings() {
// Modern dapp browsers... // Modern dapp browsers...
// if (window.ethereum) { if (window.ethereum) {
// this.web3 = new Web3(window.ethereum) this.web3 = new Web3(window.ethereum)
// this.setState({ web3Connected: true })
// this.getPermissions(this.web3.eth)
// }
try {
// Request account access
await window.ethereum.enable()
this.setState({ web3Connected: true })
this.initAllTheTings()
} catch (error) {
// User denied account access...
Logger.error(error)
this.setState({ error })
}
}
// Legacy dapp browsers... // Legacy dapp browsers...
if (window.web3) { else if (window.web3) {
// this.web3 = new Web3(Web3.givenProvider || 'ws://localhost:8546')
this.web3 = new Web3(window.web3.currentProvider) this.web3 = new Web3(window.web3.currentProvider)
this.setState({ web3Connected: true }) this.setState({ web3Connected: true })
this.fetchAccounts() this.initAllTheTings()
this.fetchNetwork()
this.initAccountsPoll()
this.initNetworkPoll()
} }
// Non-dapp browsers... // Non-dapp browsers...
else { else {
@ -73,6 +70,13 @@ export default class Web3Donation extends PureComponent {
} }
} }
initAllTheTings() {
this.fetchAccounts()
this.fetchNetwork()
this.initAccountsPoll()
this.initNetworkPoll()
}
resetAllTheThings() { resetAllTheThings() {
clearInterval(this.interval) clearInterval(this.interval)
clearInterval(this.networkInterval) clearInterval(this.networkInterval)
@ -96,11 +100,10 @@ export default class Web3Donation extends PureComponent {
web3 && web3 &&
web3.eth && web3.eth &&
//web3.eth.net.getId((err, netId) => { web3.eth.net.getId((err, netId) => {
web3.version.getNetwork((err, netId) => {
if (err) this.setState({ error: err }) if (err) this.setState({ error: err })
if (netId != this.state.networkId) { if (netId !== this.state.networkId) {
this.setState({ this.setState({
error: null, error: null,
networkId: netId networkId: netId
@ -124,41 +127,39 @@ export default class Web3Donation extends PureComponent {
this.setState({ this.setState({
error: null, error: null,
accounts, accounts,
selectedAccount: accounts[0] selectedAccount: accounts[0].toLowerCase()
}) })
}) })
} }
handleButton = () => { sendTransaction() {
const { web3 } = this const { web3 } = this
this.setState({ loading: true }) web3.eth
.sendTransaction({
// 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, from: this.state.selectedAccount,
to: this.props.address, to: this.props.address,
value: this.state.amount * 1e18 // ETH -> Wei value: this.state.amount * 1e18 // ETH -> Wei
}, })
(error, transactionHash) => { .once('transactionHash', transactionHash => {
if (error) this.setState({ error, loading: false }) this.setState({
if (!transactionHash) this.setState({ loading: true }) transactionHash,
this.setState({ transactionHash, loading: false }) message: 'Waiting for network confirmation, hang on...'
} })
) })
.on('error', error => this.setState({ error, loading: false }))
.then(() => {
this.setState({ message: 'Confirmed. You are awesome, thanks!' })
})
}
handleButton = () => {
this.setState({
loading: true,
message: 'Waiting for your confirmation...'
})
this.sendTransaction()
} }
onAmountChange = ({ target }) => { onAmountChange = ({ target }) => {
@ -175,9 +176,12 @@ export default class Web3Donation extends PureComponent {
amount, amount,
networkName, networkName,
error, error,
transactionHash transactionHash,
confirmationNumber,
message
} = this.state } = this.state
const hasCorrectNetwork = networkId === '1'
const hasCorrectNetwork = networkId === 1
const hasAccount = accounts.length !== 0 const hasAccount = accounts.length !== 0
return ( return (
@ -190,7 +194,7 @@ export default class Web3Donation extends PureComponent {
{web3Connected && ( {web3Connected && (
<div className={styles.web3Row}> <div className={styles.web3Row}>
{loading ? ( {loading ? (
'Hang on...' message
) : ( ) : (
<InputGroup <InputGroup
hasCorrectNetwork={hasCorrectNetwork} hasCorrectNetwork={hasCorrectNetwork}
@ -211,6 +215,7 @@ export default class Web3Donation extends PureComponent {
error={error} error={error}
transactionHash={transactionHash} transactionHash={transactionHash}
web3Connected={web3Connected} web3Connected={web3Connected}
confirmationNumber={confirmationNumber}
/> />
</div> </div>
) )

View File

@ -19,4 +19,7 @@
.web3Row { .web3Row {
min-height: 58px; min-height: 58px;
display: flex;
align-items: center;
justify-content: center;
} }

View File

@ -2,19 +2,19 @@ export const getNetworkName = async netId => {
let networkName let networkName
switch (netId) { switch (netId) {
case '1': case 1:
networkName = 'Main' networkName = 'Main'
break break
case '2': case 2:
networkName = 'Morden' networkName = 'Morden'
break break
case '3': case 3:
networkName = 'Ropsten' networkName = 'Ropsten'
break break
case '4': case 4:
networkName = 'Rinkeby' networkName = 'Rinkeby'
break break
case '42': case 42:
networkName = 'Kovan' networkName = 'Kovan'
break break
default: default:

View File

@ -11,7 +11,7 @@ samp {
code, code,
kbd { kbd {
padding: .1em; padding: .1rem;
} }
code, code,
@ -29,6 +29,7 @@ kbd {
border-radius: $border-radius; border-radius: $border-radius;
border: 1px solid $kbd-bg; border: 1px solid $kbd-bg;
box-shadow: inset 0 1px 0 rgba(#fff, .4); box-shadow: inset 0 1px 0 rgba(#fff, .4);
padding: .15rem .4rem;
} }
pre { pre {

View File

@ -520,139 +520,6 @@ copyright Copyright (C) 2017 +
color: #d8dee9; color: #d8dee9;
} }
/* +--- Languages ---+ */
/*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language C/C++ +
project nord-atom-syntax +
repository https://github.com/arcticicestudio/nord-atom-syntax +
author Arctic Ice Studio +
email development@arcticicestudio.com +
copyright Copyright (C) 2017 +
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
[Language Packages]
> language-c (https://atom.io/packages/language-c)
[References]
Standard C++
(https://isocpp.org)
*/
.source.c .keyword.operator,
.source.cpp .keyword.operator {
color: #81a1c1;
}
.source.c .meta.preprocessor,
.source.cpp .meta.preprocessor {
color: #5e81ac;
}
.source.c .punctuation.definition.directive,
.source.cpp .punctuation.definition.directive {
color: #81a1c1;
}
.source.c .punctuation.separator.pointer-access,
.source.cpp .punctuation.separator.pointer-access {
color: #81a1c1;
}
.source.c .string.include,
.source.cpp .string.include {
color: #8fbcbb;
}
/*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language Clojure +
project nord-atom-syntax +
repository https://github.com/arcticicestudio/nord-atom-syntax +
author Arctic Ice Studio +
email development@arcticicestudio.com +
copyright Copyright (C) 2017 +
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
[Language Packages]
> language-clojure (https://atom.io/packages/language-clojure)
[References]
Clojure
(https://clojure.org)
*/
.source.clojure .entity.global {
color: #8fbcbb;
}
.source.clojure .meta.symbol {
color: #d8dee9;
}
/*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language CoffeeScript +
project nord-atom-syntax +
repository https://github.com/arcticicestudio/nord-atom-syntax +
author Arctic Ice Studio +
email development@arcticicestudio.com +
copyright Copyright (C) 2017 +
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
[Language Packages]
> language-coffee-script (https://atom.io/packages/language-coffee-script)
[References]
CoffeeScript
(http://coffeescript.org)
*/
.source.coffee .variable.other.instance {
color: #d8dee9;
font-style: italic;
}
/*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language C# +
project nord-atom-syntax +
repository https://github.com/arcticicestudio/nord-atom-syntax +
author Arctic Ice Studio +
email development@arcticicestudio.com +
copyright Copyright (C) 2017 +
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
[Language Packages]
> language-csharp (https://atom.io/packages/language-csharp)
[References]
Microsoft .NET Framework
(https://www.microsoft.com/net)
*/
.source.cs .entity.name.interface.class {
font-weight: bold;
}
.source.cs .meta.method.annotation {
color: #d08770;
}
.source.cs .meta.namespace.body {
color: #8fbcbb;
}
.source.cs .meta.directive.preprocessor {
color: #5e81ac;
}
.source.cs .meta .generic.method.identifier {
color: #d8dee9;
}
.source.cs .punctuation.section.class.begin,
.source.cs .punctuation.section.class.end {
color: #d8dee9;
}
/* /*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language CSS + title Language CSS +
@ -738,35 +605,6 @@ copyright Copyright (C) 2017 +
color: #8fbcbb; color: #8fbcbb;
} }
/*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language Elixir +
project nord-atom-syntax +
repository https://github.com/arcticicestudio/nord-atom-syntax +
author Arctic Ice Studio +
email development@arcticicestudio.com +
copyright Copyright (C) 2017 +
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
[Language Packages]
> language-elixir (https://atom.io/packages/language-elixir)
[References]
Elixir
(http://elixir-lang.org)
*/
.source.elixir .variable.other.readwrite.module {
color: #d08770;
}
.source.elixir .variable.other.readwrite.constant {
color: #8fbcbb;
}
.source.elixir .punctuation.definition.variable {
color: #d08770;
}
/* /*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language GitHub Flavored Markdown + title Language GitHub Flavored Markdown +
@ -804,131 +642,6 @@ copyright Copyright (C) 2017 +
color: #8fbcbb; color: #8fbcbb;
} }
/*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language GLSL +
project nord-atom-syntax +
repository https://github.com/arcticicestudio/nord-atom-syntax +
author Arctic Ice Studio +
email development@arcticicestudio.com +
copyright Copyright (C) 2017 +
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
[Language Packages]
> language-glsl (https://atom.io/packages/language-glsl)
[References]
OpenGL
(https://www.opengl.org/documentation/glsl)
*/
.source.glsl .punctuation.definition.directive {
color: #81a1c1;
}
.source.glsl .string.include {
color: #8fbcbb;
}
/*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language Go +
project nord-atom-syntax +
repository https://github.com/arcticicestudio/nord-atom-syntax +
author Arctic Ice Studio +
email development@arcticicestudio.com +
copyright Copyright (C) 2017 +
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
[Language Packages]
> language-go (https://atom.io/packages/language-go)
[References]
Go
(https://golang.org)
*/
.source.go .entity.name.package {
color: #8fbcbb;
}
.source.go .storage.type.string {
color: #8fbcbb;
}
/*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language HAML +
project nord-atom-syntax +
repository https://github.com/arcticicestudio/nord-atom-syntax +
author Arctic Ice Studio +
email development@arcticicestudio.com +
copyright Copyright (C) 2017 +
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
[Language Packages]
> language-haml (https://atom.io/packages/language-haml)
[References]
HAML
(http://haml.info)
*/
.text.haml .meta.prolog {
color: #5e81ac;
}
.text.haml .meta.section.object {
color: #8fbcbb;
}
.text.haml .punctuation.definition.prolog {
color: #81a1c1;
}
.text.haml .punctuation.definition.tag {
color: #81a1c1;
}
.text.haml .variable.other.instance {
color: #d8dee9;
font-style: italic;
}
/*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language Haskell +
project nord-atom-syntax +
repository https://github.com/arcticicestudio/nord-atom-syntax +
author Arctic Ice Studio +
email development@arcticicestudio.com +
copyright Copyright (C) 2017 +
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
[Language Packages]
> language-haskell (https://atom.io/packages/language-haskell)
[References]
Haskell
(https://www.haskell.org)
*/
.source.haskell .entity.name.function.infix {
color: #81a1c1;
}
.source.haskell .identifier {
color: #d8dee9;
}
.source.haskell .meta.preprocessor {
color: #5e81ac;
}
.source.haskell .punctuation.definition.directive {
color: #81a1c1;
}
.source.haskell .support.other.module {
color: #8fbcbb;
}
/* /*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language HTML + title Language HTML +
@ -951,105 +664,6 @@ copyright Copyright (C) 2017 +
font-style: italic; font-style: italic;
} }
/*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language Java +
project nord-atom-syntax +
repository https://github.com/arcticicestudio/nord-atom-syntax +
author Arctic Ice Studio +
email development@arcticicestudio.com +
copyright Copyright (C) 2017 +
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Supports Java, Scala and Java Properties.
[Index]
> Java
> Scala
> Java Properties
[Language Packages]
> language-java (https://atom.io/packages/language-java)
> language-scala (https://atom.io/packages/language-scala)
[References]
Java
(https://java.com)
(http://openjdk.java.net)
Scala
(http://scala-lang.org)
*/
/* +------+
+ Java +
+------+ */
.source.java .comment.block.javadoc .variable.parameter {
color: #88c0d0;
}
.source.java .keyword.operator.instanceof {
color: #81a1c1;
}
.source.java .keyword.other.documentation.javadoc {
color: #8fbcbb;
}
.source.java .punctuation.bracket.angle {
color: #81a1c1;
}
.source.java .storage.modifier.import,
.source.java .storage.modifier.package {
color: #8fbcbb;
}
.source.java .storage.type {
color: #8fbcbb;
}
.source.java .storage.type.annotation {
color: #d08770;
}
.source.java .storage.type.primitive {
color: #81a1c1;
}
/* +-------+
+ Scala +
+-------+ */
.source.scala .entity.name.package {
color: #8fbcbb;
}
.source.scala .meta.import {
color: #8fbcbb;
}
.source.scala .meta.import .variable {
color: #8fbcbb;
}
.source.scala .scala.type {
color: #8fbcbb;
}
.source.scala .scala.type .class {
color: #8fbcbb;
}
/* +-----------------+
+ Java Properties +
+-----------------+ */
.source.java-properties .meta.key-pair {
color: #81a1c1;
}
.source.java-properties .meta.key-pair > .punctuation {
color: #d8dee9;
}
/* /*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language JavaScript + title Language JavaScript +
@ -1130,27 +744,6 @@ copyright Copyright (C) 2017 +
color: #d8dee9; color: #d8dee9;
} }
/*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language Julia +
project nord-atom-syntax +
repository https://github.com/arcticicestudio/nord-atom-syntax +
author Arctic Ice Studio +
email development@arcticicestudio.com +
copyright Copyright (C) 2017 +
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
[Language Packages]
> language-julia (https://atom.io/packages/language-julia)
[References]
Julia Language
(http://julialang.org)
*/
.source.julia .support.function.macro {
color: #d08770;
}
/* /*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language LESSCSS + title Language LESSCSS +
@ -1252,53 +845,6 @@ copyright Copyright (C) 2017 +
color: #d8dee9; color: #d8dee9;
} }
/*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language RestructuredText +
project nord-atom-syntax +
repository https://github.com/arcticicestudio/nord-atom-syntax +
author Arctic Ice Studio +
email development@arcticicestudio.com +
copyright Copyright (C) 2017 +
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
[Language Packages]
> language-gfm (https://atom.io/packages/language-restructuredtext)
[References]
Docutils
http://docutils.sourceforge.net/rst.html
*/
.text.restructuredtext .entity.name.tag {
color: #81a1c1;
}
.text.restructuredtext .markup.bold,
.text.restructuredtext .markup.italic {
color: #81a1c1;
}
.text.restructuredtext .markup.other.command {
color: #8fbcbb;
font-style: italic;
}
.text.restructuredtext .punctuation.definition {
color: #81a1c1;
}
.text.restructuredtext .punctuation.definition.heading {
color: #81a1c1;
}
.text.restructuredtext .punctuation.separator.line-block {
color: #81a1c1;
}
.text.restructuredtext .string.other.link.title {
color: #88c0d0;
}
/* /*
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
title Language Ruby + title Language Ruby +