From 8d5e2d6a7e6c3ed356e7024edfb3448af86dcca1 Mon Sep 17 00:00:00 2001 From: Matthias Kretschmann Date: Sun, 26 May 2019 16:55:56 +0200 Subject: [PATCH 1/4] open source section --- .travis.yml | 3 +- content/meta.yml | 56 +++++++++--------- content/repos.yml | 11 ++++ gatsby-config.js | 2 +- jest/__fixtures__/meta.json | 2 +- package.json | 1 + src/components/Layout.jsx | 4 +- src/components/atoms/Button.module.scss | 2 +- src/components/atoms/SEO.jsx | 4 +- src/components/atoms/Typekit.jsx | 4 +- src/components/atoms/Vcard.jsx | 4 +- src/components/atoms/Vcard.test.jsx | 4 +- .../molecules/Availability.module.scss | 2 +- .../molecules/Availability.test.jsx | 4 +- src/components/molecules/LogoUnit.jsx | 4 +- src/components/molecules/LogoUnit.test.jsx | 2 +- src/components/molecules/Networks.test.jsx | 2 +- .../molecules/ProjectImage.module.scss | 2 +- .../molecules/ProjectTechstack.module.scss | 2 +- src/components/molecules/Repository.jsx | 46 +++++++++++++++ .../molecules/Repository.module.scss | 59 +++++++++++++++++++ src/components/organisms/Footer.jsx | 4 +- src/components/organisms/Header.jsx | 4 +- src/components/organisms/Repositories.jsx | 47 +++++++++++++++ .../organisms/Repositories.module.scss | 29 +++++++++ src/hooks/use-meta.js | 6 +- src/images/star.svg | 3 + src/pages/index.jsx | 8 +++ src/store/AppProvider.jsx | 21 +++---- src/styles/_variables.scss | 1 + 30 files changed, 272 insertions(+), 71 deletions(-) create mode 100644 content/repos.yml create mode 100644 src/components/molecules/Repository.jsx create mode 100644 src/components/molecules/Repository.module.scss create mode 100644 src/components/organisms/Repositories.jsx create mode 100644 src/components/organisms/Repositories.module.scss create mode 100644 src/images/star.svg diff --git a/.travis.yml b/.travis.yml index a1469fc..ebec289 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ dist: xenial language: node_js -node_js: - - '11' +node_js: node cache: directories: diff --git a/content/meta.yml b/content/meta.yml index 46647ff..8f02107 100644 --- a/content/meta.yml +++ b/content/meta.yml @@ -1,34 +1,34 @@ -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 +- 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: - Email: mailto:m@kretschmann.io - Blog: https://kremalicious.com - Twitter: https://twitter.com/kremalicious - GitHub: https://github.com/kremalicious - Dribbble: https://dribbble.com/kremalicious + 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 -availability: - status: false - available: 'πŸ‘” Available for new projects. Let’s talk!' - unavailable: Not available for new projects. + availability: + status: false + available: 'πŸ‘” Available for new projects. Let’s talk!' + unavailable: Not available for new projects. -# Footer actions -gpg: https://kretschmann.io/pub.gpg -addressbook: /matthias-kretschmann.vcf + # Footer actions + gpg: https://kretschmann.io/pub.gpg + addressbook: /matthias-kretschmann.vcf -typekitID: dtg3zui + typekitID: dtg3zui -# Analytics tools -matomoUrl: https://analytics.kremalicious.com -matomoSite: 2 + # Analytics tools + matomoUrl: https://analytics.kremalicious.com + matomoSite: 2 -allowedHosts: - - matthiaskretschmann.com - - beta.matthiaskretschmann.com - - localhost + allowedHosts: + - matthiaskretschmann.com + - beta.matthiaskretschmann.com + - localhost diff --git a/content/repos.yml b/content/repos.yml new file mode 100644 index 0000000..a982972 --- /dev/null +++ b/content/repos.yml @@ -0,0 +1,11 @@ +- user: kremalicious + repos: + - portfolio + - blog + - blowfish + - gatsby-plugin-matomo + - gatsby-redirect-from + - hyper-mac-pro + - appstorebadges + - kbdfun + - wp-icons-template diff --git a/gatsby-config.js b/gatsby-config.js index b3702fa..71cb387 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -2,7 +2,7 @@ const path = require('path') const fs = require('fs') const yaml = require('js-yaml') const meta = yaml.load(fs.readFileSync('./content/meta.yml', 'utf8')) -const { title, description, url, matomoSite, matomoUrl } = meta +const { title, description, url, matomoSite, matomoUrl } = meta[0] module.exports = { siteMetadata: { diff --git a/jest/__fixtures__/meta.json b/jest/__fixtures__/meta.json index c4704af..5320c95 100644 --- a/jest/__fixtures__/meta.json +++ b/jest/__fixtures__/meta.json @@ -1,5 +1,5 @@ { - "contentYaml": { + "metaYaml": { "title": "Matthias Kretschmann", "tagline": "Designer & Developer", "description": "Portfolio of web & ui designer/developer hybrid Matthias Kretschmann.", diff --git a/package.json b/package.json index 17495ff..66d5054 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "new": "babel-node ./scripts/new.js" }, "dependencies": { + "axios": "^0.18.0", "classnames": "^2.2.6", "file-saver": "^2.0.1", "gatsby": "^2.7.1", diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx index ef17191..b696d87 100644 --- a/src/components/Layout.jsx +++ b/src/components/Layout.jsx @@ -16,7 +16,7 @@ import styles from './Layout.module.scss' const query = graphql` query { - contentYaml { + metaYaml { allowedHosts } } @@ -41,7 +41,7 @@ export default class Layout extends PureComponent { { - const { allowedHosts } = data.contentYaml + const { allowedHosts } = data.metaYaml return ( <> diff --git a/src/components/atoms/Button.module.scss b/src/components/atoms/Button.module.scss index edb727d..59e1cfb 100644 --- a/src/components/atoms/Button.module.scss +++ b/src/components/atoms/Button.module.scss @@ -5,7 +5,7 @@ width: 100%; color: $brand-cyan; text-align: center; - border-radius: .25rem; + border-radius: $border-radius; padding: $spacer / 4 $spacer / 2; transition-property: all; background: rgba(#fff, .15); diff --git a/src/components/atoms/SEO.jsx b/src/components/atoms/SEO.jsx index 0081220..b67d986 100644 --- a/src/components/atoms/SEO.jsx +++ b/src/components/atoms/SEO.jsx @@ -5,7 +5,7 @@ import { StaticQuery, graphql } from 'gatsby' const query = graphql` query { - contentYaml { + metaYaml { title tagline description @@ -75,7 +75,7 @@ export default class SEO extends PureComponent { query={query} render={data => { const { project } = this.props - const meta = data.contentYaml + const meta = data.metaYaml const title = (project && project.title) || null const description = (project && project.fields.excerpt) || meta.description diff --git a/src/components/atoms/Typekit.jsx b/src/components/atoms/Typekit.jsx index 29e6a28..f69fec0 100644 --- a/src/components/atoms/Typekit.jsx +++ b/src/components/atoms/Typekit.jsx @@ -19,7 +19,7 @@ const TypekitScript = typekitID => ( const query = graphql` query { - contentYaml { + metaYaml { typekitID } } @@ -29,7 +29,7 @@ const Typekit = () => ( { - const { typekitID } = data.contentYaml + const { typekitID } = data.metaYaml return ( typekitID && ( diff --git a/src/components/atoms/Vcard.jsx b/src/components/atoms/Vcard.jsx index e6b25a8..9795578 100644 --- a/src/components/atoms/Vcard.jsx +++ b/src/components/atoms/Vcard.jsx @@ -5,7 +5,7 @@ import vCard from 'vcf' const query = graphql` query { - contentYaml { + metaYaml { title tagline description @@ -37,7 +37,7 @@ export default class Vcard extends PureComponent { { - const meta = data.contentYaml + const meta = data.metaYaml const handleAddressbookClick = e => { e.preventDefault() diff --git a/src/components/atoms/Vcard.test.jsx b/src/components/atoms/Vcard.test.jsx index 39bd2b4..aac10df 100644 --- a/src/components/atoms/Vcard.test.jsx +++ b/src/components/atoms/Vcard.test.jsx @@ -24,14 +24,14 @@ describe('Vcard', () => { }) it('combined vCard download process finishes', async () => { - await init(data.contentYaml) + await init(data.metaYaml) expect(global.URL.createObjectURL).toHaveBeenCalledTimes(1) }) it('vCard can be constructed', async () => { const vcard = await constructVcard( 'data:image/jpeg;base64,00', - data.contentYaml + data.metaYaml ) expect(vcard).toBeDefined() }) diff --git a/src/components/molecules/Availability.module.scss b/src/components/molecules/Availability.module.scss index 7898fca..b7ab80d 100644 --- a/src/components/molecules/Availability.module.scss +++ b/src/components/molecules/Availability.module.scss @@ -1,7 +1,7 @@ @import 'variables'; .availability { - border-radius: .25rem; + border-radius: $border-radius; color: $text-color-light; z-index: 2; padding: $spacer / 2; diff --git a/src/components/molecules/Availability.test.jsx b/src/components/molecules/Availability.test.jsx index d656d2a..ace6a2b 100644 --- a/src/components/molecules/Availability.test.jsx +++ b/src/components/molecules/Availability.test.jsx @@ -14,7 +14,7 @@ describe('Availability', () => { it('renders correctly when status: true', () => { useStaticQuery.mockImplementationOnce(() => { return { - contentYaml: { + metaYaml: { availability: { status: true, available: 'I am available.', @@ -32,7 +32,7 @@ describe('Availability', () => { it('renders correctly when status: false', () => { useStaticQuery.mockImplementationOnce(() => { return { - contentYaml: { + metaYaml: { availability: { status: false, available: 'I am available.', diff --git a/src/components/molecules/LogoUnit.jsx b/src/components/molecules/LogoUnit.jsx index 1f8c67b..f6f656a 100644 --- a/src/components/molecules/LogoUnit.jsx +++ b/src/components/molecules/LogoUnit.jsx @@ -9,7 +9,7 @@ import styles from './LogoUnit.module.scss' const query = graphql` query { - contentYaml { + metaYaml { title tagline } @@ -35,7 +35,7 @@ export default class LogoUnit extends PureComponent { { - const { title, tagline } = data.contentYaml + const { title, tagline } = data.metaYaml return (
diff --git a/src/components/molecules/LogoUnit.test.jsx b/src/components/molecules/LogoUnit.test.jsx index 190f710..81b3142 100644 --- a/src/components/molecules/LogoUnit.test.jsx +++ b/src/components/molecules/LogoUnit.test.jsx @@ -10,7 +10,7 @@ beforeEach(() => { describe('LogoUnit', () => { it('renders correctly from data file values', () => { - const { title, tagline } = data.contentYaml + const { title, tagline } = data.metaYaml const { container, getByTestId } = render() expect(container.firstChild).toBeInTheDocument() diff --git a/src/components/molecules/Networks.test.jsx b/src/components/molecules/Networks.test.jsx index 921e86f..b65d5b5 100644 --- a/src/components/molecules/Networks.test.jsx +++ b/src/components/molecules/Networks.test.jsx @@ -10,7 +10,7 @@ beforeEach(() => { describe('Networks', () => { it('renders correctly from data file values', () => { - const { social } = data.contentYaml + const { social } = data.metaYaml const { container, getByTestId } = render() expect(container.firstChild).toBeInTheDocument() diff --git a/src/components/molecules/ProjectImage.module.scss b/src/components/molecules/ProjectImage.module.scss index 1284614..b4524d4 100644 --- a/src/components/molecules/ProjectImage.module.scss +++ b/src/components/molecules/ProjectImage.module.scss @@ -9,7 +9,7 @@ @media (min-width: $projectImageMaxWidth) { max-width: $projectImageMaxWidth; - border-radius: .25rem; + border-radius: $border-radius; overflow: hidden; } diff --git a/src/components/molecules/ProjectTechstack.module.scss b/src/components/molecules/ProjectTechstack.module.scss index 200fd80..08f9f47 100644 --- a/src/components/molecules/ProjectTechstack.module.scss +++ b/src/components/molecules/ProjectTechstack.module.scss @@ -15,7 +15,7 @@ padding: $spacer / 4; text-align: center; background: rgba(#fff, .15); - border-radius: .25rem; + border-radius: $border-radius; border: .05rem solid transparent; color: $brand-grey-light; font-size: $font-size-small; diff --git a/src/components/molecules/Repository.jsx b/src/components/molecules/Repository.jsx new file mode 100644 index 0000000..9982930 --- /dev/null +++ b/src/components/molecules/Repository.jsx @@ -0,0 +1,46 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { ReactComponent as Star } from '../../images/star.svg' +import styles from './Repository.module.scss' +import LinkIcon from '../atoms/LinkIcon' + +const Repository = ({ repo }) => { + const { name, description, html_url, homepage, stargazers_count } = repo + + // for blog & portfolio and if there's no homepage, use github url + // else use homepage field + const repoLink = + name === 'blog' || name === 'portfolio' || !homepage ? html_url : homepage + + return ( +
+

+ {name} +

+

{description}

+

+ {name === 'portfolio' || name === 'blog' + ? null + : homepage && ( + + Learn more + + )} + + + GitHub + + + + {stargazers_count} + +

+
+ ) +} + +Repository.propTypes = { + repo: PropTypes.object.isRequired +} + +export default Repository diff --git a/src/components/molecules/Repository.module.scss b/src/components/molecules/Repository.module.scss new file mode 100644 index 0000000..5d335ed --- /dev/null +++ b/src/components/molecules/Repository.module.scss @@ -0,0 +1,59 @@ +@import 'variables'; + +.repo { + padding: $spacer; + border-radius: $border-radius; + background: rgba(#fff, .15); + box-shadow: 0 3px 5px rgba($brand-main, .1), + 0 5px 16px rgba($brand-main, .1); + display: flex; + flex-wrap: wrap; + align-items: flex-start; + + :global(.dark) & { + background: darken($body-background-color--dark, 1%); + box-shadow: 0 3px 5px rgba(darken($brand-main, 20%), .1), + 0 5px 16px rgba(darken($brand-main, 20%), .1); + } + + > * { + width: 100%; + } + + p { + font-size: $font-size-small; + } + + p:last-child { + margin: 0; + } +} + +.repoTitle { + font-size: $font-size-h4; + margin-bottom: $spacer / 2; +} + +.meta { + font-size: $font-size-small; + align-self: flex-end; + display: flex; + justify-content: space-between; + + a { + display: inline-block; + color: $brand-grey-light; + font-variant-numeric: lining-nums; + + &:hover, + &:focus { + color: $brand-cyan; + } + } + + svg { + fill: currentColor; + width: $font-size-mini; + height: $font-size-mini; + } +} diff --git a/src/components/organisms/Footer.jsx b/src/components/organisms/Footer.jsx index cd100b0..154e7fa 100644 --- a/src/components/organisms/Footer.jsx +++ b/src/components/organisms/Footer.jsx @@ -14,7 +14,7 @@ const query = graphql` bugs } - contentYaml { + metaYaml { title url gpg @@ -68,7 +68,7 @@ export default class Footer extends PureComponent { query={query} render={data => { const pkg = data.portfolioJson - const meta = data.contentYaml + const meta = data.metaYaml return }} diff --git a/src/components/organisms/Header.jsx b/src/components/organisms/Header.jsx index 4de942b..4e9aa56 100644 --- a/src/components/organisms/Header.jsx +++ b/src/components/organisms/Header.jsx @@ -10,7 +10,7 @@ import styles from './Header.module.scss' const query = graphql` query { - contentYaml { + metaYaml { availability { status } @@ -30,7 +30,7 @@ export default class Header extends PureComponent { { - const meta = data.contentYaml + const meta = data.metaYaml let headerClasses = classNames([styles.header], { [styles.minimal]: minimal diff --git a/src/components/organisms/Repositories.jsx b/src/components/organisms/Repositories.jsx new file mode 100644 index 0000000..55e7c82 --- /dev/null +++ b/src/components/organisms/Repositories.jsx @@ -0,0 +1,47 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import axios from 'axios' +import Repository from '../molecules/Repository' +import styles from './Repositories.module.scss' + +export default class Repositories extends PureComponent { + static propTypes = { + user: PropTypes.string.isRequired, + repos: PropTypes.array.isRequired + } + + state = { repos: [] } + + async componentDidMount() { + try { + const repos = await this.getGithubRepos(this.props.user) + this.setState({ repos }) + } catch (error) { + console.error(error.message) // eslint-disable-line + } + } + + async getGithubRepos(user) { + const allRepos = await axios.get( + `https://api.github.com/users/${user}/repos?per_page=100` + ) + const repos = allRepos.data + .filter(({ name }) => this.props.repos.includes(name)) + .sort((a, b) => b.pushed_at.localeCompare(a.pushed_at)) // sort by pushed to, newest first + + return repos + } + + render() { + return ( +
+

Open Source Projects

+
+ {this.state.repos.map(repo => ( + + ))} +
+
+ ) + } +} diff --git a/src/components/organisms/Repositories.module.scss b/src/components/organisms/Repositories.module.scss new file mode 100644 index 0000000..e733182 --- /dev/null +++ b/src/components/organisms/Repositories.module.scss @@ -0,0 +1,29 @@ +@import 'variables'; + +.section { + max-width: calc(#{$projectImageMaxWidth} - #{$spacer * 2}); + margin: $spacer * 3 auto 0 auto; + padding-left: $spacer; + padding-right: $spacer; +} + +.sectionTitle { + font-size: $font-size-h3; + margin-bottom: $spacer * 2; + text-align: center; +} + +.repos { + display: grid; + grid-template-columns: 1fr; + grid-gap: $spacer * 2; + + @media (min-width: $screen-sm) { + grid-template-columns: 1fr 1fr; + grid-gap: $spacer * 2; + } + + @media (min-width: $screen-md) { + grid-gap: $spacer * 3; + } +} diff --git a/src/hooks/use-meta.js b/src/hooks/use-meta.js index 47b0d9a..ca41e21 100644 --- a/src/hooks/use-meta.js +++ b/src/hooks/use-meta.js @@ -2,7 +2,7 @@ import { useStaticQuery, graphql } from 'gatsby' const query = graphql` query Meta { - contentYaml { + metaYaml { title tagline description @@ -31,6 +31,6 @@ const query = graphql` ` export const useMeta = () => { - const { contentYaml } = useStaticQuery(query) - return contentYaml + const { metaYaml } = useStaticQuery(query) + return metaYaml } diff --git a/src/images/star.svg b/src/images/star.svg new file mode 100644 index 0000000..4fbb9b6 --- /dev/null +++ b/src/images/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/pages/index.jsx b/src/pages/index.jsx index a3ae107..0c85b1d 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -5,6 +5,7 @@ import SEO from '../components/atoms/SEO' import ProjectImage from '../components/molecules/ProjectImage' import { ReactComponent as Images } from '../images/images.svg' import styles from './index.module.scss' +import Repositories from '../components/organisms/Repositories' function getImageCount(images, slug) { let array = [] @@ -56,6 +57,8 @@ export default class Home extends PureComponent { ) })}
+ + ) } @@ -88,5 +91,10 @@ export const IndexQuery = graphql` } } } + + reposYaml { + user + repos + } } ` diff --git a/src/store/AppProvider.jsx b/src/store/AppProvider.jsx index c6f2dab..eae4c63 100644 --- a/src/store/AppProvider.jsx +++ b/src/store/AppProvider.jsx @@ -5,22 +5,23 @@ import { getLocationTimes } from '../utils/getLocationTimes' import { getCountry } from '../utils/getCountry' export default class AppProvider extends PureComponent { + static propTypes = { + children: PropTypes.any.isRequired + } + state = { dark: false, toggleDark: () => this.toggleDark(), geolocation: null } - static propTypes = { - children: PropTypes.any.isRequired - } - store = typeof localStorage === 'undefined' ? null : localStorage mounted = false async componentDidMount() { this.mounted = true + const geolocation = await getCountry() this.setState({ geolocation }) this.checkDark() @@ -30,16 +31,12 @@ export default class AppProvider extends PureComponent { this.mounted = false } - setDark() { - this.mounted && this.setState({ dark: true }) - } - - setLight() { - this.mounted && this.setState({ dark: false }) + setDark(dark) { + this.mounted && this.setState({ dark }) } darkLocalStorageMode(darkLocalStorage) { - darkLocalStorage === 'true' ? this.setDark() : this.setLight() + darkLocalStorage === 'true' ? this.setDark(true) : this.setDark(false) } // @@ -51,7 +48,7 @@ export default class AppProvider extends PureComponent { const now = new Date().getHours() const weWantItDarkTimes = now >= sunset || now <= sunrise - !dark && weWantItDarkTimes ? this.setDark() : this.setLight() + !dark && weWantItDarkTimes ? this.setDark(true) : this.setDark(false) } async checkDark() { diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 52a4acc..00e1ef2 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -67,6 +67,7 @@ $color-headings--dark: $brand-main-light; ///////////////////////////////////// $spacer: ($font-size-base * $line-height); +$border-radius: .25rem; // Responsive breakpoints ///////////////////////////////////// From c008007d74d0a18a13e2a5bc19884e77f480fd80 Mon Sep 17 00:00:00 2001 From: Matthias Kretschmann Date: Sun, 26 May 2019 17:50:19 +0200 Subject: [PATCH 2/4] fetch repos on build time --- gatsby-node.js | 44 +++++++++++++++++++++++ src/components/organisms/Repositories.jsx | 27 ++------------ src/pages/index.jsx | 14 +++----- 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/gatsby-node.js b/gatsby-node.js index 5fd1526..c42b4ef 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -1,7 +1,13 @@ +/* eslint-disable no-console */ + const path = require('path') const remark = require('remark') const markdown = require('remark-parse') const html = require('remark-html') +const axios = require('axios') +const fs = require('fs') +const yaml = require('js-yaml') +const reposYaml = yaml.load(fs.readFileSync('./content/repos.yml', 'utf8')) function truncate(n, useWordBoundary) { if (this.length <= n) { @@ -15,6 +21,30 @@ function truncate(n, useWordBoundary) { ) } +async function getGithubRepos(data) { + const allRepos = await axios.get( + `https://api.github.com/users/${data.user}/repos?per_page=100` + ) + const repos = allRepos.data + // filter by what's defined in content/repos.yml + .filter(({ name }) => data.repos.includes(name)) + // sort by pushed to, newest first + .sort((a, b) => b.pushed_at.localeCompare(a.pushed_at)) + + return repos +} + +let repos + +exports.onPreBootstrap = async () => { + try { + repos = await getGithubRepos(reposYaml[0]) + console.log(`success getGithubRepos: ${repos.length} repos`) + } catch (error) { + console.error(error.message) + } +} + exports.onCreateNode = ({ node, actions }) => { const { createNodeField } = actions @@ -53,6 +83,20 @@ exports.onCreateNode = ({ node, actions }) => { } } +exports.onCreatePage = async ({ page, actions }) => { + const { createPage } = actions + + // Add repos to front page's context + if (page.path === '/') + createPage({ + ...page, + context: { + ...page.context, + repos + } + }) +} + // // Create project pages from projects.yml // diff --git a/src/components/organisms/Repositories.jsx b/src/components/organisms/Repositories.jsx index 55e7c82..3535ffd 100644 --- a/src/components/organisms/Repositories.jsx +++ b/src/components/organisms/Repositories.jsx @@ -1,43 +1,20 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import axios from 'axios' + import Repository from '../molecules/Repository' import styles from './Repositories.module.scss' export default class Repositories extends PureComponent { static propTypes = { - user: PropTypes.string.isRequired, repos: PropTypes.array.isRequired } - state = { repos: [] } - - async componentDidMount() { - try { - const repos = await this.getGithubRepos(this.props.user) - this.setState({ repos }) - } catch (error) { - console.error(error.message) // eslint-disable-line - } - } - - async getGithubRepos(user) { - const allRepos = await axios.get( - `https://api.github.com/users/${user}/repos?per_page=100` - ) - const repos = allRepos.data - .filter(({ name }) => this.props.repos.includes(name)) - .sort((a, b) => b.pushed_at.localeCompare(a.pushed_at)) // sort by pushed to, newest first - - return repos - } - render() { return (

Open Source Projects

- {this.state.repos.map(repo => ( + {this.props.repos.map(repo => ( ))}
diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 0c85b1d..598b016 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -20,12 +20,13 @@ function getImageCount(images, slug) { export default class Home extends PureComponent { static propTypes = { - data: PropTypes.object, - location: PropTypes.object + data: PropTypes.object.isRequired, + pageContext: PropTypes.object.isRequired, + location: PropTypes.object.isRequired } render() { - const { data } = this.props + const { data, pageContext } = this.props const projects = data.allProjectsYaml.edges const images = data.projectImageFiles.edges @@ -58,7 +59,7 @@ export default class Home extends PureComponent { })} - + ) } @@ -91,10 +92,5 @@ export const IndexQuery = graphql` } } } - - reposYaml { - user - repos - } } ` From 05b1115533ad722f467cdde98257ef2e72dd0f33 Mon Sep 17 00:00:00 2001 From: Matthias Kretschmann Date: Sun, 26 May 2019 22:20:16 +0200 Subject: [PATCH 3/4] add tests --- gatsby-node.js | 13 ++- jest/__fixtures__/repos.json | 102 ++++++++++++++++++ package.json | 1 + src/components/atoms/LinkIcon.jsx | 3 + src/components/molecules/Repository.jsx | 5 +- src/components/molecules/Repository.test.jsx | 40 +++++++ .../organisms/Repositories.test.jsx | 11 ++ src/pages/__tests__/index.test.jsx | 10 +- src/pages/index.jsx | 3 +- 9 files changed, 180 insertions(+), 8 deletions(-) create mode 100644 jest/__fixtures__/repos.json create mode 100644 src/components/molecules/Repository.test.jsx create mode 100644 src/components/organisms/Repositories.test.jsx diff --git a/gatsby-node.js b/gatsby-node.js index c42b4ef..a5dbc8e 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -8,6 +8,8 @@ const axios = require('axios') const fs = require('fs') const yaml = require('js-yaml') const reposYaml = yaml.load(fs.readFileSync('./content/repos.yml', 'utf8')) +const { performance } = require('perf_hooks') +const chalk = require('chalk') function truncate(n, useWordBoundary) { if (this.length <= n) { @@ -37,11 +39,18 @@ async function getGithubRepos(data) { let repos exports.onPreBootstrap = async () => { + const t0 = performance.now() + try { repos = await getGithubRepos(reposYaml[0]) - console.log(`success getGithubRepos: ${repos.length} repos`) + const t1 = performance.now() + const ms = t1 - t0 + const s = ((ms / 1000) % 60).toFixed(3) + console.log( + chalk.green('success ') + `getGithubRepos: ${repos.length} repos - ${s} s` + ) } catch (error) { - console.error(error.message) + throw Error(error.message) } } diff --git a/jest/__fixtures__/repos.json b/jest/__fixtures__/repos.json new file mode 100644 index 0000000..609c8f7 --- /dev/null +++ b/jest/__fixtures__/repos.json @@ -0,0 +1,102 @@ +[ + { + "id": 133283555, + "node_id": "MDEwOlJlcG9zaXRvcnkxMzMyODM1NTU=", + "name": "portfolio", + "full_name": "kremalicious/portfolio", + "private": false, + "owner": { + "login": "kremalicious", + "id": 90316, + "node_id": "MDQ6VXNlcjkwMzE2", + "avatar_url": "https://avatars1.githubusercontent.com/u/90316?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/kremalicious", + "html_url": "https://github.com/kremalicious", + "followers_url": "https://api.github.com/users/kremalicious/followers", + "following_url": "https://api.github.com/users/kremalicious/following{/other_user}", + "gists_url": "https://api.github.com/users/kremalicious/gists{/gist_id}", + "starred_url": "https://api.github.com/users/kremalicious/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/kremalicious/subscriptions", + "organizations_url": "https://api.github.com/users/kremalicious/orgs", + "repos_url": "https://api.github.com/users/kremalicious/repos", + "events_url": "https://api.github.com/users/kremalicious/events{/privacy}", + "received_events_url": "https://api.github.com/users/kremalicious/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/kremalicious/portfolio", + "description": "πŸ‘” Portfolio thingy, built with Gatsby", + "fork": false, + "url": "https://api.github.com/repos/kremalicious/portfolio", + "forks_url": "https://api.github.com/repos/kremalicious/portfolio/forks", + "keys_url": "https://api.github.com/repos/kremalicious/portfolio/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/kremalicious/portfolio/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/kremalicious/portfolio/teams", + "hooks_url": "https://api.github.com/repos/kremalicious/portfolio/hooks", + "issue_events_url": "https://api.github.com/repos/kremalicious/portfolio/issues/events{/number}", + "events_url": "https://api.github.com/repos/kremalicious/portfolio/events", + "assignees_url": "https://api.github.com/repos/kremalicious/portfolio/assignees{/user}", + "branches_url": "https://api.github.com/repos/kremalicious/portfolio/branches{/branch}", + "tags_url": "https://api.github.com/repos/kremalicious/portfolio/tags", + "blobs_url": "https://api.github.com/repos/kremalicious/portfolio/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/kremalicious/portfolio/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/kremalicious/portfolio/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/kremalicious/portfolio/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/kremalicious/portfolio/statuses/{sha}", + "languages_url": "https://api.github.com/repos/kremalicious/portfolio/languages", + "stargazers_url": "https://api.github.com/repos/kremalicious/portfolio/stargazers", + "contributors_url": "https://api.github.com/repos/kremalicious/portfolio/contributors", + "subscribers_url": "https://api.github.com/repos/kremalicious/portfolio/subscribers", + "subscription_url": "https://api.github.com/repos/kremalicious/portfolio/subscription", + "commits_url": "https://api.github.com/repos/kremalicious/portfolio/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/kremalicious/portfolio/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/kremalicious/portfolio/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/kremalicious/portfolio/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/kremalicious/portfolio/contents/{+path}", + "compare_url": "https://api.github.com/repos/kremalicious/portfolio/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/kremalicious/portfolio/merges", + "archive_url": "https://api.github.com/repos/kremalicious/portfolio/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/kremalicious/portfolio/downloads", + "issues_url": "https://api.github.com/repos/kremalicious/portfolio/issues{/number}", + "pulls_url": "https://api.github.com/repos/kremalicious/portfolio/pulls{/number}", + "milestones_url": "https://api.github.com/repos/kremalicious/portfolio/milestones{/number}", + "notifications_url": "https://api.github.com/repos/kremalicious/portfolio/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/kremalicious/portfolio/labels{/name}", + "releases_url": "https://api.github.com/repos/kremalicious/portfolio/releases{/id}", + "deployments_url": "https://api.github.com/repos/kremalicious/portfolio/deployments", + "created_at": "2018-05-13T23:53:31Z", + "updated_at": "2019-05-26T19:34:49Z", + "pushed_at": "2019-05-26T19:35:10Z", + "git_url": "git://github.com/kremalicious/portfolio.git", + "ssh_url": "git@github.com:kremalicious/portfolio.git", + "clone_url": "https://github.com/kremalicious/portfolio.git", + "svn_url": "https://github.com/kremalicious/portfolio", + "homepage": "https://matthiaskretschmann.com", + "size": 135135, + "stargazers_count": 55, + "watchers_count": 55, + "language": "JavaScript", + "has_issues": true, + "has_projects": false, + "has_downloads": true, + "has_wiki": false, + "has_pages": false, + "forks_count": 5, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 1, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "forks": 5, + "open_issues": 1, + "watchers": 55, + "default_branch": "master" + } +] diff --git a/package.json b/package.json index 66d5054..1781454 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "babel-eslint": "^10.0.1", "babel-jest": "^24.7.1", "babel-preset-gatsby": "^0.1.11", + "chalk": "^2.4.2", "eslint": "^5.16.0", "eslint-config-prettier": "^4.2.0", "eslint-loader": "^2.1.2", diff --git a/src/components/atoms/LinkIcon.jsx b/src/components/atoms/LinkIcon.jsx index 9acfad8..cf2c9bc 100644 --- a/src/components/atoms/LinkIcon.jsx +++ b/src/components/atoms/LinkIcon.jsx @@ -10,6 +10,7 @@ import { ReactComponent as Dribbble } from '../../images/dribbble.svg' import { ReactComponent as Email } from '../../images/email.svg' import { ReactComponent as Blog } from '../../images/blog.svg' import { ReactComponent as Twitter } from '../../images/twitter.svg' +import { ReactComponent as Star } from '../../images/star.svg' const LinkIcon = ({ title, type, ...props }) => { let typeOrTitle = type ? type : title @@ -39,6 +40,8 @@ const LinkIcon = ({ title, type, ...props }) => { return case 'Twitter': return + case 'star': + return default: return null } diff --git a/src/components/molecules/Repository.jsx b/src/components/molecules/Repository.jsx index 9982930..ecb6ebc 100644 --- a/src/components/molecules/Repository.jsx +++ b/src/components/molecules/Repository.jsx @@ -1,8 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' -import { ReactComponent as Star } from '../../images/star.svg' -import styles from './Repository.module.scss' import LinkIcon from '../atoms/LinkIcon' +import styles from './Repository.module.scss' const Repository = ({ repo }) => { const { name, description, html_url, homepage, stargazers_count } = repo @@ -32,7 +31,7 @@ const Repository = ({ repo }) => { - {stargazers_count} + {stargazers_count}

diff --git a/src/components/molecules/Repository.test.jsx b/src/components/molecules/Repository.test.jsx new file mode 100644 index 0000000..ee256c2 --- /dev/null +++ b/src/components/molecules/Repository.test.jsx @@ -0,0 +1,40 @@ +import React from 'react' +import { render } from 'react-testing-library' +import Repository from './Repository' +import repos from '../../../jest/__fixtures__/repos.json' + +describe('Repository', () => { + it('renders correctly', () => { + const { container } = render() + expect(container.firstChild).toBeInTheDocument() + }) + + it('uses html_url as main link for portfolio & blog', () => { + const repo1 = { + name: 'portfolio', + html_url: 'html_url' + } + + const { container } = render() + expect(container.querySelector('h1 > a').getAttribute('href')).toBe( + repo1.html_url + ) + }) + + it('renders homepage link when provided', () => { + const repo = { + name: 'Hello', + homepage: 'hello' + } + + const { container } = render() + expect(container.querySelectorAll('p:last-child a').length).toBe(3) + }) + + it('renders no link without homepage', () => { + const repo = { name: 'Hello' } + + const { container } = render() + expect(container.querySelectorAll('p:last-child a').length).toBe(2) + }) +}) diff --git a/src/components/organisms/Repositories.test.jsx b/src/components/organisms/Repositories.test.jsx new file mode 100644 index 0000000..916cd35 --- /dev/null +++ b/src/components/organisms/Repositories.test.jsx @@ -0,0 +1,11 @@ +import React from 'react' +import { render } from 'react-testing-library' +import Repositories from './Repositories' +import repos from '../../../jest/__fixtures__/repos.json' + +describe('Repositories', () => { + it('renders correctly', () => { + const { container } = render() + expect(container.firstChild).toBeInTheDocument() + }) +}) diff --git a/src/pages/__tests__/index.test.jsx b/src/pages/__tests__/index.test.jsx index a08e405..1230658 100644 --- a/src/pages/__tests__/index.test.jsx +++ b/src/pages/__tests__/index.test.jsx @@ -16,8 +16,16 @@ describe('Home', () => { ...projectImageFiles } + const pageContext = { + repos: [ + { + name: 'Hello' + } + ] + } + it('renders correctly from data file values', () => { - const { container } = render() + const { container } = render() expect(container.firstChild).toBeInTheDocument() }) }) diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 598b016..a93844e 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -21,8 +21,7 @@ function getImageCount(images, slug) { export default class Home extends PureComponent { static propTypes = { data: PropTypes.object.isRequired, - pageContext: PropTypes.object.isRequired, - location: PropTypes.object.isRequired + pageContext: PropTypes.object.isRequired } render() { From e30bb68cb292ad0e806a95e4b3fbe47a66760282 Mon Sep 17 00:00:00 2001 From: Matthias Kretschmann Date: Sun, 26 May 2019 22:55:28 +0200 Subject: [PATCH 4/4] documentation --- README.md | 13 +++++++++++++ gatsby-node.js | 36 ++++++++++++++++++++++-------------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7e648eb..14361f1 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ - [πŸŽ‰ Features](#-features) - [⛡️ Lighthouse score](#️-lighthouse-score) - [πŸ’ One data file to rule all pages](#-one-data-file-to-rule-all-pages) + - [🐱 GitHub repositories](#-github-repositories) - [πŸ’… Theme switcher](#-theme-switcher) - [πŸ† SEO component](#-seo-component) - [πŸ“‡ Client-side vCard creation](#-client-side-vcard-creation) @@ -52,6 +53,18 @@ Gatsby automatically creates pages from each item in that file utilizing the [`P - [`content/projects.yml`](content/projects.yml) - [`src/templates/Project.jsx`](src/templates/Project.jsx) +### 🐱 GitHub repositories + +The open source section at the bottom of the front page shows selected GitHub repositories, sourced from GitHub. + +On build time, all my public repositories are fetched from GitHub, then filtered against the ones defined in `content/repos.yml`, sorted by the last push date, and provided via the page context of the front page. + +If you want to know how, have a look at the respective components: + +- [`gatsby-node.js`](gatsby-node.js) +- [`content/repos.yml`](content/repos.yml) +- [`src/components/molecules/Repository.jsx`](src/components/molecules/Repository.jsx) + ### πŸ’… 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. diff --git a/gatsby-node.js b/gatsby-node.js index a5dbc8e..fb13b3f 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -23,6 +23,9 @@ function truncate(n, useWordBoundary) { ) } +// +// Get GitHub repos +// async function getGithubRepos(data) { const allRepos = await axios.get( `https://api.github.com/users/${data.user}/repos?per_page=100` @@ -36,6 +39,9 @@ async function getGithubRepos(data) { return repos } +// +// Get GitHub repos once and store for later build stages +// let repos exports.onPreBootstrap = async () => { @@ -54,6 +60,22 @@ exports.onPreBootstrap = async () => { } } +// +// Add repos to front page's context +// +exports.onCreatePage = async ({ page, actions }) => { + const { createPage } = actions + + if (page.path === '/') + createPage({ + ...page, + context: { + ...page.context, + repos + } + }) +} + exports.onCreateNode = ({ node, actions }) => { const { createNodeField } = actions @@ -92,20 +114,6 @@ exports.onCreateNode = ({ node, actions }) => { } } -exports.onCreatePage = async ({ page, actions }) => { - const { createPage } = actions - - // Add repos to front page's context - if (page.path === '/') - createPage({ - ...page, - context: { - ...page.context, - repos - } - }) -} - // // Create project pages from projects.yml //