From f44e0557ed3ac2c7307f7ed2cd8d70e25e32d6b2 Mon Sep 17 00:00:00 2001 From: Matthias Kretschmann Date: Sun, 11 Aug 2019 21:47:22 +0200 Subject: [PATCH 01/11] add resume --- README.md | 11 + content/meta.yml | 12 +- content/resume.json | 237 ++++++++++++++++++ gatsby-config.js | 40 ++- jest/__fixtures__/meta.json | 26 -- src/components/Layout.jsx | 3 +- src/components/atoms/SEO.jsx | 11 +- src/components/atoms/Vcard.jsx | 28 ++- src/components/molecules/LogoUnit.jsx | 3 +- src/components/molecules/Networks.jsx | 37 ++- src/components/molecules/Networks.module.scss | 6 +- src/components/organisms/Header.jsx | 7 +- src/hooks/use-resume.js | 62 +++++ src/pages/__tests__/resume.test.jsx | 10 + src/pages/resume/ResumeItem.jsx | 45 ++++ src/pages/resume/ResumeItem.module.scss | 52 ++++ src/pages/resume/index.jsx | 70 ++++++ src/pages/resume/index.module.scss | 66 +++++ src/styles/_variables.scss | 8 +- static/robots.txt | 1 + 20 files changed, 640 insertions(+), 95 deletions(-) create mode 100644 content/resume.json create mode 100644 src/hooks/use-resume.js create mode 100644 src/pages/__tests__/resume.test.jsx create mode 100644 src/pages/resume/ResumeItem.jsx create mode 100644 src/pages/resume/ResumeItem.module.scss create mode 100644 src/pages/resume/index.jsx create mode 100644 src/pages/resume/index.module.scss diff --git a/README.md b/README.md index 8b90770..7d45f34 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ - [πŸ’ One data file to rule all pages](#-one-data-file-to-rule-all-pages) - [🐱 GitHub repositories](#-github-repositories) - [πŸ’… Theme switcher](#-theme-switcher) + - [πŸ—‚ Resume](#-resume) - [πŸ† SEO component](#-seo-component) - [πŸ“‡ Client-side vCard creation](#-client-side-vcard-creation) - [πŸ’« 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/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 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. diff --git a/content/meta.yml b/content/meta.yml index 6dbe389..9b4e2b8 100644 --- a/content/meta.yml +++ b/content/meta.yml @@ -1,19 +1,11 @@ +# more personal metadata can be found in ./resume.json + - 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 - 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: status: false available: 'πŸ‘” Available for new projects. Let’s talk!' diff --git a/content/resume.json b/content/resume.json new file mode 100644 index 0000000..7e4e6a6 --- /dev/null +++ b/content/resume.json @@ -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" + } + ] +} diff --git a/gatsby-config.js b/gatsby-config.js index 9af4263..c2d295e 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -11,38 +11,20 @@ module.exports = { siteUrl: `${url}` }, 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 +34,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: { diff --git a/jest/__fixtures__/meta.json b/jest/__fixtures__/meta.json index b330035..374c42d 100644 --- a/jest/__fixtures__/meta.json +++ b/jest/__fixtures__/meta.json @@ -4,32 +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": { - "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": { "status": false, "available": "πŸ‘” Available for new projects. Let’s talk!", diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx index 6cf4441..dc434c1 100644 --- a/src/components/Layout.jsx +++ b/src/components/Layout.jsx @@ -28,6 +28,7 @@ export default function Layout({ children, location }) { const isHomepage = location.pathname === '/' || location.pathname === '/offline-plugin-app-shell-fallback/' + const isResume = location.pathname === '/resume' return ( <> @@ -40,7 +41,7 @@ export default function Layout({ children, location }) { delay={timeout} delayChildren={timeout} > -
+
{children}
diff --git a/src/components/atoms/SEO.jsx b/src/components/atoms/SEO.jsx index 8512b8e..725e075 100644 --- a/src/components/atoms/SEO.jsx +++ b/src/components/atoms/SEO.jsx @@ -1,7 +1,14 @@ import React from 'react' import Helmet from 'react-helmet' 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 }) => ( ( {/* Twitter Card tags */} - + diff --git a/src/components/atoms/Vcard.jsx b/src/components/atoms/Vcard.jsx index e8f1472..b462868 100644 --- a/src/components/atoms/Vcard.jsx +++ b/src/components/atoms/Vcard.jsx @@ -2,20 +2,34 @@ 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 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 => { e.preventDefault() - init(metaYaml) + init(meta) } return ( Add to addressbook @@ -24,10 +38,8 @@ 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 dataUrl = await toDataURL(meta.photoSrc, 'image/jpeg') const vcard = await constructVcard(dataUrl, meta) downloadVcard(vcard, meta) @@ -49,12 +61,12 @@ export const constructVcard = async (dataUrl, meta) => { // 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('nickname', 'kremalicious') 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' }) diff --git a/src/components/molecules/LogoUnit.jsx b/src/components/molecules/LogoUnit.jsx index 2aa52c4..74efc66 100644 --- a/src/components/molecules/LogoUnit.jsx +++ b/src/components/molecules/LogoUnit.jsx @@ -8,7 +8,8 @@ import { ReactComponent as Logo } from '../../images/logo.svg' import styles from './LogoUnit.module.scss' LogoUnit.propTypes = { - minimal: PropTypes.bool + minimal: PropTypes.bool, + isResume: PropTypes.bool } export default function LogoUnit({ minimal }) { diff --git a/src/components/molecules/Networks.jsx b/src/components/molecules/Networks.jsx index 96bcd44..c6430e8 100644 --- a/src/components/molecules/Networks.jsx +++ b/src/components/molecules/Networks.jsx @@ -4,8 +4,25 @@ 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 NetworkLink = ({ name, url }) => ( + + + {name} + +) + +NetworkLink.propTypes = { + name: PropTypes.string.isRequired, + url: PropTypes.string.isRequired +} + export default function Networks({ small, hide }) { const { social } = useMeta() if (hide) return null @@ -17,17 +34,15 @@ export default function Networks({ small, hide }) { return ( - {Object.keys(social).map((key, i) => ( - - - {key} - - ))} + + + {profiles.map(profile => ( + + ))} ) } diff --git a/src/components/molecules/Networks.module.scss b/src/components/molecules/Networks.module.scss index c66ea40..1a679be 100644 --- a/src/components/molecules/Networks.module.scss +++ b/src/components/molecules/Networks.module.scss @@ -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; diff --git a/src/components/organisms/Header.jsx b/src/components/organisms/Header.jsx index b13f24d..95e330d 100644 --- a/src/components/organisms/Header.jsx +++ b/src/components/organisms/Header.jsx @@ -8,16 +8,17 @@ import styles from './Header.module.scss' import { useMeta } from '../../hooks/use-meta' 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() return (
- +
diff --git a/src/hooks/use-resume.js b/src/hooks/use-resume.js new file mode 100644 index 0000000..ebbee65 --- /dev/null +++ b/src/hooks/use-resume.js @@ -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 +} diff --git a/src/pages/__tests__/resume.test.jsx b/src/pages/__tests__/resume.test.jsx new file mode 100644 index 0000000..85c0b88 --- /dev/null +++ b/src/pages/__tests__/resume.test.jsx @@ -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() + expect(container.firstChild).toBeInTheDocument() + }) +}) diff --git a/src/pages/resume/ResumeItem.jsx b/src/pages/resume/ResumeItem.jsx new file mode 100644 index 0000000..0b91189 --- /dev/null +++ b/src/pages/resume/ResumeItem.jsx @@ -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 ( +
+ + {dateStart} + {dateEnd ? !isSameYear && `–${dateEnd}` : '–present'}{' '} + +

{title}

+
{subTitle}
+

+ {text} +

+
+ ) +} + +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 + }) +} diff --git a/src/pages/resume/ResumeItem.module.scss b/src/pages/resume/ResumeItem.module.scss new file mode 100644 index 0000000..b4e6dc1 --- /dev/null +++ b/src/pages/resume/ResumeItem.module.scss @@ -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%; + } +} diff --git a/src/pages/resume/index.jsx b/src/pages/resume/index.jsx new file mode 100644 index 0000000..eec1b5a --- /dev/null +++ b/src/pages/resume/index.jsx @@ -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 ( + <> + + +
+
+

RΓ©sumΓ©

+

{basics.name}

+

{basics.label}

+
+ +
+ +
+ +
+

Work

+
+
+ {work.map(workPlace => ( + + ))} +
+ +
+

Education

+
+
+ {education.map(eduPlace => ( + + ))} +
+
+ + ) +} diff --git a/src/pages/resume/index.module.scss b/src/pages/resume/index.module.scss new file mode 100644 index 0000000..cffc176 --- /dev/null +++ b/src/pages/resume/index.module.scss @@ -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; + } +} diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 24588c5..0d58290 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -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; diff --git a/static/robots.txt b/static/robots.txt index 214e411..2307ece 100644 --- a/static/robots.txt +++ b/static/robots.txt @@ -2,3 +2,4 @@ # www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449 User-agent: * +Disallow: /resume From 9d5e33b146abdf4568610cfd06fa94b69fb696f1 Mon Sep 17 00:00:00 2001 From: Matthias Kretschmann Date: Fri, 8 Nov 2019 23:00:47 +0100 Subject: [PATCH 02/11] resume tweaks, markdown summaries --- README.md | 8 +- content/resume.json | 28 +-- gatsby-node.js | 4 +- jest/__fixtures__/resume.json | 276 ++++++++++++++++++++++++ package.json | 2 + src/components/atoms/SEO.jsx | 51 ++--- src/components/atoms/Vcard.jsx | 13 +- src/components/molecules/Networks.jsx | 23 +- src/components/organisms/Header.jsx | 10 +- src/hooks/use-meta.js | 16 -- src/hooks/use-resume.js | 7 +- src/hooks/use-resume.test.js | 18 ++ src/pages/resume/ResumeItem.jsx | 57 ++++- src/pages/resume/ResumeItem.module.scss | 13 +- src/pages/resume/index.jsx | 24 ++- src/pages/resume/index.module.scss | 3 +- 16 files changed, 450 insertions(+), 103 deletions(-) create mode 100644 jest/__fixtures__/resume.json create mode 100644 src/hooks/use-resume.test.js diff --git a/README.md b/README.md index 7d45f34..56b9942 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ - [πŸ’ One data file to rule all pages](#-one-data-file-to-rule-all-pages) - [🐱 GitHub repositories](#-github-repositories) - [πŸ’… Theme switcher](#-theme-switcher) - - [πŸ—‚ Resume](#-resume) + - [πŸ—‚ JSON Resume](#-json-resume) - [πŸ† SEO component](#-seo-component) - [πŸ“‡ Client-side vCard creation](#-client-side-vcard-creation) - [πŸ’« Page transitions](#-page-transitions) @@ -77,14 +77,14 @@ If you want to know how, have a look at the respective components: - [`src/components/molecules/ThemeSwitch.jsx`](src/components/molecules/ThemeSwitch.jsx) - [`src/hooks/use-dark-mode.jsx`](src/hooks/use-dark-mode.jsx) -### πŸ—‚ Resume +### πŸ—‚ JSON Resume -Resume page based on [JSON Resume](https://jsonresume.org) standard. +Resume page based on [JSON Resume](https://jsonresume.org) standard. Most metadata and social profiles are defined in [`content/resume.json`](content/resume.json) and used throughout the site. 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/pages/resume.jsx`](src/pages/resume.jsx) - [`src/hooks/use-resume.js`](src/hooks/use-resume.js) ### πŸ† SEO component diff --git a/content/resume.json b/content/resume.json index 7e4e6a6..5ce0dd4 100644 --- a/content/resume.json +++ b/content/resume.json @@ -34,7 +34,6 @@ ], "location": { "city": "Berlin", - "country": "Germany", "countryCode": "DE" } }, @@ -44,7 +43,7 @@ "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.", + "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"] }, { @@ -53,7 +52,7 @@ "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.", + "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"] }, { @@ -71,7 +70,7 @@ "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.", + "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"] }, { @@ -80,7 +79,7 @@ "website": "https://sharethemeal.org", "startDate": "2014-10-01", "endDate": "2015-06-01", - "summary": "...", + "summary": "[app and website](/sharethemeal)", "highlights": ["Started the company"] }, { @@ -89,21 +88,21 @@ "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." + "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 for thousands of students and staff. Additionally, conceptualizing, creating and maintaining the blog network & community for all students & staff." + "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 with a basic HTML/CSS template for students of Media Informatics at the Harz University of Applied Sciences to learn and use." + "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", @@ -144,7 +143,7 @@ { "institution": "Martin Luther University Halle-Wittenberg", "area": "Media/Communication Science & Art History", - "studyType": "Bachelor", + "studyType": "Bachelor of Arts", "startDate": "2008-01-01", "endDate": "2012-01-01" }, @@ -160,7 +159,8 @@ { "title": "German Design Award", "date": "2015-11-01", - "awarder": "ezeep GmbH" + "awarder": "ezeep GmbH", + "summary": "Nominated in the category _Interactive User Experience (Excellent Communications Design)_" }, { "title": "CeBIT Preview Award", @@ -225,13 +225,13 @@ } ], "languages": [ - { - "language": "German", - "fluency": "Native speaker" - }, { "language": "English", "fluency": "Advanced speaker" + }, + { + "language": "German", + "fluency": "Native speaker" } ] } diff --git a/gatsby-node.js b/gatsby-node.js index 0a4ff71..0152446 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -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') diff --git a/jest/__fixtures__/resume.json b/jest/__fixtures__/resume.json new file mode 100644 index 0000000..12b5acd --- /dev/null +++ b/jest/__fixtures__/resume.json @@ -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 + } + ] + } +} diff --git a/package.json b/package.json index a18aa02..a0d0205 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/components/atoms/SEO.jsx b/src/components/atoms/SEO.jsx index 725e075..faff192 100644 --- a/src/components/atoms/SEO.jsx +++ b/src/components/atoms/SEO.jsx @@ -1,7 +1,7 @@ import React from 'react' import Helmet from 'react-helmet' 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 }) => { @@ -10,33 +10,34 @@ const MetaTags = ({ title, description, url, image, meta }) => { ({ network }) => network === 'Twitter' )[0].username -const MetaTags = ({ title, description, url, image, meta }) => ( - - + return ( + + - {/* General tags */} - - - + {/* General tags */} + + + - {/* OpenGraph tags */} - - - - + {/* OpenGraph tags */} + + + + - {/* Twitter Card tags */} - - - - - - -) + {/* Twitter Card tags */} + + + + + + + ) +} MetaTags.propTypes = { title: PropTypes.string, diff --git a/src/components/atoms/Vcard.jsx b/src/components/atoms/Vcard.jsx index b462868..d1b3bcc 100644 --- a/src/components/atoms/Vcard.jsx +++ b/src/components/atoms/Vcard.jsx @@ -7,7 +7,6 @@ import { useResume } from '../../hooks/use-resume' export default function Vcard() { const metaYaml = useMeta() const { basics } = useResume() - const data = useStaticQuery(query) const photoSrc = basics.picture.childImageSharp.fixed.src const { name, label, email, profiles } = basics @@ -56,6 +55,12 @@ export const downloadVcard = (vcard, meta) => { export const constructVcard = async (dataUrl, 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 @@ -66,9 +71,9 @@ export const constructVcard = async (dataUrl, meta) => { contact.set('email', meta.email) contact.set('nickname', 'kremalicious') contact.set('url', meta.url, { type: 'Portfolio' }) - contact.add('url', meta.social.Blog, { type: 'Blog' }) - contact.add('x-socialprofile', meta.social.Twitter, { type: 'twitter' }) - contact.add('x-socialprofile', meta.social.GitHub, { type: 'GitHub' }) + 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') diff --git a/src/components/molecules/Networks.jsx b/src/components/molecules/Networks.jsx index c6430e8..917ce3f 100644 --- a/src/components/molecules/Networks.jsx +++ b/src/components/molecules/Networks.jsx @@ -3,10 +3,12 @@ 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 }) => ( - key === 'Email' ? `u-email ${styles.link}` : `u-url ${styles.link}` - return ( - {profiles.map(profile => ( - - ))} + {basics.profiles.map(profile => ( + + ))} ) } diff --git a/src/components/organisms/Header.jsx b/src/components/organisms/Header.jsx index 95e330d..021ed69 100644 --- a/src/components/organisms/Header.jsx +++ b/src/components/organisms/Header.jsx @@ -18,9 +18,13 @@ export default function Header({ minimal, isResume }) { return (
- - - + {!isResume && ( + <> + + + + + )}
) } diff --git a/src/hooks/use-meta.js b/src/hooks/use-meta.js index b917d18..5335eef 100644 --- a/src/hooks/use-meta.js +++ b/src/hooks/use-meta.js @@ -7,7 +7,6 @@ const query = graphql` tagline description url - email img { childImageSharp { resize(width: 980) { @@ -15,21 +14,6 @@ const query = graphql` } } } - avatar { - childImageSharp { - resize { - src - } - } - } - social { - Mail - Blog - Twitter - GitHub - Dribbble - Keybase - } availability { status available diff --git a/src/hooks/use-resume.js b/src/hooks/use-resume.js index ebbee65..d422316 100644 --- a/src/hooks/use-resume.js +++ b/src/hooks/use-resume.js @@ -23,7 +23,6 @@ const query = graphql` } location { city - country countryCode } } @@ -52,6 +51,12 @@ const query = graphql` summary highlights } + awards { + title + date + awarder + summary + } } } ` diff --git a/src/hooks/use-resume.test.js b/src/hooks/use-resume.test.js new file mode 100644 index 0000000..ef4ba21 --- /dev/null +++ b/src/hooks/use-resume.test.js @@ -0,0 +1,18 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { useStaticQuery } from 'gatsby' +import { useResume } from './use-resume' +import data from '../../jest/__fixtures__/resume.json' + +beforeEach(() => { + useStaticQuery.mockImplementationOnce(() => ({ ...data })) +}) + +describe('useResume', () => { + it('renders correctly', () => { + const { basics } = useResume() + const { container } = render(
{basics.name}
) + + expect(container.textContent).toBe(data.contentJson.basics.name) + }) +}) diff --git a/src/pages/resume/ResumeItem.jsx b/src/pages/resume/ResumeItem.jsx index 0b91189..b18f209 100644 --- a/src/pages/resume/ResumeItem.jsx +++ b/src/pages/resume/ResumeItem.jsx @@ -1,14 +1,43 @@ 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' -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 +export const markdownOutput = text => + remark() + .use(parse, { gfm: true, commonmark: true, pedantic: true }) + .use(html) + .use(breaks) + .use(remark2react) + .processSync(text).contents - const dateStart = new Date(startDate).getFullYear() +export default function ResumeItem({ workPlace, eduPlace, award }) { + const title = workPlace + ? workPlace.company + : award + ? award.title + : eduPlace.institution + const subTitle = workPlace + ? workPlace.position + : award + ? award.awarder + : eduPlace.area + const text = workPlace + ? workPlace.summary + : award && award.summary + ? award.summary + : eduPlace + ? eduPlace.studyType + : null + const { startDate, endDate, date } = workPlace || eduPlace || award + + const dateStart = date + ? new Date(date).getFullYear() + : new Date(startDate).getFullYear() const dateEnd = endDate && new Date(endDate).getFullYear() const isSameYear = dateStart === dateEnd @@ -16,13 +45,15 @@ export default function ResumeItem({ workPlace, eduPlace }) {
{dateStart} - {dateEnd ? !isSameYear && `–${dateEnd}` : '–present'}{' '} + {dateEnd + ? !isSameYear && `–${dateEnd}` + : !date + ? '–present' + : null}{' '}

{title}

{subTitle}
-

- {text} -

+ {text && markdownOutput(text)}
) } @@ -41,5 +72,11 @@ ResumeItem.propTypes = { 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 }) } diff --git a/src/pages/resume/ResumeItem.module.scss b/src/pages/resume/ResumeItem.module.scss index b4e6dc1..1ce0ee5 100644 --- a/src/pages/resume/ResumeItem.module.scss +++ b/src/pages/resume/ResumeItem.module.scss @@ -4,22 +4,27 @@ padding-bottom: $spacer * 3; padding-left: $spacer; position: relative; - border-left: 1px solid $brand-grey-light; + border-left: 1px solid rgba($brand-grey-light, 0.25); &::before { content: ''; display: block; - width: 1rem; - height: 1rem; + width: $font-size-small; + height: $font-size-small; border-radius: 50%; background: $brand-grey-light; position: absolute; - left: -0.5rem; + left: -($font-size-small / 2); + top: 0.1rem; } p:last-child { margin-bottom: 0; } + + &:last-child { + border: none; + } } .title { diff --git a/src/pages/resume/index.jsx b/src/pages/resume/index.jsx index eec1b5a..034633b 100644 --- a/src/pages/resume/index.jsx +++ b/src/pages/resume/index.jsx @@ -7,7 +7,8 @@ import styles from './index.module.scss' import ResumeItem from './ResumeItem' export default function Resume() { - const { basics, education, languages, work } = useResume() + const { basics, education, languages, work, awards } = useResume() + const { name, label, email, website, location } = basics return ( <> @@ -16,25 +17,25 @@ export default function Resume() {

RΓ©sumΓ©

-

{basics.name}

-

{basics.label}

+

{name}

+

{label}

+
+

Awards

+
+
+ {awards.map(award => ( + + ))} +
+

Education

diff --git a/src/pages/resume/index.module.scss b/src/pages/resume/index.module.scss index cffc176..a38214f 100644 --- a/src/pages/resume/index.module.scss +++ b/src/pages/resume/index.module.scss @@ -46,13 +46,14 @@ } @media (min-width: $screen-md) { - margin-top: $spacer * 2; + margin-top: $spacer * 2.25; } } .subTitle { font-size: $font-size-h3; margin-bottom: 0; + margin-top: -($spacer / 3); } @media print { From 1dde88a511e3e824a3723b46dd1f9d0385539267 Mon Sep 17 00:00:00 2001 From: Matthias Kretschmann Date: Wed, 13 Nov 2019 14:08:57 +0100 Subject: [PATCH 03/11] test tweaks --- jest/__fixtures__/meta.json | 7 ++++++ jest/setup-test-env.js | 19 ++++++++++++++ src/components/atoms/Vcard.test.jsx | 7 ------ .../molecules/Availability.test.jsx | 2 -- src/components/molecules/LogoUnit.test.jsx | 9 ------- src/components/molecules/Networks.jsx | 2 +- src/components/molecules/Networks.test.jsx | 21 +--------------- src/components/organisms/Footer.test.jsx | 11 -------- src/components/organisms/Header.test.jsx | 8 ------ src/hooks/use-resume.test.js | 18 ------------- src/pages/__tests__/index.test.jsx | 9 ------- src/pages/resume/ResumeItem.module.scss | 4 +-- src/pages/resume/index.jsx | 25 +++++++++++++------ src/pages/resume/index.module.scss | 19 ++++++++++---- 14 files changed, 61 insertions(+), 100 deletions(-) delete mode 100644 src/hooks/use-resume.test.js diff --git a/jest/__fixtures__/meta.json b/jest/__fixtures__/meta.json index 374c42d..a760765 100644 --- a/jest/__fixtures__/meta.json +++ b/jest/__fixtures__/meta.json @@ -4,6 +4,13 @@ "tagline": "Designer & Developer", "description": "Portfolio of web & ui designer/developer hybrid Matthias Kretschmann.", "url": "https://matthiaskretschmann.com", + "img": { + "childImageSharp": { + "resize": { + "src": "/static/5ecbb5694b0b2152aa71398164af38b2/da1a7/twitter-card.png" + } + } + }, "availability": { "status": false, "available": "πŸ‘” Available for new projects. Let’s talk!", diff --git a/jest/setup-test-env.js b/jest/setup-test-env.js index ecbbfda..184a34c 100644 --- a/jest/setup-test-env.js +++ b/jest/setup-test-env.js @@ -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 } + }) +}) diff --git a/src/components/atoms/Vcard.test.jsx b/src/components/atoms/Vcard.test.jsx index e9c8e23..899b42b 100644 --- a/src/components/atoms/Vcard.test.jsx +++ b/src/components/atoms/Vcard.test.jsx @@ -1,17 +1,10 @@ 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' describe('Vcard', () => { beforeEach(() => { - useStaticQuery.mockImplementationOnce(() => { - return { - ...data - } - }) - global.URL.createObjectURL = jest.fn() }) diff --git a/src/components/molecules/Availability.test.jsx b/src/components/molecules/Availability.test.jsx index 8817fdf..2536602 100644 --- a/src/components/molecules/Availability.test.jsx +++ b/src/components/molecules/Availability.test.jsx @@ -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() expect(container.firstChild).toBeInTheDocument() }) diff --git a/src/components/molecules/LogoUnit.test.jsx b/src/components/molecules/LogoUnit.test.jsx index 379aa58..1058e47 100644 --- a/src/components/molecules/LogoUnit.test.jsx +++ b/src/components/molecules/LogoUnit.test.jsx @@ -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 diff --git a/src/components/molecules/Networks.jsx b/src/components/molecules/Networks.jsx index 917ce3f..a6f08c0 100644 --- a/src/components/molecules/Networks.jsx +++ b/src/components/molecules/Networks.jsx @@ -33,7 +33,7 @@ export default function Networks({ small, hide }) { return ( - + {basics.profiles.map(profile => ( { - useStaticQuery.mockImplementationOnce(() => { - return { - ...data - } - }) -}) describe('Networks', () => { it('renders correctly from data file values', () => { - const { social } = data.metaYaml - const { container, getByTestId } = render() - + const { container } = render() 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() - expect(container.firstChild).toBeInTheDocument() expect(container.querySelector('.small')).toBeInTheDocument() }) it('can be hidden', () => { const { container } = render() - expect(container.firstChild).not.toBeInTheDocument() }) }) diff --git a/src/components/organisms/Footer.test.jsx b/src/components/organisms/Footer.test.jsx index f39c375..8a6213d 100644 --- a/src/components/organisms/Footer.test.jsx +++ b/src/components/organisms/Footer.test.jsx @@ -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(