1
0
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:
Matthias Kretschmann 2019-05-27 00:20:46 +02:00 committed by GitHub
commit 41c151971c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 486 additions and 75 deletions

View File

@ -1,7 +1,6 @@
dist: xenial dist: xenial
language: node_js language: node_js
node_js: node_js: node
- '11'
cache: cache:
directories: directories:

View File

@ -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.

View File

@ -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
View 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

View File

@ -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: {

View File

@ -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

View File

@ -1,5 +1,5 @@
{ {
"contentYaml": { "metaYaml": {
"title": "Matthias Kretschmann", "title": "Matthias Kretschmann",
"tagline": "Designer & Developer", "tagline": "Designer & Developer",
"description": "Portfolio of web &amp; ui designer/developer hybrid Matthias Kretschmann.", "description": "Portfolio of web &amp; ui designer/developer hybrid Matthias Kretschmann.",

View 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"
}
]

View File

@ -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",

View File

@ -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 (
<> <>

View File

@ -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);

View File

@ -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
} }

View File

@ -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

View File

@ -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 && (

View File

@ -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()

View File

@ -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()
}) })

View File

@ -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;

View File

@ -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.',

View File

@ -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}>

View File

@ -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()

View File

@ -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()

View File

@ -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;
} }

View File

@ -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;

View 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

View 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;
}
}

View 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)
})
})

View File

@ -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} />
}} }}

View File

@ -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

View 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>
)
}
}

View 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;
}
}

View 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()
})
})

View File

@ -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
View 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

View File

@ -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()
}) })
}) })

View File

@ -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} />
</> </>
) )
} }

View File

@ -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() {

View File

@ -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
///////////////////////////////////// /////////////////////////////////////