Unverified Commit ad87824f authored by Anurag Hazra's avatar Anurag Hazra Committed by GitHub
Browse files

Merge pull request #128 from anuraghazra/top-langs

feat: added Top languages card
parents 938df3f7 bc7a9857
Loading
Loading
Loading
Loading

api/top-langs.js

0 → 100644
+53 −0
Original line number Diff line number Diff line
require("dotenv").config();
const {
  renderError,
  clampValue,
  parseBoolean,
  CONSTANTS,
} = require("../src/utils");
const fetchTopLanguages = require("../src/fetchTopLanguages");
const renderTopLanguages = require("../src/renderTopLanguages");

module.exports = async (req, res) => {
  const {
    username,
    hide_langs_below,
    hide_title,
    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,
      hide_title: parseBoolean(hide_title),
      card_width: parseInt(card_width, 10),
      hide_langs_below: parseFloat(hide_langs_below, 10),
      title_color,
      text_color,
      bg_color,
      theme,
    })
  );
};
+72 −38
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@

- [GitHub Stats Card](#github-stats-card)
- [GitHub Extra Pins](#github-extra-pins)
- [Top Languages Card](#top-languages-card)
- [Themes](#themes)
- [Customization](#customization)
- [Deploy Yourself](#deploy-on-your-own-vercel-instance)
@@ -93,27 +94,80 @@ You can customize the appearance of your `Stats Card` or `Repo Card` however you

Customization Options:

| Option        | type      | description                          | Stats Card (default) | Repo Card (default) |
| ------------- | --------- | ------------------------------------ | -------------------- | ------------------- |
| title_color   | hex color | title color                          | 2f80ed               | 2f80ed              |
| text_color    | hex color | body color                           | 333                  | 333                 |
| icon_color    | hex color | icon color                           | 4c71f2               | 586069              |
| bg_color      | hex color | card bg color                        | FFFEFE               | FFFEFE              |
| line_height   | number    | control the line-height between text | 30                   | N/A                 |
| hide_rank     | boolean   | hides the ranking                    | false                | N/A                 |
| hide_title    | boolean   | hides the stats title                | false                | N/A                 |
| hide_border   | boolean   | hides the stats card border          | false                | N/A                 |
| show_owner    | boolean   | shows owner name in repo card        | N/A                  | false               |
| show_icons    | boolean   | shows icons                          | false                | N/A                 |
| theme         | string    | sets inbuilt theme                   | 'default'            | 'default_repocard'  |
| cache_seconds | number    | manually set custom cache control    | 1800                 | 1800                |
| Option           | type      | description                                    | Stats Card (default) | Repo Card (default) | Top Lang Card (default) |
| ---------------- | --------- | ---------------------------------------------- | -------------------- | ------------------- | ----------------------- |
| title_color      | hex color | title color                                    | 2f80ed               | 2f80ed              | 2f80ed                  |
| text_color       | hex color | body color                                     | 333                  | 333                 | 333                     |
| icon_color       | hex color | icon color                                     | 4c71f2               | 586069              | 586069                  |
| bg_color         | hex color | card bg color                                  | FFFEFE               | FFFEFE              | FFFEFE                  |
| line_height      | number    | control the line-height between text           | 30                   | N/A                 | N/A                     |
| hide_rank        | boolean   | hides the ranking                              | false                | N/A                 | N/A                     |
| hide_title       | boolean   | hides the stats title                          | false                | N/A                 | false                   |
| hide_border      | boolean   | hides the stats card border                    | false                | N/A                 | N/A                     |
| show_owner       | boolean   | shows owner name in repo card                  | N/A                  | false               | N/A                     |
| show_icons       | boolean   | shows icons                                    | false                | N/A                 | N/A                     |
| theme            | string    | sets inbuilt theme                             | 'default'            | 'default_repocard'  | 'default                |
| cache_seconds    | number    | manually set custom cache control              | 1800                 | 1800                | '1800'                  |
| hide_langs_below | number    | hide langs below certain threshold (lang card) | N/A                  | N/A                 | undefined               |

> Note on cache: Repo cards have default cache of 30mins (1800 seconds) if the fork count & star count is less than 1k otherwise it's 2hours (7200). Also note that cache is clamped to minimum of 30min and maximum of 24hours

---
# GitHub Extra Pins

GitHub extra pins allow you to pin more than 6 repositories in your profile using a GitHub readme profile.

Yey! You are no longer limited to 6 pinned repositories.

### Usage

Copy-paste this code into your readme and change the links.

Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`

```md
[![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats)
```

### Demo

[![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats)

Use [show_owner](#customization) variable to include the repo's owner username

[![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&show_owner=true)](https://github.com/anuraghazra/github-readme-stats)

# Top Languages Card

Top languages card shows github user's top langauges which has been mostly used.

_NOTE: Top languages does not indicate my skill level or something like that, it's a github metric of which languages i have the most code on github, it's a new feature of github-readme-stats_

### Usage

Copy-paste this code into your readme and change the links.

Endpoint: `api/top-langs?username=anuraghazra`

```md
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats)
```

### Hide languages below certain threshold

You can use `?hide_langs_below=NUMBER` parameter to hide languages below a specified threshold percentage.

```md
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&hide_langs_below=1)](https://github.com/anuraghazra/github-readme-stats)
```

### Demo

[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats)

---

### All Demos

- Default

![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra)
@@ -140,31 +194,11 @@ Choose from any of the [default themes](#themes)

![Customized Card](https://github-readme-stats.vercel.app/api/pin?username=anuraghazra&repo=github-readme-stats&title_color=fff&icon_color=f9f9f9&text_color=9f9f9f&bg_color=151515)

---

# GitHub Extra Pins

GitHub extra pins allow you to pin more than 6 repositories in your profile using a GitHub readme profile.

Yey! You are no longer limited to 6 pinned repositories.

### Usage
- Top languages

Copy-paste this code into your readme and change the links.

Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`

```md
[![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats)
```
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats)

### Demo

[![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats)

Use [show_owner](#customization) variable to include the repo's owner username

[![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&show_owner=true)](https://github.com/anuraghazra/github-readme-stats)
---

### Quick Tip (Align The Repo Cards)

+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;
+96 −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 data-testid="lang-name" 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 
        height="8" 
        fill="${color}"
        rx="5" ry="5" x="0" y="25" 
        data-testid="lang-progress" 
        width="${progressPercentage}%"
      >
      </rect>
    </svg>
  `;
};

const renderTopLanguages = (topLangs, options = {}) => {
  const {
    hide_title,
    card_width,
    title_color,
    text_color,
    bg_color,
    hide_langs_below,
    theme,
  } = options;

  let langs = Object.values(topLangs);

  const totalSize = langs.reduce((acc, curr) => {
    return acc + curr.size;
  }, 0);

  // hide langs
  langs = langs.filter((lang) => {
    if (!hide_langs_below) return true;
    return (lang.size / totalSize) * 100 > hide_langs_below;
  });

  // 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;
  let height = 45 + (langs.length + 1) * 40;

  if (hide_title) {
    height -= 30;
  }
  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"/>

      
      ${
        hide_title
          ? ""
          : `<text data-testid="header" x="25" y="35" class="header">Top Languages</text>`
      }

      <svg data-testid="lang-items" x="25" y="${hide_title ? 25 : 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",
      }).join("")}
      </svg>
    </svg>
  `;
};

module.exports = renderTopLanguages;
+84 −0
Original line number Diff line number Diff line
require("@testing-library/jest-dom");
const axios = require("axios");
const MockAdapter = require("axios-mock-adapter");
const fetchTopLanguages = require("../src/fetchTopLanguages");

const mock = new MockAdapter(axios);

afterEach(() => {
  mock.reset();
});

const data_langs = {
  data: {
    user: {
      repositories: {
        nodes: [
          {
            languages: {
              edges: [{ size: 100, node: { color: "#0f0", name: "HTML" } }],
            },
          },
          {
            languages: {
              edges: [{ size: 100, node: { color: "#0f0", name: "HTML" } }],
            },
          },
          {
            languages: {
              edges: [
                { size: 100, node: { color: "#0ff", name: "javascript" } },
              ],
            },
          },
          {
            languages: {
              edges: [
                { size: 100, node: { color: "#0ff", name: "javascript" } },
              ],
            },
          },
        ],
      },
    },
  },
};

const error = {
  errors: [
    {
      type: "NOT_FOUND",
      path: ["user"],
      locations: [],
      message: "Could not resolve to a User with the login of 'noname'.",
    },
  ],
};

describe("FetchTopLanguages", () => {
  it("should fetch correct language data", async () => {
    mock.onPost("https://api.github.com/graphql").reply(200, data_langs);

    let repo = await fetchTopLanguages("anuraghazra");
    expect(repo).toStrictEqual({
      HTML: {
        color: "#0f0",
        name: "HTML",
        size: 200,
      },
      javascript: {
        color: "#0ff",
        name: "javascript",
        size: 200,
      },
    });
  });

  it("should throw error", async () => {
    mock.onPost("https://api.github.com/graphql").reply(200, error);

    await expect(fetchTopLanguages("anuraghazra")).rejects.toThrow(
      "Could not resolve to a User with the login of 'noname'."
    );
  });
});
Loading