1
0
mirror of https://github.com/kremalicious/portfolio.git synced 2024-06-28 16:47:41 +02:00

Merge pull request #188 from kremalicious/feature/resume

Refactor to use JSON Resume
This commit is contained in:
Matthias Kretschmann 2019-11-19 23:54:10 +01:00 committed by GitHub
commit dbd486f96d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1276 additions and 248 deletions

View File

@ -1,6 +1,6 @@
name: Test
on: [push, pull_request]
on: [push]
jobs:
test:
@ -22,8 +22,8 @@ jobs:
uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-node-
key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-node-${{ matrix.node-version }}-
- name: Cache Gatsby build output
uses: actions/cache@v1

View File

@ -16,6 +16,7 @@
- [🎉 Features](#-features)
- [⛵️ Lighthouse score](#-lighthouse-score)
- [🗂 JSON Resume](#-json-resume)
- [💍 One data file to rule all pages](#-one-data-file-to-rule-all-pages)
- [🐱 GitHub repositories](#-github-repositories)
- [💅 Theme switcher](#-theme-switcher)
@ -37,27 +38,39 @@
## 🎉 Features
The whole [portfolio](https://matthiaskretschmann.com) is a React-based Single Page App built with [Gatsby v2](https://www.gatsbyjs.org).
The whole [portfolio](https://matthiaskretschmann.com) is a React-based single page app built with [Gatsby v2](https://www.gatsbyjs.org).
Most metadata is powered by one `resume.json` file based on [🗂 JSON Resume](#-json-resume), and one `projects.yml` file to [define the displayed projects](#-one-data-file-to-rule-all-pages).
### ⛵️ Lighthouse score
![Lighthouse scores](https://lighthouse.now.sh/?perf=100&pwa=100&a11y=100&bp=100&seo=100)
### 🗂 JSON Resume
Most site metadata and social profiles are defined in [`content/resume.json`](content/resume.json) based on the [JSON Resume](https://jsonresume.org) standard and used throughout the site as a custom React hook. Additionally, a resume page is created under `/resume`.
If you want to know how, have a look at the respective components:
- [`content/resume.json`](content/resume.json)
- [`src/pages/resume/index.jsx`](src/pages/resume/index.jsx)
- [`src/hooks/use-resume.js`](src/hooks/use-resume.js)
### 💍 One data file to rule all pages
All content is powered by one YAML file where all the portfolio's projects are defined. The project description itself is transformed from Markdown written inside the YAML file into HTML on build time.
All displayed project content is powered by one YAML file where all the portfolio's projects are defined. The project description itself is transformed from Markdown written inside the YAML file into HTML on build time.
Gatsby automatically creates pages from each item in that file utilizing the [`Project.jsx`](src/templates/Project.jsx) template.
- [`gatsby-node.js`](gatsby-node.js)
- [`content/projects.yml`](content/projects.yml)
- [`gatsby-node.js`](gatsby-node.js)
- [`src/templates/Project.jsx`](src/templates/Project.jsx)
### 🐱 GitHub repositories
The open source section at the bottom of the front page shows selected GitHub repositories, sourced from GitHub.
On build time, all my public repositories are fetched from GitHub, then filtered against the ones defined in `content/repos.yml`, sorted by the last push date, and provided via the page context of the front page.
On build time, all my public repositories are fetched from GitHub, then filtered against the ones defined in `content/repos.yml`, sorted by the last push date, and provided via the `pageContext` of the front page.
If you want to know how, have a look at the respective components:

View File

@ -1,18 +1,7 @@
- title: Matthias Kretschmann
tagline: Designer & Developer
description: Portfolio of web & ui designer/developer hybrid Matthias Kretschmann.
url: https://matthiaskretschmann.com
email: m@kretschmann.io
avatar: ../src/images/avatar.jpg
img: ../src/images/twitter-card.png
# most personal metadata can be found in ./resume.json
social:
Mail: mailto:m@kretschmann.io
Blog: https://kremalicious.com
Twitter: https://twitter.com/kremalicious
GitHub: https://github.com/kremalicious
Dribbble: https://dribbble.com/kremalicious
Keybase: https://keybase.io/kremalicious
- description: Portfolio of web & ui designer/developer hybrid Matthias Kretschmann.
img: ../src/images/twitter-card.png
availability:
status: false

236
content/resume.json Normal file
View File

@ -0,0 +1,236 @@
{
"basics": {
"name": "Matthias Kretschmann",
"label": "Designer & Developer",
"picture": "../src/images/avatar.jpg",
"email": "m@kretschmann.io",
"website": "https://matthiaskretschmann.com",
"summary": "",
"profiles": [
{
"network": "Blog",
"url": "https://kremalicious.com"
},
{
"network": "Twitter",
"username": "kremalicious",
"url": "https://twitter.com/kremalicious"
},
{
"network": "GitHub",
"username": "kremalicious",
"url": "https://github.com/kremalicious"
},
{
"network": "Dribbble",
"username": "kremalicious",
"url": "https://dribbble.com/kremalicious"
},
{
"network": "Keybase",
"username": "kremalicious",
"url": "https://keybase.io/kremalicious"
}
],
"location": {
"city": "Berlin",
"countryCode": "DE"
}
},
"work": [
{
"company": "Ocean Protocol Foundation",
"position": "Lead UI Designer & Developer",
"website": "https://oceanprotocol.com",
"startDate": "2017-01-01",
"summary": "Co-Founded the Ocean Protocol project and as a core developer leading the execution of [multiple user interfaces](/oceanprotocol) and core components.\n\nIn general, leading the UI design & development of Ocean Protocol's user interfaces, iterating on a components-based UI design system spanning all of Ocean Protocol's web properties. This also includes the conceptualization, execution and iteration of the creative and visual direction of the Ocean Protocol brand."
},
{
"company": "BigchainDB GmbH",
"position": "Lead UI Designer & Developer",
"website": "https://bigchaindb.com",
"startDate": "2016-12-01",
"endDate": "2018-12-31",
"summary": "Leading the UI design & development of all BigchainDB web properties. I created the initial BigchainDB brand and further conceptualized, executed and iterated on the creative and visual direction of BigchainDB. This included creating and iterating on a components-based UI design system for all of [BigchainDB's user interfaces](/bigchaindb)."
},
{
"company": "ascribe GmbH",
"position": "UI Designer & Developer",
"website": "https://ascribe.io",
"startDate": "2016-01-01",
"endDate": "2017-12-31",
"summary": "Leading the technical architecture of ascribe's web presence, and maintaining the front-end of the product."
},
{
"company": "ChartMogul Ltd.",
"position": "Lead UI Engineer",
"website": "https://chartmogul.com",
"startDate": "2015-07-15",
"endDate": "2017-02-01",
"summary": "Co-designing and leading the UI design & development of various [ChartMogul web properties](/chartmogul), helping the company to position itself as a market leader. This included the creation of a components-based UI design system and implementing it across all web touch points.\n\nBesides designing and implementing new features, I maintained the front-end of the ChartMogul application and implemented the UI design system by refactoring most of its front-end codebase."
},
{
"company": "UN World Food Programme/ShareTheMeal",
"position": "UI Engineer",
"website": "https://sharethemeal.org",
"startDate": "2014-10-01",
"endDate": "2015-06-01",
"summary": "Leading the creation of the [website for ShareTheMeal](/sharethemeal) and assisting in building and consulting for the iOS and Android app."
},
{
"company": "ezeep GmbH",
"position": "Lead Designer & Front End Developer",
"website": "https://ezeep.com",
"startDate": "2012-01-01",
"endDate": "2014-09-01",
"summary": "Creating an unprecedented, market-leading & award-winning user experience around printing based on the principles of emotional design way ahead of all competitors.\n\nThis included defining the product based on user & market research in an iterative process and designing & building [ezeeps numerous touch points](/ezeep), like the web app, web site, desktop apps for Windows & Mac OS X and apps for iOS & Android.\n\nOn top of that I created the corporate identity and a consistent visual branding, including the logo."
},
{
"company": "Martin Luther University Halle-Wittenberg",
"position": "UI/UX Designer & Front End Developer",
"startDate": "2009-02-01",
"endDate": "2012-01-01",
"summary": "Conceptualizing & implementing [numerous in-house and public facing interfaces](/unihalle) for thousands of students and staff. Additionally, conceptualizing, creating and maintaining the blog network & community for all students & staff."
},
{
"company": "Harz University of Applied Sciences",
"position": "Consultant & Teacher",
"startDate": "2011-02-01",
"endDate": "2011-05-01",
"summary": "Conceptualizing a web design & development university seminar and building a [responsive & fluid grid framework](https://github.com/kremalicious/hsresponsive) with a basic HTML/CSS template for students of Media Informatics at the Harz University of Applied Sciences to learn and use."
},
{
"company": "Martin Luther University Halle-Wittenberg",
"position": "Consultant & Teacher",
"startDate": "2011-02-01",
"endDate": "2011-05-01",
"summary": "Conceptualizing a WordPress-based web design university seminar and building a minimal starting theme for students of media & communication science at the MLU Halle-Wittenberg to learn and use."
},
{
"company": "Shortmoves",
"position": "Web Designer & Developer",
"startDate": "2009-01-01",
"endDate": "2010-01-01",
"summary": "Creating & managing the web presence and marketing material of the International Shortfilm Festival Shortmoves in Halle (Saale), Germany."
},
{
"company": "Agentur Ahron",
"position": "Co-Founder & Photojournalist & Photographer",
"startDate": "2005-01-01",
"endDate": "2008-12-31",
"summary": "Co-founded and built up a photo agency from the ground up and worked as a photographer ranging from journalistic works for news agencies & newspapers to photographic work for private clients."
},
{
"company": "Freelance",
"position": "Designer & Developer",
"startDate": "2004-01-01",
"summary": "Numerous projects and clients as a UI/UX Designer, Front End Developer, Icon Designer & Photographer."
}
],
"education": [
{
"institution": "Self-taught",
"area": "UI Design & Web Development",
"studyType": "Autodidactic",
"startDate": "1999-01-01",
"endDate": "2004-01-01"
},
{
"institution": "Martin Luther University Halle-Wittenberg",
"area": "Media/Communication Science & Art History",
"studyType": "Bachelor of Arts",
"startDate": "2008-01-01",
"endDate": "2012-01-01"
},
{
"institution": "Martin Luther University Halle-Wittenberg",
"area": "Political Science & Sociology",
"studyType": "Magister Artium",
"startDate": "2006-01-01",
"endDate": "2008-01-01"
}
],
"awards": [
{
"title": "German Design Award",
"date": "2015-11-01",
"awarder": "ezeep GmbH",
"summary": "Nominated in the category _Interactive User Experience (Excellent Communications Design)_"
},
{
"title": "CeBIT Preview Award",
"date": "2013-11-01",
"awarder": "ezeep GmbH"
}
],
"skills": [
{
"name": "Design",
"level": "Master",
"keywords": [
"Product Design",
"Service Design",
"Interface Design",
"User Experience Design",
"Communication Design",
"Interaction Design",
"Information Architecture",
"Icon Design",
"Web Design",
"Typography",
"Design management"
]
},
{
"name": "Web Development",
"level": "Master",
"keywords": [
"HTML",
"CSS",
"Javascript",
"Node.js",
"npm ecosystem",
"SASS/SCSS",
"Less",
"Stylus",
"Gulp",
"Gatsby",
"React",
"Styled Components",
"JAMstack"
]
},
{
"name": "General Software Development",
"level": "Master",
"keywords": [
"Git",
"GitHub",
"Bash",
"UNIX",
"Agile: Kanban & Scrum",
"Prototyping",
"Incremental"
]
},
{
"name": "DevOps",
"level": "Intermediate",
"keywords": ["AWS", "Now", "Serverless", "Cloudflare", "NGINX", "Apache"]
}
],
"languages": [
{
"language": "English",
"fluency": "Advanced speaker"
},
{
"language": "German",
"fluency": "Native speaker"
}
],
"meta": {
"canonical": "https://matthiaskretschmann.com/resume",
"lastModified": ""
}
}

View File

@ -2,47 +2,31 @@ const path = require('path')
const fs = require('fs')
const yaml = require('js-yaml')
const meta = yaml.load(fs.readFileSync('./content/meta.yml', 'utf8'))
const { title, url, matomoSite, matomoUrl } = meta[0]
const resume = require('./content/resume.json')
const { matomoSite, matomoUrl } = meta[0]
const { name, website } = resume.basics
require('dotenv').config()
module.exports = {
siteMetadata: {
siteUrl: `${url}`
siteUrl: `${website}`
},
plugins: [
'gatsby-transformer-yaml',
'gatsby-transformer-json',
{
resolve: 'gatsby-transformer-yaml',
resolve: 'gatsby-source-filesystem',
options: {
plugins: [
{
resolve: 'gatsby-source-filesystem',
options: {
name: 'content',
path: path.join(__dirname, 'content')
}
}
]
name: 'content',
path: path.join(__dirname, 'content')
}
},
{
resolve: 'gatsby-transformer-json',
resolve: 'gatsby-source-filesystem',
options: {
plugins: [
{
resolve: 'gatsby-source-filesystem',
options: {
name: 'pkg',
path: path.join(__dirname, 'package.json')
}
}
]
}
},
{
resolve: 'gatsby-plugin-sass',
options: {
includePaths: [`${__dirname}/node_modules`, `${__dirname}/src/styles`]
name: 'pkg',
path: path.join(__dirname, 'package.json')
}
},
{
@ -52,6 +36,12 @@ module.exports = {
path: path.join(__dirname, 'src', 'images')
}
},
{
resolve: 'gatsby-plugin-sass',
options: {
includePaths: [`${__dirname}/node_modules`, `${__dirname}/src/styles`]
}
},
{
resolve: 'gatsby-plugin-svgr',
options: {
@ -62,7 +52,7 @@ module.exports = {
resolve: 'gatsby-plugin-matomo',
options: {
siteId: `${matomoSite}`,
siteUrl: `${url}`,
siteUrl: `${website}`,
matomoUrl: `${matomoUrl}`,
localScript: '/piwik.js'
}
@ -70,7 +60,7 @@ module.exports = {
{
resolve: 'gatsby-plugin-manifest',
options: {
name: title.toLowerCase(),
name: name.toLowerCase(),
short_name: 'mk',
start_url: '/',
background_color: '#e7eef4',

View File

@ -2,7 +2,7 @@
const path = require('path')
const remark = require('remark')
const markdown = require('remark-parse')
const parse = require('remark-parse')
const html = require('remark-html')
const axios = require('axios')
const fs = require('fs')
@ -111,7 +111,7 @@ exports.onCreateNode = ({ node, actions }) => {
let descriptionHtml
remark()
.use(markdown, { gfm: true, commonmark: true, pedantic: true })
.use(parse, { gfm: true, commonmark: true, pedantic: true })
.use(html)
.process(descriptionWithLineBreaks, (err, file) => {
if (err) throw Error('Could not transform project description')

View File

@ -4,17 +4,6 @@
"tagline": "Designer & Developer",
"description": "Portfolio of web & ui designer/developer hybrid Matthias Kretschmann.",
"url": "https://matthiaskretschmann.com",
"email": "m@kretschmann.io",
"avatar": {
"childImageSharp": {
"fluid": {
"src": "/static/b45f45aa8d98d4e4019a242d38f2f248/bc3a8/avatar.jpg"
},
"resize": {
"src": "/static/b45f45aa8d98d4e4019a242d38f2f248/bc3a8/avatar.jpg"
}
}
},
"img": {
"childImageSharp": {
"resize": {
@ -22,14 +11,6 @@
}
}
},
"social": {
"Email": "mailto:m@kretschmann.io",
"Blog": "https://kremalicious.com",
"Twitter": "https://twitter.com/kremalicious",
"GitHub": "https://github.com/kremalicious",
"Dribbble": "https://dribbble.com/kremalicious",
"Keybase": "https://keybase.io/kremalicious"
},
"availability": {
"status": false,
"available": "👔 Available for new projects. <a href=\"mailto:m@kretschmann.io\">Lets talk</a>!",

View File

@ -0,0 +1,276 @@
{
"contentJson": {
"basics": {
"name": "Matthias Kretschmann",
"label": "Designer & Developer",
"picture": {
"childImageSharp": {
"fixed": {
"aspectRatio": 1,
"width": 256,
"height": 256,
"src": "/static/b45f45aa8d98d4e4019a242d38f2f248/c296b/avatar.jpg",
"srcSet": "/static/b45f45aa8d98d4e4019a242d38f2f248/c296b/avatar.jpg 1x,\n/static/b45f45aa8d98d4e4019a242d38f2f248/28b3a/avatar.jpg 1.5x,\n/static/b45f45aa8d98d4e4019a242d38f2f248/72cad/avatar.jpg 2x",
"srcWebp": "/static/b45f45aa8d98d4e4019a242d38f2f248/59c88/avatar.webp",
"srcSetWebp": "/static/b45f45aa8d98d4e4019a242d38f2f248/59c88/avatar.webp 1x,\n/static/b45f45aa8d98d4e4019a242d38f2f248/bd640/avatar.webp 1.5x,\n/static/b45f45aa8d98d4e4019a242d38f2f248/b957b/avatar.webp 2x",
"originalName": "avatar.jpg"
}
}
},
"email": "m@kretschmann.io",
"website": "https://matthiaskretschmann.com",
"summary": "",
"profiles": [
{
"network": "Blog",
"url": "https://kremalicious.com",
"username": null
},
{
"network": "Twitter",
"url": "https://twitter.com/kremalicious",
"username": "kremalicious"
},
{
"network": "GitHub",
"url": "https://github.com/kremalicious",
"username": "kremalicious"
},
{
"network": "Dribbble",
"url": "https://dribbble.com/kremalicious",
"username": "kremalicious"
},
{
"network": "Keybase",
"url": "https://keybase.io/kremalicious",
"username": "kremalicious"
}
],
"location": {
"city": "Berlin",
"countryCode": "DE"
}
},
"education": [
{
"institution": "Self-taught",
"area": "UI Design & Web Development",
"studyType": "Autodidactic",
"startDate": "1999-01-01",
"endDate": "2004-01-01"
},
{
"institution": "Martin Luther University Halle-Wittenberg",
"area": "Media/Communication Science & Art History",
"studyType": "Bachelor of Arts",
"startDate": "2008-01-01",
"endDate": "2012-01-01"
},
{
"institution": "Martin Luther University Halle-Wittenberg",
"area": "Political Science & Sociology",
"studyType": "Magister Artium",
"startDate": "2006-01-01",
"endDate": "2008-01-01"
}
],
"languages": [
{
"language": "English",
"fluency": "Advanced speaker"
},
{
"language": "German",
"fluency": "Native speaker"
}
],
"skills": [
{
"name": "Design",
"level": "Master",
"keywords": [
"Product Design",
"Service Design",
"Interface Design",
"User Experience Design",
"Communication Design",
"Interaction Design",
"Information Architecture",
"Icon Design",
"Web Design",
"Typography",
"Design management"
]
},
{
"name": "Web Development",
"level": "Master",
"keywords": [
"HTML",
"CSS",
"Javascript",
"Node.js",
"npm ecosystem",
"SASS/SCSS",
"Less",
"Stylus",
"Gulp",
"Gatsby",
"React",
"Styled Components",
"JAMstack"
]
},
{
"name": "General Software Development",
"level": "Master",
"keywords": [
"Git",
"GitHub",
"Bash",
"UNIX",
"Agile: Kanban & Scrum",
"Prototyping",
"Incremental"
]
},
{
"name": "DevOps",
"level": "Intermediate",
"keywords": [
"AWS",
"Now",
"Serverless",
"Cloudflare",
"NGINX",
"Apache"
]
}
],
"work": [
{
"company": "Ocean Protocol Foundation",
"position": "Lead UI Designer & Developer",
"website": "https://oceanprotocol.com",
"startDate": "2017-01-01",
"endDate": null,
"summary": "Leading the UI design & development of Ocean Protocol's user interfaces, iterating on a components-based UI design system spanning all of Ocean Protocol's web properties. \n\nConceptualize, execute and iterate on the creative and visual direction of the Ocean Protocol brand.\n\nAs a core developer leading the execution of [multiple user interfaces](/oceanprotocol) and core components.",
"highlights": ["Started the company"]
},
{
"company": "BigchainDB GmbH",
"position": "Lead UI Designer & Developer",
"website": "https://bigchaindb.com",
"startDate": "2016-12-01",
"endDate": "2018-12-31",
"summary": "Leading the UI design & development of all BigchainDB web properties. I created the initial BigchainDB brand and further conceptualized, executed and iterated on the creative and visual direction of BigchainDB. This included creating and iterating on a components-based UI design system for all of [BigchainDB's user interfaces](/bigchaindb).",
"highlights": ["Started the company"]
},
{
"company": "ascribe GmbH",
"position": "UI Designer & Developer",
"website": "https://ascribe.io",
"startDate": "2016-01-01",
"endDate": "2017-12-31",
"summary": "Description...",
"highlights": ["Started the company"]
},
{
"company": "ChartMogul Ltd.",
"position": "Lead UI Engineer",
"website": "https://chartmogul.com",
"startDate": "2015-07-15",
"endDate": "2017-02-01",
"summary": "Co-designing and leading the UI design & development of various [ChartMogul web properties](/chartmogul). This included the creation of a components-based UI design system and implementing it across all web touch points.\n\nBesides designing and implementing new features, I maintained the front-end of the ChartMogul application and implemented the UI design system by refactoring most of its front-end codebase.",
"highlights": ["Started the company"]
},
{
"company": "UN World Food Programme/ShareTheMeal",
"position": "UI Engineer",
"website": "https://sharethemeal.org",
"startDate": "2014-10-01",
"endDate": "2015-06-01",
"summary": "[app and website](/sharethemeal)",
"highlights": ["Started the company"]
},
{
"company": "ezeep GmbH",
"position": "Lead Designer & Front End Developer",
"website": "https://ezeep.com",
"startDate": "2012-01-01",
"endDate": "2014-09-01",
"summary": "Creating an unprecedented, market-leading & award-winning user experience around printing based on the principles of emotional design way ahead of all competitors.\n\nThis included defining the product based on user & market research in an iterative process and designing & building [ezeeps numerous touch points](/ezeep), like the web app, web site, desktop apps for Windows & Mac OS X and apps for iOS & Android.\n\nOn top of that I created the corporate identity and a consistent visual branding, including the logo.",
"highlights": null
},
{
"company": "Martin Luther University Halle-Wittenberg",
"position": "UI/UX Designer & Front End Developer",
"website": null,
"startDate": "2009-02-01",
"endDate": "2012-01-01",
"summary": "Conceptualizing & implementing [numerous in-house and public facing interfaces](/unihalle) for thousands of students and staff. Additionally, conceptualizing, creating and maintaining the blog network & community for all students & staff.",
"highlights": null
},
{
"company": "Harz University of Applied Sciences",
"position": "Consultant & Teacher",
"website": null,
"startDate": "2011-02-01",
"endDate": "2011-05-01",
"summary": "Conceptualizing a web design & development university seminar and building a [responsive & fluid grid framework](https://github.com/kremalicious/hsresponsive) with a basic HTML/CSS template for students of Media Informatics at the Harz University of Applied Sciences to learn and use.",
"highlights": null
},
{
"company": "Martin Luther University Halle-Wittenberg",
"position": "Consultant & Teacher",
"website": null,
"startDate": "2011-02-01",
"endDate": "2011-05-01",
"summary": "Conceptualizing a WordPress-based web design university seminar and building a minimal starting theme for students of media & communication science at the MLU Halle-Wittenberg to learn and use.",
"highlights": null
},
{
"company": "Shortmoves",
"position": "Web Designer & Developer",
"website": null,
"startDate": "2009-01-01",
"endDate": "2010-01-01",
"summary": "Creating & managing the web presence and marketing material of the International Shortfilm Festival Shortmoves in Halle (Saale), Germany.",
"highlights": null
},
{
"company": "Agentur Ahron",
"position": "Co-Founder & Photojournalist & Photographer",
"website": null,
"startDate": "2005-01-01",
"endDate": "2008-12-31",
"summary": "Co-founded and built up a photo agency from the ground up and worked as a photographer ranging from journalistic works for news agencies & newspapers to photographic work for private clients.",
"highlights": null
},
{
"company": "Freelance",
"position": "Designer & Developer",
"website": null,
"startDate": "2004-01-01",
"endDate": null,
"summary": "Numerous projects and clients as a UI/UX Designer, Front End Developer, Icon Designer & Photographer.",
"highlights": null
}
],
"awards": [
{
"title": "German Design Award",
"date": "2015-11-01",
"awarder": "ezeep GmbH",
"summary": "Nominated in the category _Interactive User Experience (Excellent Communications Design)_"
},
{
"title": "CeBIT Preview Award",
"date": "2013-11-01",
"awarder": "ezeep GmbH",
"summary": null
}
]
}
}

View File

@ -1,2 +1,21 @@
import '@testing-library/jest-dom/extend-expect'
import 'jest-canvas-mock'
import { StaticQuery, useStaticQuery } from 'gatsby'
import meta from './__fixtures__/meta.json'
import resume from './__fixtures__/resume.json'
beforeAll(() => {
const photoSrc = resume.contentJson.basics.picture.childImageSharp.fixed.src
const dataMock = {
...meta,
...resume,
photoSrc,
portfolioJson: { bugs: '' }
}
StaticQuery.mockImplementation(({ render }) => render({ ...dataMock }))
useStaticQuery.mockImplementation(() => {
return { ...dataMock }
})
})

69
package-lock.json generated
View File

@ -3042,6 +3042,14 @@
}
}
},
"@mapbox/hast-util-table-cell-style": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.1.3.tgz",
"integrity": "sha512-QsEsh5YaDvHoMQ2YHdvZy2iDnU3GgKVBTcHf6cILyoWDZtPSdlG444pL/ioPYO/GpXSfODBb9sefEetfC4v9oA==",
"requires": {
"unist-util-visit": "^1.3.0"
}
},
"@mikaelkristiansson/domready": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@mikaelkristiansson/domready/-/domready-1.0.9.tgz",
@ -12300,9 +12308,9 @@
"integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ=="
},
"handlebars": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.2.tgz",
"integrity": "sha512-29Zxv/cynYB7mkT1rVWQnV7mGX6v7H/miQ6dbEpYTKq5eJBN7PsRB+ViYJlcT6JINTSu4dVB9kOqEun78h6Exg==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz",
"integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==",
"dev": true,
"requires": {
"neo-async": "^2.6.0",
@ -12455,6 +12463,19 @@
"minimalistic-assert": "^1.0.1"
}
},
"hast-to-hyperscript": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-7.0.4.tgz",
"integrity": "sha512-vmwriQ2H0RPS9ho4Kkbf3n3lY436QKLq6VaGA1pzBh36hBi3tm1DO9bR+kaJIbpT10UqaANDkMjxvjVfr+cnOA==",
"requires": {
"comma-separated-tokens": "^1.0.0",
"property-information": "^5.3.0",
"space-separated-tokens": "^1.0.0",
"style-to-object": "^0.2.1",
"unist-util-is": "^3.0.0",
"web-namespaces": "^1.1.2"
}
},
"hast-util-is-element": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-1.0.3.tgz",
@ -13158,6 +13179,11 @@
"prop-types": "^15.5.10"
}
},
"inline-style-parser": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
"integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
},
"inquirer": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.0.tgz",
@ -20679,6 +20705,14 @@
"react-is": "^16.8.1"
}
},
"property-information": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-5.3.0.tgz",
"integrity": "sha512-IslotQn1hBCZDY7SaJ3zmCjVea219VTwmOk6Pu3z9haU9m4+T8GwaDubur+6NMHEU+Fjs/6/p66z6QULPkcL1w==",
"requires": {
"xtend": "^4.0.1"
}
},
"proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
@ -21531,6 +21565,11 @@
}
}
},
"remark-breaks": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-1.0.3.tgz",
"integrity": "sha512-ip5hvJE8vsUJCGfgHaEJbf/JfO6KTZV+NBG68AWkEMhrjHW3Qh7EorED41mCt0FFSTrUDeRiNHovKO7cqgPZmw=="
},
"remark-html": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/remark-html/-/remark-html-10.0.0.tgz",
@ -21564,6 +21603,17 @@
"xtend": "^4.0.1"
}
},
"remark-react": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/remark-react/-/remark-react-6.0.0.tgz",
"integrity": "sha512-5g73p8ZuqKoSdKByEf6IbXtVaHnbSEV0aamhIIqpzeNvj1wWDPX0USSPs4Gf3ZAsQIehIp6QiqJIbbXpq74bug==",
"requires": {
"@mapbox/hast-util-table-cell-style": "^0.1.3",
"hast-to-hyperscript": "^7.0.0",
"hast-util-sanitize": "^2.0.0",
"mdast-util-to-hast": "^6.0.0"
}
},
"remark-stringify": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-7.0.4.tgz",
@ -23631,6 +23681,14 @@
"integrity": "sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=",
"dev": true
},
"style-to-object": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.2.3.tgz",
"integrity": "sha512-1d/k4EY2N7jVLOqf2j04dTc37TPOv/hHxZmvpg8Pdh8UYydxeu/C1W1U4vD8alzf5V2Gt7rLsmkr4dxAlDm9ng==",
"requires": {
"inline-style-parser": "0.1.1"
}
},
"style-value-types": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-3.1.7.tgz",
@ -25715,6 +25773,11 @@
"defaults": "^1.0.3"
}
},
"web-namespaces": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.3.tgz",
"integrity": "sha512-r8sAtNmgR0WKOKOxzuSgk09JsHlpKlB+uHi937qypOu3PZ17UxPrierFKDye/uNHjNTTEshu5PId8rojIPj/tA=="
},
"webidl-conversions": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",

