1
0
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:
Matthias Kretschmann 2019-10-02 13:35:50 +02:00
parent 6d502cf313
commit 0946b30b67
Signed by: m
GPG Key ID: 606EEEF3C479A91F
149 changed files with 3465 additions and 3614 deletions

View File

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

View File

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

View File

@ -1,34 +1,36 @@
{ {
"parser": "babel-eslint", "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"
}
}
} }

View File

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

View File

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

View File

@ -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
} }
} }

View File

@ -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='

View File

@ -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;
} }

View File

@ -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'
] ]
} }

View File

@ -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

View File

@ -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 => {

View File

@ -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/**/*']
} }

View File

@ -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)

View File

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

View File

@ -1,6 +1,7 @@
import { render } from '@testing-library/react' import { 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)

View File

@ -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
View File

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

@ -2,19 +2,19 @@
@import 'mixins'; @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
View File

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

View File

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

View File

@ -2,95 +2,95 @@
@import 'mixins'; @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;
} }
} }

View File

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

View File

@ -1,9 +1,8 @@
import React, { Fragment } from 'react' import 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

View File

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

View File

@ -2,43 +2,43 @@
@import 'mixins'; @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);
}
} }
}
} }

View File

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

View File

@ -1,10 +1,10 @@
@import 'variables'; @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;
} }

View File

@ -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

View File

@ -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;
}
}
}
} }

View File

@ -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

View File

@ -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;
} }
} }

View File

@ -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

View File

@ -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);
}
} }
}
} }

View File

@ -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

View File

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

View File

@ -1,29 +1,29 @@
@import 'variables'; @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;
} }
} }

View File

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

View File

@ -5,32 +5,32 @@
///////////////////////////////////// /////////////////////////////////////
.hentry__title { .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;
} }

View File

@ -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

View File

@ -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;
} }

View File

@ -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>

View File

@ -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%);
}
} }
}
} }

View File

@ -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"

View File

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

View File

@ -1,27 +1,27 @@
@import 'variables'; @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;
} }
} }

View File

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

View File

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

View File

@ -2,74 +2,74 @@
@import 'mixins'; @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;
} }
} }

View File

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

View File

@ -1,34 +1,34 @@
@import 'variables'; @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;
} }
} }

View File

@ -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

View File

@ -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;
} }

View File

@ -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

View File

@ -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;
} }
} }

View File

@ -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() }} />
/> )
)
}
} }

View File

@ -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;
} }
} }

View File

@ -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) {

View File

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

View File

@ -2,97 +2,97 @@
@import 'mixins'; @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;
} }
} }

View File

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

View File

@ -2,60 +2,60 @@
@import 'mixins'; @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;
} }
} }

View File

@ -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)
}
}

View File

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

View File

@ -1,69 +1,69 @@
@import 'variables'; @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;
}
} }
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,57 +2,57 @@
@import 'mixins'; @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;
} }

View File

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

View File

@ -1,5 +1,4 @@
import React, { PureComponent } from 'react' import 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>
)
}
} }

View File

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

View File

@ -2,77 +2,77 @@
@import 'mixins'; @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;
} }

View File

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

View File

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

View File

@ -1,20 +1,20 @@
@import 'mixins'; @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;
} }
} }

View File

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

View File

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

View File

@ -1,32 +1,32 @@
@import 'variables'; @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;
} }
} }

View File

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

View File

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

View File

@ -1,111 +1,111 @@
@import 'variables'; @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;
} }
} }

View File

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

View File

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

View File

@ -1,54 +1,54 @@
@import 'variables'; @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;
} }
} }

View File

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

View File

@ -1,6 +1,5 @@
import React from 'react' import 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