1
0
Fork 0

migrate to TypeScript

This commit is contained in:
Matthias Kretschmann 2019-10-02 13:35:50 +02:00
parent 6d502cf313
commit 0946b30b67
Signed by: m
GPG Key ID: 606EEEF3C479A91F
149 changed files with 3465 additions and 3614 deletions

View File

@ -1,5 +0,0 @@
version: '2'
checks:
method-lines:
config:
threshold: 55 # Gatsby's StaticQuery makes render functions pretty long

View File

@ -1,14 +0,0 @@
# EditorConfig is awesome: http://EditorConfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
[*.scss]
indent_size = 4

View File

@ -1,34 +1,36 @@
{
"parser": "babel-eslint",
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:jsx-a11y/recommended",
"plugin:prettier/recommended"
],
"plugins": ["react", "graphql", "prettier", "jsx-a11y"],
"extends": ["eslint:recommended", "prettier"],
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"modules": true
"ecmaVersion": 2018,
"sourceType": "module"
},
"env": { "browser": true, "node": true, "es6": true, "jest": true },
"settings": { "react": { "version": "detect" } },
"overrides": [
{
"files": ["**/*.ts", "**/*.tsx"],
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:jsx-a11y/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended",
"plugin:react/recommended"
],
"plugins": ["@typescript-eslint", "react", "graphql", "jsx-a11y"],
"rules": {
"object-curly-spacing": ["error", "always"],
"react/prop-types": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off"
},
"parserOptions": {
"ecmaFeatures": { "jsx": true },
"ecmaVersion": 2018,
"sourceType": "module",
"project": "./tsconfig.json"
}
}
},
"env": {
"browser": true,
"node": true,
"es6": true,
"jest": true
},
"rules": {
"quotes": ["error", "single"],
"semi": ["error", "never"],
"object-curly-spacing": ["error", "always"],
"prettier/prettier": "error"
},
"settings": {
"react": {
"version": "16"
}
}
]
}

View File

@ -2,3 +2,4 @@ node_modules/
.cache/
static/
public/
coverage/

View File

@ -1,5 +1,6 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "none"
"trailingComma": "none",
"tabWidth": 2
}

View File

@ -2,12 +2,12 @@
"extends": [
"stylelint-config-standard",
"stylelint-config-css-modules",
"./node_modules/prettier-stylelint/config.js"
"stylelint-prettier/recommended"
],
"plugins": ["stylelint-prettier"],
"syntax": "scss",
"rules": {
"indentation": 4,
"number-leading-zero": "never",
"prettier/prettier": true,
"at-rule-no-unknown": null
}
}

View File

@ -19,15 +19,13 @@ before_install:
before_script:
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
- chmod +x ./cc-test-reporter
- "./cc-test-reporter before-build"
- './cc-test-reporter before-build'
script:
- npm test
- './cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT'
- travis_wait 60 npm run build
after_script:
- "./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT"
after_success:
- pip install --user awscli
- export PATH=$PATH:$HOME/.local/bin
@ -37,8 +35,8 @@ notifications:
email: false
slack:
template:
- "%{branch} *%{result}* build (<%{build_url}|#%{build_number}>) for <%{compare_url}|%{commit}>"
- "Execution time: *%{duration}*"
- "Message: %{message}"
- '%{branch} *%{result}* build (<%{build_url}|#%{build_number}>) for <%{compare_url}|%{commit}>'
- 'Execution time: *%{duration}*'
- 'Message: %{message}'
rooms:
- secure: "Ot7Ryl4PW0/TUo4t4Y3J6AbmxqNUtFOI72vNabNX2IdEiU78q+M3esPEkT2I/z0S2Vda9ogRkRbKa5blE2ZEo74/9CUYRXX/syPSZL9tpHDd600wmiObee469Au8dSO48n8G9U+Dm1q60O6oiEGsrrAR6fNE386QEfDhVqKKwBKHk9RcUocUO2b+0WKI7MJk+j5G4+sxv/5ax8prGx0sD6bRoGRuNpyW/MZ9uylBV2WOdmHfEY9D8GYpzVs2JqTB7xr/OL9d+puZPQSdqGfa7xtc+APFiKK//aW/ffOsNzGa4kygC94nfV4oJceMUO3v0bDpB5aXM1YG02EyQzSwpGCbtnbP9Ei/ANcGqiFjPm1/ZVAiwPzT8XZLWkFjy+sOfmF+xmszUCoRiJBVxfL0tx0d1o/JIvgA5m+/iIpro70ep0nBHTiDt2AoxaGGE9GnIT20uVXJJIdXIwTWhVx4HnkptYsFel9l2/oc24S+CnitRaCtGQCiAMNNCESL1AcHCRot/4gm3uuZLdYEA1juHUvgEEH6jG5T2XWaq4uEbDZKdu8y7YMW105FytEsyNU3Tzem4c024EIAhBshSfg5N/iwVeic47E1QAz/5RtfBNLQaEPY4TGJYJvTOaCevjYC7mKlYBEoZmsfT0uNaWqEXUxUwLg5Ih8JoLQKvH6H4fA="
- secure: 'Ot7Ryl4PW0/TUo4t4Y3J6AbmxqNUtFOI72vNabNX2IdEiU78q+M3esPEkT2I/z0S2Vda9ogRkRbKa5blE2ZEo74/9CUYRXX/syPSZL9tpHDd600wmiObee469Au8dSO48n8G9U+Dm1q60O6oiEGsrrAR6fNE386QEfDhVqKKwBKHk9RcUocUO2b+0WKI7MJk+j5G4+sxv/5ax8prGx0sD6bRoGRuNpyW/MZ9uylBV2WOdmHfEY9D8GYpzVs2JqTB7xr/OL9d+puZPQSdqGfa7xtc+APFiKK//aW/ffOsNzGa4kygC94nfV4oJceMUO3v0bDpB5aXM1YG02EyQzSwpGCbtnbP9Ei/ANcGqiFjPm1/ZVAiwPzT8XZLWkFjy+sOfmF+xmszUCoRiJBVxfL0tx0d1o/JIvgA5m+/iIpro70ep0nBHTiDt2AoxaGGE9GnIT20uVXJJIdXIwTWhVx4HnkptYsFel9l2/oc24S+CnitRaCtGQCiAMNNCESL1AcHCRot/4gm3uuZLdYEA1juHUvgEEH6jG5T2XWaq4uEbDZKdu8y7YMW105FytEsyNU3Tzem4c024EIAhBshSfg5N/iwVeic47E1QAz/5RtfBNLQaEPY4TGJYJvTOaCevjYC7mKlYBEoZmsfT0uNaWqEXUxUwLg5Ih8JoLQKvH6H4fA='

View File

@ -1,88 +1,85 @@
kbd {
font-size: 18px;
color: #444;
font-family: 'Lucida Grande', Lucida, Verdana, sans-serif;
font-weight: normal;
font-style: normal;
text-align: center;
line-height: 1em;
text-shadow: 0 1px 0 #fff;
display: inline;
padding: .3em .55em;
border-radius: 6px;
background-clip: padding-box;
border: 1px solid #bbb;
background-color: #f7f7f7;
background-image: linear-gradient(
to bottom,
rgba(0, 0, 0, .1),
rgba(0, 0, 0, 0)
);
background-repeat: repeat-x;
box-shadow: 0 2px 0 #bbb, 0 3px 1px #999, 0 3px 0 #bbb, inset 0 1px 1px #fff,
inset 0 -1px 3px #ccc;
font-size: 18px;
color: #444;
font-family: 'Lucida Grande', Lucida, Verdana, sans-serif;
font-weight: normal;
font-style: normal;
text-align: center;
line-height: 1em;
text-shadow: 0 1px 0 #fff;
display: inline;
padding: 0.3em 0.55em;
border-radius: 6px;
background-clip: padding-box;
border: 1px solid #bbb;
background-color: #f7f7f7;
background-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.1),
rgba(0, 0, 0, 0)
);
background-repeat: repeat-x;
box-shadow: 0 2px 0 #bbb, 0 3px 1px #999, 0 3px 0 #bbb, inset 0 1px 1px #fff,
inset 0 -1px 3px #ccc;
}
kbd.dark {
color: #eee;
text-shadow: 0 -1px 0 #000;
border-color: #000;
background-color: #4d4c4c;
background-image: linear-gradient(
rgba(0, 0, 0, .5),
rgba(0, 0, 0, 0) 80%,
rgba(0, 0, 0, 0)
);
background-repeat: no-repeat;
box-shadow: 0 2px 0 #000, 0 3px 1px #999, inset 0 1px 1px #aaa,
inset 0 -1px 3px #272727;
color: #eee;
text-shadow: 0 -1px 0 #000;
border-color: #000;
background-color: #4d4c4c;
background-image: linear-gradient(
rgba(0, 0, 0, 0.5),
rgba(0, 0, 0, 0) 80%,
rgba(0, 0, 0, 0)
);
background-repeat: no-repeat;
box-shadow: 0 2px 0 #000, 0 3px 1px #999, inset 0 1px 1px #aaa,
inset 0 -1px 3px #272727;
}
kbd.ios {
font-family: Helvetica, 'Helvetica Neue', Arial, sans-serif;
color: #000;
border-color: rgba(0, 0, 0, .6);
border-top-color: rgba(0, 0, 0, .4);
background-color: #b7b7bc;
background-image: linear-gradient(to bottom, #efeff0, #b7b7bc);
background-repeat: repeat-x;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6), 0 2px 3px rgba(0, 0, 0, .1),
inset 0 1px 0 #fff;
font-family: Helvetica, 'Helvetica Neue', Arial, sans-serif;
color: #000;
border-color: rgba(0, 0, 0, 0.6);
border-top-color: rgba(0, 0, 0, 0.4);
background-color: #b7b7bc;
background-image: linear-gradient(to bottom, #efeff0, #b7b7bc);
background-repeat: repeat-x;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.6), 0 2px 3px rgba(0, 0, 0, 0.1),
inset 0 1px 0 #fff;
}
kbd.android {
font-family: 'RobotoRegular', 'Helvetica Neue', Helvetica, Arial, sans-serif;
color: #fff;
text-shadow: none;
padding: .3em;
border: 1px solid rgba(0, 0, 0, .05);
border-radius: 3px;
background-clip: padding-box;
background: #5e5e5e;
box-shadow: 0 2px 2px rgba(0, 0, 0, .3), 0 1px 0 #444,
inset 0 1px 0 #868686;
font-family: 'RobotoRegular', 'Helvetica Neue', Helvetica, Arial, sans-serif;
color: #fff;
text-shadow: none;
padding: 0.3em;
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 3px;
background-clip: padding-box;
background: #5e5e5e;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.3), 0 1px 0 #444, inset 0 1px 0 #868686;
}
kbd.android.dark {
background: #222;
box-shadow: 0 2px 2px rgba(0, 0, 0, .7), 0 1px 0 #444,
inset 0 1px 0 #505050;
background: #222;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.7), 0 1px 0 #444, inset 0 1px 0 #505050;
}
kbd.android.color {
background: #083c5b;
box-shadow: 0 2px 2px rgba(0, 0, 0, .7), 0 1px 0 #444,
inset 0 1px 0 #36647b;
background: #083c5b;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.7), 0 1px 0 #444, inset 0 1px 0 #36647b;
}
@font-face {
font-family: 'RobotoRegular';
src: url('/media/Roboto-Regular-webfont.eot');
src: url('/media/Roboto-Regular-webfont.eot?#iefix')
format('embedded-opentype'),
url('/media/Roboto-Regular-webfont.woff') format('woff'),
url('/media/Roboto-Regular-webfont.ttf') format('truetype'),
url('/media/Roboto-Regular-webfont.svg#RobotoRegular') format('svg');
font-weight: normal;
font-style: normal;
font-family: 'RobotoRegular';
src: url('/media/Roboto-Regular-webfont.eot');
src: url('/media/Roboto-Regular-webfont.eot?#iefix')
format('embedded-opentype'),
url('/media/Roboto-Regular-webfont.woff') format('woff'),
url('/media/Roboto-Regular-webfont.ttf') format('truetype'),
url('/media/Roboto-Regular-webfont.svg#RobotoRegular') format('svg');
font-weight: normal;
font-style: normal;
}

View File

@ -212,6 +212,7 @@ module.exports = {
'gatsby-plugin-catch-links',
'gatsby-redirect-from',
'gatsby-plugin-meta-redirect',
'gatsby-plugin-offline'
'gatsby-plugin-offline',
'gatsby-plugin-typescript'
]
}

View File

