diff --git a/gatsby-config.js b/gatsby-config.js index 65715d9f..2096bb54 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -17,6 +17,10 @@ module.exports = { { key: 'guides', name: '📚 Guias' }, { key: 'archive', name: '📥 Arquivo' }, ], + github: { + owner: "leic-pt", + repository: "resumos-leic", + }, navbar: { siteTitle: 'Resumos LEIC-A', links: [ @@ -49,6 +53,7 @@ module.exports = { path: `${__dirname}/content`, }, }, + `gatsby-source-github-contributors`, `gatsby-plugin-image`, `gatsby-plugin-sharp`, { diff --git a/package.json b/package.json index 04051966..22a81854 100755 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "license": "GPL-3.0", "devDependencies": { + "@octokit/graphql": "^7.0.2", "prettier": "^3.0.0" }, "dependencies": { diff --git a/plugins/gatsby-source-github-contributors/gatsby-node.js b/plugins/gatsby-source-github-contributors/gatsby-node.js new file mode 100644 index 00000000..0bacb410 --- /dev/null +++ b/plugins/gatsby-source-github-contributors/gatsby-node.js @@ -0,0 +1,101 @@ +const { graphql } = require("@octokit/graphql"); + +// mutates `contributors` +function mergeContributors(contributors, pullRequests) { + pullRequests.filter(({author}) => !!author.login).forEach(({author, labels}) => { + const { name, login } = author; + const contributor = contributors[login] || (contributors[login] = {}); + const labelSet = contributor.labels || (contributor.labels = new Set()); + + labels.nodes.filter(label => !!label.name).forEach(label => labelSet.add(label.name)); + + if (!contributor.name) { + contributor.name = name; + } + }) +} + +exports.sourceNodes = async ({ + actions, + cache, + createContentDigest, + createNodeId, + getNodesByType, +}) => { + const { createNode, touchNode } = actions + + const githubToken = process.env.GITHUB_TOKEN; + if (!githubToken) { + console.info("No GitHub token found (GITHUB_TOKEN env var), skipping contributors list"); + return; + } + + const graphqlGh = graphql.defaults({ + headers: { + authorization: `bearer ${githubToken}`, + } + }); + + const contributors = {}; + + // TODO cache old pagination + // const lastUpdated = await cache.get("lastUpdated") + // // 30 days cache + // if (lastUpdated > Date.now() - 1000 * 60 * 60 * 24 * 30) { + // getNodesByType("Space").forEach(node => touchNode(node)) + // return + // } + + // await cache.set("lastUpdated", Date.now()) + + const getPullRequests = async (cursor) => { + const { repository } = await graphqlGh(` + query fetchPullRequests($cursor: String) { + repository(owner: "leic-pt", name: "resumos-leic") { + pullRequests(first: 10, after: $cursor, states: [MERGED], orderBy: {field: UPDATED_AT, direction: DESC}) { + pageInfo { + endCursor + hasNextPage + } + nodes { + updatedAt + labels (first: 50) { + nodes { + name + } + } + author { + ... on User { + name + login + } + } + } + } + } + }`); + + return repository?.pullRequests + } + + mergeContributors(contributors, (await getPullRequests(null)).nodes); + + Object.entries(contributors).forEach(([username, contributor]) => { + const data = { + username, + ...contributor, + name: contributor.name || username, + labels: [...(contributor.labels || [])], + }; + + createNode({ + ...data, + id: createNodeId(data.username), + internal: { + type: "Contributor", + content: JSON.stringify(data), + contentDigest: createContentDigest(data), + }, + }) + }); +} diff --git a/plugins/gatsby-source-github-contributors/package.json b/plugins/gatsby-source-github-contributors/package.json new file mode 100644 index 00000000..00dbedb3 --- /dev/null +++ b/plugins/gatsby-source-github-contributors/package.json @@ -0,0 +1,4 @@ +{ + "name": "gatsby-source-github-contributors", + "description": "Fetch GitHub contributors for this repository" +} diff --git a/src/components/Contributors.js b/src/components/Contributors.js new file mode 100644 index 00000000..e5af2028 --- /dev/null +++ b/src/components/Contributors.js @@ -0,0 +1,60 @@ +import { Link, useStaticQuery, graphql } from 'gatsby'; +import React, { useMemo } from 'react'; +import ExternalLink from './ExternalLink'; +import SearchBar from './SearchBar'; +import ThemeSettings from './ThemeSettings'; + +const Contributors = ({ getLabelName, contributors }) => { + const data = useStaticQuery(graphql` + query ContributorsQuery { + site { + siteMetadata { + github { + owner + repository + } + } + } + } + `); + + const {owner, repository} = data.site.siteMetadata.github; + + const sortedContributors = useMemo(() => { + return contributors.map(contributor => { + const labels = contributor.labels + .map(getLabelName) + .filter(Boolean) + .map(label => ({ label, bold: false })); + return { + username: contributor.username, + name: contributor.name, + labels, + boldCount: labels.filter(({bold}) => bold).length, + } + }) + .sort((a,b) => a.name.localeCompare(b.name)) + .sort((a,b) => b.boldCount - a.boldCount) + .sort((a,b) => b.labels.length - a.labels.length) + }, [contributors, getLabelName]) + + return ( + + ); +}; + +export default Contributors; diff --git a/src/components/HomePageLayout.js b/src/components/HomePageLayout.js index bf6402e5..ff048217 100644 --- a/src/components/HomePageLayout.js +++ b/src/components/HomePageLayout.js @@ -1,5 +1,5 @@ import { graphql } from 'gatsby'; -import React from 'react'; +import React, { useCallback } from 'react'; import IstLogo from '../images/ist-logo.svg'; import '../styles/homepage.css'; import '../styles/main.css'; @@ -8,9 +8,25 @@ import Footer from './Footer'; import Navbar from './Navbar'; import PageMetadata from './PageMetadata'; import SectionButton, { SectionButtonLayout } from './SectionButton'; +import Contributors from './Contributors'; const HomePageLayout = ({ data }) => { - const { markdownRemark: page } = data; + const { markdownRemark: page, allContributor: contributors } = data; + + const getLabelName = useCallback((label) => { + const { years } = page.frontmatter; + + for (const year of years) { + for (const semester of year.semesters) { + for (const course of semester.courses) { + if (course.link === `/${label}`) { + return course.name; + } + } + } + } + }, [page.frontmatter.years]); + return (
@@ -47,7 +63,10 @@ const HomePageLayout = ({ data }) => {
))} -
+
+
+ +
); @@ -80,6 +99,13 @@ export const pageQuery = graphql` } } } + allContributor { + nodes { + username + name + labels + } + } } `; diff --git a/yarn.lock b/yarn.lock index 8ee56804..7f9367a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1860,6 +1860,54 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@octokit/endpoint@^9.0.0": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-9.0.4.tgz#8afda5ad1ffc3073d08f2b450964c610b821d1ea" + integrity sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw== + dependencies: + "@octokit/types" "^12.0.0" + universal-user-agent "^6.0.0" + +"@octokit/graphql@^7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-7.0.2.tgz#3df14b9968192f9060d94ed9e3aa9780a76e7f99" + integrity sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q== + dependencies: + "@octokit/request" "^8.0.1" + "@octokit/types" "^12.0.0" + universal-user-agent "^6.0.0" + +"@octokit/openapi-types@^19.1.0": + version "19.1.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-19.1.0.tgz#75ec7e64743870fc73e1ab4bc6ec252ecdd624dc" + integrity sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw== + +"@octokit/request-error@^5.0.0": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-5.0.1.tgz#277e3ce3b540b41525e07ba24c5ef5e868a72db9" + integrity sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ== + dependencies: + "@octokit/types" "^12.0.0" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request@^8.0.1": + version "8.1.6" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-8.1.6.tgz#a76a859c30421737a3918b40973c2ff369009571" + integrity sha512-YhPaGml3ncZC1NfXpP3WZ7iliL1ap6tLkAp6MvbK2fTTPytzVUyUesBBogcdMm86uRYO5rHaM1xIWxigWZ17MQ== + dependencies: + "@octokit/endpoint" "^9.0.0" + "@octokit/request-error" "^5.0.0" + "@octokit/types" "^12.0.0" + universal-user-agent "^6.0.0" + +"@octokit/types@^12.0.0": + version "12.4.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-12.4.0.tgz#8f97b601e91ce6b9776ed8152217e77a71be7aac" + integrity sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ== + dependencies: + "@octokit/openapi-types" "^19.1.0" + "@parcel/bundler-default@2.8.3": version "2.8.3" resolved "https://registry.yarnpkg.com/@parcel/bundler-default/-/bundler-default-2.8.3.tgz#d64739dbc2dbd59d6629861bf77a8083aced5229" @@ -4775,6 +4823,11 @@ dependency-graph@^0.11.0: resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27" integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg== +deprecation@^2.0.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" @@ -11866,6 +11919,11 @@ unist-util-visit@^4.0.0: unist-util-is "^5.0.0" unist-util-visit-parents "^5.1.1" +universal-user-agent@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.1.tgz#15f20f55da3c930c57bddbf1734c6654d5fd35aa" + integrity sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ== + universalify@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"