Skip to content

feat: combined stats card with streaks and languages#10

Merged
compscidr merged 1 commit into
masterfrom
feat/combined-card
Mar 20, 2026
Merged

feat: combined stats card with streaks and languages#10
compscidr merged 1 commit into
masterfrom
feat/combined-card

Conversation

@compscidr

Copy link
Copy Markdown
Owner

Summary

New /api/combined endpoint that renders a single card combining all three views:

  • Left column: overview stats (stars, forks, contributions, lines changed, views, repos) + contribution streaks (current + longest with dates)
  • Right column: language progress bar + per-language breakdown

All three data sources (overview, streak, top-langs) are fetched in parallel. The streak uses the lightweight fetcher so it won't timeout.

Usage

![Stats](https://github-readme-stats-navy-three-39.vercel.app/api/combined?username=compscidr&theme=dark)

Supports: theme, langs_count, exclude_repo, hide_border, custom_title, card_width, and all color options.

Test plan

  • Verify the combined card renders with all sections
  • Check it fits in the GitHub profile README width

🤖 Generated with Claude Code

Single card combining all three views:
- Left column: overview stats (stars, forks, contributions, lines
  changed, views, repos) + streak (current + longest with dates)
- Right column: language progress bar + per-language breakdown

Fetches overview, streak, and top-langs data in parallel.
New endpoint: /api/combined?username=X&theme=dark

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Mar 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
github-readme-stats Ready Ready Preview, Comment Mar 20, 2026 4:07am

Copilot AI review requested due to automatic review settings March 20, 2026 04:03
@compscidr compscidr merged commit de59045 into master Mar 20, 2026
9 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new /api/combined endpoint and corresponding SVG renderer to produce a single “combined” card that merges overview stats, contribution streaks, and top languages into a two-column layout.

Changes:

  • Added renderCombinedCard to render a two-column combined SVG (stats + streaks on the left, language bar + breakdown on the right).
  • Added /api/combined route that fetches overview/streak/top-langs in parallel and returns the combined SVG with cache headers.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

File Description
src/cards/combined.js New combined card renderer (layout, formatting helpers, styles, accessibility labels).
api/combined.js New API route wiring: access guard, parallel fetch, cache/error handling, option parsing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/cards/combined.js
}

