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 `
+
+ `;
+};
+
+module.exports = renderTopLanguages;