mirror of
https://github.com/kremalicious/portfolio.git
synced 2025-02-14 21:10:41 +01:00
Merge pull request #134 from kremalicious/feature/code
New open source section
This commit is contained in:
commit
41c151971c
@ -1,7 +1,6 @@
|
|||||||
dist: xenial
|
dist: xenial
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js: node
|
||||||
- '11'
|
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
|
13
README.md
13
README.md
@ -17,6 +17,7 @@
|
|||||||
- [🎉 Features](#-features)
|
- [🎉 Features](#-features)
|
||||||
- [⛵️ Lighthouse score](#️-lighthouse-score)
|
- [⛵️ Lighthouse score](#️-lighthouse-score)
|
||||||
- [💍 One data file to rule all pages](#-one-data-file-to-rule-all-pages)
|
- [💍 One data file to rule all pages](#-one-data-file-to-rule-all-pages)
|
||||||
|
- [🐱 GitHub repositories](#-github-repositories)
|
||||||
- [💅 Theme switcher](#-theme-switcher)
|
- [💅 Theme switcher](#-theme-switcher)
|
||||||
- [🏆 SEO component](#-seo-component)
|
- [🏆 SEO component](#-seo-component)
|
||||||
- [📇 Client-side vCard creation](#-client-side-vcard-creation)
|
- [📇 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)
|
- [`content/projects.yml`](content/projects.yml)
|
||||||
- [`src/templates/Project.jsx`](src/templates/Project.jsx)
|
- [`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
|
### 💅 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.
|
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.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
title: Matthias Kretschmann
|
- title: Matthias Kretschmann
|
||||||
tagline: Designer & Developer
|
tagline: Designer & Developer
|
||||||
description: Portfolio of web & ui designer/developer hybrid Matthias Kretschmann.
|
description: Portfolio of web & ui designer/developer hybrid Matthias Kretschmann.
|
||||||
url: https://matthiaskretschmann.com
|
url: https://matthiaskretschmann.com
|
||||||
|
11
content/repos.yml
Normal file
11
content/repos.yml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
- user: kremalicious
|
||||||
|
repos:
|
||||||
|
- portfolio
|
||||||
|
- blog
|
||||||
|
- blowfish
|
||||||
|
- gatsby-plugin-matomo
|
||||||
|
- gatsby-redirect-from
|
||||||
|
- hyper-mac-pro
|
||||||
|
- appstorebadges
|
||||||
|
- kbdfun
|
||||||
|
- wp-icons-template
|
@ -2,7 +2,7 @@ const path = require('path')
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const yaml = require('js-yaml')
|
const yaml = require('js-yaml')
|
||||||
const meta = yaml.load(fs.readFileSync('./content/meta.yml', 'utf8'))
|
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 = {
|
module.exports = {
|
||||||
siteMetadata: {
|
siteMetadata: {
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const remark = require('remark')
|
const remark = require('remark')
|
||||||
const markdown = require('remark-parse')
|
const markdown = require('remark-parse')
|
||||||
const html = require('remark-html')
|
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'))
|
||||||
|
const { performance } = require('perf_hooks')
|
||||||
|
const chalk = require('chalk')
|
||||||
|
|
||||||
function truncate(n, useWordBoundary) {
|
function truncate(n, useWordBoundary) {
|
||||||
if (this.length <= n) {
|
if (this.length <= n) {
|
||||||
@ -15,6 +23,59 @@ 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`
|
||||||
|
)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Get GitHub repos once and store for later build stages
|
||||||
|
//
|
||||||
|
let repos
|
||||||
|
|
||||||
|
exports.onPreBootstrap = async () => {
|
||||||
|
const t0 = performance.now()
|
||||||
|
|
||||||
|
try {
|
||||||
|
repos = await getGithubRepos(reposYaml[0])
|
||||||
|
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) {
|
||||||
|
throw Error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 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 }) => {
|
exports.onCreateNode = ({ node, actions }) => {
|
||||||
const { createNodeField } = actions
|
const { createNodeField } = actions
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"contentYaml": {
|
"metaYaml": {
|
||||||
"title": "Matthias Kretschmann",
|
"title": "Matthias Kretschmann",
|
||||||
"tagline": "Designer & Developer",
|
"tagline": "Designer & Developer",
|
||||||
"description": "Portfolio of web & ui designer/developer hybrid Matthias Kretschmann.",
|
"description": "Portfolio of web & ui designer/developer hybrid Matthias Kretschmann.",
|
||||||
|
102
jest/__fixtures__/repos.json
Normal file
102
jest/__fixtures__/repos.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
]
|
@ -23,6 +23,7 @@
|
|||||||
"new": "babel-node ./scripts/new.js"
|
"new": "babel-node ./scripts/new.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^0.18.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"file-saver": "^2.0.1",
|
"file-saver": "^2.0.1",
|
||||||
"gatsby": "^2.7.1",
|
"gatsby": "^2.7.1",
|
||||||
@ -63,6 +64,7 @@
|
|||||||
"babel-eslint": "^10.0.1",
|
"babel-eslint": "^10.0.1",
|
||||||
"babel-jest": "^24.7.1",
|
"babel-jest": "^24.7.1",
|
||||||
"babel-preset-gatsby": "^0.1.11",
|
"babel-preset-gatsby": "^0.1.11",
|
||||||
|
"chalk": "^2.4.2",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^5.16.0",
|
||||||
"eslint-config-prettier": "^4.2.0",
|
"eslint-config-prettier": "^4.2.0",
|
||||||
"eslint-loader": "^2.1.2",
|
"eslint-loader": "^2.1.2",
|
||||||
|
@ -16,7 +16,7 @@ import styles from './Layout.module.scss'
|
|||||||
|
|
||||||
const query = graphql`
|
const query = graphql`
|
||||||
query {
|
query {
|
||||||
contentYaml {
|
metaYaml {
|
||||||
allowedHosts
|
allowedHosts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,7 +41,7 @@ export default class Layout extends PureComponent {
|
|||||||
<StaticQuery
|
<StaticQuery
|
||||||
query={query}
|
query={query}
|
||||||
render={data => {
|
render={data => {
|
||||||
const { allowedHosts } = data.contentYaml
|
const { allowedHosts } = data.metaYaml
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
color: $brand-cyan;
|
color: $brand-cyan;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-radius: .25rem;
|
border-radius: $border-radius;
|
||||||
padding: $spacer / 4 $spacer / 2;
|
padding: $spacer / 4 $spacer / 2;
|
||||||
transition-property: all;
|
transition-property: all;
|
||||||
background: rgba(#fff, .15);
|
background: rgba(#fff, .15);
|
||||||
|
@ -10,6 +10,7 @@ import { ReactComponent as Dribbble } from '../../images/dribbble.svg'
|
|||||||
import { ReactComponent as Email } from '../../images/email.svg'
|
import { ReactComponent as Email } from '../../images/email.svg'
|
||||||
import { ReactComponent as Blog } from '../../images/blog.svg'
|
import { ReactComponent as Blog } from '../../images/blog.svg'
|
||||||
import { ReactComponent as Twitter } from '../../images/twitter.svg'
|
import { ReactComponent as Twitter } from '../../images/twitter.svg'
|
||||||
|
import { ReactComponent as Star } from '../../images/star.svg'
|
||||||
|
|
||||||
const LinkIcon = ({ title, type, ...props }) => {
|
const LinkIcon = ({ title, type, ...props }) => {
|
||||||
let typeOrTitle = type ? type : title
|
let typeOrTitle = type ? type : title
|
||||||
@ -39,6 +40,8 @@ const LinkIcon = ({ title, type, ...props }) => {
|
|||||||
return <Blog {...props} />
|
return <Blog {...props} />
|
||||||
case 'Twitter':
|
case 'Twitter':
|
||||||
return <Twitter {...props} />
|
return <Twitter {...props} />
|
||||||
|
case 'star':
|
||||||
|
return <Star {...props} />
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { StaticQuery, graphql } from 'gatsby'
|
|||||||
|
|
||||||
const query = graphql`
|
const query = graphql`
|
||||||
query {
|
query {
|
||||||
contentYaml {
|
metaYaml {
|
||||||
title
|
title
|
||||||
tagline
|
tagline
|
||||||
description
|
description
|
||||||
@ -75,7 +75,7 @@ export default class SEO extends PureComponent {
|
|||||||
query={query}
|
query={query}
|
||||||
render={data => {
|
render={data => {
|
||||||
const { project } = this.props
|
const { project } = this.props
|
||||||
const meta = data.contentYaml
|
const meta = data.metaYaml
|
||||||
const title = (project && project.title) || null
|
const title = (project && project.title) || null
|
||||||
const description =
|
const description =
|
||||||
(project && project.fields.excerpt) || meta.description
|
(project && project.fields.excerpt) || meta.description
|
||||||
|
@ -19,7 +19,7 @@ const TypekitScript = typekitID => (
|
|||||||
|
|
||||||
const query = graphql`
|
const query = graphql`
|
||||||
query {
|
query {
|
||||||
contentYaml {
|
metaYaml {
|
||||||
typekitID
|
typekitID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -29,7 +29,7 @@ const Typekit = () => (
|
|||||||
<StaticQuery
|
<StaticQuery
|
||||||
query={query}
|
query={query}
|
||||||
render={data => {
|
render={data => {
|
||||||
const { typekitID } = data.contentYaml
|
const { typekitID } = data.metaYaml
|
||||||
|
|
||||||
return (
|
return (
|
||||||
typekitID && (
|
typekitID && (
|
||||||
|
@ -5,7 +5,7 @@ import vCard from 'vcf'
|
|||||||
|
|
||||||
const query = graphql`
|
const query = graphql`
|
||||||
query {
|
query {
|
||||||
contentYaml {
|
metaYaml {
|
||||||
title
|
title
|
||||||
tagline
|
tagline
|
||||||
description
|
description
|
||||||
@ -37,7 +37,7 @@ export default class Vcard extends PureComponent {
|
|||||||
<StaticQuery
|
<StaticQuery
|
||||||
query={query}
|
query={query}
|
||||||
render={data => {
|
render={data => {
|
||||||
const meta = data.contentYaml
|
const meta = data.metaYaml
|
||||||
|
|
||||||
const handleAddressbookClick = e => {
|
const handleAddressbookClick = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
@ -24,14 +24,14 @@ describe('Vcard', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('combined vCard download process finishes', async () => {
|
it('combined vCard download process finishes', async () => {
|
||||||
await init(data.contentYaml)
|
await init(data.metaYaml)
|
||||||
expect(global.URL.createObjectURL).toHaveBeenCalledTimes(1)
|
expect(global.URL.createObjectURL).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('vCard can be constructed', async () => {
|
it('vCard can be constructed', async () => {
|
||||||
const vcard = await constructVcard(
|
const vcard = await constructVcard(
|
||||||
'data:image/jpeg;base64,00',
|
'data:image/jpeg;base64,00',
|
||||||
data.contentYaml
|
data.metaYaml
|
||||||
)
|
)
|
||||||
expect(vcard).toBeDefined()
|
expect(vcard).toBeDefined()
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
.availability {
|
.availability {
|
||||||
border-radius: .25rem;
|
border-radius: $border-radius;
|
||||||
color: $text-color-light;
|
color: $text-color-light;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
padding: $spacer / 2;
|
padding: $spacer / 2;
|
||||||
|
@ -14,7 +14,7 @@ describe('Availability', () => {
|
|||||||
it('renders correctly when status: true', () => {
|
it('renders correctly when status: true', () => {
|
||||||
useStaticQuery.mockImplementationOnce(() => {
|
useStaticQuery.mockImplementationOnce(() => {
|
||||||
return {
|
return {
|
||||||
contentYaml: {
|
metaYaml: {
|
||||||
availability: {
|
availability: {
|
||||||
status: true,
|
status: true,
|
||||||
available: 'I am available.',
|
available: 'I am available.',
|
||||||
@ -32,7 +32,7 @@ describe('Availability', () => {
|
|||||||
it('renders correctly when status: false', () => {
|
it('renders correctly when status: false', () => {
|
||||||
useStaticQuery.mockImplementationOnce(() => {
|
useStaticQuery.mockImplementationOnce(() => {
|
||||||
return {
|
return {
|
||||||
contentYaml: {
|
metaYaml: {
|
||||||
availability: {
|
availability: {
|
||||||
status: false,
|
status: false,
|
||||||
available: 'I am available.',
|
available: 'I am available.',
|
||||||
|
@ -9,7 +9,7 @@ import styles from './LogoUnit.module.scss'
|
|||||||
|
|
||||||
const query = graphql`
|
const query = graphql`
|
||||||
query {
|
query {
|
||||||
contentYaml {
|
metaYaml {
|
||||||
title
|
title
|
||||||
tagline
|
tagline
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ export default class LogoUnit extends PureComponent {
|
|||||||
<StaticQuery
|
<StaticQuery
|
||||||
query={query}
|
query={query}
|
||||||
render={data => {
|
render={data => {
|
||||||
const { title, tagline } = data.contentYaml
|
const { title, tagline } = data.metaYaml
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={this.wrapClasses}>
|
<div className={this.wrapClasses}>
|
||||||
|
@ -10,7 +10,7 @@ beforeEach(() => {
|
|||||||
|
|
||||||
describe('LogoUnit', () => {
|
describe('LogoUnit', () => {
|
||||||
it('renders correctly from data file values', () => {
|
it('renders correctly from data file values', () => {
|
||||||
const { title, tagline } = data.contentYaml
|
const { title, tagline } = data.metaYaml
|
||||||
const { container, getByTestId } = render(<LogoUnit />)
|
const { container, getByTestId } = render(<LogoUnit />)
|
||||||
|
|
||||||
expect(container.firstChild).toBeInTheDocument()
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
@ -10,7 +10,7 @@ beforeEach(() => {
|
|||||||
|
|
||||||
describe('Networks', () => {
|
describe('Networks', () => {
|
||||||
it('renders correctly from data file values', () => {
|
it('renders correctly from data file values', () => {
|
||||||
const { social } = data.contentYaml
|
const { social } = data.metaYaml
|
||||||
const { container, getByTestId } = render(<Networks />)
|
const { container, getByTestId } = render(<Networks />)
|
||||||
|
|
||||||
expect(container.firstChild).toBeInTheDocument()
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
@media (min-width: $projectImageMaxWidth) {
|
@media (min-width: $projectImageMaxWidth) {
|
||||||
max-width: $projectImageMaxWidth;
|
max-width: $projectImageMaxWidth;
|
||||||
border-radius: .25rem;
|
border-radius: $border-radius;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
padding: $spacer / 4;
|
padding: $spacer / 4;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: rgba(#fff, .15);
|
background: rgba(#fff, .15);
|
||||||
border-radius: .25rem;
|
border-radius: $border-radius;
|
||||||
border: .05rem solid transparent;
|
border: .05rem solid transparent;
|
||||||
color: $brand-grey-light;
|
color: $brand-grey-light;
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
|
45
src/components/molecules/Repository.jsx
Normal file
45
src/components/molecules/Repository.jsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import LinkIcon from '../atoms/LinkIcon'
|
||||||
|
import styles from './Repository.module.scss'
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={styles.repo}>
|
||||||
|
<h1 className={styles.repoTitle}>
|
||||||
|
<a href={repoLink}>{name}</a>
|
||||||
|
</h1>
|
||||||
|
<p>{description}</p>
|
||||||
|
<p className={styles.meta}>
|
||||||
|
{name === 'portfolio' || name === 'blog'
|
||||||
|
? null
|
||||||
|
: homepage && (
|
||||||
|
<a href={homepage}>
|
||||||
|
<LinkIcon title="website" /> Learn more
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<a href={html_url}>
|
||||||
|
<LinkIcon title="github" /> GitHub
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href={`${html_url}/stargazers`}>
|
||||||
|
<LinkIcon title="star" /> {stargazers_count}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Repository.propTypes = {
|
||||||
|
repo: PropTypes.object.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Repository
|
59
src/components/molecules/Repository.module.scss
Normal file
59
src/components/molecules/Repository.module.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
40
src/components/molecules/Repository.test.jsx
Normal file
40
src/components/molecules/Repository.test.jsx
Normal file
@ -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(<Repository repo={repos[0]} />)
|
||||||
|
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(<Repository repo={repo1} />)
|
||||||
|
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(<Repository repo={repo} />)
|
||||||
|
expect(container.querySelectorAll('p:last-child a').length).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders no link without homepage', () => {
|
||||||
|
const repo = { name: 'Hello' }
|
||||||
|
|
||||||
|
const { container } = render(<Repository repo={repo} />)
|
||||||
|
expect(container.querySelectorAll('p:last-child a').length).toBe(2)
|
||||||
|
})
|
||||||
|
})
|
@ -14,7 +14,7 @@ const query = graphql`
|
|||||||
bugs
|
bugs
|
||||||
}
|
}
|
||||||
|
|
||||||
contentYaml {
|
metaYaml {
|
||||||
title
|
title
|
||||||
url
|
url
|
||||||
gpg
|
gpg
|
||||||
@ -68,7 +68,7 @@ export default class Footer extends PureComponent {
|
|||||||
query={query}
|
query={query}
|
||||||
render={data => {
|
render={data => {
|
||||||
const pkg = data.portfolioJson
|
const pkg = data.portfolioJson
|
||||||
const meta = data.contentYaml
|
const meta = data.metaYaml
|
||||||
|
|
||||||
return <FooterMarkup year={this.state.year} pkg={pkg} meta={meta} />
|
return <FooterMarkup year={this.state.year} pkg={pkg} meta={meta} />
|
||||||
}}
|
}}
|
||||||
|
@ -10,7 +10,7 @@ import styles from './Header.module.scss'
|
|||||||
|
|
||||||
const query = graphql`
|
const query = graphql`
|
||||||
query {
|
query {
|
||||||
contentYaml {
|
metaYaml {
|
||||||
availability {
|
availability {
|
||||||
status
|
status
|
||||||
}
|
}
|
||||||
@ -30,7 +30,7 @@ export default class Header extends PureComponent {
|
|||||||
<StaticQuery
|
<StaticQuery
|
||||||
query={query}
|
query={query}
|
||||||
render={data => {
|
render={data => {
|
||||||
const meta = data.contentYaml
|
const meta = data.metaYaml
|
||||||
|
|
||||||
let headerClasses = classNames([styles.header], {
|
let headerClasses = classNames([styles.header], {
|
||||||
[styles.minimal]: minimal
|
[styles.minimal]: minimal
|
||||||
|
24
src/components/organisms/Repositories.jsx
Normal file
24
src/components/organisms/Repositories.jsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React, { PureComponent } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import Repository from '../molecules/Repository'
|
||||||
|
import styles from './Repositories.module.scss'
|
||||||
|
|
||||||
|
export default class Repositories extends PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
repos: PropTypes.array.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h1 className={styles.sectionTitle}>Open Source Projects</h1>
|
||||||
|
<div className={styles.repos}>
|
||||||
|
{this.props.repos.map(repo => (
|
||||||
|
<Repository key={repo.name} repo={repo} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
29
src/components/organisms/Repositories.module.scss
Normal file
29
src/components/organisms/Repositories.module.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
11
src/components/organisms/Repositories.test.jsx
Normal file
11
src/components/organisms/Repositories.test.jsx
Normal file
@ -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(<Repositories repos={repos} />)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
@ -2,7 +2,7 @@ import { useStaticQuery, graphql } from 'gatsby'
|
|||||||
|
|
||||||
const query = graphql`
|
const query = graphql`
|
||||||
query Meta {
|
query Meta {
|
||||||
contentYaml {
|
metaYaml {
|
||||||
title
|
title
|
||||||
tagline
|
tagline
|
||||||
description
|
description
|
||||||
@ -31,6 +31,6 @@ const query = graphql`
|
|||||||
`
|
`
|
||||||
|
|
||||||
export const useMeta = () => {
|
export const useMeta = () => {
|
||||||
const { contentYaml } = useStaticQuery(query)
|
const { metaYaml } = useStaticQuery(query)
|
||||||
return contentYaml
|
return metaYaml
|
||||||
}
|
}
|
||||||
|
3
src/images/star.svg
Normal file
3
src/images/star.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||||
|
<polygon points="22.136 19.625 32 12 20 12 16 0 12 12 0 12 9.875 19.594 6 32 16.016 24.32 26.008 32"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 196 B |
@ -16,8 +16,16 @@ describe('Home', () => {
|
|||||||
...projectImageFiles
|
...projectImageFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pageContext = {
|
||||||
|
repos: [
|
||||||
|
{
|
||||||
|
name: 'Hello'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
it('renders correctly from data file values', () => {
|
it('renders correctly from data file values', () => {
|
||||||
const { container } = render(<Home data={data} />)
|
const { container } = render(<Home data={data} pageContext={pageContext} />)
|
||||||
expect(container.firstChild).toBeInTheDocument()
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -5,6 +5,7 @@ import SEO from '../components/atoms/SEO'
|
|||||||
import ProjectImage from '../components/molecules/ProjectImage'
|
import ProjectImage from '../components/molecules/ProjectImage'
|
||||||
import { ReactComponent as Images } from '../images/images.svg'
|
import { ReactComponent as Images } from '../images/images.svg'
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
|
import Repositories from '../components/organisms/Repositories'
|
||||||
|
|
||||||
function getImageCount(images, slug) {
|
function getImageCount(images, slug) {
|
||||||
let array = []
|
let array = []
|
||||||
@ -19,12 +20,12 @@ function getImageCount(images, slug) {
|
|||||||
|
|
||||||
export default class Home extends PureComponent {
|
export default class Home extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
data: PropTypes.object,
|
data: PropTypes.object.isRequired,
|
||||||
location: PropTypes.object
|
pageContext: PropTypes.object.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data } = this.props
|
const { data, pageContext } = this.props
|
||||||
const projects = data.allProjectsYaml.edges
|
const projects = data.allProjectsYaml.edges
|
||||||
const images = data.projectImageFiles.edges
|
const images = data.projectImageFiles.edges
|
||||||
|
|
||||||
@ -56,6 +57,8 @@ export default class Home extends PureComponent {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Repositories repos={pageContext.repos} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,22 +5,23 @@ import { getLocationTimes } from '../utils/getLocationTimes'
|
|||||||
import { getCountry } from '../utils/getCountry'
|
import { getCountry } from '../utils/getCountry'
|
||||||
|
|
||||||
export default class AppProvider extends PureComponent {
|
export default class AppProvider extends PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.any.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
dark: false,
|
dark: false,
|
||||||
toggleDark: () => this.toggleDark(),
|
toggleDark: () => this.toggleDark(),
|
||||||
geolocation: null
|
geolocation: null
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
children: PropTypes.any.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
store = typeof localStorage === 'undefined' ? null : localStorage
|
store = typeof localStorage === 'undefined' ? null : localStorage
|
||||||
|
|
||||||
mounted = false
|
mounted = false
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
this.mounted = true
|
this.mounted = true
|
||||||
|
|
||||||
const geolocation = await getCountry()
|
const geolocation = await getCountry()
|
||||||
this.setState({ geolocation })
|
this.setState({ geolocation })
|
||||||
this.checkDark()
|
this.checkDark()
|
||||||
@ -30,16 +31,12 @@ export default class AppProvider extends PureComponent {
|
|||||||
this.mounted = false
|
this.mounted = false
|
||||||
}
|
}
|
||||||
|
|
||||||
setDark() {
|
setDark(dark) {
|
||||||
this.mounted && this.setState({ dark: true })
|
this.mounted && this.setState({ dark })
|
||||||
}
|
|
||||||
|
|
||||||
setLight() {
|
|
||||||
this.mounted && this.setState({ dark: false })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
darkLocalStorageMode(darkLocalStorage) {
|
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 now = new Date().getHours()
|
||||||
const weWantItDarkTimes = now >= sunset || now <= sunrise
|
const weWantItDarkTimes = now >= sunset || now <= sunrise
|
||||||
|
|
||||||
!dark && weWantItDarkTimes ? this.setDark() : this.setLight()
|
!dark && weWantItDarkTimes ? this.setDark(true) : this.setDark(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkDark() {
|
async checkDark() {
|
||||||
|
@ -67,6 +67,7 @@ $color-headings--dark: $brand-main-light;
|
|||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
|
|
||||||
$spacer: ($font-size-base * $line-height);
|
$spacer: ($font-size-base * $line-height);
|
||||||
|
$border-radius: .25rem;
|
||||||
|
|
||||||
// Responsive breakpoints
|
// Responsive breakpoints
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
|
Loading…
x
Reference in New Issue
Block a user