Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
253eb39b19 | ||
|
|
06f0021660 | ||
|
|
f27daeb931 | ||
|
|
b6aa462881 | ||
|
|
be79fe4051 | ||
|
|
b039fa16af | ||
|
|
61442cb359 | ||
|
|
431f2a0b5b | ||
|
|
2709d5b434 | ||
|
|
96f89ad2b7 | ||
|
|
7dcb60f576 | ||
|
|
d5f410d4ec | ||
|
|
1c0332352a | ||
|
|
4febc6644a | ||
|
|
6fedd69f86 | ||
|
|
1078c85455 | ||
|
|
9e4d83247e | ||
|
|
597dac2985 | ||
|
|
0ea2668176 | ||
|
|
1091fc7695 | ||
|
|
6d4fbfecfb | ||
|
|
331295139d | ||
|
|
a726e8dbaf | ||
|
|
817f736b4e | ||
|
|
987fb0c30d | ||
|
|
95f1233ab9 | ||
|
|
1a53fb9a84 | ||
|
|
579ed2bab8 |
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ["https://www.paypal.me/anuraghazra"] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots / Live demo link (paste the github-readme-stats link as markdown image)**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -7,6 +7,7 @@ module.exports = async (req, res) => {
|
||||
const {
|
||||
username,
|
||||
hide,
|
||||
hide_title,
|
||||
hide_border,
|
||||
hide_rank,
|
||||
show_icons,
|
||||
@@ -31,6 +32,7 @@ module.exports = async (req, res) => {
|
||||
renderStatsCard(stats, {
|
||||
hide: JSON.parse(hide || "[]"),
|
||||
show_icons: parseBoolean(show_icons),
|
||||
hide_title: parseBoolean(hide_title),
|
||||
hide_border: parseBoolean(hide_border),
|
||||
hide_rank: parseBoolean(hide_rank),
|
||||
line_height,
|
||||
|
||||
+4
-2
@@ -1,5 +1,5 @@
|
||||
require("dotenv").config();
|
||||
const { renderError } = require("../src/utils");
|
||||
const { renderError, parseBoolean } = require("../src/utils");
|
||||
const fetchRepo = require("../src/fetchRepo");
|
||||
const renderRepoCard = require("../src/renderRepoCard");
|
||||
|
||||
@@ -11,10 +11,11 @@ module.exports = async (req, res) => {
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
show_owner,
|
||||
} = req.query;
|
||||
|
||||
let repoData;
|
||||
|
||||
|
||||
res.setHeader("Cache-Control", "public, max-age=1800");
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
|
||||
@@ -31,6 +32,7 @@ module.exports = async (req, res) => {
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
show_owner: parseBoolean(show_owner),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<p align="center">
|
||||
<img width="100px" src="https://res.cloudinary.com/anuraghazra/image/upload/v1594908242/logo_ccswme.svg" align="center" alt="Github Readme Stats" />
|
||||
<h2 align="center">GitHub Readme Stats</h2>
|
||||
<p align="center">Get dynamically generated GitHub stats on your readmes!</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/actions">
|
||||
@@ -22,6 +24,7 @@
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues">Request Feature</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">Loved the project? Please consider <a href="https://www.paypal.me/anuraghazra">donating</a> to help it improve!
|
||||
|
||||
# Features
|
||||
|
||||
@@ -60,24 +63,24 @@ To enable icons, you can pass `show_icons=true` in the query param, like so:
|
||||

|
||||
```
|
||||
|
||||
Other options:
|
||||
|
||||
- `&hide_border=true` hide the border box if you don't like it :D
|
||||
- `&line_height=30` control the line-height between text
|
||||
- `&hide_rank=true` hides the ranking
|
||||
|
||||
### Customization
|
||||
|
||||
You can customize the appearance of your `Stats Card` or `Repo Card` however you want with URL params.
|
||||
|
||||
Customization Options:
|
||||
|
||||
| Option | type | Stats Card (default) | Repo Card (default) |
|
||||
| ----------- | --------- | ---------------------- | ---------------------- |
|
||||
| title_color | hex color | #2f80ed | #2f80ed |
|
||||
| text_color | hex color | #333 | #333 |
|
||||
| icon_color | hex color | #4c71f2 | #586069 |
|
||||
| bg_color | hex color | rgba(255, 255, 255, 0) | rgba(255, 255, 255, 0) |
|
||||
| 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 |
|
||||
|
||||
- You can also customize the cards to be compatible with dark mode
|
||||
|
||||
@@ -127,6 +130,10 @@ Endpoint: `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
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
### Quick Tip (Align The Repo Cards)
|
||||
|
||||
You usually won't be able to layout the images side by side. To do that you can use this approach:
|
||||
@@ -144,6 +151,8 @@ You usually won't be able to layout the images side by side. To do that you can
|
||||
|
||||
Since the GitHub API only allows 5k requests per hour, it is possible that my `https://github-readme-stats.vercel.app/api` could hit the rate limiter. If you host it on your own Vercel server, then you don't have to worry about anything. Click on the deploy button to get started!
|
||||
|
||||
NOTE: Since [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) we should be able to handle more than 5k requests and have no issues with downtime :D
|
||||
|
||||
[](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
<details>
|
||||
@@ -151,21 +160,36 @@ Since the GitHub API only allows 5k requests per hour, it is possible that my `h
|
||||
|
||||
1. Go to [vercel.com](https://vercel.com/)
|
||||
1. Click on `Log in`
|
||||

|
||||

|
||||
1. Sign in with GitHub by pressing `Continue with GitHub`
|
||||

|
||||

|
||||
1. Sign into GitHub and allow access to all repositories, if prompted
|
||||
1. Fork this repo
|
||||
1. Go back to your [Vercel dashboard](https://vercel.com/dashboard)
|
||||
1. Select `Import Project`
|
||||

|
||||

|
||||
1. Select `Import Git Repository`
|
||||

|
||||
1. Select root and keep everything as is, just add your environment variable named GITHUB_TOKEN (as shown), which will contain a personal access token (PAT), which you can easily create [here](https://github.com/settings/tokens/new) (leave everything as is, just name it something, it can be anything you want)
|
||||

|
||||

|
||||
1. Select root and keep everything as is, just add your environment variable named PAT_1 (as shown), which will contain a personal access token (PAT), which you can easily create [here](https://github.com/settings/tokens/new) (leave everything as is, just name it something, it can be anything you want)
|
||||

|
||||
1. Click deploy, and you're good to go. See your domains to use the API!
|
||||
</details>
|
||||
|
||||
## :sparkling_heart: Support the project
|
||||
|
||||
I open-source almost everything I can, and I try to reply to everyone needing help using these projects. Obviously,
|
||||
this takes time. You can use this service for free.
|
||||
|
||||
However, if you are using this project and happy with it or just want to encourage me to continue creating stuff, there are few ways you can do it :-
|
||||
|
||||
- Giving proper credit when you use github-readme-stats on your readme, linking back to it :D
|
||||
- Starring and sharing the project :rocket:
|
||||
- [](https://www.paypal.me/anuraghazra) - You can make one-time donations via PayPal. I'll probably buy a ~~coffee~~ tea. :tea:
|
||||
|
||||
Thanks! :heart:
|
||||
|
||||
---
|
||||
|
||||
Contributions are welcomed! <3
|
||||
|
||||
Made with :heart: and JavaScript.
|
||||
|
||||
+13
-4
@@ -7,6 +7,9 @@ const fetcher = (variables, token) => {
|
||||
query: `
|
||||
fragment RepoInfo on Repository {
|
||||
name
|
||||
nameWithOwner
|
||||
isPrivate
|
||||
isArchived
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
@@ -52,15 +55,21 @@ async function fetchRepo(username, reponame) {
|
||||
throw new Error("Not found");
|
||||
}
|
||||
|
||||
if (data.organization === null && data.user) {
|
||||
if (!data.user.repository) {
|
||||
const isUser = data.organization === null && data.user;
|
||||
const isOrg = data.user === null && data.organization;
|
||||
|
||||
if (isUser) {
|
||||
if (!data.user.repository || data.user.repository.isPrivate) {
|
||||
throw new Error("User Repository Not found");
|
||||
}
|
||||
return data.user.repository;
|
||||
}
|
||||
|
||||
if (data.user === null && data.organization) {
|
||||
if (!data.organization.repository) {
|
||||
if (isOrg) {
|
||||
if (
|
||||
!data.organization.repository ||
|
||||
data.organization.repository.isPrivate
|
||||
) {
|
||||
throw new Error("Organization Repository Not found");
|
||||
}
|
||||
return data.organization.repository;
|
||||
|
||||
@@ -64,9 +64,6 @@ const getStyles = ({
|
||||
}
|
||||
|
||||
.bold { font-weight: 700 }
|
||||
.star-icon {
|
||||
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
|
||||
}
|
||||
.icon {
|
||||
fill: ${iconColor};
|
||||
display: ${!!show_icons ? "block" : "none"};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
const icons = {
|
||||
star: `<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>`,
|
||||
commits: `<path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"/>`,
|
||||
prs: `<path fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"/>`,
|
||||
issues: `<path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"/>`,
|
||||
icon: `<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>`,
|
||||
contribs: `<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>`,
|
||||
fork: `<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path>`,
|
||||
};
|
||||
|
||||
module.exports = icons;
|
||||
+63
-21
@@ -1,9 +1,24 @@
|
||||
const { kFormatter, encodeHTML, fallbackColor } = require("../src/utils");
|
||||
const {
|
||||
kFormatter,
|
||||
encodeHTML,
|
||||
fallbackColor,
|
||||
FlexLayout,
|
||||
} = require("../src/utils");
|
||||
const icons = require("./icons");
|
||||
|
||||
const renderRepoCard = (repo, options = {}) => {
|
||||
const { name, description, primaryLanguage, stargazers, forkCount } = repo;
|
||||
const { title_color, icon_color, text_color, bg_color } = options;
|
||||
const {
|
||||
name,
|
||||
nameWithOwner,
|
||||
description,
|
||||
primaryLanguage,
|
||||
stargazers,
|
||||
isArchived,
|
||||
forkCount,
|
||||
} = repo;
|
||||
const { title_color, icon_color, text_color, bg_color, show_owner } = options;
|
||||
|
||||
const header = show_owner ? nameWithOwner : name;
|
||||
const langName = primaryLanguage ? primaryLanguage.name : "Unspecified";
|
||||
const langColor = primaryLanguage ? primaryLanguage.color : "#333";
|
||||
|
||||
@@ -22,40 +37,67 @@ const renderRepoCard = (repo, options = {}) => {
|
||||
|
||||
const totalStars = kFormatter(stargazers.totalCount);
|
||||
const totalForks = kFormatter(forkCount);
|
||||
|
||||
const archiveBadge = isArchived
|
||||
? `
|
||||
<g data-testid="archive-badge" class="archive-badge" transform="translate(320, 38)">
|
||||
<rect stroke="${textColor}" stroke-width="1" width="70" height="20" x="-12" y="-14" ry="10" rx="10"></rect>
|
||||
<text fill="${textColor}">Archived</text>
|
||||
</g>
|
||||
`
|
||||
: "";
|
||||
|
||||
const svgLanguage = `
|
||||
<g transform="translate(30, 100)">
|
||||
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${langColor}" />
|
||||
<text data-testid="lang" class="gray" x="15">${langName}</text>
|
||||
</g>
|
||||
`;
|
||||
|
||||
const svgStars =
|
||||
stargazers.totalCount > 0 &&
|
||||
`
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
${icons.star}
|
||||
</svg>
|
||||
<text data-testid="stargazers" class="gray" x="25">${totalStars}</text>
|
||||
`;
|
||||
|
||||
const svgForks =
|
||||
totalForks > 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>
|
||||
`;
|
||||
|
||||
return `
|
||||
<svg width="400" height="${height}" viewBox="0 0 400 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<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} }
|
||||
.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} }
|
||||
.archive-badge { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; }
|
||||
.archive-badge rect { opacity: 0.2 }
|
||||
</style>
|
||||
<rect data-testid="card-border" 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">
|
||||
<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path>
|
||||
${icons.contribs}
|
||||
</svg>
|
||||
|
||||
<text x="50" y="38" class="header">${name}</text>
|
||||
${archiveBadge}
|
||||
|
||||
<text x="50" y="38" class="header">${header}</text>
|
||||
<text class="description" x="25" y="70">${encodeHTML(desc)}</text>
|
||||
|
||||
<g transform="translate(30, 100)">
|
||||
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${langColor}" />
|
||||
<text data-testid="lang" class="gray" x="15">${langName}</text>
|
||||
</g>
|
||||
|
||||
${svgLanguage}
|
||||
|
||||
<g transform="translate(${155 - shiftText}, 100)">
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path>
|
||||
</svg>
|
||||
<text data-testid="stargazers" class="gray" x="25">${totalStars}</text>
|
||||
${FlexLayout({ items: [svgStars, svgForks], gap: 65 }).join("")}
|
||||
</g>
|
||||
|
||||
<g transform="translate(${220 - shiftText}, 100)">
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path>
|
||||
</svg>
|
||||
<text data-testid="forkcount" class="gray" x="25">${totalForks}</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
+79
-49
@@ -1,21 +1,25 @@
|
||||
const { kFormatter, fallbackColor } = require("../src/utils");
|
||||
const { kFormatter, fallbackColor, FlexLayout } = require("../src/utils");
|
||||
const getStyles = require("./getStyles");
|
||||
const icons = require("./icons");
|
||||
|
||||
const createTextNode = ({ icon, label, value, id, index, lineHeight }) => {
|
||||
const classname = icon === "★" && "star-icon";
|
||||
const createTextNode = ({ icon, label, value, id, index, showIcons }) => {
|
||||
const kValue = kFormatter(value);
|
||||
const staggerDelay = (index + 3) * 150;
|
||||
// manually calculating lineHeight based on index instead of using <tspan dy="" />
|
||||
// to fix firefox layout bug
|
||||
const lheight = lineHeight * (index + 1);
|
||||
|
||||
const labelOffset = showIcons ? `x="25"` : "";
|
||||
const iconSvg = showIcons
|
||||
? `
|
||||
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
${icon}
|
||||
</svg>
|
||||
`
|
||||
: "";
|
||||
return `
|
||||
<text class="stagger" style="animation-delay: ${staggerDelay}ms" x="25" y="${lheight}">
|
||||
<tspan dx="0" data-testid="icon" class="icon ${classname}">${icon}</tspan>
|
||||
<tspan dx="0" class="stat bold">
|
||||
${label}:
|
||||
</tspan>
|
||||
<tspan x="160" data-testid="${id}" class="stat">${kValue}</tspan>
|
||||
</text>
|
||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)">
|
||||
${iconSvg}
|
||||
<text class="stat bold" ${labelOffset} y="12.5">${label}:</text>
|
||||
<text class="stat" x="135" y="12.5" data-testid="${id}">${kValue}</text>
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -32,6 +36,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
const {
|
||||
hide = [],
|
||||
show_icons = false,
|
||||
hide_title = false,
|
||||
hide_border = false,
|
||||
hide_rank = false,
|
||||
line_height = 25,
|
||||
@@ -48,54 +53,79 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
const textColor = fallbackColor(text_color, "#333");
|
||||
const bgColor = fallbackColor(bg_color, "#FFFEFE");
|
||||
|
||||
// Meta data for creating text nodes with createTextNode function
|
||||
const STATS = {
|
||||
stars: {
|
||||
icon: "★",
|
||||
icon: icons.star,
|
||||
label: "Total Stars",
|
||||
value: totalStars,
|
||||
id: "stars",
|
||||
},
|
||||
commits: {
|
||||
icon: "🕗",
|
||||
icon: icons.commits,
|
||||
label: "Total Commits",
|
||||
value: totalCommits,
|
||||
id: "commits",
|
||||
},
|
||||
prs: {
|
||||
icon: "🔀",
|
||||
icon: icons.prs,
|
||||
label: "Total PRs",
|
||||
value: totalPRs,
|
||||
id: "prs",
|
||||
},
|
||||
issues: {
|
||||
icon: "ⓘ",
|
||||
icon: icons.issues,
|
||||
label: "Total Issues",
|
||||
value: totalIssues,
|
||||
id: "issues",
|
||||
},
|
||||
contribs: {
|
||||
icon: "📕",
|
||||
icon: icons.contribs,
|
||||
label: "Contributed to",
|
||||
value: contributedTo,
|
||||
id: "contribs",
|
||||
},
|
||||
};
|
||||
|
||||
// filter out hidden stats defined by user & create the text nodes
|
||||
const statItems = Object.keys(STATS)
|
||||
.filter((key) => !hide.includes(key))
|
||||
.map((key, index) =>
|
||||
// create the text nodes, and pass index so that we can calculate the line spacing
|
||||
createTextNode({ ...STATS[key], index, lineHeight: lheight })
|
||||
createTextNode({
|
||||
...STATS[key],
|
||||
index,
|
||||
showIcons: show_icons,
|
||||
})
|
||||
);
|
||||
|
||||
// Calculate the card height depending on how many items there are
|
||||
// but if rank circle is visible clamp the minimum height to `150`
|
||||
const height = Math.max(
|
||||
let height = Math.max(
|
||||
45 + (statItems.length + 1) * lheight,
|
||||
hide_rank ? 0 : 150
|
||||
);
|
||||
|
||||
const border = `
|
||||
// the better user's score the the rank will be closer to zero so
|
||||
// subtracting 100 to get the progress in 100%
|
||||
const progress = 100 - rank.score;
|
||||
|
||||
const styles = getStyles({
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
show_icons,
|
||||
progress,
|
||||
});
|
||||
|
||||
// Conditionally rendered elements
|
||||
const title = hide_title
|
||||
? ""
|
||||
: `<text x="25" y="35" class="header">${name}'s GitHub Stats</text>`;
|
||||
|
||||
const border = hide_border
|
||||
? ""
|
||||
: `
|
||||
<rect
|
||||
data-testid="card-border"
|
||||
x="0.5"
|
||||
@@ -115,29 +145,22 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
})">
|
||||
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
|
||||
<circle class="rank-circle" cx="-10" cy="8" r="40" />
|
||||
<text
|
||||
x="${rank.level.length === 1 ? "-4" : "0"}"
|
||||
y="0"
|
||||
alignment-baseline="central"
|
||||
dominant-baseline="central"
|
||||
text-anchor="middle"
|
||||
class="rank-text"
|
||||
>
|
||||
${rank.level}
|
||||
</text>
|
||||
<g class="rank-text">
|
||||
<text
|
||||
x="${rank.level.length === 1 ? "-4" : "0"}"
|
||||
y="0"
|
||||
alignment-baseline="central"
|
||||
dominant-baseline="central"
|
||||
text-anchor="middle"
|
||||
>
|
||||
${rank.level}
|
||||
</text>
|
||||
</g>
|
||||
</g>`;
|
||||
|
||||
// the better user's score the the rank will be closer to zero so
|
||||
// subtracting 100 to get the progress in 100%
|
||||
let progress = 100 - rank.score;
|
||||
|
||||
const styles = getStyles({
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
show_icons,
|
||||
progress,
|
||||
});
|
||||
if (hide_title) {
|
||||
height -= 30;
|
||||
}
|
||||
|
||||
return `
|
||||
<svg width="495" height="${height}" viewBox="0 0 495 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -145,15 +168,22 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
${styles}
|
||||
</style>
|
||||
|
||||
${hide_border ? "" : border}
|
||||
${border}
|
||||
${title}
|
||||
|
||||
${rankCircle}
|
||||
|
||||
<text x="25" y="35" class="header">${name}'s GitHub Stats</text>
|
||||
<g data-testid="card-body-content" transform="translate(0, ${
|
||||
hide_title ? -30 : 0
|
||||
})">
|
||||
${rankCircle}
|
||||
|
||||
<svg x="0" y="45">
|
||||
${statItems.toString().replace(/\,/gm, "")}
|
||||
</svg>
|
||||
<svg x="0" y="55">
|
||||
${FlexLayout({
|
||||
items: statItems,
|
||||
gap: lheight,
|
||||
direction: "column",
|
||||
}).join("")}
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
+27
-11
@@ -48,17 +48,32 @@ function fallbackColor(color, fallbackColor) {
|
||||
}
|
||||
|
||||
function request(data, headers) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios({
|
||||
url: "https://api.github.com/graphql",
|
||||
method: "post",
|
||||
headers: {
|
||||
...headers,
|
||||
},
|
||||
data,
|
||||
})
|
||||
.then((response) => resolve(response))
|
||||
.catch((error) => reject(error));
|
||||
return axios({
|
||||
url: "https://api.github.com/graphql",
|
||||
method: "post",
|
||||
headers,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String[]} items
|
||||
* @param {Number} gap
|
||||
* @param {string} direction
|
||||
*
|
||||
* @description
|
||||
* Auto layout utility, allows us to layout things
|
||||
* vertically or horizontally with proper gaping
|
||||
*/
|
||||
function FlexLayout({ items, gap, direction }) {
|
||||
// filter() for filtering out empty strings
|
||||
return items.filter(Boolean).map((item, i) => {
|
||||
let transform = `translate(${gap * i}, 0)`;
|
||||
if (direction === "column") {
|
||||
transform = `translate(0, ${gap * i})`;
|
||||
}
|
||||
return `<g transform="${transform}">${item}</g>`;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,4 +85,5 @@ module.exports = {
|
||||
request,
|
||||
parseBoolean,
|
||||
fallbackColor,
|
||||
FlexLayout,
|
||||
};
|
||||
|
||||
@@ -80,4 +80,17 @@ describe("Test fetchRepo", () => {
|
||||
"Not found"
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error if repository is private", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, {
|
||||
data: {
|
||||
user: { repository: { ...data_repo, isPrivate: true } },
|
||||
organization: null,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
|
||||
"User Repository Not found"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ const { renderError } = require("../src/utils");
|
||||
|
||||
const data_repo = {
|
||||
repository: {
|
||||
username: "anuraghazra",
|
||||
name: "convoychat",
|
||||
stargazers: { totalCount: 38000 },
|
||||
description: "Help us take over the world! React + TS + GraphQL Chat App",
|
||||
@@ -61,6 +62,7 @@ describe("Test /api/pin", () => {
|
||||
icon_color: "fff",
|
||||
text_color: "fff",
|
||||
bg_color: "fff",
|
||||
full_name: "1",
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
|
||||
@@ -6,6 +6,7 @@ const { queryByTestId } = require("@testing-library/dom");
|
||||
|
||||
const data_repo = {
|
||||
repository: {
|
||||
nameWithOwner: "anuraghazra/convoychat",
|
||||
name: "convoychat",
|
||||
stargazers: { totalCount: 38000 },
|
||||
description: "Help us take over the world! React + TS + GraphQL Chat App",
|
||||
@@ -22,9 +23,10 @@ describe("Test renderRepoCard", () => {
|
||||
it("should render correctly", () => {
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository);
|
||||
|
||||
expect(document.getElementsByClassName("header")[0]).toHaveTextContent(
|
||||
"convoychat"
|
||||
);
|
||||
const [header] = document.getElementsByClassName("header");
|
||||
|
||||
expect(header).toHaveTextContent("convoychat");
|
||||
expect(header).not.toHaveTextContent("anuraghazra");
|
||||
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
|
||||
"Help us take over the world! React + TS + GraphQL Chat .."
|
||||
);
|
||||
@@ -39,6 +41,15 @@ describe("Test renderRepoCard", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should display username in title (full repo name)", () => {
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository, {
|
||||
show_owner: true,
|
||||
});
|
||||
expect(document.getElementsByClassName("header")[0]).toHaveTextContent(
|
||||
"anuraghazra/convoychat"
|
||||
);
|
||||
});
|
||||
|
||||
it("should trim description", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
@@ -136,4 +147,43 @@ describe("Test renderRepoCard", () => {
|
||||
"#252525"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render archive badge if repo is archived", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
isArchived: true,
|
||||
});
|
||||
|
||||
expect(queryByTestId(document.body, "archive-badge")).toHaveTextContent(
|
||||
"Archived"
|
||||
);
|
||||
});
|
||||
|
||||
it("should not render star count or fork count if either of the are zero", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
stargazers: { totalCount: 0 },
|
||||
});
|
||||
|
||||
expect(queryByTestId(document.body, "stargazers")).toBeNull();
|
||||
expect(queryByTestId(document.body, "forkcount")).toBeDefined();
|
||||
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
stargazers: { totalCount: 1 },
|
||||
forkCount: 0,
|
||||
});
|
||||
|
||||
expect(queryByTestId(document.body, "stargazers")).toBeDefined();
|
||||
expect(queryByTestId(document.body, "forkcount")).toBeNull();
|
||||
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
stargazers: { totalCount: 0 },
|
||||
forkCount: 0,
|
||||
});
|
||||
|
||||
expect(queryByTestId(document.body, "stargazers")).toBeNull();
|
||||
expect(queryByTestId(document.body, "forkcount")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,11 @@ require("@testing-library/jest-dom");
|
||||
const cssToObject = require("css-to-object");
|
||||
const renderStatsCard = require("../src/renderStatsCard");
|
||||
|
||||
const { getByTestId, queryByTestId } = require("@testing-library/dom");
|
||||
const {
|
||||
getByTestId,
|
||||
queryByTestId,
|
||||
queryAllByTestId,
|
||||
} = require("@testing-library/dom");
|
||||
|
||||
describe("Test renderStatsCard", () => {
|
||||
const stats = {
|
||||
@@ -66,7 +70,6 @@ describe("Test renderStatsCard", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats);
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
console.log(styleTag.textContent);
|
||||
const stylesObject = cssToObject(styleTag.textContent);
|
||||
|
||||
const headerClassStyles = stylesObject[".header"];
|
||||
@@ -107,4 +110,56 @@ describe("Test renderStatsCard", () => {
|
||||
"#252525"
|
||||
);
|
||||
});
|
||||
|
||||
it("should hide the title", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
hide_title: true,
|
||||
});
|
||||
|
||||
expect(document.getElementsByClassName("header")[0]).toBeUndefined();
|
||||
expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
|
||||
"height",
|
||||
"165"
|
||||
);
|
||||
expect(queryByTestId(document.body, "card-body-content")).toHaveAttribute(
|
||||
"transform",
|
||||
"translate(0, -30)"
|
||||
);
|
||||
});
|
||||
|
||||
it("should not hide the title", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {});
|
||||
|
||||
expect(document.getElementsByClassName("header")[0]).toBeDefined();
|
||||
expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
|
||||
"height",
|
||||
"195"
|
||||
);
|
||||
expect(queryByTestId(document.body, "card-body-content")).toHaveAttribute(
|
||||
"transform",
|
||||
"translate(0, 0)"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render icons correctly", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
show_icons: true,
|
||||
});
|
||||
|
||||
expect(queryAllByTestId(document.body, "icon")[0]).toBeDefined();
|
||||
expect(queryByTestId(document.body, "stars")).toBeDefined();
|
||||
expect(
|
||||
queryByTestId(document.body, "stars").previousElementSibling // the label
|
||||
).toHaveAttribute("x", "25");
|
||||
});
|
||||
|
||||
it("should not have icons if show_icons is false", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, { show_icons: false });
|
||||
|
||||
expect(queryAllByTestId(document.body, "icon")[0]).not.toBeDefined();
|
||||
expect(queryByTestId(document.body, "stars")).toBeDefined();
|
||||
expect(
|
||||
queryByTestId(document.body, "stars").previousElementSibling // the label
|
||||
).not.toHaveAttribute("x");
|
||||
});
|
||||
});
|
||||
|
||||
+27
-1
@@ -1,4 +1,9 @@
|
||||
const { kFormatter, encodeHTML, renderError } = require("../src/utils");
|
||||
const {
|
||||
kFormatter,
|
||||
encodeHTML,
|
||||
renderError,
|
||||
FlexLayout,
|
||||
} = require("../src/utils");
|
||||
|
||||
describe("Test utils.js", () => {
|
||||
it("should test kFormatter", () => {
|
||||
@@ -23,4 +28,25 @@ describe("Test utils.js", () => {
|
||||
"Something went wrong"
|
||||
);
|
||||
});
|
||||
|
||||
it("should test FlexLayout", () => {
|
||||
const layout = FlexLayout({
|
||||
items: ["<text>1</text>", "<text>2</text>"],
|
||||
gap: 60,
|
||||
}).join("");
|
||||
|
||||
expect(layout).toBe(
|
||||
`<g transform=\"translate(0, 0)\"><text>1</text></g><g transform=\"translate(60, 0)\"><text>2</text></g>`
|
||||
);
|
||||
|
||||
const columns = FlexLayout({
|
||||
items: ["<text>1</text>", "<text>2</text>"],
|
||||
gap: 60,
|
||||
direction: "column",
|
||||
}).join("");
|
||||
|
||||
expect(columns).toBe(
|
||||
`<g transform=\"translate(0, 0)\"><text>1</text></g><g transform=\"translate(0, 60)\"><text>2</text></g>`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user