@ -2,13 +2,7 @@ const path = require('path')
const { createFilePath } = require('gatsby-source-filesystem')
const { repoContentPath } = require('../config')
// Create slug, date & github file link for posts from file path values
exports.createMarkdownFields = (node, createNodeField, getNode) => {
const fileNode = getNode(node.parent)
const parsedFilePath = path.parse(fileNode.relativePath)
const slugOriginal = createFilePath({ node, getNode })
// slug
function createSlug(node, createNodeField, slugOriginal, parsedFilePath) {
let slug
if (parsedFilePath.name === 'index') {
@ -22,8 +16,9 @@ exports.createMarkdownFields = (node, createNodeField, getNode) => {
name: 'slug',
value: slug
})
}
// date
function createDate(node, createNodeField, slugOriginal) {
// grab date from file path
let date = new Date(slugOriginal.substring(1, 11)).toISOString() // grab date from file path
@ -36,6 +31,16 @@ exports.createMarkdownFields = (node, createNodeField, getNode) => {
name: 'date',
value: date
})
}
// Create slug, date & github file link for posts from file path values
exports.createMarkdownFields = (node, createNodeField, getNode) => {
const fileNode = getNode(node.parent)
const parsedFilePath = path.parse(fileNode.relativePath)
const slugOriginal = createFilePath({ node, getNode })
createSlug(node, createNodeField, slugOriginal, parsedFilePath)
createDate(node, createNodeField, slugOriginal)
// github file link
const type = fileNode.sourceInstanceName

View File

@ -1,5 +1,5 @@
const path = require('path')
const postsTemplate = path.resolve('src/templates/Posts.jsx')
const postsTemplate = path.resolve('src/templates/Posts.tsx')
const redirects = [
{ f: '/feed', t: '/feed.xml' },
@ -7,7 +7,7 @@ const redirects = [
]
exports.generatePostPages = (createPage, posts, numPages) => {
const postTemplate = path.resolve('src/templates/Post.jsx')
const postTemplate = path.resolve('src/templates/Post.tsx')
// Create Post pages
posts.forEach(post => {

View File

@ -1,6 +1,6 @@
module.exports = {
transform: {
'^.+\\.jsx?$': '<rootDir>/jest/jest-preprocess.js'
'^.+\\.tsx?$': '<rootDir>/jest/jest-preprocess.js'
},
moduleNameMapper: {
'.+\\.(css|styl|less|sass|scss)$': 'identity-obj-proxy',
@ -15,5 +15,6 @@ module.exports = {
},
testURL: 'http://localhost',
setupFiles: ['<rootDir>/jest/loadershim.js'],
setupFilesAfterEnv: ['<rootDir>/jest/setup-test-env.js']
setupFilesAfterEnv: ['<rootDir>/jest/setup-test-env.js'],
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/@types/**/*']
}

View File

@ -1,5 +1,7 @@
const { createTransformer } = require('babel-jest')
const babelOptions = {
presets: ['babel-preset-gatsby']
presets: ['babel-preset-gatsby', '@babel/preset-typescript']
}
module.exports = require('babel-jest').createTransformer(babelOptions)
module.exports = createTransformer(babelOptions)

View File

@ -1 +1 @@
import '@testing-library/jest-dom/extend-expect'
require('@testing-library/jest-dom/extend-expect')

View File

@ -1,6 +1,7 @@
import { render } from '@testing-library/react'
import { ReactElement } from 'react'
const testRender = component => {
const testRender = (component: ReactElement) => {
it('renders without crashing', () => {
const { container } = render(component)

View File

@ -10,17 +10,15 @@
"start": "gatsby develop",
"build": "gatsby build && npm run copy",
"ssr": "npm run build && serve -s public/",
"rename:scrypt": "sed -i -e 's|./build/Release/scrypt|scrypt|g' node_modules/scrypt/index.js",
"copy": "cp -R content/media/ public",
"format": "run-p 'prettier -- --write' format:css",
"format:css": "prettier-stylelint --write --quiet 'src/**/*.{css,scss}'",
"lint": "run-p --continue-on-error lint:js lint:css lint:md",
"lint:js": "eslint --ignore-path .gitignore --ignore-path .prettierignore --ext .js,.jsx .",
"lint:css": "prettier-stylelint --quiet 'src/**/*.{css,scss}'",
"lint:md": "markdownlint './**/*.{md,markdown}' --ignore './{node_modules,public,.cache,.git}/**/*'",
"prettier": "prettier '**/*.{js,jsx,yml,yaml,md}'",
"test": "npm run lint && jest --coverage",
"test:watch": "npm run lint && jest --coverage --watch",
"rename:scrypt": "sed -i -e 's|./build/Release/scrypt|scrypt|g' node_modules/scrypt/index.js",
"copy": "cp -R content/media/ public",
"lint": "run-p --continue-on-error lint:js lint:css lint:md",
"lint:js": "eslint --ignore-path .gitignore --ignore-path .prettierignore --ext .js,.jsx,.ts,.tsx .",
"lint:css": "stylelint 'src/**/*.{css,scss}'",
"lint:md": "markdownlint './**/*.{md,markdown}' --ignore './{node_modules,public,.cache,.git,coverage}/**/*'",
"format": "npm run lint:js -- --fix && npm run lint:css -- --fix",
"deploy": "./scripts/deploy.sh",
"new": "babel-node ./scripts/new.js"
},
@ -31,41 +29,42 @@
"dms2dec": "^1.1.0",
"fast-exif": "^1.0.1",
"fraction.js": "^4.0.12",
"gatsby": "^2.15.18",
"gatsby-image": "^2.2.19",
"gatsby-plugin-catch-links": "^2.1.8",
"gatsby-plugin-feed": "^2.3.11",
"gatsby": "^2.15.28",
"gatsby-image": "^2.2.23",
"gatsby-plugin-catch-links": "^2.1.12",
"gatsby-plugin-feed": "^2.3.15",
"gatsby-plugin-lunr": "^1.5.2",
"gatsby-plugin-manifest": "^2.2.17",
"gatsby-plugin-manifest": "^2.2.20",
"gatsby-plugin-matomo": "^0.7.2",
"gatsby-plugin-meta-redirect": "^1.1.1",
"gatsby-plugin-offline": "^2.2.10",
"gatsby-plugin-react-helmet": "^3.1.6",
"gatsby-plugin-sass": "^2.1.13",
"gatsby-plugin-sharp": "^2.2.24",
"gatsby-plugin-sitemap": "^2.2.13",
"gatsby-plugin-react-helmet": "^3.1.10",
"gatsby-plugin-sass": "^2.1.17",
"gatsby-plugin-sharp": "^2.2.27",
"gatsby-plugin-sitemap": "^2.2.16",
"gatsby-plugin-svgr": "^2.0.2",
"gatsby-plugin-typescript": "^2.1.11",
"gatsby-plugin-webpack-size": "^1.0.0",
"gatsby-redirect-from": "^0.2.1",
"gatsby-remark-autolink-headers": "^2.1.9",
"gatsby-remark-copy-linked-files": "^2.1.17",
"gatsby-remark-images": "^3.1.22",
"gatsby-remark-smartypants": "^2.1.7",
"gatsby-remark-autolink-headers": "^2.1.13",
"gatsby-remark-copy-linked-files": "^2.1.23",
"gatsby-remark-images": "^3.1.25",
"gatsby-remark-smartypants": "^2.1.11",
"gatsby-remark-vscode": "^1.2.0",
"gatsby-source-filesystem": "^2.1.24",
"gatsby-source-graphql": "^2.1.12",
"gatsby-transformer-remark": "^2.6.23",
"gatsby-transformer-sharp": "^2.2.15",
"graphql": "^14.5.6",
"gatsby-source-filesystem": "^2.1.28",
"gatsby-source-graphql": "^2.1.17",
"gatsby-transformer-remark": "^2.6.26",
"gatsby-transformer-sharp": "^2.2.19",
"graphql": "^14.5.8",
"intersection-observer": "^0.7.0",
"js-scrypt": "^0.2.0",
"load-script": "^1.0.0",
"pigeon-maps": "^0.14.0",
"pigeon-marker": "^0.3.4",
"react": "^16.9.0",
"react": "^16.10.1",
"react-blockies": "^1.4.1",
"react-clipboard.js": "^2.0.13",
"react-dom": "^16.9.0",
"react-dom": "^16.10.1",
"react-helmet": "^5.2.1",
"react-modal": "^3.10.1",
"react-pose": "^4.0.8",
@ -78,20 +77,31 @@
"web3": "^1.2.1"
},
"devDependencies": {
"@babel/node": "^7.6.0",
"@babel/preset-env": "^7.6.0",
"@svgr/webpack": "^4.3.1",
"@babel/node": "^7.6.2",
"@babel/preset-env": "^7.6.2",
"@babel/preset-typescript": "^7.6.0",
"@svgr/webpack": "^4.3.3",
"@testing-library/jest-dom": "^4.1.0",
"@testing-library/react": "^9.1.4",
"@testing-library/react": "^9.2.0",
"@types/jest": "^24.0.18",
"@types/node": "^12.7.8",
"@types/react": "^16.9.4",
"@types/react-dom": "^16.9.1",
"@types/react-helmet": "^5.0.11",
"@types/react-modal": "^3.8.3",
"@types/react-transition-group": "^4.2.2",
"@types/web3": "^1.0.20",
"@typescript-eslint/eslint-plugin": "^2.3.2",
"@typescript-eslint/parser": "^2.3.2",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"eslint": "^6.4.0",
"eslint-config-prettier": "^6.2.0",
"eslint-loader": "^3.0.0",
"eslint-plugin-graphql": "^3.0.3",
"eslint": "^6.5.1",
"eslint-config-prettier": "^6.3.0",
"eslint-loader": "^3.0.2",
"eslint-plugin-graphql": "^3.1.0",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-react": "^7.14.3",
"eslint-plugin-react": "^7.15.0",
"fs-extra": "^8.1.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^24.9.0",
@ -104,8 +114,10 @@
"prettier-stylelint": "^0.4.2",
"stylelint": "^11.0.0",
"stylelint-config-css-modules": "^1.5.0",
"stylelint-config-prettier": "^6.0.0",
"stylelint-config-standard": "^19.0.0",
"stylelint-scss": "^3.11.0",
"stylelint-prettier": "^1.1.1",
"typescript": "^3.6.3",
"why-did-you-update": "^1.0.6"
},
"engines": {

13
src/@types/declarations.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
declare module '*.scss' {
const content: { [className: string]: string }
export = content
}
/* eslint-disable-next-line @typescript-eslint/no-empty-interface */
interface SvgrComponent
extends React.StatelessComponent<React.SVGAttributes<SVGElement>> {}
declare module '*.svg' {
const value: SvgrComponent
export default value
}

1
src/@types/pigeon-maps.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'pigeon-maps'

1
src/@types/pigeon-marker.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'pigeon-marker'

1
src/@types/react-blockies.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'react-blockies'

1
src/@types/react-time.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'react-time'

1
src/@types/remark-react.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'remark-react'

View File

@ -1,33 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import Container from './atoms/Container'
import Typekit from './atoms/Typekit'
import Header from './organisms/Header'
import Footer from './organisms/Footer'
import styles from './Layout.module.scss'
// if (process.env.NODE_ENV !== 'production') {
// const { whyDidYouUpdate } = require('why-did-you-update')
// whyDidYouUpdate(React)
// }
const Layout = ({ children }) => (
<>
<Typekit />
<Header />
<main className={styles.document} id="document">
<div className={styles.content}>
<Container>{children}</Container>
</div>
</main>
<Footer />
</>
)
Layout.propTypes = {
children: PropTypes.any.isRequired
}
export default Layout

View File

@ -2,19 +2,19 @@
@import 'mixins';
#___gatsby {
// display: flex;
// min-height: 100vh;
// flex-direction: column;
position: relative;
// display: flex;
// min-height: 100vh;
// flex-direction: column;
position: relative;
}
.content {
padding: 0 $spacer / $line-height;
width: 100%;
padding: 0 $spacer / $line-height;
width: 100%;
@media (min-width: $screen-sm) {
padding: 0 ($spacer * 2);
}
@media (min-width: $screen-sm) {
padding: 0 ($spacer * 2);
}
}
// topbar and footer as fixed
@ -22,27 +22,27 @@
/////////////////////////////////////
.document {
@include transition;
@include transition;
width: 100%;
padding-top: ($spacer * 2);
background-color: $page-background-color;
border-top: 1px solid rgba(255, 255, 255, .7);
border-bottom: 1px solid rgba(255, 255, 255, .7);
padding-bottom: $spacer * 2;
box-shadow: 0 1px 4px rgba($brand-main, .1),
0 -1px 4px rgba($brand-main, .2);
transform: translate3d(0, 0, 0);
width: 100%;
padding-top: ($spacer * 2);
background-color: $page-background-color;
border-top: 1px solid rgba(255, 255, 255, 0.7);
border-bottom: 1px solid rgba(255, 255, 255, 0.7);
padding-bottom: $spacer * 2;
box-shadow: 0 1px 4px rgba($brand-main, 0.1),
0 -1px 4px rgba($brand-main, 0.2);
transform: translate3d(0, 0, 0);
:global(.has-menu-open) & {
transform: translate3d(0, ($spacer * 3), 0);
}
:global(.has-menu-open) & {
transform: translate3d(0, ($spacer * 3), 0);
}
@media (min-width: $screen-sm) and (min-height: 500px) {
margin-top: $spacer * 2.65;
margin-bottom: $spacer * 19; // height of footer
position: relative;
z-index: 2;
min-height: 500px;
}
@media (min-width: $screen-sm) and (min-height: 500px) {
margin-top: $spacer * 2.65;
margin-bottom: $spacer * 19; // height of footer
position: relative;
z-index: 2;
min-height: 500px;
}
}

33
src/components/Layout.tsx Normal file
View File

@ -0,0 +1,33 @@
import React, { ReactElement } from 'react'
import Container from './atoms/Container'
import Typekit from './atoms/Typekit'
import Header from './organisms/Header'
import Footer from './organisms/Footer'
import styles from './Layout.module.scss'
// if (process.env.NODE_ENV !== 'production') {
// const { whyDidYouUpdate } = require('why-did-you-update')
// whyDidYouUpdate(React)
// }
export default function Layout({
children
}: {
location?: Location
children: any
}): ReactElement {
return (
<>
<Typekit />
<Header />
<main className={styles.document} id="document">
<div className={styles.content}>
<Container>{children}</Container>
</div>
</main>
<Footer />
</>
)
}

View File

@ -1,115 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import ModalThanks from '../molecules/ModalThanks'
import styles from './PostActions.module.scss'
import { ReactComponent as Twitter } from '../../images/twitter.svg'
import { ReactComponent as Bitcoin } from '../../images/bitcoin.svg'
import { ReactComponent as GitHub } from '../../images/github.svg'
const ActionContent = ({ title, text, textLink }) => (
<>
<h1 className={styles.actionTitle}>{title}</h1>
<p className={styles.actionText}>
{text} <span className={styles.link}>{textLink}</span>
</p>
</>
)
ActionContent.propTypes = {
title: PropTypes.string,
text: PropTypes.string,
textLink: PropTypes.string
}
const ActionTwitter = ({ url, slug }) => (
<a
className={styles.action}
href={`https://twitter.com/intent/tweet?text=@kremalicious&url=${url}${slug}`}
>
<Twitter />
<ActionContent
title="Have a comment?"
text="Hit me up"
textLink="@kremalicious"
/>
</a>
)
ActionTwitter.propTypes = {
url: PropTypes.string.isRequired,
slug: PropTypes.string.isRequired
}
const ActionCrypto = ({ toggleModal }) => (
<button className={styles.action} onClick={toggleModal}>
<Bitcoin />
<ActionContent
title="Found something useful?"
text="Say thanks with"
textLink="Bitcoins or Ether"
/>
</button>
)
ActionCrypto.propTypes = {
toggleModal: PropTypes.func.isRequired
}
const ActionGitHub = ({ githubLink }) => (
<a className={styles.action} href={githubLink}>
<GitHub />
<ActionContent
title="Edit on GitHub"
text="Contribute to this post on"
textLink="GitHub"
/>
</a>
)
ActionGitHub.propTypes = {
githubLink: PropTypes.string.isRequired
}
export default class PostActions extends PureComponent {
state = {
showModal: false
}
static propTypes = {
slug: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
githubLink: PropTypes.string.isRequired
}
toggleModal = () => {
this.setState({ showModal: !this.state.showModal })
}
render() {
const { url, slug, githubLink } = this.props
return (
<aside className={styles.actions}>
<div>
<ActionTwitter url={url} slug={slug} />
</div>
<div>
<ActionCrypto toggleModal={this.toggleModal} />
</div>
<div>
<ActionGitHub githubLink={githubLink} />
</div>
{this.state.showModal && (
<ModalThanks
isOpen={this.state.showModal}
handleCloseModal={this.toggleModal}
/>
)}
</aside>
)
}
}

View File

@ -2,95 +2,95 @@
@import 'mixins';
.actions {
@include breakoutviewport;
@include breakoutviewport;
margin-top: $spacer * 3;
background: rgba(#fff, .5);
padding-top: $spacer;
padding-bottom: $spacer;
border-radius: $border-radius;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-top: $spacer * 3;
background: rgba(#fff, 0.5);
padding-top: $spacer;
padding-bottom: $spacer;
border-radius: $border-radius;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
@media (min-width: $screen-md) {
margin-left: -100%;
margin-right: -18%;
padding-left: 80%;
@media (min-width: $screen-md) {
margin-left: -100%;
margin-right: -18%;
padding-left: 80%;
}
> div {
flex: 0 0 100%;
border-bottom: 1px dashed rgba($brand-grey-light, 0.3);
&:last-child {
border-bottom: 0;
}
> div {
flex: 0 0 100%;
border-bottom: 1px dashed rgba($brand-grey-light, .3);
@media (min-width: $screen-sm) {
flex: 0 0 33.33333%;
border-bottom: 0;
border-left: 1px dashed rgba($brand-grey-light, 0.3);
&:last-child {
border-bottom: 0;
}
@media (min-width: $screen-sm) {
flex: 0 0 33.33333%;
border-bottom: 0;
border-left: 1px dashed rgba($brand-grey-light, .3);
&:first-child {
border-left: 0;
}
}
&:first-child {
border-left: 0;
}
}
}
}
.link {
transition: .2s ease-out;
color: $link-color;
transition: 0.2s ease-out;
color: $link-color;
}
.actionTitle {
font-size: $font-size-base;
color: $text-color;
margin-top: 0;
margin-bottom: $spacer / 4;
transition: color .2s ease-out;
font-size: $font-size-base;
color: $text-color;
margin-top: 0;
margin-bottom: $spacer / 4;
transition: color 0.2s ease-out;
}
.actionText {
font-size: $font-size-small;
color: $brand-grey-light;
margin-bottom: 0;
transition: color .2s ease-out;
font-size: $font-size-small;
color: $brand-grey-light;
margin-bottom: 0;
transition: color 0.2s ease-out;
}
.action {
display: block;
margin: 0;
padding-top: $spacer;
padding-bottom: $spacer;
padding-left: $spacer * 2;
padding-right: $spacer;
position: relative;
text-align: left;
display: block;
margin: 0;
padding-top: $spacer;
padding-bottom: $spacer;
padding-left: $spacer * 2;
padding-right: $spacer;
position: relative;
text-align: left;
&:hover,
&:focus {
.link,
.actionTitle,
.actionText {
color: $link-color-hover;
}
&:hover,
&:focus {
.link,
.actionTitle,
.actionText {
color: $link-color-hover;
}
}
&:active {
.link,
.actionTitle,
.actionText {
transition: none;
color: $link-color-active;
}
&:active {
.link,
.actionTitle,
.actionText {
transition: none;
color: $link-color-active;
}
}
svg {
position: absolute;
left: $spacer;
top: $spacer;
fill: $brand-grey-light;
}
svg {
position: absolute;
left: $spacer;
top: $spacer;
fill: $brand-grey-light;
}
}

View File

@ -0,0 +1,96 @@
import React, { useState } from 'react'
import ModalThanks from '../molecules/ModalThanks'
import styles from './PostActions.module.scss'
import { ReactComponent as Twitter } from '../../images/twitter.svg'
import { ReactComponent as Bitcoin } from '../../images/bitcoin.svg'
import { ReactComponent as GitHub } from '../../images/github.svg'
const ActionContent = ({
title,
text,
textLink
}: {
title: string
text: string
textLink: string
}) => (
<>
<h1 className={styles.actionTitle}>{title}</h1>
<p className={styles.actionText}>
{text} <span className={styles.link}>{textLink}</span>
</p>
</>
)
const ActionTwitter = ({ url, slug }: { url: string; slug: string }) => (
<a
className={styles.action}
href={`https://twitter.com/intent/tweet?text=@kremalicious&url=${url}${slug}`}
>
<Twitter />
<ActionContent
title="Have a comment?"
text="Hit me up"
textLink="@kremalicious"
/>
</a>
)
const ActionCrypto = ({ toggleModal }: { toggleModal(): void }) => (
<button className={styles.action} onClick={toggleModal}>
<Bitcoin />
<ActionContent
title="Found something useful?"
text="Say thanks with"
textLink="Bitcoins or Ether"
/>
</button>
)
const ActionGitHub = ({ githubLink }: { githubLink: string }) => (
<a className={styles.action} href={githubLink}>
<GitHub />
<ActionContent
title="Edit on GitHub"
text="Contribute to this post on"
textLink="GitHub"
/>
</a>
)
export default function PostActions({
slug,
url,
githubLink
}: {
slug: string
url: string
githubLink: string
}) {
const [showModal, setShowModal] = useState(false)
const toggleModal = () => {
setShowModal(!showModal)
}
return (
<aside className={styles.actions}>
<div>
<ActionTwitter url={url} slug={slug} />
</div>
<div>
<ActionCrypto toggleModal={toggleModal} />
</div>
<div>
<ActionGitHub githubLink={githubLink} />
</div>
{showModal && (
<ModalThanks isOpen={showModal} handleCloseModal={toggleModal} />
)}
</aside>
)
}

View File

@ -1,9 +1,8 @@
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import Changelog from '../atoms/Changelog'
// Remove lead paragraph from content
const PostContent = ({ post }) => {
const PostContent = ({ post }: { post: any }) => {
const separator = '<!-- more -->'
const changelog = post.frontmatter.changelog
@ -19,15 +18,11 @@ const PostContent = ({ post }) => {
}
return (
<Fragment>
<>
<div dangerouslySetInnerHTML={{ __html: content }} />
{changelog && <Changelog repo={changelog} />}
</Fragment>
</>
)
}
PostContent.propTypes = {
post: PropTypes.object
}
export default PostContent

View File

@ -1,26 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import Image from '../atoms/Image'
import styles from './PostImage.module.scss'
const PostImage = ({ title, fluid, fixed, alt }) => (
<figure className={styles.postImage}>
<Image
fluid={fluid ? fluid : null}
fixed={fixed ? fixed : null}
alt={alt}
/>
{title && (
<figcaption className={styles.postImageTitle}>{title}</figcaption>
)}
</figure>
)
PostImage.propTypes = {
fluid: PropTypes.object,
fixed: PropTypes.object,
alt: PropTypes.string.isRequired,
title: PropTypes.string
}
export default PostImage

View File

@ -2,43 +2,43 @@
@import 'mixins';
.postImageTitle {
transition: .1s ease-out;
font-size: $font-size-h3;
font-family: $font-family-headings;
line-height: $line-height-headings;
font-weight: $font-weight-headings;
font-style: normal;
text-align: left;
letter-spacing: -.02em;
margin: 0;
position: absolute;
top: 10%;
padding: $spacer / 3 $spacer;
background: rgba($link-color, .85);
color: #fff;
text-shadow: 0 1px 0 #000;
left: 0;
opacity: 0;
transform: translate3d(0, -20px, 0);
transition: 0.1s ease-out;
font-size: $font-size-h3;
font-family: $font-family-headings;
line-height: $line-height-headings;
font-weight: $font-weight-headings;
font-style: normal;
text-align: left;
letter-spacing: -0.02em;
margin: 0;
position: absolute;
top: 10%;
padding: $spacer / 3 $spacer;
background: rgba($link-color, 0.85);
color: #fff;
text-shadow: 0 1px 0 #000;
left: 0;
opacity: 0;
transform: translate3d(0, -20px, 0);
}
.postImage {
@include breakoutviewport();
@include breakoutviewport();
max-width: none;
max-width: none;
display: block;
margin-top: $spacer * 1.5;
margin-bottom: $spacer * 1.5;
a & {
position: relative;
display: block;
margin-top: $spacer * 1.5;
margin-bottom: $spacer * 1.5;
}
a & {
position: relative;
display: block;
}
a:hover & {
.postImageTitle {
opacity: 1;
transform: translate3d(0, 0, 0);
}
a:hover & {
.postImageTitle {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
}

View File

@ -0,0 +1,22 @@
import React from 'react'
import Image from '../atoms/Image'
import styles from './PostImage.module.scss'
import { FluidObject, FixedObject } from 'gatsby-image'
interface PostImageProps {
title?: string
fluid?: FluidObject
fixed?: FixedObject
alt: string
}
const PostImage = ({ title, fluid, fixed, alt }: PostImageProps) => (
<figure className={styles.postImage}>
<Image fluid={fluid} fixed={fixed} alt={alt} />
{title && (
<figcaption className={styles.postImageTitle}>{title}</figcaption>
)}
</figure>
)
export default PostImage

View File

@ -1,10 +1,10 @@
@import 'variables';
.lead {
font-size: $font-size-large;
margin-bottom: $spacer;
font-size: $font-size-large;
margin-bottom: $spacer;
}
.index {
font-size: $font-size-base;
font-size: $font-size-base;
}

View File

@ -1,10 +1,15 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './PostLead.module.scss'
// Extract lead paragraph from content
// Grab everything before more tag, or just first paragraph
const PostLead = ({ post, index }) => {
const PostLead = ({
post,
index
}: {
post: { html: string }
index?: boolean
}) => {
let lead
const content = post.html
const separator = '<!-- more -->'
@ -23,9 +28,4 @@ const PostLead = ({ post, index }) => {
)
}
PostLead.propTypes = {
post: PropTypes.object,
index: PropTypes.bool
}
export default PostLead

View File

@ -1,24 +1,24 @@
@import 'variables';
.postLinkActions {
display: flex;
justify-content: space-between;
margin-top: $spacer * 2;
display: flex;
justify-content: space-between;
margin-top: $spacer * 2;
a {
svg {
width: $font-size-small;
height: $font-size-small;
display: inline-block;
fill: $text-color-light;
}
&:last-child {
svg {
width: $font-size-base;
height: $font-size-base;
fill: $brand-cyan;
}
}
a {
svg {
width: $font-size-small;
height: $font-size-small;
display: inline-block;
fill: $text-color-light;
}
&:last-child {
svg {
width: $font-size-base;
height: $font-size-base;
fill: $brand-cyan;
}
}
}
}

View File

@ -1,12 +1,17 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Link } from 'gatsby'
import { ReactComponent as Forward } from '../../images/forward.svg'
import { ReactComponent as Infinity } from '../../images/infinity.svg'
import styles from './PostLinkActions.module.scss'
import stylesPostMore from './PostMore.module.scss'
const PostLinkActions = ({ linkurl, slug }) => (
const PostLinkActions = ({
linkurl,
slug
}: {
linkurl?: string
slug: string
}) => (
<div className={styles.postLinkActions}>
<a className={stylesPostMore.postMore} href={linkurl}>
Go to source <Forward />
@ -17,9 +22,4 @@ const PostLinkActions = ({ linkurl, slug }) => (
</div>
)
PostLinkActions.propTypes = {
slug: PropTypes.string.isRequired,
linkurl: PropTypes.string
}
export default PostLinkActions

View File

@ -4,82 +4,82 @@
/////////////////////////////////////
.entryMeta {
font-size: $font-size-small;
margin-top: $spacer * 2;
color: $brand-grey-light;
font-size: $font-size-small;
margin-top: $spacer * 2;
color: $brand-grey-light;
}
.byline,
.time,
.tags,
.categories {
text-align: center;
text-align: center;
}
.byline,
.time {
font-style: italic;
font-style: italic;
}
.byline {
margin-bottom: 0;
margin-bottom: 0;
}
.by {
display: block;
display: block;
}
.time {
margin-bottom: $spacer * 2;
margin-bottom: $spacer * 2;
}
// Types & Tags
/////////////////////////////////////
.type {
text-align: center;
a {
font-size: $font-size-mini;
text-align: center;
color: $text-color;
line-height: 1;
text-transform: uppercase;
border: 1px solid $text-color;
border-radius: $border-radius;
padding: 4px 8px;
margin: 0;
display: inline-block;
a {
font-size: $font-size-mini;
text-align: center;
color: $text-color;
line-height: 1;
text-transform: uppercase;
border: 1px solid $text-color;
border-radius: $border-radius;
padding: 4px 8px;
margin: 0;
display: inline-block;
&:hover,
&:focus {
color: $link-color;
border-color: $link-color;
}
&:active {
background: $link-color;
top: 0;
color: #fff;
}
&:hover,
&:focus {
color: $link-color;
border-color: $link-color;
}
&:active {
background: $link-color;
top: 0;
color: #fff;
}
}
}
.tags {
margin-top: $spacer / 2;
margin-top: $spacer / 2;
}
.tag {
color: $text-color;
margin-left: $spacer / 2;
margin-right: $spacer / 2;
margin-bottom: $spacer / 2;
white-space: nowrap;
display: inline-block;
color: $text-color;
margin-left: $spacer / 2;
margin-right: $spacer / 2;
margin-bottom: $spacer / 2;
white-space: nowrap;
display: inline-block;
&::before {
color: $brand-grey-light;
content: '#';
margin-right: 1px;
}
&::before {
color: $brand-grey-light;
content: '#';
margin-right: 1px;
}
}

View File

@ -1,11 +1,10 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Link } from 'gatsby'
import Time from 'react-time'
import slugify from 'slugify'
import styles from './PostMeta.module.scss'
const PostMeta = ({ post, meta }) => {
const PostMeta = ({ post, meta }: { post: any; meta: any }) => {
const { author, updated, tags, type } = post.frontmatter
const { date } = post.fields
@ -40,7 +39,7 @@ const PostMeta = ({ post, meta }) => {
{tags && (
<div className={styles.tags}>
{tags.map(tag => {
{tags.map((tag: string) => {
const to = tag === 'goodies' ? '/goodies' : `/tags/${slugify(tag)}/`
return (
@ -55,9 +54,4 @@ const PostMeta = ({ post, meta }) => {
)
}
PostMeta.propTypes = {
post: PropTypes.object.isRequired,
meta: PropTypes.object.isRequired
}
export default PostMeta

View File

@ -1,29 +1,29 @@
@import 'variables';
.postMore {
display: inline-block;
font-family: $font-family-headings;
font-weight: $font-weight-headings;
font-size: $font-size-base * 0.9;
color: $link-color;
text-transform: uppercase;
margin-top: $spacer;
svg {
display: inline-block;
font-family: $font-family-headings;
font-weight: $font-weight-headings;
font-size: $font-size-base * .9;
color: $link-color;
text-transform: uppercase;
margin-top: $spacer;
margin: 0;
top: 0.2rem;
position: relative;
width: 1.1rem;
height: 1.1rem;
fill: $text-color-light;
transition: 0.2s ease-out;
}
&:hover,
&:focus {
svg {
display: inline-block;
margin: 0;
top: .2rem;
position: relative;
width: 1.1rem;
height: 1.1rem;
fill: $text-color-light;
transition: .2s ease-out;
}
&:hover,
&:focus {
svg {
transform: translate3d(.2rem, 0, 0);
}
transform: translate3d(0.2rem, 0, 0);
}
}
}

View File

@ -1,19 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Link } from 'gatsby'
import styles from './PostMore.module.scss'
import { ReactComponent as Caret } from '../../images/chevron-right.svg'
const PostMore = ({ to, children }) => (
const PostMore = ({ to, children }: { to: string; children: string }) => (
<Link className={styles.postMore} to={to}>
{children}
<Caret />
</Link>
)
PostMore.propTypes = {
to: PropTypes.string.isRequired,
children: PropTypes.string.isRequired
}
export default PostMore

View File

@ -1,36 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { Link } from 'gatsby'
import Image from '../atoms/Image'
import styles from './PostTeaser.module.scss'
export default class PostTeaser extends PureComponent {
static propTypes = {
post: PropTypes.object.isRequired,
toggleSearch: PropTypes.func
}
render() {
const { post, toggleSearch } = this.props
return (
<li>
<Link to={post.fields.slug} onClick={toggleSearch && toggleSearch}>
{post.frontmatter.image ? (
<>
<Image
fluid={post.frontmatter.image.childImageSharp.fluid}
alt={post.frontmatter.title}
/>
<h4 className={styles.postTitle}>{post.frontmatter.title}</h4>
</>
) : (
<div className={styles.empty}>
<h4 className={styles.postTitle}>{post.frontmatter.title}</h4>
</div>
)}
</Link>
</li>
)
}
}

View File

@ -1,29 +1,29 @@
@import 'variables';
.postTitle {
display: inline-block;
margin-top: $spacer / 4;
margin-bottom: 0;
font-size: $font-size-small;
line-height: $line-height-small;
color: $brand-grey-light;
padding-left: .2rem;
padding-right: .2rem;
transition: color .2s ease-out;
display: inline-block;
margin-top: $spacer / 4;
margin-bottom: 0;
font-size: $font-size-small;
line-height: $line-height-small;
color: $brand-grey-light;
padding-left: 0.2rem;
padding-right: 0.2rem;
transition: color 0.2s ease-out;
@media (min-width: $screen-md) {
font-size: $font-size-base;
}
@media (min-width: $screen-md) {
font-size: $font-size-base;
}
}
.empty {
height: 100%;
min-height: 80px;
display: flex;
align-items: center;
padding: $spacer / 4;
height: 100%;
min-height: 80px;
display: flex;
align-items: center;
padding: $spacer / 4;
.postTitle {
margin-top: 0;
}
.postTitle {
margin-top: 0;
}
}

View File

@ -0,0 +1,32 @@
import React from 'react'
import { Link } from 'gatsby'
import Image from '../atoms/Image'
import styles from './PostTeaser.module.scss'
export default function PostTeaser({
post,
toggleSearch
}: {
post: { fields: { slug: string }; frontmatter: { image: any; title: string } }
toggleSearch?: () => void
}) {
const { image, title } = post.frontmatter
const { slug } = post.fields
return (
<li>
<Link to={slug} onClick={toggleSearch && toggleSearch}>
{image ? (
<>
<Image fluid={image.childImageSharp.fluid} alt={title} />
<h4 className={styles.postTitle}>{title}</h4>
</>
) : (
<div className={styles.empty}>
<h4 className={styles.postTitle}>{title}</h4>
</div>
)}
</Link>
</li>
)
}

View File

@ -5,32 +5,32 @@
/////////////////////////////////////
.hentry__title {
font-size: $font-size-h1;
color: $color-headings;
margin-top: 0;
margin-bottom: $spacer;
font-size: $font-size-h1;
color: $color-headings;
margin-top: 0;
margin-bottom: $spacer;
}
.hentry__title__link {
font-size: $font-size-h3;
font-size: $font-size-h3;
svg {
width: $font-size-base;
height: $font-size-base;
display: inline-block;
fill: $text-color-light;
vertical-align: baseline;
}
svg {
width: $font-size-base;
height: $font-size-base;
display: inline-block;
fill: $text-color-light;
vertical-align: baseline;
}
}
.linkurl {
@include ellipsis();
@include ellipsis();
width: 100%;
color: $text-color;
font-family: $font-family-base;
font-size: $font-size-small;
padding: ($spacer/4) 0;
margin-top: -($spacer);
margin-bottom: $spacer;
width: 100%;
color: $text-color;
font-family: $font-family-base;
font-size: $font-size-small;
padding: ($spacer/4) 0;
margin-top: -($spacer);
margin-bottom: $spacer;
}

View File

@ -1,14 +1,23 @@
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import { Link } from 'gatsby'
import { ReactComponent as Forward } from '../../images/forward.svg'
import styles from './PostTitle.module.scss'
const PostTitle = ({ type, slug, linkurl, title }) => {
export default function PostTitle({
type,
slug,
linkurl,
title
}: {
type?: string
slug?: string
linkurl?: string
title: string
}) {
const linkHostname = linkurl ? new URL(linkurl).hostname : null
return type === 'link' ? (
<Fragment>
<>
<h1
className={[styles.hentry__title, styles.hentry__title__link].join(' ')}
>
@ -17,7 +26,7 @@ const PostTitle = ({ type, slug, linkurl, title }) => {
</a>
</h1>
<div className={styles.linkurl}>{linkHostname}</div>
</Fragment>
</>
) : slug ? (
<h1 className={styles.hentry__title}>
<Link to={slug}>{title}</Link>
@ -26,12 +35,3 @@ const PostTitle = ({ type, slug, linkurl, title }) => {
<h1 className={styles.hentry__title}>{title}</h1>
)
}
PostTitle.propTypes = {
type: PropTypes.string,
title: PropTypes.string.isRequired,
slug: PropTypes.string,
linkurl: PropTypes.string
}
export default PostTitle

View File

@ -1,46 +1,46 @@
@import 'variables';
.search {
position: absolute;
left: $spacer / 2;
right: $spacer / 2;
top: -($spacer / 4);
z-index: 10;
position: absolute;
left: $spacer / 2;
right: $spacer / 2;
top: -($spacer / 4);
z-index: 10;
input {
width: 100%;
}
input {
width: 100%;
}
@media (min-width: $screen-md) {
left: 0;
right: 0;
}
@media (min-width: $screen-md) {
left: 0;
right: 0;
}
}
.appear,
.enter {
opacity: .01;
transform: translate3d(0, -100px, 0);
opacity: 0.01;
transform: translate3d(0, -100px, 0);
&.appearActive,
&.enterActive {
opacity: 1;
transition: .2s ease-out;
transform: translate3d(0, 0, 0);
}
&.appearActive,
&.enterActive {
opacity: 1;
transition: 0.2s ease-out;
transform: translate3d(0, 0, 0);
}
}
.exit {
opacity: 1;
transform: translate3d(0, 0, 0);
opacity: 1;
transform: translate3d(0, 0, 0);
&.exitActive {
opacity: .01;
transition: .2s ease-in;
transform: translate3d(0, -100px, 0);
}
&.exitActive {
opacity: 0.01;
transition: 0.2s ease-in;
transform: translate3d(0, -100px, 0);
}
}
:global(.hasSearchOpen) {
overflow: hidden;
overflow: hidden;
}

View File

@ -8,7 +8,10 @@ import SearchResults from './SearchResults'
import styles from './Search.module.scss'
export default class Search extends PureComponent {
export default class Search extends PureComponent<
{},
{ searchOpen: boolean; query: string; results: string[] }
> {
state = {
searchOpen: false,
query: '',
@ -25,14 +28,14 @@ export default class Search extends PureComponent {
}))
}
getSearchResults(query) {
getSearchResults(query: string) {
if (!query || !window.__LUNR__) return []
const lunrIndex = window.__LUNR__[this.props.lng]
const results = lunrIndex.index.search(query)
return results.map(({ ref }) => lunrIndex.store[ref])
}
search = event => {
search = (event: any) => {
const query = event.target.value
// wildcard search https://lunrjs.com/guides/searching.html#wildcards
const results = query.length > 1 ? this.getSearchResults(`${query}*`) : []
@ -65,7 +68,7 @@ export default class Search extends PureComponent {
<section className={styles.search}>
<SearchInput
value={query}
onChange={this.search}
onChange={() => this.search}
onToggle={this.toggleSearch}
/>
</section>

View File

@ -1,33 +1,33 @@
@import 'variables';
.searchButton {
padding: .65rem .85rem;
text-align: center;
line-height: 1;
vertical-align: middle;
display: inline-block;
margin-right: $spacer / 4;
padding: 0.65rem 0.85rem;
text-align: center;
line-height: 1;
vertical-align: middle;
display: inline-block;
margin-right: $spacer / 4;
&:focus {
outline: 0;
}
&:focus {
outline: 0;
}
svg {
fill: $text-color-light;
width: 21px;
height: 21px;
}
&:hover,
&:focus {
svg {
fill: $text-color-light;
width: 21px;
height: 21px;
fill: $brand-cyan;
}
}
&:hover,
&:focus {
svg {
fill: $brand-cyan;
}
}
&:active {
svg {
fill: darken($brand-cyan, 30%);
}
&:active {
svg {
fill: darken($brand-cyan, 30%);
}
}
}

View File

@ -2,7 +2,7 @@ import React from 'react'
import { ReactComponent as SearchIcon } from '../../images/magnifying-glass.svg'
import styles from './SearchButton.module.scss'
const SearchButton = props => (
const SearchButton = (props: any) => (
<button
type="button"
title="Search"

View File

@ -1,31 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Input from '../atoms/Input'
import styles from './SearchInput.module.scss'
export default class SearchInput extends PureComponent {
static propTypes = {
onToggle: PropTypes.func
}
render() {
return (
<>
<Input
className={styles.searchInput}
type="search"
placeholder="Search everything"
autoFocus // eslint-disable-line
{...this.props}
/>
<button
className={styles.searchInputClose}
onClick={this.props.onToggle}
title="Close search"
>
&times;
</button>
</>
)
}
}

View File

@ -1,27 +1,27 @@
@import 'variables';
.searchInput {
composes: input from '../atoms/Input.module.scss';
composes: input from '../atoms/Input.module.scss';
background: $input-bg-focus;
&::-webkit-search-cancel-button {
display: none;
}
&:hover {
background: $input-bg-focus;
&::-webkit-search-cancel-button {
display: none;
}
&:hover {
background: $input-bg-focus;
}
}
}
.searchInputClose {
position: absolute;
right: $spacer / 2;
top: $spacer / 5;
font-size: $font-size-h3;
color: $brand-grey-light;
position: absolute;
right: $spacer / 2;
top: $spacer / 5;
font-size: $font-size-h3;
color: $brand-grey-light;
&:hover,
&:focus {
color: $link-color;
}
&:hover,
&:focus {
color: $link-color;
}
}

View File

@ -0,0 +1,33 @@
import React from 'react'
import Input from '../atoms/Input'
import styles from './SearchInput.module.scss'
export default function SearchInput({
value,
onToggle,
onChange
}: {
value: string
onToggle(): void
onChange(): void
}) {
return (
<>
<Input
className={styles.searchInput}
type="search"
placeholder="Search everything"
autoFocus // eslint-disable-line
value={value}
onChange={onChange}
/>
<button
className={styles.searchInputClose}
onClick={onToggle}
title="Close search"
>
&times;
</button>
</>
)
}

View File

@ -1,82 +0,0 @@
import React, { PureComponent } from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
import { graphql, StaticQuery } from 'gatsby'
import Container from '../atoms/Container'
import PostTeaser from '../Post/PostTeaser'
import SearchResultsEmpty from './SearchResultsEmpty'
import styles from './SearchResults.module.scss'
const query = graphql`
query {
allMarkdownRemark {
edges {
node {
id
frontmatter {
title
image {
childImageSharp {
...ImageFluidThumb
}
}
}
fields {
slug
}
}
}
}
}
`
export default class SearchResults extends PureComponent {
static propTypes = {
results: PropTypes.array.isRequired,
searchQuery: PropTypes.string.isRequired,
toggleSearch: PropTypes.func.isRequired
}
render() {
const { searchQuery, results, toggleSearch } = this.props
return (
<StaticQuery
query={query}
render={data => {
const posts = data.allMarkdownRemark.edges
// creating portal to break out of DOM node we're in
// and render the results in content container
return ReactDOM.createPortal(
<div className={styles.searchResults}>
<Container>
{results.length > 0 ? (
<ul>
{results.map(page =>
posts
.filter(post => post.node.fields.slug === page.slug)
.map(({ node }) => (
<PostTeaser
key={page.slug}
post={node}
toggleSearch={toggleSearch}
/>
))
)}
</ul>
) : (
<SearchResultsEmpty
searchQuery={searchQuery}
results={results}
/>
)}
</Container>
</div>,
document.getElementById('document')
)
}}
/>
)
}
}

View File

@ -2,74 +2,74 @@
@import 'mixins';
.searchResults {
position: absolute;
left: 0;
right: 0;
z-index: 10;
top: 0;
bottom: 0;
background: rgba($body-background-color, .95);
backdrop-filter: blur(5px);
animation: fadein .3s;
overflow: scroll;
-webkit-overflow-scrolling: touch;
height: 91vh;
position: absolute;
left: 0;
right: 0;
z-index: 10;
top: 0;
bottom: 0;
background: rgba($body-background-color, 0.95);
backdrop-filter: blur(5px);
animation: fadein 0.3s;
overflow: scroll;
-webkit-overflow-scrolling: touch;
height: 91vh;
ul {
@include breakoutviewport;
ul {
@include breakoutviewport;
padding: $spacer $spacer / 2;
margin-bottom: 0;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: $spacer $spacer / 2;
margin-bottom: 0;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
@media (min-width: $screen-md) {
padding-left: 0;
padding-right: 0;
}
li {
display: block;
flex: 0 0 48%;
margin-bottom: $spacer;
@media (min-width: $screen-sm) {
flex-basis: 31%;
}
&::before {
display: none;
}
}
@media (min-width: $screen-md) {
padding-left: 0;
padding-right: 0;
}
img {
margin-bottom: 0;
li {
display: block;
flex: 0 0 48%;
margin-bottom: $spacer;
@media (min-width: $screen-sm) {
flex-basis: 31%;
}
&::before {
display: none;
}
}
}
img {
margin-bottom: 0;
}
a {
display: block;
> div {
margin-bottom: 0;
}
a {
display: block;
> div {
margin-bottom: 0;
}
&:hover,
&:focus {
h4 {
color: $link-color;
}
}
&:hover,
&:focus {
h4 {
color: $link-color;
}
}
}
}
@keyframes fadein {
0% {
opacity: 0;
}
0% {
opacity: 0;
}
100% {
opacity: 1;
}
100% {
opacity: 1;
}
}

View File

@ -0,0 +1,79 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { graphql, StaticQuery } from 'gatsby'
import Container from '../atoms/Container'
import PostTeaser from '../Post/PostTeaser'
import SearchResultsEmpty from './SearchResultsEmpty'
import styles from './SearchResults.module.scss'
const query = graphql`
query {
allMarkdownRemark {
edges {
node {
id
frontmatter {
title
image {
childImageSharp {
...ImageFluidThumb
}
}
}
fields {
slug
}
}
}
}
}
`
export default function SearchResults({
searchQuery,
results,
toggleSearch
}: {
searchQuery: string
results: any
toggleSearch(): void
}) {
return (
<StaticQuery
query={query}
render={data => {
const posts = data.allMarkdownRemark.edges
// creating portal to break out of DOM node we're in
// and render the results in content container
return ReactDOM.createPortal(
<div className={styles.searchResults}>
<Container>
{results.length > 0 ? (
<ul>
{results.map(page =>
posts
.filter(post => post.node.fields.slug === page.slug)
.map(({ node }) => (
<PostTeaser
key={page.slug}
post={node}
toggleSearch={toggleSearch}
/>
))
)}
</ul>
) : (
<SearchResultsEmpty
searchQuery={searchQuery}
results={results}
/>
)}
</Container>
</div>,
document.getElementById('document')
)
}}
/>
)
}

View File

@ -1,34 +1,34 @@
@import 'variables';
.empty {
padding-top: 15vh;
display: flex;
justify-content: center;
padding-top: 15vh;
display: flex;
justify-content: center;
}
.emptyMessage {
color: $brand-grey-light;
color: $brand-grey-light;
}
.emptyMessageText {
margin-bottom: 0;
position: relative;
margin-bottom: 0;
position: relative;
&::after {
overflow: hidden;
display: inline-block;
vertical-align: bottom;
animation: ellipsis steps(4, end) 1s infinite;
content: '\2026'; // ascii code for the ellipsis character
width: 0;
position: absolute;
left: 101%;
bottom: 0;
}
&::after {
overflow: hidden;
display: inline-block;
vertical-align: bottom;
animation: ellipsis steps(4, end) 1s infinite;
content: '\2026'; // ascii code for the ellipsis character
width: 0;
position: absolute;
left: 101%;
bottom: 0;
}
}
@keyframes ellipsis {
to {
width: 1rem;
}
to {
width: 1rem;
}
}

View File

@ -1,8 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './SearchResultsEmpty.module.scss'
const SearchResultsEmpty = ({ searchQuery, results }) => (
const SearchResultsEmpty = ({
searchQuery,
results
}: {
searchQuery: string
results: []
}) => (
<div className={styles.empty}>
<header className={styles.emptyMessage}>
<p className={styles.emptyMessageText}>
@ -16,9 +21,4 @@ const SearchResultsEmpty = ({ searchQuery, results }) => (
</div>
)
SearchResultsEmpty.propTypes = {
results: PropTypes.array.isRequired,
searchQuery: PropTypes.string.isRequired
}
export default SearchResultsEmpty

View File

@ -1,19 +1,19 @@
@import 'variables';
.account {
font-size: $font-size-mini;
color: $brand-grey-light;
max-width: 8rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: $font-size-mini;
color: $brand-grey-light;
max-width: 8rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.identicon {
border-radius: 50%;
overflow: hidden;
display: inline-block;
vertical-align: middle;
margin-right: $spacer / 8;
margin-left: $spacer;
border-radius: 50%;
overflow: hidden;
display: inline-block;
vertical-align: middle;
margin-right: $spacer / 8;
margin-left: $spacer;
}

View File

@ -1,17 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
import Blockies from 'react-blockies'
import styles from './Account.module.scss'
const Account = ({ account }) => (
const Account = ({ account }: { account: string }) => (
<div className={styles.account} title={account}>
<Blockies seed={account} scale={2} size={8} className={styles.identicon} />
{account}
</div>
)
Account.propTypes = {
account: PropTypes.string.isRequired
}
export default Account

View File

@ -2,44 +2,44 @@
@import 'mixins';
.alert {
font-size: $font-size-small;
font-size: $font-size-small;
display: inline-block;
&:empty {
display: none;
}
&::after {
overflow: hidden;
display: inline-block;
&:empty {
display: none;
}
&::after {
overflow: hidden;
display: inline-block;
vertical-align: bottom;
animation: ellipsis steps(4, end) 1s infinite;
content: '\2026'; // ascii code for the ellipsis character
width: 0;
position: absolute;
}
vertical-align: bottom;
animation: ellipsis steps(4, end) 1s infinite;
content: '\2026'; // ascii code for the ellipsis character
width: 0;
position: absolute;
}
}
.error {
composes: alert;
color: darken($alert-error, 60%);
composes: alert;
color: darken($alert-error, 60%);
&::after {
display: none;
}
&::after {
display: none;
}
}
.success {
composes: alert;
color: darken($alert-success, 60%);
composes: alert;
color: darken($alert-success, 60%);
&::after {
display: none;
}
&::after {
display: none;
}
}
@keyframes ellipsis {
to {
width: .75rem;
}
to {
width: 0.75rem;
}
}

View File

@ -1,8 +1,10 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import styles from './Alerts.module.scss'
export const alertMessages = (networkName, transactionHash) => ({
export const alertMessages = (
networkName?: string,
transactionHash?: string
) => ({
noAccount:
'Web3 detected, but no account. Are you logged into your MetaMask account?',
noCorrectNetwork: `Please connect to <strong>Main</strong> network. You are on <strong>${networkName}</strong> right now.`,
@ -14,31 +16,31 @@ export const alertMessages = (networkName, transactionHash) => ({
success: 'Confirmed. You are awesome, thanks!'
})
export default class Alerts extends PureComponent {
static propTypes = {
message: PropTypes.object,
transactionHash: PropTypes.string
}
constructMessage = () => {
const { transactionHash, message } = this.props
export default function Alerts({
transactionHash,
message
}: {
transactionHash: string | null
message: { text: MessageChannel; status: string } | null
}) {
const constructMessage = () => {
let messageOutput
if (transactionHash) {
messageOutput =
message &&
message.text +
'<br />' +
alertMessages(null, transactionHash).transaction
'<br />' +
alertMessages(null, transactionHash).transaction
} else {
messageOutput = message.text
messageOutput = message && message.text
}
return messageOutput
}
classes() {
const { status } = this.props.message
const classes = () => {
const { status } = message
if (status === 'success') {
return styles.success
@ -48,12 +50,10 @@ export default class Alerts extends PureComponent {
return styles.alert
}
render() {
return (
<div
className={this.classes()}
dangerouslySetInnerHTML={{ __html: this.constructMessage() }}
/>
)
}
return (
<div
className={classes()}
dangerouslySetInnerHTML={{ __html: constructMessage() }}
/>
)
}

View File

@ -1,11 +1,11 @@
@import 'variables';
.conversion {
font-size: $font-size-mini;
color: $brand-grey-light;
text-align: center;
font-size: $font-size-mini;
color: $brand-grey-light;
text-align: center;
span {
margin-left: $spacer / 2;
}
span {
margin-left: $spacer / 2;
}
}

View File

@ -1,13 +1,11 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { getFiat } from './utils'
import styles from './Conversion.module.scss'
export default class Conversion extends PureComponent {
static propTypes = {
amount: PropTypes.string.isRequired
}
export default class Conversion extends PureComponent<
{ amount: string },
{ euro: string; dollar: string }
> {
state = {
euro: '0.00',
dollar: '0.00'
@ -17,7 +15,7 @@ export default class Conversion extends PureComponent {
this.getFiatResponse()
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: any) {
const { amount } = this.props
if (amount !== prevProps.amount) {

View File

@ -1,48 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Input from '../atoms/Input'
import Account from './Account'
import Conversion from './Conversion'
import styles from './InputGroup.module.scss'
export default class InputGroup extends PureComponent {
static propTypes = {
amount: PropTypes.string.isRequired,
onAmountChange: PropTypes.func.isRequired,
sendTransaction: PropTypes.func.isRequired,
selectedAccount: PropTypes.string
}
render() {
const {
amount,
onAmountChange,
sendTransaction,
selectedAccount
} = this.props
return (
<div className={styles.inputGroup}>
<div className={styles.input}>
<Input
type="number"
value={amount}
onChange={onAmountChange}
min="0"
step="0.01"
/>
<div className={styles.currency}>
<span>ETH</span>
</div>
</div>
<button className="btn btn-primary" onClick={sendTransaction}>
Make it rain
</button>
<div className={styles.infoline}>
<Conversion amount={amount} />
{selectedAccount && <Account account={selectedAccount} />}
</div>
</div>
)
}
}

View File

@ -2,97 +2,97 @@
@import 'mixins';
.inputGroup {
max-width: 18rem;
margin: auto;
position: relative;
animation: fadeIn .8s ease-out backwards;
max-width: 18rem;
margin: auto;
position: relative;
animation: fadeIn 0.8s ease-out backwards;
@media (min-width: $screen-sm) {
display: flex;
flex-wrap: wrap;
}
button {
width: 100%;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-color: lighten($brand-grey-light, 10%);
@media (min-width: $screen-sm) {
display: flex;
flex-wrap: wrap;
}
button {
width: 100%;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-color: lighten($brand-grey-light, 10%);
@media (min-width: $screen-sm) {
width: 50%;
border-top-right-radius: $border-radius;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 0;
}
width: 50%;
border-top-right-radius: $border-radius;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 0;
}
}
}
.input {
position: relative;
position: relative;
@media (min-width: $screen-sm) {
width: 50%;
}
input {
text-align: center;
border: 1px solid lighten($brand-grey-light, 20%);
font-size: $font-size-large;
padding: $spacer / 3 $spacer / 3 $spacer / 3 $spacer * 1.7;
border-bottom: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
@media (min-width: $screen-sm) {
width: 50%;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-bottom-left-radius: $border-radius;
border-bottom: 1px solid lighten($brand-grey-light, 20%);
border-right: 0;
}
input {
text-align: center;
border: 1px solid lighten($brand-grey-light, 20%);
font-size: $font-size-large;
padding: $spacer / 3 $spacer / 3 $spacer / 3 $spacer * 1.7;
border-bottom: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
@media (min-width: $screen-sm) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-bottom-left-radius: $border-radius;
border-bottom: 1px solid lighten($brand-grey-light, 20%);
border-right: 0;
}
&::-webkit-inner-spin-button {
margin-left: -($spacer / 2);
}
&::-webkit-inner-spin-button {
margin-left: -($spacer / 2);
}
}
}
.currency {
position: absolute;
top: 1px;
bottom: 1px;
left: 1px;
font-size: $font-size-small;
padding: $spacer / 3;
color: $brand-grey-light;
background: $brand-light;
border-right: 1px solid rgba($brand-grey-light, .4);
border-top-left-radius: $border-radius;
border-bottom-left-radius: $border-radius;
display: flex;
align-items: center;
position: absolute;
top: 1px;
bottom: 1px;
left: 1px;
font-size: $font-size-small;
padding: $spacer / 3;
color: $brand-grey-light;
background: $brand-light;
border-right: 1px solid rgba($brand-grey-light, 0.4);
border-top-left-radius: $border-radius;
border-bottom-left-radius: $border-radius;
display: flex;
align-items: center;
}
.infoline {
flex-basis: 100%;
display: flex;
align-items: center;
justify-content: center;
margin-top: $spacer / 4;
animation: fadeIn .5s .8s ease-out backwards;
flex-basis: 100%;
display: flex;
align-items: center;
justify-content: center;
margin-top: $spacer / 4;
animation: fadeIn 0.5s 0.8s ease-out backwards;
}
.message {
composes: message from './index.module.scss';
composes: message from './index.module.scss';
}
@keyframes fadeIn {
from {
opacity: .01;
}
from {
opacity: 0.01;
}
to {
opacity: 1;
}
to {
opacity: 1;
}
}

View File

@ -0,0 +1,41 @@
import React from 'react'
import Input from '../atoms/Input'
import Account from './Account'
import Conversion from './Conversion'
import styles from './InputGroup.module.scss'
export default function InputGroup({
amount,
onAmountChange,
sendTransaction,
selectedAccount
}: {
amount: string
onAmountChange(target: any): void
sendTransaction(): void
selectedAccount?: string | null
}) {
return (
<div className={styles.inputGroup}>
<div className={styles.input}>
<Input
type="number"
value={amount}
onChange={onAmountChange}
min="0"
step="0.01"
/>
<div className={styles.currency}>
<span>ETH</span>
</div>
</div>
<button className="btn btn-primary" onClick={sendTransaction}>
Make it rain
</button>
<div className={styles.infoline}>
<Conversion amount={amount} />
{selectedAccount && <Account account={selectedAccount} />}
</div>
</div>
)
}

View File

@ -2,60 +2,60 @@
@import 'mixins';
.web3 {
@include divider;
@include divider;
width: 100%;
text-align: center;
margin-top: $spacer / 2;
margin-bottom: $spacer;
padding-bottom: $spacer * 1.5;
width: 100%;
text-align: center;
margin-top: $spacer / 2;
margin-bottom: $spacer;
padding-bottom: $spacer * 1.5;
small {
color: darken($alert-info, 60%);
margin-top: -($spacer / 2);
display: block;
}
small {
color: darken($alert-info, 60%);
margin-top: -($spacer / 2);
display: block;
}
}
.web3Row {
min-height: 77px;
display: flex;
align-items: center;
justify-content: center;
min-height: 77px;
display: flex;
align-items: center;
justify-content: center;
&:empty {
display: none;
}
&:empty {
display: none;
}
}
.message {
font-size: $font-size-small;
position: relative;
font-size: $font-size-small;
position: relative;
&::after {
overflow: hidden;
display: inline-block;
vertical-align: bottom;
animation: ellipsis steps(4, end) 1s infinite;
content: '\2026'; // ascii code for the ellipsis character
width: 0;
position: absolute;
left: 100%;
bottom: 0;
}
&::after {
overflow: hidden;
display: inline-block;
vertical-align: bottom;
animation: ellipsis steps(4, end) 1s infinite;
content: '\2026'; // ascii code for the ellipsis character
width: 0;
position: absolute;
left: 100%;
bottom: 0;
}
}
.success {
composes: message;
color: green;
composes: message;
color: green;
&::after {
display: none;
}
&::after {
display: none;
}
}
@keyframes ellipsis {
to {
width: .75rem;
}
to {
width: 0.75rem;
}
}

View File

@ -1,5 +1,50 @@
import Web3 from 'web3'
export class Logger {
static dispatch(verb: any, ...args: any) {
// eslint-disable-next-line no-console
console[verb](...args)
}
static log(...args: any) {
Logger.dispatch('log', ...args)
}
static debug(...args: any) {
Logger.dispatch('debug', ...args)
}
static error(...args: any) {
Logger.dispatch('error', ...args)
}
}
export const getNetworkName = (netId: number) => {
let networkName
switch (netId) {
case 1:
networkName = 'Main'
break
case 2:
networkName = 'Morden'
break
case 3:
networkName = 'Ropsten'
break
case 4:
networkName = 'Rinkeby'
break
case 42:
networkName = 'Kovan'
break
default:
networkName = 'Private'
}
return networkName
}
export const getWeb3 = async () => {
let web3
@ -30,77 +75,34 @@ export const getWeb3 = async () => {
}
}
export const getAccounts = async web3 => {
export const getAccounts = async (web3: Web3) => {
const ethAccounts = await web3.eth.getAccounts()
return ethAccounts
}
export const getNetwork = async web3 => {
export const getNetwork = async (web3: Web3) => {
const netId = await web3.eth.net.getId()
const networkName = getNetworkName(netId)
return { netId, networkName }
}
export const getNetworkName = netId => {
let networkName
switch (netId) {
case 1:
networkName = 'Main'
break
case 2:
networkName = 'Morden'
break
case 3:
networkName = 'Ropsten'
break
case 4:
networkName = 'Rinkeby'
break
case 42:
networkName = 'Kovan'
break
default:
networkName = 'Private'
}
return networkName
}
export const getFiat = async amount => {
export const getFiat = async (amount: number) => {
const url = 'https://api.coinmarketcap.com/v1/ticker/ethereum/?convert=EUR'
try {
const response = await fetch(url)
if (!response.ok) Logger.error(response.statusText)
const data = await response.json()
/* eslint-disable @typescript-eslint/camelcase */
const { price_usd, price_eur } = data[0]
const dollar = (amount * price_usd).toFixed(2)
const euro = (amount * price_eur).toFixed(2)
/* eslint-enable @typescript-eslint/camelcase */
return { dollar, euro }
} catch (error) {
Logger.error(error)
}
}
export class Logger {
static dispatch(verb, ...args) {
// eslint-disable-next-line no-console
console[verb](...args)
}
static log(...args) {
Logger.dispatch('log', ...args)
}
static debug(...args) {
Logger.dispatch('debug', ...args)
}
static error(...args) {
Logger.dispatch('error', ...args)
}
}

View File

@ -1,86 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { StaticQuery, graphql } from 'gatsby'
import remark from 'remark'
import remarkReact from 'remark-react'
import styles from './Changelog.module.scss'
const queryGithub = graphql`
query GitHubReposInfo {
github {
viewer {
repositories(first: 100, privacy: PUBLIC, isFork: false) {
edges {
node {
name
url
owner {
login
}
object(expression: "master:CHANGELOG.md") {
id
... on GitHub_Blob {
text
}
}
}
}
}
}
}
}
`
const Changelog = ({ repo }) => (
<StaticQuery
query={queryGithub}
render={data => {
const repositoriesGitHub = data.github.viewer.repositories.edges
let repoFilteredArray = repositoriesGitHub
.map(({ node }) => {
if (node.name === repo) return node
})
.filter(n => n)
const repoMatch = repoFilteredArray[0]
const { object, url, owner } = repoMatch
if (repoMatch === undefined || object === undefined) return null
const changelogHtml =
object &&
remark()
.use(remarkReact)
.processSync(object.text).contents
const filePathUrl = `${url}/tree/master/CHANGELOG.md`
const filePathDisplay = `${owner.login}/${repo}:CHANGELOG.md`
return (
<div className={styles.changelog}>
<h2 className={styles.changelogTitle} id="changelog">
Changelog
</h2>
<div className={styles.changelogContent}>
{changelogHtml}
<p className={styles.changelogSource}>
<em>
sourced from{' '}
<a href={filePathUrl}>
<code>{filePathDisplay}</code>
</a>
</em>
</p>
</div>
</div>
)
}}
/>
)
Changelog.propTypes = {
repo: PropTypes.string.isRequired
}
export default Changelog

View File

@ -1,69 +1,69 @@
@import 'variables';
.changelogTitle {
margin-top: $spacer * 3;
margin-bottom: 0;
margin-top: $spacer * 3;
margin-bottom: 0;
}
.changelogContent {
padding-top: $spacer * 2;
padding-left: $spacer / 2;
margin-left: $spacer / 2;
border-left: 1px solid $brand-grey-dimmed;
padding-top: $spacer * 2;
padding-left: $spacer / 2;
margin-left: $spacer / 2;
border-left: 1px solid $brand-grey-dimmed;
h2 {
position: relative;
h2 {
position: relative;
&::before {
content: '';
width: .4rem;
height: .4rem;
border-radius: 50%;
display: inline-block;
background: $color-headings;
position: absolute;
left: -($spacer / 1.5);
top: $font-size-large / 3;
}
&::before {
content: '';
width: 0.4rem;
height: 0.4rem;
border-radius: 50%;
display: inline-block;
background: $color-headings;
position: absolute;
left: -($spacer / 1.5);
top: $font-size-large / 3;
}
}
h2,
h3 {
font-size: $font-size-large;
background: none;
padding: 0;
margin-left: 0;
margin-top: $spacer / 8;
margin-bottom: $spacer / $line-height;
}
h2,
h3 {
font-size: $font-size-large;
background: none;
padding: 0;
margin-left: 0;
margin-top: $spacer / 8;
margin-bottom: $spacer / $line-height;
}
ul {
font-size: $font-size-small;
margin-left: $spacer / 8;
}
ul {
font-size: $font-size-small;
margin-left: $spacer / 8;
}
}
.changelogSource {
font-size: $font-size-mini;
font-family: $font-family-base;
font-weight: $font-weight-base;
padding-top: $spacer / 2;
padding-bottom: $spacer / 2;
font-size: $font-size-mini;
font-family: $font-family-base;
font-weight: $font-weight-base;
padding-top: $spacer / 2;
padding-bottom: $spacer / 2;
&,
a {
color: $brand-grey-light;
&,
a {
color: $brand-grey-light;
}
a {
margin-left: $spacer / 8;
code {
font-size: ($font-size-mini * 0.9);
}
a {
margin-left: $spacer / 8;
code {
font-size: ($font-size-mini * .9);
}
&:hover {
color: $link-color;
}
&:hover {
color: $link-color;
}
}
}

View File

@ -0,0 +1,75 @@
import React from 'react'
import { graphql, useStaticQuery } from 'gatsby'
import remark from 'remark'
import remarkReact from 'remark-react'
import styles from './Changelog.module.scss'
const queryGithub = graphql`
query GitHubReposInfo {
github {
viewer {
repositories(first: 100, privacy: PUBLIC, isFork: false) {
edges {
node {
name
url
owner {
login
}
object(expression: "master:CHANGELOG.md") {
id
... on GitHub_Blob {
text
}
}
}
}
}
}
}
}
`
export default function Changelog({ repo }: { repo: string }) {
const data = useStaticQuery(queryGithub)
const repositoriesGitHub = data.github.viewer.repositories.edges
const repoFilteredArray = repositoriesGitHub
.map(({ node }: { node: any }) => {
if (node.name === repo) return node
})
.filter((n: any) => n)
const repoMatch = repoFilteredArray[0]
const { object, url, owner } = repoMatch
if (repoMatch === undefined || object === undefined) return null
const changelogHtml =
object &&
remark()
.use(remarkReact)
.processSync(object.text).contents
const filePathUrl = `${url}/tree/master/CHANGELOG.md`
const filePathDisplay = `${owner.login}/${repo}:CHANGELOG.md`
return (
<div className={styles.changelog}>
<h2 className={styles.changelogTitle} id="changelog">
Changelog
</h2>
<div className={styles.changelogContent}>
{changelogHtml}
<p className={styles.changelogSource}>
<em>
sourced from{' '}
<a href={filePathUrl}>
<code>{filePathDisplay}</code>
</a>
</em>
</p>
</div>
</div>
)
}

View File

@ -1,13 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './Container.module.scss'
const Container = ({ children }) => (
<section className={styles.container}>{children}</section>
)
Container.propTypes = {
children: PropTypes.any.isRequired
}
export default Container

View File

@ -1,5 +1,5 @@
.container {
max-width: 35rem;
margin-left: auto;
margin-right: auto;
max-width: 35rem;
margin-left: auto;
margin-right: auto;
}

View File

@ -0,0 +1,10 @@
import React, { ReactElement } from 'react'
import styles from './Container.module.scss'
export default function Container({
children
}: {
children: any
}): ReactElement {
return <section className={styles.container}>{children}</section>
}

View File

@ -1,38 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import ExifMap from './ExifMap'
import styles from './Exif.module.scss'
export default class Exif extends PureComponent {
static propTypes = {
exif: PropTypes.object
}
render() {
const {
iso,
model,
fstop,
shutterspeed,
focalLength,
exposure,
gps
} = this.props.exif
return (
<>
<aside className={styles.exif}>
<div className={styles.data}>
{model && <span title="Camera model">{model}</span>}
{fstop && <span title="Aperture">{fstop}</span>}
{shutterspeed && <span title="Shutter speed">{shutterspeed}</span>}
{exposure && <span title="Exposure">{exposure}</span>}
{iso && <span title="ISO">{iso}</span>}
{focalLength && <span title="Focal length">{focalLength}</span>}
</div>
<div className={styles.map}>{gps && <ExifMap gps={gps} />}</div>
</aside>
</>
)
}
}

View File

@ -2,57 +2,57 @@
@import 'mixins';
.exif {
margin-top: -($spacer * 1.5);
margin-bottom: $spacer * 2;
margin-top: -($spacer * 1.5);
margin-bottom: $spacer * 2;
}
.data {
@include breakoutviewport;
@include breakoutviewport;
font-size: $font-size-mini;
color: $brand-grey-light;
display: flex;
flex-wrap: wrap;
justify-content: center;
text-align: center;
margin-bottom: -3px;
font-size: $font-size-mini;
color: $brand-grey-light;
display: flex;
flex-wrap: wrap;
justify-content: center;
text-align: center;
margin-bottom: -3px;
span {
display: block;
flex: 1 1 20%;
white-space: nowrap;
padding: $spacer / 1.5;
border-bottom: 1px solid $brand-grey-dimmed;
&:first-child {
flex-basis: 100%;
}
}
@media (min-width: $screen-sm) {
margin-bottom: 0;
span {
display: block;
flex: 1 1 20%;
white-space: nowrap;
padding: $spacer / 1.5;
border-bottom: 1px solid $brand-grey-dimmed;
border-left: 1px solid $brand-grey-dimmed;
border-bottom: 0;
padding: $spacer;
&:first-child {
flex-basis: 100%;
}
}
@media (min-width: $screen-sm) {
margin-bottom: 0;
span {
border-left: 1px solid $brand-grey-dimmed;
border-bottom: 0;
padding: $spacer;
&,
&:first-child {
flex: 1 1 auto;
}
&:first-child {
border-left: 0;
}
}
&,
&:first-child {
flex: 1 1 auto;
}
&:first-child {
border-left: 0;
}
}
}
}
.map {
@include breakoutviewport;
@include media-frame;
@include breakoutviewport;
@include media-frame;
overflow: hidden;
height: 160px;
overflow: hidden;
height: 160px;
}

View File

@ -0,0 +1,34 @@
import React from 'react'
import ExifMap from './ExifMap'
import styles from './Exif.module.scss'
interface ExifProps {
iso: string
model: string
fstop: string
shutterspeed: string
focalLength: string
exposure: string
gps: {
latitude: string
longitude: string
}
}
export default function Exif({ exif }: { exif: ExifProps }) {
const { iso, model, fstop, shutterspeed, focalLength, exposure, gps } = exif
return (
<aside className={styles.exif}>
<div className={styles.data}>
{model && <span title="Camera model">{model}</span>}
{fstop && <span title="Aperture">{fstop}</span>}
{shutterspeed && <span title="Shutter speed">{shutterspeed}</span>}
{exposure && <span title="Exposure">{exposure}</span>}
{iso && <span title="ISO">{iso}</span>}
{focalLength && <span title="Focal length">{focalLength}</span>}
</div>
<div className={styles.map}>{gps && <ExifMap gps={gps} />}</div>
</aside>
)
}

View File

@ -1,5 +1,4 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import React, { useState } from 'react'
import Map from 'pigeon-maps'
import Marker from 'pigeon-marker'
@ -9,7 +8,11 @@ const MAPBOX_ACCESS_TOKEN =
const retina =
typeof window !== 'undefined' && window.devicePixelRatio >= 2 ? '@2x' : ''
const mapbox = (mapboxId, accessToken) => (x, y, z) =>
const mapbox = (mapboxId: string, accessToken: string) => (
x: string,
y: string,
z: string
) =>
`https://api.mapbox.com/styles/v1/mapbox/${mapboxId}/tiles/256/${z}/${x}/${y}${retina}?access_token=${accessToken}`
const providers = {
@ -28,38 +31,30 @@ const providers = {
dark: mapbox('dark-v9', MAPBOX_ACCESS_TOKEN)
}
export default class ExifMap extends PureComponent {
state = { zoom: 12 }
export default function ExifMap({
gps
}: {
gps: { latitude: string; longitude: string }
}) {
const [zoom, setZoom] = useState(12)
static propTypes = {
gps: PropTypes.object
const zoomIn = () => {
setZoom(Math.min(zoom + 4, 20))
}
zoomIn = () => {
this.setState({
zoom: Math.min(this.state.zoom + 4, 20)
})
}
const { latitude, longitude } = gps
render() {
const { latitude, longitude } = this.props.gps
return (
<Map
center={[latitude, longitude]}
zoom={this.state.zoom}
height={160}
attribution={false}
provider={providers['light']}
metaWheelZoom={true}
metaWheelZoomWarning={'META+wheel to zoom'}
>
<Marker
anchor={[latitude, longitude]}
payload={1}
onClick={this.zoomIn}
/>
</Map>
)
}
return (
<Map
center={[latitude, longitude]}
zoom={zoom}
height={160}
attribution={false}
provider={providers['light']}
metaWheelZoom={true}
metaWheelZoomWarning={'META+wheel to zoom'}
>
<Marker anchor={[latitude, longitude]} payload={1} onClick={zoomIn} />
</Map>
)
}

View File

@ -1,19 +0,0 @@
import React from 'react'
import styles from './Hamburger.module.scss'
const Hamburger = props => (
<button
type="button"
title="Menu"
className={styles.hamburgerButton}
{...props}
>
<span className={styles.hamburger}>
<span className={styles.hamburgerLine} />
<span className={styles.hamburgerLine} />
<span className={styles.hamburgerLine} />
</span>
</button>
)
export default Hamburger

View File

@ -2,77 +2,77 @@
@import 'mixins';
.hamburgerLine {
@include transition;
@include transition;
display: block;
position: absolute;
height: 3px;
width: 100%;
background: $text-color-light;
border-radius: 20px;
opacity: 1;
left: 0;
transform: rotate(0deg);
display: block;
position: absolute;
height: 3px;
width: 100%;
background: $text-color-light;
border-radius: 20px;
opacity: 1;
left: 0;
transform: rotate(0deg);
&:nth-child(1) {
top: 0;
transform-origin: left center;
}
&:nth-child(2) {
top: 5px;
transform-origin: left center;
}
&:nth-child(3) {
top: 10px;
transform-origin: left center;
}
// open state
:global(.has-menu-open) & {
&:nth-child(1) {
top: 0;
transform-origin: left center;
transform: rotate(45deg);
top: -1px;
}
&:nth-child(2) {
top: 5px;
transform-origin: left center;
width: 0%;
opacity: 0;
}
&:nth-child(3) {
top: 10px;
transform-origin: left center;
}
// open state
:global(.has-menu-open) & {
&:nth-child(1) {
transform: rotate(45deg);
top: -1px;
}
&:nth-child(2) {
width: 0%;
opacity: 0;
}
&:nth-child(3) {
transform: rotate(-45deg);
top: 12px;
}
transform: rotate(-45deg);
top: 12px;
}
}
}
.hamburgerButton {
padding: .65rem .85rem;
text-align: center;
line-height: 1;
vertical-align: middle;
display: inline-block;
margin: 0;
margin-right: -($spacer / 2);
padding: 0.65rem 0.85rem;
text-align: center;
line-height: 1;
vertical-align: middle;
display: inline-block;
margin: 0;
margin-right: -($spacer / 2);
&:hover,
&:focus {
outline: 0;
&:hover,
&:focus {
outline: 0;
.hamburgerLine {
background: $brand-cyan;
}
.hamburgerLine {
background: $brand-cyan;
}
}
}
.hamburger {
width: 18px;
height: 18px;
display: block;
position: relative;
transform: rotate(0deg);
cursor: pointer;
margin-top: 6px;
width: 18px;
height: 18px;
display: block;
position: relative;
transform: rotate(0deg);
cursor: pointer;
margin-top: 6px;
}

View File

@ -0,0 +1,19 @@
import React from 'react'
import styles from './Hamburger.module.scss'
export default function Hamburger({ onClick }: { onClick(): void }) {
return (
<button
type="button"
title="Menu"
className={styles.hamburgerButton}
onClick={onClick}
>
<span className={styles.hamburger}>
<span className={styles.hamburgerLine} />
<span className={styles.hamburgerLine} />
<span className={styles.hamburgerLine} />
</span>
</button>
)
}

View File

@ -1,43 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { graphql } from 'gatsby'
import Img from 'gatsby-image'
import styles from './Image.module.scss'
export default class Image extends PureComponent {
static propTypes = {
fluid: PropTypes.object,
fixed: PropTypes.object,
alt: PropTypes.string.isRequired
}
render() {
const { fluid, fixed, alt } = this.props
return (
<Img
className={styles.imageWrap}
backgroundColor="#dfe8ef"
fluid={fluid ? fluid : null}
fixed={fixed ? fixed : null}
alt={alt}
/>
)
}
}
export const imageSizeDefault = graphql`
fragment ImageFluid on ImageSharp {
fluid(maxWidth: 940, quality: 85) {
...GatsbyImageSharpFluid_withWebp_noBase64
}
}
`
export const imageSizeThumb = graphql`
fragment ImageFluidThumb on ImageSharp {
fluid(maxWidth: 200, maxHeight: 85, quality: 85, cropFocus: CENTER) {
...GatsbyImageSharpFluid_withWebp_noBase64
}
}
`

View File

@ -1,20 +1,20 @@
@import 'mixins';
.imageWrap {
@include media-frame;
@include media-frame;
margin-left: auto;
margin-right: auto;
margin-bottom: $spacer;
display: block;
margin-left: auto;
margin-right: auto;
margin-bottom: $spacer;
display: block;
@media (min-width: 940px) {
max-width: 940px;
border-radius: .25rem;
overflow: hidden;
}
@media (min-width: 940px) {
max-width: 940px;
border-radius: 0.25rem;
overflow: hidden;
}
a:hover & {
border-color: $link-color !important;
}
a:hover & {
border-color: $link-color !important;
}
}

View File

@ -0,0 +1,40 @@
import React from 'react'
import { graphql } from 'gatsby'
import Img, { FixedObject, FluidObject } from 'gatsby-image'
import styles from './Image.module.scss'
export default function Image({
fluid,
fixed,
alt
}: {
fluid?: FluidObject
fixed?: FixedObject
alt: string
}) {
return (
<Img
className={styles.imageWrap}
backgroundColor="#dfe8ef"
fluid={fluid}
fixed={fixed}
alt={alt}
/>
)
}
export const imageSizeDefault = graphql`
fragment ImageFluid on ImageSharp {
fluid(maxWidth: 940, quality: 85) {
...GatsbyImageSharpFluid_withWebp_noBase64
}
}
`
export const imageSizeThumb = graphql`
fragment ImageFluidThumb on ImageSharp {
fluid(maxWidth: 200, maxHeight: 85, quality: 85, cropFocus: CENTER) {
...GatsbyImageSharpFluid_withWebp_noBase64
}
}
`

View File

@ -1,6 +0,0 @@
import React from 'react'
import styles from './Input.module.scss'
const Input = props => <input className={styles.input} {...props} />
export default Input

View File

@ -1,32 +1,32 @@
@import 'variables';
.input {
display: block;
width: 100%;
padding: $padding-base-vertical $padding-base-horizontal;
font-size: $input-font-size;
font-weight: $input-font-weight;
line-height: $line-height;
color: $input-color;
background-color: $input-bg;
background-image: none; // Reset unusual Firefox-on-Android default style
border: 0;
border-radius: $input-border-radius;
box-shadow: none;
transition: all ease-in-out .15s;
appearance: none;
display: block;
width: 100%;
padding: $padding-base-vertical $padding-base-horizontal;
font-size: $input-font-size;
font-weight: $input-font-weight;
line-height: $line-height;
color: $input-color;
background-color: $input-bg;
background-image: none; // Reset unusual Firefox-on-Android default style
border: 0;
border-radius: $input-border-radius;
box-shadow: none;
transition: all ease-in-out 0.15s;
appearance: none;
&:hover {
background: lighten($input-bg, 30%);
}
&:hover {
background: lighten($input-bg, 30%);
}
&:focus {
background-color: $input-bg-focus;
border-color: $input-border-focus;
outline: 0;
}
&:focus {
background-color: $input-bg-focus;
border-color: $input-border-focus;
outline: 0;
}
&[disabled] {
color: $brand-grey-dimmed;
}
&[disabled] {
color: $brand-grey-dimmed;
}
}

View File

@ -0,0 +1,6 @@
import React from 'react'
import styles from './Input.module.scss'
export default function Input(props: any) {
return <input className={styles.input} {...props} />
}

View File

@ -1,40 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import ReactModal from 'react-modal'
import styles from './Modal.module.scss'
ReactModal.setAppElement('#___gatsby')
export default class Modal extends PureComponent {
static propTypes = {
title: PropTypes.string,
isOpen: PropTypes.bool,
handleCloseModal: PropTypes.func.isRequired,
children: PropTypes.node.isRequired
}
render() {
if (!this.props.isOpen) {
return null
}
const { children, title, handleCloseModal } = this.props
return (
<ReactModal
overlayClassName={styles.modal}
className={styles.modal__content}
htmlOpenClassName={styles.isModalOpen}
shouldReturnFocusAfterClose={false}
{...this.props}
>
{title && <h1 className={styles.modal__title}>{title}</h1>}
{children}
<button className={styles.modal__close} onClick={handleCloseModal}>
&times;
</button>
</ReactModal>
)
}
}

View File

@ -1,111 +1,111 @@
@import 'variables';
.modal {
position: fixed;
overflow: auto;
-webkit-overflow-scrolling: touch;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9;
background: rgba($body-background-color, .95);
backdrop-filter: blur(5px);
animation: fadein .3s;
padding: $spacer;
position: fixed;
overflow: auto;
-webkit-overflow-scrolling: touch;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9;
background: rgba($body-background-color, 0.95);
backdrop-filter: blur(5px);
animation: fadein 0.3s;
padding: $spacer;
@media (min-width: $screen-sm) {
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 6vh;
}
@media (min-width: $screen-sm) {
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 6vh;
}
}
.modal__content {
outline: 0;
background: transparent;
position: relative;
border-radius: $border-radius;
border: 1px solid rgba($brand-grey-light, .4);
box-shadow: 0 5px 30px rgba($brand-grey-light, .2);
padding: 0 $spacer / 2 $spacer / 2;
max-width: 100%;
outline: 0;
background: transparent;
position: relative;
border-radius: $border-radius;
border: 1px solid rgba($brand-grey-light, 0.4);
box-shadow: 0 5px 30px rgba($brand-grey-light, 0.2);
padding: 0 $spacer / 2 $spacer / 2;
max-width: 100%;
@media (min-width: $screen-md) {
max-width: $screen-sm;
padding: 0 $spacer $spacer;
}
@media (min-width: $screen-md) {
max-width: $screen-sm;
padding: 0 $spacer $spacer;
}
}
.modal__close {
display: block;
cursor: pointer;
background: transparent;
border: 0;
appearance: none;
line-height: 1;
font-size: $font-size-h2;
padding: 4px;
position: absolute;
top: 0;
right: ($spacer/4);
color: $brand-grey-light;
font-weight: 500;
outline: 0;
display: block;
cursor: pointer;
background: transparent;
border: 0;
appearance: none;
line-height: 1;
font-size: $font-size-h2;
padding: 4px;
position: absolute;
top: 0;
right: ($spacer/4);
color: $brand-grey-light;
font-weight: 500;
outline: 0;
&:hover,
&:focus {
color: $brand-grey;
}
&:hover,
&:focus {
color: $brand-grey;
}
}
.isModalOpen {
// Prevent background scrolling when modal is open
overflow: hidden;
// Prevent background scrolling when modal is open
overflow: hidden;
// more cross-browser backdrop-filter
// body > div:first-child {
// transition: filter .85s ease-out;
// filter: blur(5px);
// }
// more cross-browser backdrop-filter
// body > div:first-child {
// transition: filter .85s ease-out;
// filter: blur(5px);
// }
}
.modal__title {
font-size: $font-size-h4;
margin-top: $spacer / 2;
margin-bottom: $spacer / 2;
margin-left: -($spacer / 2);
margin-right: -($spacer / 2);
border-bottom: 1px solid rgba($brand-grey-light, .4);
padding: 0 $spacer;
padding-bottom: ($spacer/2);
font-size: $font-size-h4;
margin-top: $spacer / 2;
margin-bottom: $spacer / 2;
margin-left: -($spacer / 2);
margin-right: -($spacer / 2);
border-bottom: 1px solid rgba($brand-grey-light, 0.4);
padding: 0 $spacer;
padding-bottom: ($spacer/2);
@media (min-width: $screen-md) {
margin-left: -($spacer);
margin-right: -($spacer);
}
@media (min-width: $screen-md) {
margin-left: -($spacer);
margin-right: -($spacer);
}
}
//
// Overlay/content animations
//
@keyframes fadein {
0% {
opacity: 0;
}
0% {
opacity: 0;
}
100% {
opacity: 1;
}
100% {
opacity: 1;
}
}
@keyframes fadeout {
0% {
opacity: 1;
}
0% {
opacity: 1;
}
100% {
opacity: 0;
}
100% {
opacity: 0;
}
}

View File

@ -0,0 +1,38 @@
import React, { ReactChildren } from 'react'
import ReactModal from 'react-modal'
import styles from './Modal.module.scss'
ReactModal.setAppElement('#___gatsby')
export default function Modal({
title,
isOpen,
handleCloseModal,
children,
...props
}: {
title?: string
isOpen?: boolean
handleCloseModal: any
children: ReactChildren
}) {
if (!isOpen) return null
return (
<ReactModal
overlayClassName={styles.modal}
className={styles.modal__content}
htmlOpenClassName={styles.isModalOpen}
shouldReturnFocusAfterClose={false}
isOpen={isOpen}
{...props}
>
{title && <h1 className={styles.modal__title}>{title}</h1>}
{children}
<button className={styles.modal__close} onClick={handleCloseModal}>
&times;
</button>
</ReactModal>
)
}

View File

@ -1,44 +0,0 @@
import React 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'
import styles from './Qr.module.scss'
const onCopySuccess = e => {
e.trigger.classList.add(styles.copied)
}
const Qr = ({ address, title }) => (
<>
{title && <h4>{title}</h4>}
<QRCode
bgColor="transparent"
fgColor="#6b7f88"
level="Q"
style={{ width: 120 }}
value={address}
className={styles.qr}
/>
<pre className={styles.code}>
<code>{address}</code>
<Clipboard
data-clipboard-text={address}
button-title="Copy to clipboard"
onSuccess={e => onCopySuccess(e)}
className={styles.button}
>
<IconClipboard />
</Clipboard>
</pre>
</>
)
Qr.propTypes = {
address: PropTypes.string.isRequired,
title: PropTypes.string
}
export default Qr

View File

@ -1,54 +1,54 @@
@import 'variables';
.qr {
margin-bottom: $spacer / 2;
margin-bottom: $spacer / 2;
}
.code {
margin: 0;
position: relative;
padding: 0;
padding-right: 2rem;
margin: 0;
position: relative;
padding: 0;
padding-right: 2rem;
code {
padding: $spacer / 2;
font-size: .65rem;
text-align: center;
}
code {
padding: $spacer / 2;
font-size: 0.65rem;
text-align: center;
}
}
.button {
margin: 0;
position: absolute;
right: 0;
top: 0;
bottom: 0;
border: 0;
box-shadow: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
background: rgba($brand-grey, .3);
padding: $spacer / 3;
margin: 0;
position: absolute;
right: 0;
top: 0;
bottom: 0;
border: 0;
box-shadow: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
background: rgba($brand-grey, 0.3);
padding: $spacer / 3;
svg {
width: 1rem;
height: 1rem;
fill: $brand-grey-light;
transition: 0.15s ease-out;
}
&:hover {
svg {
width: 1rem;
height: 1rem;
fill: $brand-grey-light;
transition: .15s ease-out;
}
&:hover {
svg {
fill: $brand-grey-dimmed;
}
fill: $brand-grey-dimmed;
}
}
}
.copied {
background: green;
background: green;
// stylelint-disable-next-line no-descending-specificity
svg {
fill: $brand-grey-dimmed;
}
// stylelint-disable-next-line no-descending-specificity
svg {
fill: $brand-grey-dimmed;
}
}

View File

@ -0,0 +1,44 @@
import React from 'react'
import { QRCode } from 'react-qr-svg'
import Clipboard from 'react-clipboard.js'
import { ReactComponent as IconClipboard } from '../../images/clipboard.svg'
import styles from './Qr.module.scss'
const onCopySuccess = (e: any) => {
e.trigger.classList.add(styles.copied)
}
export default function Qr({
address,
title
}: {
address: string
title?: string
}) {
return (
<>
{title && <h4>{title}</h4>}
<QRCode
bgColor="transparent"
fgColor="#6b7f88"
level="Q"
style={{ width: 120 }}
value={address}
className={styles.qr}
/>
<pre className={styles.code}>
<code>{address}</code>
<Clipboard
data-clipboard-text={address}
button-title="Copy to clipboard"
onSuccess={e => onCopySuccess(e)}
className={styles.button}
>
<IconClipboard />
</Clipboard>
</pre>
</>
)
}

View File

@ -1,6 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import { StaticQuery, graphql } from 'gatsby'
import { graphql, useStaticQuery } from 'gatsby'
import Helmet from 'react-helmet'
const query = graphql`
@ -28,13 +27,13 @@ const query = graphql`
`
const createSchemaOrg = (
blogURL,
title,
siteMeta,
postSEO,
postURL,
image,
description
blogURL: string,
title: string,
siteMeta: any,
postSEO: boolean,
postURL: string,
image: string,
description: string
) => {
const schemaOrgJSONLD = [
{
@ -89,6 +88,14 @@ const MetaTags = ({
postSEO,
title,
siteMeta
}: {
description: string
image: string
url: string
schema: string
postSEO: boolean
title: string
siteMeta: any
}) => (
<Helmet
defaultTitle={`${siteMeta.siteTitle} ¦ ${siteMeta.siteDescription}`}
@ -130,76 +137,63 @@ const MetaTags = ({
</Helmet>
)
MetaTags.propTypes = {
description: PropTypes.string,
image: PropTypes.string,
url: PropTypes.string,
schema: PropTypes.string,
postSEO: PropTypes.bool,
title: PropTypes.string,
siteMeta: PropTypes.object
export default function SEO({
post,
slug,
postSEO
}: {
post?: any
slug?: string
postSEO?: boolean
}) {
const data = useStaticQuery(query)
const siteMeta = data.site.siteMetadata
const logo = data.logo.edges[0].node.relativePath
let title
let description
let image
let postURL
if (postSEO) {
const postMeta = post.frontmatter
title = `${postMeta.title} ¦ ${siteMeta.siteTitle}`
description = postMeta.description ? postMeta.description : post.excerpt
image = postMeta.image
? postMeta.image.childImageSharp.fluid.src
: `/${logo}`
postURL = `${siteMeta.siteUrl}${slug}`
} else {
title = `${siteMeta.siteTitle} ¦ ${siteMeta.siteDescription}`
description = siteMeta.siteDescription
image = `/${logo}`
}
image = `${siteMeta.siteUrl}${image}`
const blogURL = siteMeta.siteUrl
const url = postSEO ? postURL : blogURL
let schema = createSchemaOrg(
blogURL,
title,
siteMeta,
postSEO,
postURL,
image,
description
)
schema = JSON.stringify(schema)
return (
<MetaTags
description={description}
image={image}
url={url}
schema={schema}
postSEO={postSEO}
title={title}
siteMeta={siteMeta}
/>
)
}
const SEO = ({ post, slug, postSEO }) => (
<StaticQuery
query={query}
render={data => {
const siteMeta = data.site.siteMetadata
const logo = data.logo.edges[0].node.relativePath
let title
let description
let image
let postURL
if (postSEO) {
const postMeta = post.frontmatter
title = `${postMeta.title} ¦ ${siteMeta.siteTitle}`
description = postMeta.description ? postMeta.description : post.excerpt
image = postMeta.image
? postMeta.image.childImageSharp.fluid.src
: `/${logo}`
postURL = `${siteMeta.siteUrl}${slug}`
} else {
title = `${siteMeta.siteTitle} ¦ ${siteMeta.siteDescription}`
description = siteMeta.siteDescription
image = `/${logo}`
}
image = `${siteMeta.siteUrl}${image}`
const blogURL = siteMeta.siteUrl
const url = postSEO ? postURL : blogURL
let schema = createSchemaOrg(
blogURL,
title,
siteMeta,
postSEO,
postURL,
image,
description
)
schema = JSON.stringify(schema)
return (
<MetaTags
description={description}
image={image}
url={url}
schema={schema}
postSEO={postSEO}
title={title}
siteMeta={siteMeta}
/>
)
}}
/>
)
SEO.propTypes = {
post: PropTypes.object,
slug: PropTypes.string,
postSEO: PropTypes.bool
}
export default SEO

Some files were not shown because too many files have changed in this diff Show More