1
0
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:
Matthias Kretschmann 2019-10-02 21:57:42 +02:00 committed by GitHub
commit 25092036be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
157 changed files with 3785 additions and 3979 deletions

View File

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

View File

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

View File

@ -1,4 +1,6 @@
plugins/gatsby-redirect-from
node_modules
public
.cache
node_modules/
.cache/
static/
public/
coverage/

View File

@ -1,34 +1,36 @@
{
"parser": "babel-eslint",
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:jsx-a11y/recommended",
"plugin:prettier/recommended"
],
"plugins": ["react", "graphql", "prettier", "jsx-a11y"],
"extends": ["eslint:recommended", "prettier"],
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"modules": true
}
},
"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"
}
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,8 +15,8 @@ const feedContent = edge => {
: `${html}${footer}`
}
const generateJsonFeed = async posts => {
const jsonItems = await posts.map(edge => {
async function jsonItems(posts) {
return await posts.map(edge => {
const { frontmatter, fields, excerpt } = edge.node
const { slug, date } = fields
@ -33,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)

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
@import 'variables';
.searchButton {
padding: .65rem .85rem;
padding: 0.65rem 0.85rem;
text-align: center;
line-height: 1;
vertical-align: middle;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -40,6 +40,6 @@
@keyframes ellipsis {
to {
width: .75rem;
width: 0.75rem;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -56,6 +56,6 @@
@keyframes ellipsis {
to {
width: .75rem;
width: 0.75rem;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -49,7 +49,7 @@
}
.hamburgerButton {
padding: .65rem .85rem;
padding: 0.65rem 0.85rem;
text-align: center;
line-height: 1;
vertical-align: middle;

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@
@media (min-width: 940px) {
max-width: 940px;
border-radius: .25rem;
border-radius: 0.25rem;
overflow: hidden;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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}
/>
)
}

View File

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

View File

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

View File

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

View 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