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

refactor: added reusable Card class to reduce code & test duplication (#260)

* refactor: added reusable Card class to reduce code & test duplication

* fix: top-langs card width & documented card_width option
parent 34b5dcb1
Loading
Loading
Loading
Loading
+4 −3
Original line number Diff line number Diff line
@@ -14,13 +14,14 @@ module.exports = async (req, res) => {
    username,
    hide,
    hide_title,
    hide_border,
    card_width,
    title_color,
    text_color,
    bg_color,
    theme,
    cache_seconds,
    layout
    layout,
  } = req.query;
  let topLangs;

@@ -42,15 +43,15 @@ module.exports = async (req, res) => {

  res.send(
    renderTopLanguages(topLangs, {
      theme,
      hide_title: parseBoolean(hide_title),
      hide_border: parseBoolean(hide_border),
      card_width: parseInt(card_width, 10),
      hide: parseArray(hide),
      title_color,
      text_color,
      bg_color,
      theme,
      layout
      layout,
    })
  );
};
+1 −0
Original line number Diff line number Diff line
@@ -137,6 +137,7 @@ Customization Options:
| cache_seconds | number    | manually set custom cache control           | 1800                 | 1800                | 1800                    |
| count_private | boolean   | counts private contributions too if enabled | false                | N/A                 | N/A                     |
| layout        | string    | choose a layout option                      | N/A                  | N/A                 | 'default'               |
| card_width    | number    | set the card width                          | N/A                  | N/A                 | 300                     |

> 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

src/Card.js

0 → 100644
+139 −0
Original line number Diff line number Diff line
const { FlexLayout } = require("./utils");
const { getAnimations } = require("./getStyles");

class Card {
  constructor({
    width = 100,
    height = 100,
    colors = {},
    title = "",
    titlePrefixIcon,
  }) {
    this.width = width;
    this.height = height;

    this.hideBorder = false;
    this.hideTitle = false;

    // returns theme based colors with proper overrides and defaults
    this.colors = colors;
    this.title = title;
    this.css = "";

    this.paddingX = 25;
    this.paddingY = 35;
    this.titlePrefixIcon = titlePrefixIcon;
    this.animations = true;
  }

  disableAnimations() {
    this.animations = false;
  }

  setCSS(value) {
    this.css = value;
  }

  setHideBorder(value) {
    this.hideBorder = value;
  }

  setHideTitle(value) {
    this.hideTitle = value;
    if (value) {
      this.height -= 30;
    }
  }

  setTitle(text) {
    this.title = text;
  }

  renderTitle() {
    const titleText = `
      <text
        x="0"
        y="0"
        class="header"
        data-testid="header"
      >${this.title}</text>
    `;

    const prefixIcon = `
      <svg
        class="icon"
        x="0"
        y="-13"
        viewBox="0 0 16 16"
        version="1.1"
        width="16"
        height="16"
      >
        ${this.titlePrefixIcon}
      </svg>
    `;
    return `
      <g
        data-testid="card-title"
        transform="translate(${this.paddingX}, ${this.paddingY})"
      >
        ${FlexLayout({
          items: [this.titlePrefixIcon && prefixIcon, titleText],
          gap: 25,
        }).join("")}
      </g>
    `;
  }

  render(body) {
    return `
      <svg
        width="${this.width}"
        height="${this.height}"
        viewBox="0 0 ${this.width} ${this.height}"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
      >
        <style>
          .header {
            font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
            fill: ${this.colors.titleColor};
            animation: fadeInAnimation 0.8s ease-in-out forwards;
          }
          ${this.css}

          ${
            process.env.NODE_ENV === "test" || !this.animations
              ? ""
              : getAnimations()
          }
        </style>

        <rect
          data-testid="card-bg"
          x="0.5"
          y="0.5"
          rx="4.5"
          height="99%"
          stroke="#E4E2E2"
          width="${this.width - 1}"
          fill="${this.colors.bgColor}"
          stroke-opacity="${this.hideBorder ? 0 : 1}"
        />

        ${this.hideTitle ? "" : this.renderTitle()}

        <g
          data-testid="main-card-body"
          transform="translate(0, ${
            this.hideTitle ? this.paddingX : this.paddingY + 20
          })"
        >
          ${body}
        </g>
      </svg>
    `;
  }
}

module.exports = Card;
+23 −23
Original line number Diff line number Diff line
@@ -9,31 +9,36 @@ const calculateCircleProgress = (value) => {
  return percentage;
};

const getAnimations = ({ progress }) => {
const getProgressAnimation = ({ progress }) => {
  return `
    /* Animations */
    @keyframes scaleIn {
    @keyframes rankAnimation {
      from {
        transform: translate(-5px, 5px) scale(0);
        stroke-dashoffset: ${calculateCircleProgress(0)};
      }
      to {
        transform: translate(-5px, 5px) scale(1);
        stroke-dashoffset: ${calculateCircleProgress(progress)};
      }
    }
    @keyframes fadeIn {
  `;
};

const getAnimations = () => {
  return `
    /* Animations */
    @keyframes scaleInAnimation {
      from {
        opacity: 0;
        transform: translate(-5px, 5px) scale(0);
      }
      to {
        opacity: 1;
        transform: translate(-5px, 5px) scale(1);
      }
    }
    @keyframes rankAnimation {
    @keyframes fadeInAnimation {
      from {
        stroke-dashoffset: ${calculateCircleProgress(0)};
        opacity: 0;
      }
      to {
        stroke-dashoffset: ${calculateCircleProgress(progress)};
        opacity: 1;
      }
    }
  `;
@@ -47,20 +52,16 @@ const getStyles = ({
  progress,
}) => {
  return `
    .header {
      font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor}; 
      animation: fadeIn 0.8s ease-in-out forwards;
    }
    .stat {
      font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
    }
    .stagger {
      opacity: 0;
      animation: fadeIn 0.3s ease-in-out forwards;
      animation: fadeInAnimation 0.3s ease-in-out forwards;
    }
    .rank-text {
      font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor}; 
      animation: scaleIn 0.3s ease-in-out forwards;
      animation: scaleInAnimation 0.3s ease-in-out forwards;
    }
    
    .bold { font-weight: 700 }
@@ -86,9 +87,8 @@ const getStyles = ({
      transform: rotate(-90deg);
      animation: rankAnimation 1s forwards ease-in-out;
    }

    ${process.env.NODE_ENV === "test" ? "" : getAnimations({ progress })}
    ${process.env.NODE_ENV === "test" ? "" : getProgressAnimation({ progress })}
  `;
};

module.exports = getStyles;
module.exports = { getStyles, getAnimations };
+65 −57
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ const {
} = require("../src/utils");
const icons = require("./icons");
const toEmoji = require("emoji-name-map");
const Card = require("./Card");

const renderRepoCard = (repo, options = {}) => {
  const {
@@ -84,42 +85,50 @@ const renderRepoCard = (repo, options = {}) => {
    `
    : "";

  const svgStars =
    stargazers.totalCount > 0 &&
    `
  const iconWithLabel = (icon, label, testid) => {
    return `
      <svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
      ${icons.star}
        ${icon}
      </svg>
    <text data-testid="stargazers" class="gray" x="25">${totalStars}</text>
      <text data-testid="${testid}" class="gray" x="25">${label}</text>
    `;

  };
  const svgStars =
    stargazers.totalCount > 0 &&
    iconWithLabel(icons.star, totalStars, "stargazers");
  const svgForks =
    forkCount > 0 &&
    `
    <svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
      ${icons.fork}
    </svg>
    <text data-testid="forkcount" class="gray" x="25">${totalForks}</text>
  `;
    forkCount > 0 && iconWithLabel(icons.fork, totalForks, "forkcount");

  const starAndForkCount = FlexLayout({
    items: [svgStars, svgForks],
    gap: 65,
  }).join("");

  const card = new Card({
    title: header,
    titlePrefixIcon: icons.contribs,
    width: 400,
    height,
    colors: {
      titleColor,
      textColor,
      iconColor,
      bgColor,
    },
  });

  return `
    <svg version="1.1" width="400" height="${height}" viewBox="0 0 400 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
      <style>
      .header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor} }
  card.disableAnimations();
  card.setHideBorder(false);
  card.setHideTitle(false);
  card.setCSS(`
    .description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
    .gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
    .icon { fill: ${iconColor} }
    .badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
    .badge rect { opacity: 0.2 }
      </style>

      <rect data-testid="card-bg" x="0.5" y="0.5" width="399" height="99%" rx="4.5" fill="${bgColor}" stroke="#E4E2E2"/>
      <svg class="icon" x="25" y="25" viewBox="0 0 16 16" version="1.1" width="16" height="16">
        ${icons.contribs}
      </svg>

      <text x="50" y="38" class="header">${header}</text>
  `);

  return card.render(`
    ${
      isTemplate
        ? getBadgeSVG("Template")
@@ -128,24 +137,23 @@ const renderRepoCard = (repo, options = {}) => {
        : ""
    }

      <text class="description" x="25" y="50">
    <text class="description" x="25" y="-5">
      ${multiLineDescription
        .map((line) => `<tspan dy="1.2em" x="25">${encodeHTML(line)}</tspan>`)
        .join("")}
    </text>

      <g transform="translate(0, ${height - 20})">
    <g transform="translate(0, ${height - 75})">
      ${svgLanguage}

      <g
        data-testid="star-fork-group" 
        transform="translate(${primaryLanguage ? 155 - shiftText : 25}, 0)"
      >
          ${FlexLayout({ items: [svgStars, svgForks], gap: 65 }).join("")}
        ${starAndForkCount}
      </g>
    </g>
    </svg>
  `;
  `);
};

module.exports = renderRepoCard;
Loading