mirror of
https://github.com/kremalicious/portfolio.git
synced 2025-02-14 21:10:41 +01:00
commit
f81542ba37
@ -23,5 +23,10 @@
|
||||
"semi": ["error", "never"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"prettier/prettier": "error"
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "16"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ language: node_js
|
||||
node_js: node
|
||||
|
||||
addons:
|
||||
chrome: stable
|
||||
chrome: beta
|
||||
|
||||
cache:
|
||||
directories:
|
||||
@ -13,12 +13,6 @@ cache:
|
||||
install:
|
||||
- npm i
|
||||
|
||||
before_script:
|
||||
- export DISPLAY=:99.0
|
||||
- export CHROME_PATH="$(pwd)/chrome-linux/chrome"
|
||||
- sh -e /etc/init.d/xvfb start
|
||||
- sleep 3 # wait for xvfb to boot
|
||||
|
||||
script:
|
||||
- npm test
|
||||
- npm run build
|
||||
|
@ -14,7 +14,7 @@
|
||||
</p>
|
||||
|
||||
- [🎉 Features](#-features)
|
||||
- [⛵️ Lighthouse score](#-lighthouse-score)
|
||||
- [⛵️ Lighthouse score](#️-lighthouse-score)
|
||||
- [💍 One data file to rule all pages](#-one-data-file-to-rule-all-pages)
|
||||
- [💅 Theme switcher](#-theme-switcher)
|
||||
- [🏆 SEO component](#-seo-component)
|
||||
|
@ -3,6 +3,18 @@ const remark = require('remark')
|
||||
const markdown = require('remark-parse')
|
||||
const html = require('remark-html')
|
||||
|
||||
function truncate(n, useWordBoundary) {
|
||||
if (this.length <= n) {
|
||||
return this
|
||||
}
|
||||
const subString = this.substr(0, n - 1)
|
||||
return (
|
||||
(useWordBoundary
|
||||
? subString.substr(0, subString.lastIndexOf(' '))
|
||||
: subString) + '...'
|
||||
)
|
||||
}
|
||||
|
||||
exports.onCreateNode = ({ node, actions }) => {
|
||||
const { createNodeField } = actions
|
||||
|
||||
@ -29,6 +41,15 @@ exports.onCreateNode = ({ node, actions }) => {
|
||||
name: 'descriptionHtml',
|
||||
value: descriptionHtml
|
||||
})
|
||||
|
||||
// Create excerpt from description
|
||||
const excerpt = truncate.apply(description, [320, true])
|
||||
|
||||
createNodeField({
|
||||
node,
|
||||
name: 'excerpt',
|
||||
value: excerpt
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
16
package.json
16
package.json
@ -22,17 +22,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"file-saver": "^2.0.0",
|
||||
"gatsby": "^2.0.60",
|
||||
"gatsby": "^2.0.63",
|
||||
"gatsby-image": "^2.0.22",
|
||||
"gatsby-plugin-favicon": "^3.1.4",
|
||||
"gatsby-plugin-matomo": "^0.6.0",
|
||||
"gatsby-plugin-offline": "^2.0.18",
|
||||
"gatsby-plugin-react-helmet": "^3.0.4",
|
||||
"gatsby-plugin-sass": "^2.0.5",
|
||||
"gatsby-plugin-sass": "^2.0.7",
|
||||
"gatsby-plugin-sharp": "^2.0.14",
|
||||
"gatsby-plugin-sitemap": "^2.0.3",
|
||||
"gatsby-plugin-svgr": "^2.0.1",
|
||||
"gatsby-source-filesystem": "^2.0.10",
|
||||
"gatsby-source-filesystem": "^2.0.11",
|
||||
"gatsby-transformer-json": "^2.1.6",
|
||||
"gatsby-transformer-sharp": "^2.1.9",
|
||||
"gatsby-transformer-yaml": "^2.1.6",
|
||||
@ -44,7 +44,7 @@
|
||||
"react": "^16.6.3",
|
||||
"react-dom": "^16.6.3",
|
||||
"react-helmet": "^5.2.0",
|
||||
"react-pose": "^4.0.2",
|
||||
"react-pose": "^4.0.4",
|
||||
"remark": "^10.0.1",
|
||||
"remark-html": "^9.0.0",
|
||||
"remark-parse": "^6.0.3",
|
||||
@ -52,9 +52,9 @@
|
||||
"vcf": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.6",
|
||||
"@babel/node": "^7.0.0",
|
||||
"@babel/preset-env": "^7.1.6",
|
||||
"@babel/core": "^7.2.0",
|
||||
"@babel/node": "^7.2.0",
|
||||
"@babel/preset-env": "^7.2.0",
|
||||
"@svgr/webpack": "^4.1.0",
|
||||
"ava": "^0.25.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
@ -70,7 +70,7 @@
|
||||
"prepend": "^1.0.2",
|
||||
"prettier": "^1.15.3",
|
||||
"prettier-stylelint": "^0.4.2",
|
||||
"slugify": "^1.3.3",
|
||||
"slugify": "^1.3.4",
|
||||
"stylelint": "^9.9.0",
|
||||
"stylelint-config-css-modules": "^1.3.0",
|
||||
"stylelint-config-standard": "^18.2.0",
|
||||
|
@ -12,26 +12,22 @@ set -e;
|
||||
|
||||
function s3sync {
|
||||
aws s3 sync ./public s3://"$1" \
|
||||
--exclude "*" \
|
||||
--include "*.js" \
|
||||
--include "*.css" \
|
||||
--include "static/*" \
|
||||
--include "icons/*" \
|
||||
--exclude "sw.js" \
|
||||
--exclude "workbox*/*" \
|
||||
--cache-control public,max-age=31536000,immutable \
|
||||
--delete \
|
||||
--acl public-read \
|
||||
--quiet
|
||||
--acl public-read
|
||||
|
||||
aws s3 sync ./public s3://"$1" \
|
||||
--exclude "*" \
|
||||
--include "*.html" \
|
||||
--include "sw.js" \
|
||||
--include "chunk-map.json" \
|
||||
--include "sitemap.xml" \
|
||||
--include ".iconstats.json" \
|
||||
--include "humans.txt" \
|
||||
--include "robots.txt" \
|
||||
--cache-control public,max-age=0,must-revalidate \
|
||||
--delete \
|
||||
--acl public-read \
|
||||
--quiet
|
||||
--acl public-read
|
||||
}
|
||||
|
||||
##
|
||||
|
@ -8,14 +8,17 @@ const launchChromeAndRunLighthouse = (
|
||||
opts = { chromeFlags: ['--headless'] },
|
||||
config = null
|
||||
) =>
|
||||
chromeLauncher.launch({ chromeFlags: opts.chromeFlags }).then(chrome => {
|
||||
opts.port = chrome.port
|
||||
return lighthouse(url, opts, config).then(results =>
|
||||
chrome.kill().then(() => results.lhr)
|
||||
)
|
||||
})
|
||||
chromeLauncher
|
||||
.launch({ chromeFlags: opts.chromeFlags })
|
||||
.then(async chrome => {
|
||||
opts.port = chrome.port
|
||||
const results = await lighthouse(url, opts, config)
|
||||
await chrome.kill()
|
||||
return results.lhr
|
||||
})
|
||||
|
||||
let scores
|
||||
|
||||
test.before(async () => {
|
||||
console.log(`Auditing ${siteMetadata.siteUrl}.\n`) // eslint-disable-line no-console
|
||||
scores = await launchChromeAndRunLighthouse(siteMetadata.siteUrl).then(
|
||||
|
@ -1,20 +1,8 @@
|
||||
import React from 'react'
|
||||
import React, { PureComponent } from 'react'
|
||||
import Helmet from 'react-helmet'
|
||||
import PropTypes from 'prop-types'
|
||||
import { StaticQuery, graphql } from 'gatsby'
|
||||
|
||||
function truncate(n, useWordBoundary) {
|
||||
if (this.length <= n) {
|
||||
return this
|
||||
}
|
||||
const subString = this.substr(0, n - 1)
|
||||
return (
|
||||
(useWordBoundary
|
||||
? subString.substr(0, subString.lastIndexOf(' '))
|
||||
: subString) + '...'
|
||||
)
|
||||
}
|
||||
|
||||
const query = graphql`
|
||||
query {
|
||||
dataYaml {
|
||||
@ -22,7 +10,6 @@ const query = graphql`
|
||||
tagline
|
||||
description
|
||||
url
|
||||
email
|
||||
img {
|
||||
childImageSharp {
|
||||
resize(width: 980) {
|
||||
@ -31,11 +18,7 @@ const query = graphql`
|
||||
}
|
||||
}
|
||||
social {
|
||||
Email
|
||||
Blog
|
||||
Twitter
|
||||
GitHub
|
||||
Dribbble
|
||||
}
|
||||
gpg
|
||||
addressbook
|
||||
@ -43,60 +26,75 @@ const query = graphql`
|
||||
}
|
||||
`
|
||||
|
||||
const SEO = ({ project }) => (
|
||||
<StaticQuery
|
||||
query={query}
|
||||
render={data => {
|
||||
const meta = data.dataYaml
|
||||
const MetaTags = ({ title, description, url, image, meta }) => {
|
||||
return (
|
||||
<Helmet
|
||||
defaultTitle={`${meta.title.toLowerCase()} { ${meta.tagline.toLowerCase()} }`}
|
||||
titleTemplate={`%s // ${meta.title.toLowerCase()} { ${meta.tagline.toLowerCase()} }`}
|
||||
title={title}
|
||||
>
|
||||
<html lang="en" />
|
||||
|
||||
const title = project.title || meta.title
|
||||
const tagline = meta.tagline
|
||||
const description = project.description
|
||||
? truncate.apply(project.description, [320, true])
|
||||
: truncate.apply(meta.description, [320, true])
|
||||
const image = project.img
|
||||
? project.img.childImageSharp.twitterImage.src
|
||||
: meta.img.childImageSharp.resize.src
|
||||
const url = project.slug ? `${meta.url}${project.slug}` : meta.url
|
||||
{/* General tags */}
|
||||
<meta name="description" content={description} />
|
||||
<meta name="image" content={`${meta.url}${image}`} />
|
||||
<link rel="canonical" href={url} />
|
||||
|
||||
return (
|
||||
<Helmet
|
||||
defaultTitle={`${title.toLowerCase()} { ${tagline.toLowerCase()} }`}
|
||||
titleTemplate={`%s // ${title.toLowerCase()} { ${tagline.toLowerCase()} }`}
|
||||
>
|
||||
<html lang="en" />
|
||||
{/* OpenGraph tags */}
|
||||
<meta property="og:url" content={url} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={`${meta.url}${image}`} />
|
||||
|
||||
<title>{title}</title>
|
||||
|
||||
{/* General tags */}
|
||||
<meta name="description" content={description} />
|
||||
<meta name="image" content={`${meta.url}${image}`} />
|
||||
<link rel="canonical" href={url} />
|
||||
|
||||
{/* OpenGraph tags */}
|
||||
<meta property="og:url" content={url} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={`${meta.url}${image}`} />
|
||||
|
||||
{/* Twitter Card tags */}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:creator" content={meta.social.Twitter} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={`${meta.url}${image}`} />
|
||||
</Helmet>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
SEO.propTypes = {
|
||||
project: PropTypes.object
|
||||
{/* Twitter Card tags */}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:creator" content={meta.social.Twitter} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={`${meta.url}${image}`} />
|
||||
</Helmet>
|
||||
)
|
||||
}
|
||||
|
||||
SEO.defaultProps = {
|
||||
project: {}
|
||||
MetaTags.propTypes = {
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
meta: PropTypes.object
|
||||
}
|
||||
|
||||
export default SEO
|
||||
export default class SEO extends PureComponent {
|
||||
static propTypes = {
|
||||
project: PropTypes.object
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StaticQuery
|
||||
query={query}
|
||||
render={data => {
|
||||
const { project } = this.props
|
||||
const meta = data.dataYaml
|
||||
const title = (project && project.title) || meta.title
|
||||
const description =
|
||||
(project && project.fields.excerpt) || meta.description
|
||||
const image =
|
||||
(project && project.img.childImageSharp.twitterImage.src) ||
|
||||
meta.img.childImageSharp.resize.src
|
||||
const url = (project && `${meta.url}${project.slug}`) || meta.url
|
||||
|
||||
return (
|
||||
<MetaTags
|
||||
title={title}
|
||||
description={description}
|
||||
url={url}
|
||||
image={image}
|
||||
meta={meta}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import PropTypes from 'prop-types'
|
||||
import { StaticQuery, graphql } from 'gatsby'
|
||||
import posed from 'react-pose'
|
||||
import { moveInBottom } from '../atoms/Transitions'
|
||||
|
||||
import { ReactComponent as Logo } from '../../images/logo.svg'
|
||||
import styles from './LogoUnit.module.scss'
|
||||
|
||||
@ -19,37 +18,21 @@ const query = graphql`
|
||||
const Animation = posed.div(moveInBottom)
|
||||
|
||||
export default class LogoUnit extends PureComponent {
|
||||
state = { isMinimal: false }
|
||||
|
||||
static propTypes = {
|
||||
minimal: PropTypes.bool
|
||||
}
|
||||
|
||||
checkMinimal = () => {
|
||||
const { minimal } = this.props
|
||||
|
||||
this.setState({ isMinimal: minimal })
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.checkMinimal()
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.checkMinimal()
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StaticQuery
|
||||
query={query}
|
||||
render={data => {
|
||||
const { title, tagline } = data.dataYaml
|
||||
const { isMinimal } = this.state
|
||||
const { minimal } = this.props
|
||||
|
||||
return (
|
||||
<Animation>
|
||||
<div className={isMinimal ? styles.minimal : styles.logounit}>
|
||||
<div className={minimal ? styles.minimal : styles.logounit}>
|
||||
<Logo className={styles.logounit__logo} />
|
||||
<h1 className={styles.logounit__title}>
|
||||
{title.toLowerCase()}
|
||||
|
@ -22,24 +22,8 @@ export default class Header extends PureComponent {
|
||||
minimal: PropTypes.bool
|
||||
}
|
||||
|
||||
state = { isMinimal: this.props.minimal }
|
||||
|
||||
checkMinimal = () => {
|
||||
const { minimal } = this.props
|
||||
|
||||
this.setState({ isMinimal: minimal })
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.checkMinimal()
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.checkMinimal()
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isMinimal } = this.state
|
||||
const { minimal } = this.props
|
||||
|
||||
return (
|
||||
<StaticQuery
|
||||
@ -48,16 +32,16 @@ export default class Header extends PureComponent {
|
||||
const meta = data.dataYaml
|
||||
|
||||
return (
|
||||
<header className={isMinimal ? styles.minimal : styles.header}>
|
||||
<header className={minimal ? styles.minimal : styles.header}>
|
||||
<ThemeSwitch />
|
||||
|
||||
<Link className={styles.header__link} to={'/'}>
|
||||
<LogoUnit minimal={isMinimal} />
|
||||
<LogoUnit minimal={minimal} />
|
||||
</Link>
|
||||
|
||||
<Networks hide={isMinimal} />
|
||||
<Networks hide={minimal} />
|
||||
|
||||
<Availability hide={isMinimal && !meta.availability.status} />
|
||||
<Availability hide={minimal && !meta.availability.status} />
|
||||
</header>
|
||||
)
|
||||
}}
|
||||
|
@ -27,19 +27,23 @@ class ProjectMeta extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
const ProjectImages = ({ projectImages, title }) => (
|
||||
<FullWidth>
|
||||
{projectImages.map(({ node }) => (
|
||||
<div className={styles.imageWrap} key={node.id}>
|
||||
<ProjectImage fluid={node.fluid} alt={title} />
|
||||
</div>
|
||||
))}
|
||||
</FullWidth>
|
||||
)
|
||||
class ProjectImages extends PureComponent {
|
||||
static propTypes = {
|
||||
projectImages: PropTypes.array,
|
||||
title: PropTypes.string
|
||||
}
|
||||
|
||||
ProjectImages.propTypes = {
|
||||
projectImages: PropTypes.array,
|
||||
title: PropTypes.string
|
||||
render() {
|
||||
return (
|
||||
<FullWidth>
|
||||
{this.props.projectImages.map(({ node }) => (
|
||||
<div className={styles.imageWrap} key={node.id}>
|
||||
<ProjectImage fluid={node.fluid} alt={this.props.title} />
|
||||
</div>
|
||||
))}
|
||||
</FullWidth>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default class Project extends PureComponent {
|
||||
@ -49,9 +53,10 @@ export default class Project extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const project = this.props.data.projectsYaml
|
||||
const projectImages = this.props.data.projectImages.edges
|
||||
const descriptionHtml = this.props.data.projectsYaml.fields.descriptionHtml
|
||||
const { data } = this.props
|
||||
const project = data.projectsYaml
|
||||
const projectImages = data.projectImages.edges
|
||||
const descriptionHtml = data.projectsYaml.fields.descriptionHtml
|
||||
const { title, links, techstack } = project
|
||||
|
||||
return (
|
||||
@ -76,14 +81,14 @@ export default class Project extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
export const projectAndProjectsQuery = graphql`
|
||||
export const projectQuery = graphql`
|
||||
query($slug: String!) {
|
||||
projectsYaml(slug: { eq: $slug }) {
|
||||
title
|
||||
slug
|
||||
description
|
||||
fields {
|
||||
descriptionHtml
|
||||
excerpt
|
||||
}
|
||||
links {
|
||||
title
|
||||
|
Loading…
Reference in New Issue
Block a user