Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 15 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,82 +5,34 @@
<a href="https://github-stats-extended.vercel.app/api?username=anuraghazra"><img src="https://github-stats-extended.vercel.app/api?username=anuraghazra"></a>
</div>

This project is the [extended, actively maintained successor](docs/fork.md) of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). It generates [various stats cards](#card-types), e.g. about your GitHub contributions, your top languages, etc. You can [customize](#advanced-customization) the cards via multiple parameters.

## Table of Contents

- [Quick Start](#quick-start)
- [Migration from github-readme-stats](#migration-from-github-readme-stats)
- [Card Types](#card-types)
- [Advanced Customization](#advanced-customization)
- [Run It Yourself](#run-it-yourself)
- [Acknowledgements](#acknowledgements)
- [Contributing](#contributing)
This project is the [extended, actively maintained successor](docs/fork.md) of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). It generates stats cards about your GitHub contributions, your top languages and more, all customizable through a large set of parameters.

## Quick Start

- Copy and paste this into your markdown:
```markdown
[![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra)](https://github.com/stats-organization/github-stats-extended)
```
- Change the `?username=` value to your GitHub username.
- Done!

---

As more comfortable alternative, use the [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) to create your custom stats card. Copy the generated markdown code and paste it into your [GitHub profile README](https://docs.github.com/en/account-and-profile/how-tos/profile-customization/managing-your-profile-readme#adding-a-profile-readme). Done!

## Migration from github-readme-stats
Copy and paste this into your markdown, then change the `?username=` value to your GitHub username:

To migrate from [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) you only need to change the domain from `github-readme-stats.vercel.app` to `github-stats-extended.vercel.app`:

```diff
- https://github-readme-stats.vercel.app/api?username=octocat&theme=radical
+ https://github-stats-extended.vercel.app/api?username=octocat&theme=radical
```markdown
[![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra)](https://github.com/stats-organization/github-stats-extended)
```

GitHub-Stats-Extended aims to be fully compatible with github-readme-stats. For more details see [Compatibility Notes](docs/fork.md#compatibility-notes).

## Card Types

- Show your GitHub statistics:

![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra)

- ...your top languages...:

![Top Langs](https://github-stats-extended.vercel.app/api/top-langs/?username=anuraghazra&langs_count=4)

- ...and development time:

[![Alan's WakaTime stats](https://github-stats-extended.vercel.app/api/wakatime?username=alan&langs_count=6)](https://wakatime.com/@alan)

- Pin more than 6 repos in your GitHub profile:

[![Readme Card](https://github-stats-extended.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats)

- Pin Gists in your GitHub profile:

[![Gist Card](https://github-stats-extended.vercel.app/api/gist?id=bbfce31e0217a3689c8d961a356cb10d)](https://gist.github.com/Yizack/bbfce31e0217a3689c8d961a356cb10d)

- Customize all the cards:

[![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api/?username=anuraghazra&show_icons=true&theme=calm&rank_icon=github&include_all_commits=true&custom_title=Anurag's+Stats&disable_animations=true&number_format=long&show=prs_merged_percentage,prs_reviewed)](https://github-stats-extended.vercel.app/api/?username=anuraghazra&show_icons=true&theme=calm&rank_icon=github&include_all_commits=true&custom_title=Anurag's+Stats&disable_animations=true&number_format=long&show=prs_merged_percentage,prs_reviewed)

## Advanced Customization
As a more comfortable alternative, use the [card wizard](https://github-stats-extended.vercel.app/frontend) to configure your card visually and copy the generated markdown.

The [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) offers some essential customization options. For more advanced customization check out the [advanced documentation](docs/advanced_documentation.md).
## Documentation

## Run It Yourself
The full documentation is served by the deployment it describes, so every sample it shows is rendered by the version that is actually running:

If you want to run GitHub-Stats-Extended on your own, there are two main deployment options: you can use [github-readme-stats-action](https://github.com/stats-organization/github-readme-stats-action) to generate cards in your own GitHub Actions workflow. Or you can self-host GitHub-Stats-Extended on Vercel.
**[github-stats-extended.vercel.app/frontend/docs](https://github-stats-extended.vercel.app/frontend/docs)**

See [Run It Yourself](docs/deploy.md) for detailed instructions.
- [Overview](docs/index.md) — card types, quick start and migration from github-readme-stats.
- [Advanced Customization](docs/advanced_documentation.md) — every parameter the cards accept.
- [Available Themes](packages/core/src/themes/README.md) — the built-in themes.
- [Run It Yourself](docs/deploy.md) — GitHub Actions or a self-hosted Vercel deployment.
- [Fork Information](docs/fork.md) — what this project adds on top of github-readme-stats.

## Acknowledgements

This project is based on [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). On top of that project's functionality GitHub-Stats-Extended adds several new features and improvements. See [Fork Information](docs/fork.md) for a list of changes. The frontend added to GitHub-Stats-Extended is based on [GitHub Trends](https://github.com/avgupta456/github-trends). Big thanks to [@anuraghazra](https://github.com/anuraghazra), [@avgupta456](https://github.com/avgupta456), [@rickstaa](https://github.com/rickstaa), [@qwerty541](https://github.com/qwerty541) and everyone else who worked on these projects! ❤️
This project is based on [github-readme-stats](https://github.com/anuraghazra/github-readme-stats), and the card wizard on [GitHub Trends](https://github.com/avgupta456/github-trends). Big thanks to [@anuraghazra](https://github.com/anuraghazra), [@avgupta456](https://github.com/avgupta456), [@rickstaa](https://github.com/rickstaa), [@qwerty541](https://github.com/qwerty541) and everyone else who worked on these projects! ❤️

## Contributing

Contributions are welcome!
Contributions are welcome! See [CONTRIBUTING.md](.github/CONTRIBUTING.md) to get started.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { Page } from "@playwright/test";
* Puts the SPA into an authenticated state without contacting GitHub by
* stubbing the OAuth code exchange and the follow-up user-access lookup:
* `authenticate` returns a userId (which flips `isAuthenticated` to true), and
* `user-access` returns metadata so AppTrends does not immediately log back out.
* `user-access` returns metadata so the wizard does not immediately log back out.
* @param page - The Playwright page to install the route handlers on.
*/
async function mockAuthEndpoints(page: Page): Promise<void> {
Expand All @@ -25,7 +25,7 @@ async function mockAuthEndpoints(page: Page): Promise<void> {
);
}

test.describe("AppTrends auth-driven stage transition", () => {
test.describe("the wizard auth-driven stage transition", () => {
test("auto-advances from Login to Select a Card once authenticated", async ({
page,
}) => {
Expand All @@ -34,7 +34,7 @@ test.describe("AppTrends auth-driven stage transition", () => {
// Land on the app as GitHub does after the OAuth redirect (URL carries `code`).
await page.goto("?code=test-oauth-code");

// AppTrends starts unauthenticated on stage 0 ("Login"). When the code
// the wizard starts unauthenticated on stage 0 ("Login"). When the code
// exchange flips `isAuthenticated` false -> true, the render-phase
// transition must advance the stepper to stage 1 ("Select a Card").
await expect(page.getByRole("heading", { level: 1 })).toContainText(
Expand Down
6 changes: 6 additions & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,18 @@
"uuid": "^14.0.1"
},
"devDependencies": {
"@types/markdown-it": "14.1.2",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "6.0.3",
"clsx": "2.1.1",
"jsdom": "catalog:default",
"markdown-it": "14.3.0",
"markdown-it-anchor": "9.2.1",
"markdown-it-github-alerts": "1.0.1",
"tailwindcss": "4.3.3",
"vite": "catalog:default",
"vite-plugin-markdown": "2.2.0",
"vitest": "catalog:default"
},
"browserslist": {
Expand Down
34 changes: 34 additions & 0 deletions apps/frontend/src/Root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Suspense, lazy, useEffect, useState } from "react";
import type { JSX } from "react";

import { isDocsPath } from "./route";

/*
* Both views are loaded on demand so neither pays for the other:
* - the wizard does not ship the markdown renderer
* - the documentation does not ship the card builder
*/
const DocsApp = lazy(async () => ({
default: (await import("./docs/DocsApp")).DocsApp,
}));

const Wizard = lazy(async () => ({
default: (await import("./wizard/Wizard")).Wizard,
}));

export function Root(): JSX.Element {
const [docs, setDocs] = useState(() => isDocsPath(window.location.pathname));

useEffect(() => {
const handlePopState = () => {
setDocs(isDocsPath(window.location.pathname));
};

window.addEventListener("popstate", handlePopState);
return () => {
window.removeEventListener("popstate", handlePopState);
};
}, []);

return <Suspense fallback={null}>{docs ? <DocsApp /> : <Wizard />}</Suspense>;
}
22 changes: 11 additions & 11 deletions apps/frontend/src/axios-override.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ import {
DEMO_WAKATIME_USER,
HOST,
} from "./constants";
import additionalUserStars from "./mockData/additional_user_stars.json" with { type: "json" };
import commentedIssues from "./mockData/commented_issues.json" with { type: "json" };
import commentedPrs from "./mockData/commented_prs.json" with { type: "json" };
import commits from "./mockData/commits.json" with { type: "json" };
import gist_graphql from "./mockData/gist-graphql.json" with { type: "json" };
import gist_rest from "./mockData/gist-rest.json" with { type: "json" };
import repository from "./mockData/repository.json" with { type: "json" };
import reviewedPrs from "./mockData/reviewed_prs.json" with { type: "json" };
import topLanguages from "./mockData/top_languages.json" with { type: "json" };
import userStats from "./mockData/user_stats.json" with { type: "json" };
import wakatimeProxy from "./mockData/wakatime_proxy.json" with { type: "json" };
import additionalUserStars from "./wizard/mockData/additional_user_stars.json" with { type: "json" };
import commentedIssues from "./wizard/mockData/commented_issues.json" with { type: "json" };
import commentedPrs from "./wizard/mockData/commented_prs.json" with { type: "json" };
import commits from "./wizard/mockData/commits.json" with { type: "json" };
import gist_graphql from "./wizard/mockData/gist-graphql.json" with { type: "json" };
import gist_rest from "./wizard/mockData/gist-rest.json" with { type: "json" };
import repository from "./wizard/mockData/repository.json" with { type: "json" };
import reviewedPrs from "./wizard/mockData/reviewed_prs.json" with { type: "json" };
import topLanguages from "./wizard/mockData/top_languages.json" with { type: "json" };
import userStats from "./wizard/mockData/user_stats.json" with { type: "json" };
import wakatimeProxy from "./wizard/mockData/wakatime_proxy.json" with { type: "json" };

const cachedAxios = setupCache(axios, {
// Cache for 30 minutes
Expand Down
45 changes: 45 additions & 0 deletions apps/frontend/src/docs/DocsApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useEffect } from "react";
import type { JSX } from "react";

import { WIZARD_PATH } from "../route";
import { AppBar } from "../shared/AppBar";
import { LinkExternal } from "../shared/LinkExternal";

import { DocsContent } from "./DocsContent";
import { DocsSidebar } from "./DocsSidebar";
import { repoBlobUrl } from "./resolveDocUrl";
import { useDocRoute } from "./useDocRoute";

// Loaded with this view, so the wizard never downloads the markdown styles.
import "./docs.css";

export function DocsApp(): JSX.Element {
const { page, navigate } = useDocRoute();

useEffect(() => {
document.title = `${page.title} · GitHub Stats Extended`;
}, [page.title]);

return (
<>
<AppBar crossLink={{ href: WIZARD_PATH, label: "Wizard" }} />

<div className="mx-auto max-w-6xl w-full px-4 py-6 flex flex-col md:flex-row gap-8">
<DocsSidebar currentSlug={page.slug} onNavigate={navigate} />

<main className="min-w-0 flex-1">
<DocsContent page={page} onNavigate={navigate} />

<footer className="mt-12 pt-4 border-t border-base-300 text-sm opacity-70">
<LinkExternal
href={repoBlobUrl(page.file)}
className="hover:underline"
>
Edit this page on GitHub
</LinkExternal>
</footer>
</main>
</div>
</>
);
}
60 changes: 60 additions & 0 deletions apps/frontend/src/docs/DocsContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useMemo } from "react";
import type { JSX, MouseEvent } from "react";

import { isPlainClick } from "./isPlainClick";
import type { DocPage } from "./pages";
import { transformDocHtml } from "./transformDocHtml";

interface DocsContentProps {
page: DocPage;
onNavigate: (slug: string, hash?: string) => void;
}

/**
* Whether the document opens with its own title.
* The generated theme list starts at `<h2>`,
* so the page supplies a heading for it rather than leaving the document without one.
*/
function hasTopLevelHeading(html: string): boolean {
return /<h1[\s>]/i.test(html);
}

export function DocsContent({
page,
onNavigate,
}: DocsContentProps): JSX.Element {
const html = useMemo(
() => transformDocHtml(page.html, page.file),
[page.html, page.file],
);

/**
* Intercepts clicks on links this app can serve itself, so moving between
* documents does not reload the page. Everything else keeps its default
* behaviour.
*/
const handleClick = (event: MouseEvent<HTMLDivElement>) => {
const anchor = (event.target as HTMLElement).closest("a");
const href = anchor?.getAttribute("href");
if (!href?.startsWith("?page=") || !isPlainClick(event)) {
return;
}

event.preventDefault();
const [query = "", hash] = href.slice(1).split("#");
const slug = new URLSearchParams(query).get("page");
if (slug) {
onNavigate(slug, hash);
}
};

return (
// Link handling is delegated from the container; the rendered anchors keep
// their href and focus behaviour, so keyboard users are unaffected.
<div className="docs-content" onClick={handleClick}>
{!hasTopLevelHeading(page.html) && <h1>{page.title}</h1>}
{/* eslint-disable-next-line @eslint-react/dom-no-dangerously-set-innerhtml -- the markup is the repository's own markdown, compiled at build time; nothing here comes from user input or the network */}
<div dangerouslySetInnerHTML={{ __html: html }} />
</div>
);
}
47 changes: 47 additions & 0 deletions apps/frontend/src/docs/DocsSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { clsx } from "clsx";
import type { JSX } from "react";

import { isPlainClick } from "./isPlainClick";
import { DOC_PAGES } from "./pages";

interface DocsSidebarProps {
currentSlug: string;
onNavigate: (slug: string) => void;
}

export function DocsSidebar({
currentSlug,
onNavigate,
}: DocsSidebarProps): JSX.Element {
return (
<nav
aria-label="Documentation"
className="bg-base-200 md:bg-transparent md:w-56 md:shrink-0 md:sticky md:top-4 md:self-start"
>
<ul className="menu w-full gap-1">
{DOC_PAGES.map((page) => {
const current = page.slug === currentSlug;

return (
<li key={page.slug}>
<a
href={`?page=${page.slug}`}
aria-current={current ? "page" : undefined}
className={clsx(current && "menu-active font-medium")}
onClick={(event) => {
if (!isPlainClick(event)) {
return;
}
event.preventDefault();
onNavigate(page.slug);
}}
>
{page.title}
</a>
</li>
);
})}
</ul>
</nav>
);
}
Loading