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

Merge pull request #172 from kremalicious/feature/typescript

migrate to TypeScript
This commit is contained in:
Matthias Kretschmann 2019-10-02 21:57:42 +02:00 committed by GitHub
commit 25092036be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
157 changed files with 3785 additions and 3979 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,4 +1,6 @@
plugins/gatsby-redirect-from
node_modules
public
.cache
node_modules/
.cache/
static/
public/
coverage/

View File

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

View File

@ -1,4 +0,0 @@
node_modules/
.cache/
static/
public/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,8 +15,8 @@ const feedContent = edge => {
: `${html}${footer}`
}
const generateJsonFeed = async posts => {
const jsonItems = await posts.map(edge => {
async function jsonItems(posts) {
return await posts.map(edge => {
const { frontmatter, fields, excerpt } = edge.node
const { slug, date } = fields
@ -33,27 +33,29 @@ const generateJsonFeed = async posts => {
content_html: feedContent(edge)
}
})
}
const jsonFeed = {
version: 'https://jsonfeed.org/version/1',
title: siteTitle,
description: siteDescription,
home_page_url: siteUrl,
feed_url: path.join(siteUrl, 'feed.json'),
user_comment:
'This feed allows you to read the posts from this site in any feed reader that supports the JSON Feed format. To add this feed to your reader, copy the following URL — https://kremalicious.com/feed.json — and add it your reader.',
favicon: path.join(siteUrl, 'favicon.ico'),
icon: path.join(siteUrl, 'apple-touch-icon.png'),
author: {
name: author.name,
url: author.uri
},
items: jsonItems
}
const createJsonFeed = posts => ({
version: 'https://jsonfeed.org/version/1',
title: siteTitle,
description: siteDescription,
home_page_url: siteUrl,
feed_url: path.join(siteUrl, 'feed.json'),
user_comment:
'This feed allows you to read the posts from this site in any feed reader that supports the JSON Feed format. To add this feed to your reader, copy the following URL — https://kremalicious.com/feed.json — and add it your reader.',
favicon: path.join(siteUrl, 'favicon.ico'),
icon: path.join(siteUrl, 'apple-touch-icon.png'),
author: {
name: author.name,
url: author.uri
},
items: jsonItems(posts)
})
const generateJsonFeed = async posts => {
await writeFile(
path.join('./public', 'feed.json'),
JSON.stringify(jsonFeed),
JSON.stringify(createJsonFeed(posts)),
'utf8'
).catch(err => {
throw Error('\nFailed to write JSON Feed file: ', err)

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@ -0,0 +1,99 @@
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'
import { useSiteMetadata } from '../../hooks/use-site-metadata'
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 = ({ slug }: { slug: string }) => {
const { siteUrl } = useSiteMetadata()
return (
<a
className={styles.action}
href={`https://twitter.com/intent/tweet?text=@kremalicious&url=${siteUrl}${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,
githubLink
}: {
slug: string
githubLink: string
}) {
const [showModal, setShowModal] = useState(false)
const toggleModal = () => {
setShowModal(!showModal)
}
return (
<aside className={styles.actions}>
<div>
<ActionTwitter slug={slug} />
</div>
<div>
<ActionCrypto toggleModal={toggleModal} />
</div>
<div>
<ActionGitHub githubLink={githubLink} />
</div>
{showModal && (
<ModalThanks isOpen={showModal} handleCloseModal={toggleModal} />
)}
</aside>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +1,21 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Link } from 'gatsby'
import Time from 'react-time'
import slugify from 'slugify'
import styles from './PostMeta.module.scss'
import { useSiteMetadata } from '../../hooks/use-site-metadata'
const PostMeta = ({ post, meta }) => {
const PostMeta = ({ post }: { post: any }) => {
const { author, updated, tags, type } = post.frontmatter
const siteMeta = useSiteMetadata()
const { date } = post.fields
return (
<footer className={styles.entryMeta}>
<div className={styles.byline}>
<span className={styles.by}>by</span>
<a className="fn" rel="author" href={meta.author.uri}>
{author || meta.author.name}
<a className="fn" rel="author" href={siteMeta.author.uri}>
{author || siteMeta.author.name}
</a>
</div>
@ -40,7 +41,7 @@ const PostMeta = ({ post, meta }) => {
{tags && (
<div className={styles.tags}>
{tags.map(tag => {
{tags.map((tag: string) => {
const to = tag === 'goodies' ? '/goodies' : `/tags/${slugify(tag)}/`
return (
@ -55,9 +56,4 @@ const PostMeta = ({ post, meta }) => {
)
}
PostMeta.propTypes = {
post: PropTypes.object.isRequired,
meta: PropTypes.object.isRequired
}
export default PostMeta

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,84 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Helmet from 'react-helmet'
import { CSSTransition } from 'react-transition-group'
import SearchInput from './SearchInput'
import SearchButton from './SearchButton'
import SearchResults from './SearchResults'
import styles from './Search.module.scss'
export default class Search extends PureComponent {
state = {
searchOpen: false,
query: '',
results: []
}
static propTypes = {
lng: PropTypes.string.isRequired
}
toggleSearch = () => {
this.setState(prevState => ({
searchOpen: !prevState.searchOpen
}))
}
getSearchResults(query) {
if (!query || !window.__LUNR__) return []
const lunrIndex = window.__LUNR__[this.props.lng]
const results = lunrIndex.index.search(query)
return results.map(({ ref }) => lunrIndex.store[ref])
}
search = event => {
const query = event.target.value
// wildcard search https://lunrjs.com/guides/searching.html#wildcards
const results = query.length > 1 ? this.getSearchResults(`${query}*`) : []
this.setState({
results,
query
})
}
render() {
const { searchOpen, query, results } = this.state
return (
<>
<SearchButton onClick={this.toggleSearch} />
{searchOpen && (
<>
<Helmet>
<body className="hasSearchOpen" />
</Helmet>
<CSSTransition
appear={searchOpen}
in={searchOpen}
timeout={200}
classNames={styles}
>
<section className={styles.search}>
<SearchInput
value={query}
onChange={this.search}
onToggle={this.toggleSearch}
/>
</section>
</CSSTransition>
<SearchResults
searchQuery={query}
results={results}
toggleSearch={this.toggleSearch}
/>
</>
)}
</>
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,70 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { graphql, useStaticQuery } 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
}) {
const data = useStaticQuery(query)
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 }: { node: any }) => (
<PostTeaser
key={page.slug}
post={node}
toggleSearch={toggleSearch}
/>
))
)}
</ul>
) : (
<SearchResultsEmpty searchQuery={searchQuery} results={results} />
)}
</Container>
</div>,
document.getElementById('document')
)
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,45 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Search from '.'
import { useStaticQuery } from 'gatsby'
describe('Search', () => {
beforeEach(() => {
useStaticQuery.mockImplementation(() => {
return {
allMarkdownRemark: {
edges: [
{
node: {
id: 'ddd',
frontmatter: {
title: 'Hello',
image: {
childImageSharp: 'hello'
}
},
fields: {
slug: '/hello/'
}
}
}
]
}
}
})
const portalRoot = document.createElement('div')
portalRoot.setAttribute('id', 'document')
document.body.appendChild(portalRoot)
})
it('can be opened', () => {
const { getByTitle, getByPlaceholderText } = render(<Search lng="en" />)
fireEvent.click(getByTitle('Search'))
fireEvent.change(getByPlaceholderText('Search everything'), {
target: { value: 'hello' }
})
fireEvent.click(getByTitle('Close search'))
})
})

View File

@ -0,0 +1,68 @@
import React, { useState } from 'react'
import Helmet from 'react-helmet'
import { CSSTransition } from 'react-transition-group'
import SearchInput from './SearchInput'
import SearchButton from './SearchButton'
import SearchResults from './SearchResults'
import styles from './index.module.scss'
function getSearchResults(query: string, lng: string) {
if (!query || !window.__LUNR__) return []
const lunrIndex = window.__LUNR__[lng]
const results = lunrIndex.index.search(query)
return results.map(({ ref }: { ref: string }) => lunrIndex.store[ref])
}
export default function Search({ lng }: { lng: string }) {
const [searchOpen, setSearchOpen] = useState(false)
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const toggleSearch = () => {
setSearchOpen(!searchOpen)
}
const search = (event: any) => {
const query = event.target.value
// wildcard search https://lunrjs.com/guides/searching.html#wildcards
const results = query.length > 1 ? getSearchResults(`${query}*`, lng) : []
setQuery(query)
setResults(results)
}
return (
<>
<SearchButton onClick={toggleSearch} />
{searchOpen && (
<>
<Helmet>
<body className="hasSearchOpen" />
</Helmet>
<CSSTransition
appear={searchOpen}
in={searchOpen}
timeout={200}
classNames={styles}
>
<section className={styles.search}>
<SearchInput
value={query}
onChange={(e: Event) => search(e)}
onToggle={toggleSearch}
/>
</section>
</CSSTransition>
<SearchResults
searchQuery={query}
results={results}
toggleSearch={toggleSearch}
/>
</>
)}
</>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,5 +5,5 @@ import testRender from '../../../jest/testRender'
import Hamburger from './Hamburger'
describe('Hamburger', () => {
testRender(<Hamburger />)
testRender(<Hamburger onClick={() => null} />)
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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