Skip to content

Commit 9ea456b

Browse files
committed
feat: add GitHub Stats, Top Languages, and Pinned Repos components
1 parent b49ca20 commit 9ea456b

6 files changed

Lines changed: 473 additions & 2 deletions

File tree

.github/workflows/generate.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ jobs:
3131
node scripts/generate-typing.js
3232
node scripts/generate-footer.js
3333
node scripts/generate-sections.js
34+
node scripts/generate-stats.js
35+
node scripts/generate-langs.js
36+
node scripts/generate-repos.js
3437
env:
3538
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
3639
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -39,5 +42,5 @@ jobs:
3942
run: |
4043
git config user.name "github-actions[bot]"
4144
git config user.email "github-actions[bot]@users.noreply.github.com"
42-
git add assets/header.svg assets/typing.svg assets/footer.svg assets/sections/
45+
git add assets/header.svg assets/typing.svg assets/footer.svg assets/sections/ assets/stats.svg assets/langs.svg assets/repos.svg
4346
git diff --staged --quiet || (git commit -m "chore: regenerate assets" && git push origin main)

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,24 @@ A zero-effort GitHub profile README setup. Fork it and your profile is live inst
44

55
> 👇 See setup instructions below.
66
7+
<br>
8+
79
---
810

9-
> 👤 Example profile.
11+
> 👤 Example profile.,
1012
1113
<img src="assets/header.svg" width="100%"/>
1214
<br>
1315
<img src="assets/typing.svg" width="100%"/>
1416
<br>
17+
<img src="assets/stats.svg" width="100%"/>
18+
<br>
19+
<div align="center">
20+
<img src="assets/langs.svg" width="100%"/>
21+
</div>
22+
<br>
23+
<img src="assets/repos.svg" width="100%"/>
24+
<br>
1525
<div align="center">
1626

1727
<img src="assets/sections/lang.svg"/>
@@ -28,6 +38,8 @@ A zero-effort GitHub profile README setup. Fork it and your profile is live inst
2838

2939
---
3040

41+
<br>
42+
3143
## ✨ Features
3244

3345
- **Up in minutes** — just follow the Quick Start steps below, no configuration required
@@ -43,8 +55,12 @@ A zero-effort GitHub profile README setup. Fork it and your profile is live inst
4355
- **GitHub API integration** — name and bio are pulled directly from your GitHub profile, no manual input needed
4456
- **Fork-friendly** — works for any GitHub username automatically, no hardcoded values
4557

58+
<br>
59+
4660
---
4761

62+
<br>
63+
4864
## 🚀 Quick Start
4965

5066
### 1. Fork this repository
@@ -69,8 +85,12 @@ Go to **Actions** → **Generate Assets** → **Run workflow**. Your profile is
6985

