From 2e45c29330a7a4e67e2b7be14667ae9b5d88e68d Mon Sep 17 00:00:00 2001 From: Matthias Kretschmann Date: Mon, 14 May 2018 01:50:11 +0200 Subject: [PATCH] documentation, switch to Travis --- .gitlab-ci.yml | 29 ---- .travis.yml | 24 ++++ LICENSE | 7 + README.md | 141 ++++++++++++++++++- deploy.sh | 14 -- gatsby-browser.js | 0 gatsby-node.js | 2 + gatsby-ssr.js | 0 package.json | 10 +- scripts/deploy.sh | 43 ++++++ scripts/new-project.js | 20 +++ scripts/new-project.yml | 12 ++ src/components/atoms/{Vcard.js => Vcard.jsx} | 14 +- src/images/twitter-card.png | Bin 4782 -> 3882 bytes static/humans.txt | 2 +- 15 files changed, 262 insertions(+), 56 deletions(-) delete mode 100644 .gitlab-ci.yml create mode 100644 .travis.yml create mode 100644 LICENSE delete mode 100755 deploy.sh delete mode 100644 gatsby-browser.js delete mode 100644 gatsby-ssr.js create mode 100755 scripts/deploy.sh create mode 100644 scripts/new-project.js create mode 100644 scripts/new-project.yml rename src/components/atoms/{Vcard.js => Vcard.jsx} (76%) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 327a40f..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,29 +0,0 @@ -cache: - paths: - - node_modules/ - - public/ - -stages: - - build - - deploy - -build: - image: node:latest - stage: build - script: - - npm i -g gatsby-cli - - export PATH="$PATH:/usr/local/bin/gatsby" - - npm i - - npm test - - npm run build - artifacts: - paths: - - public - -deploy: - image: garland/aws-cli-docker:latest - stage: deploy - script: - - aws s3 sync --delete --acl public-read ./public s3://matthiaskretschmann.com - only: - - master diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..211d90a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,24 @@ +language: node_js +node_js: node + +cache: + directories: + - node_modules + - public + +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 + +script: + - npm test + - npm run build + +after_success: + - npm run deploy + +notifications: + email: false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..21e55a5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2018 Matthias Kretschmann + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 2baf986..f235dff 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,152 @@ -# matthiaskretschmann.com -> Portfolio of Matthias Kretschmann +# [matthiaskretschmann.com](https://matthiaskretschmann.com) -[![pipeline status](https://git.berlin/m/portfolio/badges/master/pipeline.svg)](https://git.berlin/m/portfolio/commits/master) +[![image](src/images/twitter-card.png)](https://matthiaskretschmann.com) + +> 👔 Portfolio thingy, built with [Gatsby](https://www.gatsbyjs.org). + +[![Build Status](https://travis-ci.com/kremalicious/portfolio.svg?branch=master)](https://travis-ci.com/kremalicious/portfolio) + +## Table of Contents + +* [Features](#features) + * [One data file to rule all pages](#one-data-file-to-rule-all-pages) + * [Theme switcher](#theme-switcher) + * [SEO component](#seo-component) + * [Client-side vCard creation](#client-side-vcard-creation) + * [Matomo (formerly Piwik) analytics tracking](#matomo-formerly-piwik-analytics-tracking) + * [Project images](#project-images) + * [Importing SVG assets](#importing-svg-assets) +* [Development](#development) + * [Linting](#linting) + * [Add a new project](#add-a-new-project) +* [Deployment](#deployment) +* [Licenses](#licenses) + +--- + +## Features + +The whole [portfolio](https://matthiaskretschmann.com) is a React-based Single Page App built with [Gatsby](https://www.gatsbyjs.org). + +Despite being built with React, and despite all the project images making it a very image-heavy portfolio, Gatsby makes the final site super fast. So fast, it's almost unreal and feels like magic. And makes it work without JavaScript by server-side rendering all routes. [And so much more](https://www.gatsbyjs.org/features/). + +### One data file to rule all pages + +All content is powered by one YAML file, [`data/projects.yml`](data/projects.yml) where all the portfolio's projects are defined. The project description itself is transformed from Markdown. + +Gatsby automatically creates pages from each item in that file utilizing the [`src/templates/Project.jsx`](src/templates/Project.jsx) template. + +### Theme switcher + +Includes a theme switcher which allows user to toggle between a light and a dark theme. Switching between them also happens automatically based on time of day. + +If you want to know how, have a look at the respective component under [`src/components/molecules/ThemeSwitch.jsx`](src/components/molecules/ThemeSwitch.jsx) + +### SEO component + +Includes a SEO component which automatically switches all required `meta` tags for search engines, Twitter Cards, and Facebook OpenGraph tags based on the browsed route/page. + +If you want to know how, have a look at the respective component under [`src/components/atoms/SEO.jsx`](src/components/atoms/SEO.jsx) + +### Client-side vCard creation + +The _Add to addressbook_ link in the footer automatically creates a downloadable vCard file on the client-side, based on data defined in `data/meta.yml`. + +If you want to know how, have a look at the respective component under [`src/components/atoms/Vcard.jsx`](src/components/atoms/Vcard.jsx) + +### Matomo (formerly Piwik) analytics tracking + +Site sends usage statistics to my own [Matomo](https://matomo.org) installation. To make this work in Gatsby, I created and open sourced a plugin, [gatsby-plugin-matomo](https://github.com/kremalicious/gatsby-plugin-matomo), which is in use on this site. + +### Project images + +All project images live under `src/images` and are automatically attached to each project based on the inclusion of the project's `slug` in their filenames. + +_(TODO: automatically add the inital image to each project node, so it doesn't have to be defined in the projects.yml file.)_ + +All project images make use of the excellent [gatsby-image](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-image) plugin, working in tandem with [gatsby-plugin-sharp](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-sharp) and [gatsby-transformer-sharp](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-transformer-sharp). + +All together, Gatsby automatically generates all required image sizes for delivering responsible, responsive images to visitors, including lazy loading of all images. Also includes the [intersection-observer polyfill](https://github.com/w3c/IntersectionObserver) to make lazy loading work properly in Safari. + +All project images use one single component defined in [`src/components/atoms/ProjectImage.jsx`](src/components/atoms/ProjectImage.jsx). In there, one main GraphQL query fragment is defined, which then gets used throughout other GraphQL queries. + +### Importing SVG assets + +Makes use of `gatsby-plugin-svgr` so SVG assets can be imported like so: + +```js +import { ReactComponent as Logo } from '../images/logo.svg' +``` ## Development ```bash +git clone git@github.com:kremalicious/portfolio.git +cd portfolio/ + npm i npm start ``` -Get SVG images +### Linting -```js -import { ReactComponent as Logo } from '../../images/logo.svg' +ESlint, Prettier, and Stylelint are setup for all linting purposes: + +```bash +npm run lint +``` + +To automatically format all code files: + +```bash +npm run format +npm run format:css +``` + +### Add a new project + +To add a new project, run the following command. This adds a new item to the top of the `projects.yml` file, creating the title & slug from the argument: + +```bash +npm run new -- "Hello" +``` + +Then continue modifying the new entry in [`data/projects.yml`](data/projects.yml). + +Finally, add as many images as needed with the file name format and put into `src/images/`: + +``` +portfolio-SLUG-01.png +portfolio-SLUG-02.png +portfolio-SLUG-03.png +... ``` ## Deployment +Automatic deployments are triggered upon successful tests & builds on Travis: + +* push to `master` initiates a live 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: + ```bash npm run deploy ``` + +The deploymeng script can be used locally too, the branch checks are only happening for Travis builds, allowing to deploy any branch from local machine. + +## Licenses + +All images and projects are plain ol' copyright: + +**© Copyright 2018 Matthias Kretschmann** + +Most displayed projects are subject to the copyright of their respective owners. + +All the rest, like all code and documentation, is under: + +**The MIT License** + +[Full MIT license text](LICENSE) diff --git a/deploy.sh b/deploy.sh deleted file mode 100755 index 9de2d16..0000000 --- a/deploy.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -set -e; - -aws s3 sync \ - --delete \ - --acl public-read \ - ./public s3://matthiaskretschmann.com - -echo "---------------------------------------------" -echo " ✓ done deployment " -echo "---------------------------------------------" - -exit; diff --git a/gatsby-browser.js b/gatsby-browser.js deleted file mode 100644 index e69de29..0000000 diff --git a/gatsby-node.js b/gatsby-node.js index 616d499..504f091 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -1,5 +1,7 @@ const path = require('path') +// Intersection Observer polyfill +// requires `npm install intersection-observer` // https://github.com/gatsbyjs/gatsby/issues/2288#issuecomment-334467821 exports.modifyWebpackConfig = ({ config, stage }) => { if (stage === 'build-html') { diff --git a/gatsby-ssr.js b/gatsby-ssr.js deleted file mode 100644 index e69de29..0000000 diff --git a/package.json b/package.json index a517ae3..4d6c495 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "format": "prettier --write 'src/**/*.{js,jsx}'", "format:css": "prettier-stylelint --write --quiet 'src/**/*.{css,scss}'", "test": "npm run lint", - "deploy": "./deploy.sh" + "deploy": "./scripts/deploy.sh", + "new": "node ./scripts/new-project.js" }, "dependencies": { "camel-case": "^3.0.0", @@ -50,12 +51,13 @@ "eslint-plugin-graphql": "^2.1.1", "eslint-plugin-prettier": "^2.6.0", "eslint-plugin-react": "^7.8.2", + "ora": "^2.1.0", + "prepend": "^1.0.2", "prettier": "^1.12.1", "prettier-stylelint": "^0.4.2", + "slugify": "^1.3.0", "stylelint": "^9.2.0", "stylelint-config-standard": "^18.2.0" }, - "browserslist": [ - "defaults" - ] + "browserslist": ["defaults"] } diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..0e53629 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# +# required environment variables: +# AWS_ACCESS_KEY_ID +# AWS_SECRET_ACCESS_KEY +# AWS_DEFAULT_REGION +# AWS_S3_BUCKET +# +set -e; + +## +## check for pull request against master +## +if [ "$TRAVIS_PULL_REQUEST" != "false" ] && [ "$TRAVIS_BRANCH" == "master" ]; then + + aws s3 sync \ + --delete \ + --acl public-read \ + ./public s3://"$AWS_S3_BUCKET_BETA" + +## +## check for master push which is no pull request +## +elif [ "$TRAVIS_BRANCH" == "master" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ] || [ "$TRAVIS" != true ]; then + + aws s3 sync \ + --delete \ + --acl public-read \ + ./public s3://"$AWS_S3_BUCKET" + + echo "---------------------------------------------" + echo " ✓ done deployment " + echo "---------------------------------------------" + + exit; + +else + + echo "---------------------------------------------" + echo " nothing to deploy " + echo "---------------------------------------------" + +fi diff --git a/scripts/new-project.js b/scripts/new-project.js new file mode 100644 index 0000000..6bf0270 --- /dev/null +++ b/scripts/new-project.js @@ -0,0 +1,20 @@ +const fs = require('fs') +const path = require('path') +const prepend = require('prepend') +const slugify = require('slugify') +const ora = require('ora') + +const templatePath = path.join(__dirname, 'new-project.yml') +const template = fs.readFileSync(templatePath).toString() + +const title = process.argv[2] +const titleSlug = slugify(title) +const spinner = ora(`Adding '${title}'.`).start() + +const newContents = template.replace('TITLE', title).replace('SLUG', titleSlug) +const projects = path.join(__dirname, '..', 'data', 'projects.yml') + +prepend(projects, newContents, error => { + if (error) throw error + spinner.succeed(`Added '${title}' to top of projects.yml file.`) +}) diff --git a/scripts/new-project.yml b/scripts/new-project.yml new file mode 100644 index 0000000..be9598a --- /dev/null +++ b/scripts/new-project.yml @@ -0,0 +1,12 @@ +- + + title: TITLE + slug: "/SLUG/" + img: "../src/images/portfolio-SLUG-01.png" + description: > + Markdown can be used here. + links: + - title: Link + url: https://url + techstack: + - Tech used diff --git a/src/components/atoms/Vcard.js b/src/components/atoms/Vcard.jsx similarity index 76% rename from src/components/atoms/Vcard.js rename to src/components/atoms/Vcard.jsx index ae1b679..0ba5c4e 100644 --- a/src/components/atoms/Vcard.js +++ b/src/components/atoms/Vcard.jsx @@ -8,11 +8,15 @@ class Vcard extends PureComponent { super(props) } + // Helper function to create base64 string from avatar image + // without the need to read image file from file system toDataURL(src, callback, outputFormat) { const img = new Image() img.crossOrigin = 'Anonymous' img.onload = function() { + // yeah, we're gonna create a fake canvas to render the image + // and then create a base64 string from the rendered result const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') let dataURL @@ -37,11 +41,13 @@ class Vcard extends PureComponent { const contact = new vCard() const photoSrc = meta.avatar.childImageSharp.original.src + // first, convert the avatar to base64, + // then construct all vCard elements this.toDataURL( photoSrc, dataUrl => { - // stripping this data out of base64 string - // is required for vcard for whatever reason + // stripping this data out of base64 string is required + // for vcard to actually display the image for whatever reason const dataUrlCleaned = dataUrl.split('data:image/jpeg;base64,').join('') contact.set('photo', dataUrlCleaned, { encoding: 'b', type: 'JPEG' }) contact.set('fn', meta.title) @@ -61,6 +67,8 @@ class Vcard extends PureComponent { ) } + // Construct the download from a blob of the just constructed vCard, + // and save it to user's file system downloadVcard(vcard) { const name = this.props.meta.addressbook.split('/').join('') const blob = new Blob([vcard], { type: 'text/x-vcard' }) @@ -75,6 +83,8 @@ class Vcard extends PureComponent { render() { return ( diff --git a/src/images/twitter-card.png b/src/images/twitter-card.png index 4f6d7f3be7b1580087f3634d20e8012998383e20..efe746119571b83296d6bacaf17bd644bcf14398 100644 GIT binary patch delta 2616 zcmX|DYg7~076z%hl!8?rJ_`5*v=-5lffAeefIxjrFg!${fV4m`1SpbVO$cM{wLVy2 zYovN-G@+pk5hVx_B0&-@Rt(4^P7p96ZEsd>Fo;)&S5n{}XxCk9e$4FI=iA@jXYcPE zg?E+LcNEvW1N);HFGolJd^MVhoZ+4T&)Hg5oV~c(KESK-{-efUI@Aw3np!*Vw01T> z?9zZo*WJ?5bHD3}c=bFra$oaPrk$PLJYFO-YSka-d%slJ*}WURCv;czS5Jor%)O;) zZg*3JTUY+eZs~iWxw{Y8yUmYIU6y+=+9UVV0KcgAwxb!{Z9YWXN9`Z&^K*M|>A%w? zOG9nqwnHnA2Hs9wD9zhZ(>ppkARUX>@;il{B8jc+o9X$ywbJdM8rE=jiYd!d#Dxw_ z`y%n0cT&ZVmimhizq1BmeIvJLImi}-I-OY+3NKgY3iFSarkzC>^c7f5o{hR#D?KAo zhJTgtAf|`B+K68tN^x|7oHEo^>94F4x>#G`)=AoC7bq=A6gHZ?TsvM1j~i)|@(KV! zNsLiGADl5|PE7|*Uamij9>wTNYj?d4pyU?0K&PInk#HggV($>=EivZ3S$@!$)5aAP zj4oAqV+u7b<+u#DhhuxL+Ad!~`jWmGkldoPqLK)gD);dR@OaM3PbM}wy3UdD(m_+w zDrv)RR@B9;6k_UyirHC&D#8@?A*`rq!i9jBbl;L6;Sdgg)H5|+fwaAZ48MVQA&arX zMDV&95>1?ZqMLs@TfjLw)t^gR-9C|H0dxl(8wQIF<(&J1|Z^WdNl_^%VH#W|dKL&hpr z)G(AwDJ~K6Ln`J~euNcukcg5+o(D`pRR6521(`Q-20WR+zj&{w`ojUz zXS*xZ!4>58k%!P$PM+%)DNvBM^!or;TI1BRBjui3fpvHfhaYoWw6V75hZ=0&Y8r zRs|@+BsY-8oXd|LTH!UG-%Wn6H?2&t957ro4ZP+HLe=K~?jqh+#~64D^J~6!BBom0 zewra>FMwyJzf1V=pux3=Jkx*{;=-7L^m-W{083Yw3VucrF7Y2#19SRE+d`zV#ak~GU|DKqaF=Ux@|RWaa&n3pFc+D|#&IrbS5Gn&xu4)G}>)>wK| zsTrSn++ITqgeAwAuLeUFW~T?KW;3>Bp#G0Rfuvhbv?)VE8<^NXSNb+EQ?yRCY;49D>xHYQQ8HUy7*cNdeC z(y)PSLf^y-g=ug&TRE;LJD=z-*-{59)^moAP^nmReyR|j?GE#y*};-t$`Mp%6``w8 z%}2sO;jo-b%uMXgU&bOD@!kq-x)L<#3sgh335)o?3~|6?xC=D+wQ}?IIuIYgH&u&k z$PVVZf6%8*V<vsFC%n;Q}KP|d@zsv+djg; z?b(8Kwb}7lT!Y*kP3ZeAPUXrPP2Q3sAOFW7QcFEpWXqs#V#r;GI_8@)7q+orvRWyrrSxoaBTZDS8fRjKDL@i!Uq`cn> zJmOabBBFW_ycI2BUMM(;FJy>gfqw9AnEy<8QCyjh6$Wqx4Zy)OyR9(9El9*rn=>tc zJG@`jQJJRYC%FcSsuPRQ63A18Ud^#q-_x5$^ZhtOU*?TzO&yhNEx!cQ+*2^%5G=_f zP*e_lsn0G#Tp`~t=WP8!BG@n23@&UU4RlE|%u)1Me=9D%LBKly;-V=8*5{c}*}NBe?kz5`?|&?(n6Td)~Fjbz3fC}rk7!;nbj zb`JeEu=&m9j5l7JT_MKt7tbs;vRJR{V9!d?!tGou9hlP~TJQjwt2gyl1M%x2FItFw zzvCmS*@MS3sxHDmj$c5Tg#Y%eZ^pH0r{?-6VnckuI<(;KVg?)sN3_$w&*)gBHyuN6 z%%PB6;(80N&HTTw=yS?s86YIH> z<p$wsq63by!(w%U>JPN);PQBnHMC=p{$&GtTy^1 zSqBWR>614t#limLdWtOjWLk()br>|o2rj(|PW!TokUH=^p|5J1YI_q3fA7BJc|cE& zh5hDK2vjV`4(rP-9r<13e|`D-S6@G-!8P-@yOylduF>vUu=x&ksu;Rabc~3IL*<*E z-kI(3~y@bs&Q?TUk+eVy)>pWg>NJ&W(Kr%3U-fWtmvN z^aF5F+VIbX$!%WNBJs9msk1k!c?t8xJKx!|!t;!EFh3@LZ4&)!7jVI|+rwanDs~?} F`9HLeU(5gi delta 3646 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 diff --git a/static/humans.txt b/static/humans.txt index 2f1fef6..d4d76f7 100644 --- a/static/humans.txt +++ b/static/humans.txt @@ -11,7 +11,7 @@ /* SITE */ Typography : Brandon Grotesque, FF Tisa Sans Web Pro - Software : Gatsby.js, React, VS Code, macOS + Software : Gatsby.js, React, VS Code, Sketch, macOS