card.setAccessibilityLabel({
title: defaultTitle,

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The accessibility label title always uses defaultTitle, so if custom_title is provided the visible title and the screen-reader title will diverge. Use card.title (or otherwise incorporate custom_title) for setAccessibilityLabel().title to keep a11y output consistent with the rendered header.

Suggested change
title: defaultTitle,
title: custom_title || defaultTitle,

Copilot uses AI. Check for mistakes.
Comment thread api/combined.js
Comment on lines +69 to +71
def: CACHE_TTL.STATS_CARD.DEFAULT,
min: CACHE_TTL.STATS_CARD.MIN,
max: CACHE_TTL.STATS_CARD.MAX,

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint renders top-languages data but resolves cache seconds using CACHE_TTL.STATS_CARD bounds (min 12h). That can significantly increase GitHub GraphQL load compared to /api/top-langs (min 2d), increasing rate-limit/latency risk for a more expensive fetcher. Consider using CACHE_TTL.TOP_LANGS_CARD bounds (or at least min/max aligned to top-langs) for /api/combined.

Suggested change
def: CACHE_TTL.STATS_CARD.DEFAULT,
min: CACHE_TTL.STATS_CARD.MIN,
max: CACHE_TTL.STATS_CARD.MAX,
def: CACHE_TTL.TOP_LANGS_CARD.DEFAULT,
min: CACHE_TTL.TOP_LANGS_CARD.MIN,
max: CACHE_TTL.TOP_LANGS_CARD.MAX,

Copilot uses AI. Check for mistakes.
Comment thread api/combined.js
Comment on lines +90 to +93
disable_animations: parseBoolean(disable_animations),
langs_count: parseInt(langs_count, 10) || 8,
hide: parseArray(hide),
},

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hide is parsed from the query and passed into renderCombinedCard, but renderCombinedCard doesn't read or apply a hide option. This is dead/unimplemented API surface and can confuse users. Either implement hide handling in the combined card (and document supported keys) or remove it from the endpoint options.

Copilot uses AI. Check for mistakes.
Comment thread api/combined.js
Comment on lines +59 to +95
try {
// Fetch all three data sources in parallel.
const [overview, streak, langs] = await Promise.all([
fetchOverview(username),
fetchStreak(username),
fetchTopLanguages(username, parseArray(exclude_repo)),
]);

const cacheSeconds = resolveCacheSeconds({
requested: parseInt(cache_seconds, 10),
def: CACHE_TTL.STATS_CARD.DEFAULT,
min: CACHE_TTL.STATS_CARD.MIN,
max: CACHE_TTL.STATS_CARD.MAX,
});

setCacheHeaders(res, cacheSeconds);

return res.send(
renderCombinedCard(
{ overview, streak, langs },
{
hide_border: parseBoolean(hide_border),
card_width: parseInt(card_width, 10),
title_color,
icon_color,
text_color,
bg_color,
theme,
custom_title,
border_radius,
border_color,
disable_animations: parseBoolean(disable_animations),
langs_count: parseInt(langs_count, 10) || 8,
hide: parseArray(hide),
},
),
);

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s no automated test coverage added for the new /api/combined route (query parsing, caching headers, and error rendering). Similar endpoints like /api/top-langs, /api/pin, and /api/gist have dedicated Jest tests, so adding a tests/combined.test.js would help prevent regressions.

Copilot uses AI. Check for mistakes.
Comment thread src/cards/combined.js
Comment on lines +225 to +226
<text x="${streakCenterLeft}" y="52" text-anchor="middle" class="streak-number"
${disable_animations ? "" : `class="stagger" style="animation-delay: 1800ms"`}>${streak.currentStreak}</text>

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When animations are enabled, this <text> ends up with two class attributes (class="streak-number" and the injected class="stagger"). Duplicate attributes make the SVG invalid XML and can cause the element (or whole SVG) to fail to render. Merge these into a single class attribute (e.g., append stagger to the existing class list) and only conditionally add the style/delay.

Suggested change
<text x="${streakCenterLeft}" y="52" text-anchor="middle" class="streak-number"
${disable_animations ? "" : `class="stagger" style="animation-delay: 1800ms"`}>${streak.currentStreak}</text>
<text x="${streakCenterLeft}" y="52" text-anchor="middle" class="streak-number${disable_animations ? "" : " stagger"}"
${disable_animations ? "" : 'style="animation-delay: 1800ms"'}>${streak.currentStreak}</text>

Copilot uses AI. Check for mistakes.
Comment thread src/cards/combined.js
Comment on lines +236 to +238
<!-- Longest streak number -->
<text x="${streakCenterRight}" y="40" text-anchor="middle" class="streak-number"
${disable_animations ? "" : `class="stagger" style="animation-delay: 2100ms"`}>${streak.longestStreak}</text>

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue here: when animations are enabled, this <text> will have duplicate class attributes (existing class="streak-number" plus injected class="stagger"). Combine them into a single class attribute to avoid invalid SVG/XML.

Copilot uses AI. Check for mistakes.
Comment thread src/cards/combined.js
const langEntries = Object.values(langs);
const numLangs = clampValue(parseInt(String(langs_count), 10), 1, 20);
const topLangs = langEntries.slice(0, numLangs);
const totalSize = topLangs.reduce((sum, lang) => sum + lang.size, 0);

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totalSize can be 0 when langs is empty (e.g., user has no repos, or everything is excluded). That will make later percentage calculations produce NaN/Infinity and generate invalid width/text values in the SVG. Add an early fallback (e.g., render an empty-state message or skip the progress bar/list) when topLangs.length === 0 or totalSize === 0.

Suggested change
const totalSize = topLangs.reduce((sum, lang) => sum + lang.size, 0);
const totalSize =
topLangs.reduce((sum, lang) => sum + lang.size, 0) || 1;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants