diff --git a/api/top-langs.js b/api/top-langs.js new file mode 100644 index 00000000..5899d9de --- /dev/null +++ b/api/top-langs.js @@ -0,0 +1,44 @@ +require("dotenv").config(); +const { renderError, clampValue, CONSTANTS } = require("../src/utils"); +const fetchTopLanguages = require("../src/fetchTopLanguages"); +const renderTopLanguages = require("../src/renderTopLanguages"); + +module.exports = async (req, res) => { + const { + username, + card_width, + title_color, + text_color, + bg_color, + theme, + cache_seconds, + } = req.query; + let topLangs; + + res.setHeader("Content-Type", "image/svg+xml"); + + try { + topLangs = await fetchTopLanguages(username); + } catch (err) { + return res.send(renderError(err.message)); + } + + const cacheSeconds = clampValue( + parseInt(cache_seconds || CONSTANTS.THIRTY_MINUTES, 10), + CONSTANTS.THIRTY_MINUTES, + CONSTANTS.ONE_DAY + ); + + res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`); + + res.send( + renderTopLanguages(topLangs, { + theme, + card_width: parseInt(card_width, 10), + title_color, + text_color, + bg_color, + theme, + }) + ); +}; diff --git a/src/fetchTopLanguages.js b/src/fetchTopLanguages.js new file mode 100644 index 00000000..95c9b483 --- /dev/null +++ b/src/fetchTopLanguages.js @@ -0,0 +1,84 @@ +const { request } = require("./utils"); +const retryer = require("./retryer"); +require("dotenv").config(); + +const fetcher = (variables, token) => { + return request( + { + query: ` + query userInfo($login: String!) { + user(login: $login) { + repositories(isFork: false, first: 100) { + nodes { + languages(first: 1) { + edges { + size + node { + color + name + } + } + } + } + } + } + } + `, + variables, + }, + { + Authorization: `bearer ${token}`, + } + ); +}; + +async function fetchTopLanguages(username) { + if (!username) throw Error("Invalid username"); + + let res = await retryer(fetcher, { login: username }); + + if (res.data.errors) { + console.log(res.data.errors); + throw Error(res.data.errors[0].message || "Could not fetch user"); + } + + let repoNodes = res.data.data.user.repositories.nodes; + + // TODO: perf improvement + repoNodes = repoNodes + .filter((node) => { + return node.languages.edges.length > 0; + }) + .sort((a, b) => { + return b.languages.edges[0].size - a.languages.edges[0].size; + }) + .map((node) => { + return node.languages.edges[0]; + }) + .reduce((acc, prev) => { + let langSize = prev.size; + if (acc[prev.node.name] && prev.node.name === acc[prev.node.name].name) { + langSize = prev.size + acc[prev.node.name].size; + } + + return { + ...acc, + [prev.node.name]: { + name: prev.node.name, + color: prev.node.color, + size: langSize, + }, + }; + }, {}); + + const topLangs = Object.keys(repoNodes) + .slice(0, 5) + .reduce((result, key) => { + result[key] = repoNodes[key]; + return result; + }, {}); + + return topLangs; +} + +module.exports = fetchTopLanguages; diff --git a/src/renderTopLanguages.js b/src/renderTopLanguages.js new file mode 100644 index 00000000..608afbeb --- /dev/null +++ b/src/renderTopLanguages.js @@ -0,0 +1,67 @@ +const { getCardColors, FlexLayout, clampValue } = require("../src/utils"); + +const createProgressNode = ({ width, color, name, progress }) => { + const paddingRight = 95; + const progressTextX = width - paddingRight + 10; + const progressWidth = width - paddingRight; + const progressPercentage = clampValue(progress, 2, 100); + + return ` + ${name} + ${progress}% + + + + + `; +}; + +const renderTopLanguages = (topLangs, options = {}) => { + const { title_color, text_color, bg_color, theme, card_width } = options; + + const langs = Object.values(topLangs); + + const totalSize = langs.reduce((acc, curr) => { + return acc + curr.size; + }, 0); + + // returns theme based colors with proper overrides and defaults + const { titleColor, textColor, bgColor } = getCardColors({ + title_color, + text_color, + bg_color, + theme, + }); + + const width = isNaN(card_width) ? 300 : card_width; + const height = 45 + (langs.length + 1) * 40; + + return ` + + + + + Top Languages + + + ${FlexLayout({ + items: langs.map((lang) => { + return createProgressNode({ + width: width, + name: lang.name, + color: lang.color || "#858585", + progress: ((lang.size / totalSize) * 100).toFixed(2), + }); + }), + gap: 40, + direction: "column", + })} + + + `; +}; + +module.exports = renderTopLanguages;