mirror of
https://github.com/kremalicious/portfolio.git
synced 2025-01-03 10:25:00 +01:00
add resume
This commit is contained in:
parent
13bdde4686
commit
f44e0557ed
11
README.md
11
README.md
@ -19,6 +19,7 @@
|
|||||||
- [💍 One data file to rule all pages](#-one-data-file-to-rule-all-pages)
|
- [💍 One data file to rule all pages](#-one-data-file-to-rule-all-pages)
|
||||||
- [🐱 GitHub repositories](#-github-repositories)
|
- [🐱 GitHub repositories](#-github-repositories)
|
||||||
- [💅 Theme switcher](#-theme-switcher)
|
- [💅 Theme switcher](#-theme-switcher)
|
||||||
|
- [🗂 Resume](#-resume)
|
||||||
- [🏆 SEO component](#-seo-component)
|
- [🏆 SEO component](#-seo-component)
|
||||||
- [📇 Client-side vCard creation](#-client-side-vcard-creation)
|
- [📇 Client-side vCard creation](#-client-side-vcard-creation)
|
||||||
- [💫 Page transitions](#-page-transitions)
|
- [💫 Page transitions](#-page-transitions)
|
||||||
@ -76,6 +77,16 @@ If you want to know how, have a look at the respective components:
|
|||||||
- [`src/components/molecules/ThemeSwitch.jsx`](src/components/molecules/ThemeSwitch.jsx)
|
- [`src/components/molecules/ThemeSwitch.jsx`](src/components/molecules/ThemeSwitch.jsx)
|
||||||
- [`src/hooks/use-dark-mode.jsx`](src/hooks/use-dark-mode.jsx)
|
- [`src/hooks/use-dark-mode.jsx`](src/hooks/use-dark-mode.jsx)
|
||||||
|
|
||||||
|
### 🗂 Resume
|
||||||
|
|
||||||
|
Resume page based on [JSON Resume](https://jsonresume.org) standard.
|
||||||
|
|
||||||
|
If you want to know how, have a look at the respective components:
|
||||||
|
|
||||||
|
- [`src/pages/resume.jsx`](src/pages/resume.jsx)
|
||||||
|
- [`content/resume.json`](content/resume.json)
|
||||||
|
- [`src/hooks/use-resume.js`](src/hooks/use-resume.js)
|
||||||
|
|
||||||
### 🏆 SEO component
|
### 🏆 SEO component
|
||||||
|
|
||||||
Includes a SEO component which automatically switches all required `meta` tags for search engines, Twitter Cards, and Facebook OpenGraph tags based on the browsed route/page.
|
Includes a SEO component which automatically switches all required `meta` tags for search engines, Twitter Cards, and Facebook OpenGraph tags based on the browsed route/page.
|
||||||
|
@ -1,19 +1,11 @@
|
|||||||
|
# more personal metadata can be found in ./resume.json
|
||||||
|
|
||||||
- title: Matthias Kretschmann
|
- title: Matthias Kretschmann
|
||||||
tagline: Designer & Developer
|
tagline: Designer & Developer
|
||||||
description: Portfolio of web & ui designer/developer hybrid Matthias Kretschmann.
|
description: Portfolio of web & ui designer/developer hybrid Matthias Kretschmann.
|
||||||
url: https://matthiaskretschmann.com
|
url: https://matthiaskretschmann.com
|
||||||
email: m@kretschmann.io
|
|
||||||
avatar: ../src/images/avatar.jpg
|
|
||||||
img: ../src/images/twitter-card.png
|
img: ../src/images/twitter-card.png
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
availability:
|
availability:
|
||||||
status: false
|
status: false
|
||||||
available: '👔 Available for new projects. <a href="mailto:m@kretschmann.io">Let’s talk</a>!'
|
available: '👔 Available for new projects. <a href="mailto:m@kretschmann.io">Let’s talk</a>!'
|
||||||
|
237
content/resume.json
Normal file
237
content/resume.json
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"country": "Germany",
|
||||||
|
"countryCode": "DE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"work": [
|
||||||
|
{
|
||||||
|
"company": "Ocean Protocol Foundation",
|
||||||
|
"position": "Lead UI Designer & Developer",
|
||||||
|
"website": "https://oceanprotocol.com",
|
||||||
|
"startDate": "2017-01-01",
|
||||||
|
"summary": "Leading the UI design & development of Ocean Protocol, iterating on a components-based UI design system spanning all of Ocean Protocol's web properties. Additionally, I conceptualize, execute and iterate on the creative and visual direction of the Ocean Protocol brand.",
|
||||||
|
"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 web properties.",
|
||||||
|
"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. This included the creation of a components-based UI design system and implementing it across all web touch points. Besides 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": "...",
|
||||||
|
"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. This included defining the product based on user & market research in an iterative process and designing & building ezeep’s numerous touch points, like the web app, web site, desktop apps for Windows & Mac OS X and apps for iOS & Android. On 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 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 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",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "German",
|
||||||
|
"fluency": "Native speaker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "English",
|
||||||
|
"fluency": "Advanced speaker"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -11,38 +11,20 @@ module.exports = {
|
|||||||
siteUrl: `${url}`
|
siteUrl: `${url}`
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
'gatsby-transformer-yaml',
|
||||||
|
'gatsby-transformer-json',
|
||||||
{
|
{
|
||||||
resolve: 'gatsby-transformer-yaml',
|
resolve: 'gatsby-source-filesystem',
|
||||||
options: {
|
options: {
|
||||||
plugins: [
|
name: 'content',
|
||||||
{
|
path: path.join(__dirname, 'content')
|
||||||
resolve: 'gatsby-source-filesystem',
|
|
||||||
options: {
|
|
||||||
name: 'content',
|
|
||||||
path: path.join(__dirname, 'content')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resolve: 'gatsby-transformer-json',
|
resolve: 'gatsby-source-filesystem',
|
||||||
options: {
|
options: {
|
||||||
plugins: [
|
name: 'pkg',
|
||||||
{
|
path: path.join(__dirname, 'package.json')
|
||||||
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`]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -52,6 +34,12 @@ module.exports = {
|
|||||||
path: path.join(__dirname, 'src', 'images')
|
path: path.join(__dirname, 'src', 'images')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
resolve: 'gatsby-plugin-sass',
|
||||||
|
options: {
|
||||||
|
includePaths: [`${__dirname}/node_modules`, `${__dirname}/src/styles`]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
resolve: 'gatsby-plugin-svgr',
|
resolve: 'gatsby-plugin-svgr',
|
||||||
options: {
|
options: {
|
||||||
|
@ -4,32 +4,6 @@
|
|||||||
"tagline": "Designer & Developer",
|
"tagline": "Designer & Developer",
|
||||||
"description": "Portfolio of web & ui designer/developer hybrid Matthias Kretschmann.",
|
"description": "Portfolio of web & ui designer/developer hybrid Matthias Kretschmann.",
|
||||||
"url": "https://matthiaskretschmann.com",
|
"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": {
|
|
||||||
"src": "/static/5ecbb5694b0b2152aa71398164af38b2/da1a7/twitter-card.png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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": {
|
"availability": {
|
||||||
"status": false,
|
"status": false,
|
||||||
"available": "👔 Available for new projects. <a href=\"mailto:m@kretschmann.io\">Let’s talk</a>!",
|
"available": "👔 Available for new projects. <a href=\"mailto:m@kretschmann.io\">Let’s talk</a>!",
|
||||||
|
@ -28,6 +28,7 @@ export default function Layout({ children, location }) {
|
|||||||
const isHomepage =
|
const isHomepage =
|
||||||
location.pathname === '/' ||
|
location.pathname === '/' ||
|
||||||
location.pathname === '/offline-plugin-app-shell-fallback/'
|
location.pathname === '/offline-plugin-app-shell-fallback/'
|
||||||
|
const isResume = location.pathname === '/resume'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -40,7 +41,7 @@ export default function Layout({ children, location }) {
|
|||||||
delay={timeout}
|
delay={timeout}
|
||||||
delayChildren={timeout}
|
delayChildren={timeout}
|
||||||
>
|
>
|
||||||
<Header minimal={!isHomepage} />
|
<Header minimal={!isHomepage} isResume={isResume} />
|
||||||
<main className={styles.screen}>{children}</main>
|
<main className={styles.screen}>{children}</main>
|
||||||
</RoutesContainer>
|
</RoutesContainer>
|
||||||
</PoseGroup>
|
</PoseGroup>
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Helmet from 'react-helmet'
|
import Helmet from 'react-helmet'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { useMeta } from '../../hooks/use-meta'
|
import { useMeta } from '../../hooks/use-meta'`
|
||||||
|
import { useResume } from '../../hooks/use-resume'
|
||||||
|
|
||||||
|
const MetaTags = ({ title, description, url, image, meta }) => {
|
||||||
|
const resume = useResume()
|
||||||
|
const twitterHandle = resume.basics.profiles.filter(
|
||||||
|
({ network }) => network === 'Twitter'
|
||||||
|
)[0].username
|
||||||
|
|
||||||
const MetaTags = ({ title, description, url, image, meta }) => (
|
const MetaTags = ({ title, description, url, image, meta }) => (
|
||||||
<Helmet
|
<Helmet
|
||||||
@ -24,7 +31,7 @@ const MetaTags = ({ title, description, url, image, meta }) => (
|
|||||||
|
|
||||||
{/* Twitter Card tags */}
|
{/* Twitter Card tags */}
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:creator" content={meta.social.Twitter} />
|
<meta name="twitter:creator" content={twitterHandle} />
|
||||||
<meta name="twitter:title" content={title} />
|
<meta name="twitter:title" content={title} />
|
||||||
<meta name="twitter:description" content={description} />
|
<meta name="twitter:description" content={description} />
|
||||||
<meta name="twitter:image" content={`${meta.url}${image}`} />
|
<meta name="twitter:image" content={`${meta.url}${image}`} />
|
||||||
|
@ -2,20 +2,34 @@ import React from 'react'
|
|||||||
import saveAs from 'file-saver'
|
import saveAs from 'file-saver'
|
||||||
import vCard from 'vcf'
|
import vCard from 'vcf'
|
||||||
import { useMeta } from '../../hooks/use-meta'
|
import { useMeta } from '../../hooks/use-meta'
|
||||||
|
import { useResume } from '../../hooks/use-resume'
|
||||||
|
|
||||||
export default function Vcard() {
|
export default function Vcard() {
|
||||||
const metaYaml = useMeta()
|
const metaYaml = useMeta()
|
||||||
|
const { basics } = useResume()
|
||||||
|
const data = useStaticQuery(query)
|
||||||
|
const photoSrc = basics.picture.childImageSharp.fixed.src
|
||||||
|
const { name, label, email, profiles } = basics
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
...metaYaml,
|
||||||
|
photoSrc,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
email,
|
||||||
|
profiles
|
||||||
|
}
|
||||||
|
|
||||||
const handleAddressbookClick = e => {
|
const handleAddressbookClick = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
init(metaYaml)
|
init(meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
// href is kinda fake, only there for usability
|
// href is kinda fake, only there for usability
|
||||||
// so user knows what to expect when hovering the link before clicking
|
// so user knows what to expect when hovering the link before clicking
|
||||||
href={metaYaml.addressbook}
|
href={meta.addressbook}
|
||||||
onClick={handleAddressbookClick}
|
onClick={handleAddressbookClick}
|
||||||
>
|
>
|
||||||
Add to addressbook
|
Add to addressbook
|
||||||
@ -24,10 +38,8 @@ export default function Vcard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const init = async meta => {
|
export const init = async meta => {
|
||||||
const photoSrc = meta.avatar.childImageSharp.resize.src
|
|
||||||
|
|
||||||
// first, convert the avatar to base64, then construct all vCard elements
|
// first, convert the avatar to base64, then construct all vCard elements
|
||||||
const dataUrl = await toDataURL(photoSrc, 'image/jpeg')
|
const dataUrl = await toDataURL(meta.photoSrc, 'image/jpeg')
|
||||||
const vcard = await constructVcard(dataUrl, meta)
|
const vcard = await constructVcard(dataUrl, meta)
|
||||||
|
|
||||||
downloadVcard(vcard, meta)
|
downloadVcard(vcard, meta)
|
||||||
@ -49,12 +61,12 @@ export const constructVcard = async (dataUrl, meta) => {
|
|||||||
// for vcard to actually display the image for whatever reason
|
// for vcard to actually display the image for whatever reason
|
||||||
// const dataUrlCleaned = dataUrl.split('data:image/jpeg;base64,').join('')
|
// const dataUrlCleaned = dataUrl.split('data:image/jpeg;base64,').join('')
|
||||||
// contact.set('photo', dataUrlCleaned, { encoding: 'b', type: 'JPEG' })
|
// contact.set('photo', dataUrlCleaned, { encoding: 'b', type: 'JPEG' })
|
||||||
contact.set('fn', meta.title)
|
contact.set('fn', meta.name)
|
||||||
contact.set('title', meta.tagline)
|
contact.set('title', meta.label)
|
||||||
contact.set('email', meta.email)
|
contact.set('email', meta.email)
|
||||||
|
contact.set('nickname', 'kremalicious')
|
||||||
contact.set('url', meta.url, { type: 'Portfolio' })
|
contact.set('url', meta.url, { type: 'Portfolio' })
|
||||||
contact.add('url', meta.social.Blog, { type: 'Blog' })
|
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.Twitter, { type: 'twitter' })
|
||||||
contact.add('x-socialprofile', meta.social.GitHub, { type: 'GitHub' })
|
contact.add('x-socialprofile', meta.social.GitHub, { type: 'GitHub' })
|
||||||
|
|
||||||
|
@ -8,7 +8,8 @@ import { ReactComponent as Logo } from '../../images/logo.svg'
|
|||||||
import styles from './LogoUnit.module.scss'
|
import styles from './LogoUnit.module.scss'
|
||||||
|
|
||||||
LogoUnit.propTypes = {
|
LogoUnit.propTypes = {
|
||||||
minimal: PropTypes.bool
|
minimal: PropTypes.bool,
|
||||||
|
isResume: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LogoUnit({ minimal }) {
|
export default function LogoUnit({ minimal }) {
|
||||||
|
@ -4,8 +4,25 @@ import posed from 'react-pose'
|
|||||||
import { moveInTop } from '../atoms/Transitions'
|
import { moveInTop } from '../atoms/Transitions'
|
||||||
import Icon from '../atoms/Icon'
|
import Icon from '../atoms/Icon'
|
||||||
import { useMeta } from '../../hooks/use-meta'
|
import { useMeta } from '../../hooks/use-meta'
|
||||||
|
import { useResume } from '../../hooks/use-resume'
|
||||||
import styles from './Networks.module.scss'
|
import styles from './Networks.module.scss'
|
||||||
|
|
||||||
|
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 }) {
|
export default function Networks({ small, hide }) {
|
||||||
const { social } = useMeta()
|
const { social } = useMeta()
|
||||||
if (hide) return null
|
if (hide) return null
|
||||||
@ -17,17 +34,15 @@ export default function Networks({ small, hide }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Animation className={small ? styles.small : styles.networks}>
|
<Animation className={small ? styles.small : styles.networks}>
|
||||||
{Object.keys(social).map((key, i) => (
|
<NetworkLink name={'Email'} url={`mailto:${basics.email}`} />
|
||||||
<a
|
|
||||||
className={linkClasses(key)}
|
{profiles.map(profile => (
|
||||||
href={social[key]}
|
<NetworkLink
|
||||||
key={i}
|
key={profile.network}
|
||||||
data-testid={`network-${key.toLowerCase()}`}
|
name={profile.network}
|
||||||
>
|
url={profile.url}
|
||||||
<Icon name={key} />
|
/>
|
||||||
<span className={styles.title}>{key}</span>
|
))}
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</Animation>
|
</Animation>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -21,11 +21,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
margin-left: $spacer / 2;
|
margin-left: $spacer / $line-height;
|
||||||
margin-right: $spacer / 2;
|
margin-right: $spacer / $line-height;
|
||||||
margin-bottom: $spacer / 2;
|
margin-bottom: $spacer / 2;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: block;
|
display: inline-block;
|
||||||
flex: 0 1;
|
flex: 0 1;
|
||||||
min-width: 2.5rem;
|
min-width: 2.5rem;
|
||||||
|
|
||||||
|
@ -8,16 +8,17 @@ import styles from './Header.module.scss'
|
|||||||
import { useMeta } from '../../hooks/use-meta'
|
import { useMeta } from '../../hooks/use-meta'
|
||||||
|
|
||||||
Header.propTypes = {
|
Header.propTypes = {
|
||||||
minimal: PropTypes.bool
|
minimal: PropTypes.bool,
|
||||||
|
isResume: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Header({ minimal }) {
|
export default function Header({ minimal, isResume }) {
|
||||||
const { availability } = useMeta()
|
const { availability } = useMeta()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={minimal ? styles.minimal : styles.header}>
|
<header className={minimal ? styles.minimal : styles.header}>
|
||||||
<ThemeSwitch />
|
<ThemeSwitch />
|
||||||
<LogoUnit minimal={minimal} />
|
<LogoUnit minimal={minimal} isResume={isResume} />
|
||||||
<Networks hide={minimal} />
|
<Networks hide={minimal} />
|
||||||
<Availability hide={minimal && !availability.status} />
|
<Availability hide={minimal && !availability.status} />
|
||||||
</header>
|
</header>
|
||||||
|
62
src/hooks/use-resume.js
Normal file
62
src/hooks/use-resume.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
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
|
||||||
|
country
|
||||||
|
countryCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
education {
|
||||||
|
institution
|
||||||
|
area
|
||||||
|
studyType
|
||||||
|
startDate
|
||||||
|
endDate
|
||||||
|
}
|
||||||
|
languages {
|
||||||
|
language
|
||||||
|
fluency
|
||||||
|
}
|
||||||
|
skills {
|
||||||
|
name
|
||||||
|
level
|
||||||
|
keywords
|
||||||
|
}
|
||||||
|
work {
|
||||||
|
company
|
||||||
|
position
|
||||||
|
website
|
||||||
|
startDate
|
||||||
|
endDate
|
||||||
|
summary
|
||||||
|
highlights
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const useResume = () => {
|
||||||
|
const { contentJson } = useStaticQuery(query)
|
||||||
|
return contentJson
|
||||||
|
}
|
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()
|
||||||
|
})
|
||||||
|
})
|
45
src/pages/resume/ResumeItem.jsx
Normal file
45
src/pages/resume/ResumeItem.jsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import styles from './ResumeItem.module.scss'
|
||||||
|
|
||||||
|
export default function ResumeItem({ workPlace, eduPlace }) {
|
||||||
|
const title = workPlace ? workPlace.company : eduPlace.institution
|
||||||
|
const subTitle = workPlace ? workPlace.position : eduPlace.area
|
||||||
|
const text = workPlace ? workPlace.summary : eduPlace.studyType
|
||||||
|
const { startDate, endDate } = workPlace || eduPlace
|
||||||
|
|
||||||
|
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>
|
||||||
|
<p>
|
||||||
|
<em>{text}</em>
|
||||||
|
</p>
|
||||||
|
</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
|
||||||
|
})
|
||||||
|
}
|
52
src/pages/resume/ResumeItem.module.scss
Normal file
52
src/pages/resume/ResumeItem.module.scss
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
@import 'variables';
|
||||||
|
|
||||||
|
.resumeItem {
|
||||||
|
padding-bottom: $spacer * 3;
|
||||||
|
padding-left: $spacer;
|
||||||
|
position: relative;
|
||||||
|
border-left: 1px solid $brand-grey-light;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $brand-grey-light;
|
||||||
|
position: absolute;
|
||||||
|
left: -0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
|
||||||
|
@media (min-width: $screen-md) {
|
||||||
|
text-align: right;
|
||||||
|
position: absolute;
|
||||||
|
top: -0.3rem;
|
||||||
|
right: 105%;
|
||||||
|
}
|
||||||
|
}
|
70
src/pages/resume/index.jsx
Normal file
70
src/pages/resume/index.jsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import shortid from 'shortid'
|
||||||
|
import SEO from '../../components/atoms/SEO'
|
||||||
|
import LinkIcon from '../../components/atoms/LinkIcon'
|
||||||
|
import { useResume } from '../../hooks/use-resume'
|
||||||
|
import styles from './index.module.scss'
|
||||||
|
import ResumeItem from './ResumeItem'
|
||||||
|
|
||||||
|
export default function Resume() {
|
||||||
|
const { basics, education, languages, work } = useResume()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SEO />
|
||||||
|
|
||||||
|
<div className={styles.resume}>
|
||||||
|
<header>
|
||||||
|
<p>Résumé</p>
|
||||||
|
<h1 className={styles.title}>{basics.name}</h1>
|
||||||
|
<h2 className={styles.label}>{basics.label}</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ul className={styles.contact}>
|
||||||
|
<li>
|
||||||
|
<a href={basics.website}>
|
||||||
|
<LinkIcon type="website" />
|
||||||
|
{basics.website.replace('https://', '')}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<LinkIcon type="Email" />
|
||||||
|
{basics.email}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<LinkIcon type="Info" />
|
||||||
|
{basics.location.city}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<LinkIcon type="Info" />
|
||||||
|
{languages.map(item => (
|
||||||
|
<span
|
||||||
|
key={item.language}
|
||||||
|
>{`${item.language} (${item.fluency})`}</span>
|
||||||
|
))}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className={styles.subTitle}>Work</h3>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{work.map(workPlace => (
|
||||||
|
<ResumeItem key={shortid.generate()} workPlace={workPlace} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className={styles.subTitle}>Education</h3>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{education.map(eduPlace => (
|
||||||
|
<ResumeItem key={shortid.generate()} eduPlace={eduPlace} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
66
src/pages/resume/index.module.scss
Normal file
66
src/pages/resume/index.module.scss
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: $font-size-h2;
|
||||||
|
margin-bottom: $spacer / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: $brand-grey-light;
|
||||||
|
width: $font-size-small;
|
||||||
|
height: $font-size-small;
|
||||||
|
opacity: 0.5;
|
||||||
|
margin-right: $spacer / 4;
|
||||||
|
|
||||||
|
:global(.dark) & {
|
||||||
|
fill: $brand-grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $screen-md) {
|
||||||
|
margin-top: $spacer * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subTitle {
|
||||||
|
font-size: $font-size-h3;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.resume {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html) {
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
}
|
@ -40,10 +40,10 @@ $font-size-mini: 0.7rem;
|
|||||||
|
|
||||||
$font-size-h1: 2.5rem;
|
$font-size-h1: 2.5rem;
|
||||||
$font-size-h2: 2rem;
|
$font-size-h2: 2rem;
|
||||||
$font-size-h3: 1.75rem;
|
$font-size-h3: 1.65rem;
|
||||||
$font-size-h4: $font-size-large;
|
$font-size-h4: 1.45rem;
|
||||||
$font-size-h5: $font-size-base;
|
$font-size-h5: $font-size-large;
|
||||||
$font-size-h6: $font-size-small;
|
$font-size-h6: $font-size-base;
|
||||||
|
|
||||||
$line-height: 1.5;
|
$line-height: 1.5;
|
||||||
$line-height-small: 1.1428571429;
|
$line-height-small: 1.1428571429;
|
||||||
|
@ -2,3 +2,4 @@
|
|||||||
# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449
|
# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449
|
||||||
|
|
||||||
User-agent: *
|
User-agent: *
|
||||||
|
Disallow: /resume
|
||||||
|
Loading…
Reference in New Issue
Block a user