diff --git a/assets/assets.sketch b/assets/assets.sketch index 5106ded..42a7d23 100644 Binary files a/assets/assets.sketch and b/assets/assets.sketch differ diff --git a/gatsby-browser.js b/gatsby-browser.js index 10143bc..fffb184 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -4,20 +4,24 @@ * @see https://www.gatsbyjs.org/docs/browser-apis/ */ +require('./src/styles/app.scss') // main styling + +const { activeEnv, isProd } = require('./utils/environment') const printCorporateMessage = require('./gatsby/browser/corporateMessage') const setDefaultTime = require('./gatsby/browser/defaultTime') -require('./src/styles/app.scss') // main styling - // module.exports.wrapPageElement = require('./gatsby/wrapPageElement') module.exports.wrapRootElement = require('./gatsby/wrapRootElement') module.exports.onClientEntry = () => { setDefaultTime() + console.info(`Environment: %c${activeEnv}`, 'color: blue;') } module.exports.onInitialClientRender = () => { - printCorporateMessage() + if (isProd) { + printCorporateMessage() + } } module.exports.onServiceWorkerUpdateReady = () => { diff --git a/gatsby/node/onCreateNode.js b/gatsby/node/onCreateNode.js index 0778959..d478c93 100644 --- a/gatsby/node/onCreateNode.js +++ b/gatsby/node/onCreateNode.js @@ -15,7 +15,6 @@ module.exports = async ({ node, getNode, actions }) => { console.log() console.log(dim(`Creating ${source} node`)) console.log(bold(fileNode.relativePath)) - console.log(node.frontmatter) console.log(fileNode) } @@ -23,6 +22,7 @@ module.exports = async ({ node, getNode, actions }) => { return // skip this unpublished stuff only in production } + let parent = null // set parent if the source is a children of another source let slug = node.frontmatter.slug || undefined if (!slug) { slug = createFilePath({ node, getNode }) @@ -46,20 +46,42 @@ module.exports = async ({ node, getNode, actions }) => { } node.frontmatter = { ...defaults, ...node.frontmatter } break + case 'writings': defaults = { ...defaults, categories: [], tags: [], } + break + + case 'education': + parent = 'about' + defaults = { + ...defaults, + qualifications: [], + } node.frontmatter = { ...defaults, ...node.frontmatter } break + + case 'experience': + parent = 'about' + defaults = { + ...defaults, + responsibilities: [], + } + node.frontmatter = { ...defaults, ...node.frontmatter } + break + } + + if (isDev) { + console.log(node.frontmatter) } createNodeField({ node, name: 'slug', - value: `/${source}/${slug}`, + value: parent ? `/${parent}/${source}/${slug}` : `/${source}/${slug}`, }) createNodeField({ node, diff --git a/src/components/timeline/Item.jsx b/src/components/timeline/Item.jsx new file mode 100644 index 0000000..3f56d24 --- /dev/null +++ b/src/components/timeline/Item.jsx @@ -0,0 +1,18 @@ +import React from 'react' +import { Link as NativeLink } from 'gatsby' + +import Time from '../Time' + +const Item = ({ title, description, started, ended, link }) => ( +
+
+
+
+ {title} +
+
{description}
+
+) + +export default Item diff --git a/src/components/timeline/Section.jsx b/src/components/timeline/Section.jsx new file mode 100644 index 0000000..284cd4f --- /dev/null +++ b/src/components/timeline/Section.jsx @@ -0,0 +1,22 @@ +import React from 'react' + +import Item from './Item' +import Icon from '../Icon' + +const Section = ({ name, icon, data }) => { + return ( +
+
+ +

{name}

+
+
+ {data.map((props, index) => ( + + ))} +
+
+ ) +} + +export default Section diff --git a/src/components/timeline/Timeline.jsx b/src/components/timeline/Timeline.jsx new file mode 100644 index 0000000..7692257 --- /dev/null +++ b/src/components/timeline/Timeline.jsx @@ -0,0 +1,17 @@ +import React from 'react' + +import Section from './Section' + +const Timeline = ({ data }) => { + return ( +
+
+ {data.map((props, index) => ( +
+ ))} +
+
+ ) +} + +export default Timeline diff --git a/src/images/icons/book.svg b/src/images/icons/book.svg new file mode 100644 index 0000000..b317a65 --- /dev/null +++ b/src/images/icons/book.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/images/icons/briefcase.svg b/src/images/icons/briefcase.svg new file mode 100644 index 0000000..9590788 --- /dev/null +++ b/src/images/icons/briefcase.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pages/about/education.jsx b/src/pages/about/education.jsx new file mode 100644 index 0000000..9ec67cb --- /dev/null +++ b/src/pages/about/education.jsx @@ -0,0 +1,83 @@ +import React from 'react' +import { graphql } from 'gatsby' + +import { HistoryConsumer } from '../../provider/history' +import Head from '../../components/Head' +import Timeline from '../../components/timeline/Timeline' + +class Page extends React.Component { + state = { + pageName: 'Education', + } + + componentDidMount() { + const { breadcrumb } = this.props.pageContext + + this.props.history.update({ + location: breadcrumb.location, + crumbLabel: this.state.pageName, + crumbs: breadcrumb.crumbs, + }) + } + + render() { + const { + allMdx: { edges }, + } = this.props.data + + const timelineSection = edges.map(({ node: { frontmatter, fields, excerpt } }) => ({ + title: frontmatter.title, + description: excerpt, + started: frontmatter.started, + ended: frontmatter.ended, + link: fields.slug, + })) + const timelineData = [ + { + name: 'Education', + icon: 'book', + data: timelineSection, + }, + ] + + return ( + <> + + +

{this.state.pageName}

+ + + + ) + } +} + +export default React.forwardRef((props, ref) => ( + + {(history) => } + +)) + +export const query = graphql` + query EducationQuery { + allMdx( + filter: { fields: { source: { eq: "education" } } } + sort: { fields: [frontmatter___ended, frontmatter___started], order: DESC } + ) { + edges { + node { + frontmatter { + title + qualifications + started + ended + } + fields { + slug + } + excerpt + } + } + } + } +` diff --git a/src/pages/about/experience.jsx b/src/pages/about/experience.jsx new file mode 100644 index 0000000..0f285ec --- /dev/null +++ b/src/pages/about/experience.jsx @@ -0,0 +1,83 @@ +import React from 'react' +import { graphql } from 'gatsby' + +import { HistoryConsumer } from '../../provider/history' +import Head from '../../components/Head' +import Timeline from '../../components/timeline/Timeline' + +class Page extends React.Component { + state = { + pageName: 'Experience', + } + + componentDidMount() { + const { breadcrumb } = this.props.pageContext + + this.props.history.update({ + location: breadcrumb.location, + crumbLabel: this.state.pageName, + crumbs: breadcrumb.crumbs, + }) + } + + render() { + const { + allMdx: { edges }, + } = this.props.data + + const timelineSection = edges.map(({ node: { frontmatter, fields, excerpt } }) => ({ + title: frontmatter.title, + description: excerpt, + started: frontmatter.started, + ended: frontmatter.ended, + link: fields.slug, + })) + const timelineData = [ + { + name: 'Experience', + icon: 'briefcase', + data: timelineSection, + }, + ] + + return ( + <> + + +

{this.state.pageName}

+ + + + ) + } +} + +export default React.forwardRef((props, ref) => ( + + {(history) => } + +)) + +export const query = graphql` + query ExperienceQuery { + allMdx( + filter: { fields: { source: { eq: "experience" } } } + sort: { fields: [frontmatter___ended, frontmatter___started], order: DESC } + ) { + edges { + node { + frontmatter { + title + position + started + ended + } + fields { + slug + } + excerpt + } + } + } + } +` diff --git a/src/styles/app.scss b/src/styles/app.scss index f001626..441cde9 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -59,6 +59,7 @@ @import "components/separator"; @import "components/status"; @import "components/theme-switch"; +@import "components/timeline"; @import "components/toast"; @import "components/tooltip"; diff --git a/src/styles/components/_timeline.scss b/src/styles/components/_timeline.scss new file mode 100644 index 0000000..501fdef --- /dev/null +++ b/src/styles/components/_timeline.scss @@ -0,0 +1,102 @@ +#timeline { + @include breakpoint-up(md) { + margin-bottom: 80px; + } + + header { + display: grid; + grid-template-columns: 30px 1fr; + align-items: center; + margin-bottom: var(--spacing-lg); + column-gap: var(--spacing-lg); + user-select: none; + + @include breakpoint-up(md) { + grid-template-columns: 50px 1fr; + margin-bottom: var(--spacing-xl); + } + + .icon { + --icon-size: 100%; + } + + .title { + font-size: var(--text-lg); + letter-spacing: var(--spacing-sm); + text-transform: uppercase; + + @include breakpoint-up(md) { + font-size: var(--text-xxl); + } + } + } + + .items { + position: relative; + + &::before { + content: ""; + position: absolute; + top: 10px; + left: 15px; + width: 2px; + height: 110%; + background: var(--color-light); + background: linear-gradient(0deg, transparent 0%, var(--color-light) 30%); + + @include breakpoint-up(md) { + left: 25px; + height: 130%; + } + } + + .item { + position: relative; + margin-bottom: var(--spacing-xl); + padding-left: calc(30px + var(--spacing-lg)); + + @include breakpoint-up(md) { + padding-left: calc(50px + var(--spacing-lg)); + } + + &::before { + content: ""; + position: absolute; + top: 5px; + left: 10px; + width: 12px; + height: 12px; + border: 2px solid var(--color-light); + border-radius: var(--border-radius-full); + background: var(--background-color-normal); + box-shadow: 0 0 0 8px var(--background-color-normal); + + @include breakpoint-up(md) { + top: 4px; + left: 18px; + width: 16px; + height: 16px; + } + } + + .timespan { + color: var(--color-06); + } + + .title { + margin-top: var(--spacing-xxs); + margin-bottom: var(--spacing-xxs); + color: var(--color-primary); + font-size: var(--text-lg); + + &:hover { + color: var(--color-primary-dark); + } + + a { + display: block; + } + } + } + } +} diff --git a/src/templates/EducationSingle.jsx b/src/templates/EducationSingle.jsx new file mode 100644 index 0000000..f79010f --- /dev/null +++ b/src/templates/EducationSingle.jsx @@ -0,0 +1,127 @@ +import React from 'react' +import { graphql } from 'gatsby' + +import { HistoryConsumer } from '../provider/history' +import Article from '../layouts/Article' +import Head from '../components/Head' +import DataType from '../components/DataType' +import Time from '../components/Time' +import { Link } from '../elements/Link' +import Hero from '../components/Hero' +import { stringTruncateMiddle } from '../utils/helper' + +class Page extends React.Component { + state = { + pageName: 'Education', + } + + componentDidMount() { + const { breadcrumb } = this.props.pageContext + + this.props.history.update({ + location: breadcrumb.location, + crumbLabel: this.props.data.mdx.frontmatter.title, + crumbs: breadcrumb.crumbs, + }) + } + + render() { + const { + mdx: { + fields: { slug }, + frontmatter: { tags, title, qualifications, institution, started, ended }, + body, + excerpt, + }, + } = this.props.data + + const keywords = + tags?.length > 0 ? tags : qualifications?.length > 0 ? qualifications : null + + return ( + <> + + + +
+
+

