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 (
+
+
+
+ {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.
*