1
0
mirror of https://github.com/kremalicious/portfolio.git synced 2024-12-23 01:29:41 +01:00

Merge pull request #10 from kremalicious/feature/gatsby-v2

Update to Gatsby v2
This commit is contained in:
Matthias Kretschmann 2018-07-14 02:38:58 +02:00 committed by GitHub
commit eba94c3877
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 635 additions and 1163 deletions

View File

@ -1,2 +1,5 @@
exclude_patterns: version: "2"
- src/lib/vcf/ checks:
method-lines:
config:
threshold: 50 # Gatsby's StaticQuery makes render functions pretty long

View File

@ -18,9 +18,6 @@
"node": true, "node": true,
"es6": true "es6": true
}, },
"globals": {
"graphql": true
},
"rules": { "rules": {
"quotes": ["error", "single"], "quotes": ["error", "single"],
"semi": ["error", "never"], "semi": ["error", "never"],

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ yarn-debug.log*
yarn.lock yarn.lock
package-lock.json package-lock.json
plugins/gatsby-plugin-matomo plugins/gatsby-plugin-matomo
src/components/svg

View File

@ -1,5 +1,4 @@
node_modules/ node_modules/
.cache/ .cache/
src/lib/vcf
static/ static/
public/ public/

View File

@ -7,10 +7,6 @@ cache:
- public - public
install: install:
- pip install --user awscli
- export PATH=$PATH:$HOME/.local/bin
- npm i -g gatsby-cli
- export PATH="$PATH:/usr/local/bin/gatsby"
- npm i - npm i
script: script:
@ -18,9 +14,9 @@ script:
- npm run build - npm run build
after_success: after_success:
- pip install --user awscli
- export PATH=$PATH:$HOME/.local/bin
- npm run deploy - npm run deploy
notifications: notifications:
email: false email: false
slack:
secure: v/sqm2fgXTi7RJIjVsaJz1i3bV/WNFyTkLLto2p/fXjoWHVWJBdodANTTH+9lgkbft6JdBcU4WDLDkLP8VKlR9JffFc1DvbxGmF3VTfP2kJcnCbo+koLHBtzR1oRwDB1aulp6+II0ROPS9OyOv+Xx45bj4jOt9y/r0Qcp2V5fKvSUEwEecxaTmvBEn3fvZ9COSgL8ufq9hCCT/6r3Bxl0Pp28Fj23pLwuM6tCKjriM4w5WY0xY2Sv4unz6u7LNec5DZH3QZPH4/pdRVBmC3rxt3quw8IMt6NNXG3ODUPRqS7GB44Q+k8L+15Dn5Cx22o92wULpXKTDvHmLLj2lej50V0cyoj7U4SbDkVOohUG23fn10/AzvZogYA2ugf3YXKJCBbyyloJV6jFnZAAvKRSD8B5PFnuhf+uLEAfX9Of1FLNO2c7XIQa86ht5xrpsRFCzNCFhtyHNc8pXwSv7EUv7K2bccCGKv0a7DxYquP0OB3OSkI6tvRc3mvYpe5k2WKNHFRByud14ywGxMn0FYBwtsHbW3QJ/z/WaFYS2hJXjtpjHQW1wNDy+67UTiZFi1anovUHoZvkrIKtRqY1eyXPJB31TqPHWSX0er0IfdhfE3o9mMuoSVUip6S29eY7hhVk5S3IsK1YlPAFW8pa9OPiUBt8z+gbf8WN23qi61Bn7g=

View File

@ -19,8 +19,6 @@ RUN apk update && \
libpng-dev \ libpng-dev \
&& rm -rf /var/cache/apk/* && rm -rf /var/cache/apk/*
RUN npm install --global gatsby-cli --no-optional
RUN mkdir -p /portfolio RUN mkdir -p /portfolio
WORKDIR /portfolio WORKDIR /portfolio
VOLUME /portfolio VOLUME /portfolio

View File

@ -15,19 +15,19 @@
## Table of Contents ## Table of Contents
* [Features](#features) - [Features](#features)
* [One data file to rule all pages](#one-data-file-to-rule-all-pages) - [One data file to rule all pages](#one-data-file-to-rule-all-pages)
* [Theme switcher](#theme-switcher) - [Theme switcher](#theme-switcher)
* [SEO component](#seo-component) - [SEO component](#seo-component)
* [Client-side vCard creation](#client-side-vcard-creation) - [Client-side vCard creation](#client-side-vcard-creation)
* [Matomo (formerly Piwik) analytics tracking](#matomo-formerly-piwik-analytics-tracking) - [Matomo (formerly Piwik) analytics tracking](#matomo-formerly-piwik-analytics-tracking)
* [Project images](#project-images) - [Project images](#project-images)
* [Importing SVG assets](#importing-svg-assets) - [Importing SVG assets](#importing-svg-assets)
* [Development](#development) - [Development](#development)
* [Linting](#linting) - [Linting](#linting)
* [Add a new project](#add-a-new-project) - [Add a new project](#add-a-new-project)
* [Deployment](#deployment) - [Deployment](#deployment)
* [Licenses](#licenses) - [Licenses](#licenses)
--- ---
@ -79,10 +79,10 @@ All project images use one single component defined in [`src/components/atoms/Pr
### Importing SVG assets ### Importing SVG assets
Makes use of `gatsby-plugin-svgr` so SVG assets can be imported like so: All SVG assets under `src/images/` will be converted to React components before every build. Makes use of `SVGR` so SVG assets can be imported like so:
```js ```js
import { ReactComponent as Logo } from '../images/logo.svg' import Logo from './components/svg/Logo'
``` ```
## Development ## Development
@ -133,8 +133,8 @@ portfolio-SLUG-03.png
Automatic deployments are triggered upon successful tests & builds on Travis: Automatic deployments are triggered upon successful tests & builds on Travis:
* push to `master` initiates a live deployment - push to `master` initiates a live deployment
* any Pull Request, and subsequent pushes to it, initiates a beta deployment - any Pull Request, and subsequent pushes to it, initiates a beta deployment
The deploy command simply calls the [`scripts/deploy.sh`](scripts/deploy.sh) script, syncing the contents of the `public/` folder to S3: The deploy command simply calls the [`scripts/deploy.sh`](scripts/deploy.sh) script, syncing the contents of the `public/` folder to S3:

10
gatsby-browser.js Normal file
View File

@ -0,0 +1,10 @@
exports.onInitialClientRender = () => {
require('./src/styles/base.scss')
}
exports.onClientEntry = () => {
// IntersectionObserver polyfill for gatsby-image (Safari, IE)
if (typeof window.IntersectionObserver === 'undefined') {
require('intersection-observer')
}
}

View File

@ -2,20 +2,19 @@ const path = require('path')
const fs = require('fs') const fs = require('fs')
const yaml = require('js-yaml') const yaml = require('js-yaml')
const meta = yaml.load(fs.readFileSync('./data/meta.yml', 'utf8')) const meta = yaml.load(fs.readFileSync('./data/meta.yml', 'utf8'))
const { url, matomoUrl, matomoSite } = meta const { url, matomoSite, matomoUrl } = meta
module.exports = { module.exports = {
siteMetadata: { siteMetadata: {
siteUrl: `${url}` siteUrl: `${url}`
}, },
plugins: [ plugins: [
'gatsby-plugin-react-next',
'gatsby-plugin-react-helmet', 'gatsby-plugin-react-helmet',
'gatsby-transformer-yaml', 'gatsby-transformer-yaml',
'gatsby-transformer-sharp', 'gatsby-transformer-sharp',
'gatsby-plugin-sharp', 'gatsby-plugin-sharp',
'gatsby-plugin-sitemap', 'gatsby-plugin-sitemap',
'gatsby-plugin-offline', // 'gatsby-plugin-offline',
{ {
resolve: 'gatsby-transformer-json', resolve: 'gatsby-transformer-json',
options: { options: {
@ -59,14 +58,6 @@ module.exports = {
localScript: '/piwik.js' localScript: '/piwik.js'
} }
}, },
{
resolve: 'gatsby-plugin-svgr',
options: {
icon: false,
viewBox: true
// see https://github.com/smooth-code/svgr for a list of all options
}
},
{ {
resolve: 'gatsby-plugin-favicon', resolve: 'gatsby-plugin-favicon',
options: { options: {

View File

@ -3,17 +3,28 @@ const path = require('path')
// Intersection Observer polyfill // Intersection Observer polyfill
// requires `npm install intersection-observer` // requires `npm install intersection-observer`
// https://github.com/gatsbyjs/gatsby/issues/2288#issuecomment-334467821 // https://github.com/gatsbyjs/gatsby/issues/2288#issuecomment-334467821
exports.modifyWebpackConfig = ({ config, stage }) => { // exports.onCreateWebpackConfig = ({ actions, loaders, stage }) => {
if (stage === 'build-html') { // const { setWebpackConfig } = actions
config.loader('null', {
test: /intersection-observer/,
loader: 'null-loader'
})
}
}
exports.createPages = ({ boundActionCreators, graphql }) => { // if (stage === 'build-html') {
const { createPage } = boundActionCreators // const nullRule = {
// test: /intersection-observer/,
// use: [loaders.null()]
// }
// setWebpackConfig({
// module: {
// rules: [nullRule]
// }
// })
// }
// }
//
// Create project pages from projects.yml
//
exports.createPages = async ({ actions, graphql }) => {
const { createPage } = actions
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const template = path.resolve('src/templates/Project.jsx') const template = path.resolve('src/templates/Project.jsx')
@ -26,44 +37,6 @@ exports.createPages = ({ boundActionCreators, graphql }) => {
node { node {
slug slug
} }
previous {
title
slug
img {
id
childImageSharp {
sizes(maxWidth: 500, quality: 80) {
aspectRatio
src
srcSet
srcWebp
srcSetWebp
sizes
originalImg
originalName
}
}
}
}
next {
title
slug
img {
id
childImageSharp {
sizes(maxWidth: 500, quality: 80) {
aspectRatio
src
srcSet
srcWebp
srcSetWebp
sizes
originalImg
originalName
}
}
}
}
} }
} }
} }
@ -72,21 +45,17 @@ exports.createPages = ({ boundActionCreators, graphql }) => {
reject(result.errors) reject(result.errors)
} }
result.data.allProjectsYaml.edges.forEach( result.data.allProjectsYaml.edges.forEach(({ node }) => {
({ node, previous, next }) => {
const slug = node.slug const slug = node.slug
createPage({ createPage({
path: slug, path: slug,
component: template, component: template,
context: { context: {
slug, slug
previous,
next
} }
}) })
} })
)
resolve() resolve()
}) })

View File

@ -1,5 +1,5 @@
{ {
"name": "portfolio", "name": "@kremalicious/portfolio",
"version": "0.1.0", "version": "0.1.0",
"homepage": "https://matthiaskretschmann.com", "homepage": "https://matthiaskretschmann.com",
"repository": "github:kremalicious/portfolio", "repository": "github:kremalicious/portfolio",
@ -9,57 +9,59 @@
"scripts": { "scripts": {
"lint:js": "eslint ./gatsby-*.{js,jsx} && eslint ./src/**/*.{js,jsx}", "lint:js": "eslint ./gatsby-*.{js,jsx} && eslint ./src/**/*.{js,jsx}",
"lint:css": "stylelint ./src/**/*.{css,scss}", "lint:css": "stylelint ./src/**/*.{css,scss}",
"lint": "npm run lint:js && npm run lint:css", "lint": "npm run svg && npm run lint:js && npm run lint:css",
"build": "gatsby build", "build": "npm run svg && ./node_modules/gatsby/dist/bin/gatsby.js build",
"start": "docker-compose up", "start": "npm run svg && ./node_modules/gatsby/dist/bin/gatsby.js develop",
"format": "prettier --write 'src/**/*.{js,jsx}'", "format": "prettier --write 'src/**/*.{js,jsx}'",
"format:css": "prettier-stylelint --write --quiet 'src/**/*.{css,scss}'", "format:css": "prettier-stylelint --write --quiet 'src/**/*.{css,scss}'",
"test": "npm run lint", "test": "npm run lint",
"deploy": "./scripts/deploy.sh", "deploy": "./scripts/deploy.sh",
"new": "node ./scripts/new-project.js" "new": "node ./scripts/new.js",
"svg": "./scripts/svg.sh"
}, },
"dependencies": { "dependencies": {
"camel-case": "^3.0.0",
"file-saver": "^1.3.8", "file-saver": "^1.3.8",
"gatsby": "^1.9.270", "gatsby": "^2.0.0-beta.35",
"gatsby-image": "^1.0.52", "gatsby-image": "^2.0.0-beta.6",
"gatsby-link": "^1.6.44", "gatsby-plugin-favicon": "github:TuckerWhitehouse/gatsby-plugin-favicon#gatsby-v2",
"gatsby-plugin-favicon": "^2.1.1", "gatsby-plugin-matomo": "^0.4.1",
"gatsby-plugin-matomo": "^0.4.0", "gatsby-plugin-offline": "^2.0.0-beta.3",
"gatsby-plugin-offline": "^1.0.18", "gatsby-plugin-react-helmet": "next",
"gatsby-plugin-react-helmet": "^2.0.11", "gatsby-plugin-sass": "next",
"gatsby-plugin-react-next": "^1.0.11", "gatsby-plugin-sharp": "^2.0.0-beta.5",
"gatsby-plugin-sass": "^1.0.26", "gatsby-plugin-sitemap": "next",
"gatsby-plugin-sharp": "^1.6.47", "gatsby-source-filesystem": "^2.0.1-beta.4",
"gatsby-plugin-sitemap": "^1.2.25", "gatsby-transformer-json": "next",
"gatsby-plugin-svgr": "^1.0.1", "gatsby-transformer-sharp": "next",
"gatsby-source-filesystem": "^1.5.38", "gatsby-transformer-yaml": "next",
"gatsby-transformer-sharp": "^1.6.26", "giphy-js-sdk-core": "^1.0.5",
"gatsby-transformer-yaml": "^1.5.17", "graphql": "^0.13.2",
"giphy-js-sdk-core": "^1.0.3",
"intersection-observer": "^0.5.0", "intersection-observer": "^0.5.0",
"js-yaml": "^3.12.0", "js-yaml": "^3.12.0",
"node-sass": "^4.9.2",
"react": "^16.4.1",
"react-dom": "^16.4.1",
"react-helmet": "^5.2.0", "react-helmet": "^5.2.0",
"react-markdown": "^3.3.2", "react-markdown": "^3.3.4",
"react-transition-group": "^2.3.1", "react-transition-group": "^2.4.0",
"vcf": "^2.0.1" "vcf": "^2.0.1"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^8.2.3", "@svgr/cli": "^2.1.1",
"eslint": "^5.0.0", "babel-eslint": "^8.2.6",
"eslint": "^5.1.0",
"eslint-config-prettier": "^2.9.0", "eslint-config-prettier": "^2.9.0",
"eslint-loader": "^2.0.0", "eslint-loader": "^2.0.0",
"eslint-plugin-graphql": "^2.1.1", "eslint-plugin-graphql": "^2.1.1",
"eslint-plugin-prettier": "^2.6.0", "eslint-plugin-prettier": "^2.6.2",
"eslint-plugin-react": "^7.9.1", "eslint-plugin-react": "^7.10.0",
"gatsby-transformer-json": "^1.0.19",
"ora": "^2.1.0", "ora": "^2.1.0",
"prepend": "^1.0.2", "prepend": "^1.0.2",
"prettier": "^1.13.4", "prettier": "^1.13.7",
"prettier-stylelint": "^0.4.2", "prettier-stylelint": "^0.4.2",
"slugify": "^1.3.0", "slugify": "^1.3.0",
"stylelint": "^9.2.1", "stylelint": "^9.3.0",
"stylelint-config-css-modules": "^1.2.0", "stylelint-config-css-modules": "^1.3.0",
"stylelint-config-standard": "^18.2.0" "stylelint-config-standard": "^18.2.0"
}, },
"browserslist": [ "browserslist": [

View File

@ -1,10 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
export PATH="$PATH:/usr/local/bin/gatsby"
# echo "Running npm install..." # echo "Running npm install..."
# npm install # npm install
# rm -rf ./public # rm -rf ./public
gatsby develop --host 0.0.0.0 npm start

View File

@ -4,7 +4,7 @@ const prepend = require('prepend')
const slugify = require('slugify') const slugify = require('slugify')
const ora = require('ora') const ora = require('ora')
const templatePath = path.join(__dirname, 'new-project.yml') const templatePath = path.join(__dirname, 'new.yml')
const template = fs.readFileSync(templatePath).toString() const template = fs.readFileSync(templatePath).toString()
const spinner = ora('Adding new project').start() const spinner = ora('Adding new project').start()

12
scripts/svg.sh Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -e
SRC='./src/images'
OUT='./src/components/svg'
printf "Creating SVG components...\\n\\n"
# Usage: svgr [-d out-dir] [src-dir]
./node_modules/@svgr/cli/bin/svgr --icon -d $OUT $SRC
printf "\\n🎉 Successfully created SVG components\\n\\n"

80
src/components/Layout.jsx Normal file
View File

@ -0,0 +1,80 @@
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import { StaticQuery, graphql } from 'gatsby'
import Head from './atoms/Head'
import Header from './organisms/Header'
import Footer from './organisms/Footer'
import styles from './Layout.module.scss'
const Layout = ({ children, location }) => {
const isHomepage = location.pathname === '/'
return (
<StaticQuery
query={graphql`
query {
# the data/meta.yml file
dataYaml {
title
tagline
description
url
email
avatar {
childImageSharp {
original: resize {
src
}
small: resize(width: 256) {
src
}
}
}
img {
childImageSharp {
resize(width: 980) {
src
}
}
}
social {
Email
Blog
Twitter
GitHub
Dribbble
}
availability {
status
}
gpg
addressbook
}
}
`}
render={data => {
const meta = data.dataYaml
return (
<Fragment>
<Head meta={meta} />
<Header meta={meta} isHomepage={isHomepage} />
<main className={styles.screen} location={location}>
{children}
</main>
<Footer meta={meta} />
</Fragment>
)
}}
/>
)
}
Layout.propTypes = {
children: PropTypes.any.isRequired,
location: PropTypes.object.isRequired
}
export default Layout

View File

@ -0,0 +1,6 @@
@import 'variables';
.screen {
flex: 1;
padding: $spacer;
}

View File

@ -0,0 +1,15 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './Button.module.scss'
const Button = props => (
<a className={styles.button} {...props}>
{props.children}
</a>
)
Button.propTypes = {
children: PropTypes.node
}
export default Button

View File

@ -0,0 +1,48 @@
@import 'variables';
.button {
display: block;
width: 100%;
color: $brand-cyan;
text-align: center;
border-radius: .25rem;
padding: $spacer / 4 $spacer / 2;
transition-property: all;
background: rgba(#fff, .15);
border: .05rem solid rgba($brand-cyan, .75);
font-size: $font-size-small;
text-transform: uppercase;
svg {
fill: $brand-grey-light;
margin-right: $spacer / 3;
transition: .2s ease-out;
margin-bottom: -.1rem;
width: $font-size-small;
height: $font-size-small;
}
&:hover,
&:focus {
color: lighten($brand-cyan, 10%);
border-color: rgba(lighten($brand-cyan, 10%), .75);
transform: translate3d(0, -.1rem, 0);
box-shadow: 0 6px 10px rgba($brand-main, .1),
0 10px 25px rgba($brand-main, .05);
}
&:active {
transition: none;
background: rgba(#fff, .15);
}
:global(.dark) & {
background: darken($body-background-color--dark, 1%);
&:hover,
&:focus {
box-shadow: 0 6px 10px rgba(darken($brand-main, 20%), .1),
0 10px 25px rgba(darken($brand-main, 20%), .2);
}
}
}

View File

@ -5,7 +5,7 @@ import SEO from './SEO'
import Typekit from './Typekit' import Typekit from './Typekit'
const Head = ({ meta }) => { const Head = ({ meta }) => {
const { title, tagline, typekitID } = meta const { title, tagline } = meta
return ( return (
<Fragment> <Fragment>
@ -17,7 +17,7 @@ const Head = ({ meta }) => {
<meta name="theme-color" content="#e7eef4" /> <meta name="theme-color" content="#e7eef4" />
</Helmet> </Helmet>
{typekitID && <Typekit id={typekitID} />} <Typekit />
<SEO meta={meta} /> <SEO meta={meta} />
</Fragment> </Fragment>

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { ReactComponent as Logo } from '../../images/logo.svg' import Logo from '../svg/Logo'
import styles from './LogoUnit.module.scss' import styles from './LogoUnit.module.scss'
const LogoUnit = ({ meta, minimal }) => { const LogoUnit = ({ meta, minimal }) => {

View File

@ -25,11 +25,13 @@
margin-right: $spacer / 4; margin-right: $spacer / 4;
color: $brand-main; color: $brand-main;
line-height: $line-height; line-height: $line-height;
transition: color .2s ease-out;
} }
.logounit__description { .logounit__description {
font-size: $font-size-h4; font-size: $font-size-h4;
color: $brand-grey; color: $brand-grey;
transition: color .2s ease-out;
:global(.dark) & { :global(.dark) & {
color: $brand-grey-light; color: $brand-grey-light;

View File

@ -1,28 +1,28 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { graphql } from 'gatsby'
import Img from 'gatsby-image' import Img from 'gatsby-image'
import 'intersection-observer' import styles from './ProjectImage.module.scss'
import './ProjectImage.scss'
const ProjectImage = ({ sizes, alt }) => ( const ProjectImage = props => (
<Img <Img
className="project__image" className={styles.project__image}
outerWrapperClassName="project__image-wrap" outerWrapperClassName={styles.project__imagewrap}
backgroundColor="#6b7f88" backgroundColor="#6b7f88"
sizes={sizes} fluid={props.fluid}
alt={alt} alt={props.alt}
/> />
) )
ProjectImage.propTypes = { ProjectImage.propTypes = {
sizes: PropTypes.object.isRequired, fluid: PropTypes.object.isRequired,
alt: PropTypes.string alt: PropTypes.string
} }
export const projectImage = graphql` export const projectImage = graphql`
fragment ProjectImageSizes on ImageSharp { fragment ProjectImageFluid on ImageSharp {
sizes(maxWidth: 1200, quality: 85) { fluid(maxWidth: 1200, quality: 85) {
...GatsbyImageSharpSizes_noBase64 ...GatsbyImageSharpFluid_withWebp
} }
} }
` `

View File

@ -1,6 +1,6 @@
@import 'variables'; @import 'variables';
.project__image-wrap { .project__imagewrap {
max-height: 100vh; max-height: 100vh;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@ -14,7 +14,7 @@
overflow: hidden; overflow: hidden;
} }
.dark & { :global(.dark) & {
box-shadow: 0 3px 5px rgba(darken($brand-main, 20%), .15), box-shadow: 0 3px 5px rgba(darken($brand-main, 20%), .15),
0 5px 16px rgba(darken($brand-main, 20%), .15); 0 5px 16px rgba(darken($brand-main, 20%), .15);
} }

View File

@ -1,13 +1,14 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { StaticQuery, graphql } from 'gatsby'
import Helmet from 'react-helmet' import Helmet from 'react-helmet'
const TypekitScript = props => ( const TypekitScript = typekitID => (
<script> <script>
{` {`
(function(d) { (function(d) {
var config = { var config = {
kitId: '${props.id}', kitId: '${typekitID}',
scriptTimeout: 3000, scriptTimeout: 3000,
async: true async: true
}, },
@ -17,13 +18,30 @@ const TypekitScript = props => (
</script> </script>
) )
const Typekit = props => ( const Typekit = () => (
<StaticQuery
query={graphql`
query {
dataYaml {
typekitID
}
}
`}
render={data => {
const { typekitID } = data.dataYaml
return (
typekitID && (
<Helmet> <Helmet>
<link rel="dns-prefetch" href="https://use.typekit.net/" /> <link rel="dns-prefetch" href="https://use.typekit.net/" />
<link rel="dns-prefetch" href="https://p.typekit.net/" /> <link rel="dns-prefetch" href="https://p.typekit.net/" />
{TypekitScript(props)} {TypekitScript(typekitID)}
</Helmet> </Helmet>
)
)
}}
/>
) )
export default Typekit export default Typekit

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import FileSaver from 'file-saver' import FileSaver from 'file-saver'
import vCard from '../../lib/vcf/vcard' import vCard from 'vcf'
class Vcard extends PureComponent { class Vcard extends PureComponent {
constructor(props) { constructor(props) {

View File

@ -1,5 +1,6 @@
import React, { Fragment, PureComponent } from 'react' import React, { Fragment, PureComponent } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { StaticQuery, graphql } from 'gatsby'
import { MoveIn } from '../atoms/Animations' import { MoveIn } from '../atoms/Animations'
import styles from './Availability.module.scss' import styles from './Availability.module.scss'
@ -9,7 +10,21 @@ class Availability extends PureComponent {
} }
render() { render() {
const { availability } = this.props.meta return (
<StaticQuery
query={graphql`
query {
dataYaml {
availability {
status
available
unavailable
}
}
}
`}
render={data => {
const { availability } = data.dataYaml
const { status, available, unavailable } = availability const { status, available, unavailable } = availability
return ( return (
@ -33,11 +48,13 @@ class Availability extends PureComponent {
)} )}
</Fragment> </Fragment>
) )
}}
/>
)
} }
} }
Availability.propTypes = { Availability.propTypes = {
meta: PropTypes.object,
hide: PropTypes.bool hide: PropTypes.bool
} }

View File

@ -2,11 +2,11 @@ import React, { PureComponent } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { FadeIn } from '../atoms/Animations' import { FadeIn } from '../atoms/Animations'
import { ReactComponent as Email } from '../../images/email.svg' import Email from '../svg/Email'
import { ReactComponent as Blog } from '../../images/blog.svg' import Blog from '../svg/Blog'
import { ReactComponent as Twitter } from '../../images/twitter.svg' import Twitter from '../svg/Twitter'
import { ReactComponent as GitHub } from '../../images/github.svg' import GitHub from '../svg/Github'
import { ReactComponent as Dribbble } from '../../images/dribbble.svg' import Dribbble from '../svg/Dribbble'
import icons from '../atoms/Icons.module.scss' import icons from '../atoms/Icons.module.scss'
import styles from './Networks.module.scss' import styles from './Networks.module.scss'

View File

@ -1,12 +1,13 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { ReactComponent as Link } from '../../images/link.svg' import Button from '../atoms/Button'
import { ReactComponent as Download } from '../../images/download.svg' import Link from '../svg/Link'
import { ReactComponent as Info } from '../../images/info.svg' import Download from '../svg/Download'
import { ReactComponent as Styleguide } from '../../images/styleguide.svg' import Info from '../svg/Info'
import { ReactComponent as GitHub } from '../../images/github.svg' import Styleguide from '../svg/Styleguide'
import { ReactComponent as Dribbble } from '../../images/dribbble.svg' import GitHub from '../svg/Github'
import Dribbble from '../svg/Dribbble'
import icons from '../atoms/Icons.module.scss' import icons from '../atoms/Icons.module.scss'
import styles from './ProjectLinks.module.scss' import styles from './ProjectLinks.module.scss'
@ -42,10 +43,10 @@ const ProjectLinks = ({ links }) => (
return ( return (
<li key={title}> <li key={title}>
<a href={url}> <Button href={url}>
<LinkIcon title={title} className={icons.icon} /> <LinkIcon title={title} className={icons.icon} />
{title} {title}
</a> </Button>
</li> </li>
) )
})} })}

View File

@ -16,49 +16,6 @@
margin-left: $spacer / 2; margin-left: $spacer / 2;
margin-bottom: $spacer / 2; margin-bottom: $spacer / 2;
} }
svg {
margin-right: $spacer / 3;
transition: .2s ease-out;
margin-bottom: -.1rem;
}
a {
display: block;
width: 100%;
color: $brand-cyan;
text-align: center;
border-radius: .25rem;
padding: $spacer / 4 $spacer / 2;
transition-property: all;
background: rgba(#fff, .15);
svg {
fill: $brand-grey-light;
}
&:hover,
&:focus {
transform: translate3d(0, -.1rem, 0);
box-shadow: 0 6px 10px rgba($brand-main, .1),
0 10px 25px rgba($brand-main, .1);
}
&:active {
transition: none;
background: rgba(#fff, .15);
}
:global(.dark) & {
background: darken($body-background-color--dark, 1%);
&:hover,
&:focus {
box-shadow: 0 6px 10px rgba(darken($brand-main, 20%), .1),
0 10px 25px rgba(darken($brand-main, 20%), .1);
}
}
}
} }
.title { .title {

View File

@ -1,14 +1,15 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Link from 'gatsby-link' import { Link, graphql, StaticQuery } from 'gatsby'
import Img from 'gatsby-image' import Img from 'gatsby-image'
import FullWidth from '../atoms/FullWidth'
import styles from './ProjectNav.module.scss' import styles from './ProjectNav.module.scss'
const ProjectLink = ({ node }) => ( const ProjectLink = ({ node }) => (
<Link className={styles.link} to={node.slug}> <Link className={styles.link} to={node.slug}>
<Img <Img
className={styles.image} className={styles.image}
sizes={node.img.childImageSharp.sizes} fluid={node.img.childImageSharp.fluid}
alt={node.title} alt={node.title}
/> />
<h1 className={styles.title}>{node.title}</h1> <h1 className={styles.title}>{node.title}</h1>
@ -19,52 +20,86 @@ class ProjectNav extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = {
scrolledToCurrent: false
}
this.scrollToCurrent = this.scrollToCurrent.bind(this) this.scrollToCurrent = this.scrollToCurrent.bind(this)
} }
componentDidMount() { componentDidMount() {
this.scrollToCurrent() this.scrollToCurrent()
this.setState({ scrolledToCurrent: true })
} }
componentDidUpdate() { componentDidUpdate() {
this.scrollToCurrent() this.scrollToCurrent()
} }
componentWillUnmount() {
this.setState({ scrolledToCurrent: false })
}
scrollToCurrent = () => { scrollToCurrent = () => {
const current = this.currentItem const current = this.currentItem
const currentLeft = current.getBoundingClientRect().left const currentLeft = current.getBoundingClientRect().left
const currentWidth = current.clientWidth const currentWidth = current.clientWidth
const finalPosition = currentLeft - window.innerWidth / 2 + currentWidth / 2 const finalPosition = currentLeft - window.innerWidth / 2 + currentWidth / 2
this.scrollContainer.scrollLeft = finalPosition this.scrollContainer.scrollLeft += finalPosition
} }
render() { render() {
const { projects, project } = this.props const { slug } = this.props
return ( return (
<StaticQuery
query={graphql`
query {
allProjectsYaml {
edges {
node {
title
slug
img {
childImageSharp {
fluid(maxWidth: 500, quality: 85) {
...GatsbyImageSharpFluid_noBase64
}
}
}
}
}
}
}
`}
render={data => {
const projects = data.allProjectsYaml.edges
return (
<FullWidth>
<nav <nav
className={styles.projectNav} className={styles.projectNav}
ref={node => { ref={node => (this.scrollContainer = node)}
this.scrollContainer = node
}}
> >
{projects.map(({ node }) => { {projects.map(({ node }) => {
const current = node.slug === project.slug const current = node.slug === slug
return ( return (
<div <div
className={styles.item} className={styles.item}
key={node.slug} key={node.slug}
ref={node => { ref={node => current && (this.currentItem = node)}
if (current) this.currentItem = node
}}
> >
<ProjectLink node={node} /> <ProjectLink node={node} />
</div> </div>
) )
})} })}
</nav> </nav>
</FullWidth>
)
}}
/>
) )
} }
} }
@ -74,8 +109,7 @@ ProjectLink.propTypes = {
} }
ProjectNav.propTypes = { ProjectNav.propTypes = {
projects: PropTypes.array, slug: PropTypes.string
project: PropTypes.object
} }
export default ProjectNav export default ProjectNav

View File

@ -1,7 +1,7 @@
@import 'variables'; @import 'variables';
.projectNav { .projectNav {
composes: fullWidth from '../atoms/FullWidth.module.scss'; // composes: fullWidth from '../atoms/FullWidth.module.scss';
white-space: nowrap; white-space: nowrap;
overflow-y: hidden; overflow-y: hidden;
overflow-x: scroll; overflow-x: scroll;

View File

@ -16,11 +16,12 @@
text-align: center; text-align: center;
background: rgba(#fff, .15); background: rgba(#fff, .15);
border-radius: .25rem; border-radius: .25rem;
border: .05rem solid transparent;
color: $brand-grey-light; color: $brand-grey-light;
margin-left: $spacer / 2; margin-left: $spacer / 2;
margin-bottom: $spacer / 2; margin-bottom: $spacer / 2;
flex: 0 0 calc(50% - #{$spacer / 2}); flex: 0 0 calc(50% - #{$spacer / 2});
font-size: $font-size-base; font-size: $font-size-small;
:global(.dark) & { :global(.dark) & {
background: darken($body-background-color--dark, 1%); background: darken($body-background-color--dark, 1%);

View File

@ -1,53 +1,65 @@
import React, { PureComponent, Fragment } from 'react' import React, { PureComponent, Fragment } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Helmet from 'react-helmet' import Helmet from 'react-helmet'
import { FadeIn } from '../atoms/Animations' import Day from '../svg/Day'
import { ReactComponent as Day } from '../../images/day.svg' import Night from '../svg/Night'
import { ReactComponent as Night } from '../../images/night.svg'
import styles from './ThemeSwitch.module.scss' import styles from './ThemeSwitch.module.scss'
const ThemeToggle = props => { const ThemeToggle = props => (
return ( <span id="toggle" className={styles.checkboxContainer} aria-live="assertive">
<span
id="toggle"
className={styles.checkboxContainer}
aria-live="assertive"
>
<Day className={props.dark ? null : 'active'} /> <Day className={props.dark ? null : 'active'} />
<span className={styles.checkboxFake} /> <span className={styles.checkboxFake} />
<Night className={props.dark ? 'active' : null} /> <Night className={props.dark ? 'active' : null} />
</span> </span>
) )
}
class ThemeSwitch extends PureComponent { class ThemeSwitch extends PureComponent {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { dark: false } this.state = { dark: null }
}
componentDidMount() {
const now = new Date().getHours()
if (now >= 19 || now <= 7) {
this.setState({ dark: true })
}
} }
isDark = () => this.state.dark === true isDark = () => this.state.dark === true
handleChange = () => { darkLocalStorageMode = darkLocalStorage => {
this.setState({ dark: !this.isDark() }) if (darkLocalStorage === 'true') {
this.setState({ dark: true })
} else {
this.setState({ dark: false })
}
}
darkMode = now => {
if (!this.isDark() && (now >= 19 || now <= 7)) {
this.setState({ dark: true })
} else {
this.setState({ dark: null })
}
}
componentDidMount() {
const now = new Date().getHours()
const darkLocalStorage = localStorage.getItem('dark')
if (darkLocalStorage) {
this.darkLocalStorageMode(darkLocalStorage)
} else {
this.darkMode(now)
}
}
handleChange = event => {
this.setState({ dark: event.target.checked })
localStorage.setItem('dark', event.target.checked)
} }
render() { render() {
return ( return (
<Fragment> <Fragment>
<Helmet> <Helmet>
<body className={this.state.dark ? 'dark' : null} /> <body className={this.isDark() ? 'dark' : null} />
</Helmet> </Helmet>
<FadeIn>
<aside className={styles.themeSwitch}> <aside className={styles.themeSwitch}>
<label className={styles.checkbox}> <label className={styles.checkbox}>
<span className={styles.label}>Toggle Night Mode</span> <span className={styles.label}>Toggle Night Mode</span>
@ -57,12 +69,11 @@ class ThemeSwitch extends PureComponent {
name="toggle" name="toggle"
value="toggle" value="toggle"
aria-describedby="toggle" aria-describedby="toggle"
checked={this.state.dark} checked={this.isDark()}
/> />
<ThemeToggle dark={this.state.dark} /> <ThemeToggle dark={this.isDark()} />
</label> </label>
</aside> </aside>
</FadeIn>
</Fragment> </Fragment>
) )
} }

View File

@ -1,5 +1,6 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { StaticQuery, graphql } from 'gatsby'
import Vcard from '../atoms/Vcard' import Vcard from '../atoms/Vcard'
import LogoUnit from '../atoms/LogoUnit' import LogoUnit from '../atoms/LogoUnit'
import Networks from '../molecules/Networks' import Networks from '../molecules/Networks'
@ -14,7 +15,22 @@ class Footer extends PureComponent {
render() { render() {
const meta = this.props.meta const meta = this.props.meta
const pkg = this.props.pkg
return (
<StaticQuery
query={graphql`
query {
# the package.json file
portfolioJson {
name
homepage
repository
bugs
}
}
`}
render={data => {
const pkg = data.portfolioJson
return ( return (
<footer className={styles.footer}> <footer className={styles.footer}>
@ -28,11 +44,15 @@ class Footer extends PureComponent {
</p> </p>
<p className={styles.footer__copyright}> <p className={styles.footer__copyright}>
<small> <small>
&copy; {this.state.year} {meta.title} &mdash; All Rights Reserved &copy; {this.state.year} {meta.title} &mdash; All Rights
Reserved
</small> </small>
</p> </p>
</footer> </footer>
) )
}}
/>
)
} }
} }

View File

@ -38,5 +38,5 @@
} }
.footer__copyright { .footer__copyright {
opacity: .6; opacity: .7;
} }

View File

@ -1,7 +1,6 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import Link from 'gatsby-link' import { Link } from 'gatsby'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { FadeIn } from '../atoms/Animations'
import Networks from '../molecules/Networks' import Networks from '../molecules/Networks'
import Availability from '../molecules/Availability' import Availability from '../molecules/Availability'
import ThemeSwitch from '../molecules/ThemeSwitch' import ThemeSwitch from '../molecules/ThemeSwitch'
@ -11,53 +10,34 @@ import styles from './Header.module.scss'
class Header extends PureComponent { class Header extends PureComponent {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { classes: 'header' }
}
componentDidMount() {
this.toggleClasses()
}
componentDidUpdate() {
this.toggleClasses()
}
toggleClasses = () => {
if (this.props.isHomepage) {
this.setState({ classes: styles.header })
} else {
this.setState({ classes: `${styles.header} ${styles.minimal}` })
}
} }
render() { render() {
const meta = this.props.meta const { isHomepage, meta } = this.props
const isHomepage = this.props.isHomepage
return ( return (
<header className={this.state.classes}> <header
className={
isHomepage ? `${styles.header}` : `${styles.header} ${styles.minimal}`
}
>
<ThemeSwitch /> <ThemeSwitch />
<FadeIn>
<Link className={styles.header__link} to={'/'}> <Link className={styles.header__link} to={'/'}>
<LogoUnit meta={meta} minimal={!isHomepage} /> <LogoUnit meta={meta} minimal={!isHomepage} />
</Link> </Link>
</FadeIn>
<Networks meta={meta} hide={!isHomepage} /> <Networks meta={meta} hide={!isHomepage} />
<Availability <Availability hide={!isHomepage && !meta.availability.status} />
meta={meta}
hide={!isHomepage && !meta.availability.status}
/>
</header> </header>
) )
} }
} }
Header.propTypes = { Header.propTypes = {
meta: PropTypes.object, isHomepage: PropTypes.bool,
isHomepage: PropTypes.bool meta: PropTypes.object
} }
export default Header export default Header

View File

@ -1,118 +0,0 @@
import React, { Component, Fragment } from 'react'
import PropTypes from 'prop-types'
import withRouter from 'react-router-dom/withRouter'
import TransitionGroup from 'react-transition-group/TransitionGroup'
import Head from '../components/atoms/Head'
import Header from '../components/organisms/Header'
import Footer from '../components/organisms/Footer'
import { FadeIn } from '../components/atoms/Animations'
import './index.scss'
class TransitionHandler extends Component {
shouldComponentUpdate() {
return this.props.location.pathname === window.location.pathname
}
render() {
const { children } = this.props
return <div className="transition-container">{children}</div>
}
}
const Main = ({ children }) => <main className="screen">{children}</main>
const TemplateWrapper = ({ data, location, children }) => {
const meta = data.dataYaml
const pkg = data.portfolioJson
const isHomepage = location.pathname === '/'
return (
<Fragment>
<Head meta={meta} />
<Header meta={meta} isHomepage={isHomepage} />
<TransitionGroup component={Main} appear={true}>
<FadeIn
key={location.pathname}
timeout={{ enter: 200, exit: 150, appear: 200 }}
>
<TransitionHandler location={location}>
{children()}
</TransitionHandler>
</FadeIn>
</TransitionGroup>
<Footer meta={meta} pkg={pkg} />
</Fragment>
)
}
TransitionHandler.propTypes = {
children: PropTypes.any,
location: PropTypes.object.isRequired
}
Main.propTypes = {
children: PropTypes.any
}
TemplateWrapper.propTypes = {
children: PropTypes.func,
data: PropTypes.object.isRequired,
location: PropTypes.object.isRequired
}
export default withRouter(TemplateWrapper)
export const query = graphql`
query metaQuery {
# the data/meta.yml file
dataYaml {
title
tagline
description
url
email
avatar {
childImageSharp {
original: resize {
src
}
small: resize(width: 256) {
src
}
}
}
img {
childImageSharp {
resize(width: 980) {
src
}
}
}
social {
Email
Blog
Twitter
GitHub
Dribbble
}
availability {
status
available
unavailable
}
gpg
addressbook
typekitID
}
# the package.json file
portfolioJson {
name
homepage
repository
bugs
}
}
`

View File

@ -1,71 +0,0 @@
var camelCase = require('camel-case') // that's the only change
var Property = require('./property')
function set(object, key, value) {
if (Array.isArray(object[key])) {
object[key].push(value)
} else if (object[key] != null) {
object[key] = [object[key], value]
} else {
object[key] = value
}
}
function createParams(params, param) {
var parts = param.split('=')
var k = camelCase(parts[0])
var value = parts[1]
if (value == null || value === '') {
value = parts[0]
k = 'type'
}
if (k === 'type') {
value
.toLowerCase()
.split(',')
.forEach(function(value) {
set(params, k, value)
})
return params
}
set(params, k, value)
return params
}
function parseLines(lines) {
var data = {}
// NOTE: Line format:
// PROPERTY[;PARAMETER[=VALUE]]:Attribute[;Attribute]
var line = null
var pattern = /^([^;:]+)((?:;(?:[^;:]+))*)(?:\:(.+))?$/i // eslint-disable-line no-useless-escape
var len = lines.length - 1
for (var i = 1; i < len; i++) {
line = lines[i]
var match = pattern.exec(line)
if (!match) continue
var name = match[1].split('.')
var property = name.pop()
var group = name.pop()
var value = match[3]
var params = match[2] ? match[2].replace(/^;|;$/g, '').split(';') : []
var propParams = params.reduce(createParams, group ? { group: group } : {})
var propName = camelCase(property)
var propVal = new Property(propName, value, propParams)
set(data, propName, propVal)
}
return data
}
module.exports = parseLines

View File

@ -1,146 +0,0 @@
/* eslint-disable no-unused-vars */
/**
* vCard Property
* @constructor
* @memberOf vCard
* @param {String} field
* @param {String} value
* @param {Object} params
* @return {Property}
*/
function Property(field, value, params) {
if (!(this instanceof Property)) return new Property(value)
if (params != null) Object.assign(this, params)
this._field = field
this._data = value
Object.defineProperty(this, '_field', { enumerable: false })
Object.defineProperty(this, '_data', { enumerable: false })
}
/**
* Constructs a vCard.Property from jCard data
* @param {Array} data
* @return {Property}
*/
Property.fromJSON = function(data) {
var field = data[0]
var params = data[1]
if (!/text/i.test(data[2])) params.value = data[2]
var value = Array.isArray(data[3]) ? data[3].join(';') : data[3]
return new Property(field, value, params)
}
/**
* Turn a string into capitalized dash-case
* @internal used by `Property#toString()`
* @param {String} value
* @return {String}
* @ignore
*/
function capitalDashCase(value) {
return value.replace(/([A-Z])/g, '-$1').toUpperCase()
}
/**
* Property prototype
* @type {Object}
*/
Property.prototype = {
constructor: Property,
/**
* Check whether the property is of a given type
* @param {String} type
* @return {Boolean}
*/
is: function(type) {
type = (type + '').toLowerCase()
return Array.isArray(this.type)
? this.type.indexOf(type)
: this.type === type
},
/**
* Check whether the property is empty
* @return {Boolean}
*/
isEmpty: function() {
return this._data == null && Object.keys(this).length === 0
},
/**
* Clone the property
* @return {Property}
*/
clone: function() {
return new Property(this._field, this._data, this)
},
/**
* Format the property as vcf with given version
* @param {String} version
* @return {String}
*/
toString: function(version) {
var propName =
(this.group ? this.group + '.' : '') + capitalDashCase(this._field)
var keys = Object.keys(this)
var params = []
for (var i = 0; i < keys.length; i++) {
if (keys[i] === 'group') continue
params.push(capitalDashCase(keys[i]) + '=' + this[keys[i]])
}
return (
propName +
(params.length ? ';' + params.join(';') : params) +
':' +
(Array.isArray(this._data) ? this._data.join(';') : this._data)
)
},
/**
* Get the property's value
* @return {String}
*/
valueOf: function() {
return this._data
},
/**
* Format the property as jCard data
* @return {Array}
*/
toJSON: function() {
var params = Object.assign({}, this)
if (params.value === 'text') {
params.value = void 0
delete params.value
}
var data = [this._field, params, this.value || 'text']
switch (this._field) {
default:
data.push(this._data)
break
case 'adr':
case 'n':
data.push(this._data.split(';'))
}
return data
}
}
// Exports
module.exports = Property

View File

@ -1,322 +0,0 @@
/* eslint-disable no-unused-vars */
/**
* vCard
* @constructor
* @return {vCard}
*/
function vCard() {
if (!(this instanceof vCard)) return new vCard()
/** @type {String} Version number */
this.version = vCard.versions[vCard.versions.length - 1]
/** @type {Object} Card data */
this.data = {}
}
/**
* vCard MIME type
* @type {String}
*/
vCard.mimeType = 'text/vcard'
/**
* vCard file extension
* @type {String}
*/
vCard.extension = '.vcf'
/**
* vCard versions
* @type {Array}
*/
vCard.versions = ['2.1', '3.0', '4.0']
/**
* Folds a long line according to the RFC 5322.
* @see http://tools.ietf.org/html/rfc5322#section-2.1.1
* @param {String} input
* @param {Number} maxLength
* @param {Boolean} hardWrap
* @return {String}
*/
vCard.foldLine = require('foldline')
/**
* Normalizes input (cast to string, line folding, whitespace)
* @param {String} input
* @return {String}
*/
vCard.normalize = function(input) {
return (
(input + '')
// Trim whitespace
.replace(/^[\s\r\n]+|[\s\r\n]+$/g, '')
// Trim blank lines
.replace(/(\r?\n)\s*(\r?\n)|$/g, '$1')
// Unfold folded lines
.replace(/\r?\n[\x20\x09]+/g, '') // eslint-disable-line no-control-regex
)
}
/**
* Check whether a given version is supported
* @param {String} version
* @return {Boolean}
*/
vCard.isSupported = function(version) {
return /^\d\.\d$/.test(version) && vCard.versions.indexOf(version) !== -1
}
/**
* Parses a string or buffer into a vCard object
* @param {String|Buffer} value
* @return {Array<vCard>}
*/
vCard.parse = function(value) {
var objects = (value + '').split(/(?=BEGIN\:VCARD)/gi) // eslint-disable-line no-useless-escape
var cards = []
for (var i = 0; i < objects.length; i++) {
cards.push(new vCard().parse(objects[i]))
}
return cards
}
/**
* Parse an array of vcf formatted lines
* @internal used by `vCard#parse()`
* @type {Function}
*/
vCard.parseLines = require('./parse-lines')
/**
* Constructs a vCard from jCard data
* @param {Array} jcard
* @return {vCard}
*/
vCard.fromJSON = function(jcard) {
jcard = typeof jcard === 'string' ? JSON.parse(jcard) : jcard
if (jcard == null || !Array.isArray(jcard)) return new vCard()
if (!/vcard/i.test(jcard[0])) throw new Error('Object not in jCard format')
var card = new vCard()
jcard[1].forEach(function(prop) {
card.addProperty(vCard.Property.fromJSON(prop))
})
return card
}
/**
* Format a card object according to the given version
* @param {vCard} card
* @param {String} version
* @return {String}
*/
vCard.format = function(card, version) {
version = version || card.version || vCard.versions[vCard.versions.length - 1]
if (!vCard.isSupported(version))
throw new Error('Unsupported vCard version "' + version + '"')
var vcf = []
vcf.push('BEGIN:VCARD')
vcf.push('VERSION:' + version)
var props = Object.keys(card.data)
var prop = ''
for (var i = 0; i < props.length; i++) {
if (props[i] === 'version') continue
prop = card.data[props[i]]
if (Array.isArray(prop)) {
for (var k = 0; k < prop.length; k++) {
if (prop[k].isEmpty()) continue
vcf.push(vCard.foldLine(prop[k].toString(version), 75))
}
} else if (!prop.isEmpty()) {
vcf.push(vCard.foldLine(prop.toString(version), 75))
}
}
vcf.push('END:VCARD')
return vcf.join('\n')
}
// vCard Property constructor
vCard.Property = require('./property')
/**
* vCard prototype
* @type {Object}
*/
vCard.prototype = {
constructor: vCard,
/**
* Get a vCard property
* @param {String} key
* @return {Object|Array}
*/
get: function(key) {
if (this.data[key] == null) {
return this.data[key]
}
if (Array.isArray(this.data[key])) {
return this.data[key].map(function(prop) {
return prop.clone()
})
} else {
return this.data[key].clone()
}
},
/**
* Set a vCard property
* @param {String} key
* @param {String} value
* @param {Object} params
*/
set: function(key, value, params) {
return this.setProperty(new vCard.Property(key, value, params))
},
/**
* Add a vCard property
* @param {String} key
* @param {String} value
* @param {Object} params
*/
add: function(key, value, params) {
var prop = new vCard.Property(key, value, params)
this.addProperty(prop)
return this
},
/**
* Set a vCard property from an already
* constructed vCard.Property
* @param {vCard.Property} prop
*/
setProperty: function(prop) {
this.data[prop._field] = prop
return this
},
/**
* Add a vCard property from an already
* constructed vCard.Property
* @param {vCard.Property} prop
*/
addProperty: function(prop) {
var key = prop._field
if (Array.isArray(this.data[key])) {
this.data[key].push(prop)
} else if (this.data[key] != null) {
this.data[key] = [this.data[key], prop]
} else {
this.data[key] = prop
}
return this
},
/**
* Parse a vcf formatted vCard
* @param {String} value
* @return {vCard}
*/
parse: function(value) {
// Normalize & split
var lines = vCard.normalize(value).split(/\r?\n/g)
// Keep begin and end markers
// for eventual error messages
var begin = lines[0]
var version = lines[1]
var end = lines[lines.length - 1]
if (!/BEGIN:VCARD/i.test(begin))
throw new SyntaxError(
'Invalid vCard: Expected "BEGIN:VCARD" but found "' + begin + '"'
)
if (!/END:VCARD/i.test(end))
throw new SyntaxError(
'Invalid vCard: Expected "END:VCARD" but found "' + end + '"'
)
// TODO: For version 2.1, the VERSION can be anywhere between BEGIN & END
if (!/VERSION:\d\.\d/i.test(version))
throw new SyntaxError(
'Invalid vCard: Expected "VERSION:\\d.\\d" but found "' + version + '"'
)
this.version = version.substring(8, 11)
if (!vCard.isSupported(this.version))
throw new Error('Unsupported version "' + this.version + '"')
this.data = vCard.parseLines(lines)
return this
},
/**
* Format the vCard as vcf with given version
* @param {String} version
* @param {String} charset
* @return {String}
*/
toString: function(version, charset) {
version = version || this.version
return vCard.format(this, version)
},
/**
* Format the card as jCard
* @param {String} version='4.0'
* @return {Array} jCard
*/
toJCard: function(version) {
version = version || '4.0'
var keys = Object.keys(this.data)
var data = [['version', {}, 'text', version]]
var prop = null
for (var i = 0; i < keys.length; i++) {
if (keys[i] === 'version') continue
prop = this.data[keys[i]]
if (Array.isArray(prop)) {
for (var k = 0; k < prop.length; k++) {
data.push(prop[k].toJSON())
}
} else {
data.push(prop.toJSON())
}
}
return ['vcard', data]
},
/**
* Format the card as jCard
* @return {Array} jCard
*/
toJSON: function() {
return this.toJCard(this.version)
}
}
// Exports
module.exports = vCard

View File

@ -1,6 +1,8 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import Link from 'gatsby-link' import PropTypes from 'prop-types'
import { Link } from 'gatsby'
import giphyAPI from 'giphy-js-sdk-core' import giphyAPI from 'giphy-js-sdk-core'
import Layout from '../components/Layout'
import Content from '../components/atoms/Content' import Content from '../components/atoms/Content'
import './404.scss' import './404.scss'
@ -40,12 +42,13 @@ class NotFound extends Component {
render() { render() {
return ( return (
<Layout location={this.props.location}>
<Content className="content content--404"> <Content className="content content--404">
<h1>Shenanigans, page not found.</h1> <h1>Shenanigans, page not found.</h1>
<p> <p>
You might want to check the url, or{' '} You might want to check the url, or{' '}
<Link to={'/'}>go back to the homepage</Link>. Or just check out some <Link to={'/'}>go back to the homepage</Link>. Or just check out
fail gifs, entirely your choice. some fail gifs, entirely your choice.
</p> </p>
<video className="gif" src={this.state.gif} autoPlay loop /> <video className="gif" src={this.state.gif} autoPlay loop />
@ -56,8 +59,13 @@ class NotFound extends Component {
</a> </a>
</div> </div>
</Content> </Content>
</Layout>
) )
} }
} }
NotFound.propTypes = {
location: PropTypes.object
}
export default NotFound export default NotFound

View File

@ -1,14 +1,16 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Link from 'gatsby-link' import { Link, graphql } from 'gatsby'
import Layout from '../components/Layout'
import ProjectImage from '../components/atoms/ProjectImage' import ProjectImage from '../components/atoms/ProjectImage'
import FullWidth from '../components/atoms/FullWidth' import FullWidth from '../components/atoms/FullWidth'
import styles from './index.module.scss' import styles from './index.module.scss'
const Home = ({ data }) => { const Home = ({ data, location }) => {
const projects = data.allProjectsYaml.edges const projects = data.allProjectsYaml.edges
return ( return (
<Layout location={location}>
<FullWidth className="projects"> <FullWidth className="projects">
{projects.map(({ node }) => { {projects.map(({ node }) => {
const { slug, title, img } = node const { slug, title, img } = node
@ -17,23 +19,25 @@ const Home = ({ data }) => {
<article className={styles.project} key={slug}> <article className={styles.project} key={slug}>
<Link to={slug}> <Link to={slug}>
<h1 className={styles.title}>{title}</h1> <h1 className={styles.title}>{title}</h1>
<ProjectImage sizes={img.childImageSharp.sizes} alt={title} /> <ProjectImage fluid={img.childImageSharp.fluid} alt={title} />
</Link> </Link>
</article> </article>
) )
})} })}
</FullWidth> </FullWidth>
</Layout>
) )
} }
Home.propTypes = { Home.propTypes = {
data: PropTypes.object data: PropTypes.object,
location: PropTypes.object
} }
export default Home export default Home
export const IndexQuery = graphql` export const IndexQuery = graphql`
query IndexQuery { query {
allProjectsYaml { allProjectsYaml {
edges { edges {
node { node {
@ -41,7 +45,7 @@ export const IndexQuery = graphql`
slug slug
img { img {
childImageSharp { childImageSharp {
...ProjectImageSizes ...ProjectImageFluid
} }
} }
} }

View File

@ -10,20 +10,14 @@ html,
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 100%;
} }
html { html {
font-size: $font-size-root; font-size: $font-size-root;
background: $body-background-color;
&.wf-loading,
&.wf-inactive {
font-size: $font-size-root - 2px;
}
} }
body { body {
background: $body-background-color;
font-family: $font-family-base; font-family: $font-family-base;
font-weight: $font-weight-base; font-weight: $font-weight-base;
font-size: $font-size-base; font-size: $font-size-base;
@ -33,6 +27,14 @@ body {
font-feature-settings: 'liga', 'kern'; font-feature-settings: 'liga', 'kern';
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
min-height: 100vh;
background: $body-background-color;
transition: background .6s $easing;
&.dark {
background-color: $body-background-color--dark;
color: $text-color--dark;
}
} }
p, p,
@ -115,25 +117,4 @@ svg {
display: flex; display: flex;
min-height: 100vh; min-height: 100vh;
flex-direction: column; flex-direction: column;
background: $body-background-color;
transition: background .6s $easing;
.dark & {
background: $body-background-color--dark;
color: $text-color--dark;
}
}
.screen {
flex: 1;
padding: $spacer;
}
.transition-group {
position: relative;
}
.transition-container {
position: relative;
width: 100%;
} }

View File

@ -1,7 +1,9 @@
import React, { Component, Fragment } from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Helmet from 'react-helmet' import Helmet from 'react-helmet'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
import { graphql } from 'gatsby'
import Layout from '../components/Layout'
import Content from '../components/atoms/Content' import Content from '../components/atoms/Content'
import FullWidth from '../components/atoms/FullWidth' import FullWidth from '../components/atoms/FullWidth'
import ProjectImage from '../components/atoms/ProjectImage' import ProjectImage from '../components/atoms/ProjectImage'
@ -9,67 +11,55 @@ import ProjectTechstack from '../components/molecules/ProjectTechstack'
import ProjectLinks from '../components/molecules/ProjectLinks' import ProjectLinks from '../components/molecules/ProjectLinks'
import ProjectNav from '../components/molecules/ProjectNav' import ProjectNav from '../components/molecules/ProjectNav'
import SEO from '../components/atoms/SEO' import SEO from '../components/atoms/SEO'
import './Project.scss'
const ProjectMeta = props => { import styles from './Project.module.scss'
const { links, techstack } = props
return ( const ProjectMeta = ({ links, techstack }) => (
<footer className="project__meta"> <footer className={styles.project__meta}>
{!!links && <ProjectLinks links={links} />} {!!links && <ProjectLinks links={links} />}
{!!techstack && <ProjectTechstack techstack={techstack} />} {!!techstack && <ProjectTechstack techstack={techstack} />}
</footer> </footer>
) )
}
const ProjectImages = props => ( const ProjectImages = ({ projectImages, title }) => (
<FullWidth> <FullWidth>
{props.projectImages.map(({ node }) => ( {projectImages.map(({ node }) => (
<ProjectImage key={node.id} sizes={node.sizes} alt={props.title} /> <div className={styles.spacer} key={node.id}>
<ProjectImage fluid={node.fluid} alt={title} />
</div>
))} ))}
</FullWidth> </FullWidth>
) )
class Project extends Component { const Project = ({ data, location }) => {
constructor(props) { const meta = data.dataYaml
super(props) const project = data.projectsYaml
const projectImages = data.projectImages.edges
const description = this.props.data.projectsYaml.description
this.state = {
descriptionWithLineBreaks: description.split('\n').join('\n\n')
}
}
render() {
const meta = this.props.data.dataYaml
const projects = this.props.data.allProjectsYaml.edges
const project = this.props.data.projectsYaml
const projectImages = this.props.data.projectImages.edges
const { title, links, techstack } = project const { title, links, techstack } = project
const description = data.projectsYaml.description
const descriptionWithLineBreaks = description.split('\n').join('\n\n')
return ( return (
<Fragment> <Layout location={location}>
<Helmet title={title} /> <Helmet title={title} />
<SEO project={project} meta={meta} /> <SEO project={project} meta={meta} />
<article className="project"> <article className={styles.project}>
<Content> <Content>
<h1 className="project__title">{title}</h1> <h1 className={styles.project__title}>{title}</h1>
<ReactMarkdown <ReactMarkdown
source={this.state.descriptionWithLineBreaks} source={descriptionWithLineBreaks}
className="project__description" className={styles.project__description}
/> />
<ProjectImages projectImages={projectImages} title={title} /> <ProjectImages projectImages={projectImages} title={title} />
<ProjectMeta links={links} techstack={techstack} /> <ProjectMeta links={links} techstack={techstack} />
</Content> </Content>
</article> </article>
<ProjectNav projects={projects} project={project} /> <ProjectNav slug={project.slug} />
</Fragment> </Layout>
) )
}
} }
ProjectMeta.propTypes = { ProjectMeta.propTypes = {
@ -84,13 +74,13 @@ ProjectImages.propTypes = {
Project.propTypes = { Project.propTypes = {
data: PropTypes.object.isRequired, data: PropTypes.object.isRequired,
pathContext: PropTypes.object.isRequired location: PropTypes.object.isRequired
} }
export default Project export default Project
export const projectAndProjectsQuery = graphql` export const projectAndProjectsQuery = graphql`
query ProjectBySlug($slug: String!) { query($slug: String!) {
projectsYaml(slug: { eq: $slug }) { projectsYaml(slug: { eq: $slug }) {
title title
slug slug
@ -122,11 +112,6 @@ export const projectAndProjectsQuery = graphql`
GitHub GitHub
Dribbble Dribbble
} }
availability {
status
available
unavailable
}
img { img {
childImageSharp { childImageSharp {
resize(width: 980) { resize(width: 980) {
@ -137,29 +122,13 @@ export const projectAndProjectsQuery = graphql`
} }
projectImages: allImageSharp( projectImages: allImageSharp(
filter: { id: { regex: $slug } } filter: { fluid: { originalName: { regex: $slug } } }
sort: { fields: [id], order: ASC } sort: { fields: [fluid___originalName], order: ASC }
) { ) {
edges { edges {
node { node {
id id
...ProjectImageSizes ...ProjectImageFluid
}
}
}
allProjectsYaml {
edges {
node {
title
slug
img {
childImageSharp {
sizes(maxWidth: 500, quality: 85) {
...GatsbyImageSharpSizes_noBase64
}
}
}
} }
} }
} }

View File

@ -1,13 +1,14 @@
@import 'variables'; @import 'variables';
@import '../components/atoms/ProjectImage.module';
.project { // .project__imagewrap {
.project__image-wrap { .spacer {
//composes: project__imagewrap from '../components/atoms/ProjectImage.module';
margin-bottom: $spacer * 3; margin-bottom: $spacer * 3;
@media (min-width: 30rem) { @media (min-width: 30rem) {
margin-bottom: $spacer * 6; margin-bottom: $spacer * 6;
} }
}
} }
.project__title { .project__title {