mirror of
https://github.com/kremalicious/portfolio.git
synced 2024-11-15 01:25:25 +01:00
Merge pull request #188 from kremalicious/feature/resume
Refactor to use JSON Resume
This commit is contained in:
commit
dbd486f96d
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@ -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
|
||||
|
21
README.md
21
README.md
@ -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:
|
||||
|
||||
|
@ -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
236
content/resume.json
Normal 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 [ezeep’s 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": ""
|
||||
}
|
||||
}
|
@ -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',
|
||||
|
@ -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')
|
||||
|
@ -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\">Let’s talk</a>!",
|
||||
|
276
jest/__fixtures__/resume.json
Normal file
276
jest/__fixtures__/resume.json
Normal 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 [ezeep’s 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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -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
69
package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
})
|
||||
|
||||
|
@ -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()
|
||||
})
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
@ -17,6 +17,10 @@
|
||||
height: $font-size-base * 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.checkboxContainer {
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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 () => {
|
||||
|
@ -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
66
src/hooks/use-resume.js
Normal 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
|
||||
}
|
@ -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,
|
||||
|
10
src/pages/__tests__/resume.test.jsx
Normal file
10
src/pages/__tests__/resume.test.jsx
Normal 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()
|
||||
})
|
||||
})
|
48
src/pages/resume/Header.jsx
Normal file
48
src/pages/resume/Header.jsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
69
src/pages/resume/Header.module.scss
Normal file
69
src/pages/resume/Header.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
99
src/pages/resume/ResumeItem.jsx
Normal file
99
src/pages/resume/ResumeItem.jsx
Normal 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
|
||||
})
|
||||
}
|
77
src/pages/resume/ResumeItem.module.scss
Normal file
77
src/pages/resume/ResumeItem.module.scss
Normal 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;
|
||||
}
|
||||
}
|
58
src/pages/resume/index.jsx
Normal file
58
src/pages/resume/index.jsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
47
src/pages/resume/index.module.scss
Normal file
47
src/pages/resume/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -2,3 +2,4 @@
|
||||
# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449
|
||||
|
||||
User-agent: *
|
||||
Disallow: /resume
|
||||
|
Loading…
Reference in New Issue
Block a user