Commit 411d8f1f authored by anuraghazra's avatar anuraghazra
Browse files

feat: added Top languages card

parent 938df3f7
Loading
Loading
Loading
Loading

api/top-langs.js

0 → 100644
+44 −0
Original line number Diff line number Diff line
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,
    })
  );
};
+84 −0
Original line number Diff line number Diff line
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;
+67 −0
Original line number Diff line number Diff line
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 `
    <text x="2" y="15" class="lang-name">${name}</text>
    <text x="${progressTextX}" y="34" class="lang-name">${progress}%</text>
    <svg width="${progressWidth}">
      <rect rx="5" ry="5" x="0" y="25" width="${progressWidth}" height="8" fill="#ddd"></rect>
      <rect rx="5" ry="5" x="0" y="25" width="${progressPercentage}%" height="8" fill="${color}"></rect>
    </svg>
  `;
};

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 `
    <svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
      <style>
        .header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor} }
        .lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
      </style>
      <rect data-testid="card-bg" x="0.5" y="0.5" width="99.7%" height="99%" rx="4.5" fill="${bgColor}" stroke="#E4E2E2"/>

      <text x="25" y="35" class="header">Top Languages</text>

      <svg x="25" y="55">
      ${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",
      })}
      </svg>
    </svg>
  `;
};

module.exports = renderTopLanguages;