mirror of
https://github.com/kremalicious/blog.git
synced 2024-12-22 09:13:35 +01:00
migrate to TypeScript
This commit is contained in:
parent
6d502cf313
commit
0946b30b67
@ -1,5 +0,0 @@
|
|||||||
version: '2'
|
|
||||||
checks:
|
|
||||||
method-lines:
|
|
||||||
config:
|
|
||||||
threshold: 55 # Gatsby's StaticQuery makes render functions pretty long
|
|
@ -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
|
|
60
.eslintrc
60
.eslintrc
@ -1,34 +1,36 @@
|
|||||||
{
|
{
|
||||||
"parser": "babel-eslint",
|
"parser": "babel-eslint",
|
||||||
"extends": [
|
"extends": ["eslint:recommended", "prettier"],
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:jsx-a11y/recommended",
|
|
||||||
"plugin:prettier/recommended"
|
|
||||||
],
|
|
||||||
"plugins": ["react", "graphql", "prettier", "jsx-a11y"],
|
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"sourceType": "module",
|
"ecmaVersion": 2018,
|
||||||
"ecmaFeatures": {
|
"sourceType": "module"
|
||||||
"jsx": true,
|
},
|
||||||
"modules": true
|
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,3 +2,4 @@ node_modules/
|
|||||||
.cache/
|
.cache/
|
||||||
static/
|
static/
|
||||||
public/
|
public/
|
||||||
|
coverage/
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"semi": false,
|
"semi": false,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "none"
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 2
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,12 @@
|
|||||||
"extends": [
|
"extends": [
|
||||||
"stylelint-config-standard",
|
"stylelint-config-standard",
|
||||||
"stylelint-config-css-modules",
|
"stylelint-config-css-modules",
|
||||||
"./node_modules/prettier-stylelint/config.js"
|
"stylelint-prettier/recommended"
|
||||||
],
|
],
|
||||||
|
"plugins": ["stylelint-prettier"],
|
||||||
"syntax": "scss",
|
"syntax": "scss",
|
||||||
"rules": {
|
"rules": {
|
||||||
"indentation": 4,
|
"prettier/prettier": true,
|
||||||
"number-leading-zero": "never",
|
|
||||||
"at-rule-no-unknown": null
|
"at-rule-no-unknown": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
14
.travis.yml
14
.travis.yml
@ -19,15 +19,13 @@ before_install:
|
|||||||
before_script:
|
before_script:
|
||||||
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
||||||
- chmod +x ./cc-test-reporter
|
- chmod +x ./cc-test-reporter
|
||||||
- "./cc-test-reporter before-build"
|
- './cc-test-reporter before-build'
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- npm test
|
- npm test
|
||||||
|
- './cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT'
|
||||||
- travis_wait 60 npm run build
|
- travis_wait 60 npm run build
|
||||||
|
|
||||||
after_script:
|
|
||||||
- "./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT"
|
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- pip install --user awscli
|
- pip install --user awscli
|
||||||
- export PATH=$PATH:$HOME/.local/bin
|
- export PATH=$PATH:$HOME/.local/bin
|
||||||
@ -37,8 +35,8 @@ notifications:
|
|||||||
email: false
|
email: false
|
||||||
slack:
|
slack:
|
||||||
template:
|
template:
|
||||||
- "%{branch} *%{result}* build (<%{build_url}|#%{build_number}>) for <%{compare_url}|%{commit}>"
|
- '%{branch} *%{result}* build (<%{build_url}|#%{build_number}>) for <%{compare_url}|%{commit}>'
|
||||||
- "Execution time: *%{duration}*"
|
- 'Execution time: *%{duration}*'
|
||||||
- "Message: %{message}"
|
- 'Message: %{message}'
|
||||||
rooms:
|
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='
|
||||||
|
@ -1,88 +1,85 @@
|
|||||||
kbd {
|
kbd {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #444;
|
color: #444;
|
||||||
font-family: 'Lucida Grande', Lucida, Verdana, sans-serif;
|
font-family: 'Lucida Grande', Lucida, Verdana, sans-serif;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
text-shadow: 0 1px 0 #fff;
|
text-shadow: 0 1px 0 #fff;
|
||||||
display: inline;
|
display: inline;
|
||||||
padding: .3em .55em;
|
padding: 0.3em 0.55em;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
border: 1px solid #bbb;
|
border: 1px solid #bbb;
|
||||||
background-color: #f7f7f7;
|
background-color: #f7f7f7;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
rgba(0, 0, 0, .1),
|
rgba(0, 0, 0, 0.1),
|
||||||
rgba(0, 0, 0, 0)
|
rgba(0, 0, 0, 0)
|
||||||
);
|
);
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
box-shadow: 0 2px 0 #bbb, 0 3px 1px #999, 0 3px 0 #bbb, inset 0 1px 1px #fff,
|
box-shadow: 0 2px 0 #bbb, 0 3px 1px #999, 0 3px 0 #bbb, inset 0 1px 1px #fff,
|
||||||
inset 0 -1px 3px #ccc;
|
inset 0 -1px 3px #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
kbd.dark {
|
kbd.dark {
|
||||||
color: #eee;
|
color: #eee;
|
||||||
text-shadow: 0 -1px 0 #000;
|
text-shadow: 0 -1px 0 #000;
|
||||||
border-color: #000;
|
border-color: #000;
|
||||||
background-color: #4d4c4c;
|
background-color: #4d4c4c;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
rgba(0, 0, 0, .5),
|
rgba(0, 0, 0, 0.5),
|
||||||
rgba(0, 0, 0, 0) 80%,
|
rgba(0, 0, 0, 0) 80%,
|
||||||
rgba(0, 0, 0, 0)
|
rgba(0, 0, 0, 0)
|
||||||
);
|
);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
box-shadow: 0 2px 0 #000, 0 3px 1px #999, inset 0 1px 1px #aaa,
|
box-shadow: 0 2px 0 #000, 0 3px 1px #999, inset 0 1px 1px #aaa,
|
||||||
inset 0 -1px 3px #272727;
|
inset 0 -1px 3px #272727;
|
||||||
}
|
}
|
||||||
|
|
||||||
kbd.ios {
|
kbd.ios {
|
||||||
font-family: Helvetica, 'Helvetica Neue', Arial, sans-serif;
|
font-family: Helvetica, 'Helvetica Neue', Arial, sans-serif;
|
||||||
color: #000;
|
color: #000;
|
||||||
border-color: rgba(0, 0, 0, .6);
|
border-color: rgba(0, 0, 0, 0.6);
|
||||||
border-top-color: rgba(0, 0, 0, .4);
|
border-top-color: rgba(0, 0, 0, 0.4);
|
||||||
background-color: #b7b7bc;
|
background-color: #b7b7bc;
|
||||||
background-image: linear-gradient(to bottom, #efeff0, #b7b7bc);
|
background-image: linear-gradient(to bottom, #efeff0, #b7b7bc);
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .6), 0 2px 3px rgba(0, 0, 0, .1),
|
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;
|
inset 0 1px 0 #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
kbd.android {
|
kbd.android {
|
||||||
font-family: 'RobotoRegular', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'RobotoRegular', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
padding: .3em;
|
padding: 0.3em;
|
||||||
border: 1px solid rgba(0, 0, 0, .05);
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
background: #5e5e5e;
|
background: #5e5e5e;
|
||||||
box-shadow: 0 2px 2px rgba(0, 0, 0, .3), 0 1px 0 #444,
|
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.3), 0 1px 0 #444, inset 0 1px 0 #868686;
|
||||||
inset 0 1px 0 #868686;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kbd.android.dark {
|
kbd.android.dark {
|
||||||
background: #222;
|
background: #222;
|
||||||
box-shadow: 0 2px 2px rgba(0, 0, 0, .7), 0 1px 0 #444,
|
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.7), 0 1px 0 #444, inset 0 1px 0 #505050;
|
||||||
inset 0 1px 0 #505050;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kbd.android.color {
|
kbd.android.color {
|
||||||
background: #083c5b;
|
background: #083c5b;
|
||||||
box-shadow: 0 2px 2px rgba(0, 0, 0, .7), 0 1px 0 #444,
|
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.7), 0 1px 0 #444, inset 0 1px 0 #36647b;
|
||||||
inset 0 1px 0 #36647b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'RobotoRegular';
|
font-family: 'RobotoRegular';
|
||||||
src: url('/media/Roboto-Regular-webfont.eot');
|
src: url('/media/Roboto-Regular-webfont.eot');
|
||||||
src: url('/media/Roboto-Regular-webfont.eot?#iefix')
|
src: url('/media/Roboto-Regular-webfont.eot?#iefix')
|
||||||
format('embedded-opentype'),
|
format('embedded-opentype'),
|
||||||
url('/media/Roboto-Regular-webfont.woff') format('woff'),
|
url('/media/Roboto-Regular-webfont.woff') format('woff'),
|
||||||
url('/media/Roboto-Regular-webfont.ttf') format('truetype'),
|
url('/media/Roboto-Regular-webfont.ttf') format('truetype'),
|
||||||
url('/media/Roboto-Regular-webfont.svg#RobotoRegular') format('svg');
|
url('/media/Roboto-Regular-webfont.svg#RobotoRegular') format('svg');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
@ -212,6 +212,7 @@ module.exports = {
|
|||||||
'gatsby-plugin-catch-links',
|
'gatsby-plugin-catch-links',
|
||||||
'gatsby-redirect-from',
|
'gatsby-redirect-from',
|
||||||
'gatsby-plugin-meta-redirect',
|
'gatsby-plugin-meta-redirect',
|
||||||
'gatsby-plugin-offline'
|
'gatsby-plugin-offline',
|
||||||
|
'gatsby-plugin-typescript'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,7 @@ const path = require('path')
|
|||||||
const { createFilePath } = require('gatsby-source-filesystem')
|
const { createFilePath } = require('gatsby-source-filesystem')
|
||||||
const { repoContentPath } = require('../config')
|
const { repoContentPath } = require('../config')
|
||||||
|
|
||||||
// Create slug, date & github file link for posts from file path values
|
function createSlug(node, createNodeField, slugOriginal, parsedFilePath) {
|
||||||
exports.createMarkdownFields = (node, createNodeField, getNode) => {
|
|
||||||
const fileNode = getNode(node.parent)
|
|
||||||
const parsedFilePath = path.parse(fileNode.relativePath)
|
|
||||||
const slugOriginal = createFilePath({ node, getNode })
|
|
||||||
|
|
||||||
// slug
|
|
||||||
let slug
|
let slug
|
||||||
|
|
||||||
if (parsedFilePath.name === 'index') {
|
if (parsedFilePath.name === 'index') {
|
||||||
@ -22,8 +16,9 @@ exports.createMarkdownFields = (node, createNodeField, getNode) => {
|
|||||||
name: 'slug',
|
name: 'slug',
|
||||||
value: slug
|
value: slug
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// date
|
function createDate(node, createNodeField, slugOriginal) {
|
||||||
// grab date from file path
|
// grab date from file path
|
||||||
let date = new Date(slugOriginal.substring(1, 11)).toISOString() // 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',
|
name: 'date',
|
||||||
value: 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
|
// github file link
|
||||||
const type = fileNode.sourceInstanceName
|
const type = fileNode.sourceInstanceName
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const postsTemplate = path.resolve('src/templates/Posts.jsx')
|
const postsTemplate = path.resolve('src/templates/Posts.tsx')
|
||||||
|
|
||||||
const redirects = [
|
const redirects = [
|
||||||
{ f: '/feed', t: '/feed.xml' },
|
{ f: '/feed', t: '/feed.xml' },
|
||||||
@ -7,7 +7,7 @@ const redirects = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
exports.generatePostPages = (createPage, posts, numPages) => {
|
exports.generatePostPages = (createPage, posts, numPages) => {
|
||||||
const postTemplate = path.resolve('src/templates/Post.jsx')
|
const postTemplate = path.resolve('src/templates/Post.tsx')
|
||||||
|
|
||||||
// Create Post pages
|
// Create Post pages
|
||||||
posts.forEach(post => {
|
posts.forEach(post => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.jsx?$': '<rootDir>/jest/jest-preprocess.js'
|
'^.+\\.tsx?$': '<rootDir>/jest/jest-preprocess.js'
|
||||||
},
|
},
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'.+\\.(css|styl|less|sass|scss)$': 'identity-obj-proxy',
|
'.+\\.(css|styl|less|sass|scss)$': 'identity-obj-proxy',
|
||||||
@ -15,5 +15,6 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
testURL: 'http://localhost',
|
testURL: 'http://localhost',
|
||||||
setupFiles: ['<rootDir>/jest/loadershim.js'],
|
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/**/*']
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
const { createTransformer } = require('babel-jest')
|
||||||
|
|
||||||
const babelOptions = {
|
const babelOptions = {
|
||||||
presets: ['babel-preset-gatsby']
|
presets: ['babel-preset-gatsby', '@babel/preset-typescript']
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = require('babel-jest').createTransformer(babelOptions)
|
module.exports = createTransformer(babelOptions)
|
||||||
|
@ -1 +1 @@
|
|||||||
import '@testing-library/jest-dom/extend-expect'
|
require('@testing-library/jest-dom/extend-expect')
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { render } from '@testing-library/react'
|
import { render } from '@testing-library/react'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
|
||||||
const testRender = component => {
|
const testRender = (component: ReactElement) => {
|
||||||
it('renders without crashing', () => {
|
it('renders without crashing', () => {
|
||||||
const { container } = render(component)
|
const { container } = render(component)
|
||||||
|
|
90
package.json
90
package.json
@ -10,17 +10,15 @@
|
|||||||
"start": "gatsby develop",
|
"start": "gatsby develop",
|
||||||
"build": "gatsby build && npm run copy",
|
"build": "gatsby build && npm run copy",
|
||||||
"ssr": "npm run build && serve -s public/",
|
"ssr": "npm run build && serve -s public/",
|
||||||
"rename:scrypt": "sed -i -e 's|./build/Release/scrypt|scrypt|g' node_modules/scrypt/index.js",
|
|
||||||
"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": "npm run lint && jest --coverage",
|
||||||
"test:watch": "npm run lint && jest --coverage --watch",
|
"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",
|
"deploy": "./scripts/deploy.sh",
|
||||||
"new": "babel-node ./scripts/new.js"
|
"new": "babel-node ./scripts/new.js"
|
||||||
},
|
},
|
||||||
@ -31,41 +29,42 @@
|
|||||||
"dms2dec": "^1.1.0",
|
"dms2dec": "^1.1.0",
|
||||||
"fast-exif": "^1.0.1",
|
"fast-exif": "^1.0.1",
|
||||||
"fraction.js": "^4.0.12",
|
"fraction.js": "^4.0.12",
|
||||||
"gatsby": "^2.15.18",
|
"gatsby": "^2.15.28",
|
||||||
"gatsby-image": "^2.2.19",
|
"gatsby-image": "^2.2.23",
|
||||||
"gatsby-plugin-catch-links": "^2.1.8",
|
"gatsby-plugin-catch-links": "^2.1.12",
|
||||||
"gatsby-plugin-feed": "^2.3.11",
|
"gatsby-plugin-feed": "^2.3.15",
|
||||||
"gatsby-plugin-lunr": "^1.5.2",
|
"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-matomo": "^0.7.2",
|
||||||
"gatsby-plugin-meta-redirect": "^1.1.1",
|
"gatsby-plugin-meta-redirect": "^1.1.1",
|
||||||
"gatsby-plugin-offline": "^2.2.10",
|
"gatsby-plugin-offline": "^2.2.10",
|
||||||
"gatsby-plugin-react-helmet": "^3.1.6",
|
"gatsby-plugin-react-helmet": "^3.1.10",
|
||||||
"gatsby-plugin-sass": "^2.1.13",
|
"gatsby-plugin-sass": "^2.1.17",
|
||||||
"gatsby-plugin-sharp": "^2.2.24",
|
"gatsby-plugin-sharp": "^2.2.27",
|
||||||
"gatsby-plugin-sitemap": "^2.2.13",
|
"gatsby-plugin-sitemap": "^2.2.16",
|
||||||
"gatsby-plugin-svgr": "^2.0.2",
|
"gatsby-plugin-svgr": "^2.0.2",
|
||||||
|
"gatsby-plugin-typescript": "^2.1.11",
|
||||||
"gatsby-plugin-webpack-size": "^1.0.0",
|
"gatsby-plugin-webpack-size": "^1.0.0",
|
||||||
"gatsby-redirect-from": "^0.2.1",
|
"gatsby-redirect-from": "^0.2.1",
|
||||||
"gatsby-remark-autolink-headers": "^2.1.9",
|
"gatsby-remark-autolink-headers": "^2.1.13",
|
||||||
"gatsby-remark-copy-linked-files": "^2.1.17",
|
"gatsby-remark-copy-linked-files": "^2.1.23",
|
||||||
"gatsby-remark-images": "^3.1.22",
|
"gatsby-remark-images": "^3.1.25",
|
||||||
"gatsby-remark-smartypants": "^2.1.7",
|
"gatsby-remark-smartypants": "^2.1.11",
|
||||||
"gatsby-remark-vscode": "^1.2.0",
|
"gatsby-remark-vscode": "^1.2.0",
|
||||||
"gatsby-source-filesystem": "^2.1.24",
|
"gatsby-source-filesystem": "^2.1.28",
|
||||||
"gatsby-source-graphql": "^2.1.12",
|
"gatsby-source-graphql": "^2.1.17",
|
||||||
"gatsby-transformer-remark": "^2.6.23",
|
"gatsby-transformer-remark": "^2.6.26",
|
||||||
"gatsby-transformer-sharp": "^2.2.15",
|
"gatsby-transformer-sharp": "^2.2.19",
|
||||||
"graphql": "^14.5.6",
|
"graphql": "^14.5.8",
|
||||||
"intersection-observer": "^0.7.0",
|
"intersection-observer": "^0.7.0",
|
||||||
"js-scrypt": "^0.2.0",
|
"js-scrypt": "^0.2.0",
|
||||||
"load-script": "^1.0.0",
|
"load-script": "^1.0.0",
|
||||||
"pigeon-maps": "^0.14.0",
|
"pigeon-maps": "^0.14.0",
|
||||||
"pigeon-marker": "^0.3.4",
|
"pigeon-marker": "^0.3.4",
|
||||||
"react": "^16.9.0",
|
"react": "^16.10.1",
|
||||||
"react-blockies": "^1.4.1",
|
"react-blockies": "^1.4.1",
|
||||||
"react-clipboard.js": "^2.0.13",
|
"react-clipboard.js": "^2.0.13",
|
||||||
"react-dom": "^16.9.0",
|
"react-dom": "^16.10.1",
|
||||||
"react-helmet": "^5.2.1",
|
"react-helmet": "^5.2.1",
|
||||||
"react-modal": "^3.10.1",
|
"react-modal": "^3.10.1",
|
||||||
"react-pose": "^4.0.8",
|
"react-pose": "^4.0.8",
|
||||||
@ -78,20 +77,31 @@
|
|||||||
"web3": "^1.2.1"
|
"web3": "^1.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/node": "^7.6.0",
|
"@babel/node": "^7.6.2",
|
||||||
"@babel/preset-env": "^7.6.0",
|
"@babel/preset-env": "^7.6.2",
|
||||||
"@svgr/webpack": "^4.3.1",
|
"@babel/preset-typescript": "^7.6.0",
|
||||||
|
"@svgr/webpack": "^4.3.3",
|
||||||
"@testing-library/jest-dom": "^4.1.0",
|
"@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-eslint": "^10.0.3",
|
||||||
"babel-jest": "^24.9.0",
|
"babel-jest": "^24.9.0",
|
||||||
"eslint": "^6.4.0",
|
"eslint": "^6.5.1",
|
||||||
"eslint-config-prettier": "^6.2.0",
|
"eslint-config-prettier": "^6.3.0",
|
||||||
"eslint-loader": "^3.0.0",
|
"eslint-loader": "^3.0.2",
|
||||||
"eslint-plugin-graphql": "^3.0.3",
|
"eslint-plugin-graphql": "^3.1.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||||
"eslint-plugin-prettier": "^3.1.1",
|
"eslint-plugin-prettier": "^3.1.1",
|
||||||
"eslint-plugin-react": "^7.14.3",
|
"eslint-plugin-react": "^7.15.0",
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^24.9.0",
|
"jest": "^24.9.0",
|
||||||
@ -104,8 +114,10 @@
|
|||||||
"prettier-stylelint": "^0.4.2",
|
"prettier-stylelint": "^0.4.2",
|
||||||
"stylelint": "^11.0.0",
|
"stylelint": "^11.0.0",
|
||||||
"stylelint-config-css-modules": "^1.5.0",
|
"stylelint-config-css-modules": "^1.5.0",
|
||||||
|
"stylelint-config-prettier": "^6.0.0",
|
||||||
"stylelint-config-standard": "^19.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"
|
"why-did-you-update": "^1.0.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
13
src/@types/declarations.d.ts
vendored
Normal file
13
src/@types/declarations.d.ts
vendored
Normal 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
1
src/@types/pigeon-maps.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module 'pigeon-maps'
|
1
src/@types/pigeon-marker.d.ts
vendored
Normal file
1
src/@types/pigeon-marker.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module 'pigeon-marker'
|
1
src/@types/react-blockies.d.ts
vendored
Normal file
1
src/@types/react-blockies.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module 'react-blockies'
|
1
src/@types/react-time.d.ts
vendored
Normal file
1
src/@types/react-time.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module 'react-time'
|
1
src/@types/remark-react.d.ts
vendored
Normal file
1
src/@types/remark-react.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module 'remark-react'
|
@ -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
|
|
@ -2,19 +2,19 @@
|
|||||||
@import 'mixins';
|
@import 'mixins';
|
||||||
|
|
||||||
#___gatsby {
|
#___gatsby {
|
||||||
// display: flex;
|
// display: flex;
|
||||||
// min-height: 100vh;
|
// min-height: 100vh;
|
||||||
// flex-direction: column;
|
// flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 0 $spacer / $line-height;
|
padding: 0 $spacer / $line-height;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@media (min-width: $screen-sm) {
|
@media (min-width: $screen-sm) {
|
||||||
padding: 0 ($spacer * 2);
|
padding: 0 ($spacer * 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// topbar and footer as fixed
|
// topbar and footer as fixed
|
||||||
@ -22,27 +22,27 @@
|
|||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
|
|
||||||
.document {
|
.document {
|
||||||
@include transition;
|
@include transition;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-top: ($spacer * 2);
|
padding-top: ($spacer * 2);
|
||||||
background-color: $page-background-color;
|
background-color: $page-background-color;
|
||||||
border-top: 1px solid rgba(255, 255, 255, .7);
|
border-top: 1px solid rgba(255, 255, 255, 0.7);
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, .7);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.7);
|
||||||
padding-bottom: $spacer * 2;
|
padding-bottom: $spacer * 2;
|
||||||
box-shadow: 0 1px 4px rgba($brand-main, .1),
|
box-shadow: 0 1px 4px rgba($brand-main, 0.1),
|
||||||
0 -1px 4px rgba($brand-main, .2);
|
0 -1px 4px rgba($brand-main, 0.2);
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
|
|
||||||
:global(.has-menu-open) & {
|
:global(.has-menu-open) & {
|
||||||
transform: translate3d(0, ($spacer * 3), 0);
|
transform: translate3d(0, ($spacer * 3), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $screen-sm) and (min-height: 500px) {
|
@media (min-width: $screen-sm) and (min-height: 500px) {
|
||||||
margin-top: $spacer * 2.65;
|
margin-top: $spacer * 2.65;
|
||||||
margin-bottom: $spacer * 19; // height of footer
|
margin-bottom: $spacer * 19; // height of footer
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
33
src/components/Layout.tsx
Normal file
33
src/components/Layout.tsx
Normal 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 />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,95 +2,95 @@
|
|||||||
@import 'mixins';
|
@import 'mixins';
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
@include breakoutviewport;
|
@include breakoutviewport;
|
||||||
|
|
||||||
margin-top: $spacer * 3;
|
margin-top: $spacer * 3;
|
||||||
background: rgba(#fff, .5);
|
background: rgba(#fff, 0.5);
|
||||||
padding-top: $spacer;
|
padding-top: $spacer;
|
||||||
padding-bottom: $spacer;
|
padding-bottom: $spacer;
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
@media (min-width: $screen-md) {
|
@media (min-width: $screen-md) {
|
||||||
margin-left: -100%;
|
margin-left: -100%;
|
||||||
margin-right: -18%;
|
margin-right: -18%;
|
||||||
padding-left: 80%;
|
padding-left: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
border-bottom: 1px dashed rgba($brand-grey-light, 0.3);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
> div {
|
@media (min-width: $screen-sm) {
|
||||||
flex: 0 0 100%;
|
flex: 0 0 33.33333%;
|
||||||
border-bottom: 1px dashed rgba($brand-grey-light, .3);
|
border-bottom: 0;
|
||||||
|
border-left: 1px dashed rgba($brand-grey-light, 0.3);
|
||||||
|
|
||||||
&:last-child {
|
&:first-child {
|
||||||
border-bottom: 0;
|
border-left: 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
transition: .2s ease-out;
|
transition: 0.2s ease-out;
|
||||||
color: $link-color;
|
color: $link-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionTitle {
|
.actionTitle {
|
||||||
font-size: $font-size-base;
|
font-size: $font-size-base;
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: $spacer / 4;
|
margin-bottom: $spacer / 4;
|
||||||
transition: color .2s ease-out;
|
transition: color 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionText {
|
.actionText {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
color: $brand-grey-light;
|
color: $brand-grey-light;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
transition: color .2s ease-out;
|
transition: color 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action {
|
.action {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-top: $spacer;
|
padding-top: $spacer;
|
||||||
padding-bottom: $spacer;
|
padding-bottom: $spacer;
|
||||||
padding-left: $spacer * 2;
|
padding-left: $spacer * 2;
|
||||||
padding-right: $spacer;
|
padding-right: $spacer;
|
||||||
position: relative;
|
position: relative;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
.link,
|
.link,
|
||||||
.actionTitle,
|
.actionTitle,
|
||||||
.actionText {
|
.actionText {
|
||||||
color: $link-color-hover;
|
color: $link-color-hover;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
.link,
|
.link,
|
||||||
.actionTitle,
|
.actionTitle,
|
||||||
.actionText {
|
.actionText {
|
||||||
transition: none;
|
transition: none;
|
||||||
color: $link-color-active;
|
color: $link-color-active;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: $spacer;
|
left: $spacer;
|
||||||
top: $spacer;
|
top: $spacer;
|
||||||
fill: $brand-grey-light;
|
fill: $brand-grey-light;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
96
src/components/Post/PostActions.tsx
Normal file
96
src/components/Post/PostActions.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -1,9 +1,8 @@
|
|||||||
import React, { Fragment } from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Changelog from '../atoms/Changelog'
|
import Changelog from '../atoms/Changelog'
|
||||||
|
|
||||||
// Remove lead paragraph from content
|
// Remove lead paragraph from content
|
||||||
const PostContent = ({ post }) => {
|
const PostContent = ({ post }: { post: any }) => {
|
||||||
const separator = '<!-- more -->'
|
const separator = '<!-- more -->'
|
||||||
const changelog = post.frontmatter.changelog
|
const changelog = post.frontmatter.changelog
|
||||||
|
|
||||||
@ -19,15 +18,11 @@ const PostContent = ({ post }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<div dangerouslySetInnerHTML={{ __html: content }} />
|
<div dangerouslySetInnerHTML={{ __html: content }} />
|
||||||
{changelog && <Changelog repo={changelog} />}
|
{changelog && <Changelog repo={changelog} />}
|
||||||
</Fragment>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
PostContent.propTypes = {
|
|
||||||
post: PropTypes.object
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PostContent
|
export default PostContent
|
@ -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
|
|
@ -2,43 +2,43 @@
|
|||||||
@import 'mixins';
|
@import 'mixins';
|
||||||
|
|
||||||
.postImageTitle {
|
.postImageTitle {
|
||||||
transition: .1s ease-out;
|
transition: 0.1s ease-out;
|
||||||
font-size: $font-size-h3;
|
font-size: $font-size-h3;
|
||||||
font-family: $font-family-headings;
|
font-family: $font-family-headings;
|
||||||
line-height: $line-height-headings;
|
line-height: $line-height-headings;
|
||||||
font-weight: $font-weight-headings;
|
font-weight: $font-weight-headings;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
letter-spacing: -.02em;
|
letter-spacing: -0.02em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10%;
|
top: 10%;
|
||||||
padding: $spacer / 3 $spacer;
|
padding: $spacer / 3 $spacer;
|
||||||
background: rgba($link-color, .85);
|
background: rgba($link-color, 0.85);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-shadow: 0 1px 0 #000;
|
text-shadow: 0 1px 0 #000;
|
||||||
left: 0;
|
left: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translate3d(0, -20px, 0);
|
transform: translate3d(0, -20px, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.postImage {
|
.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;
|
display: block;
|
||||||
margin-top: $spacer * 1.5;
|
}
|
||||||
margin-bottom: $spacer * 1.5;
|
|
||||||
|
|
||||||
a & {
|
a:hover & {
|
||||||
position: relative;
|
.postImageTitle {
|
||||||
display: block;
|
opacity: 1;
|
||||||
}
|
transform: translate3d(0, 0, 0);
|
||||||
|
|
||||||
a:hover & {
|
|
||||||
.postImageTitle {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
22
src/components/Post/PostImage.tsx
Normal file
22
src/components/Post/PostImage.tsx
Normal 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
|
@ -1,10 +1,10 @@
|
|||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
.lead {
|
.lead {
|
||||||
font-size: $font-size-large;
|
font-size: $font-size-large;
|
||||||
margin-bottom: $spacer;
|
margin-bottom: $spacer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.index {
|
.index {
|
||||||
font-size: $font-size-base;
|
font-size: $font-size-base;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import styles from './PostLead.module.scss'
|
import styles from './PostLead.module.scss'
|
||||||
|
|
||||||
// Extract lead paragraph from content
|
// Extract lead paragraph from content
|
||||||
// Grab everything before more tag, or just first paragraph
|
// Grab everything before more tag, or just first paragraph
|
||||||
const PostLead = ({ post, index }) => {
|
const PostLead = ({
|
||||||
|
post,
|
||||||
|
index
|
||||||
|
}: {
|
||||||
|
post: { html: string }
|
||||||
|
index?: boolean
|
||||||
|
}) => {
|
||||||
let lead
|
let lead
|
||||||
const content = post.html
|
const content = post.html
|
||||||
const separator = '<!-- more -->'
|
const separator = '<!-- more -->'
|
||||||
@ -23,9 +28,4 @@ const PostLead = ({ post, index }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
PostLead.propTypes = {
|
|
||||||
post: PropTypes.object,
|
|
||||||
index: PropTypes.bool
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PostLead
|
export default PostLead
|
@ -1,24 +1,24 @@
|
|||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
.postLinkActions {
|
.postLinkActions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-top: $spacer * 2;
|
margin-top: $spacer * 2;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
svg {
|
svg {
|
||||||
width: $font-size-small;
|
width: $font-size-small;
|
||||||
height: $font-size-small;
|
height: $font-size-small;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
fill: $text-color-light;
|
fill: $text-color-light;
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
svg {
|
|
||||||
width: $font-size-base;
|
|
||||||
height: $font-size-base;
|
|
||||||
fill: $brand-cyan;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
svg {
|
||||||
|
width: $font-size-base;
|
||||||
|
height: $font-size-base;
|
||||||
|
fill: $brand-cyan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { Link } from 'gatsby'
|
import { Link } from 'gatsby'
|
||||||
import { ReactComponent as Forward } from '../../images/forward.svg'
|
import { ReactComponent as Forward } from '../../images/forward.svg'
|
||||||
import { ReactComponent as Infinity } from '../../images/infinity.svg'
|
import { ReactComponent as Infinity } from '../../images/infinity.svg'
|
||||||
import styles from './PostLinkActions.module.scss'
|
import styles from './PostLinkActions.module.scss'
|
||||||
import stylesPostMore from './PostMore.module.scss'
|
import stylesPostMore from './PostMore.module.scss'
|
||||||
|
|
||||||
const PostLinkActions = ({ linkurl, slug }) => (
|
const PostLinkActions = ({
|
||||||
|
linkurl,
|
||||||
|
slug
|
||||||
|
}: {
|
||||||
|
linkurl?: string
|
||||||
|
slug: string
|
||||||
|
}) => (
|
||||||
<div className={styles.postLinkActions}>
|
<div className={styles.postLinkActions}>
|
||||||
<a className={stylesPostMore.postMore} href={linkurl}>
|
<a className={stylesPostMore.postMore} href={linkurl}>
|
||||||
Go to source <Forward />
|
Go to source <Forward />
|
||||||
@ -17,9 +22,4 @@ const PostLinkActions = ({ linkurl, slug }) => (
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
PostLinkActions.propTypes = {
|
|
||||||
slug: PropTypes.string.isRequired,
|
|
||||||
linkurl: PropTypes.string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PostLinkActions
|
export default PostLinkActions
|
@ -4,82 +4,82 @@
|
|||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
|
|
||||||
.entryMeta {
|
.entryMeta {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
margin-top: $spacer * 2;
|
margin-top: $spacer * 2;
|
||||||
color: $brand-grey-light;
|
color: $brand-grey-light;
|
||||||
}
|
}
|
||||||
|
|
||||||
.byline,
|
.byline,
|
||||||
.time,
|
.time,
|
||||||
.tags,
|
.tags,
|
||||||
.categories {
|
.categories {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.byline,
|
.byline,
|
||||||
.time {
|
.time {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.byline {
|
.byline {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.by {
|
.by {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
||||||
margin-bottom: $spacer * 2;
|
margin-bottom: $spacer * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Types & Tags
|
// Types & Tags
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
|
|
||||||
.type {
|
.type {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: $font-size-mini;
|
||||||
text-align: center;
|
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 {
|
&:hover,
|
||||||
font-size: $font-size-mini;
|
&:focus {
|
||||||
text-align: center;
|
color: $link-color;
|
||||||
color: $text-color;
|
border-color: $link-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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: $link-color;
|
||||||
|
top: 0;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags {
|
.tags {
|
||||||
margin-top: $spacer / 2;
|
margin-top: $spacer / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
margin-left: $spacer / 2;
|
margin-left: $spacer / 2;
|
||||||
margin-right: $spacer / 2;
|
margin-right: $spacer / 2;
|
||||||
margin-bottom: $spacer / 2;
|
margin-bottom: $spacer / 2;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
color: $brand-grey-light;
|
color: $brand-grey-light;
|
||||||
content: '#';
|
content: '#';
|
||||||
margin-right: 1px;
|
margin-right: 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { Link } from 'gatsby'
|
import { Link } from 'gatsby'
|
||||||
import Time from 'react-time'
|
import Time from 'react-time'
|
||||||
import slugify from 'slugify'
|
import slugify from 'slugify'
|
||||||
import styles from './PostMeta.module.scss'
|
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 { author, updated, tags, type } = post.frontmatter
|
||||||
const { date } = post.fields
|
const { date } = post.fields
|
||||||
|
|
||||||
@ -40,7 +39,7 @@ const PostMeta = ({ post, meta }) => {
|
|||||||
|
|
||||||
{tags && (
|
{tags && (
|
||||||
<div className={styles.tags}>
|
<div className={styles.tags}>
|
||||||
{tags.map(tag => {
|
{tags.map((tag: string) => {
|
||||||
const to = tag === 'goodies' ? '/goodies' : `/tags/${slugify(tag)}/`
|
const to = tag === 'goodies' ? '/goodies' : `/tags/${slugify(tag)}/`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -55,9 +54,4 @@ const PostMeta = ({ post, meta }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
PostMeta.propTypes = {
|
|
||||||
post: PropTypes.object.isRequired,
|
|
||||||
meta: PropTypes.object.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PostMeta
|
export default PostMeta
|
@ -1,29 +1,29 @@
|
|||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
.postMore {
|
.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;
|
display: inline-block;
|
||||||
font-family: $font-family-headings;
|
margin: 0;
|
||||||
font-weight: $font-weight-headings;
|
top: 0.2rem;
|
||||||
font-size: $font-size-base * .9;
|
position: relative;
|
||||||
color: $link-color;
|
width: 1.1rem;
|
||||||
text-transform: uppercase;
|
height: 1.1rem;
|
||||||
margin-top: $spacer;
|
fill: $text-color-light;
|
||||||
|
transition: 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
svg {
|
svg {
|
||||||
display: inline-block;
|
transform: translate3d(0.2rem, 0, 0);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { Link } from 'gatsby'
|
import { Link } from 'gatsby'
|
||||||
import styles from './PostMore.module.scss'
|
import styles from './PostMore.module.scss'
|
||||||
import { ReactComponent as Caret } from '../../images/chevron-right.svg'
|
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}>
|
<Link className={styles.postMore} to={to}>
|
||||||
{children}
|
{children}
|
||||||
<Caret />
|
<Caret />
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
|
||||||
PostMore.propTypes = {
|
|
||||||
to: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.string.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PostMore
|
export default PostMore
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +1,29 @@
|
|||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
.postTitle {
|
.postTitle {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: $spacer / 4;
|
margin-top: $spacer / 4;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
line-height: $line-height-small;
|
line-height: $line-height-small;
|
||||||
color: $brand-grey-light;
|
color: $brand-grey-light;
|
||||||
padding-left: .2rem;
|
padding-left: 0.2rem;
|
||||||
padding-right: .2rem;
|
padding-right: 0.2rem;
|
||||||
transition: color .2s ease-out;
|
transition: color 0.2s ease-out;
|
||||||
|
|
||||||
@media (min-width: $screen-md) {
|
@media (min-width: $screen-md) {
|
||||||
font-size: $font-size-base;
|
font-size: $font-size-base;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: $spacer / 4;
|
padding: $spacer / 4;
|
||||||
|
|
||||||
.postTitle {
|
.postTitle {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
32
src/components/Post/PostTeaser.tsx
Normal file
32
src/components/Post/PostTeaser.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -5,32 +5,32 @@
|
|||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
|
|
||||||
.hentry__title {
|
.hentry__title {
|
||||||
font-size: $font-size-h1;
|
font-size: $font-size-h1;
|
||||||
color: $color-headings;
|
color: $color-headings;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: $spacer;
|
margin-bottom: $spacer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hentry__title__link {
|
.hentry__title__link {
|
||||||
font-size: $font-size-h3;
|
font-size: $font-size-h3;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: $font-size-base;
|
width: $font-size-base;
|
||||||
height: $font-size-base;
|
height: $font-size-base;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
fill: $text-color-light;
|
fill: $text-color-light;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.linkurl {
|
.linkurl {
|
||||||
@include ellipsis();
|
@include ellipsis();
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
font-family: $font-family-base;
|
font-family: $font-family-base;
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
padding: ($spacer/4) 0;
|
padding: ($spacer/4) 0;
|
||||||
margin-top: -($spacer);
|
margin-top: -($spacer);
|
||||||
margin-bottom: $spacer;
|
margin-bottom: $spacer;
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,23 @@
|
|||||||
import React, { Fragment } from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { Link } from 'gatsby'
|
import { Link } from 'gatsby'
|
||||||
import { ReactComponent as Forward } from '../../images/forward.svg'
|
import { ReactComponent as Forward } from '../../images/forward.svg'
|
||||||
import styles from './PostTitle.module.scss'
|
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
|
const linkHostname = linkurl ? new URL(linkurl).hostname : null
|
||||||
|
|
||||||
return type === 'link' ? (
|
return type === 'link' ? (
|
||||||
<Fragment>
|
<>
|
||||||
<h1
|
<h1
|
||||||
className={[styles.hentry__title, styles.hentry__title__link].join(' ')}
|
className={[styles.hentry__title, styles.hentry__title__link].join(' ')}
|
||||||
>
|
>
|
||||||
@ -17,7 +26,7 @@ const PostTitle = ({ type, slug, linkurl, title }) => {
|
|||||||
</a>
|
</a>
|
||||||
</h1>
|
</h1>
|
||||||
<div className={styles.linkurl}>{linkHostname}</div>
|
<div className={styles.linkurl}>{linkHostname}</div>
|
||||||
</Fragment>
|
</>
|
||||||
) : slug ? (
|
) : slug ? (
|
||||||
<h1 className={styles.hentry__title}>
|
<h1 className={styles.hentry__title}>
|
||||||
<Link to={slug}>{title}</Link>
|
<Link to={slug}>{title}</Link>
|
||||||
@ -26,12 +35,3 @@ const PostTitle = ({ type, slug, linkurl, title }) => {
|
|||||||
<h1 className={styles.hentry__title}>{title}</h1>
|
<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
|
|
@ -1,46 +1,46 @@
|
|||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: $spacer / 2;
|
left: $spacer / 2;
|
||||||
right: $spacer / 2;
|
right: $spacer / 2;
|
||||||
top: -($spacer / 4);
|
top: -($spacer / 4);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $screen-md) {
|
@media (min-width: $screen-md) {
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.appear,
|
.appear,
|
||||||
.enter {
|
.enter {
|
||||||
opacity: .01;
|
opacity: 0.01;
|
||||||
transform: translate3d(0, -100px, 0);
|
transform: translate3d(0, -100px, 0);
|
||||||
|
|
||||||
&.appearActive,
|
&.appearActive,
|
||||||
&.enterActive {
|
&.enterActive {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: .2s ease-out;
|
transition: 0.2s ease-out;
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.exit {
|
.exit {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
|
|
||||||
&.exitActive {
|
&.exitActive {
|
||||||
opacity: .01;
|
opacity: 0.01;
|
||||||
transition: .2s ease-in;
|
transition: 0.2s ease-in;
|
||||||
transform: translate3d(0, -100px, 0);
|
transform: translate3d(0, -100px, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.hasSearchOpen) {
|
:global(.hasSearchOpen) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,10 @@ import SearchResults from './SearchResults'
|
|||||||
|
|
||||||
import styles from './Search.module.scss'
|
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 = {
|
state = {
|
||||||
searchOpen: false,
|
searchOpen: false,
|
||||||
query: '',
|
query: '',
|
||||||
@ -25,14 +28,14 @@ export default class Search extends PureComponent {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
getSearchResults(query) {
|
getSearchResults(query: string) {
|
||||||
if (!query || !window.__LUNR__) return []
|
if (!query || !window.__LUNR__) return []
|
||||||
const lunrIndex = window.__LUNR__[this.props.lng]
|
const lunrIndex = window.__LUNR__[this.props.lng]
|
||||||
const results = lunrIndex.index.search(query)
|
const results = lunrIndex.index.search(query)
|
||||||
return results.map(({ ref }) => lunrIndex.store[ref])
|
return results.map(({ ref }) => lunrIndex.store[ref])
|
||||||
}
|
}
|
||||||
|
|
||||||
search = event => {
|
search = (event: any) => {
|
||||||
const query = event.target.value
|
const query = event.target.value
|
||||||
// wildcard search https://lunrjs.com/guides/searching.html#wildcards
|
// wildcard search https://lunrjs.com/guides/searching.html#wildcards
|
||||||
const results = query.length > 1 ? this.getSearchResults(`${query}*`) : []
|
const results = query.length > 1 ? this.getSearchResults(`${query}*`) : []
|
||||||
@ -65,7 +68,7 @@ export default class Search extends PureComponent {
|
|||||||
<section className={styles.search}>
|
<section className={styles.search}>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
value={query}
|
value={query}
|
||||||
onChange={this.search}
|
onChange={() => this.search}
|
||||||
onToggle={this.toggleSearch}
|
onToggle={this.toggleSearch}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
@ -1,33 +1,33 @@
|
|||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
.searchButton {
|
.searchButton {
|
||||||
padding: .65rem .85rem;
|
padding: 0.65rem 0.85rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: $spacer / 4;
|
margin-right: $spacer / 4;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: $text-color-light;
|
||||||
|
width: 21px;
|
||||||
|
height: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
svg {
|
svg {
|
||||||
fill: $text-color-light;
|
fill: $brand-cyan;
|
||||||
width: 21px;
|
|
||||||
height: 21px;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover,
|
&:active {
|
||||||
&:focus {
|
svg {
|
||||||
svg {
|
fill: darken($brand-cyan, 30%);
|
||||||
fill: $brand-cyan;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
svg {
|
|
||||||
fill: darken($brand-cyan, 30%);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import { ReactComponent as SearchIcon } from '../../images/magnifying-glass.svg'
|
import { ReactComponent as SearchIcon } from '../../images/magnifying-glass.svg'
|
||||||
import styles from './SearchButton.module.scss'
|
import styles from './SearchButton.module.scss'
|
||||||
|
|
||||||
const SearchButton = props => (
|
const SearchButton = (props: any) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title="Search"
|
title="Search"
|
@ -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"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +1,27 @@
|
|||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
.searchInput {
|
.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;
|
background: $input-bg-focus;
|
||||||
|
}
|
||||||
&::-webkit-search-cancel-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $input-bg-focus;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchInputClose {
|
.searchInputClose {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: $spacer / 2;
|
right: $spacer / 2;
|
||||||
top: $spacer / 5;
|
top: $spacer / 5;
|
||||||
font-size: $font-size-h3;
|
font-size: $font-size-h3;
|
||||||
color: $brand-grey-light;
|
color: $brand-grey-light;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: $link-color;
|
color: $link-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
33
src/components/Search/SearchInput.tsx
Normal file
33
src/components/Search/SearchInput.tsx
Normal 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"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -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')
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,74 +2,74 @@
|
|||||||
@import 'mixins';
|
@import 'mixins';
|
||||||
|
|
||||||
.searchResults {
|
.searchResults {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba($body-background-color, .95);
|
background: rgba($body-background-color, 0.95);
|
||||||
backdrop-filter: blur(5px);
|
backdrop-filter: blur(5px);
|
||||||
animation: fadein .3s;
|
animation: fadein 0.3s;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
height: 91vh;
|
height: 91vh;
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
@include breakoutviewport;
|
@include breakoutviewport;
|
||||||
|
|
||||||
padding: $spacer $spacer / 2;
|
padding: $spacer $spacer / 2;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
@media (min-width: $screen-md) {
|
@media (min-width: $screen-md) {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
li {
|
||||||
margin-bottom: 0;
|
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 {
|
&:hover,
|
||||||
display: block;
|
&:focus {
|
||||||
|
h4 {
|
||||||
> div {
|
color: $link-color;
|
||||||
margin-bottom: 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
h4 {
|
|
||||||
color: $link-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadein {
|
@keyframes fadein {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
79
src/components/Search/SearchResults.tsx
Normal file
79
src/components/Search/SearchResults.tsx
Normal 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')
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@ -1,34 +1,34 @@
|
|||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
padding-top: 15vh;
|
padding-top: 15vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emptyMessage {
|
.emptyMessage {
|
||||||
color: $brand-grey-light;
|
color: $brand-grey-light;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emptyMessageText {
|
.emptyMessageText {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
animation: ellipsis steps(4, end) 1s infinite;
|
animation: ellipsis steps(4, end) 1s infinite;
|
||||||
content: '\2026'; // ascii code for the ellipsis character
|
content: '\2026'; // ascii code for the ellipsis character
|
||||||
width: 0;
|
width: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 101%;
|
left: 101%;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes ellipsis {
|
@keyframes ellipsis {
|
||||||
to {
|
to {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import styles from './SearchResultsEmpty.module.scss'
|
import styles from './SearchResultsEmpty.module.scss'
|
||||||
|
|
||||||
const SearchResultsEmpty = ({ searchQuery, results }) => (
|
const SearchResultsEmpty = ({
|
||||||
|
searchQuery,
|
||||||
|
results
|
||||||
|
}: {
|
||||||
|
searchQuery: string
|
||||||
|
results: []
|
||||||
|
}) => (
|
||||||
<div className={styles.empty}>
|
<div className={styles.empty}>
|
||||||
<header className={styles.emptyMessage}>
|
<header className={styles.emptyMessage}>
|
||||||
<p className={styles.emptyMessageText}>
|
<p className={styles.emptyMessageText}>
|
||||||
@ -16,9 +21,4 @@ const SearchResultsEmpty = ({ searchQuery, results }) => (
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
SearchResultsEmpty.propTypes = {
|
|
||||||
results: PropTypes.array.isRequired,
|
|
||||||
searchQuery: PropTypes.string.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SearchResultsEmpty
|
export default SearchResultsEmpty
|
@ -1,19 +1,19 @@
|
|||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
.account {
|
.account {
|
||||||
font-size: $font-size-mini;
|
font-size: $font-size-mini;
|
||||||
color: $brand-grey-light;
|
color: $brand-grey-light;
|
||||||
max-width: 8rem;
|
max-width: 8rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.identicon {
|
.identicon {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: $spacer / 8;
|
margin-right: $spacer / 8;
|
||||||
margin-left: $spacer;
|
margin-left: $spacer;
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Blockies from 'react-blockies'
|
import Blockies from 'react-blockies'
|
||||||
import styles from './Account.module.scss'
|
import styles from './Account.module.scss'
|
||||||
|
|
||||||
const Account = ({ account }) => (
|
const Account = ({ account }: { account: string }) => (
|
||||||
<div className={styles.account} title={account}>
|
<div className={styles.account} title={account}>
|
||||||
<Blockies seed={account} scale={2} size={8} className={styles.identicon} />
|
<Blockies seed={account} scale={2} size={8} className={styles.identicon} />
|
||||||
{account}
|
{account}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
Account.propTypes = {
|
|
||||||
account: PropTypes.string.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Account
|
export default Account
|
@ -2,44 +2,44 @@
|
|||||||
@import 'mixins';
|
@import 'mixins';
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
overflow: hidden;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
&:empty {
|
animation: ellipsis steps(4, end) 1s infinite;
|
||||||
display: none;
|
content: '\2026'; // ascii code for the ellipsis character
|
||||||
}
|
width: 0;
|
||||||
|
position: absolute;
|
||||||
&::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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
composes: alert;
|
composes: alert;
|
||||||
color: darken($alert-error, 60%);
|
color: darken($alert-error, 60%);
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.success {
|
.success {
|
||||||
composes: alert;
|
composes: alert;
|
||||||
color: darken($alert-success, 60%);
|
color: darken($alert-success, 60%);
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes ellipsis {
|
@keyframes ellipsis {
|
||||||
to {
|
to {
|
||||||
width: .75rem;
|
width: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import React, { PureComponent } from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import styles from './Alerts.module.scss'
|
import styles from './Alerts.module.scss'
|
||||||
|
|
||||||
export const alertMessages = (networkName, transactionHash) => ({
|
export const alertMessages = (
|
||||||
|
networkName?: string,
|
||||||
|
transactionHash?: string
|
||||||
|
) => ({
|
||||||
noAccount:
|
noAccount:
|
||||||
'Web3 detected, but no account. Are you logged into your MetaMask account?',
|
'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.`,
|
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!'
|
success: 'Confirmed. You are awesome, thanks!'
|
||||||
})
|
})
|
||||||
|
|
||||||
export default class Alerts extends PureComponent {
|
export default function Alerts({
|
||||||
static propTypes = {
|
transactionHash,
|
||||||
message: PropTypes.object,
|
message
|
||||||
transactionHash: PropTypes.string
|
}: {
|
||||||
}
|
transactionHash: string | null
|
||||||
|
message: { text: MessageChannel; status: string } | null
|
||||||
constructMessage = () => {
|
}) {
|
||||||
const { transactionHash, message } = this.props
|
const constructMessage = () => {
|
||||||
|
|
||||||
let messageOutput
|
let messageOutput
|
||||||
|
|
||||||
if (transactionHash) {
|
if (transactionHash) {
|
||||||
messageOutput =
|
messageOutput =
|
||||||
|
message &&
|
||||||
message.text +
|
message.text +
|
||||||
'<br />' +
|
'<br />' +
|
||||||
alertMessages(null, transactionHash).transaction
|
alertMessages(null, transactionHash).transaction
|
||||||
} else {
|
} else {
|
||||||
messageOutput = message.text
|
messageOutput = message && message.text
|
||||||
}
|
}
|
||||||
|
|
||||||
return messageOutput
|
return messageOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
classes() {
|
const classes = () => {
|
||||||
const { status } = this.props.message
|
const { status } = message
|
||||||
|
|
||||||
if (status === 'success') {
|
if (status === 'success') {
|
||||||
return styles.success
|
return styles.success
|
||||||
@ -48,12 +50,10 @@ export default class Alerts extends PureComponent {
|
|||||||
return styles.alert
|
return styles.alert
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<div
|
||||||
<div
|
className={classes()}
|
||||||
className={this.classes()}
|
dangerouslySetInnerHTML={{ __html: constructMessage() }}
|
||||||
dangerouslySetInnerHTML={{ __html: this.constructMessage() }}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,11 +1,11 @@
|
|||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
.conversion {
|
.conversion {
|
||||||
font-size: $font-size-mini;
|
font-size: $font-size-mini;
|
||||||
color: $brand-grey-light;
|
color: $brand-grey-light;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin-left: $spacer / 2;
|
margin-left: $spacer / 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import React, { PureComponent } from 'react'
|
import React, { PureComponent } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { getFiat } from './utils'
|
import { getFiat } from './utils'
|
||||||
import styles from './Conversion.module.scss'
|
import styles from './Conversion.module.scss'
|
||||||
|
|
||||||
export default class Conversion extends PureComponent {
|
export default class Conversion extends PureComponent<
|
||||||
static propTypes = {
|
{ amount: string },
|
||||||
amount: PropTypes.string.isRequired
|
{ euro: string; dollar: string }
|
||||||
}
|
> {
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
euro: '0.00',
|
euro: '0.00',
|
||||||
dollar: '0.00'
|
dollar: '0.00'
|
||||||
@ -17,7 +15,7 @@ export default class Conversion extends PureComponent {
|
|||||||
this.getFiatResponse()
|
this.getFiatResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps: any) {
|
||||||
const { amount } = this.props
|
const { amount } = this.props
|
||||||
|
|
||||||
if (amount !== prevProps.amount) {
|
if (amount !== prevProps.amount) {
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,97 +2,97 @@
|
|||||||
@import 'mixins';
|
@import 'mixins';
|
||||||
|
|
||||||
.inputGroup {
|
.inputGroup {
|
||||||
max-width: 18rem;
|
max-width: 18rem;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
animation: fadeIn .8s ease-out backwards;
|
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) {
|
@media (min-width: $screen-sm) {
|
||||||
display: flex;
|
width: 50%;
|
||||||
flex-wrap: wrap;
|
border-top-right-radius: $border-radius;
|
||||||
}
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
button {
|
border-left: 0;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.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) {
|
@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 {
|
&::-webkit-inner-spin-button {
|
||||||
text-align: center;
|
margin-left: -($spacer / 2);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.currency {
|
.currency {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1px;
|
top: 1px;
|
||||||
bottom: 1px;
|
bottom: 1px;
|
||||||
left: 1px;
|
left: 1px;
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
padding: $spacer / 3;
|
padding: $spacer / 3;
|
||||||
color: $brand-grey-light;
|
color: $brand-grey-light;
|
||||||
background: $brand-light;
|
background: $brand-light;
|
||||||
border-right: 1px solid rgba($brand-grey-light, .4);
|
border-right: 1px solid rgba($brand-grey-light, 0.4);
|
||||||
border-top-left-radius: $border-radius;
|
border-top-left-radius: $border-radius;
|
||||||
border-bottom-left-radius: $border-radius;
|
border-bottom-left-radius: $border-radius;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infoline {
|
.infoline {
|
||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: $spacer / 4;
|
margin-top: $spacer / 4;
|
||||||
animation: fadeIn .5s .8s ease-out backwards;
|
animation: fadeIn 0.5s 0.8s ease-out backwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
composes: message from './index.module.scss';
|
composes: message from './index.module.scss';
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
opacity: .01;
|
opacity: 0.01;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
41
src/components/Web3Donation/InputGroup.tsx
Normal file
41
src/components/Web3Donation/InputGroup.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -2,60 +2,60 @@
|
|||||||
@import 'mixins';
|
@import 'mixins';
|
||||||
|
|
||||||
.web3 {
|
.web3 {
|
||||||
@include divider;
|
@include divider;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: $spacer / 2;
|
margin-top: $spacer / 2;
|
||||||
margin-bottom: $spacer;
|
margin-bottom: $spacer;
|
||||||
padding-bottom: $spacer * 1.5;
|
padding-bottom: $spacer * 1.5;
|
||||||
|
|
||||||
small {
|
small {
|
||||||
color: darken($alert-info, 60%);
|
color: darken($alert-info, 60%);
|
||||||
margin-top: -($spacer / 2);
|
margin-top: -($spacer / 2);
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.web3Row {
|
.web3Row {
|
||||||
min-height: 77px;
|
min-height: 77px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
animation: ellipsis steps(4, end) 1s infinite;
|
animation: ellipsis steps(4, end) 1s infinite;
|
||||||
content: '\2026'; // ascii code for the ellipsis character
|
content: '\2026'; // ascii code for the ellipsis character
|
||||||
width: 0;
|
width: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 100%;
|
left: 100%;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.success {
|
.success {
|
||||||
composes: message;
|
composes: message;
|
||||||
color: green;
|
color: green;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes ellipsis {
|
@keyframes ellipsis {
|
||||||
to {
|
to {
|
||||||
width: .75rem;
|
width: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,50 @@
|
|||||||
import Web3 from 'web3'
|
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 () => {
|
export const getWeb3 = async () => {
|
||||||
let web3
|
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()
|
const ethAccounts = await web3.eth.getAccounts()
|
||||||
|
|
||||||
return ethAccounts
|
return ethAccounts
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNetwork = async web3 => {
|
export const getNetwork = async (web3: Web3) => {
|
||||||
const netId = await web3.eth.net.getId()
|
const netId = await web3.eth.net.getId()
|
||||||
const networkName = getNetworkName(netId)
|
const networkName = getNetworkName(netId)
|
||||||
|
|
||||||
return { netId, networkName }
|
return { netId, networkName }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNetworkName = netId => {
|
export const getFiat = async (amount: 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 getFiat = async amount => {
|
|
||||||
const url = 'https://api.coinmarketcap.com/v1/ticker/ethereum/?convert=EUR'
|
const url = 'https://api.coinmarketcap.com/v1/ticker/ethereum/?convert=EUR'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url)
|
const response = await fetch(url)
|
||||||
if (!response.ok) Logger.error(response.statusText)
|
if (!response.ok) Logger.error(response.statusText)
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
const { price_usd, price_eur } = data[0]
|
const { price_usd, price_eur } = data[0]
|
||||||
const dollar = (amount * price_usd).toFixed(2)
|
const dollar = (amount * price_usd).toFixed(2)
|
||||||
const euro = (amount * price_eur).toFixed(2)
|
const euro = (amount * price_eur).toFixed(2)
|
||||||
|
/* eslint-enable @typescript-eslint/camelcase */
|
||||||
|
|
||||||
return { dollar, euro }
|
return { dollar, euro }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
@ -1,69 +1,69 @@
|
|||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
.changelogTitle {
|
.changelogTitle {
|
||||||
margin-top: $spacer * 3;
|
margin-top: $spacer * 3;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.changelogContent {
|
.changelogContent {
|
||||||
padding-top: $spacer * 2;
|
padding-top: $spacer * 2;
|
||||||
padding-left: $spacer / 2;
|
padding-left: $spacer / 2;
|
||||||
margin-left: $spacer / 2;
|
margin-left: $spacer / 2;
|
||||||
border-left: 1px solid $brand-grey-dimmed;
|
border-left: 1px solid $brand-grey-dimmed;
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
width: .4rem;
|
width: 0.4rem;
|
||||||
height: .4rem;
|
height: 0.4rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: $color-headings;
|
background: $color-headings;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -($spacer / 1.5);
|
left: -($spacer / 1.5);
|
||||||
top: $font-size-large / 3;
|
top: $font-size-large / 3;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
h2,
|
h2,
|
||||||
h3 {
|
h3 {
|
||||||
font-size: $font-size-large;
|
font-size: $font-size-large;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-top: $spacer / 8;
|
margin-top: $spacer / 8;
|
||||||
margin-bottom: $spacer / $line-height;
|
margin-bottom: $spacer / $line-height;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
margin-left: $spacer / 8;
|
margin-left: $spacer / 8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.changelogSource {
|
.changelogSource {
|
||||||
font-size: $font-size-mini;
|
font-size: $font-size-mini;
|
||||||
font-family: $font-family-base;
|
font-family: $font-family-base;
|
||||||
font-weight: $font-weight-base;
|
font-weight: $font-weight-base;
|
||||||
padding-top: $spacer / 2;
|
padding-top: $spacer / 2;
|
||||||
padding-bottom: $spacer / 2;
|
padding-bottom: $spacer / 2;
|
||||||
|
|
||||||
&,
|
&,
|
||||||
a {
|
a {
|
||||||
color: $brand-grey-light;
|
color: $brand-grey-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin-left: $spacer / 8;
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: ($font-size-mini * 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
&:hover {
|
||||||
margin-left: $spacer / 8;
|
color: $link-color;
|
||||||
|
|
||||||
code {
|
|
||||||
font-size: ($font-size-mini * .9);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $link-color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
75
src/components/atoms/Changelog.tsx
Normal file
75
src/components/atoms/Changelog.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
|
@ -1,5 +1,5 @@
|
|||||||
.container {
|
.container {
|
||||||
max-width: 35rem;
|
max-width: 35rem;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
10
src/components/atoms/Container.tsx
Normal file
10
src/components/atoms/Container.tsx
Normal 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>
|
||||||
|
}
|
@ -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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,57 +2,57 @@
|
|||||||
@import 'mixins';
|
@import 'mixins';
|
||||||
|
|
||||||
.exif {
|
.exif {
|
||||||
margin-top: -($spacer * 1.5);
|
margin-top: -($spacer * 1.5);
|
||||||
margin-bottom: $spacer * 2;
|
margin-bottom: $spacer * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data {
|
.data {
|
||||||
@include breakoutviewport;
|
@include breakoutviewport;
|
||||||
|
|
||||||
font-size: $font-size-mini;
|
font-size: $font-size-mini;
|
||||||
color: $brand-grey-light;
|
color: $brand-grey-light;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: -3px;
|
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 {
|
span {
|
||||||
display: block;
|
border-left: 1px solid $brand-grey-dimmed;
|
||||||
flex: 1 1 20%;
|
border-bottom: 0;
|
||||||
white-space: nowrap;
|
padding: $spacer;
|
||||||
padding: $spacer / 1.5;
|
|
||||||
border-bottom: 1px solid $brand-grey-dimmed;
|
|
||||||
|
|
||||||
&:first-child {
|
&,
|
||||||
flex-basis: 100%;
|
&:first-child {
|
||||||
}
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $screen-sm) {
|
&:first-child {
|
||||||
margin-bottom: 0;
|
border-left: 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.map {
|
.map {
|
||||||
@include breakoutviewport;
|
@include breakoutviewport;
|
||||||
@include media-frame;
|
@include media-frame;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 160px;
|
height: 160px;
|
||||||
}
|
}
|
||||||
|
34
src/components/atoms/Exif.tsx
Normal file
34
src/components/atoms/Exif.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import React, { PureComponent } from 'react'
|
import React, { useState } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Map from 'pigeon-maps'
|
import Map from 'pigeon-maps'
|
||||||
import Marker from 'pigeon-marker'
|
import Marker from 'pigeon-marker'
|
||||||
|
|
||||||
@ -9,7 +8,11 @@ const MAPBOX_ACCESS_TOKEN =
|
|||||||
const retina =
|
const retina =
|
||||||
typeof window !== 'undefined' && window.devicePixelRatio >= 2 ? '@2x' : ''
|
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}`
|
`https://api.mapbox.com/styles/v1/mapbox/${mapboxId}/tiles/256/${z}/${x}/${y}${retina}?access_token=${accessToken}`
|
||||||
|
|
||||||
const providers = {
|
const providers = {
|
||||||
@ -28,38 +31,30 @@ const providers = {
|
|||||||
dark: mapbox('dark-v9', MAPBOX_ACCESS_TOKEN)
|
dark: mapbox('dark-v9', MAPBOX_ACCESS_TOKEN)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ExifMap extends PureComponent {
|
export default function ExifMap({
|
||||||
state = { zoom: 12 }
|
gps
|
||||||
|
}: {
|
||||||
|
gps: { latitude: string; longitude: string }
|
||||||
|
}) {
|
||||||
|
const [zoom, setZoom] = useState(12)
|
||||||
|
|
||||||
static propTypes = {
|
const zoomIn = () => {
|
||||||
gps: PropTypes.object
|
setZoom(Math.min(zoom + 4, 20))
|
||||||
}
|
}
|
||||||
|
|
||||||
zoomIn = () => {
|
const { latitude, longitude } = gps
|
||||||
this.setState({
|
|
||||||
zoom: Math.min(this.state.zoom + 4, 20)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const { latitude, longitude } = this.props.gps
|
<Map
|
||||||
|
center={[latitude, longitude]}
|
||||||
return (
|
zoom={zoom}
|
||||||
<Map
|
height={160}
|
||||||
center={[latitude, longitude]}
|
attribution={false}
|
||||||
zoom={this.state.zoom}
|
provider={providers['light']}
|
||||||
height={160}
|
metaWheelZoom={true}
|
||||||
attribution={false}
|
metaWheelZoomWarning={'META+wheel to zoom'}
|
||||||
provider={providers['light']}
|
>
|
||||||
metaWheelZoom={true}
|
<Marker anchor={[latitude, longitude]} payload={1} onClick={zoomIn} />
|
||||||
metaWheelZoomWarning={'META+wheel to zoom'}
|
</Map>
|
||||||
>
|
)
|
||||||
<Marker
|
|
||||||
anchor={[latitude, longitude]}
|
|
||||||
payload={1}
|
|
||||||
onClick={this.zoomIn}
|
|
||||||
/>
|
|
||||||
</Map>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -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
|
|
@ -2,77 +2,77 @@
|
|||||||
@import 'mixins';
|
@import 'mixins';
|
||||||
|
|
||||||
.hamburgerLine {
|
.hamburgerLine {
|
||||||
@include transition;
|
@include transition;
|
||||||
|
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: $text-color-light;
|
background: $text-color-light;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
left: 0;
|
left: 0;
|
||||||
transform: rotate(0deg);
|
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) {
|
&:nth-child(1) {
|
||||||
top: 0;
|
transform: rotate(45deg);
|
||||||
transform-origin: left center;
|
top: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(2) {
|
&:nth-child(2) {
|
||||||
top: 5px;
|
width: 0%;
|
||||||
transform-origin: left center;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(3) {
|
&:nth-child(3) {
|
||||||
top: 10px;
|
transform: rotate(-45deg);
|
||||||
transform-origin: left center;
|
top: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamburgerButton {
|
.hamburgerButton {
|
||||||
padding: .65rem .85rem;
|
padding: 0.65rem 0.85rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-right: -($spacer / 2);
|
margin-right: -($spacer / 2);
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
|
|
||||||
.hamburgerLine {
|
.hamburgerLine {
|
||||||
background: $brand-cyan;
|
background: $brand-cyan;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamburger {
|
.hamburger {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
19
src/components/atoms/Hamburger.tsx
Normal file
19
src/components/atoms/Hamburger.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
@ -1,20 +1,20 @@
|
|||||||
@import 'mixins';
|
@import 'mixins';
|
||||||
|
|
||||||
.imageWrap {
|
.imageWrap {
|
||||||
@include media-frame;
|
@include media-frame;
|
||||||
|
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
margin-bottom: $spacer;
|
margin-bottom: $spacer;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
@media (min-width: 940px) {
|
@media (min-width: 940px) {
|
||||||
max-width: 940px;
|
max-width: 940px;
|
||||||
border-radius: .25rem;
|
border-radius: 0.25rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover & {
|
a:hover & {
|
||||||
border-color: $link-color !important;
|
border-color: $link-color !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
40
src/components/atoms/Image.tsx
Normal file
40
src/components/atoms/Image.tsx
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
@ -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
|
|
@ -1,32 +1,32 @@
|
|||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: $padding-base-vertical $padding-base-horizontal;
|
padding: $padding-base-vertical $padding-base-horizontal;
|
||||||
font-size: $input-font-size;
|
font-size: $input-font-size;
|
||||||
font-weight: $input-font-weight;
|
font-weight: $input-font-weight;
|
||||||
line-height: $line-height;
|
line-height: $line-height;
|
||||||
color: $input-color;
|
color: $input-color;
|
||||||
background-color: $input-bg;
|
background-color: $input-bg;
|
||||||
background-image: none; // Reset unusual Firefox-on-Android default style
|
background-image: none; // Reset unusual Firefox-on-Android default style
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: $input-border-radius;
|
border-radius: $input-border-radius;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
transition: all ease-in-out .15s;
|
transition: all ease-in-out 0.15s;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: lighten($input-bg, 30%);
|
background: lighten($input-bg, 30%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
background-color: $input-bg-focus;
|
background-color: $input-bg-focus;
|
||||||
border-color: $input-border-focus;
|
border-color: $input-border-focus;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[disabled] {
|
&[disabled] {
|
||||||
color: $brand-grey-dimmed;
|
color: $brand-grey-dimmed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
src/components/atoms/Input.tsx
Normal file
6
src/components/atoms/Input.tsx
Normal 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} />
|
||||||
|
}
|
@ -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}>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</ReactModal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,111 +1,111 @@
|
|||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
background: rgba($body-background-color, .95);
|
background: rgba($body-background-color, 0.95);
|
||||||
backdrop-filter: blur(5px);
|
backdrop-filter: blur(5px);
|
||||||
animation: fadein .3s;
|
animation: fadein 0.3s;
|
||||||
padding: $spacer;
|
padding: $spacer;
|
||||||
|
|
||||||
@media (min-width: $screen-sm) {
|
@media (min-width: $screen-sm) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-top: 6vh;
|
padding-top: 6vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__content {
|
.modal__content {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
border: 1px solid rgba($brand-grey-light, .4);
|
border: 1px solid rgba($brand-grey-light, 0.4);
|
||||||
box-shadow: 0 5px 30px rgba($brand-grey-light, .2);
|
box-shadow: 0 5px 30px rgba($brand-grey-light, 0.2);
|
||||||
padding: 0 $spacer / 2 $spacer / 2;
|
padding: 0 $spacer / 2 $spacer / 2;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
@media (min-width: $screen-md) {
|
@media (min-width: $screen-md) {
|
||||||
max-width: $screen-sm;
|
max-width: $screen-sm;
|
||||||
padding: 0 $spacer $spacer;
|
padding: 0 $spacer $spacer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__close {
|
.modal__close {
|
||||||
display: block;
|
display: block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-size: $font-size-h2;
|
font-size: $font-size-h2;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: ($spacer/4);
|
right: ($spacer/4);
|
||||||
color: $brand-grey-light;
|
color: $brand-grey-light;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: $brand-grey;
|
color: $brand-grey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.isModalOpen {
|
.isModalOpen {
|
||||||
// Prevent background scrolling when modal is open
|
// Prevent background scrolling when modal is open
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
// more cross-browser backdrop-filter
|
// more cross-browser backdrop-filter
|
||||||
// body > div:first-child {
|
// body > div:first-child {
|
||||||
// transition: filter .85s ease-out;
|
// transition: filter .85s ease-out;
|
||||||
// filter: blur(5px);
|
// filter: blur(5px);
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__title {
|
.modal__title {
|
||||||
font-size: $font-size-h4;
|
font-size: $font-size-h4;
|
||||||
margin-top: $spacer / 2;
|
margin-top: $spacer / 2;
|
||||||
margin-bottom: $spacer / 2;
|
margin-bottom: $spacer / 2;
|
||||||
margin-left: -($spacer / 2);
|
margin-left: -($spacer / 2);
|
||||||
margin-right: -($spacer / 2);
|
margin-right: -($spacer / 2);
|
||||||
border-bottom: 1px solid rgba($brand-grey-light, .4);
|
border-bottom: 1px solid rgba($brand-grey-light, 0.4);
|
||||||
padding: 0 $spacer;
|
padding: 0 $spacer;
|
||||||
padding-bottom: ($spacer/2);
|
padding-bottom: ($spacer/2);
|
||||||
|
|
||||||
@media (min-width: $screen-md) {
|
@media (min-width: $screen-md) {
|
||||||
margin-left: -($spacer);
|
margin-left: -($spacer);
|
||||||
margin-right: -($spacer);
|
margin-right: -($spacer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Overlay/content animations
|
// Overlay/content animations
|
||||||
//
|
//
|
||||||
@keyframes fadein {
|
@keyframes fadein {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeout {
|
@keyframes fadeout {
|
||||||
0% {
|
0% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
38
src/components/atoms/Modal.tsx
Normal file
38
src/components/atoms/Modal.tsx
Normal 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}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</ReactModal>
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
|
@ -1,54 +1,54 @@
|
|||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
.qr {
|
.qr {
|
||||||
margin-bottom: $spacer / 2;
|
margin-bottom: $spacer / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code {
|
.code {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
padding-right: 2rem;
|
padding-right: 2rem;
|
||||||
|
|
||||||
code {
|
code {
|
||||||
padding: $spacer / 2;
|
padding: $spacer / 2;
|
||||||
font-size: .65rem;
|
font-size: 0.65rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
background: rgba($brand-grey, .3);
|
background: rgba($brand-grey, 0.3);
|
||||||
padding: $spacer / 3;
|
padding: $spacer / 3;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
fill: $brand-grey-light;
|
||||||
|
transition: 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
svg {
|
svg {
|
||||||
width: 1rem;
|
fill: $brand-grey-dimmed;
|
||||||
height: 1rem;
|
|
||||||
fill: $brand-grey-light;
|
|
||||||
transition: .15s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
svg {
|
|
||||||
fill: $brand-grey-dimmed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.copied {
|
.copied {
|
||||||
background: green;
|
background: green;
|
||||||
|
|
||||||
// stylelint-disable-next-line no-descending-specificity
|
// stylelint-disable-next-line no-descending-specificity
|
||||||
svg {
|
svg {
|
||||||
fill: $brand-grey-dimmed;
|
fill: $brand-grey-dimmed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
44
src/components/atoms/Qr.tsx
Normal file
44
src/components/atoms/Qr.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import { graphql, useStaticQuery } from 'gatsby'
|
||||||
import { StaticQuery, graphql } from 'gatsby'
|
|
||||||
import Helmet from 'react-helmet'
|
import Helmet from 'react-helmet'
|
||||||
|
|
||||||
const query = graphql`
|
const query = graphql`
|
||||||
@ -28,13 +27,13 @@ const query = graphql`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const createSchemaOrg = (
|
const createSchemaOrg = (
|
||||||
blogURL,
|
blogURL: string,
|
||||||
title,
|
title: string,
|
||||||
siteMeta,
|
siteMeta: any,
|
||||||
postSEO,
|
postSEO: boolean,
|
||||||
postURL,
|
postURL: string,
|
||||||
image,
|
image: string,
|
||||||
description
|
description: string
|
||||||
) => {
|
) => {
|
||||||
const schemaOrgJSONLD = [
|
const schemaOrgJSONLD = [
|
||||||
{
|
{
|
||||||
@ -89,6 +88,14 @@ const MetaTags = ({
|
|||||||
postSEO,
|
postSEO,
|
||||||
title,
|
title,
|
||||||
siteMeta
|
siteMeta
|
||||||
|
}: {
|
||||||
|
description: string
|
||||||
|
image: string
|
||||||
|
url: string
|
||||||
|
schema: string
|
||||||
|
postSEO: boolean
|
||||||
|
title: string
|
||||||
|
siteMeta: any
|
||||||
}) => (
|
}) => (
|
||||||
<Helmet
|
<Helmet
|
||||||
defaultTitle={`${siteMeta.siteTitle} ¦ ${siteMeta.siteDescription}`}
|
defaultTitle={`${siteMeta.siteTitle} ¦ ${siteMeta.siteDescription}`}
|
||||||
@ -130,76 +137,63 @@ const MetaTags = ({
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
)
|
)
|
||||||
|
|
||||||
MetaTags.propTypes = {
|
export default function SEO({
|
||||||
description: PropTypes.string,
|
post,
|
||||||
image: PropTypes.string,
|
slug,
|
||||||
url: PropTypes.string,
|
postSEO
|
||||||
schema: PropTypes.string,
|
}: {
|
||||||
postSEO: PropTypes.bool,
|
post?: any
|
||||||
title: PropTypes.string,
|
slug?: string
|
||||||
siteMeta: PropTypes.object
|
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
Loading…
Reference in New Issue
Block a user