{title}

+

{qualifications.join(' | ')}

+
+
+
    +
  • + Timespan:
  • +
  • + Institution: {institution.name} +
  • +
  • + Address:{' '} + + {institution.location} + +
  • +
  • + Website:{' '} + {institution.website ? ( + + {stringTruncateMiddle(institution.website, 30)} + + ) : ( + + )} +
  • +
+
+
+
+ +
+ + ) + } +} + +export default React.forwardRef((props, ref) => ( + + {(history) => } + +)) + +export const query = graphql` + query($slug: String!) { + mdx(fields: { slug: { eq: $slug }, source: { eq: "education" } }) { + body + excerpt(pruneLength: 150) + fields { + slug + } + frontmatter { + title + started + ended + qualifications + institution { + name + location + website + } + } + } + } +` diff --git a/src/templates/ExperienceSingle.jsx b/src/templates/ExperienceSingle.jsx new file mode 100644 index 0000000..6924b26 --- /dev/null +++ b/src/templates/ExperienceSingle.jsx @@ -0,0 +1,134 @@ +import React from 'react' +import { graphql } from 'gatsby' + +import { HistoryConsumer } from '../provider/history' +import Article from '../layouts/Article' +import Head from '../components/Head' +import DataType from '../components/DataType' +import Time from '../components/Time' +import { Link } from '../elements/Link' +import Hero from '../components/Hero' + +class Page extends React.Component { + state = { + pageName: 'Experience', + } + + componentDidMount() { + const { breadcrumb } = this.props.pageContext + + this.props.history.update({ + location: breadcrumb.location, + crumbLabel: this.props.data.mdx.title, + crumbs: breadcrumb.crumbs, + }) + } + + render() { + const { + mdx: { + fields: { slug }, + frontmatter: { tags, responsibilities, title, position, company, started, ended }, + body, + excerpt, + }, + } = this.props.data + + const keywords = + tags?.length > 0 ? tags : responsibilities?.length > 0 ? responsibilities : null + + return ( + <> + + + +
+
+

{title}

+

{responsibilities.join(' | ')}

+
+
+
    +
  • + Timespan:
  • +
  • + Company: {company.name} +
  • +
  • + Position: {position} +
  • +
  • + Address:{' '} + + {company.address} + +
  • +
  • + Industry: {company.industry} +
  • +
  • + Website:{' '} + {company.website ? ( + + {company.website} + + ) : ( + + )} +
  • +
+
+
+
+ +
+ + ) + } +} + +export default React.forwardRef((props, ref) => ( + + {(history) => } + +)) + +export const query = graphql` + query($slug: String!) { + mdx(fields: { slug: { eq: $slug }, source: { eq: "experience" } }) { + body + excerpt(pruneLength: 150) + fields { + slug + } + frontmatter { + title + started + ended + position + responsibilities + company { + name + address + industry + website + } + } + } + } +` diff --git a/src/utils/helper.js b/src/utils/helper.js index 53bf686..f8855a0 100644 --- a/src/utils/helper.js +++ b/src/utils/helper.js @@ -68,6 +68,28 @@ export function stringSlugify(text, separator) { return text } +/** + * Truncate a string from the middle. + * + * @param {string} str string to truncate + * @param {number} maxLength string max length + * @param {string} separator set a separator + */ +export function stringTruncateMiddle(str, maxLength = 50, separator = '...') { + if (str.length < maxLength) { + return str + } + + const length = str.length + const charsToShow = maxLength - separator.length + const frontChars = Math.ceil(charsToShow / 2) + const backChars = Math.floor(charsToShow / 2) + + return str.substr(0, frontChars) + separator + str.substr(length - backChars) +} + +// 100 - 50 = 50 + /** * Turn an array to an object by key/value. *