diff --git a/layouts/css/_utils.scss b/layouts/css/_utils.scss index 47bb272352fe8..92b230207c0d9 100644 --- a/layouts/css/_utils.scss +++ b/layouts/css/_utils.scss @@ -25,3 +25,7 @@ white-space: nowrap; border: none; } + +.hidden { + display: none; +} diff --git a/layouts/css/page-modules/_contributor-card.scss b/layouts/css/page-modules/_contributor-card.scss new file mode 100644 index 0000000000000..699608986bed2 --- /dev/null +++ b/layouts/css/page-modules/_contributor-card.scss @@ -0,0 +1,32 @@ +.contributor-card { + display: flex; + align-items: center; + min-height: 42px; + width: 300px; + padding: 1.5em 1em; + margin: 1em auto; + border: 1px solid $white; + border-radius: 3px; + + > a { + height: 40px; + width: 40px; + flex: 0 0 auto; + } + + p { + padding-left: 1em; + margin: 0; + flex: 1 1 1px; + } + + .spinner-border { + margin: 5px; + } +} + +@media (max-width: 350px) { + .contributor-card { + width: auto; + } +} diff --git a/layouts/css/page-modules/_header.scss b/layouts/css/page-modules/_header.scss index 955a41604d85e..1f173bc48a22d 100644 --- a/layouts/css/page-modules/_header.scss +++ b/layouts/css/page-modules/_header.scss @@ -52,10 +52,6 @@ header { margin: 0; padding: 0; - &.hidden { - display: none; - } - a { color: $light-gray2; } diff --git a/layouts/css/page-modules/_jsfoundation.scss b/layouts/css/page-modules/_jsfoundation.scss index 02a34906accd4..588c3efb4115e 100644 --- a/layouts/css/page-modules/_jsfoundation.scss +++ b/layouts/css/page-modules/_jsfoundation.scss @@ -22,6 +22,21 @@ margin-left: auto; } +.thanking-contributor { + max-width: 300px; + display: flex; + align-items: center; + padding: .5em 1em; + margin-top: 1em; + border: 1px solid $white; + border-radius: 3px; + + img { + border-radius: 50%; + margin-right: 1em; + } +} + @media screen and (max-width: 700px) { .issue-link-container { flex-wrap: wrap; diff --git a/layouts/css/page-modules/spinner.scss b/layouts/css/page-modules/spinner.scss new file mode 100644 index 0000000000000..b3756d56dc702 --- /dev/null +++ b/layouts/css/page-modules/spinner.scss @@ -0,0 +1,18 @@ +// borrowed from Bootstrap +@keyframes spinner-border { + to { + transform: rotate(360deg); + } +} + +.spinner-border { + box-sizing: border-box; + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: text-bottom; + border: .25em solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: spinner-border .75s linear infinite; +} diff --git a/layouts/css/styles.scss b/layouts/css/styles.scss index de05e1251db62..971838040c6ee 100644 --- a/layouts/css/styles.scss +++ b/layouts/css/styles.scss @@ -21,6 +21,8 @@ @import "page-modules/prev-next-navigation"; @import "page-modules/release-schedule"; @import "page-modules/resources"; +@import "page-modules/contributor-card"; +@import "page-modules/spinner"; @import "vendor/prism-tomorrow"; article a { diff --git a/layouts/partials/contributor-card.hbs b/layouts/partials/contributor-card.hbs new file mode 100644 index 0000000000000..ea68ef996eb92 --- /dev/null +++ b/layouts/partials/contributor-card.hbs @@ -0,0 +1,15 @@ +<div class="contributor-card"> + <a href="#" rel="nofollow noopener noreferrer"> + <div class="spinner-border" role="status"> + <span class="sr-only">Loading...</span> + </div> + <img id="contributor-avatar" class="hidden" src="/static/images/logos/js-green.svg" alt="Avatar of a Node.js contributor" width="40" height="40"> + </a> + + <p> + Thank you <a href="#" id="contributor-username" rel="nofollow noopener noreferrer">username</a> for being a <a href="https://github.com/nodejs/node/graphs/contributors" rel="nofollow noopener noreferrer" title="List of all Node.js contributors">Node.js contributor</a> + <a href="https://github.com/nodejs/node/graphs/contributors" rel="nofollow noopener noreferrer"> + <strong id="contributor-contributions">0 contributions</strong> + </a> + </p> +</div> diff --git a/layouts/partials/footer.hbs b/layouts/partials/footer.hbs index 8d074ee2dd7f0..f11b640b7156e 100644 --- a/layouts/partials/footer.hbs +++ b/layouts/partials/footer.hbs @@ -20,6 +20,9 @@ <p> <a href="https://raw.githubusercontent.com/nodejs/node/master/LICENSE">Node.js Project Licensing Information</a>. </p> + + {{> contributor-card }} + </div> </div> diff --git a/layouts/partials/html-head.hbs b/layouts/partials/html-head.hbs index 6b330d9569fe7..bcbdc0f869c57 100644 --- a/layouts/partials/html-head.hbs +++ b/layouts/partials/html-head.hbs @@ -4,6 +4,7 @@ <link rel="dns-prefetch" href="https://fonts.googleapis.com"> <link rel="dns-prefetch" href="https://fonts.gstatic.com"> + <link rel="dns-prefetch" href="https://api.github.com"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600&display=fallback"> <link rel="stylesheet" href="/static/css/styles.css"> diff --git a/static/js/main.js b/static/js/main.js index ba994763a3a15..b66d44b671d02 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -49,6 +49,135 @@ }) })() +;(function () { + var contributorCard = document.querySelector('.contributor-card') + + if (!contributorCard) { + return + } + + var contributorAvatar = contributorCard.querySelector('#contributor-avatar') + var contributorUsername = contributorCard.querySelector('#contributor-username') + var contributorContributions = contributorCard.querySelector('#contributor-contributions') + var loadingSpinner = contributorCard.querySelector('.spinner-border') + + if (window.IntersectionObserver) { + var observer = new window.IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.intersectionRatio > 0.5) { + // In viewport, fetch a random contributor + fetchRandomContributor() + + observer.unobserve(entry.target) + } + }) + }, + { threshold: 0.5 } + ) + + observer.observe(document.querySelector('footer')) + } else { + // Does not support IntersectionObserver + fetchRandomContributor() + } + + function fetchRandomContributor () { + var maxContributors + var fetchDate + var needToRefetch = false + + if (window.localStorage) { + maxContributors = window.localStorage.getItem('max_contributors') + fetchDate = parseInt(window.localStorage.getItem('fetch_date'), 10) + } + + // If fetch date is a month old (2592000000 ms === 30 days) + if (Date.now() - fetchDate >= 2592000000) { + needToRefetch = true + } + + // If localStorage and data is less than 1 month old, fetch 1 time + if (maxContributors && !needToRefetch) { + getContributor(Math.floor(Math.random() * Math.floor(parseInt(maxContributors))) + 1) + } else { + getMaxContributors(function (randomPage, lastPage) { + getContributor(randomPage) + + if (window.localStorage) { + window.localStorage.setItem('max_contributors', lastPage) + } + }) + } + } + + function getMaxContributors (callback) { + var xhr = new window.XMLHttpRequest() + xhr.open('GET', 'https://api.github.com/repos/nodejs/node/contributors?per_page=1', true) + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + // Get Headers Links last page to generate a random contributor + var links = linkParser(xhr.getResponseHeader('Link')) + var randomPage = Math.floor(Math.random() * Math.floor(parseInt(links.last.page, 10))) + 1 + + if (window.localStorage) { + window.localStorage.setItem('fetch_date', Date.now()) + } + callback(randomPage, links.last.page) + } else { + return contributorCard.parentNode.removeChild(contributorCard) + } + } + } + + xhr.send() + } + + function getContributor (randomPage) { + var xhr = new window.XMLHttpRequest() + xhr.open('GET', 'https://api.github.com/repos/nodejs/node/contributors?per_page=1&page=' + randomPage, true) + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + var contributor = JSON.parse(xhr.responseText)[0] + + // Remove loading spinner and show avatar + loadingSpinner.parentNode.removeChild(loadingSpinner) + contributorAvatar.classList.remove('hidden') + // Set new values + contributorAvatar.src = contributor.avatar_url + '&s=80' + contributorAvatar.parentElement.href = contributor.html_url + contributorUsername.textContent = contributor.login + contributorUsername.href = contributor.html_url + contributorContributions.textContent = contributor.contributions + ' contributions' + contributorContributions.parentElement.href = 'https://github.com/nodejs/node/commits?author=' + contributor.login + } else { + return contributorCard.parentNode.removeChild(contributorCard) + } + } + } + + xhr.send() + } + + function linkParser (linkHeader) { + var regex = /<([^?]+\?per_page=1&[a-z]+=([\d]+))>;[\s]*rel="([a-z]+)"/g + var array = [] + var object = {} + + while ((array = regex.exec(linkHeader)) !== null) { + object[array[3]] = { + url: array[1], + page: array[2] + } + } + + return object + } +})() + ;(function (d, n) { 'use strict'