7086
Edit `config.json` to personalize your typing lines, badge sections, colors, and more. See the [Customize](#%EF%B8%8F-customize) section below for details.
7187

88+
<br>
89+
7290
---
7391

92+
<br>
93+
7494
## ⚙️ Customize
7595

7696
All customization happens in `config.json`.
@@ -135,6 +155,8 @@ To add a new section, add a new object to the array and add `<img src="assets/se
135155

136156
---
137157

158+
<br>
159+
138160
## 📝 Editing README.md
139161

140162
All generated assets are just image files — place them anywhere in `README.md` however you like. Here's the full example used in this repo:
@@ -166,8 +188,12 @@ All generated assets are just image files — place them anywhere in `README.md`
166188
| Section (label + badges) | `assets/sections/{id}.svg` |
167189
| Footer | `assets/footer.svg` |
168190

191+
<br>
192+
169193
---
170194

195+
<br>
196+
171197
## 📄 License
172198

173199
MIT © [BerkaySevinc](https://github.com/BerkaySevinc)

config.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,27 @@
6060
}
6161
],
6262

63+
"stats": {
64+
65+
"_generates": "assets/stats.svg",
66+
"_comment": "GitHub Stats Card — fetched automatically via GitHub GraphQL API"
67+
68+
},
69+
70+
"langs": {
71+
72+
"_generates": "assets/langs.svg",
73+
"_comment": "Top Languages Chart — fetched automatically via GitHub GraphQL API"
74+
75+
},
76+
77+
"repos": {
78+
79+
"_generates": "assets/repos.svg",
80+
"_comment": "Pinned Repositories — fetched automatically via GitHub GraphQL API (falls back to top-starred repos)"
81+
82+
},
83+
6384
"footer": {
6485

6586
"_generates": "assets/footer.svg"

scripts/generate-langs.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// easy-github-profile — github.com/BerkaySevinc/easy-github-profile
2+
// Copyright (c) 2025 BerkaySevinc — MIT License
3+
4+
const { writeFileSync } = require('fs');
5+
const { join } = require('path');
6+
7+
const MAX_LANGS = 6;
8+
const BAR_X = 20, BAR_Y = 42, BAR_W = 760, BAR_H = 22;
9+
10+
async function fetchLangs(owner, token) {
11+
const headers = { 'User-Agent': 'github-profile-generator', 'Content-Type': 'application/json' };
12+
if (token) headers['Authorization'] = `Bearer ${token}`;
13+
14+
const query = `query($login: String!) {
15+
user(login: $login) {
16+
repositories(ownerAffiliations: OWNER, isFork: false, first: 100) {
17+
nodes {
18+
languages(first: 10, orderBy: { field: SIZE, direction: DESC }) {
19+
edges { size node { name color } }
20+
}
21+
}
22+
}
23+
}
24+
}`;
25+
26+
const res = await fetch('https://api.github.com/graphql', {
27+
method: 'POST', headers,
28+
body: JSON.stringify({ query, variables: { login: owner } }),
29+
});
30+
if (!res.ok) throw new Error(`GraphQL HTTP ${res.status}: ${res.statusText}`);
31+
32+
const json = await res.json();
33+
if (json.errors?.length) throw new Error(json.errors[0].message);
34+
35+
const totals = new Map();
36+
for (const repo of json.data.user.repositories.nodes) {
37+
for (const { size, node } of repo.languages.edges) {
38+
const existing = totals.get(node.name);
39+
if (existing) {
40+
existing.size += size;
41+
} else {
42+
totals.set(node.name, { size, color: node.color || '#808080' });
43+
}
44+
}
45+
}
46+
47+
return [...totals.entries()]
48+
.map(([name, { size, color }]) => ({ name, size, color }))
49+
.sort((a, b) => b.size - a.size)
50+
.slice(0, MAX_LANGS);
51+
}
52+
53+
function escapeXml(str) {
54+
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
55+
}
56+
57+
function buildSvg(langs) {
58+
const W = 800, H = 120;
59+
60+
if (!langs.length) {
61+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="60" viewBox="0 0 ${W} 60">
62+
<style>
63+
@media (prefers-color-scheme: dark) { .msg { fill: #8b949e; } }
64+
@media (prefers-color-scheme: light) { .msg { fill: #636e7b; } }
65+
.msg { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif; font-size: 13px; }
66+
</style>
67+
<text class="msg" x="${W / 2}" y="35" text-anchor="middle">No language data available.</text>
68+
</svg>`;
69+
}
70+
71+
const totalSize = langs.reduce((s, l) => s + l.size, 0);
72+
const withPct = langs.map(l => ({ ...l, pct: l.size / totalSize }));
73+
74+
// Stacked bar segments
75+
let barX = BAR_X;
76+
let barSegs = '';
77+
for (const lang of withPct) {
78+
const segW = Math.round(lang.pct * BAR_W);
79+
if (segW < 1) continue;
80+
barSegs += ` <rect x="${barX}" y="${BAR_Y}" width="${segW}" height="${BAR_H}" fill="${lang.color}"/>\n`;
81+
barX += segW;
82+
}
83+
84+
// Legend items
85+
const itemW = Math.floor((W - 40) / langs.length);
86+
let legend = '';
87+
for (let i = 0; i < langs.length; i++) {
88+
const lx = 20 + i * itemW;
89+
legend += ` <circle cx="${lx + 5}" cy="88" r="5" fill="${langs[i].color}"/>\n`;
90+
legend += ` <text x="${lx + 16}" y="93" class="leg">${escapeXml(langs[i].name)} ${(withPct[i].pct * 100).toFixed(1)}%</text>\n`;
91+
}
92+
93+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
94+
<defs>
95+
<mask id="bar-mask">
96+
<rect x="${BAR_X}" y="${BAR_Y}" width="0" height="${BAR_H}" fill="white">
97+
<animate attributeName="width" from="0" to="${BAR_W}" dur="1.2s"
98+
calcMode="spline" keyTimes="0;1" keySplines="0.25 0.1 0.25 1"
99+
fill="freeze"/>
100+
</rect>
101+
</mask>
102+
<clipPath id="bar-shape">
103+
<rect x="${BAR_X}" y="${BAR_Y}" width="${BAR_W}" height="${BAR_H}" rx="4"/>
104+
</clipPath>
105+
</defs>
106+
<style>
107+
@media (prefers-color-scheme: dark) {
108+
.ttl { fill: #e6edf3; }
109+
.trk { fill: #21262d; }
110+
.leg { fill: #8b949e; }
111+
}
112+
@media (prefers-color-scheme: light) {
113+
.ttl { fill: #1f2328; }
114+
.trk { fill: #eaeef2; }
115+
.leg { fill: #636e7b; }
116+
}
117+
.ttl { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif; font-size: 14px; font-weight: 600; }
118+
.leg { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif; font-size: 11px; }
119+
</style>
120+
121+
<text class="ttl" x="20" y="24">Top Languages</text>
122+
123+
<!-- Bar track (background) -->
124+
<rect class="trk" x="${BAR_X}" y="${BAR_Y}" width="${BAR_W}" height="${BAR_H}" rx="4"/>
125+
126+
<!-- Colored segments: rounded via clipPath, animated via mask -->
127+
<g clip-path="url(#bar-shape)" mask="url(#bar-mask)">
128+
${barSegs} </g>
129+
130+
<!-- Legend -->
131+
${legend}
132+
</svg>`;
133+
}
134+
135+
async function main() {
136+
const owner = process.env.GITHUB_REPOSITORY_OWNER;
137+
if (!owner) {
138+
console.error('Error: GITHUB_REPOSITORY_OWNER environment variable is not set.');
139+
process.exit(1);
140+
}
141+
142+
const langs = await fetchLangs(owner, process.env.GITHUB_TOKEN);
143+
writeFileSync(join(__dirname, '..', 'assets', 'langs.svg'), buildSvg(langs), 'utf8');
144+
console.log(`Generated assets/langs.svg — ${langs.map(l => l.name).join(', ')}`);
145+
}
146+
147+
main();

0 commit comments

Comments
 (0)