mirror of
https://github.com/kremalicious/blog.git
synced 2024-12-23 01:30:01 +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/
|
50
.eslintrc
50
.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
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"jest": 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": {
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "never"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"prettier/prettier": "error"
|
||||
"react/prop-types": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "16"
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": { "jsx": true },
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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='
|
||||
|
@ -8,14 +8,14 @@ kbd {
|
||||
line-height: 1em;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
display: inline;
|
||||
padding: .3em .55em;
|
||||
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, .1),
|
||||
rgba(0, 0, 0, 0.1),
|
||||
rgba(0, 0, 0, 0)
|
||||
);
|
||||
background-repeat: repeat-x;
|
||||
@ -29,7 +29,7 @@ kbd.dark {
|
||||
border-color: #000;
|
||||
background-color: #4d4c4c;
|
||||
background-image: linear-gradient(
|
||||
rgba(0, 0, 0, .5),
|
||||
rgba(0, 0, 0, 0.5),
|
||||
rgba(0, 0, 0, 0) 80%,
|
||||
rgba(0, 0, 0, 0)
|
||||
);
|
||||
@ -41,12 +41,12 @@ kbd.dark {
|
||||
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);
|
||||
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, .6), 0 2px 3px rgba(0, 0, 0, .1),
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.6), 0 2px 3px rgba(0, 0, 0, 0.1),
|
||||
inset 0 1px 0 #fff;
|
||||
}
|
||||
|
||||
@ -54,25 +54,22 @@ 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);
|
||||
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, .3), 0 1px 0 #444,
|
||||
inset 0 1px 0 #868686;
|
||||
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;
|
||||
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;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.7), 0 1px 0 #444, inset 0 1px 0 #36647b;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
@ -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,8 +33,9 @@ const generateJsonFeed = async posts => {
|
||||
content_html: feedContent(edge)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const jsonFeed = {
|
||||
const createJsonFeed = posts => ({
|
||||
version: 'https://jsonfeed.org/version/1',
|
||||
title: siteTitle,
|
||||
description: siteDescription,
|
||||
@ -48,12 +49,13 @@ const generateJsonFeed = async posts => {
|
||||
name: author.name,
|
||||
url: author.uri
|
||||
},
|
||||
items: jsonItems
|
||||
}
|
||||
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
|
@ -27,11 +27,11 @@
|
||||
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);
|
||||
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, .1),
|
||||
0 -1px 4px rgba($brand-main, .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) & {
|
||||
|
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>
|
||||
)
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
@include breakoutviewport;
|
||||
|
||||
margin-top: $spacer * 3;
|
||||
background: rgba(#fff, .5);
|
||||
background: rgba(#fff, 0.5);
|
||||
padding-top: $spacer;
|
||||
padding-bottom: $spacer;
|
||||
border-radius: $border-radius;
|
||||
@ -21,7 +21,7 @@
|
||||
|
||||
> div {
|
||||
flex: 0 0 100%;
|
||||
border-bottom: 1px dashed rgba($brand-grey-light, .3);
|
||||
border-bottom: 1px dashed rgba($brand-grey-light, 0.3);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
@ -30,7 +30,7 @@
|
||||
@media (min-width: $screen-sm) {
|
||||
flex: 0 0 33.33333%;
|
||||
border-bottom: 0;
|
||||
border-left: 1px dashed rgba($brand-grey-light, .3);
|
||||
border-left: 1px dashed rgba($brand-grey-light, 0.3);
|
||||
|
||||
&:first-child {
|
||||
border-left: 0;
|
||||
@ -40,7 +40,7 @@
|
||||
}
|
||||
|
||||
.link {
|
||||
transition: .2s ease-out;
|
||||
transition: 0.2s ease-out;
|
||||
color: $link-color;
|
||||
}
|
||||
|
||||
@ -49,14 +49,14 @@
|
||||
color: $text-color;
|
||||
margin-top: 0;
|
||||
margin-bottom: $spacer / 4;
|
||||
transition: color .2s ease-out;
|
||||
transition: color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.actionText {
|
||||
font-size: $font-size-small;
|
||||
color: $brand-grey-light;
|
||||
margin-bottom: 0;
|
||||
transition: color .2s ease-out;
|
||||
transition: color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.action {
|
||||
|
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,19 +2,19 @@
|
||||
@import 'mixins';
|
||||
|
||||
.postImageTitle {
|
||||
transition: .1s ease-out;
|
||||
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: -.02em;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
padding: $spacer / 3 $spacer;
|
||||
background: rgba($link-color, .85);
|
||||
background: rgba($link-color, 0.85);
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 0 #000;
|
||||
left: 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,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,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
|
@ -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
|
@ -4,7 +4,7 @@
|
||||
display: inline-block;
|
||||
font-family: $font-family-headings;
|
||||
font-weight: $font-weight-headings;
|
||||
font-size: $font-size-base * .9;
|
||||
font-size: $font-size-base * 0.9;
|
||||
color: $link-color;
|
||||
text-transform: uppercase;
|
||||
margin-top: $spacer;
|
||||
@ -12,18 +12,18 @@
|
||||
svg {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
top: .2rem;
|
||||
top: 0.2rem;
|
||||
position: relative;
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
fill: $text-color-light;
|
||||
transition: .2s ease-out;
|
||||
transition: 0.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>
|
||||
)
|
||||
}
|
||||
}
|
@ -7,9 +7,9 @@
|
||||
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;
|
||||
padding-left: 0.2rem;
|
||||
padding-right: 0.2rem;
|
||||
transition: color 0.2s ease-out;
|
||||
|
||||
@media (min-width: $screen-md) {
|
||||
font-size: $font-size-base;
|
||||
|
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>
|
||||
)
|
||||
}
|
@ -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,7 +1,7 @@
|
||||
@import 'variables';
|
||||
|
||||
.searchButton {
|
||||
padding: .65rem .85rem;
|
||||
padding: 0.65rem 0.85rem;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
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')
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
@ -8,9 +8,9 @@
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba($body-background-color, .95);
|
||||
background: rgba($body-background-color, 0.95);
|
||||
backdrop-filter: blur(5px);
|
||||
animation: fadein .3s;
|
||||
animation: fadein 0.3s;
|
||||
overflow: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
height: 91vh;
|
||||
|
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,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,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
|
@ -40,6 +40,6 @@
|
||||
|
||||
@keyframes ellipsis {
|
||||
to {
|
||||
width: .75rem;
|
||||
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
|
||||
} 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() }}
|
||||
className={classes()}
|
||||
dangerouslySetInnerHTML={{ __html: constructMessage() }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
max-width: 18rem;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
animation: fadeIn .8s ease-out backwards;
|
||||
animation: fadeIn 0.8s ease-out backwards;
|
||||
|
||||
@media (min-width: $screen-sm) {
|
||||
display: flex;
|
||||
@ -67,7 +67,7 @@
|
||||
padding: $spacer / 3;
|
||||
color: $brand-grey-light;
|
||||
background: $brand-light;
|
||||
border-right: 1px solid rgba($brand-grey-light, .4);
|
||||
border-right: 1px solid rgba($brand-grey-light, 0.4);
|
||||
border-top-left-radius: $border-radius;
|
||||
border-bottom-left-radius: $border-radius;
|
||||
display: flex;
|
||||
@ -80,7 +80,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: $spacer / 4;
|
||||
animation: fadeIn .5s .8s ease-out backwards;
|
||||
animation: fadeIn 0.5s 0.8s ease-out backwards;
|
||||
}
|
||||
|
||||
.message {
|
||||
@ -89,7 +89,7 @@
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: .01;
|
||||
opacity: 0.01;
|
||||
}
|
||||
|
||||
to {
|
||||
|
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>
|
||||
)
|
||||
}
|
@ -56,6 +56,6 @@
|
||||
|
||||
@keyframes ellipsis {
|
||||
to {
|
||||
width: .75rem;
|
||||
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
|
@ -16,8 +16,8 @@
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: .4rem;
|
||||
height: .4rem;
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
background: $color-headings;
|
||||
@ -59,7 +59,7 @@
|
||||
margin-left: $spacer / 8;
|
||||
|
||||
code {
|
||||
font-size: ($font-size-mini * .9);
|
||||
font-size: ($font-size-mini * 0.9);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
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
|
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
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)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { latitude, longitude } = this.props.gps
|
||||
const { latitude, longitude } = gps
|
||||
|
||||
return (
|
||||
<Map
|
||||
center={[latitude, longitude]}
|
||||
zoom={this.state.zoom}
|
||||
zoom={zoom}
|
||||
height={160}
|
||||
attribution={false}
|
||||
provider={providers['light']}
|
||||
metaWheelZoom={true}
|
||||
metaWheelZoomWarning={'META+wheel to zoom'}
|
||||
>
|
||||
<Marker
|
||||
anchor={[latitude, longitude]}
|
||||
payload={1}
|
||||
onClick={this.zoomIn}
|
||||
/>
|
||||
<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
|
@ -49,7 +49,7 @@
|
||||
}
|
||||
|
||||
.hamburgerButton {
|
||||
padding: .65rem .85rem;
|
||||
padding: 0.65rem 0.85rem;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
`
|
@ -10,7 +10,7 @@
|
||||
|
||||
@media (min-width: 940px) {
|
||||
max-width: 940px;
|
||||
border-radius: .25rem;
|
||||
border-radius: 0.25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
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
|
@ -13,7 +13,7 @@
|
||||
border: 0;
|
||||
border-radius: $input-border-radius;
|
||||
box-shadow: none;
|
||||
transition: all ease-in-out .15s;
|
||||
transition: all ease-in-out 0.15s;
|
||||
appearance: none;
|
||||
|
||||
&:hover {
|
||||
|
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>
|
||||
)
|
||||
}
|
||||
}
|
@ -9,9 +9,9 @@
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9;
|
||||
background: rgba($body-background-color, .95);
|
||||
background: rgba($body-background-color, 0.95);
|
||||
backdrop-filter: blur(5px);
|
||||
animation: fadein .3s;
|
||||
animation: fadein 0.3s;
|
||||
padding: $spacer;
|
||||
|
||||
@media (min-width: $screen-sm) {
|
||||
@ -27,8 +27,8 @@
|
||||
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);
|
||||
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%;
|
||||
|
||||
@ -77,7 +77,7 @@
|
||||
margin-bottom: $spacer / 2;
|
||||
margin-left: -($spacer / 2);
|
||||
margin-right: -($spacer / 2);
|
||||
border-bottom: 1px solid rgba($brand-grey-light, .4);
|
||||
border-bottom: 1px solid rgba($brand-grey-light, 0.4);
|
||||
padding: 0 $spacer;
|
||||
padding-bottom: ($spacer/2);
|
||||
|
||||
|
38
src/components/atoms/Modal.tsx
Normal file
38
src/components/atoms/Modal.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React, { ReactChildren } from 'react'
|
||||
import ReactModal from 'react-modal'
|
||||
|
||||
import styles from './Modal.module.scss'
|
||||
|
||||
ReactModal.setAppElement('#___gatsby')
|
||||
|
||||
export default function Modal({
|
||||
title,
|
||||
isOpen,
|
||||
handleCloseModal,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
title?: string
|
||||
isOpen?: boolean
|
||||
handleCloseModal: any
|
||||
children: ReactChildren
|
||||
}) {
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
overlayClassName={styles.modal}
|
||||
className={styles.modal__content}
|
||||
htmlOpenClassName={styles.isModalOpen}
|
||||
shouldReturnFocusAfterClose={false}
|
||||
isOpen={isOpen}
|
||||
{...props}
|
||||
>
|
||||
{title && <h1 className={styles.modal__title}>{title}</h1>}
|
||||
{children}
|
||||
<button className={styles.modal__close} onClick={handleCloseModal}>
|
||||
×
|
||||
</button>
|
||||
</ReactModal>
|
||||
)
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { QRCode } from 'react-qr-svg'
|
||||
import Clipboard from 'react-clipboard.js'
|
||||
import { ReactComponent as IconClipboard } from '../../images/clipboard.svg'
|
||||
|
||||
import styles from './Qr.module.scss'
|
||||
|
||||
const onCopySuccess = e => {
|
||||
e.trigger.classList.add(styles.copied)
|
||||
}
|
||||
|
||||
const Qr = ({ address, title }) => (
|
||||
<>
|
||||
{title && <h4>{title}</h4>}
|
||||
<QRCode
|
||||
bgColor="transparent"
|
||||
fgColor="#6b7f88"
|
||||
level="Q"
|
||||
style={{ width: 120 }}
|
||||
value={address}
|
||||
className={styles.qr}
|
||||
/>
|
||||
|
||||
<pre className={styles.code}>
|
||||
<code>{address}</code>
|
||||
<Clipboard
|
||||
data-clipboard-text={address}
|
||||
button-title="Copy to clipboard"
|
||||
onSuccess={e => onCopySuccess(e)}
|
||||
className={styles.button}
|
||||
>
|
||||
<IconClipboard />
|
||||
</Clipboard>
|
||||
</pre>
|
||||
</>
|
||||
)
|
||||
|
||||
Qr.propTypes = {
|
||||
address: PropTypes.string.isRequired,
|
||||
title: PropTypes.string
|
||||
}
|
||||
|
||||
export default Qr
|
@ -12,7 +12,7 @@
|
||||
|
||||
code {
|
||||
padding: $spacer / 2;
|
||||
font-size: .65rem;
|
||||
font-size: 0.65rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@ -27,14 +27,14 @@
|
||||
box-shadow: none;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
background: rgba($brand-grey, .3);
|
||||
background: rgba($brand-grey, 0.3);
|
||||
padding: $spacer / 3;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
fill: $brand-grey-light;
|
||||
transition: .15s ease-out;
|
||||
transition: 0.15s ease-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
44
src/components/atoms/Qr.tsx
Normal file
44
src/components/atoms/Qr.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
import { QRCode } from 'react-qr-svg'
|
||||
import Clipboard from 'react-clipboard.js'
|
||||
import { ReactComponent as IconClipboard } from '../../images/clipboard.svg'
|
||||
|
||||
import styles from './Qr.module.scss'
|
||||
|
||||
const onCopySuccess = (e: any) => {
|
||||
e.trigger.classList.add(styles.copied)
|
||||
}
|
||||
|
||||
export default function Qr({
|
||||
address,
|
||||
title
|
||||
}: {
|
||||
address: string
|
||||
title?: string
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{title && <h4>{title}</h4>}
|
||||
<QRCode
|
||||
bgColor="transparent"
|
||||
fgColor="#6b7f88"
|
||||
level="Q"
|
||||
style={{ width: 120 }}
|
||||
value={address}
|
||||
className={styles.qr}
|
||||
/>
|
||||
|
||||
<pre className={styles.code}>
|
||||
<code>{address}</code>
|
||||
<Clipboard
|
||||
data-clipboard-text={address}
|
||||
button-title="Copy to clipboard"
|
||||
onSuccess={e => onCopySuccess(e)}
|
||||
className={styles.button}
|
||||
>
|
||||
<IconClipboard />
|
||||
</Clipboard>
|
||||
</pre>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,205 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { StaticQuery, graphql } from 'gatsby'
|
||||
import Helmet from 'react-helmet'
|
||||
|
||||
const query = graphql`
|
||||
query {
|
||||
site {
|
||||
siteMetadata {
|
||||
siteTitle
|
||||
siteDescription
|
||||
siteUrl
|
||||
author {
|
||||
name
|
||||
twitter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logo: allFile(filter: { name: { eq: "apple-touch-icon" } }) {
|
||||
edges {
|
||||
node {
|
||||
relativePath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const createSchemaOrg = (
|
||||
blogURL,
|
||||
title,
|
||||
siteMeta,
|
||||
postSEO,
|
||||
postURL,
|
||||
image,
|
||||
description
|
||||
) => {
|
||||
const schemaOrgJSONLD = [
|
||||
{
|
||||
'@context': 'http://schema.org',
|
||||
'@type': 'WebSite',
|
||||
url: blogURL,
|
||||
name: title,
|
||||
alternateName: siteMeta.titleAlt ? siteMeta.titleAlt : ''
|
||||
}
|
||||
]
|
||||
|
||||
if (postSEO) {
|
||||
schemaOrgJSONLD.push(
|
||||
{
|
||||
'@context': 'http://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 1,
|
||||
item: {
|
||||
'@id': postURL,
|
||||
name: title,
|
||||
image
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'@context': 'http://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
url: blogURL,
|
||||
name: title,
|
||||
alternateName: siteMeta.titleAlt ? siteMeta.titleAlt : '',
|
||||
headline: title,
|
||||
image: {
|
||||
'@type': 'ImageObject',
|
||||
url: image
|
||||
},
|
||||
description
|
||||
}
|
||||
)
|
||||
}
|
||||
return schemaOrgJSONLD
|
||||
}
|
||||
|
||||
const MetaTags = ({
|
||||
description,
|
||||
image,
|
||||
url,
|
||||
schema,
|
||||
postSEO,
|
||||
title,
|
||||
siteMeta
|
||||
}) => (
|
||||
<Helmet
|
||||
defaultTitle={`${siteMeta.siteTitle} ¦ ${siteMeta.siteDescription}`}
|
||||
titleTemplate={`%s ¦ ${siteMeta.siteTitle}`}
|
||||
>
|
||||
<html lang="en" />
|
||||
|
||||
{/* General tags */}
|
||||
<meta name="description" content={description} />
|
||||
<meta name="image" content={image} />
|
||||
<link rel="canonical" href={url} />
|
||||
|
||||
{/* Schema.org tags */}
|
||||
<script type="application/ld+json">{schema}</script>
|
||||
|
||||
{/* OpenGraph tags */}
|
||||
<meta property="og:url" content={url} />
|
||||
{postSEO && <meta property="og:type" content="article" />}
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={image} />
|
||||
|
||||
{/* Twitter Card tags */}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta
|
||||
name="twitter:creator"
|
||||
content={siteMeta.author.twitter ? siteMeta.author.twitter : ''}
|
||||
/>
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={image} />
|
||||
|
||||
<link
|
||||
rel="alternate"
|
||||
title="JSON Feed"
|
||||
type="application/json"
|
||||
href={`${siteMeta.siteUrl}/feed.json`}
|
||||
/>
|
||||
</Helmet>
|
||||
)
|
||||
|
||||
MetaTags.propTypes = {
|
||||
description: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
schema: PropTypes.string,
|
||||
postSEO: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
siteMeta: PropTypes.object
|
||||
}
|
||||
|
||||
const SEO = ({ post, slug, postSEO }) => (
|
||||
<StaticQuery
|
||||
query={query}
|
||||
render={data => {
|
||||
const siteMeta = data.site.siteMetadata
|
||||
const logo = data.logo.edges[0].node.relativePath
|
||||
|
||||
let title
|
||||
let description
|
||||
let image
|
||||
let postURL
|
||||
|
||||
if (postSEO) {
|
||||
const postMeta = post.frontmatter
|
||||
title = `${postMeta.title} ¦ ${siteMeta.siteTitle}`
|
||||
description = postMeta.description ? postMeta.description : post.excerpt
|
||||
image = postMeta.image
|
||||
? postMeta.image.childImageSharp.fluid.src
|
||||
: `/${logo}`
|
||||
postURL = `${siteMeta.siteUrl}${slug}`
|
||||
} else {
|
||||
title = `${siteMeta.siteTitle} ¦ ${siteMeta.siteDescription}`
|
||||
description = siteMeta.siteDescription
|
||||
image = `/${logo}`
|
||||
}
|
||||
|
||||
image = `${siteMeta.siteUrl}${image}`
|
||||
const blogURL = siteMeta.siteUrl
|
||||
const url = postSEO ? postURL : blogURL
|
||||
|
||||
let schema = createSchemaOrg(
|
||||
blogURL,
|
||||
title,
|
||||
siteMeta,
|
||||
postSEO,
|
||||
postURL,
|
||||
image,
|
||||
description
|
||||
)
|
||||
schema = JSON.stringify(schema)
|
||||
|
||||
return (
|
||||
<MetaTags
|
||||
description={description}
|
||||
image={image}
|
||||
url={url}
|
||||
schema={schema}
|
||||
postSEO={postSEO}
|
||||
title={title}
|
||||
siteMeta={siteMeta}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
SEO.propTypes = {
|
||||
post: PropTypes.object,
|
||||
slug: PropTypes.string,
|
||||
postSEO: PropTypes.bool
|
||||
}
|
||||
|
||||
export default SEO
|
185
src/components/atoms/SEO.tsx
Normal file
185
src/components/atoms/SEO.tsx
Normal file
@ -0,0 +1,185 @@
|
||||
import React from 'react'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
import Helmet from 'react-helmet'
|
||||
import { useSiteMetadata } from '../../hooks/use-site-metadata'
|
||||
|
||||
const query = graphql`
|
||||
query {
|
||||
logo: allFile(filter: { name: { eq: "apple-touch-icon" } }) {
|
||||
edges {
|
||||
node {
|
||||
relativePath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const createSchemaOrg = (
|
||||
blogURL: string,
|
||||
title: string,
|
||||
postSEO: boolean,
|
||||
postURL: string,
|
||||
image: string,
|
||||
description: string
|
||||
) => {
|
||||
const schemaOrgJSONLD = [
|
||||
{
|
||||
'@context': 'http://schema.org',
|
||||
'@type': 'WebSite',
|
||||
url: blogURL,
|
||||
name: title
|
||||
}
|
||||
]
|
||||
|
||||
if (postSEO) {
|
||||
schemaOrgJSONLD.push(
|
||||
{
|
||||
'@context': 'http://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 1,
|
||||
item: {
|
||||
'@id': postURL,
|
||||
name: title,
|
||||
image
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'@context': 'http://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
url: blogURL,
|
||||
name: title,
|
||||
headline: title,
|
||||
image: {
|
||||
'@type': 'ImageObject',
|
||||
url: image
|
||||
},
|
||||
description
|
||||
}
|
||||
)
|
||||
}
|
||||
return schemaOrgJSONLD
|
||||
}
|
||||
|
||||
const MetaTags = ({
|
||||
description,
|
||||
image,
|
||||
url,
|
||||
schema,
|
||||
postSEO,
|
||||
title
|
||||
}: {
|
||||
description: string
|
||||
image: string
|
||||
url: string
|
||||
schema: string
|
||||
postSEO: boolean
|
||||
title: string
|
||||
}) => {
|
||||
const { siteTitle, siteDescription, siteUrl, author } = useSiteMetadata()
|
||||
|
||||
return (
|
||||
<Helmet
|
||||
defaultTitle={`${siteTitle} ¦ ${siteDescription}`}
|
||||
titleTemplate={`%s ¦ ${siteTitle}`}
|
||||
>
|
||||
<html lang="en" />
|
||||
|
||||
{/* General tags */}
|
||||
<meta name="description" content={description} />
|
||||
<meta name="image" content={image} />
|
||||
<link rel="canonical" href={url} />
|
||||
|
||||
{/* Schema.org tags */}
|
||||
<script type="application/ld+json">{schema}</script>
|
||||
|
||||
{/* OpenGraph tags */}
|
||||
<meta property="og:url" content={url} />
|
||||
{postSEO && <meta property="og:type" content="article" />}
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={image} />
|
||||
|
||||
{/* Twitter Card tags */}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta
|
||||
name="twitter:creator"
|
||||
content={author.twitter ? author.twitter : ''}
|
||||
/>
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={image} />
|
||||
|
||||
<link
|
||||
rel="alternate"
|
||||
title="JSON Feed"
|
||||
type="application/json"
|
||||
href={`${siteUrl}/feed.json`}
|
||||
/>
|
||||
</Helmet>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SEO({
|
||||
post,
|
||||
slug,
|
||||
postSEO
|
||||
}: {
|
||||
post?: any
|
||||
slug?: string
|
||||
postSEO?: boolean
|
||||
}) {
|
||||
const data = useStaticQuery(query)
|
||||
const logo = data.logo.edges[0].node.relativePath
|
||||
const { siteTitle, siteUrl, siteDescription } = useSiteMetadata()
|
||||
|
||||
let title
|
||||
let description
|
||||
let image
|
||||
let postURL
|
||||
|
||||
if (postSEO) {
|
||||
const postMeta = post.frontmatter
|
||||
title = `${postMeta.title} ¦ ${siteTitle}`
|
||||
description = postMeta.description ? postMeta.description : post.excerpt
|
||||
image = postMeta.image
|
||||
? postMeta.image.childImageSharp.fluid.src
|
||||
: `/${logo}`
|
||||
postURL = `${siteUrl}${slug}`
|
||||
} else {
|
||||
title = `${siteTitle} ¦ ${siteDescription}`
|
||||
description = siteDescription
|
||||
image = `/${logo}`
|
||||
}
|
||||
|
||||
image = `${siteUrl}${image}`
|
||||
const blogURL = siteUrl
|
||||
const url = postSEO ? postURL : blogURL
|
||||
|
||||
let schema = createSchemaOrg(
|
||||
blogURL,
|
||||
title,
|
||||
postSEO,
|
||||
postURL,
|
||||
image,
|
||||
description
|
||||
)
|
||||
|
||||
schema = JSON.stringify(schema)
|
||||
|
||||
return (
|
||||
<MetaTags
|
||||
description={description}
|
||||
image={image}
|
||||
url={url}
|
||||
schema={schema}
|
||||
postSEO={postSEO}
|
||||
title={title}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
import { StaticQuery, graphql } from 'gatsby'
|
||||
import Helmet from 'react-helmet'
|
||||
import { useSiteMetadata } from '../../hooks/use-site-metadata'
|
||||
|
||||
const TypekitScript = typekitID => (
|
||||
const TypekitScript = (typekitID: string) => (
|
||||
<script>
|
||||
{`
|
||||
(function(d) {
|
||||
@ -17,21 +17,8 @@ const TypekitScript = typekitID => (
|
||||
</script>
|
||||
)
|
||||
|
||||
const query = graphql`
|
||||
query {
|
||||
site {
|
||||
siteMetadata {
|
||||
typekitID
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Typekit = () => (
|
||||
<StaticQuery
|
||||
query={query}
|
||||
render={data => {
|
||||
const { typekitID } = data.site.siteMetadata
|
||||
export default function Typekit() {
|
||||
const { typekitID } = useSiteMetadata()
|
||||
|
||||
return (
|
||||
typekitID && (
|
||||
@ -43,8 +30,4 @@ const Typekit = () => (
|
||||
</Helmet>
|
||||
)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
export default Typekit
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Link, graphql, StaticQuery } from 'gatsby'
|
||||
import Image from '../atoms/Image'
|
||||
import styles from './Featured.module.scss'
|
||||
|
||||
const query = graphql`
|
||||
query {
|
||||
allMarkdownRemark(
|
||||
filter: { frontmatter: { featured: { eq: true } } }
|
||||
sort: { fields: [fields___date], order: DESC }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
frontmatter {
|
||||
title
|
||||
image {
|
||||
childImageSharp {
|
||||
...ImageFluidThumb
|
||||
}
|
||||
}
|
||||
}
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Featured = () => (
|
||||
<StaticQuery
|
||||
query={query}
|
||||
render={data => (
|
||||
<div className={styles.featured}>
|
||||
{data.allMarkdownRemark.edges.map(({ node }) => {
|
||||
const { title, image } = node.frontmatter
|
||||
const { slug } = node.fields
|
||||
|
||||
return (
|
||||
<article className={styles.featuredItem} key={node.id}>
|
||||
<Link to={slug}>
|
||||
<Image fluid={image.childImageSharp.fluid} alt={title} />
|
||||
<h1 className={styles.featuredTitle}>{title}</h1>
|
||||
</Link>
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
|
||||
export default Featured
|
@ -26,7 +26,7 @@
|
||||
}
|
||||
|
||||
.featuredTitle {
|
||||
transition: .1s ease-out;
|
||||
transition: 0.1s ease-out;
|
||||
font-size: $font-size-base;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
@ -34,7 +34,7 @@
|
||||
min-width: 45%;
|
||||
text-align: right;
|
||||
padding: $spacer / 3;
|
||||
background: rgba($link-color, .85);
|
||||
background: rgba($link-color, 0.85);
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 0 #000;
|
||||
left: 0;
|
||||
|
52
src/components/molecules/Featured.tsx
Normal file
52
src/components/molecules/Featured.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
import { Link, graphql, useStaticQuery } from 'gatsby'
|
||||
import Image from '../atoms/Image'
|
||||
import styles from './Featured.module.scss'
|
||||
|
||||
const query = graphql`
|
||||
query {
|
||||
allMarkdownRemark(
|
||||
filter: { frontmatter: { featured: { eq: true } } }
|
||||
sort: { fields: [fields___date], order: DESC }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
frontmatter {
|
||||
title
|
||||
image {
|
||||
childImageSharp {
|
||||
...ImageFluidThumb
|
||||
}
|
||||
}
|
||||
}
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function Featured() {
|
||||
const data = useStaticQuery(query)
|
||||
|
||||
return (
|
||||
<div className={styles.featured}>
|
||||
{data.allMarkdownRemark.edges.map(({ node }: { node: any }) => {
|
||||
const { title, image } = node.frontmatter
|
||||
const { slug } = node.fields
|
||||
|
||||
return (
|
||||
<article className={styles.featuredItem} key={node.id}>
|
||||
<Link to={slug}>
|
||||
<Image fluid={image.childImageSharp.fluid} alt={title} />
|
||||
<h1 className={styles.featuredTitle}>{title}</h1>
|
||||
</Link>
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user