mirror of
https://github.com/kremalicious/blog.git
synced 2024-12-22 17:23:50 +01:00
Merge pull request #172 from kremalicious/feature/typescript
migrate to TypeScript
This commit is contained in:
commit
25092036be
@ -1,5 +0,0 @@
|
||||
version: '2'
|
||||
checks:
|
||||
method-lines:
|
||||
config:
|
||||
threshold: 55 # Gatsby's StaticQuery makes render functions pretty long
|
@ -1,14 +0,0 @@
|
||||
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.scss]
|
||||
indent_size = 4
|
@ -1,4 +1,6 @@
|
||||
plugins/gatsby-redirect-from
|
||||
node_modules
|
||||
public
|
||||
.cache
|
||||
node_modules/
|
||||
.cache/
|
||||
static/
|
||||
public/
|
||||
coverage/
|
60
.eslintrc
60
.eslintrc
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,4 +0,0 @@
|
||||
node_modules/
|
||||
.cache/
|
||||
static/
|
||||
public/
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none"
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
16
.travis.yml
16
.travis.yml
@ -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='
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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'
|
||||
]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 => {
|
||||
|
@ -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)
|
||||
|
@ -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/**/*']
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -1 +1 @@
|
||||
import '@testing-library/jest-dom/extend-expect'
|
||||
require('@testing-library/jest-dom/extend-expect')
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { render } from '@testing-library/react'
|
||||
import { ReactElement } from 'react'
|
||||
|
||||
const testRender = component => {
|
||||
const testRender = (component: ReactElement) => {
|
||||
it('renders without crashing', () => {
|
||||
const { container } = render(component)
|
||||
|
90
package.json
90
package.json
@ -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
13
src/@types/declarations.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
declare module '*.scss' {
|
||||
const content: { [className: string]: string }
|
||||
export = content
|
||||
}
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-empty-interface */
|
||||
interface SvgrComponent
|
||||
extends React.StatelessComponent<React.SVGAttributes<SVGElement>> {}
|
||||
|
||||
declare module '*.svg' {
|
||||
const value: SvgrComponent
|
||||
export default value
|
||||
}
|
1
src/@types/pigeon-maps.d.ts
vendored
Normal file
1
src/@types/pigeon-maps.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'pigeon-maps'
|
1
src/@types/pigeon-marker.d.ts
vendored
Normal file
1
src/@types/pigeon-marker.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'pigeon-marker'
|
1
src/@types/react-blockies.d.ts
vendored
Normal file
1
src/@types/react-blockies.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'react-blockies'
|
1
src/@types/react-time.d.ts
vendored
Normal file
1
src/@types/react-time.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'react-time'
|
1
src/@types/remark-react.d.ts
vendored
Normal file
1
src/@types/remark-react.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'remark-react'
|
@ -1,33 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Container from './atoms/Container'
|
||||
import Typekit from './atoms/Typekit'
|
||||
import Header from './organisms/Header'
|
||||
import Footer from './organisms/Footer'
|
||||
import styles from './Layout.module.scss'
|
||||
|
||||
// if (process.env.NODE_ENV !== 'production') {
|
||||
// const { whyDidYouUpdate } = require('why-did-you-update')
|
||||
// whyDidYouUpdate(React)
|
||||
// }
|
||||
|
||||
const Layout = ({ children }) => (
|
||||
<>
|
||||
<Typekit />
|
||||
<Header />
|
||||
|
||||
<main className={styles.document} id="document">
|
||||
<div className={styles.content}>
|
||||
<Container>{children}</Container>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
|
||||
Layout.propTypes = {
|
||||
children: PropTypes.any.isRequired
|
||||
}
|
||||
|
||||
export default Layout
|
@ -2,19 +2,19 @@
|
||||
@import 'mixins';
|
||||
|
||||
#___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
33
src/components/Layout.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import Container from './atoms/Container'
|
||||
import Typekit from './atoms/Typekit'
|
||||
import Header from './organisms/Header'
|
||||
import Footer from './organisms/Footer'
|
||||
import styles from './Layout.module.scss'
|
||||
|
||||
// if (process.env.NODE_ENV !== 'production') {
|
||||
// const { whyDidYouUpdate } = require('why-did-you-update')
|
||||
// whyDidYouUpdate(React)
|
||||
// }
|
||||
|
||||
export default function Layout({
|
||||
children
|
||||
}: {
|
||||
location?: Location
|
||||
children: any
|
||||
}): ReactElement {
|
||||
return (
|
||||
<>
|
||||
<Typekit />
|
||||
<Header />
|
||||
|
||||
<main className={styles.document} id="document">
|
||||
<div className={styles.content}>
|
||||
<Container>{children}</Container>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ModalThanks from '../molecules/ModalThanks'
|
||||
import styles from './PostActions.module.scss'
|
||||
|
||||
import { ReactComponent as Twitter } from '../../images/twitter.svg'
|
||||
import { ReactComponent as Bitcoin } from '../../images/bitcoin.svg'
|
||||
import { ReactComponent as GitHub } from '../../images/github.svg'
|
||||
|
||||
const ActionContent = ({ title, text, textLink }) => (
|
||||
<>
|
||||
<h1 className={styles.actionTitle}>{title}</h1>
|
||||
<p className={styles.actionText}>
|
||||
{text} <span className={styles.link}>{textLink}</span>
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
|
||||
ActionContent.propTypes = {
|
||||
title: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
textLink: PropTypes.string
|
||||
}
|
||||
|
||||
const ActionTwitter = ({ url, slug }) => (
|
||||
<a
|
||||
className={styles.action}
|
||||
href={`https://twitter.com/intent/tweet?text=@kremalicious&url=${url}${slug}`}
|
||||
>
|
||||
<Twitter />
|
||||
<ActionContent
|
||||
title="Have a comment?"
|
||||
text="Hit me up"
|
||||
textLink="@kremalicious"
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
|
||||
ActionTwitter.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
slug: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
const ActionCrypto = ({ toggleModal }) => (
|
||||
<button className={styles.action} onClick={toggleModal}>
|
||||
<Bitcoin />
|
||||
<ActionContent
|
||||
title="Found something useful?"
|
||||
text="Say thanks with"
|
||||
textLink="Bitcoins or Ether"
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
|
||||
ActionCrypto.propTypes = {
|
||||
toggleModal: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
const ActionGitHub = ({ githubLink }) => (
|
||||
<a className={styles.action} href={githubLink}>
|
||||
<GitHub />
|
||||
<ActionContent
|
||||
title="Edit on GitHub"
|
||||
text="Contribute to this post on"
|
||||
textLink="GitHub"
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
|
||||
ActionGitHub.propTypes = {
|
||||
githubLink: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
export default class PostActions extends PureComponent {
|
||||
state = {
|
||||
showModal: false
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
slug: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
githubLink: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
toggleModal = () => {
|
||||
this.setState({ showModal: !this.state.showModal })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { url, slug, githubLink } = this.props
|
||||
|
||||
return (
|
||||
<aside className={styles.actions}>
|
||||
<div>
|
||||
<ActionTwitter url={url} slug={slug} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ActionCrypto toggleModal={this.toggleModal} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ActionGitHub githubLink={githubLink} />
|
||||
</div>
|
||||
|
||||
{this.state.showModal && (
|
||||
<ModalThanks
|
||||
isOpen={this.state.showModal}
|
||||
handleCloseModal={this.toggleModal}
|
||||
/>
|
||||
)}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
}
|
@ -2,95 +2,95 @@
|
||||
@import 'mixins';
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
99
src/components/Post/PostActions.tsx
Normal file
99
src/components/Post/PostActions.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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
|
@ -1,26 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Image from '../atoms/Image'
|
||||
import styles from './PostImage.module.scss'
|
||||
|
||||
const PostImage = ({ title, fluid, fixed, alt }) => (
|
||||
<figure className={styles.postImage}>
|
||||
<Image
|
||||
fluid={fluid ? fluid : null}
|
||||
fixed={fixed ? fixed : null}
|
||||
alt={alt}
|
||||
/>
|
||||
{title && (
|
||||
<figcaption className={styles.postImageTitle}>{title}</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
)
|
||||
|
||||
PostImage.propTypes = {
|
||||
fluid: PropTypes.object,
|
||||
fixed: PropTypes.object,
|
||||
alt: PropTypes.string.isRequired,
|
||||
title: PropTypes.string
|
||||
}
|
||||
|
||||
export default PostImage
|
@ -2,43 +2,43 @@
|
||||
@import 'mixins';
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
22
src/components/Post/PostImage.tsx
Normal file
22
src/components/Post/PostImage.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import Image from '../atoms/Image'
|
||||
import styles from './PostImage.module.scss'
|
||||
import { FluidObject, FixedObject } from 'gatsby-image'
|
||||
|
||||
interface PostImageProps {
|
||||
title?: string
|
||||
fluid?: FluidObject
|
||||
fixed?: FixedObject
|
||||
alt: string
|
||||
}
|
||||
|
||||
const PostImage = ({ title, fluid, fixed, alt }: PostImageProps) => (
|
||||
<figure className={styles.postImage}>
|
||||
<Image fluid={fluid} fixed={fixed} alt={alt} />
|
||||
{title && (
|
||||
<figcaption className={styles.postImageTitle}>{title}</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
)
|
||||
|
||||
export default PostImage
|
@ -1,10 +1,10 @@
|
||||
@import 'variables';
|
||||
|
||||
.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;
|
||||
}
|
||||
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
@ -1,36 +0,0 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Link } from 'gatsby'
|
||||
import Image from '../atoms/Image'
|
||||
import styles from './PostTeaser.module.scss'
|
||||
|
||||
export default class PostTeaser extends PureComponent {
|
||||
static propTypes = {
|
||||
post: PropTypes.object.isRequired,
|
||||
toggleSearch: PropTypes.func
|
||||
}
|
||||
|
||||
render() {
|
||||
const { post, toggleSearch } = this.props
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Link to={post.fields.slug} onClick={toggleSearch && toggleSearch}>
|
||||
{post.frontmatter.image ? (
|
||||
<>
|
||||
<Image
|
||||
fluid={post.frontmatter.image.childImageSharp.fluid}
|
||||
alt={post.frontmatter.title}
|
||||
/>
|
||||
<h4 className={styles.postTitle}>{post.frontmatter.title}</h4>
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.empty}>
|
||||
<h4 className={styles.postTitle}>{post.frontmatter.title}</h4>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,29 +1,29 @@
|
||||
@import 'variables';
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
32
src/components/Post/PostTeaser.tsx
Normal file
32
src/components/Post/PostTeaser.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
import Image from '../atoms/Image'
|
||||
import styles from './PostTeaser.module.scss'
|
||||
|
||||
export default function PostTeaser({
|
||||
post,
|
||||
toggleSearch
|
||||
}: {
|
||||
post: { fields: { slug: string }; frontmatter: { image: any; title: string } }
|
||||
toggleSearch?: () => void
|
||||
}) {
|
||||
const { image, title } = post.frontmatter
|
||||
const { slug } = post.fields
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Link to={slug} onClick={toggleSearch && toggleSearch}>
|
||||
{image ? (
|
||||
<>
|
||||
<Image fluid={image.childImageSharp.fluid} alt={title} />
|
||||
<h4 className={styles.postTitle}>{title}</h4>
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.empty}>
|
||||
<h4 className={styles.postTitle}>{title}</h4>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
}
|
@ -5,32 +5,32 @@
|
||||
/////////////////////////////////////
|
||||
|
||||
.hentry__title {
|
||||
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;
|
||||
}
|
||||
|
@ -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
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
@ -1,31 +0,0 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Input from '../atoms/Input'
|
||||
import styles from './SearchInput.module.scss'
|
||||
|
||||
export default class SearchInput extends PureComponent {
|
||||
static propTypes = {
|
||||
onToggle: PropTypes.func
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
className={styles.searchInput}
|
||||
type="search"
|
||||
placeholder="Search everything"
|
||||
autoFocus // eslint-disable-line
|
||||
{...this.props}
|
||||
/>
|
||||
<button
|
||||
className={styles.searchInputClose}
|
||||
onClick={this.props.onToggle}
|
||||
title="Close search"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,27 +1,27 @@
|
||||
@import 'variables';
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
33
src/components/Search/SearchInput.tsx
Normal file
33
src/components/Search/SearchInput.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
import Input from '../atoms/Input'
|
||||
import styles from './SearchInput.module.scss'
|
||||
|
||||
export default function SearchInput({
|
||||
value,
|
||||
onToggle,
|
||||
onChange
|
||||
}: {
|
||||
value: string
|
||||
onToggle(): void
|
||||
onChange(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"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import { graphql, StaticQuery } from 'gatsby'
|
||||
import Container from '../atoms/Container'
|
||||
import PostTeaser from '../Post/PostTeaser'
|
||||
import SearchResultsEmpty from './SearchResultsEmpty'
|
||||
import styles from './SearchResults.module.scss'
|
||||
|
||||
const query = graphql`
|
||||
query {
|
||||
allMarkdownRemark {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
frontmatter {
|
||||
title
|
||||
image {
|
||||
childImageSharp {
|
||||
...ImageFluidThumb
|
||||
}
|
||||
}
|
||||
}
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default class SearchResults extends PureComponent {
|
||||
static propTypes = {
|
||||
results: PropTypes.array.isRequired,
|
||||
searchQuery: PropTypes.string.isRequired,
|
||||
toggleSearch: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
const { searchQuery, results, toggleSearch } = this.props
|
||||
|
||||
return (
|
||||
<StaticQuery
|
||||
query={query}
|
||||
render={data => {
|
||||
const posts = data.allMarkdownRemark.edges
|
||||
|
||||
// creating portal to break out of DOM node we're in
|
||||
// and render the results in content container
|
||||
return ReactDOM.createPortal(
|
||||
<div className={styles.searchResults}>
|
||||
<Container>
|
||||
{results.length > 0 ? (
|
||||
<ul>
|
||||
{results.map(page =>
|
||||
posts
|
||||
.filter(post => post.node.fields.slug === page.slug)
|
||||
.map(({ node }) => (
|
||||
<PostTeaser
|
||||
key={page.slug}
|
||||
post={node}
|
||||
toggleSearch={toggleSearch}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
) : (
|
||||
<SearchResultsEmpty
|
||||
searchQuery={searchQuery}
|
||||
results={results}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</div>,
|
||||
document.getElementById('document')
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
@ -2,74 +2,74 @@
|
||||
@import 'mixins';
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
70
src/components/Search/SearchResults.tsx
Normal file
70
src/components/Search/SearchResults.tsx
Normal 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')
|
||||
)
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
46
src/components/Search/index.module.scss
Normal file
46
src/components/Search/index.module.scss
Normal 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;
|
||||
}
|
45
src/components/Search/index.test.tsx
Normal file
45
src/components/Search/index.test.tsx
Normal 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'))
|
||||
})
|
||||
})
|
68
src/components/Search/index.tsx
Normal file
68
src/components/Search/index.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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() }}
|
||||
/>
|
||||
)
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
@ -1,48 +0,0 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Input from '../atoms/Input'
|
||||
import Account from './Account'
|
||||
import Conversion from './Conversion'
|
||||
import styles from './InputGroup.module.scss'
|
||||
|
||||
export default class InputGroup extends PureComponent {
|
||||
static propTypes = {
|
||||
amount: PropTypes.string.isRequired,
|
||||
onAmountChange: PropTypes.func.isRequired,
|
||||
sendTransaction: PropTypes.func.isRequired,
|
||||
selectedAccount: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
amount,
|
||||
onAmountChange,
|
||||
sendTransaction,
|
||||
selectedAccount
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div className={styles.inputGroup}>
|
||||
<div className={styles.input}>
|
||||
<Input
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={onAmountChange}
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
<div className={styles.currency}>
|
||||
<span>ETH</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={sendTransaction}>
|
||||
Make it rain
|
||||
</button>
|
||||
<div className={styles.infoline}>
|
||||
<Conversion amount={amount} />
|
||||
{selectedAccount && <Account account={selectedAccount} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -2,97 +2,97 @@
|
||||
@import 'mixins';
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
41
src/components/Web3Donation/InputGroup.tsx
Normal file
41
src/components/Web3Donation/InputGroup.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import Input from '../atoms/Input'
|
||||
import Account from './Account'
|
||||
import Conversion from './Conversion'
|
||||
import styles from './InputGroup.module.scss'
|
||||
|
||||
export default function InputGroup({
|
||||
amount,
|
||||
onAmountChange,
|
||||
sendTransaction,
|
||||
selectedAccount
|
||||
}: {
|
||||
amount: string
|
||||
onAmountChange(target: any): void
|
||||
sendTransaction(): void
|
||||
selectedAccount?: string | null
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.inputGroup}>
|
||||
<div className={styles.input}>
|
||||
<Input
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={onAmountChange}
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
<div className={styles.currency}>
|
||||
<span>ETH</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={sendTransaction}>
|
||||
Make it rain
|
||||
</button>
|
||||
<div className={styles.infoline}>
|
||||
<Conversion amount={amount} />
|
||||
{selectedAccount && <Account account={selectedAccount} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -2,60 +2,60 @@
|
||||
@import 'mixins';
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { StaticQuery, graphql } from 'gatsby'
|
||||
import remark from 'remark'
|
||||
import remarkReact from 'remark-react'
|
||||
import styles from './Changelog.module.scss'
|
||||
|
||||
const queryGithub = graphql`
|
||||
query GitHubReposInfo {
|
||||
github {
|
||||
viewer {
|
||||
repositories(first: 100, privacy: PUBLIC, isFork: false) {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
url
|
||||
owner {
|
||||
login
|
||||
}
|
||||
object(expression: "master:CHANGELOG.md") {
|
||||
id
|
||||
... on GitHub_Blob {
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Changelog = ({ repo }) => (
|
||||
<StaticQuery
|
||||
query={queryGithub}
|
||||
render={data => {
|
||||
const repositoriesGitHub = data.github.viewer.repositories.edges
|
||||
|
||||
let repoFilteredArray = repositoriesGitHub
|
||||
.map(({ node }) => {
|
||||
if (node.name === repo) return node
|
||||
})
|
||||
.filter(n => n)
|
||||
|
||||
const repoMatch = repoFilteredArray[0]
|
||||
const { object, url, owner } = repoMatch
|
||||
|
||||
if (repoMatch === undefined || object === undefined) return null
|
||||
|
||||
const changelogHtml =
|
||||
object &&
|
||||
remark()
|
||||
.use(remarkReact)
|
||||
.processSync(object.text).contents
|
||||
|
||||
const filePathUrl = `${url}/tree/master/CHANGELOG.md`
|
||||
const filePathDisplay = `${owner.login}/${repo}:CHANGELOG.md`
|
||||
|
||||
return (
|
||||
<div className={styles.changelog}>
|
||||
<h2 className={styles.changelogTitle} id="changelog">
|
||||
Changelog
|
||||
</h2>
|
||||
<div className={styles.changelogContent}>
|
||||
{changelogHtml}
|
||||
<p className={styles.changelogSource}>
|
||||
<em>
|
||||
sourced from{' '}
|
||||
<a href={filePathUrl}>
|
||||
<code>{filePathDisplay}</code>
|
||||
</a>
|
||||
</em>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
Changelog.propTypes = {
|
||||
repo: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
export default Changelog
|
@ -1,69 +1,69 @@
|
||||
@import 'variables';
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
75
src/components/atoms/Changelog.tsx
Normal file
75
src/components/atoms/Changelog.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import React from 'react'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
import remark from 'remark'
|
||||
import remarkReact from 'remark-react'
|
||||
import styles from './Changelog.module.scss'
|
||||
|
||||
const queryGithub = graphql`
|
||||
query GitHubReposInfo {
|
||||
github {
|
||||
viewer {
|
||||
repositories(first: 100, privacy: PUBLIC, isFork: false) {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
url
|
||||
owner {
|
||||
login
|
||||
}
|
||||
object(expression: "master:CHANGELOG.md") {
|
||||
id
|
||||
... on GitHub_Blob {
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function Changelog({ repo }: { repo: string }) {
|
||||
const data = useStaticQuery(queryGithub)
|
||||
const repositoriesGitHub = data.github.viewer.repositories.edges
|
||||
|
||||
const repoFilteredArray = repositoriesGitHub
|
||||
.map(({ node }: { node: any }) => {
|
||||
if (node.name === repo) return node
|
||||
})
|
||||
.filter((n: any) => n)
|
||||
|
||||
const repoMatch = repoFilteredArray[0]
|
||||
const { object, url, owner } = repoMatch
|
||||
|
||||
if (repoMatch === undefined || object === undefined) return null
|
||||
|
||||
const changelogHtml =
|
||||
object &&
|
||||
remark()
|
||||
.use(remarkReact)
|
||||
.processSync(object.text).contents
|
||||
|
||||
const filePathUrl = `${url}/tree/master/CHANGELOG.md`
|
||||
const filePathDisplay = `${owner.login}/${repo}:CHANGELOG.md`
|
||||
|
||||
return (
|
||||
<div className={styles.changelog}>
|
||||
<h2 className={styles.changelogTitle} id="changelog">
|
||||
Changelog
|
||||
</h2>
|
||||
<div className={styles.changelogContent}>
|
||||
{changelogHtml}
|
||||
<p className={styles.changelogSource}>
|
||||
<em>
|
||||
sourced from{' '}
|
||||
<a href={filePathUrl}>
|
||||
<code>{filePathDisplay}</code>
|
||||
</a>
|
||||
</em>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styles from './Container.module.scss'
|
||||
|
||||
const Container = ({ children }) => (
|
||||
<section className={styles.container}>{children}</section>
|
||||
)
|
||||
|
||||
Container.propTypes = {
|
||||
children: PropTypes.any.isRequired
|
||||
}
|
||||
|
||||
export default Container
|
@ -1,5 +1,5 @@
|
||||
.container {
|
||||
max-width: 35rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 35rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
10
src/components/atoms/Container.tsx
Normal file
10
src/components/atoms/Container.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import styles from './Container.module.scss'
|
||||
|
||||
export default function Container({
|
||||
children
|
||||
}: {
|
||||
children: any
|
||||
}): ReactElement {
|
||||
return <section className={styles.container}>{children}</section>
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ExifMap from './ExifMap'
|
||||
import styles from './Exif.module.scss'
|
||||
|
||||
export default class Exif extends PureComponent {
|
||||
static propTypes = {
|
||||
exif: PropTypes.object
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
iso,
|
||||
model,
|
||||
fstop,
|
||||
shutterspeed,
|
||||
focalLength,
|
||||
exposure,
|
||||
gps
|
||||
} = this.props.exif
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside className={styles.exif}>
|
||||
<div className={styles.data}>
|
||||
{model && <span title="Camera model">{model}</span>}
|
||||
{fstop && <span title="Aperture">{fstop}</span>}
|
||||
{shutterspeed && <span title="Shutter speed">{shutterspeed}</span>}
|
||||
{exposure && <span title="Exposure">{exposure}</span>}
|
||||
{iso && <span title="ISO">{iso}</span>}
|
||||
{focalLength && <span title="Focal length">{focalLength}</span>}
|
||||
</div>
|
||||
<div className={styles.map}>{gps && <ExifMap gps={gps} />}</div>
|
||||
</aside>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
@ -2,57 +2,57 @@
|
||||
@import 'mixins';
|
||||
|
||||
.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;
|
||||
}
|
||||
|
34
src/components/atoms/Exif.tsx
Normal file
34
src/components/atoms/Exif.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import ExifMap from './ExifMap'
|
||||
import styles from './Exif.module.scss'
|
||||
|
||||
interface ExifProps {
|
||||
iso: string
|
||||
model: string
|
||||
fstop: string
|
||||
shutterspeed: string
|
||||
focalLength: string
|
||||
exposure: string
|
||||
gps: {
|
||||
latitude: string
|
||||
longitude: string
|
||||
}
|
||||
}
|
||||
|
||||
export default function Exif({ exif }: { exif: ExifProps }) {
|
||||
const { iso, model, fstop, shutterspeed, focalLength, exposure, gps } = exif
|
||||
|
||||
return (
|
||||
<aside className={styles.exif}>
|
||||
<div className={styles.data}>
|
||||
{model && <span title="Camera model">{model}</span>}
|
||||
{fstop && <span title="Aperture">{fstop}</span>}
|
||||
{shutterspeed && <span title="Shutter speed">{shutterspeed}</span>}
|
||||
{exposure && <span title="Exposure">{exposure}</span>}
|
||||
{iso && <span title="ISO">{iso}</span>}
|
||||
{focalLength && <span title="Focal length">{focalLength}</span>}
|
||||
</div>
|
||||
<div className={styles.map}>{gps && <ExifMap gps={gps} />}</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import 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>
|
||||
)
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import React from 'react'
|
||||
import styles from './Hamburger.module.scss'
|
||||
|
||||
const Hamburger = props => (
|
||||
<button
|
||||
type="button"
|
||||
title="Menu"
|
||||
className={styles.hamburgerButton}
|
||||
{...props}
|
||||
>
|
||||
<span className={styles.hamburger}>
|
||||
<span className={styles.hamburgerLine} />
|
||||
<span className={styles.hamburgerLine} />
|
||||
<span className={styles.hamburgerLine} />
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
export default Hamburger
|
@ -2,77 +2,77 @@
|
||||
@import 'mixins';
|
||||
|
||||
.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;
|
||||
}
|
||||
|
@ -5,5 +5,5 @@ import testRender from '../../../jest/testRender'
|
||||
import Hamburger from './Hamburger'
|
||||
|
||||
describe('Hamburger', () => {
|
||||
testRender(<Hamburger />)
|
||||
testRender(<Hamburger onClick={() => null} />)
|
||||
})
|
19
src/components/atoms/Hamburger.tsx
Normal file
19
src/components/atoms/Hamburger.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
import styles from './Hamburger.module.scss'
|
||||
|
||||
export default function Hamburger({ onClick }: { onClick(): void }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
title="Menu"
|
||||
className={styles.hamburgerButton}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className={styles.hamburger}>
|
||||
<span className={styles.hamburgerLine} />
|
||||
<span className={styles.hamburgerLine} />
|
||||
<span className={styles.hamburgerLine} />
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { graphql } from 'gatsby'
|
||||
import Img from 'gatsby-image'
|
||||
import styles from './Image.module.scss'
|
||||
|
||||
export default class Image extends PureComponent {
|
||||
static propTypes = {
|
||||
fluid: PropTypes.object,
|
||||
fixed: PropTypes.object,
|
||||
alt: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
const { fluid, fixed, alt } = this.props
|
||||
|
||||
return (
|
||||
<Img
|
||||
className={styles.imageWrap}
|
||||
backgroundColor="#dfe8ef"
|
||||
fluid={fluid ? fluid : null}
|
||||
fixed={fixed ? fixed : null}
|
||||
alt={alt}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const imageSizeDefault = graphql`
|
||||
fragment ImageFluid on ImageSharp {
|
||||
fluid(maxWidth: 940, quality: 85) {
|
||||
...GatsbyImageSharpFluid_withWebp_noBase64
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const imageSizeThumb = graphql`
|
||||
fragment ImageFluidThumb on ImageSharp {
|
||||
fluid(maxWidth: 200, maxHeight: 85, quality: 85, cropFocus: CENTER) {
|
||||
...GatsbyImageSharpFluid_withWebp_noBase64
|
||||
}
|
||||
}
|
||||
`
|
@ -1,20 +1,20 @@
|
||||
@import 'mixins';
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
40
src/components/atoms/Image.tsx
Normal file
40
src/components/atoms/Image.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import React from 'react'
|
||||
import { graphql } from 'gatsby'
|
||||
import Img, { FixedObject, FluidObject } from 'gatsby-image'
|
||||
import styles from './Image.module.scss'
|
||||
|
||||
export default function Image({
|
||||
fluid,
|
||||
fixed,
|
||||
alt
|
||||
}: {
|
||||
fluid?: FluidObject
|
||||
fixed?: FixedObject
|
||||
alt: string
|
||||
}) {
|
||||
return (
|
||||
<Img
|
||||
className={styles.imageWrap}
|
||||
backgroundColor="#dfe8ef"
|
||||
fluid={fluid}
|
||||
fixed={fixed}
|
||||
alt={alt}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const imageSizeDefault = graphql`
|
||||
fragment ImageFluid on ImageSharp {
|
||||
fluid(maxWidth: 940, quality: 85) {
|
||||
...GatsbyImageSharpFluid_withWebp_noBase64
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const imageSizeThumb = graphql`
|
||||
fragment ImageFluidThumb on ImageSharp {
|
||||
fluid(maxWidth: 200, maxHeight: 85, quality: 85, cropFocus: CENTER) {
|
||||
...GatsbyImageSharpFluid_withWebp_noBase64
|
||||
}
|
||||
}
|
||||
`
|
@ -1,6 +0,0 @@
|
||||
import React from 'react'
|
||||
import styles from './Input.module.scss'
|
||||
|
||||
const Input = props => <input className={styles.input} {...props} />
|
||||
|
||||
export default Input
|
@ -1,32 +1,32 @@
|
||||
@import 'variables';
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
6
src/components/atoms/Input.tsx
Normal file
6
src/components/atoms/Input.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import React from 'react'
|
||||
import styles from './Input.module.scss'
|
||||
|
||||
export default function Input(props: any) {
|
||||
return <input className={styles.input} {...props} />
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ReactModal from 'react-modal'
|
||||
|
||||
import styles from './Modal.module.scss'
|
||||
|
||||
ReactModal.setAppElement('#___gatsby')
|
||||
|
||||
export default class Modal extends PureComponent {
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
isOpen: PropTypes.bool,
|
||||
handleCloseModal: PropTypes.func.isRequired,
|
||||
children: PropTypes.node.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.isOpen) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { children, title, handleCloseModal } = this.props
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
overlayClassName={styles.modal}
|
||||
className={styles.modal__content}
|
||||
htmlOpenClassName={styles.isModalOpen}
|
||||
shouldReturnFocusAfterClose={false}
|
||||
{...this.props}
|
||||
>
|
||||
{title && <h1 className={styles.modal__title}>{title}</h1>}
|
||||
{children}
|
||||
<button className={styles.modal__close} onClick={handleCloseModal}>
|
||||
×
|
||||
</button>
|
||||
</ReactModal>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,111 +1,111 @@
|
||||
@import 'variables';
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
38
src/components/atoms/Modal.tsx
Normal file
38
src/components/atoms/Modal.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React, { ReactChildren } from 'react'
|
||||
import ReactModal from 'react-modal'
|
||||
|
||||
import styles from './Modal.module.scss'
|
||||
|
||||
ReactModal.setAppElement('#___gatsby')
|
||||
|
||||
export default function Modal({
|
||||
title,
|
||||
isOpen,
|
||||
handleCloseModal,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
title?: string
|
||||
isOpen?: boolean
|
||||
handleCloseModal: any
|
||||
children: ReactChildren
|
||||
}) {
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
overlayClassName={styles.modal}
|
||||
className={styles.modal__content}
|
||||
htmlOpenClassName={styles.isModalOpen}
|
||||
shouldReturnFocusAfterClose={false}
|
||||
isOpen={isOpen}
|
||||
{...props}
|
||||
>
|
||||
{title && <h1 className={styles.modal__title}>{title}</h1>}
|
||||
{children}
|
||||
<button className={styles.modal__close} onClick={handleCloseModal}>
|
||||
×
|
||||
</button>
|
||||
</ReactModal>
|
||||
)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user