View File

@ -16,7 +16,7 @@
"build": "gatsby build",
"format": "prettier --write 'src/**/*.{js,jsx}'",
"format:css": "prettier-stylelint --write --quiet 'src/**/*.{css,scss}'",
"test": "npm run lint && jest --coverage",
"test": "npm run lint && jest --coverage --silent",
"test:watch": "npm run lint && jest --coverage --watch",
"deploy": "./scripts/deploy.sh",
"new": "babel-node ./scripts/new.js",
@ -49,8 +49,10 @@
"react-helmet": "^5.2.1",
"react-pose": "^4.0.10",
"remark": "^11.0.2",
"remark-breaks": "^1.0.3",
"remark-html": "^10.0.0",
"remark-parse": "^7.0.2",
"remark-react": "^6.0.0",
"shortid": "^2.2.15",
"suncalc": "^1.8.0",
"vcf": "^2.0.6"

View File

@ -28,6 +28,8 @@ export default function Layout({ children, location }) {
const isHomepage =
location.pathname === '/' ||
location.pathname === '/offline-plugin-app-shell-fallback/'
const isResume =
location.pathname === '/resume' || location.pathname === '/resume/'
return (
<>
@ -40,7 +42,7 @@ export default function Layout({ children, location }) {
delay={timeout}
delayChildren={timeout}
>
<Header minimal={!isHomepage} />
<Header minimal={!isHomepage} hide={isResume} />
<main className={styles.screen}>{children}</main>
</RoutesContainer>
</PoseGroup>

View File

@ -15,7 +15,12 @@ import {
FileText,
Key,
Image,
Mail
Mail,
MapPin,
Globe,
Briefcase,
Award,
BookOpen
} from 'react-feather'
import { ReactComponent as Dribbble } from '../../images/dribbble.svg'
import styles from './Icon.module.scss'
@ -41,7 +46,12 @@ const Icon = ({ name, ...props }) => {
FileText,
Key,
Image,
Mail
Mail,
MapPin,
Globe,
Briefcase,
Award,
BookOpen
}
const IconMapped = components[name]

View File

@ -2,34 +2,42 @@ import React from 'react'
import Helmet from 'react-helmet'
import PropTypes from 'prop-types'
import { useMeta } from '../../hooks/use-meta'
import { useResume } from '../../hooks/use-resume'
const MetaTags = ({ title, description, url, image, meta }) => (
<Helmet
defaultTitle={`${meta.title.toLowerCase()} { ${meta.tagline.toLowerCase()} }`}
titleTemplate={`%s // ${meta.title.toLowerCase()} { ${meta.tagline.toLowerCase()} }`}
title={title}
>
<html lang="en" />
const MetaTags = ({ title, description, url, image, meta }) => {
const { basics } = useResume()
const twitterHandle = basics.profiles.filter(
({ network }) => network === 'Twitter'
)[0].username
{/* General tags */}
<meta name="description" content={description} />
<meta name="image" content={`${meta.url}${image}`} />
<link rel="canonical" href={url} />
return (
<Helmet
defaultTitle={`${basics.name.toLowerCase()} { ${basics.label.toLowerCase()} }`}
titleTemplate={`%s // ${basics.name.toLowerCase()} { ${basics.label.toLowerCase()} }`}
title={title}
>
<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}`} />
{/* General tags */}
<meta name="description" content={description} />
<meta name="image" content={`${basics.website}${image}`} />
<link rel="canonical" href={url} />
{/* 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>
)
{/* 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={`${basics.website}${image}`} />
{/* Twitter Card tags */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:creator" content={twitterHandle} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={`${basics.website}${image}`} />
</Helmet>
)
}
MetaTags.propTypes = {
title: PropTypes.string,
@ -45,12 +53,13 @@ SEO.propTypes = {
export default function SEO({ project }) {
const meta = useMeta()
const { basics } = useResume()
const title = (project && project.title) || null
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
const url = (project && `${basics.website}${project.slug}`) || basics.website
return (
<MetaTags

View File

@ -2,20 +2,33 @@ import React from 'react'
import saveAs from 'file-saver'
import vCard from 'vcf'
import { useMeta } from '../../hooks/use-meta'
import { useResume } from '../../hooks/use-resume'
export default function Vcard() {
const metaYaml = useMeta()
const { basics } = useResume()
const photoSrc = basics.picture.childImageSharp.fixed.src
const { name, label, email, profiles } = basics
const meta = {
...metaYaml,
photoSrc,
name,
label,
email,
profiles
}
const handleAddressbookClick = e => {
e.preventDefault()
init(metaYaml)
init(meta)
}
return (
<a
// href is kinda fake, only there for usability
// so user knows what to expect when hovering the link before clicking
href={metaYaml.addressbook}
href={meta.addressbook}
onClick={handleAddressbookClick}
>
Add to addressbook
@ -24,39 +37,41 @@ export default function Vcard() {
}
export const init = async meta => {
const photoSrc = meta.avatar.childImageSharp.resize.src
// first, convert the avatar to base64, then construct all vCard elements
const dataUrl = await toDataURL(photoSrc, 'image/jpeg')
const vcard = await constructVcard(dataUrl, meta)
const dataUrl = await toDataURL(meta.photoSrc, 'image/jpeg')
const vcard = await constructVcard(meta, dataUrl)
downloadVcard(vcard, meta)
}
// Construct the download from a blob of the just constructed vCard,
// and save it to user's file system
export const downloadVcard = (vcard, meta) => {
// Construct the download from a blob of the just constructed vCard,
const { addressbook } = meta
const name = addressbook.split('/').join('')
const blob = new Blob([vcard], { type: 'text/x-vcard' })
const blob = new Blob([vcard], {
type: 'text/x-vcard'
})
// save it to user's file system
saveAs(blob, name)
}
export const constructVcard = async (dataUrl, meta) => {
export const constructVcard = async meta => {
const contact = new vCard()
const blog = meta.profiles.filter(({ network }) => network === 'Blog')[0].url
const twitter = meta.profiles.filter(
({ network }) => network === 'Twitter'
)[0].url
const github = meta.profiles.filter(({ network }) => network === 'GitHub')[0]
.url
// 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)
contact.set('title', meta.tagline)
contact.set('fn', meta.name)
contact.set('title', meta.label)
contact.set('email', meta.email)
contact.set('url', meta.url, { type: 'Portfolio' })
contact.add('url', meta.social.Blog, { type: 'Blog' })
contact.set('nickname', 'kremalicious')
contact.add('x-socialprofile', meta.social.Twitter, { type: 'twitter' })
contact.add('x-socialprofile', meta.social.GitHub, { type: 'GitHub' })
contact.set('url', meta.url, { type: 'Portfolio' })
contact.add('url', blog, { type: 'Blog' })
contact.add('x-socialprofile', twitter, { type: 'twitter' })
contact.add('x-socialprofile', github, { type: 'GitHub' })
const vcard = contact.toString('3.0')
@ -65,7 +80,7 @@ export const constructVcard = async (dataUrl, meta) => {
// Helper function to create base64 string from avatar image
// without the need to read image file from file system
export const toDataURL = async (photoSrc, outputFormat) => {
export async function toDataURL(photoSrc, outputFormat) {
const img = new Image()
img.crossOrigin = 'Anonymous'
img.src = photoSrc

View File

@ -1,17 +1,19 @@
import React from 'react'
import { render, fireEvent, waitForElement } from '@testing-library/react'
import { useStaticQuery } from 'gatsby'
import Vcard, { constructVcard, toDataURL, init } from './Vcard'
import data from '../../../jest/__fixtures__/meta.json'
import meta from '../../../jest/__fixtures__/meta.json'
import resume from '../../../jest/__fixtures__/resume.json'
const metaMock = {
...meta.metaYaml,
name: resume.contentJson.basics.name,
label: resume.contentJson.basics.label,
email: resume.contentJson.basics.email,
profiles: [...resume.contentJson.basics.profiles]
}
describe('Vcard', () => {
beforeEach(() => {
useStaticQuery.mockImplementationOnce(() => {
return {
...data
}
})
global.URL.createObjectURL = jest.fn()
})
@ -28,15 +30,12 @@ describe('Vcard', () => {
})
it('combined vCard download process finishes', async () => {
await init(data.metaYaml)
await init(metaMock)
expect(global.URL.createObjectURL).toHaveBeenCalledTimes(1)
})
it('vCard can be constructed', async () => {
const vcard = await constructVcard(
'data:image/jpeg;base64,00',
data.metaYaml
)
const vcard = await constructVcard(metaMock, 'data:image/jpeg;base64,00')
expect(vcard).toBeDefined()
})

View File

@ -2,11 +2,9 @@ import React from 'react'
import { render } from '@testing-library/react'
import Availability from './Availability'
import { useStaticQuery } from 'gatsby'
import data from '../../../jest/__fixtures__/meta.json'
describe('Availability', () => {
it('renders correctly from data file values', () => {
useStaticQuery.mockImplementation(() => ({ ...data }))
const { container } = render(<Availability />)
expect(container.firstChild).toBeInTheDocument()
})

View File

@ -2,26 +2,29 @@ import React from 'react'
import PropTypes from 'prop-types'
import { Link } from 'gatsby'
import posed from 'react-pose'
import { useMeta } from '../../hooks/use-meta'
import { moveInBottom } from '../atoms/Transitions'
import { ReactComponent as Logo } from '../../images/logo.svg'
import styles from './LogoUnit.module.scss'
import { useResume } from '../../hooks/use-resume'
LogoUnit.propTypes = {
minimal: PropTypes.bool
minimal: PropTypes.bool,
isResume: PropTypes.bool
}
export default function LogoUnit({ minimal }) {
const { title, tagline } = useMeta()
const { basics } = useResume()
const Animation = posed.div(moveInBottom)
return (
<Animation>
<Link className={minimal ? styles.minimal : styles.logounit} to={'/'}>
<Logo className={styles.logo} />
<h1 className={`p-name ${styles.title}`}>{title.toLowerCase()}</h1>
<h1 className={`p-name ${styles.title}`}>
{basics.name.toLowerCase()}
</h1>
<p className={`p-job-title ${styles.description}`}>
{tagline.toLowerCase()}
{basics.label.toLowerCase()}
</p>
</Link>
</Animation>

View File

@ -1,17 +1,8 @@
import React from 'react'
import { render } from '@testing-library/react'
import { useStaticQuery } from 'gatsby'
import LogoUnit from './LogoUnit'
import data from '../../../jest/__fixtures__/meta.json'
beforeEach(() => {
useStaticQuery.mockImplementationOnce(() => {
return {
...data
}
})
})
describe('LogoUnit', () => {
it('renders correctly from data file values', () => {
const { title, tagline } = data.metaYaml

View File

@ -3,30 +3,44 @@ import PropTypes from 'prop-types'
import posed from 'react-pose'
import { moveInTop } from '../atoms/Transitions'
import Icon from '../atoms/Icon'
import { useMeta } from '../../hooks/use-meta'
import { useResume } from '../../hooks/use-resume'
import styles from './Networks.module.scss'
const linkClasses = key =>
key === 'Email' ? `u-email ${styles.link}` : `u-url ${styles.link}`
const NetworkLink = ({ name, url }) => (
<a
className={linkClasses(name)}
href={url}
data-testid={`network-${name.toLowerCase()}`}
>
<Icon name={name} />
<span className={styles.title}>{name}</span>
</a>
)
NetworkLink.propTypes = {
name: PropTypes.string.isRequired,
url: PropTypes.string.isRequired
}
export default function Networks({ small, hide }) {
const { social } = useMeta()
const { basics } = useResume()
if (hide) return null
const Animation = posed.aside(moveInTop)
const linkClasses = key =>
key === 'Email' ? `u-email ${styles.link}` : `u-url ${styles.link}`
return (
<Animation className={small ? styles.small : styles.networks}>
{Object.keys(social).map((key, i) => (
<a
className={linkClasses(key)}
href={social[key]}
key={i}
data-testid={`network-${key.toLowerCase()}`}
>
<Icon name={key} />
<span className={styles.title}>{key}</span>
</a>
<NetworkLink name="Mail" url={`mailto:${basics.email}`} />
{basics.profiles.map(profile => (
<NetworkLink
key={profile.network}
name={profile.network}
url={profile.url}
/>
))}
</Animation>
)

View File

@ -21,11 +21,11 @@
}
.link {
margin-left: $spacer / 2;
margin-right: $spacer / 2;
margin-left: $spacer / $line-height;
margin-right: $spacer / $line-height;
margin-bottom: $spacer / 2;
text-align: center;
display: block;
display: inline-block;
flex: 0 1;
min-width: 2.5rem;

View File

@ -1,41 +1,22 @@
import React from 'react'
import { render } from '@testing-library/react'
import { useStaticQuery } from 'gatsby'
import Networks from './Networks'
import data from '../../../jest/__fixtures__/meta.json'
beforeEach(() => {
useStaticQuery.mockImplementationOnce(() => {
return {
...data
}
})
})
describe('Networks', () => {
it('renders correctly from data file values', () => {
const { social } = data.metaYaml
const { container, getByTestId } = render(<Networks />)
const { container } = render(<Networks />)
expect(container.firstChild).toBeInTheDocument()
expect(container.firstChild.nodeName).toBe('ASIDE')
expect(getByTestId('network-email').href).toBe(social.Email)
expect(getByTestId('network-blog').href).toBe(social.Blog + '/')
expect(getByTestId('network-twitter').href).toBe(social.Twitter)
expect(getByTestId('network-github').href).toBe(social.GitHub)
expect(getByTestId('network-dribbble').href).toBe(social.Dribbble)
})
it('renders correctly in small variant', () => {
const { container } = render(<Networks small={true} />)
expect(container.firstChild).toBeInTheDocument()
expect(container.querySelector('.small')).toBeInTheDocument()
})
it('can be hidden', () => {
const { container } = render(<Networks hide={true} />)
expect(container.firstChild).not.toBeInTheDocument()
})
})

View File

@ -17,6 +17,10 @@
height: $font-size-base * 0.9;
}
}
@media print {
display: none;
}
}
.checkboxContainer {

View File

@ -1,19 +1,8 @@
import React from 'react'
import { render } from '@testing-library/react'
import { useStaticQuery } from 'gatsby'
import Footer from './Footer'
import data from '../../../jest/__fixtures__/meta.json'
describe('Footer', () => {
beforeEach(() => {
useStaticQuery.mockImplementation(() => {
return {
...data,
portfolioJson: { bugs: '' }
}
})
})
it('renders correctly', () => {
const { container } = render(<Footer />)
expect(container.firstChild).toBeInTheDocument()

View File

@ -8,18 +8,23 @@ import styles from './Header.module.scss'
import { useMeta } from '../../hooks/use-meta'
Header.propTypes = {
minimal: PropTypes.bool
minimal: PropTypes.bool,
hide: PropTypes.bool
}
export default function Header({ minimal }) {
export default function Header({ minimal, hide }) {
const { availability } = useMeta()
return (
<header className={minimal ? styles.minimal : styles.header}>
<ThemeSwitch />
<LogoUnit minimal={minimal} />
<Networks hide={minimal} />
<Availability hide={minimal && !availability.status} />
{!hide && (
<>
<LogoUnit minimal={minimal} />
<Networks hide={minimal} />
<Availability hide={minimal && !availability.status} />
</>
)}
</header>
)
}

View File

@ -1,16 +1,8 @@
import React from 'react'
import { render, cleanup, wait } from '@testing-library/react'
import { useStaticQuery } from 'gatsby'
import Header from './Header'
import data from '../../../jest/__fixtures__/meta.json'
describe('Header', () => {
beforeEach(() => {
useStaticQuery.mockImplementation(() => {
return { ...data }
})
})
afterEach(cleanup)
it('renders correctly', async () => {

View File

@ -3,11 +3,7 @@ import { useStaticQuery, graphql } from 'gatsby'
const query = graphql`
query Meta {
metaYaml {
title
tagline
description
url
email
img {
childImageSharp {
resize(width: 980) {
@ -15,21 +11,6 @@ const query = graphql`
}
}
}
avatar {
childImageSharp {
resize {
src
}
}
}
social {
Mail
Blog
Twitter
GitHub
Dribbble
Keybase
}
availability {
status
available

66
src/hooks/use-resume.js Normal file
View File

@ -0,0 +1,66 @@
import { useStaticQuery, graphql } from 'gatsby'
const query = graphql`
query Resume {
contentJson {
basics {
name
label
picture {
childImageSharp {
fixed(width: 256, height: 256) {
...GatsbyImageSharpFixed_withWebp_noBase64
}
}
}
email
website
summary
profiles {
network
url
username
}
location {
city
countryCode
}
}
education {
institution
area
studyType
startDate
endDate
}
languages {
language
fluency
}
skills {
name
level
keywords
}
work {
company
position
website
startDate
endDate
summary
}
awards {
title
date
awarder
summary
}
}
}
`
export const useResume = () => {
const { contentJson } = useStaticQuery(query)
return contentJson
}

View File

@ -1,18 +1,9 @@
import React from 'react'
import { render } from '@testing-library/react'
import { StaticQuery, useStaticQuery } from 'gatsby'
import Home from '../index'
import meta from '../../../jest/__fixtures__/meta.json'
import projects from '../../../jest/__fixtures__/projects.json'
import projectImageFiles from '../../../jest/__fixtures__/projectImageFiles.json'
beforeEach(() => {
StaticQuery.mockImplementation(({ render }) => render({ ...meta }))
useStaticQuery.mockImplementation(() => {
return { ...meta }
})
})
describe('Home', () => {
const data = {
...projects,

View File

@ -0,0 +1,10 @@
import React from 'react'
import { render } from '@testing-library/react'
import Resume from '../resume'
describe('Resume', () => {
it('renders correctly from data file values', () => {
const { container } = render(<Resume />)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -0,0 +1,48 @@
import React from 'react'
import shortid from 'shortid'
import { useResume } from '../../hooks/use-resume'
import Icon from '../../components/atoms/Icon'
import styles from './Header.module.scss'
export default function Header() {
const { basics, languages } = useResume()
const { name, label, email, website, location } = basics
return (
<>
<header>
<p>Résumé</p>
<h1 className={styles.title}>{name}</h1>
<h2 className={styles.label}>{label}</h2>
</header>
<div>
<ul className={styles.contact}>
<li>
<a href={website}>
<Icon name="Compass" />
Portfolio
</a>
</li>
<li>
<Icon name="Mail" />
<a href={`mailto:${email}`}>Email</a>
</li>
<li>
<Icon name="MapPin" />
{location.city}, {location.countryCode}
</li>
<li className={styles.languages}>
<Icon name="Globe" />
{languages.map(item => (
<p key={shortid.generate()}>
{item.language}
<span>{item.fluency}</span>
</p>
))}
</li>
</ul>
</div>
</>
)
}

View File

@ -0,0 +1,69 @@
@import 'variables';
.title {
font-size: $font-size-h2;
margin-bottom: $spacer / 4;
@media print {
margin-bottom: 0;
}
}
.label {
font-size: $font-size-h3;
color: $brand-grey-light;
margin-bottom: 0;
:global(.dark) & {
color: $brand-grey-dimmed;
}
}
.contact {
list-style: none;
padding: 0;
li,
p {
margin-bottom: $spacer / 4;
}
svg {
stroke: $brand-grey-light;
margin-right: $spacer / 4;
margin-bottom: -0.1rem;
:global(.dark) & {
stroke: $brand-grey;
}
}
@media (min-width: $screen-md) {
margin-top: $spacer * 2.25;
}
}
.languages {
svg {
display: inline-block;
}
p {
margin-left: $spacer / 1.1;
&:first-of-type {
margin-left: 0;
display: inline-block;
}
}
span {
font-size: $font-size-small;
margin-left: $spacer / 4;
color: $brand-grey-light;
:global(.dark) & {
color: $brand-grey;
}
}
}

View File

@ -0,0 +1,99 @@
import React from 'react'
import PropTypes from 'prop-types'
import remark from 'remark'
import remark2react from 'remark-react'
import parse from 'remark-parse'
import html from 'remark-html'
import breaks from 'remark-breaks'
import styles from './ResumeItem.module.scss'
const markdownOutput = text =>
remark()
.use(parse, { gfm: true, commonmark: true, pedantic: true })
.use(html)
.use(breaks)
.use(remark2react)
.processSync(text).contents
function normalizeData(workPlace, eduPlace, award) {
const title = workPlace
? workPlace.company
: award
? award.title
: eduPlace
? eduPlace.institution
: null
const subTitle = workPlace
? workPlace.position
: award
? award.awarder
: eduPlace
? eduPlace.area
: null
const text = workPlace
? workPlace.summary
: award && award.summary
? award.summary
: eduPlace
? eduPlace.studyType
: null
const startDate = award
? award.date
: (workPlace && workPlace.startDate) || (eduPlace && eduPlace.startDate)
const endDate = award
? null
: (workPlace && workPlace.endDate) || (eduPlace && eduPlace.endDate)
return { title, subTitle, text, startDate, endDate }
}
export default function ResumeItem({ workPlace, eduPlace, award }) {
const { title, subTitle, text, startDate, endDate } = normalizeData(
workPlace,
eduPlace,
award
)
const dateStart = new Date(startDate).getFullYear()
const dateEnd = endDate && new Date(endDate).getFullYear()
const isSameYear = dateStart === dateEnd
return (
<div className={styles.resumeItem}>
<span className={styles.time}>
{dateStart}
{dateEnd ? !isSameYear && `${dateEnd}` : 'present'}{' '}
</span>
<h4 className={styles.title}>{title}</h4>
<h5 className={styles.subTitle}>{subTitle}</h5>
{text && markdownOutput(text)}
</div>
)
}
ResumeItem.propTypes = {
workPlace: PropTypes.shape({
startDate: PropTypes.string.isRequired,
endDate: PropTypes.string,
company: PropTypes.string.isRequired,
position: PropTypes.string.isRequired,
summary: PropTypes.string
}),
eduPlace: PropTypes.shape({
startDate: PropTypes.string.isRequired,
endDate: PropTypes.string,
institution: PropTypes.string.isRequired,
area: PropTypes.string.isRequired,
studyType: PropTypes.string
}),
award: PropTypes.shape({
date: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
awarder: PropTypes.string.isRequired,
summary: PropTypes.string
})
}

View File

@ -0,0 +1,77 @@
@import 'variables';
.resumeItem {
padding-bottom: $spacer * 3;
padding-left: $spacer;
position: relative;
border-left: 0.1rem solid rgba($brand-grey-light, 0.25);
&::before {
content: '';
display: block;
width: $font-size-mini;
height: $font-size-mini;
border-radius: 50%;
background: $body-background-color;
border: 0.1rem solid $color-headings;
position: absolute;
left: -($font-size-mini / 1.8);
top: 0.15rem;
:global(.dark) & {
background: $body-background-color--dark;
border-color: $color-headings--dark;
}
}
p:last-child {
margin-bottom: 0;
}
&:last-child {
border: none;
}
}
.title {
margin-bottom: $spacer / 3;
font-size: $font-size-h4;
position: relative;
top: -($spacer / 6);
}
.subTitle {
color: $brand-grey-light;
font-size: $font-size-h5;
:global(.dark) & {
color: $brand-grey-dimmed;
}
}
.time {
display: block;
margin-bottom: $spacer / 2;
white-space: nowrap;
font-style: italic;
@media (min-width: $screen-md) {
text-align: right;
position: absolute;
top: -0.3rem;
right: 105%;
}
}
@media print {
.resumeItem {
padding-bottom: $spacer * 2;
}
.time {
text-align: left;
position: relative;
top: auto;
right: auto;
}
}

View File

@ -0,0 +1,58 @@
import React from 'react'
import shortid from 'shortid'
import SEO from '../../components/atoms/SEO'
import Icon from '../../components/atoms/Icon'
import { useResume } from '../../hooks/use-resume'
import styles from './index.module.scss'
import Header from './Header'
import ResumeItem from './ResumeItem'
export default function Resume() {
const { education, work, awards } = useResume()
return (
<>
<SEO />
<div className={styles.resume}>
<Header />
<div>
<h3 className={styles.subTitle}>
<Icon name="Briefcase" />
Work
</h3>
</div>
<div>
{work.map(workPlace => (
<ResumeItem key={shortid.generate()} workPlace={workPlace} />
))}
</div>
<div>
<h3 className={styles.subTitle}>
<Icon name="Award" />
Awards
</h3>
</div>
<div>
{awards.map(award => (
<ResumeItem key={shortid.generate()} award={award} />
))}
</div>
<div>
<h3 className={styles.subTitle}>
<Icon name="BookOpen" />
Education
</h3>
</div>
<div>
{education.map(eduPlace => (
<ResumeItem key={shortid.generate()} eduPlace={eduPlace} />
))}
</div>
</div>
</>
)
}

View File

@ -0,0 +1,47 @@
@import 'variables';
.resume {
padding: $spacer;
display: grid;
grid-gap: $spacer * 4;
grid-template-columns: 1fr;
@media (min-width: $screen-md) {
grid-template-columns: 1fr 2fr;
max-width: calc(#{$projectImageMaxWidth} + #{$spacer * 2});
margin: 0 auto;
padding: $spacer $spacer * 3;
}
}
.subTitle {
font-size: $font-size-h3;
margin-bottom: 0;
margin-top: -($spacer / 3);
svg {
width: $font-size-large;
height: $font-size-large;
margin-right: $spacer / 4;
stroke: $brand-grey-light;
}
}
@media print {
:global(html) {
font-size: 8pt;
}
body {
background: #fff !important;
margin: 1cm 1.5cm;
}
p {
color: silver;
}
.resume {
grid-template-columns: 1fr;
}
}

View File

@ -40,10 +40,10 @@ $font-size-mini: 0.7rem;
$font-size-h1: 2.5rem;
$font-size-h2: 2rem;
$font-size-h3: 1.75rem;
$font-size-h4: $font-size-large;
$font-size-h5: $font-size-base;
$font-size-h6: $font-size-small;
$font-size-h3: 1.65rem;
$font-size-h4: 1.45rem;
$font-size-h5: $font-size-large;
$font-size-h6: $font-size-base;
$line-height: 1.5;
$line-height-small: 1.1428571429;

View File

@ -2,3 +2,4 @@
# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449
User-agent: *
Disallow: /resume