You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

181 lines
8.8 KiB

4 years ago
  1. "use strict";
  2. const parse = require("github-calendar-parser")
  3. , $ = require("elly")
  4. , addSubtractDate = require("add-subtract-date")
  5. , formatoid = require("formatoid")
  6. const DATE_FORMAT1 = "MMM D, YYYY"
  7. , DATE_FORMAT2 = "MMMM D"
  8. const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
  9. function printDayCount(dayCount) {
  10. return `${dayCount} ${(dayCount === 1) ? "day" : "days"}`
  11. }
  12. function addTooltips(container) {
  13. const tooltip = document.createElement("div")
  14. tooltip.classList.add("day-tooltip")
  15. container.appendChild(tooltip)
  16. // Add mouse event listener to show & hide tooltip
  17. const days = container.querySelectorAll("rect.day")
  18. days.forEach(day => {
  19. day.addEventListener("mouseenter", (e) => {
  20. let contribCount = e.target.getAttribute("data-count")
  21. if (contribCount === "0") {
  22. contribCount = "No contributions"
  23. } else if (contribCount === "1") {
  24. contribCount = "1 contribution"
  25. } else {
  26. contribCount = `${contribCount} contributions`
  27. }
  28. const date = new Date(e.target.getAttribute("data-date"))
  29. const dateText = `${MONTH_NAMES[date.getUTCMonth()]} ${date.getUTCDate()}, ${date.getUTCFullYear()}`
  30. tooltip.innerHTML = `<strong>${contribCount}</strong> on ${dateText}`
  31. tooltip.classList.add("is-visible")
  32. const size = e.target.getBoundingClientRect()
  33. , leftPos = size.left + window.pageXOffset - tooltip.offsetWidth / 2 + size.width / 2
  34. , topPos = size.bottom + window.pageYOffset - tooltip.offsetHeight - 2 * size.height
  35. tooltip.style.top = `${topPos}px`
  36. tooltip.style.left = `${leftPos}px`
  37. })
  38. day.addEventListener("mouseleave", () => {
  39. tooltip.classList.remove("is-visible")
  40. })
  41. })
  42. }
  43. /**
  44. * GitHubCalendar
  45. * Brings the contributions calendar from GitHub (provided username) into your page.
  46. *
  47. * @name GitHubCalendar
  48. * @function
  49. * @param {String|HTMLElement} container The calendar container (query selector or the element itself).
  50. * @param {String} username The GitHub username.
  51. * @param {Object} options An object containing the following fields:
  52. *
  53. * - `summary_text` (String): The text that appears under the calendar (defaults to: `"Summary of
  54. * pull requests, issues opened, and commits made by <username>"`).
  55. * - `proxy` (Function): A function that receives as argument the username (string) and should return a promise resolving the HTML content of the contributions page.
  56. * The default is using @Bloggify's APIs.
  57. * - `global_stats` (Boolean): If `false`, the global stats (total, longest and current streaks) will not be calculated and displayed. By default this is enabled.
  58. * - `responsive` (Boolean): If `true`, the graph is changed to scale with the container. Custom CSS should be applied to the element to scale it appropriately. By default this is disabled.
  59. * - `tooltips` (Boolean): If `true`, tooltips will be shown when hovered over calendar days. By default this is disabled.
  60. * - `cache` (Number) The cache time in seconds.
  61. *
  62. * @return {Promise} A promise returned by the `fetch()` call.
  63. */
  64. module.exports = function GitHubCalendar (container, username, options) {
  65. container = $(container)
  66. options = options || {}
  67. options.summary_text = options.summary_text || `Summary of pull requests, issues opened, and commits made by <a href="https://github.com/${username}" target="blank">@${username}</a>`
  68. options.cache = (options.cache || (24 * 60 * 60)) * 1000
  69. if (options.global_stats === false) {
  70. container.style.minHeight = "175px"
  71. }
  72. const cacheKeys = {
  73. content: `gh_calendar_content.${username}`,
  74. expire_at: `gh_calendar_expire.${username}`
  75. }
  76. // We need a proxy for CORS
  77. options.proxy = options.proxy || (username => {
  78. return fetch(`https://api.bloggify.net/gh-calendar/?username=${username}`).then(r => r.text())
  79. })
  80. options.getCalendar = options.getCalendar || (username => {
  81. if (options.cache && Date.now() < +localStorage.getItem(cacheKeys.expire_at)) {
  82. const content = localStorage.getItem(cacheKeys.content)
  83. if (content) {
  84. return Promise.resolve(content)
  85. }
  86. }
  87. return options.proxy(username).then(body => {
  88. if (options.cache) {
  89. localStorage.setItem(cacheKeys.content, body)
  90. localStorage.setItem(cacheKeys.expire_at, Date.now() + options.cache)
  91. }
  92. return body
  93. })
  94. })
  95. let fetchCalendar = () => options.getCalendar(username).then(body => {
  96. let div = document.createElement("div")
  97. div.innerHTML = body
  98. let cal = div.querySelector(".js-yearly-contributions")
  99. $(".position-relative h2", cal).remove()
  100. cal.querySelector(".float-left.text-gray").innerHTML = options.summary_text
  101. // If 'include-fragment' with spinner img loads instead of the svg, fetchCalendar again
  102. if (cal.querySelector("include-fragment")) {
  103. setTimeout(fetchCalendar, 500)
  104. } else {
  105. // If options includes responsive, SVG element has to be manipulated to be made responsive
  106. if (options.responsive === true) {
  107. let svg = cal.querySelector("svg.js-calendar-graph-svg")
  108. // Get the width/height properties and use them to create the viewBox
  109. let width = svg.getAttribute("width")
  110. let height = svg.getAttribute("height")
  111. // Remove height property entirely
  112. svg.removeAttribute("height")
  113. // Width property should be set to 100% to fill entire container
  114. svg.setAttribute("width", "100%")
  115. // Add a viewBox property based on the former width/height
  116. svg.setAttribute("viewBox", "0 0 " + width + " " + height)
  117. }
  118. if (options.global_stats !== false) {
  119. let parsed = parse($("svg", cal).outerHTML)
  120. , currentStreakInfo = parsed.current_streak
  121. ? `${formatoid(parsed.current_streak_range[0], DATE_FORMAT2)} &ndash; ${formatoid(parsed.current_streak_range[1], DATE_FORMAT2)}`
  122. : parsed.last_contributed
  123. ? `Last contributed in ${formatoid(parsed.last_contributed, DATE_FORMAT2)}.`
  124. : "Rock - Hard Place"
  125. , longestStreakInfo = parsed.longest_streak
  126. ? `${formatoid(parsed.longest_streak_range[0], DATE_FORMAT2)} &ndash; ${formatoid(parsed.longest_streak_range[1], DATE_FORMAT2)}`
  127. : parsed.last_contributed
  128. ? `Last contributed in ${formatoid(parsed.last_contributed, DATE_FORMAT2)}.`
  129. : "Rock - Hard Place"
  130. , firstCol = $("<div>", {
  131. "class": "contrib-column contrib-column-first table-column"
  132. , html: `<span class="text-muted">Contributions in the last year</span>
  133. <span class="contrib-number">${parsed.last_year} total</span>
  134. <span class="text-muted">${formatoid(addSubtractDate.add(addSubtractDate.subtract(new Date(), 1, "year"), 1, "day"), DATE_FORMAT1)} &ndash; ${formatoid(new Date(), DATE_FORMAT1)}</span>`
  135. })
  136. , secondCol = $("<div>", {
  137. "class": "contrib-column table-column"
  138. , html: `<span class="text-muted">Longest streak</span>
  139. <span class="contrib-number">${printDayCount(parsed.longest_streak)}</span>
  140. <span class="text-muted">${longestStreakInfo}</span>`
  141. })
  142. , thirdCol = $("<div>", {
  143. "class": "contrib-column table-column"
  144. , html: `<span class="text-muted">Current streak</span>
  145. <span class="contrib-number">${printDayCount(parsed.current_streak)}</span>
  146. <span class="text-muted">${currentStreakInfo}</span>`
  147. })
  148. cal.appendChild(firstCol)
  149. cal.appendChild(secondCol)
  150. cal.appendChild(thirdCol)
  151. }
  152. container.innerHTML = cal.innerHTML
  153. // If options includes tooltips, add tooltips listeners to SVG
  154. if (options.tooltips === true) {
  155. addTooltips(container)
  156. }
  157. }
  158. }).catch(e => console.error(e))
  159. return fetchCalendar()
  160. }