diff --git a/README.md b/README.md index 56b9942..2ca88a4 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ - [⛵️ Lighthouse score](#️-lighthouse-score) - [💍 One data file to rule all pages](#-one-data-file-to-rule-all-pages) - [🐱 GitHub repositories](#-github-repositories) + - 🗂 JSON Resume](#-json-resume) - [💅 Theme switcher](#-theme-switcher) - - [🗂 JSON Resume](#-json-resume) - [🏆 SEO component](#-seo-component) - [📇 Client-side vCard creation](#-client-side-vcard-creation) - [💫 Page transitions](#-page-transitions) @@ -66,6 +66,16 @@ If you want to know how, have a look at the respective components: - [`content/repos.yml`](content/repos.yml) - [`src/components/molecules/Repository.jsx`](src/components/molecules/Repository.jsx) +### 🗂 JSON Resume + +Resume page based on [JSON Resume](https://jsonresume.org) standard. Most site 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: + +- [`content/resume.json`](content/resume.json) +- [`src/pages/resume/index.jsx`](src/pages/resume/index.jsx) +- [`src/hooks/use-resume.js`](src/hooks/use-resume.js) + ### 💅 Theme switcher Includes a theme switcher which allows user to toggle between a light and a dark theme. Switching between them also happens automatically based on user's local sunset and sunrise times. Uses Cloudflare's geo location HTTP header functionality. @@ -77,16 +87,6 @@ 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) -### 🗂 JSON Resume - -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: - -- [`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 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/package-lock.json b/package-lock.json index ec767cc..29266d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3042,6 +3042,14 @@ } } }, + "@mapbox/hast-util-table-cell-style": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.1.3.tgz", + "integrity": "sha512-QsEsh5YaDvHoMQ2YHdvZy2iDnU3GgKVBTcHf6cILyoWDZtPSdlG444pL/ioPYO/GpXSfODBb9sefEetfC4v9oA==", + "requires": { + "unist-util-visit": "^1.3.0" + } + }, "@mikaelkristiansson/domready": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@mikaelkristiansson/domready/-/domready-1.0.9.tgz", @@ -12455,6 +12463,19 @@ "minimalistic-assert": "^1.0.1" } }, + "hast-to-hyperscript": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-7.0.4.tgz", + "integrity": "sha512-vmwriQ2H0RPS9ho4Kkbf3n3lY436QKLq6VaGA1pzBh36hBi3tm1DO9bR+kaJIbpT10UqaANDkMjxvjVfr+cnOA==", + "requires": { + "comma-separated-tokens": "^1.0.0", + "property-information": "^5.3.0", + "space-separated-tokens": "^1.0.0", + "style-to-object": "^0.2.1", + "unist-util-is": "^3.0.0", + "web-namespaces": "^1.1.2" + } + }, "hast-util-is-element": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-1.0.3.tgz", @@ -13158,6 +13179,11 @@ "prop-types": "^15.5.10" } }, + "inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, "inquirer": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.0.tgz", @@ -20679,6 +20705,14 @@ "react-is": "^16.8.1" } }, + "property-information": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.3.0.tgz", + "integrity": "sha512-IslotQn1hBCZDY7SaJ3zmCjVea219VTwmOk6Pu3z9haU9m4+T8GwaDubur+6NMHEU+Fjs/6/p66z6QULPkcL1w==", + "requires": { + "xtend": "^4.0.1" + } + }, "proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -21531,6 +21565,11 @@ } } }, + "remark-breaks": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-1.0.3.tgz", + "integrity": "sha512-ip5hvJE8vsUJCGfgHaEJbf/JfO6KTZV+NBG68AWkEMhrjHW3Qh7EorED41mCt0FFSTrUDeRiNHovKO7cqgPZmw==" + }, "remark-html": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/remark-html/-/remark-html-10.0.0.tgz", @@ -21564,6 +21603,17 @@ "xtend": "^4.0.1" } }, + "remark-react": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/remark-react/-/remark-react-6.0.0.tgz", + "integrity": "sha512-5g73p8ZuqKoSdKByEf6IbXtVaHnbSEV0aamhIIqpzeNvj1wWDPX0USSPs4Gf3ZAsQIehIp6QiqJIbbXpq74bug==", + "requires": { + "@mapbox/hast-util-table-cell-style": "^0.1.3", + "hast-to-hyperscript": "^7.0.0", + "hast-util-sanitize": "^2.0.0", + "mdast-util-to-hast": "^6.0.0" + } + }, "remark-stringify": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-7.0.4.tgz", @@ -23631,6 +23681,14 @@ "integrity": "sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=", "dev": true }, + "style-to-object": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.2.3.tgz", + "integrity": "sha512-1d/k4EY2N7jVLOqf2j04dTc37TPOv/hHxZmvpg8Pdh8UYydxeu/C1W1U4vD8alzf5V2Gt7rLsmkr4dxAlDm9ng==", + "requires": { + "inline-style-parser": "0.1.1" + } + }, "style-value-types": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-3.1.7.tgz", @@ -25715,6 +25773,11 @@ "defaults": "^1.0.3" } }, + "web-namespaces": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.3.tgz", + "integrity": "sha512-r8sAtNmgR0WKOKOxzuSgk09JsHlpKlB+uHi937qypOu3PZ17UxPrierFKDye/uNHjNTTEshu5PId8rojIPj/tA==" + }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/src/components/atoms/Icon.jsx b/src/components/atoms/Icon.jsx index 183fb7e..7580bdf 100644 --- a/src/components/atoms/Icon.jsx +++ b/src/components/atoms/Icon.jsx @@ -15,7 +15,12 @@ import { FileText, Key, Image, - Mail + Mail, + MapPin, + Globe, + Briefcase, + Award, + BookOpen } from 'react-feather' import { ReactComponent as Dribbble } from '../../images/dribbble.svg' import styles from './Icon.module.scss' @@ -41,7 +46,12 @@ const Icon = ({ name, ...props }) => { FileText, Key, Image, - Mail + Mail, + MapPin, + Globe, + Briefcase, + Award, + BookOpen } const IconMapped = components[name] diff --git a/src/components/atoms/Vcard.jsx b/src/components/atoms/Vcard.jsx index d1b3bcc..e5f80d9 100644 --- a/src/components/atoms/Vcard.jsx +++ b/src/components/atoms/Vcard.jsx @@ -39,21 +39,19 @@ export default function Vcard() { export const init = async meta => { // first, convert the avatar to base64, then construct all vCard elements const dataUrl = await toDataURL(meta.photoSrc, 'image/jpeg') - const vcard = await constructVcard(dataUrl, meta) + const vcard = await constructVcard(meta, dataUrl) - downloadVcard(vcard, meta) -} - -// Construct the download from a blob of the just constructed vCard, -// and save it to user's file system -export const downloadVcard = (vcard, meta) => { + // Construct the download from a blob of the just constructed vCard, const { addressbook } = meta const name = addressbook.split('/').join('') - const blob = new Blob([vcard], { type: 'text/x-vcard' }) + const blob = new Blob([vcard], { + type: 'text/x-vcard' + }) + // save it to user's file system saveAs(blob, name) } -export const constructVcard = async (dataUrl, meta) => { +export const constructVcard = async meta => { const contact = new vCard() const blog = meta.profiles.filter(({ network }) => network === 'Blog')[0].url const twitter = meta.profiles.filter( @@ -82,7 +80,7 @@ export const constructVcard = async (dataUrl, meta) => { // Helper function to create base64 string from avatar image // without the need to read image file from file system -export const toDataURL = async (photoSrc, outputFormat) => { +export async function toDataURL(photoSrc, outputFormat) { const img = new Image() img.crossOrigin = 'Anonymous' img.src = photoSrc diff --git a/src/components/atoms/Vcard.test.jsx b/src/components/atoms/Vcard.test.jsx index 899b42b..ee224b9 100644 --- a/src/components/atoms/Vcard.test.jsx +++ b/src/components/atoms/Vcard.test.jsx @@ -1,7 +1,16 @@ import React from 'react' import { render, fireEvent, waitForElement } from '@testing-library/react' import Vcard, { constructVcard, toDataURL, init } from './Vcard' -import data from '../../../jest/__fixtures__/meta.json' +import meta from '../../../jest/__fixtures__/meta.json' +import resume from '../../../jest/__fixtures__/resume.json' + +const metaMock = { + ...meta.metaYaml, + name: resume.contentJson.basics.name, + label: resume.contentJson.basics.label, + email: resume.contentJson.basics.email, + profiles: [...resume.contentJson.basics.profiles] +} describe('Vcard', () => { beforeEach(() => { @@ -21,15 +30,12 @@ describe('Vcard', () => { }) it('combined vCard download process finishes', async () => { - await init(data.metaYaml) + await init(metaMock) expect(global.URL.createObjectURL).toHaveBeenCalledTimes(1) }) it('vCard can be constructed', async () => { - const vcard = await constructVcard( - 'data:image/jpeg;base64,00', - data.metaYaml - ) + const vcard = await constructVcard(metaMock, 'data:image/jpeg;base64,00') expect(vcard).toBeDefined() }) diff --git a/src/hooks/use-resume.js b/src/hooks/use-resume.js index d422316..46b9ce5 100644 --- a/src/hooks/use-resume.js +++ b/src/hooks/use-resume.js @@ -49,7 +49,6 @@ const query = graphql` startDate endDate summary - highlights } awards { title