Compare commits

...
90 Commits
Author SHA1 Message Date
martin-mfg 4340024a9a minor comment improvements 2026-08-27 09:37:15 +02:00
martin-mfg 92238f4dfd test splitting, add logs, small docs changes 2026-08-26 21:08:25 +02:00
martin-mfg b6ef01b6c6 adapt to merged docs 2026-08-26 18:39:33 +02:00
martin-mfg a21e14fa32 Merge remote-tracking branch 'origin/master' into contributed-to 2026-08-26 16:49:25 +02:00
MartinandMarco Pasqualetti 3bf8e6d9d6 update frontend docs (#515)
- add documentation for the [new GitHub light and dark
themes](https://github.com/stats-organization/github-stats-extended/pull/480),
for the [support light & dark mode in one
card](https://github.com/stats-organization/github-stats-extended/pull/430)
PR and for the [new "Total Contributions"
stat](https://github.com/stats-organization/github-stats-extended/pull/482)
- use dynamic light/dark cards in the docs at /frontend/docs

---------

Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2026-08-26 15:24:58 +02:00
martin-mfg 9ef1554064 tests and docs 2026-08-25 13:12:16 +02:00
martin-mfg bd58cfd47c add includeOwnRepos param, wire up params 2026-08-25 10:41:39 +02:00
martin-mfg c2c5fa34d8 Merge branch 'master' into contributed-to 2026-08-25 07:10:56 +02:00
Marco Pasqualetti 01f2468417 feat(core): convert wakatime card to TypeScript (#505)
Another step of #140

- `cards/wakatime.js` ➡️ `cards/wakatime.ts`
- Test and snapshot renamed to `renderWakatimeCard.test.ts` /
`renderWakatimeCard.test.ts.snap`;
  the snapshot are unchanged.
- New `common/languageColors.ts` owns the language-color table:
the gist and wakatime cards repeated `(languageColors as Record<string,
string>)[name] || "#858585"` three times, and top-languages declared a
fourth copy of that hex.
They now share `getLanguageColor()` / `DEFAULT_LANG_COLOR`, and typing
the table at the import removes the cast.
2026-08-24 21:06:11 +02:00
Marco Pasqualetti 8c99381676 chore(frontend): remove vercel.json and orphan Dockerfile (#506)
- Closes #504
2026-08-24 20:36:09 +02:00
Martin 8c78a825b5 intermediate fix for docs (#507) 2026-08-23 20:55:20 +02:00
Marco Pasqualettiandmartin-mfg e0de81e061 feat: publish the documentation and the card wizard from one Astro site (#489)
- Closes #461 

---

- Turns `apps/frontend` into an [Astro](https://astro.build/) +
[Starlight](https://starlight.astro.build/) site.
Serves the docs at `/frontend/docs` and the card wizard at `/frontend`:
one dev server, one build, one deploy, and no Python in
`vercel-preparation.sh` or in the local setup.
- The wizard becomes a page of that site: a React island on Starlight's
`splash` layout,
  so it inherits the header, footer and search.
  Its own app bar, theme picker and Redux theme slice go away.
Starlight writes the `data-theme` daisyUI already reads, so one control
themes both halves.
- The `*.md` sources move to `apps/frontend/src/content/docs/docs/` with
a `title` in
frontmatter and Starlight's `:::note` asides, which lets Astro resolve
their links and
  images natively (no markdown-conversion code of our own).
`starlight-links-validator` then fails the build on a dead internal
link.
- `packages/core/src/themes/README.md` is generated into the site
instead; README and CONTRIBUTING links follow the move.
- Tailwind now shares a page with Starlight, so `index.css` declares the
cascade-layer order
(its utilities must outrank Starlight's reset) and scopes the app's
element rules under
  `.wizard`, leaving the site identical on both halves.

---

To try it out just run

```sh
pnpm --filter ./apps/frontend run dev
```

---

| Wizard | Documentation |
|--------|--------|
| <img width="1710" height="666" alt="Screenshot 2026-08-17 at 00 54 18"
src="https://github.com/user-attachments/assets/c5b29e34-5464-41f3-9d01-08f593c87660"
/> | <img width="1710" height="720" alt="Screenshot 2026-08-17 at 00 54
25"
src="https://github.com/user-attachments/assets/e7e3f414-bf62-441d-9f6e-537a20044b0b"
/> |

---------

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-08-23 14:09:40 +02:00
martin-mfg 682f9ef566 WIP: improve new code 2026-08-23 11:08:37 +02:00
martin-mfg d61a76f88d WIP: all-time contributed to 2026-08-22 18:21:20 +02:00
Martin 3fb7c8bce5 contributions mock data, dependabot schedule (#500)
* add missing mock data for new contributions stat to frontend
* schedule dependabot twice a week
2026-08-20 10:28:36 +02:00
Marco Pasqualetti 31d28cdb1c fix: keep tests out of the build output and disable tsc emit by default (#486)
- `packages/core/tsconfig.json` covers `tests` but also set `outDir:
"build"`,
so a simple `tsc` emitted `build/tests`, which turbo then cached and
replayed on every later build.
- `tsconfig.base.json` now sets `noEmit: true`;
`packages/core/tsconfig.build.json` is the only config that opts back
in, and it takes the `outDir`.
- Removed `noEmit` from the typecheck and scripts configs.
- Removed `composite: true` from both apps: nothing references them as
TypeScript projects.

> [!NOTE]
> Local-only issue: CI never runs a standalone `tsc` and starts from a
clean checkout,
>  so the polluted `build/` never showed up there.
2026-08-16 23:14:07 +02:00
Marco Pasqualetti 56e0bb66cb test(core): use jest-dom assertions for DOM checks (#485)
- Card tests now assert with jest-dom matchers instead of reading nodes
by hand:
`toHaveTextContent`, `toHaveAttribute`, `toHaveStyle`,
`toBeInTheDocument`.
- Fixes assertions in `renderStatsCard.test.ts`: 
  `queryByTestId` returns `null`, so `toBeDefined()` could never fail. 
One of them was checking `rank-percentile-text`, which is a CSS class
and not a test id.
- Index-existence checks became `toHaveLength(n)`; 
- The five `toMatchInlineSnapshot` translation checks became explicit
`toHaveTextContent` with the same strings.
- `renderWakatimeCard.test.js` moved to the `screen` API
- `card.test.ts` uses `querySelector` over `getElementsBy*`.
2026-08-16 21:26:31 +02:00
3ab6cf918b add "Total Contributions" stat (#482)
Add a new optional stat for the stats card, "Total Contributions".

* I think this is something many users want. There are many issues where
people wonder why their "contributed to" or their "total commits"
doesn't match their contributions.
* This PR also introduces a way to fetch and sum stats which are only
available for 1 year at a time in the GitHub API. We could reuse this
later for more highly requested all-time stats if necessary.

I will add documentation in a separate PR.

---------

Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2026-08-16 17:28:03 +02:00
MartinandMarco Pasqualetti 480c26b717 add GitHub themes (#480)
This is only the code part of
https://github.com/stats-organization/github-stats-extended/pull/457.
Documentation from that PR will be merged later, as explained there.

---------

Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2026-08-14 23:29:02 +02:00
Marco Pasqualettiandmartin-mfg c86492f9c2 feat(core): generate TypeScript types from the GraphQL queries (#475)
The queries moved to `src/graphql/queries/*.graphql` and their types are
generated from
GitHub's schema instead of hand-written.

- `scripts/generate-graphql-types.js` validates each query against
`@octokit/graphql-schema` and writes one file per query plus a shared
`common.ts`,
  so a wrong field fails generation and a wrong variable fails `tsc`.
- `pnpm check-graphql-types` runs in CI, so the generated types can't
drift.
- `httpGraphQLRequest` takes a generated document: the four
`*QueryResponse` interfaces are gone and `GRAPHQL_REPOS_FIELD` is a real
fragment.
Untyped `request` stays exported for the backend, with a TODO for
considering its removal.
- Fixed nullability the hand-written types got wrong
  - (a gist's `owner` and `files[].name`, `user`, `repositories.nodes`);
  - `Lang.color` is nullable again
- `parseOwnerAffiliations` returns the schema's `RepositoryAffiliation`.
- `graphql` stays on 16: `@octokit/graphql-schema` needs `^16`.

> [!WARNING]
> The four `apps/backend` contract snapshots were refreshed:
> the recorded request text changed, the rendered SVGs did not.

---------

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-08-14 16:18:43 +02:00
Martin c177bd9ec5 top-langs: adapt test code to prod code (#476)
follow-up to
https://github.com/stats-organization/github-stats-extended/pull/473

Instead of adapting our prod code's signatures for our test code, I
think we should adapt our test code here.
2026-08-12 20:33:47 +02:00
Marco Pasqualetti 944857b2a5 feat(core): convert top languages card to TypeScript (#473)
Another step of #140.

- `src/cards/top-languages.js` + its test ➡️ `.ts`. 
- `fetchers/types.ts`: `Lang.color` made `string | null` to match
GitHub's nullable GraphQL `Language.color`.
The card already falls back to a default and a test pass `color: null`.
- Extracted the copy-pasted "resolve + validate language color" block
into `resolveLangColor`.
- Test uses the `screen` API, caches repeated `queryAll` results in
local variables, and types its language fixtures with `satisfies
TopLangData`.
2026-08-12 18:53:51 +02:00
Marco Pasqualetti a9cfee5809 feat(core): convert stats card to TypeScript (#466)
Another step of #140.

- `src/cards/stats.js` + its test ➡️ `.ts`.
- Moved `createTextNode` out of the stats card into `common/render.js`
(next to the other SVG-node builders).
  It was the one card export used by another card (repo).
- `I18n` gained a default type param (usable as a bare type),
and `Card`/`createTextNode` accept the possibly-undefined options cards
forward.
- Card tests now use the `screen` API instead of
`getByTestId(document.body, …)`.
See [recommended query
entrypoint](https://testing-library.com/docs/queries/about/#screen) and
cache repeated queries in local variables.
  Also applied on the already-migrated `gist`/`repo` tests.
2026-08-10 12:20:16 +02:00
MartinandMarco Pasqualetti 4edf85bdc2 downgrade pnpm to version 10 (#459)
Upgrading pnpm to version 11 led to errors on Vercel on each request.
https://github.com/stats-organization/github-stats-extended/issues/458
shows an example stack trace. Vercel doesn't support pnpm 11 yet
according to [their docs](https://vercel.com/docs/package-managers). I
have tried understanding and fixing the actual issue, but had to give up
at some point. So for now let's downgrade to pnpm 10, wait for Vercel to
support pnpm 11, then upgrade again.

Additionally, #458 showed that we should direct users to use the
`release` branch when self-hosting. So this PR also updates the docs
accordingly.

> [!NOTE]
When this PR is approved I should create an issue for the pnpm 11
upgrade.

---------

Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2026-08-08 09:13:52 +02:00
Marco Pasqualetti 1abd91eace chore: upgrade to pnpm 11 (#450)
- Bumps `packageManager` to `pnpm@11.20.0` and migrates
`onlyBuiltDependencies` to the new `allowBuilds` map.
- Adds
  - `savePrefix: ""`
  - `strictPeerDependencies`
  - `engineStrict`

  to `pnpm-workspace.yaml`.
  Nothing fails today, so these only guard regressions.

- Regenerates the lockfile and dedupes.
  Collapses two duplicates:
  - `typescript-eslint` 8.64.0 → 8.65.0,
- `vite>lightningcss` override, guarded by a new `lint:deps` check so
the override can't silently drift.
  
- **axios stays on 1.18.1.** 1.19 adds a params generic that breaks 5
core tests typecheck
(https://github.com/stats-organization/github-stats-extended/pull/449).
   Worth fixing in its own PR then.
2026-08-06 21:53:49 +02:00
Marco Pasqualetti dd28696356 feat(core): convert repo card to TypeScript (#447)
Another step of #140

- `src/cards/repo.js` + its test ➡️ `.ts`
- Colocated each card's options type into its own file; 
`types.ts` keeps only the shared `CommonOptions` and the options of the
js cards.
All `*CardOptions` are now `interface CardNameOptions extends
CommonOptions` (were `type` intersections).
- `fetchers/types.ts`: made `RepoInfo.description` and `primaryLanguage`
(and its fields) nullable to match the GraphQL schema.
  The card already guards for these and the tests exercise them.
- `common/I18n.ts`: widened the constructor's `locale?: string` to
accept `undefined`, since cards forward possibly-undefined query
options.
2026-08-05 22:57:14 +02:00
github-actions[bot]andmartin-mfg 5d3d3af4da Update languages colors (#446)
The
[generate-language-colors](https://github.com/stats-organization/github-stats-extended/actions/workflows/generate-language-colors.yml)
workflow detected changes in the [upstream languages JSON
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml)
and regenerated the local language colors JSON file.

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-08-05 16:01:23 +02:00
Martin 5156202340 fix update-langs, align naming (#440)
Like #433 for the update-langs workflow.
* rename the workflow, script, and pnpm command for consistency
* fix workflow which
[broke](https://github.com/stats-organization/github-stats-extended/actions/workflows/update-langs.yml)
after upgrading js-yaml a month ago
* reduced workflow permissions
2026-08-05 12:28:19 +02:00
Marco Pasqualetti 28bb41a488 feat(core): convert gist card to TypeScript (#443)
Another step of #140, continues #426. Starts `cards/`.

- `src/cards/types.d.ts` ➡️ `types.ts`. 
- `gist` card + test ➡️ `.ts`. Typed against `GistData` /
`Partial<GistCardOptions>`;
replaced the `// @ts-ignore` on the language-color lookup with a
`Record` cast.
- `Card.ts`: widened the constructor's `border_radius?: number` to `?:
number | undefined`:
cards forward possibly-undefined query options (same pattern as
`getCardColors`).
  Needed by every card.
2026-08-04 16:32:42 +02:00
MartinandMarco Pasqualetti 3ff8364844 docs: format md files with prettier - extended (#444)
Built on top of
https://github.com/stats-organization/github-stats-extended/pull/441 to
manually format two html blocks - see 2nd commit in this PR. I pushed
the suggested changes via a commit instead of using GitHub's "suggested
changes" feature so that we can preview the changes rendered by GitHub
[here](https://github.com/stats-organization/github-stats-extended/blob/prettier-md-2/docs/advanced_documentation.md).

closes #441 
(let's see if this works)

### Description from #441

- Followup to
https://github.com/stats-organization/github-stats-extended/pull/432#discussion_r3698998568

---

- Removed `*.md` from `.prettierignore` and reformatted all Markdown
files.
- `generate-theme-readme.js` now formats its output (root config via
`resolveConfig`) so `src/themes/README.md` stays formatted.

---------

Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2026-08-04 15:56:06 +02:00
Marco Pasqualetti df66b24c6a ci(generate-theme-readme): align theme readme generation with TypeScript sources (#433)
Follow-up to [the `themes/index.js` ➡️ `index.ts`
migration](https://github.com/stats-organization/github-stats-extended/pull/139),
which left the theme README generation pipeline pointing to files that
no longer exist.

* Renamed the workflow, script, and pnpm command to the consistent
`generate-theme-readme` (previously `generate-theme-doc` /
`theme-readme-gen`) and updated them to use the `.ts` source.
* Reduced workflow permissions from 12 declared scopes to the 2 actually
required:
  * `contents: write`
  * `pull-requests: write`
* Fixed the generated PR body, which incorrectly referenced language
files.

---

> [!NOTE]
> After this is merged, I'd consider applying the same permission
reduction and workflow rename to `update-langs.yml`.
2026-08-03 14:34:56 +02:00
Marco Pasqualetti fbd292a03d ci(repeat-recent-requests): replace http-request-action with curl (#434)
- Swaps the third-party `fjogeleit/http-request-action` for a plain
`curl` step in `repeat-recent-requests.yml`:
the workflow only needs a POST with a timeout, so the action added no
value.
- The request carries only what the endpoint reads. `repeat-recent`
checks `req.method` and nothing else.
  So the action's JSON body and headers were never used. 
  Each flag has a one-line comment explaining why it's there.
- Failure semantics are preserved: a `>= 400` status or a connection
failure still fails the job, and the response body and status are now
logged even on failure.

Example run:
https://github.com/marcalexiei/github-stats-extended/actions/runs/30751117550/job/91505220151
2026-08-03 14:15:07 +02:00
Marco Pasqualettiandmartin-mfg 68eddf0f81 docs: explain the Backend E2E failure and refine docs content (#432)
Related to
https://github.com/stats-organization/github-stats-extended/pull/430

> [(The failing Backend E2E test is expected, because this PR changes
the output of most cards, but in a non-visible
way.)](https://github.com/stats-organization/github-stats-extended/pull/430#issuecomment-5153247227)

- `ci.yml`: comment explaining what the job compares and why it fails.
- `CONTRIBUTING.md`: new "Tests" section, there was no testing
documentation before.
Removed the FAQ, whose three entries were all user questions, and
promoted Feature Request from `h3` to `h2`.
- `advanced_documentation`: kept the two useful FAQ answers, in context:
- language-card troubleshooting links under "Language stats algorithm",
- percent-encoding example (`&hide=jupyter%20notebook`) under "Hide
individual languages".
Demo labels changed from list items to `h4` headings: the images were
never indented into the list, so each label rendered as its own one-item
bullet.
- `deploy.md`: normalized the token-setup lists from `*` with 4 space
indents to `-` with 2 space indents.
  Marker and indentation only, nesting preserved.
- `README.md`: the seven section headings were `h1`. With the HTML
`<h1>` in the banner that made eight top-level headings on one page, so
they are now `h2`.
Heading anchors are derived from the text, so the table of contents is
unaffected.

---------

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-08-03 13:46:24 +02:00
MartinandMarco Pasqualetti 58d941d63a support light & dark mode in one card (#430)
Add support for light mode and dark mode without [brittle,
GitHub-specific
approaches](https://github.com/stats-organization/github-stats-extended/blob/master/docs/advanced_documentation.md#responsive-card-theme).

All endpoints accept new parameters: theme_light, theme_dark,
title_color_light,
title_color_dark, icon_color_light, icon_color_dark, etc.

> Priority (lowest -> highest):
>  default theme -> `theme` param -> `theme_light`/`theme_dark` param
>  -> general color params -> `*_light`/`*_dark` color params

Additionally, this PR adds the recently introduced `progBarBgColor`
parameter to `CardColors` and handles it together with the other colors
in color.ts, for consistency. And it adds one test for an archived
repository.

This PR is already too big, so I'll add documentation in a later PR, and
maybe also a simple new option for the frontend.

My main question here is: does the API and the intended behaviour make
sense? Bugs can be fixed later, but once the new parameters are
released, ideally we shouldn't change them anymore.

---------

Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2026-08-03 13:14:13 +02:00
Marco Pasqualetti 372eaaf058 feat(core): align repo star field naming and add type coverage (#429)
Followup of
https://github.com/stats-organization/github-stats-extended/pull/426#discussion_r3695160068

> Now that we have `RepoInfo`, would it make sense to define
`RepositoryData` as combination of `RepoInfo` and `RepoUserStats` (and
align the naming of "starCount"/"stargazerCount") in a later PR?
2026-08-01 21:00:35 +02:00
Marco Pasqualetti 09a72df953 feat(core): convert repo fetcher to TypeScript (#426)
Another step of #140, continues #415. 

Last fetcher: `fetchers/` is now fully TS.

- `repo` fetcher + test ➡️ `.ts`. 
Defines `RepoInfo` and `RepoQueryResponse`, calls
`retryer<RepoQueryResponse>(…)`
- `types.ts`: made `RepositoryData`'s PR/issue count fields optional:
they come from `fetchRepoUserStats`, which only sets them when the
matching `include_*` flag is on.
2026-08-01 10:10:24 +02:00
Marco Pasqualetti 587f4589b8 feat(core): convert stats fetcher to TypeScript (#415)
Another step of #140, continues #410.

- `stats` fetcher + test ➡️ `.ts`, threading typed GraphQL and
REST-search shapes through `retryer<…>`.
- Added a `src/_github-username-regex.d.ts` shim (package ships no
types).
- Fixed a bug caught by the types: statsFetcher expected pat in its
options object,
  but the caller passed it as a second argument, so it was ignored. 
Private-access users' token is now correctly passed to the main stats
query.
2026-07-29 20:05:48 +02:00
Marco Pasqualetti 259fc7c5bc feat(core): convert wakatime + top-languages fetchers to TypeScript (#410)
Another step of #140, continues #370.

- `wakatime` fetcher + test ➡️ `.ts`. Typed the props object and the
axios body (`axios.get<{ data: WakaTimeData }>`);
replaced the raw `err.response.status` access with an
`axios.isAxiosError` guard.
- `top-languages` fetcher + test ➡️ `.ts`, calling
`retryer<TopLanguagesQueryResponse>(…)`.
Split the single `let repoNodes` (reused across three shapes) into typed
values and simplified two of the three `reduce` calls (map-builder →
`for…of`, final sort ➡️ `Object.fromEntries`).
The flatten `reduce` is kept as-is — its order feeds the `repoCount`
logic.
- `types.ts`: added `count: number` to `Lang` (the fetcher always
produces it; the card only reads `name`/`color`/`size`).
2026-07-28 22:46:27 +02:00
Martin caccc4d69a bump core version to 2.1.5 (#407) 2026-07-27 19:20:46 +02:00
Martin 1c6b4c31d9 fix dropdown bugs and gradient bug (#402)
* In the frontend, "Modify Parameters" step, a few options of several
dropdowns encoded multiple parameters in their value string. That didn't
work anymore since a recent change. This PR fixes the affected dropdown
options.
* Since a recent change, using the "ambient_gradient" theme for WakaTime
card didn't work anymore. This PR fixes that.
2026-07-26 12:13:26 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 2992f63de2 ci(deps): Bump fjogeleit/http-request-action from 2.0.0 to 2.0.1 (#362)
Bumps
[fjogeleit/http-request-action](https://github.com/fjogeleit/http-request-action)
from 2.0.0 to 2.0.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/fjogeleit/http-request-action/releases">fjogeleit/http-request-action's
releases</a>.</em></p>
<blockquote>
<h2>v2.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Bump axios from 1.13.4 to 1.13.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/209">fjogeleit/http-request-action#209</a></li>
<li>Bump axios from 1.13.5 to 1.13.6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/210">fjogeleit/http-request-action#210</a></li>
<li>Bump undici from 6.23.0 to 6.24.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/211">fjogeleit/http-request-action#211</a></li>
<li>Bump axios from 1.13.6 to 1.14.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/212">fjogeleit/http-request-action#212</a></li>
<li>Revert &quot;Bump axios from 1.13.6 to 1.14.0&quot; by <a
href="https://github.com/fjogeleit"><code>@​fjogeleit</code></a> in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/213">fjogeleit/http-request-action#213</a></li>
<li>docs(readme): fix inaccuracies, heading hierarchy, and document
output by <a
href="https://github.com/kranthipoturaju"><code>@​kranthipoturaju</code></a>
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/222">fjogeleit/http-request-action#222</a></li>
<li>docs(readme): update redirecting GitHub Docs URLs by <a
href="https://github.com/kranthipoturaju"><code>@​kranthipoturaju</code></a>
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/224">fjogeleit/http-request-action#224</a></li>
<li>docs: fix inaccuracies and document missing outputs in action.yml by
<a
href="https://github.com/kranthipoturaju"><code>@​kranthipoturaju</code></a>
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/223">fjogeleit/http-request-action#223</a></li>
<li>Bump actions/checkout from 6.0.2 to 6.0.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/227">fjogeleit/http-request-action#227</a></li>
<li>Bump axios from 1.13.6 to 1.15.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/216">fjogeleit/http-request-action#216</a></li>
<li>Bump axios from 1.15.0 to 1.17.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/225">fjogeleit/http-request-action#225</a></li>
<li>Bump follow-redirects from 1.15.11 to 1.16.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/217">fjogeleit/http-request-action#217</a></li>
<li>Bump nick-fields/assert-action from 2.0.0 to 3.0.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/214">fjogeleit/http-request-action#214</a></li>
<li>fix [DEP0169] DeprecationWarning: <code>url.parse()</code> behavior
is not standardized... by <a
href="https://github.com/wvidana"><code>@​wvidana</code></a> in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/232">fjogeleit/http-request-action#232</a></li>
<li>Bump undici from 6.24.0 to 6.27.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/231">fjogeleit/http-request-action#231</a></li>
<li>Bump axios from 1.17.0 to 1.18.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/229">fjogeleit/http-request-action#229</a></li>
<li>Bump axios from 1.18.0 to 1.18.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/233">fjogeleit/http-request-action#233</a></li>
<li>Bump <code>@​vercel/ncc</code> from 0.38.4 to 0.44.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/230">fjogeleit/http-request-action#230</a></li>
<li>Bump form-data from 4.0.5 to 4.0.6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/228">fjogeleit/http-request-action#228</a></li>
<li>Bump nick-fields/assert-action from 3.0.0 to 4.0.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/234">fjogeleit/http-request-action#234</a></li>
<li>Bump actions/checkout from 6.0.3 to 7.0.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/235">fjogeleit/http-request-action#235</a></li>
<li>chore(dep): dependency updates by <a
href="https://github.com/fjogeleit"><code>@​fjogeleit</code></a> in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/236">fjogeleit/http-request-action#236</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/kranthipoturaju"><code>@​kranthipoturaju</code></a>
made their first contribution in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/222">fjogeleit/http-request-action#222</a></li>
<li><a href="https://github.com/wvidana"><code>@​wvidana</code></a> made
their first contribution in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/232">fjogeleit/http-request-action#232</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/fjogeleit/http-request-action/compare/v2.0...v2.0.1">https://github.com/fjogeleit/http-request-action/compare/v2.0...v2.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/f98bb26551cd1af7d3eb2674578a80754ece88db"><code>f98bb26</code></a>
chore(dep): dependency updates (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/236">#236</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/3584789bbffe004d18e05cd2779aa75a3e2bf69a"><code>3584789</code></a>
Bump actions/checkout from 6.0.3 to 7.0.0 (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/235">#235</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/1d6a1ee0f4e72dcbc37a910236b8a2fedf1221df"><code>1d6a1ee</code></a>
Bump nick-fields/assert-action from 3.0.0 to 4.0.1 (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/234">#234</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/5d99b6683aaaf873571876966005dd6fc8b3136a"><code>5d99b66</code></a>
Bump form-data from 4.0.5 to 4.0.6 (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/228">#228</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/a53287a79a36cc186797c68df701ae69978eb2e8"><code>a53287a</code></a>
Bump <code>@​vercel/ncc</code> from 0.38.4 to 0.44.1 (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/230">#230</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/7540a773d6694bdd6235eb1e25bb01723b7855fc"><code>7540a77</code></a>
Bump axios from 1.18.0 to 1.18.1 (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/233">#233</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/55dcde0359495be61b69697dab6a9e2f310d4ffd"><code>55dcde0</code></a>
Bump axios from 1.17.0 to 1.18.0 (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/229">#229</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/d703d5a26d1c679a77adda062ad23653f804dea3"><code>d703d5a</code></a>
Bump undici from 6.24.0 to 6.27.0 (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/231">#231</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/0b8c6ccce4d482d1466df75f3ac0f9b0e2183474"><code>0b8c6cc</code></a>
Using the globally scoped URLSearchParams from the WHATWG URL API (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/232">#232</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/ed82ab41da4893d38edab6b5fad057b285078493"><code>ed82ab4</code></a>
Bump nick-fields/assert-action from 2.0.0 to 3.0.0 (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/214">#214</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/fjogeleit/http-request-action/compare/551353b829c3646756b2ec2b3694f819d7957495...f98bb26551cd1af7d3eb2674578a80754ece88db">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=fjogeleit/http-request-action&package-manager=github_actions&previous-version=2.0.0&new-version=2.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 13:35:51 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9201b65d82 ci(deps): Bump actions/checkout from 7.0.0 to 7.0.1 (#395)
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0
to 7.0.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>skip running unsafe pr check if input is default by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2518">actions/checkout#2518</a></li>
<li>trim only ascii whitespace for branch by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2521">actions/checkout#2521</a></li>
<li>escape values passed to --unset by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2530">actions/checkout#2530</a></li>
<li>Various dependency updates</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v7...v7.0.1">https://github.com/actions/checkout/compare/v7...v7.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v7.0.1</h2>
<ul>
<li>Skip running unsafe pr check if input is default by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2518">actions/checkout#2518</a></li>
<li>Trim only ascii whitespace for branch by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2521">actions/checkout#2521</a></li>
<li>Escape values passed to --unset by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2530">actions/checkout#2530</a></li>
<li>Various dependency updates</li>
</ul>
<h2>v7.0.0</h2>
<ul>
<li>Block checking out fork PR for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Various dependency updates</li>
</ul>
<h2>v6.0.3</h2>
<ul>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/3d3c42e5aac5ba805825da76410c181273ba90b1"><code>3d3c42e</code></a>
prep v7.0.1 release (<a
href="https://redirect.github.com/actions/checkout/issues/2531">#2531</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/28802689a136bfcdb721715abd713740beecbe07"><code>2880268</code></a>
escape values passed to --unset (<a
href="https://redirect.github.com/actions/checkout/issues/2530">#2530</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/12cd2235efa0937479335606d7c3ac9f6c0973b1"><code>12cd223</code></a>
trim only ascii whitespace for branch (<a
href="https://redirect.github.com/actions/checkout/issues/2521">#2521</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/62661c4e71a304b2823ed026347b8d34c3eac541"><code>62661c4</code></a>
skip running unsafe pr check if input is default (<a
href="https://redirect.github.com/actions/checkout/issues/2518">#2518</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/e8d4307400f9427dba7cb98e488d6ab85f1cec5f"><code>e8d4307</code></a>
Bump the minor-actions-dependencies group with 2 updates (<a
href="https://redirect.github.com/actions/checkout/issues/2499">#2499</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/631c942040754b6e095e929c1677c07e10ed4f87"><code>631c942</code></a>
eslint 9 (<a
href="https://redirect.github.com/actions/checkout/issues/2474">#2474</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/4f1f4aec02e41874fa0262ea8ff5172d7978ad1e"><code>4f1f4ae</code></a>
Bump actions/upload-artifact from 4 to 7 (<a
href="https://redirect.github.com/actions/checkout/issues/2476">#2476</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/ba097532fb203f7e88c9c3c0b899b49469908a92"><code>ba09753</code></a>
Bump actions/checkout from 6 to 7 (<a
href="https://redirect.github.com/actions/checkout/issues/2488">#2488</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/b9e0990d219a03df7633c93f6f005a8fecbcab22"><code>b9e0990</code></a>
Bump docker/login-action from 3.3.0 to 4.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2479">#2479</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/e8cb398be4a550817e382abf69e4c12c76fce1f2"><code>e8cb398</code></a>
Bump docker/build-push-action from 6.5.0 to 7.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2478">#2478</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=7.0.0&new-version=7.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 13:35:23 +02:00
Martin 90508f02f3 upgrade several dependencies (#400) 2026-07-25 13:27:54 +02:00
Martin 3bda3abee7 bump core version to 2.1.4 (#399) 2026-07-24 20:28:47 +02:00
Martin 8f7cc5824b longer prerender caching 2 (#393)
Try to further reduce load on Vercel. ISR expiration time is now 3 day
minus 1 hour, and I will set `UPDATE_AFTER_HOURS` to 3 days.
2026-07-23 21:34:27 +02:00
Martin 1d7ea960f5 switch from stargazers.totalCount to stargazerCount (#391) 2026-07-23 20:30:54 +02:00
shuaixr 2b908ad146 fix: update Vercel deploy link to point to the backend directory (#382)
Fixes #381
2026-07-23 14:23:45 +02:00
Marco Pasqualetti 63ca91e1b1 feat(core): start fetchers TypeScript migration (retryer generic + gist) (#370)
Another step of #140, continues #322. 
Starts migrating `fetchers/`.

- `retryer` is now generic over the response payload:
`retryer<TData>(…)` returns `AxiosResponse<TData & ResponseErrors>` so
callers get their own `response.data` shape while the retryer still
reads `errors`/`message`.
  Defaults to `unknown` so existing callers are unaffected.
- `src/fetchers/types.d.ts` ➡️ `types.ts` (references are extensionless,
so they resolve unchanged).
- `gist` fetcher + test ➡️ `.ts`, using `retryer<GistQueryResponse>(…)`.

> [!WARNING]
> Consider reviewing #375 first, as it resolves the "Resource limits for
this query exceeded" issue.
> This PR will likely conflict with it, so it's better to merge #375
first.
2026-07-23 09:50:09 +02:00
Martin 2007f51dc0 small test improvements (#386) 2026-07-22 20:45:36 +02:00
Martin 3258ad09b0 longer prerender caching (#360)
Load on Vercel is higher than expected. It seems the problem is that
Vercel's CDN evicts our responses before they go stale, because there
are relatively few requests per card. So the repeat-recent functionality
currently causes load without achieving much.

This PR increases ISR expiration time to see how this affects load in
practice. Setting this time via env var might be a task for later.
2026-07-13 10:24:28 +02:00
MartinandMarco Pasqualetti 4ccec4604b fix warnings (#342)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2026-07-12 19:51:53 +02:00
Marco Pasqualetti b81abe07c4 refactor(frontend): consolidate Home card options into a single state object (#344)
`HomeScreen` held ~16 separate `useState` hooks for card options and
threaded them through 34 props into `CustomizeStage`.
This consolidates them into a single typed `CardOptions` state object
(new `pages/Home/cardOptions.ts`), updated via a keyed setter.

### Changes

- `CustomizeStage` props go from 34 to 5; `buildCardUrl` takes `userId`,
`selectedCard` and `options`.
- Stage descriptions move into `STAGE_LABELS`, replacing a positional
string array in the JSX.
- The three GitHub-URL paste handlers in `CustomizeStage` share one
`pastedPathTail` helper.
- A failed OAuth `authenticate` call left the page stuck on the loading
spinner;
the exchange now runs in `try/catch/finally` and the redirect is parsed
with
  `URLSearchParams` instead of substring matching.
2026-07-12 17:49:10 +02:00
Marco Pasqualetti b0100ca48e fix(core): validate username / gist id against the safe-character pattern (#356)
- Related to #342 

`stats` and `pin` already reject `username`/`repo`/`owner` that don't
match `safePattern` (`/^[-\w/.,]+$/`), but `top-langs`, `wakatime`, and
`gist` didn't.

This applies the same guard:

- `top-langs` + `wakatime`: reject a `username` containing unsafe
characters.
Matters most for `wakatime`, where `username` is interpolated into the
outbound request URL path.
- `gist`: same guard on its `id` param.

All three return the existing `error - permanent` card before any fetch.

## Additional change

Simplified the `layout` / `stats_format` checks in `top-langs`: 
dropped the redundant `typeof x !== "string" ||` (a non-string already
fails `.includes(...)`),
keeping the `x !== undefined` guard so the params stay optional.
2026-07-12 14:23:40 +02:00
Shaurya Sharmaandmartin-mfg 7c7fca420b Fix link to calculateRank source file in docs (#351)
The advanced documentation file in the docs/ folder incorrectly links to
a nonexistent folder called "backend/" for the calculate rank logic.
This PR fixes the broken link by changing the link to
"packages/core/src/calculateRank.js".

---------

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-07-11 12:48:00 +02:00
Martin 1d9db07ed4 fix color of theme switcher (#343)
The latest daisyui upgrade changed the theme switcher's color in light
mode. This PR reverts that change.
2026-07-10 19:49:41 +02:00
Martin 4043488ee0 adjust repeat-recent (#341)
* GitHub actually triggers the cron workflow every few hours. Because
they are overloaded I suppose. This PR is an attempt to give the
workflow more chances to be triggered.
* Sometimes we run into secondary rate limiting of the GitHub API when
repeating recent requests. Having less workers should fix this.
2026-07-07 16:21:48 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 807aa453d8 ci(deps): Bump actions/checkout from 6.0.3 to 7.0.0 (#299)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3
to 7.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>block checking out fork pr for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the
minor-actions-dependencies group across 1 directory by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2458">actions/checkout#2458</a></li>
<li>Bump flatted from 3.3.1 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2460">actions/checkout#2460</a></li>
<li>Bump js-yaml from 4.1.0 to 4.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2461">actions/checkout#2461</a></li>
<li>Bump <code>@​actions/core</code> and
<code>@​actions/tool-cache</code> and Remove uuid by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2459">actions/checkout#2459</a></li>
<li>upgrade module to esm and update dependencies by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2463">actions/checkout#2463</a></li>
<li>Bump the minor-npm-dependencies group across 1 directory with 3
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2462">actions/checkout#2462</a></li>
<li>getting ready for checkout v7 release by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2464">actions/checkout#2464</a></li>
<li>update error wording by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2467">actions/checkout#2467</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6.0.3...v7.0.0">https://github.com/actions/checkout/compare/v6.0.3...v7.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v7.0.0</h2>
<ul>
<li>Block checking out fork PR for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the
minor-actions-dependencies group across 1 directory by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2458">actions/checkout#2458</a></li>
<li>Bump flatted from 3.3.1 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2460">actions/checkout#2460</a></li>
<li>Bump js-yaml from 4.1.0 to 4.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2461">actions/checkout#2461</a></li>
<li>Bump <code>@​actions/core</code> and
<code>@​actions/tool-cache</code> and Remove uuid by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2459">actions/checkout#2459</a></li>
<li>upgrade module to esm and update dependencies by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2463">actions/checkout#2463</a></li>
<li>Bump the minor-npm-dependencies group across 1 directory with 3
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2462">actions/checkout#2462</a></li>
</ul>
<h2>v6.0.3</h2>
<ul>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"><code>9c091bb</code></a>
update error wording (<a
href="https://redirect.github.com/actions/checkout/issues/2467">#2467</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/1044a6dea927916f2c38ba5aeffbc0a847b1221a"><code>1044a6d</code></a>
getting ready for checkout v7 release (<a
href="https://redirect.github.com/actions/checkout/issues/2464">#2464</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/f0282184c7ce73ab54c7e4ab5a617122602e575f"><code>f028218</code></a>
Bump the minor-npm-dependencies group across 1 directory with 3 updates
(<a
href="https://redirect.github.com/actions/checkout/issues/2462">#2462</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/d914b262ffc244530a203ab40decab34c3abf34d"><code>d914b26</code></a>
upgrade module to esm and update dependencies (<a
href="https://redirect.github.com/actions/checkout/issues/2463">#2463</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/537c7ef99cef6e5ddb5e7ff5d16d14510503801d"><code>537c7ef</code></a>
Bump <code>@​actions/core</code> and <code>@​actions/tool-cache</code>
and Remove uuid (<a
href="https://redirect.github.com/actions/checkout/issues/2459">#2459</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/130a169078a413d3a5246a393625e8e742f387f6"><code>130a169</code></a>
Bump js-yaml from 4.1.0 to 4.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2461">#2461</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/7d09575332117a40b46e5e020664df234cd416f3"><code>7d09575</code></a>
Bump flatted from 3.3.1 to 3.4.2 (<a
href="https://redirect.github.com/actions/checkout/issues/2460">#2460</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/0f9f3aa320cb53abeb534aeb54048075d9697a0e"><code>0f9f3aa</code></a>
Bump actions/publish-immutable-action (<a
href="https://redirect.github.com/actions/checkout/issues/2458">#2458</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/f9e715a95fcd1f9253f77dd28f11e88d2d6460c7"><code>f9e715a</code></a>
block checking out fork pr for pull_request_target and workflow_run (<a
href="https://redirect.github.com/actions/checkout/issues/2454">#2454</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=6.0.3&new-version=7.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 09:34:52 +02:00
github-actions[bot]andmartin-mfg 6f1ad4b732 Update languages JSON (#321)
The
[update-langs](https://github.com/stats-organization/github-stats-extended/actions/workflows/update-langs.yml)
action found new/updated languages in the [upstream languages JSON
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml).

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-07-06 09:33:34 +02:00
Martin 536199bf4a several dependency upgrades again (#336) 2026-07-06 09:25:04 +02:00
Martin a2bb443789 several dependency upgrades (#334) 2026-07-06 08:24:06 +02:00
Marco Pasqualetti 5da671b171 ci: install only chromium for Playwright e2e (#329)
The frontend Playwright config only enables the chromium project, so CI
was needlessly downloading Firefox and WebKit.
Scope `playwright install` to `chromium` to speed up the e2e job.
2026-07-05 09:53:08 +02:00
Marco Pasqualettiandmartin-mfg ed9d284973 feat(frontend): build card URLs with a typed per-card builder (#328)
Card URLs were built by hand-concatenating paths and query strings in
several places.
This replaces that with a typed, immutable per-card URL builder
(`cardUrl(cardType)`), so URLs are assembled in one place with proper
encoding.

## Changes

- New `models/CardUrl.ts`: one builder per card type, exposing only the
params that card supports.
`URLSearchParams` handles joining + encoding. Also carries a per-card
`filename()` for downloads.
- `getFullSuffix.ts` ➡️ `buildCardUrl.ts`: returns a builder instead of
a string.
- The builder is passed as a prop through all card components; no
hand-written `?`/`&` concatenation left.
- New `useCardDescriptor` hook resolves per-card display metadata (guest
hint + link) and owns the
Gist URL fetch, replacing the repeated `switch (selectedCard)` blocks in
`Home`.
- Added tests for `CardUrl`.

### Notes

- Params now go through `URLSearchParams`, so commas become `%2C` and
spaces `%20`.
  The server decodes these before use, so rendered cards are unchanged.
- Builders are `useMemo`'d in `Home` to keep stable references.

---------

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-07-05 09:48:23 +02:00
Marco Pasqualettiandmartin-mfg 748d8c5430 refactor(frontend): replace eslint react plugins with @eslint-react/eslint-plugin (#326)
- Fixes #325

`eslint-plugin-react@7.37.5` doesn't declare support for ESLint 10,
producing an unmet-peer-dependency warning.

## Changes

- Swapped `eslint-plugin-react` ➡️ `@eslint-react/eslint-plugin` and
adopted its `recommended-typescript` config.
This also replaces `eslint-plugin-react-hooks` (the preset ships
equivalent `rules-of-hooks`/`exhaustive-deps`), so both old plugins are
removed.
- Fixed the violations the new preset surfaced:
  - IIFEs-in-JSX in `Home.tsx` ➡️ lifted to `const` bindings.
  - Ref naming (`*Ref`).
- 4 `set-state-in-effect` cases ➡️ rewritten as render-phase state
adjustment ([React
docs](https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)).
- Extracted the shared debounced-input logic from
`TextSection`/`NumericSection` into a `useDebouncedField` hook.
- Added a standalone Playwright spec (`e2e/app-trends-auth.spec.ts`)
covering the `AppTrends` auth-driven stage transition.

---------

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-07-03 16:27:36 +02:00
Marco Pasqualetti fef8e90c86 feat(core): migrate common/ render, ops, http, calculateRank to TypeScript (#322)
Second wave of the `packages/core` TypeScript migration (#140),
continuing #292.
Converts the remaining `common/` modules and their tests from JSDoc
`.js` to real `.ts`.

## Changes

- **Modules ➡️ `.ts`**: `common/http`, `common/ops`, `common/render`,
`calculateRank`.
- **Tests ➡️ `.ts`**: `ops`, `flexLayout`, `render`, `calculateRank`.
- **`src/_emoji-name-map.d.ts`**: `declare module` shim.
`emoji-name-map` ships no types
and has no `@types` package (same as the existing
`@uppercod/css-to-object` shim).
- **`common/color.ts`**: widened `getCardColors` optional params to `?:
string | undefined`
so `renderError` can forward possibly-`undefined` colors under
`exactOptionalPropertyTypes`.
2026-07-03 16:26:23 +02:00
Martin fa82216dfe fix primary color in step 4 (#317) 2026-06-30 09:04:48 +02:00
Marco Pasqualettiandmartin-mfg ef3f207dbf feat(frontend): add light/dark theming with theme picker (#300)
Adds light/dark theming to the frontend using the existing DaisyUI +
Tailwind setup.

- Theme picker dropdown in the header; selection is stored in Redux and
persisted to `localStorage`.
The toggle and each option show a sun/moon icon (shared `ThemeIcon`
component) so light/dark are easy to tell apart.
- Migrated hardcoded colors to DaisyUI tokens so the whole UI (and
toasts) follow the theme.
- `Button` now uses `variant` / `size` / `outline` props instead of raw
`btn-*` classes.
- Theme preview cards: (Select Theme grid and the final Display card)
sit on a two-tone backdrop (light or dark), chosen by the card theme so
previews stay consistent regardless of the app theme.
- Smooth theme-switch transitions: surfaces fade gently while
interactive elements stay snappy; respects `prefers-reduced-motion`.
- Added an e2e test for the picker.


<details><summary>Step 1</summary>
<p>


<img width="1710" height="811" alt="Screenshot 2026-06-24 at 06 54 40"
src="https://github.com/user-attachments/assets/38d629b5-eaa3-4fb1-b976-c56e7a840ced"
/>

<img width="1706" height="808" alt="Screenshot 2026-06-24 at 06 54 32"
src="https://github.com/user-attachments/assets/3811a574-016c-4a93-82a9-a340519f5257"
/>


</p>
</details> 

<details><summary>Step 4</summary>
<p>


<img width="1709" height="899" alt="Screenshot 2026-06-24 at 06 56 22"
src="https://github.com/user-attachments/assets/4053b938-1faa-427b-b621-e79ce7b2c7b1"
/>

<img width="1705" height="894" alt="Screenshot 2026-06-24 at 06 55 41"
src="https://github.com/user-attachments/assets/8aace78a-f1ca-4d29-b57e-938f7b473e67"
/>

</p>
</details>

---------

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-06-29 10:04:00 +02:00
Marco Pasqualetti 35fbec8004 feat(core): begin TypeScript migration of common modules (#292)
Starts migrating `packages/core` to TypeScript (#140).

- Converted the `common/` modules to `.ts`: 
- `Card`, `I18n`, `color`, `config`, `constants`, `error`, `fmt`,
`html`, `icons`, `retryer`, `translations`.
- Replaced JSDoc types and `@ts-check`/`@ts-ignore` comments with real
TypeScript types.
- Enabled type-checking on the source and moved `noCheck` into the build
config.
- Converted the tests that target those modules to `.ts`: 
   - `fmt`, `i18n`, `card`, `color`, `html`, `retryer`.
- Converted `tests/_setup.ts` and moved the jest-dom matcher typing
there: augmenting vitest's `Matchers` interface (per the vitest docs) so
matchers like `toHaveAttribute` are typed on `expect(...)`.
- Added a `tests/css-to-object.d.ts` shim so `@uppercod/css-to-object`
resolves under `nodenext` (it ships types but doesn't expose them via
`exports`).
2026-06-22 19:27:14 +02:00
Marco Pasqualetti 3ef27f872c chore(vscode): update prettier extension id (#293)
Update the Prettier extension ID.

Avoids prompting the user to install "Prettier (DO NOT USE)" extension.

<img width="473" height="130" alt="image"
src="https://github.com/user-attachments/assets/ea422ba1-262c-402b-a8f0-cf4cbcbff011"
/>
2026-06-21 10:15:43 +02:00
Martin 5557e9c772 upgrade several actions (#290) 2026-06-19 00:04:12 +02:00
Martin 570fc4aaed bump core version to 2.1.3 (#289) 2026-06-18 23:42:01 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 2ac454a177 build(deps-dev): Bump turbo from 2.9.17 to 2.9.18 (#286)
Bumps [turbo](https://github.com/vercel/turborepo) from 2.9.17 to
2.9.18.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vercel/turborepo/releases">turbo's
releases</a>.</em></p>
<blockquote>
<h2>Turborepo v2.9.18</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>Changelog</h3>
<ul>
<li>release(turborepo): 2.9.17 by <a
href="https://github.com/github-actions"><code>@​github-actions</code></a>[bot]
in <a
href="https://redirect.github.com/vercel/turborepo/pull/13047">vercel/turborepo#13047</a></li>
<li>ci: Fetch version.txt via API in docs alias failure notification by
<a href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a>
in <a
href="https://redirect.github.com/vercel/turborepo/pull/13050">vercel/turborepo#13050</a></li>
<li>fix: Harden cache archive symlink restore by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13051">vercel/turborepo#13051</a></li>
<li>chore: Remove web UI mode by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13052">vercel/turborepo#13052</a></li>
<li>fix: Harden query server file access by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13053">vercel/turborepo#13053</a></li>
<li>fix: Confine prune patch paths by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13054">vercel/turborepo#13054</a></li>
<li>fix: Prevent git argument injection in SCM refs by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13055">vercel/turborepo#13055</a></li>
<li>fix: Strip special mode bits from cache restore by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13056">vercel/turborepo#13056</a></li>
<li>fix: Contain incremental cache outputs by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13057">vercel/turborepo#13057</a></li>
<li>fix(turborepo): Normalize Windows daemon path hash by <a
href="https://github.com/Balance8"><code>@​Balance8</code></a> in <a
href="https://redirect.github.com/vercel/turborepo/pull/13020">vercel/turborepo#13020</a></li>
<li>fix: Preserve vt100 cell byte counts by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13058">vercel/turborepo#13058</a></li>
<li>fix: Separate artifact signature fields by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13059">vercel/turborepo#13059</a></li>
<li>fix: Validate OidHash hex buffers by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13060">vercel/turborepo#13060</a></li>
<li>fix: Block self-hosted login URLs from attempting to use Vercel's
SSO by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13061">vercel/turborepo#13061</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Balance8"><code>@​Balance8</code></a>
made their first contribution in <a
href="https://redirect.github.com/vercel/turborepo/pull/13020">vercel/turborepo#13020</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/vercel/turborepo/compare/v2.9.17...v2.9.18">https://github.com/vercel/turborepo/compare/v2.9.17...v2.9.18</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vercel/turborepo/commit/3bdce3277d2e61cdbf29f244a515dd4b896d2556"><code>3bdce32</code></a>
publish 2.9.18 to registry</li>
<li><a
href="https://github.com/vercel/turborepo/commit/2a76556b9457c514fa597a09fef23da2e1250c8c"><code>2a76556</code></a>
fix: Block self-hosted login URLs from attempting to use Vercel's SSO
(<a
href="https://redirect.github.com/vercel/turborepo/issues/13061">#13061</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/da8e3487acce10039425180c42875d7ccc484ed5"><code>da8e348</code></a>
fix: Validate OidHash hex buffers (<a
href="https://redirect.github.com/vercel/turborepo/issues/13060">#13060</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/3018717c28fc8f1ecffa2c92e3260be0dc0165aa"><code>3018717</code></a>
fix: Separate artifact signature fields (<a
href="https://redirect.github.com/vercel/turborepo/issues/13059">#13059</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/34514e278822ccbe3a083730b7032709ef16f85c"><code>34514e2</code></a>
fix: Preserve vt100 cell byte counts (<a
href="https://redirect.github.com/vercel/turborepo/issues/13058">#13058</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/24e2d3498f54fa1f8b729f18764f63cb05072bc6"><code>24e2d34</code></a>
fix(turborepo): Normalize Windows daemon path hash (<a
href="https://redirect.github.com/vercel/turborepo/issues/13020">#13020</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/16dc881cabf5c13d02c27a5d7cfdfeb60736206c"><code>16dc881</code></a>
fix: Contain incremental cache outputs (<a
href="https://redirect.github.com/vercel/turborepo/issues/13057">#13057</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/92e1f8e5365ea5a95e47c3698deb2f7adaa55eba"><code>92e1f8e</code></a>
fix: Strip special mode bits from cache restore (<a
href="https://redirect.github.com/vercel/turborepo/issues/13056">#13056</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/f46f896ef414823582131304c9749e9be011fe37"><code>f46f896</code></a>
fix: Prevent git argument injection in SCM refs (<a
href="https://redirect.github.com/vercel/turborepo/issues/13055">#13055</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/7f353ca3a6f28fbbcce931aeab7be0efe718c466"><code>7f353ca</code></a>
fix: Confine prune patch paths (<a
href="https://redirect.github.com/vercel/turborepo/issues/13054">#13054</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/vercel/turborepo/compare/v2.9.17...v2.9.18">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=turbo&package-manager=npm_and_yarn&previous-version=2.9.17&new-version=2.9.18)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 10:57:59 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b837c50885 build(deps-dev): Bump @types/node from 24.13.1 to 24.13.2 in the typescript group (#285)
Bumps the typescript group with 1 update:
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).

Updates `@types/node` from 24.13.1 to 24.13.2
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/node&package-manager=npm_and_yarn&previous-version=24.13.1&new-version=24.13.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 10:57:37 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dfb07a69b9 ci(deps): Bump actions/checkout from 6.0.2 to 6.0.3 (#270)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2
to 6.0.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Update changelog by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2357">actions/checkout#2357</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>Update changelog for v6.0.3 by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2446">actions/checkout#2446</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/yaananth"><code>@​yaananth</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6...v6.0.3">https://github.com/actions/checkout/compare/v6...v6.0.3</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v6.0.3</h2>
<ul>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>v4.2.0</h2>
<ul>
<li>Add Ref and Commit outputs by <a
href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
<li>Dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
<a
href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
</ul>
<h2>v4.1.7</h2>
<ul>
<li>Bump the minor-npm-dependencies group across 1 directory with 4
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
<li>Bump actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
<li>Check out other refs/* by commit by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/df4cb1c069e1874edd31b4311f1884172cec0e10"><code>df4cb1c</code></a>
Update changelog for v6.0.3 (<a
href="https://redirect.github.com/actions/checkout/issues/2446">#2446</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/1cce3390c2bfda521930d01229c073c7ff920824"><code>1cce339</code></a>
Fix checkout init for SHA-256 repositories (<a
href="https://redirect.github.com/actions/checkout/issues/2439">#2439</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/900f2210b1d28bbbd0bd22d17926b9e224e8f231"><code>900f221</code></a>
fix: expand merge commit SHA regex and add SHA-256 test cases (<a
href="https://redirect.github.com/actions/checkout/issues/2414">#2414</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/0c366fd6a839edf440554fa01a7085ccba70ac98"><code>0c366fd</code></a>
Update changelog (<a
href="https://redirect.github.com/actions/checkout/issues/2357">#2357</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=6.0.2&new-version=6.0.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 22:41:07 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 41b51dcfd9 build(deps-dev): Bump @types/node from 24.13.0 to 24.13.1 in the typescript group across 1 directory (#276)
Bumps the typescript group with 1 update in the / directory:
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).

Updates `@types/node` from 24.13.0 to 24.13.1
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 22:39:53 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7687c47d85 build(deps-dev): Bump the eslint group across 1 directory with 2 updates (#284)
Bumps the eslint group with 2 updates in the / directory:
[eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) and
[typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).

Updates `eslint-plugin-jsdoc` from 63.0.1 to 63.0.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/gajus/eslint-plugin-jsdoc/releases">eslint-plugin-jsdoc's
releases</a>.</em></p>
<blockquote>
<h2>v63.0.2</h2>
<h2><a
href="https://github.com/gajus/eslint-plugin-jsdoc/compare/v63.0.1...v63.0.2">63.0.2</a>
(2026-06-06)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>allow typedef returns that may be void; fixes <a
href="https://redirect.github.com/gajus/eslint-plugin-jsdoc/issues/1390">#1390</a>
(<a
href="https://redirect.github.com/gajus/eslint-plugin-jsdoc/issues/1699">#1699</a>)
(<a
href="https://github.com/gajus/eslint-plugin-jsdoc/commit/319e84b21dfc21213f42998beee26e199660747c">319e84b</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/gajus/eslint-plugin-jsdoc/commit/50a7fbcf4abf7b27ca2774f65397f2f74395ea41"><code>50a7fbc</code></a>
chore: update semver and devDeps.</li>
<li><a
href="https://github.com/gajus/eslint-plugin-jsdoc/commit/6041995ba873b5641e3b9bd1f95de004b4b9e7da"><code>6041995</code></a>
docs: fix in output</li>
<li><a
href="https://github.com/gajus/eslint-plugin-jsdoc/commit/319e84b21dfc21213f42998beee26e199660747c"><code>319e84b</code></a>
fix: allow typedef returns that may be void; fixes <a
href="https://redirect.github.com/gajus/eslint-plugin-jsdoc/issues/1390">#1390</a>
(<a
href="https://redirect.github.com/gajus/eslint-plugin-jsdoc/issues/1699">#1699</a>)</li>
<li>See full diff in <a
href="https://github.com/gajus/eslint-plugin-jsdoc/compare/v63.0.1...v63.0.2">compare
view</a></li>
</ul>
</details>
<br />

Updates `typescript-eslint` from 8.60.1 to 8.61.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">typescript-eslint's
releases</a>.</em></p>
<blockquote>
<h2>v8.61.0</h2>
<h2>8.61.0 (2026-06-08)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>ast-spec:</strong> change type of
<code>UnaryExpression.prefix</code> to always <code>true</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12372">#12372</a>)</li>
<li><strong>ast-spec:</strong> tighten types of
<code>ArrowFunction</code>, <code>YieldExpression</code>,
<code>TSTypePredicate</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12373">#12373</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>rule-schema-to-typescript-types:</strong> respect ECMAScript
line terminators (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12374">#12374</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Kirk Waiblinger <a
href="https://github.com/kirkwaiblinger"><code>@​kirkwaiblinger</code></a></li>
<li>lumir</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.61.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md">typescript-eslint's
changelog</a>.</em></p>
<blockquote>
<h2>8.61.0 (2026-06-08)</h2>
<p>This was a version bump only for typescript-eslint to align it with
other projects, there were no code changes.</p>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.61.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/typescript-eslint/typescript-eslint/commit/16a5b247affc32af21b695cf96dfd75d7ded50a3"><code>16a5b24</code></a>
chore(release): publish 8.61.0</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.0/packages/typescript-eslint">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 21:01:53 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7917d54fdf build(deps-dev): Bump @types/react from 19.2.16 to 19.2.17 in the react group across 1 directory (#283)
Bumps the react group with 1 update in the / directory:
[@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).

Updates `@types/react` from 19.2.16 to 19.2.17
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/react&package-manager=npm_and_yarn&previous-version=19.2.16&new-version=19.2.17)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 21:01:03 +02:00
Martin 7771a34790 several dependency upgrades (#282) 2026-06-17 20:52:00 +02:00
Martin 1c266ad51e dependency upgrades (#265) 2026-06-07 18:24:46 +02:00
Martin 6258d90604 fix non-mocked frontend requests (#263) 2026-06-07 10:04:19 +02:00
Martin 6d857d0f34 upgrade dependencies (#262) 2026-06-06 12:23:41 +02:00
github-actions[bot]andmartin-mfg e15deeac76 Update languages JSON (#254)
The
[update-langs](https://github.com/stats-organization/github-stats-extended/actions/workflows/update-langs.yml)
action found new/updated languages in the [upstream languages JSON
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml).

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-06-06 09:24:43 +02:00
Martin b5d50fc28f debug playwright (#261) 2026-06-06 09:23:54 +02:00
Marco Pasqualetti 1d394c7fad chore(actions): don’t repeat-recent-requests on forks (#259)
I received a notification about a failed workflow run on my fork of this
repository:

*
https://github.com/marcalexiei/github-stats-extended/actions/runs/26996444955

It looks like the `repeat-recent-requests` workflow is being triggered
on forks, even though it should only run on the canonical repositories.
This PR adds an `if` condition to prevent it from running on forks.

The condition matches the one already used in the `update-languages`
workflow:


https://github.com/stats-organization/github-stats-extended/blob/96d141e8c717f42a1a29e67f64984a19182dd575/.github/workflows/update-langs.yml#L31-L36

---

Additionally, this PR renames `update_tags.yml` to `update-tags.yml` for
consistency with the repository's workflow naming convention.
2026-06-05 19:32:58 +02:00
Martin 76e09ce3b0 improve 'Responsive Card Theme' docs (#260)
- reorder subsections
- shorten `<picture>` code
2026-06-05 18:17:02 +02:00
Martin 96d141e8c7 update documentation, step 2 (#243) 2026-05-21 23:28:09 +02:00
martin-mfg 6310073428 update documentation, step 2 2026-05-21 21:45:14 +02:00
Martin 1d8aaf0fc8 update documentation (#242) 2026-05-21 21:21:31 +02:00
martin-mfg 4a2785e286 update documentation 2026-05-21 21:19:35 +02:00
Martin 16ea9afebe set correct version for 'core' package (#218) 2026-05-08 12:54:54 +02:00
Martin c707e6b7b1 ignore git checks when publishing (#216) 2026-05-08 12:11:03 +02:00
262 changed files with 19578 additions and 9943 deletions
+1 -1
View File
@@ -21,4 +21,4 @@ If you believe someone is violating the code of conduct, we ask that you report
- Repeated harassment of others. In general, if someone asks you to stop, then stop.
- **When we disagree, try to understand why.** Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively. Remember that were different. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesnt mean that theyre wrong. Dont forget that it is human to err and blaming each other doesnt get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.
Based on the [Django Code of Conduct](https://www.djangoproject.com/conduct/).
Based on the [Django Code of Conduct](https://www.djangoproject.com/conduct/).
+50 -21
View File
@@ -10,7 +10,54 @@ pnpm run build:packages
pnpm run dev:frontend
```
The easiest way to run and test the project is to deploy it to Vercel as described in the [deployment guide](../docs/deploy.md).
### Backend server
The wizard renders its previews in the browser, but the docs reference cards by
root-relative path (`/api?username=...`), so those images only load with the backend running:
```bash
pnpm run dev:backend # card endpoints on :9000, proxied by the frontend dev server
```
It needs a [Personal Access Token](../docs/deploy.md#first-step-get-your-personal-access-token-pat) in `apps/backend/.env` (the SQL database is optional):
```
PAT_1=your_token_here
```
You can also deploy to Vercel and test there, as described in the [deployment guide](../docs/deploy.md).
## Tests
```bash
pnpm run test # unit tests
pnpm run lint # eslint
pnpm run typecheck # tsc
```
The **Backend E2E test** in CI compares the cards your branch renders against the ones served by the preview deployment, which is still on the last commit merged to master.
So if your PR changes card output at all, that job goes red until the preview catches up.
It's marked `continue-on-error`, so it won't block your PR, but do open it and check the diff is only what you expected.
If the change to card markup was intentional, update the snapshots:
```bash
pnpm --filter ./packages/core/ run test:update:snapshot
pnpm --filter ./apps/backend/ run test:update:snapshot
```
## GraphQL Queries
The GraphQL queries live in `packages/core/src/graphql/queries/*.graphql`,
and their TypeScript types are generated from GitHub's published schema into `packages/core/src/graphql/generated/`.
Those generated files are committed, so if you change a query, regenerate them and include the result in your PR:
```bash
pnpm --filter ./packages/core/ run generate-graphql-types
```
CI runs `pnpm --filter ./packages/core/ run check-graphql-types`, which fails if the committed types no longer match the queries.
Never edit the generated files by hand — change the `.graphql` file and regenerate.
## Themes Contribution
@@ -20,7 +67,7 @@ We have stopped the addition of new themes to decrease maintenance efforts. If y
GitHub-Stats-Extended supports multiple languages. If we are missing your language, you can contribute it! You can check the currently supported languages [here](../docs/advanced_documentation.md#available-locales).
To contribute your language you need to edit the [backend/src/translations.js](../backend/src/translations.js) file and add a new property to each object where the key is the language code in [ISO 639-1 standard](https://www.andiamo.co.uk/resources/iso-language-codes/) and the value is the translated string.
To contribute your language you need to edit the [packages/core/src/translations.ts](../packages/core/src/translations.ts) file and add a new property to each object where the key is the language code in [ISO 639-1 standard](https://www.andiamo.co.uk/resources/iso-language-codes/) and the value is the translated string.
## Any contributions you make will be under the MIT Software License
@@ -30,25 +77,7 @@ In short, when you submit changes, your submissions are understood to be under t
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/stats-organization/github-stats-extended/issues/new/choose). If there is already an open issue for your bug in the upstream repo [github-readme-stats](https://github.com/anuraghazra/github-readme-stats/issues) you don't need to report it here.
## Frequently Asked Questions (FAQs)
**Q:** How to hide Jupyter Notebook?
> **Ans:** &hide=jupyter%20notebook
**Q:** Language Card is incorrect
> **Ans:** Please read all the related issues/comments before opening any issues regarding language card stats:
>
> - <https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665164174>
>
> - <https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665172181>
**Q:** How to count private stats?
> see [here](../docs/fork.md#private-contributions-support)
### Feature Request
## Feature Request
**Great Feature Requests** tend to have:
+1 -1
View File
@@ -1,3 +1,3 @@
# Security Policy
Please report any security vulnerabilities to github.stats.extended@gmail.com. I will try to respond as quickly as possible, but since I am maintaining this project in my free time, please allow some time for a response.
Please report any security vulnerabilities to github.stats.extended@gmail.com. I will try to respond as quickly as possible, but since I am maintaining this project in my free time, please allow some time for a response.
@@ -14,11 +14,11 @@ runs:
steps:
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- name: Setup Node.js (via input)
if: ${{ inputs.node-version }}
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ inputs.node-version }}
cache: "pnpm"
@@ -26,7 +26,7 @@ runs:
- name: Setup Node.js (via .nvmrc)
if: ${{ !inputs.node-version }}
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
cache: "pnpm"
+7 -1
View File
@@ -4,7 +4,8 @@ updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
interval: cron
cronjob: "45 0 * * 3,6"
open-pull-requests-limit: 20
commit-message:
prefix: "build(deps)"
@@ -57,6 +58,11 @@ updates:
patterns:
- "axios"
- "axios-*"
graphql:
patterns:
- "graphql"
- "@graphql-codegen/*"
- "@octokit/graphql-schema"
# Maintain dependencies for GitHub Actions
- package-ecosystem: github-actions
+17 -5
View File
@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
@@ -59,7 +59,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
@@ -67,8 +67,10 @@ jobs:
- name: Build packages
run: pnpm run build:packages
# Only install chromium since it is the only browser enabled in
# apps/frontend/playwright.config.ts. Update this if more projects are added.
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
run: pnpm exec playwright install --with-deps chromium
- name: Run Playwright tests
run: pnpm --filter ./apps/frontend/ run test:e2e
@@ -81,11 +83,15 @@ jobs:
permissions:
contents: read
# Compares the cards we render here against the preview deployment,
# which is still on the last commit merged on master.
# So any PR that changes card output turns this red until the preview catches up.
# Worth a look, just not always a bug.
continue-on-error: true
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
@@ -106,7 +112,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
@@ -120,5 +126,11 @@ jobs:
- name: Lint (knip)
run: pnpm run lint:knip
- name: Lint (deduped deps)
run: pnpm run lint:deps
- name: Lint (generated GraphQL types)
run: pnpm --filter ./packages/core/ run check-graphql-types
- name: Typecheck
run: pnpm run typecheck
@@ -11,50 +11,37 @@ on:
# │ │ │ │ │
# │ │ │ │ │
# * * * * *
- cron: "0 0 */30 * *"
- cron: "45 22 */7 * *"
permissions:
actions: read
checks: read
contents: write
deployments: read
issues: read
discussions: read
packages: read
pages: read
pull-requests: write
repository-projects: read
security-events: read
statuses: read
jobs:
update-languages:
if: |
github.repository == 'anuraghazra/github-readme-stats' ||
github.repository == 'stats-organization/github-readme-stats' ||
github.repository == 'stats-organization/github-stats-extended'
name: Update supported languages
if: github.repository == 'stats-organization/github-stats-extended'
name: Generate Language Colors
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
- name: Run update-languages-json.js script
run: pnpm --filter ./packages/core/ run generate-langs-json
run: pnpm --filter ./packages/core/ run generate-language-colors
- name: Create Pull Request if upstream language file is changed
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
commit-message: "feat(backend): update languages JSON"
commit-message: "feat(core): update language colors"
branch: "update_langs/patch"
delete-branch: true
title: Update languages JSON
body: "The
[update-langs](https://github.com/stats-organization/github-stats-extended/actions/workflows/update-langs.yml)
action found new/updated languages in the [upstream languages JSON
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml)."
title: Update languages colors
body: >-
The [generate-language-colors](https://github.com/stats-organization/github-stats-extended/actions/workflows/generate-language-colors.yml)
workflow detected changes in the [upstream languages JSON
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml) and regenerated the local language colors JSON file.
labels: "ci"
-53
View File
@@ -1,53 +0,0 @@
name: Generate Theme Readme
on:
push:
branches:
- master
paths:
- "packages/core/src/themes/index.js"
workflow_dispatch:
permissions: {}
jobs:
generate-theme-doc:
runs-on: ubuntu-latest
name: Generate theme doc
permissions:
actions: read
checks: read
contents: write
deployments: read
issues: read
discussions: read
packages: read
pages: read
pull-requests: write
repository-projects: read
security-events: read
statuses: read
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
- name: Generate readme
run: |
pnpm --filter ./packages/core/ run theme-readme-gen
- name: Create Pull Request if themes README has changed
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8
with:
commit-message: "feat(backend): update themes README"
branch: "update_themes_readme/patch"
delete-branch: true
title: Update themes README
body: "The
[generate-theme-doc](https://github.com/stats-organization/github-stats-extended/actions/workflows/generate-theme-doc.yml)
action found new/updated languages in the [upstream languages JSON
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml)."
labels: "ci"
@@ -0,0 +1,54 @@
name: Generate Theme Readme
on:
push:
branches:
- master
paths:
- "packages/core/src/themes/index.ts"
workflow_dispatch:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# │ │ │ │ │
# │ │ │ │ │
# │ │ │ │ │
# * * * * *
- cron: "45 22 */7 * *"
permissions: {}
jobs:
generate-theme-doc:
runs-on: ubuntu-latest
name: Generate Theme Readme
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
- name: Generate readme
run: pnpm run generate-theme-readme
- name: Create Pull Request if themes README has changed
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
commit-message: "feat(core): update themes README"
branch: "update_themes_readme/patch"
delete-branch: true
title: Update themes README
body: >-
The [generate-theme-readme](https://github.com/stats-organization/github-stats-extended/actions/workflows/generate-theme-readme.yml)
workflow detected changes in
[`packages/core/src/themes/index.ts`](https://github.com/stats-organization/github-stats-extended/blob/master/packages/core/src/themes/index.ts)
and regenerated the themes README.
labels: "ci"
+2 -2
View File
@@ -14,7 +14,7 @@ jobs:
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
@@ -27,4 +27,4 @@ jobs:
pnpm --filter @stats-organization/github-readme-stats-core pack --dry-run
- name: Publish core package to npm
run: pnpm --filter @stats-organization/github-readme-stats-core publish --provenance --access public
run: pnpm --filter @stats-organization/github-readme-stats-core publish --provenance --access public --no-git-checks
+18 -11
View File
@@ -10,22 +10,29 @@ on:
# │ │ │ │ │
# │ │ │ │ │
# * * * * *
- cron: "45 * * * *"
- cron: "15,45 * * * *"
workflow_dispatch:
permissions: {}
jobs:
triggerRepeatRecent:
repeat-recent-requests:
if: github.repository == 'stats-organization/github-stats-extended'
name: Trigger server to update cached data.
runs-on: ubuntu-latest
steps:
- name: Make Request
id: myRequest
uses: fjogeleit/http-request-action@551353b829c3646756b2ec2b3694f819d7957495 # v2
with:
url: "https://github-stats-extended.vercel.app/api/repeat-recent"
method: "POST"
timeout: "300000"
- name: Show Response
run: |
echo ${{ steps.myRequest.outputs.response }}
echo ${{ steps.myRequest.outputs.status }}
args=(
--silent --show-error # no progress bar, but still report errors
--request POST # the only thing the endpoint checks
--location # a redirect would otherwise pass the status check
--max-redirs 5 # bounded well below curl's default of 50
--speed-limit 1 --speed-time 300 # give up after 300s of no traffic
--output response.txt # keep the body out of the status capture
--write-out "%{http_code}" # status is all we capture on stdout
)
status=$(curl "${args[@]}" "https://github-stats-extended.vercel.app/api/repeat-recent")
cat response.txt
printf '\nstatus: %s\n' "$status"
test "$status" -lt 400
@@ -14,5 +14,5 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: haya14busa/action-update-semver@7d2c558640ea49e798d46539536190aff8c18715 # v1.5.1
+2 -1
View File
@@ -16,11 +16,12 @@ apps/backend/vercel_token
apps/backend-copy
apps/frontend/.env
# Astro's generated types and content store
apps/frontend/.astro
.turbo
build
build-ts
*.tsbuildinfo
# IDE
-4
View File
@@ -1,5 +1 @@
pnpm-lock.yaml
# https://github.com/stats-organization/github-stats-extended/pull/26#discussion_r2709033732
# Avoid prettier format on md files to simplify merge on upstream repo
*.md
+1 -1
View File
@@ -1,7 +1,7 @@
{
"recommendations": [
"yzhang.markdown-all-in-one",
"prettier.prettier-vscode",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"github.vscode-github-actions"
]
+1 -2
View File
@@ -3,8 +3,7 @@
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.tabSize": 2,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"cSpell.words": ["Wakatime"]
"cSpell.words": ["retryer", "Wakatime"]
}
+32 -23
View File
@@ -5,29 +5,40 @@
<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 an [extended version](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.
GitHub-Stats-Extended 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) about your GitHub contributions, your top languages and more. You can [customize](#documentation) the cards via multiple parameters.
## Table of Contents
# 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)
- [Self-Hosting](#self-hosting)
- [Documentation](#documentation)
- [Acknowledgements](#acknowledgements)
- [Contributing](#contributing)
# 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!
## Quick Start
-----------------------
Copy and paste this into your markdown, then change the `?username=` value to your GitHub username:
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!
```md
[![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra)](https://github.com/stats-organization/github-stats-extended)
```
As a more comfortable alternative, use the [card wizard](https://github-stats-extended.vercel.app/frontend) to configure your card visually. Then copy the generated markdown 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).
## Migration from github-readme-stats
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
```
GitHub-Stats-Extended aims to be fully compatible with github-readme-stats. For details see [Compatibility Notes](docs/fork.md#compatibility-notes).
## Card Types
# Card Types
- Show your GitHub statistics:
![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra)
@@ -52,16 +63,14 @@ As more comfortable alternative, use the [GitHub-Stats-Extended Wizard](https://
[![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
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
# Acknowledgements
This project is based on [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). On top of their functionality I added several new features and improvements. See [Fork Information](docs/fork.md) for a list of changes. The frontend I added to the project 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! ❤️
The [card wizard](https://github-stats-extended.vercel.app/frontend) offers some essential customization options. For more advanced customization and other project info check out the [advanced documentation](docs/advanced_documentation.md).
# Self-Hosting
Since the GitHub API only allows a limited number of requests per hour, the public instance of GitHub-Stats-Extended at https://github-stats-extended.vercel.app/api could possibly hit the rate limiter. If you host your own instance you do not have to worry about anything. Also, if you don't want to give my GitHub-Stats-Extended instance access to your private contributions but still want to include these contributions in your stats, you can simply host your own instance.
## Acknowledgements
See [Deploy on your own](docs/deploy.md) for various deployment options.
This project is based on [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). The card wizard 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! ❤️
# Contributing
Contributions are welcome!
## Contributing
Contributions are welcome!
-2
View File
@@ -5,7 +5,5 @@ coverage
scripts
tests
.env
**/*.md
**/*.svg
.eslintrc.json
codecov.yml
@@ -1,5 +1,5 @@
{
"expiration": 39600,
"expiration": 255600,
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
"passQuery": true
}
@@ -1,5 +1,5 @@
{
"expiration": 39600,
"expiration": 255600,
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
"passQuery": true
}
@@ -1,5 +1,5 @@
{
"expiration": 39600,
"expiration": 255600,
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
"passQuery": true
}
@@ -1,5 +1,5 @@
{
"expiration": 39600,
"expiration": 255600,
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
"passQuery": true
}
@@ -1,5 +1,5 @@
{
"expiration": 39600,
"expiration": 255600,
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
"passQuery": true
}
+11 -1
View File
@@ -4,7 +4,17 @@ import router from "./router.js";
const app = express();
app.use(router);
app.use((req, res) => {
/*
* Vercel resolves `/api/pin/` to the same function as `/api/pin`, but the router matches the path exactly.
* Drop the trailing slash so local requests behave like deployed ones.
*/
const [pathname, query] = req.url.split("?", 2);
if (pathname !== "/" && pathname?.endsWith("/")) {
req.url = pathname.slice(0, -1) + (query === undefined ? "" : `?${query}`);
}
return router(req, res);
});
const port = process.env.PORT || process.env.port || 9000;
app.listen(port, "0.0.0.0", () => {
+4 -3
View File
@@ -8,6 +8,7 @@
"private": true,
"main": "index.js",
"scripts": {
"dev": "node --env-file-if-exists=.env express.js",
"test": "vitest",
"test:update:snapshot": "vitest -u",
"test:e2e": "vitest --config vitest.config.e2e.ts",
@@ -18,12 +19,12 @@
"devDependencies": {
"axios-mock-adapter": "2.1.0",
"express": "5.2.1",
"jsdom": "29.0.2",
"jsdom": "catalog:default",
"vitest": "catalog:default"
},
"dependencies": {
"axios": "^1.15.2",
"@stats-organization/github-readme-stats-core": "workspace:^",
"pg": "^8.20.0"
"axios": "catalog:default",
"pg": "^8.22.0"
}
}
+1 -1
View File
@@ -66,6 +66,6 @@ export async function repeatRecentRequests() {
if (urls.length === 0) {
console.log("No recent requests found.");
} else {
await makeRequests(urls, 5);
await makeRequests(urls, 3);
}
}
+3 -5
View File
@@ -29,7 +29,7 @@ const STATS_MOCK_RESPONSE = {
followers: { totalCount: 0 },
repositories: {
totalCount: 1,
nodes: [{ name: REPO, stargazers: { totalCount: 1 } }],
nodes: [{ name: REPO, stargazerCount: 1 }],
pageInfo: {
hasNextPage: false,
endCursor: "cursor",
@@ -110,9 +110,7 @@ const REPO_MOCK_RESPONSE = {
isPrivate: false,
isArchived: false,
isTemplate: false,
stargazers: {
totalCount: 1,
},
stargazerCount: 1,
description: "Simple cra test repo.",
primaryLanguage: {
color: "#f1e05a",
@@ -203,7 +201,7 @@ beforeAll(async () => {
return [200, STATS_MOCK_RESPONSE];
}
if (query.includes("query userInfo") && variables?.login === USER) {
if (query.includes("query topLanguages") && variables?.login === USER) {
return [200, TOP_LANGS_MOCK_RESPONSE];
}
@@ -1,5 +1,249 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test /api contract > should match the gradient-background response snapshot 1`] = `
{
"content": "<svg width="450" height="195" viewBox="0 0 450 195" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId">Anurag Hazra's GitHub Stats, Rank: A+</title>
<desc id="descId">Total Stars Earned: 14099, Total Commits (last year) : 200, Total PRs: 4000, Total Issues: 340, Contributed to (last year): 51</desc>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #ffffff;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #ffffff;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.stat { font-size:12px; }
}
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.rank-text {
font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: #ffffff;
animation: scaleInAnimation 0.3s ease-in-out forwards;
}
.rank-percentile-header {
font-size: 14px;
}
.rank-percentile-text {
font-size: 16px;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
.icon {
fill: #ffffff;
display: none;
}
.rank-circle-rim {
stroke: #ffffff;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #ffffff;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
@keyframes rankAnimation {
from {
stroke-dashoffset: 251.32741228718345;
}
to {
stroke-dashoffset: 25.665014275610705;
}
}
@media (prefers-color-scheme: dark) {
.header { fill: #2f80ed; }
.card-bg { fill: url(#gradient-dark); stroke: #e4e2e2; }
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #434d58;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.stat { font-size:12px; }
}
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.rank-text {
font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58;
animation: scaleInAnimation 0.3s ease-in-out forwards;
}
.rank-percentile-header {
font-size: 14px;
}
.rank-percentile-text {
font-size: 16px;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
.icon {
fill: #4c71f2;
display: none;
}
.rank-circle-rim {
stroke: #2f80ed;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #2f80ed;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
@keyframes rankAnimation {
from {
stroke-dashoffset: 251.32741228718345;
}
to {
stroke-dashoffset: 25.665014275610705;
}
}
}
/* Animations */
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeInAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
<defs>
<linearGradient id="gradient" gradientTransform="rotate(35)" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#4158d0"/>,<stop offset="50%" stop-color="#c850c0"/>,<stop offset="100%" stop-color="#ffcc70"/>
</linearGradient>
<linearGradient id="gradient-dark" gradientTransform="rotate(45)" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#60A5FA"/>,<stop offset="100%" stop-color="#4ADE80"/>
</linearGradient>
</defs>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="449" fill="url(#gradient)" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
<g transform="translate(0, 0)">
<text x="0" y="0" class="header" data-testid="header">Anurag Hazra's GitHub Stats</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<g data-testid="rank-circle" transform="translate(365, 47.5)">
<circle class="rank-circle-rim" cx="-10" cy="8" r="40"/>
<circle class="rank-circle" cx="-10" cy="8" r="40"/>
<g class="rank-text">
<text x="-5" y="3" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="level-rank-icon">
A+
</text>
</g>
</g>
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g class="stagger" style="animation-delay: 450ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5">Total Stars Earned:</text>
<text class="stat bold" x="204.01" y="12.5" data-testid="stars">14.1k</text>
</g>
</g><g transform="translate(0, 25)">
<g class="stagger" style="animation-delay: 600ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5">Total Commits (last year):</text>
<text class="stat bold" x="204.01" y="12.5" data-testid="commits">200</text>
</g>
</g><g transform="translate(0, 50)">
<g class="stagger" style="animation-delay: 750ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5">Total PRs:</text>
<text class="stat bold" x="204.01" y="12.5" data-testid="prs">4k</text>
</g>
</g><g transform="translate(0, 75)">
<g class="stagger" style="animation-delay: 900ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5">Total Issues:</text>
<text class="stat bold" x="204.01" y="12.5" data-testid="issues">340</text>
</g>
</g><g transform="translate(0, 100)">
<g class="stagger" style="animation-delay: 1050ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5">Contributed to (last year):</text>
<text class="stat bold" x="204.01" y="12.5" data-testid="contribs">51</text>
</g>
</g>
</svg>
</g>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api contract > should match the private missing-username response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
@@ -129,6 +373,7 @@ exports[`Test /api contract > should match the public happy-path response snapsh
}
/* Animations */
@@ -153,8 +398,13 @@ exports[`Test /api contract > should match the public happy-path response snapsh
</style>
<defs>
</defs>
<rect data-testid="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="449" fill="#fffefe" stroke-opacity="1"/>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="449" fill="#fffefe" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
@@ -299,6 +549,7 @@ exports[`Test /api contract > should match the public many-params response snaps
}
/* Animations */
@@ -323,8 +574,13 @@ exports[`Test /api contract > should match the public many-params response snaps
</style>
<defs>
</defs>
<rect data-testid="card-bg" x="0.5" y="0.5" rx="12" height="99%" stroke="#fedcba" width="539" fill="#0f172a" stroke-opacity="1"/>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="12" height="99%" stroke="#fedcba" width="539" fill="#0f172a" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
@@ -443,7 +699,7 @@ exports[`Test /api contract > should match the public many-params response snaps
</g>
</svg>",
"graphqlRequest": "{"query":"\\n query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n name\\n login\\n commits: contributionsCollection (from: $startTime) {\\n totalCommitContributions,\\n }\\n reviews: contributionsCollection {\\n totalPullRequestReviewContributions\\n }\\n repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {\\n totalCount\\n }\\n pullRequests(first: 1) {\\n totalCount\\n }\\n mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {\\n totalCount\\n }\\n openIssues: issues(states: OPEN) {\\n totalCount\\n }\\n closedIssues: issues(states: CLOSED) {\\n totalCount\\n }\\n followers {\\n totalCount\\n }\\n repositoryDiscussions @include(if: $includeDiscussions) {\\n totalCount\\n }\\n repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {\\n totalCount\\n }\\n \\n repositories(first: 100, after: $after, ownerAffiliations: $ownerAffiliations, orderBy: {direction: DESC, field: STARGAZERS}) {\\n totalCount\\n nodes {\\n name\\n stargazers {\\n totalCount\\n }\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n\\n }\\n }\\n","variables":{"login":"anuraghazra","first":100,"after":null,"includeMergedPullRequests":true,"includeDiscussions":true,"includeDiscussionsAnswers":true,"startTime":"2024-01-01T00:00:00Z","ownerAffiliations":["OWNER","COLLABORATOR"]}}",
"graphqlRequest": "{"query":"\\nquery userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation], $includeUserRepositories: Boolean!) {\\n user(login: $login) {\\n name\\n login\\n commits: contributionsCollection(from: $startTime) {\\n totalCommitContributions\\n }\\n reviews: contributionsCollection {\\n totalPullRequestReviewContributions\\n }\\n repositoriesContributedTo(\\n first: 1\\n contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]\\n includeUserRepositories: $includeUserRepositories\\n ) {\\n totalCount\\n }\\n pullRequests(first: 1) {\\n totalCount\\n }\\n mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {\\n totalCount\\n }\\n openIssues: issues(states: OPEN) {\\n totalCount\\n }\\n closedIssues: issues(states: CLOSED) {\\n totalCount\\n }\\n followers {\\n totalCount\\n }\\n repositoryDiscussions @include(if: $includeDiscussions) {\\n totalCount\\n }\\n repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {\\n totalCount\\n }\\n contributionsCollection {\\n contributionYears\\n }\\n ...RepoStars\\n }\\n}\\nfragment RepoStars on User {\\n repositories(\\n first: 100\\n after: $after\\n ownerAffiliations: $ownerAffiliations\\n orderBy: {direction: DESC, field: STARGAZERS}\\n ) {\\n totalCount\\n nodes {\\n ...RepoNode\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n}\\nfragment RepoNode on Repository {\\n name\\n stargazerCount\\n}","variables":{"login":"anuraghazra","after":null,"includeMergedPullRequests":true,"includeDiscussions":true,"includeDiscussionsAnswers":true,"startTime":"2024-01-01T00:00:00Z","ownerAffiliations":["OWNER","COLLABORATOR"],"includeUserRepositories":false}}",
"headers": [
[
"Cache-Control",
@@ -51,6 +51,7 @@ exports[`Test /api/gist contract > should match the public happy-path response s
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.icon { fill: #586069 }
/* Animations */
@@ -75,8 +76,13 @@ exports[`Test /api/gist contract > should match the public happy-path response s
</style>
<defs>
</defs>
<rect data-testid="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="399" fill="#fffefe" stroke-opacity="1"/>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="399" fill="#fffefe" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
@@ -145,12 +151,25 @@ exports[`Test /api/gist contract > should match the public many-params response
}
.description {
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #abcdef;
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #ffabcd;
}
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #ffabcd }
.icon { fill: #ff00aa }
@media (prefers-color-scheme: dark) {
.header { fill: #123456; }
.card-bg { fill: #0f172a; stroke: #fedcba; }
.description {
fill: #abcdef;
}
.gray { fill: #abcdef }
.icon { fill: #aa00ff }
}
/* Animations */
@@ -175,8 +194,13 @@ exports[`Test /api/gist contract > should match the public many-params response
</style>
<defs>
</defs>
<rect data-testid="card-bg" x="0.5" y="0.5" rx="12" height="99%" stroke="#fedcba" width="399" fill="#0f172a" stroke-opacity="1"/>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="12" height="99%" stroke="#fedcba" width="399" fill="#0f172a" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
@@ -215,7 +239,7 @@ exports[`Test /api/gist contract > should match the public many-params response
</g>
</svg>",
"graphqlRequest": "{"query":"\\nquery gistInfo($gistName: String!) {\\n viewer {\\n gist(name: $gistName) {\\n description\\n owner {\\n login\\n }\\n stargazerCount\\n forks {\\n totalCount\\n }\\n files {\\n name\\n language {\\n name\\n }\\n size\\n }\\n }\\n }\\n}\\n","variables":{"gistName":"happy-gist-id"}}",
"graphqlRequest": "{"query":"\\nquery gistInfo($gistName: String!) {\\n viewer {\\n gist(name: $gistName) {\\n description\\n owner {\\n login\\n }\\n stargazerCount\\n forks {\\n totalCount\\n }\\n files {\\n ...GistFileInfo\\n }\\n }\\n }\\n}\\nfragment GistFileInfo on GistFile {\\n name\\n language {\\n name\\n }\\n size\\n}","variables":{"gistName":"happy-gist-id"}}",
"headers": [
[
"Cache-Control",
@@ -56,6 +56,140 @@ exports[`Test /api/pin contract > should match the private non-whitelisted usern
}
`;
exports[`Test /api/pin contract > should match the public archived-repo response snapshot 1`] = `
{
"content": "<svg width="400" height="150" viewBox="0 0 400 150" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId"/>
<desc id="descId"/>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #2f80ed;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
.description {
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #434d58;
}
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
.badge rect { opacity: 0.2; stroke: #434d58 }
.badge text { fill: #434d58 }
.stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
.icon {
fill: #586069;
display: block;
}
/* Animations */
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeInAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
* { animation-duration: 0s !important; animation-delay: 0s !important; }
</style>
<defs>
</defs>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="399" fill="#fffefe" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
<g transform="translate(0, 0)">
<svg class="icon" x="0" y="-13" 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"/>
</svg>
</g><g transform="translate(25, 0)">
<text x="0" y="0" class="header" data-testid="header">convoychat</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<g data-testid="badge" class="badge" transform="translate(320, -18)">
<rect stroke-width="1" width="70" height="20" x="-12" y="-14" ry="10" rx="10"/>
<text x="23" y="-5" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">
Archived
</text>
</g>
<text class="description" x="25" y="-5">
<tspan dy="1.2em" x="25">Help us take over the world with a deeply customizable</tspan><tspan dy="1.2em" x="25">React, TypeScript and GraphQL chat app that has enough</tspan><tspan dy="1.2em" x="25">text to wrap across multiple lines in the repository card.</tspan>
</text>
<g transform="translate(30, 75)">
<g transform="translate(0, 0)">
<g data-testid="primary-lang">
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#2b7489"/>
<text data-testid="lang-name" class="gray" x="15">TypeScript</text>
</g>
</g><g transform="translate(80.53999967575072, 0)"><g transform="translate(0, 0)">
<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"/>
</svg>
</g><g transform="translate(20, 0)"><text data-testid="stargazers" class="gray">38k</text></g></g><g transform="translate(140.4199994087219, 0)"><g transform="translate(0, 0)">
<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"/>
</svg>
</g><g transform="translate(20, 0)"><text data-testid="forkcount" class="gray">100</text></g></g>
</g>
<svg x="0" y="0"><g transform="translate(-3, 98)">
</g></svg>
</g>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/pin contract > should match the public blacklisted-username response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
@@ -106,7 +240,8 @@ exports[`Test /api/pin contract > should match the public happy-path response sn
}
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
.badge rect { opacity: 0.2 }
.badge rect { opacity: 0.2; stroke: #434d58 }
.badge text { fill: #434d58 }
.stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.stagger {
@@ -120,6 +255,7 @@ exports[`Test /api/pin contract > should match the public happy-path response sn
display: block;
}
/* Animations */
@@ -144,8 +280,13 @@ exports[`Test /api/pin contract > should match the public happy-path response sn
</style>
<defs>
</defs>
<rect data-testid="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="399" fill="#fffefe" stroke-opacity="1"/>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="399" fill="#fffefe" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
@@ -221,14 +362,15 @@ exports[`Test /api/pin contract > should match the public many-params response s
}
.description {
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #abcdef;
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #a9fef7;
}
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #a9fef7 }
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
.badge rect { opacity: 0.2 }
.badge rect { opacity: 0.2; stroke: #a9fef7 }
.badge text { fill: #a9fef7 }
.stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
.stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #a9fef7 }
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
@@ -236,10 +378,26 @@ exports[`Test /api/pin contract > should match the public many-params response s
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
.icon {
fill: #ff00aa;
fill: #f8d847;
display: block;
}
@media (prefers-color-scheme: dark) {
.header { fill: #123456; }
.card-bg { fill: #193549; stroke: #fedcba; }
.description {
fill: #75eeb2;
}
.gray { fill: #75eeb2 }
.badge rect { stroke: #75eeb2 }
.badge text { fill: #75eeb2 }
.stat { fill: #75eeb2 }
.icon { fill: #0480ef; }
}
/* Animations */
@@ -264,8 +422,13 @@ exports[`Test /api/pin contract > should match the public many-params response s
</style>
<defs>
</defs>
<rect data-testid="card-bg" x="0.5" y="0.5" rx="12" height="99%" stroke="#fedcba" width="559" fill="#0f172a" stroke-opacity="1"/>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="12" height="99%" stroke="#fedcba" width="559" fill="#141321" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
@@ -341,7 +504,7 @@ exports[`Test /api/pin contract > should match the public many-params response s
</g>
</svg>",
"graphqlRequest": "{"query":"\\n fragment RepoInfo on Repository {\\n name\\n nameWithOwner\\n isPrivate\\n isArchived\\n isTemplate\\n stargazers {\\n totalCount\\n }\\n description\\n primaryLanguage {\\n color\\n id\\n name\\n }\\n forkCount\\n }\\n query getRepo($login: String!, $repo: String!) {\\n user(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n organization(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n }\\n ","variables":{"login":"anuraghazra","repo":"convoychat"}}",
"graphqlRequest": "{"query":"\\nquery getRepo($login: String!, $repo: String!) {\\n user(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n organization(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n}\\nfragment RepoInfo on Repository {\\n name\\n nameWithOwner\\n isPrivate\\n isArchived\\n isTemplate\\n stargazerCount\\n description\\n primaryLanguage {\\n color\\n id\\n name\\n }\\n forkCount\\n}","variables":{"login":"anuraghazra","repo":"convoychat"}}",
"headers": [
[
"Cache-Control",
@@ -138,7 +138,9 @@ exports[`Test /api/top-langs contract > should match the public happy-path respo
.lang-progress{
animation: growWidthAnimation 0.6s ease-in-out forwards;
}
.progress-background { fill: #ddd; }
/* Animations */
@@ -163,8 +165,13 @@ exports[`Test /api/top-langs contract > should match the public happy-path respo
</style>
<defs>
</defs>
<rect data-testid="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="299" fill="#fffefe" stroke-opacity="1"/>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="299" fill="#fffefe" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
@@ -183,7 +190,7 @@ exports[`Test /api/top-langs contract > should match the public happy-path respo
<text x="215" y="34" class="lang-name">70.92%</text>
<svg width="205" x="0" y="25">
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8" fill="#ddd"/>
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8"/>
<svg data-testid="lang-progress" width="70.92198581560284%">
<rect height="8" fill="#dea584" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 750ms;"/>
</svg>
@@ -196,7 +203,7 @@ exports[`Test /api/top-langs contract > should match the public happy-path respo
<text x="215" y="34" class="lang-name">12.77%</text>
<svg width="205" x="0" y="25">
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8" fill="#ddd"/>
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8"/>
<svg data-testid="lang-progress" width="12.76595744680851%">
<rect height="8" fill="#0f0" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 900ms;"/>
</svg>
@@ -209,7 +216,7 @@ exports[`Test /api/top-langs contract > should match the public happy-path respo
<text x="215" y="34" class="lang-name">8.51%</text>
<svg width="205" x="0" y="25">
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8" fill="#ddd"/>
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8"/>
<svg data-testid="lang-progress" width="8.51063829787234%">
<rect height="8" fill="#0ff" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1050ms;"/>
</svg>
@@ -222,7 +229,7 @@ exports[`Test /api/top-langs contract > should match the public happy-path respo
<text x="215" y="34" class="lang-name">6.38%</text>
<svg width="205" x="0" y="25">
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8" fill="#ddd"/>
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8"/>
<svg data-testid="lang-progress" width="6.382978723404255%">
<rect height="8" fill="#3178c6" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1200ms;"/>
</svg>
@@ -235,7 +242,7 @@ exports[`Test /api/top-langs contract > should match the public happy-path respo
<text x="215" y="34" class="lang-name">1.42%</text>
<svg width="205" x="0" y="25">
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8" fill="#ddd"/>
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8"/>
<svg data-testid="lang-progress" width="2%">
<rect height="8" fill="#f00" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1350ms;"/>
</svg>
@@ -314,7 +321,18 @@ exports[`Test /api/top-langs contract > should match the public many-params resp
.lang-progress{
animation: growWidthAnimation 0.6s ease-in-out forwards;
}
.progress-background { fill: #ddd; }
@media (prefers-color-scheme: dark) {
.header { fill: #123456; }
.card-bg { fill: #0f172a; stroke: #e4e2e2; }
.stat { fill: #abcdef; }
.lang-name { fill: #abcdef; }
.progress-background { fill: #ddd; }
}
/* Animations */
@@ -339,8 +357,13 @@ exports[`Test /api/top-langs contract > should match the public many-params resp
</style>
<defs>
</defs>
<rect data-testid="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="419" fill="#0f172a" stroke-opacity="0"/>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="419" fill="#0f172a" stroke-opacity="0"/>
@@ -389,7 +412,7 @@ exports[`Test /api/top-langs contract > should match the public many-params resp
</g>
</svg>",
"graphqlRequest": "{"query":"\\n query userInfo($login: String!, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n # do not fetch forks\\n repositories(ownerAffiliations: $ownerAffiliations, isFork: false, first: 100) {\\n nodes {\\n name\\n languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {\\n edges {\\n size\\n node {\\n color\\n name\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n ","variables":{"login":"anuraghazra","ownerAffiliations":["OWNER","COLLABORATOR"]}}",
"graphqlRequest": "{"query":"\\nquery topLanguages($login: String!, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n repositories(ownerAffiliations: $ownerAffiliations, isFork: false, first: 100) {\\n nodes {\\n ...TopLanguagesRepository\\n }\\n }\\n }\\n}\\nfragment TopLanguagesRepository on Repository {\\n name\\n languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {\\n edges {\\n ...TopLanguage\\n }\\n }\\n}\\nfragment TopLanguage on LanguageEdge {\\n size\\n node {\\n color\\n name\\n }\\n}","variables":{"login":"anuraghazra","ownerAffiliations":["OWNER","COLLABORATOR"]}}",
"headers": [
[
"Cache-Control",
@@ -109,8 +109,11 @@ exports[`Test /api/wakatime contract > should match the public happy-path respon
}
.lang-progress{
animation: growWidthAnimation 0.6s ease-in-out forwards;
fill: #2f80ed;
}
.progress-background { fill: #434d58; }
/* Animations */
@@ -135,8 +138,13 @@ exports[`Test /api/wakatime contract > should match the public happy-path respon
</style>
<defs>
</defs>
<rect data-testid="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="494" fill="#fffefe" stroke-opacity="1"/>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="494" fill="#fffefe" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
@@ -155,9 +163,9 @@ exports[`Test /api/wakatime contract > should match the public happy-path respon
<text class="stat" x="350" y="12.5">12 hrs</text>
<svg width="220" x="110" y="4">
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8" fill="#434d58"/>
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8"/>
<svg data-testid="lang-progress" width="52%">
<rect height="8" fill="#2f80ed" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 750ms;"/>
<rect height="8" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 750ms;"/>
</svg>
</svg>
@@ -168,9 +176,9 @@ exports[`Test /api/wakatime contract > should match the public happy-path respon
<text class="stat" x="350" y="12.5">6 hrs 30 mins</text>
<svg width="220" x="110" y="4">
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8" fill="#434d58"/>
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8"/>
<svg data-testid="lang-progress" width="28%">
<rect height="8" fill="#2f80ed" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 900ms;"/>
<rect height="8" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 900ms;"/>
</svg>
</svg>
@@ -181,9 +189,9 @@ exports[`Test /api/wakatime contract > should match the public happy-path respon
<text class="stat" x="350" y="12.5">3 hrs</text>
<svg width="220" x="110" y="4">
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8" fill="#434d58"/>
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8"/>
<svg data-testid="lang-progress" width="13%">
<rect height="8" fill="#2f80ed" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1050ms;"/>
<rect height="8" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1050ms;"/>
</svg>
</svg>
@@ -194,9 +202,9 @@ exports[`Test /api/wakatime contract > should match the public happy-path respon
<text class="stat" x="350" y="12.5">1 hr</text>
<svg width="220" x="110" y="4">
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8" fill="#434d58"/>
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8"/>
<svg data-testid="lang-progress" width="4%">
<rect height="8" fill="#2f80ed" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1200ms;"/>
<rect height="8" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1200ms;"/>
</svg>
</svg>
@@ -207,9 +215,9 @@ exports[`Test /api/wakatime contract > should match the public happy-path respon
<text class="stat" x="350" y="12.5">30 mins</text>
<svg width="220" x="110" y="4">
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8" fill="#434d58"/>
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8"/>
<svg data-testid="lang-progress" width="3%">
<rect height="8" fill="#2f80ed" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1350ms;"/>
<rect height="8" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1350ms;"/>
</svg>
</svg>
@@ -285,8 +293,11 @@ exports[`Test /api/wakatime contract > should match the public inaccessible-prof
}
.lang-progress{
animation: growWidthAnimation 0.6s ease-in-out forwards;
fill: #2f80ed;
}
.progress-background { fill: #434d58; }
/* Animations */
@@ -311,8 +322,13 @@ exports[`Test /api/wakatime contract > should match the public inaccessible-prof
</style>
<defs>
</defs>
<rect data-testid="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="494" fill="#fffefe" stroke-opacity="1"/>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="494" fill="#fffefe" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
@@ -326,7 +342,7 @@ exports[`Test /api/wakatime contract > should match the public inaccessible-prof
<svg x="0" y="0" width="100%">
<g transform="translate(0, 0)">
<text x="25" y="11" class="stat bold" fill="#434d58">WakaTime user profile not public</text>
<text x="25" y="11" class="stat bold">WakaTime user profile not public</text>
</g>
</svg>
@@ -353,7 +369,7 @@ exports[`Test /api/wakatime contract > should match the public many-params respo
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #123456;
fill: #ff3456;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
@@ -398,8 +414,35 @@ exports[`Test /api/wakatime contract > should match the public many-params respo
}
.lang-progress{
animation: growWidthAnimation 0.6s ease-in-out forwards;
fill: #ff3456;
}
.progress-background { fill: #abcdef; }
@media (prefers-color-scheme: dark) {
.header { fill: #ff6543; }
.card-bg { fill: #193549; stroke: #fedcba; }
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #abcdef;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.stat { font-size:12px; }
}
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
.lang-name { fill: #abcdef }
.lang-progress { fill: #ff6543; }
.progress-background { fill: #abcdef; }
}
/* Animations */
@@ -424,8 +467,13 @@ exports[`Test /api/wakatime contract > should match the public many-params respo
</style>
<defs>
</defs>
<rect data-testid="card-bg" x="0.5" y="0.5" rx="12" height="99%" stroke="#fedcba" width="619" fill="#0f172a" stroke-opacity="1"/>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="12" height="99%" stroke="#fedcba" width="619" fill="#141321" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
@@ -116,6 +116,31 @@ describe("Test /api contract", () => {
}).toMatchSnapshot();
});
it("should match the gradient-background response snapshot", async () => {
const { default: router } = await import("../../router.js");
const params = new URLSearchParams({
username: "anuraghazra",
theme_light: "ambient_gradient",
bg_color_dark: "45,60A5FA,4ADE80",
});
const req = {
headers: {},
url: `/api?${params.toString()}`,
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the public missing-username response snapshot", async () => {
const { default: router } = await import("../../router.js");
@@ -76,8 +76,10 @@ describe("Test /api/gist contract", () => {
const params = new URLSearchParams({
id: "happy-gist-id",
title_color: "123456",
icon_color: "ff00aa",
text_color: "abcdef",
icon_color_light: "ff00aa",
icon_color_dark: "aa00ff",
text_color_light: "ffabcd",
text_color_dark: "abcdef",
bg_color: "0f172a",
border_radius: "12",
border_color: "fedcba",
+28 -3
View File
@@ -68,6 +68,32 @@ describe("Test /api/pin contract", () => {
}).toMatchSnapshot();
});
it("should match the public archived-repo response snapshot", async () => {
let archived_data_user = structuredClone(data_user);
archived_data_user.data.user.repository.isArchived = true;
mock
.onPost("https://api.github.com/graphql")
.reply(200, archived_data_user);
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/pin?username=anuraghazra&repo=convoychat",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the public many-params response snapshot", async () => {
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
mock
@@ -102,9 +128,8 @@ describe("Test /api/pin contract", () => {
username: "anuraghazra",
repo: "convoychat",
title_color: "123456",
icon_color: "ff00aa",
text_color: "abcdef",
bg_color: "0f172a",
theme_light: "radical",
theme_dark: "cobalt",
card_width: "560",
show_owner: "true",
show: "prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented",
@@ -82,6 +82,7 @@ describe("Test /api/top-langs contract", () => {
title_color: "123456",
text_color: "abcdef",
bg_color: "0f172a",
theme_dark: "cobalt",
layout: "compact",
langs_count: "3",
exclude_repo: "repo-hidden",
@@ -92,9 +92,13 @@ describe("Test /api/wakatime contract", () => {
const params = new URLSearchParams({
username: "anuraghazra",
theme: "vue",
theme_light: "radical",
theme_dark: "cobalt",
title_color: "123456",
title_color_light: "ff3456",
title_color_dark: "ff6543",
text_color: "abcdef",
bg_color: "0f172a",
card_width: "620",
custom_title: "a custom title",
layout: "compact",
+7 -6
View File
@@ -8,6 +8,9 @@ export const data_stats = {
name: "Anurag Hazra",
login: "anuraghazra",
repositoriesContributedTo: { totalCount: 51 },
contributionsCollection: {
contributionYears: [2022, 2024],
},
commits: {
totalCommitContributions: 200,
},
@@ -26,9 +29,9 @@ export const data_stats = {
repositories: {
totalCount: 3,
nodes: [
{ name: "repo-keep-1", stargazers: { totalCount: 1500 } },
{ name: "repo-exclude-me", stargazers: { totalCount: 9999 } },
{ name: "repo-keep-2", stargazers: { totalCount: 2600 } },
{ name: "repo-keep-1", stargazerCount: 1500 },
{ name: "repo-exclude-me", stargazerCount: 9999 },
{ name: "repo-keep-2", stargazerCount: 2600 },
],
pageInfo: {
hasNextPage: false,
@@ -73,9 +76,7 @@ export const data_user = {
username: "anuraghazra",
name: "convoychat",
nameWithOwner: "anuraghazra/convoychat",
stargazers: {
totalCount: 38000,
},
stargazerCount: 38000,
description:
"Help us take over the world with a deeply customizable React, TypeScript and GraphQL chat app that has enough text to wrap across multiple lines in the repository card.",
primaryLanguage: {
-1
View File
@@ -7,7 +7,6 @@
"vitest.config.e2e.ts"
],
"compilerOptions": {
"composite": true,
"module": "nodenext",
"moduleResolution": "nodenext"
}
-1
View File
@@ -1,7 +1,6 @@
{
"extends": ["./tsconfig.json"],
"compilerOptions": {
"noEmit": true,
"customConditions": []
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
"redirects": [
{
"source": "/",
"destination": "https://github.com/stats-organization/github-stats-extended"
"destination": "/frontend/docs/"
}
]
}
+126
View File
@@ -0,0 +1,126 @@
import path from "node:path";
import react from "@astrojs/react";
import starlight from "@astrojs/starlight";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig, passthroughImageService } from "astro/config";
import starlightLinksValidator from "starlight-links-validator";
const base = "/frontend";
// `pnpm dev:backend` serves the card endpoints; override to point at another instance.
const BACKEND_ORIGIN = process.env.BACKEND_ORIGIN ?? "http://localhost:9000";
/*
* One app: Starlight serves the docs under `/frontend/docs`, and the card
* wizard is a page of the same site at `/frontend`.
*/
export default defineConfig({
site: "https://github-stats-extended.vercel.app",
base,
outDir: "./build",
// One screenshot; not worth a native image dependency.
image: { service: passthroughImageService() },
// Astro prefixes `base` onto the source but not the destination, hence it spelled out here.
redirects: {
// Sidebar group labels are not routes, so send them to the group's first page.
"/docs/cards": `${base}/docs/cards/stats/`,
"/docs/customization": `${base}/docs/customization/common-options/`,
},
integrations: [
react(),
starlight({
title: "GitHub Stats Extended",
description: "Dynamically generate GitHub stats for your READMEs.",
logo: { src: "./src/assets/appLogo64.png", alt: "" },
// Starlight defaults to a favicon.svg this project does not ship.
favicon: "/favicon.ico",
head: [
{
tag: "link",
attrs: { rel: "apple-touch-icon", href: `${base}/logo192.png` },
},
{
tag: "link",
attrs: { rel: "manifest", href: `${base}/manifest.json` },
},
{ tag: "meta", attrs: { name: "theme-color", content: "#000000" } },
],
social: [
{
icon: "github",
label: "GitHub",
href: "https://github.com/stats-organization/github-stats-extended",
},
],
plugins: [
starlightLinksValidator({
// The docs link to each other by site path, so a rename must fail the build.
// `/api` and `/frontend` are not part of the docs, so they don't have matching pages.
exclude: ["/api/**", "/frontend"],
}),
],
customCss: ["./src/styles/starlight-theme.css"],
components: { SiteTitle: "./src/components/SiteTitle.astro" },
sidebar: [
{ label: "Overview", slug: "docs" },
{
label: "Cards",
items: [
{ label: "Stats Card", slug: "docs/cards/stats" },
{ label: "Top Languages Card", slug: "docs/cards/top-languages" },
{ label: "Repo Pin Card", slug: "docs/cards/repo-pin" },
{ label: "Gist Pin Card", slug: "docs/cards/gist-pin" },
{ label: "WakaTime Card", slug: "docs/cards/wakatime" },
],
},
{
label: "Customization",
items: [
{
label: "Common Options",
slug: "docs/customization/common-options",
},
{ label: "Theming", slug: "docs/customization/theming" },
// Generated by packages/core scripts/generate-theme-readme.js.
{ label: "Available Themes", slug: "docs/customization/themes" },
{ label: "Locales", slug: "docs/customization/locales" },
{
label: "Aligning Cards",
slug: "docs/customization/aligning-cards",
},
],
},
{ label: "Demo Gallery", slug: "docs/demos" },
{ label: "Run It Yourself", slug: "docs/deploy" },
{ label: "Fork Information", slug: "docs/fork" },
],
}),
],
vite: {
plugins: [tailwindcss()],
/*
* On Vercel the same deployment serves `/api` and `/frontend`, so the docs can reference cards by root-relative path.
* Locally the two are separate servers, so forward `/api` to `pnpm dev:backend` to keep those paths working.
*/
server: { proxy: { "/api": BACKEND_ORIGIN } },
resolve: {
conditions: ["@stats/source"],
alias: [
{
find: "../fetchers/wakatime.js",
replacement: path.resolve(
import.meta.dirname,
"src/wakatime-override.ts",
),
},
],
},
// The backend code the wizard reuses imports `pg`, which never runs in the browser.
build: {
rolldownOptions: {
external: ["pg"],
},
},
},
});
-12
View File
@@ -1,12 +0,0 @@
FROM node:16-alpine
WORKDIR /frontend
ENV PATH /frontend/node_modules/.bin:$PATH
COPY ../package.json ../yarn.lock /frontend/
RUN yarn install --network-timeout 100000
COPY ../ /frontend
CMD ["yarn", "start"]
+49 -25
View File
@@ -1,25 +1,29 @@
import { expect, test } from "@playwright/test";
const REPO_URL = "https://github.com/stats-organization/github-stats-extended";
test("load initial page correctly", async ({ page }) => {
await page.goto("");
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/GitHub Stats Extended/);
// Page title / header branding
await expect(page.getByText("GitHub Stats Extended")).toBeVisible();
// Header branding, with the logo beside the wordmark.
const siteTitle = page.getByRole("link", { name: "GitHub Stats Extended" });
await expect(siteTitle).toBeVisible();
await expect(siteTitle.locator("img")).toBeVisible();
// Logo exists
await expect(page.locator('img[alt="logo"]')).toBeVisible();
// The two halves of the site, with this one marked as current.
await expect(
page.getByRole("link", { name: "Wizard", exact: true }),
).toHaveAttribute("aria-current", "page");
await expect(
page.getByRole("link", { name: "Docs", exact: true }),
).toHaveAttribute("href", "/frontend/docs/");
// Star on GitHub button
const starButton = page.getByRole("button", { name: /star on/i });
await expect(starButton).toBeVisible();
const githubLink = page.locator(
'a[href*="github.com/stats-organization/github-stats-extended"]',
);
await expect(githubLink).toHaveAttribute("target", "_blank");
await expect(
page.getByRole("link", { name: "GitHub", exact: true }),
).toHaveAttribute("href", REPO_URL);
// Login buttons
const publicAccessBtn = page.getByRole("button", {
@@ -39,33 +43,53 @@ test("load initial page correctly", async ({ page }) => {
await expect(guestBtn).toBeEnabled();
});
test("theme selection applies, persists and reaches the wizard", async ({
page,
}) => {
await page.goto("");
const html = page.locator("html");
const themeSelect = page.getByRole("combobox", { name: "Select theme" });
// The site's own control owns `data-theme`, which is also what daisyUI reads.
await themeSelect.selectOption("dark");
await expect(html).toHaveAttribute("data-theme", "dark");
await expect(
page.evaluate(() => localStorage.getItem("starlight-theme")),
).resolves.toBe("dark");
// The choice survives a reload.
await page.reload();
await expect(html).toHaveAttribute("data-theme", "dark");
await expect(themeSelect).toHaveValue("dark");
// Switching back works too.
await themeSelect.selectOption("light");
await expect(html).toHaveAttribute("data-theme", "light");
});
test("navigates between steps", async ({ page }) => {
await page.goto("");
// Scoped to the page content: Astro's dev toolbar has headings of its own.
const heading = page.locator("main").getByRole("heading", { level: 1 });
// We are at stage 1
await expect(page.getByRole("heading", { level: 1 })).toContainText("Login");
await expect(heading).toContainText("Login");
// Go to stage 2
await page.getByRole("button", { name: "Select card" }).click();
await expect(page.getByRole("heading", { level: 1 })).toContainText(
"Select a Card",
);
await expect(heading).toContainText("Select a Card");
// Go to stage 3
await page.getByRole("button", { name: "Modify parameters" }).click();
await expect(page.getByRole("heading", { level: 1 })).toContainText(
"Modify Card Parameters",
);
await expect(heading).toContainText("Modify Card Parameters");
// Go to stage 4
await page.getByRole("button", { name: "Select theme" }).click();
await expect(page.getByRole("heading", { level: 1 })).toContainText(
"Choose a Theme",
);
await expect(heading).toContainText("Choose a Theme");
// Go to stage 5
await page.getByRole("button", { name: "Display card" }).click();
await expect(page.getByRole("heading", { level: 1 })).toContainText(
"Display your Card",
);
await expect(heading).toContainText("Display your Card");
});
+29
View File
@@ -0,0 +1,29 @@
import { expect, test } from "@playwright/test";
// Nothing links to a redirect, so the build's link validator cannot catch a broken one.
const redirects = [
{
from: "docs/cards",
to: "/frontend/docs/cards/stats/",
heading: "Stats Card",
},
{
from: "docs/customization",
to: "/frontend/docs/customization/common-options/",
heading: "Common Options",
},
];
for (const { from, to, heading } of redirects) {
test(`/${from} redirects to ${to}`, async ({ page }) => {
for (const url of [from, `${from}/`]) {
await page.goto(url);
await expect(page).toHaveURL(to);
// Scoped to the page content: Astro's dev toolbar has headings of its own.
await expect(
page.locator("main").getByRole("heading", { level: 1 }),
).toHaveText(heading);
}
});
}
+29
View File
@@ -0,0 +1,29 @@
import { expect, test } from "@playwright/test";
test("selecting 'None' progress style hides the rank circle", async ({
page,
}) => {
await page.goto("");
// Stage 0 -> 2: go to customization
await page.getByRole("button", { name: "Modify Parameters" }).click();
// Scoped to the page content: Astro's dev toolbar has headings of its own.
await expect(
page.locator("main").getByRole("heading", { level: 1 }),
).toContainText("Modify Card Parameters");
const preview = page.locator("#svgWrapper");
const rankCircle = preview.locator('[data-testid="rank-circle"]');
// Default "Rank" progress style renders the rank circle.
await expect(preview).toBeAttached();
await expect(rankCircle).toHaveCount(1);
// Scoped: the site header has a theme combobox of its own.
await page
.locator("main")
.getByRole("combobox")
.selectOption({ label: "None" });
await expect(rankCircle).toHaveCount(0);
});
+62
View File
@@ -0,0 +1,62 @@
import { expect, test } from "@playwright/test";
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 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> {
await page.route("**/api/authenticate**", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ userId: "octocat", needDowngrade: false }),
}),
);
await page.route("**/api/user-access**", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ token: "test-token", privateAccess: "false" }),
}),
);
}
test.describe("the wizard auth-driven stage transition", () => {
test("auto-advances from Login to Select a Card once authenticated", async ({
page,
}) => {
await mockAuthEndpoints(page);
// Land on the app as GitHub does after the OAuth redirect (URL carries `code`).
await page.goto("?code=test-oauth-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").
// Scoped to the page content: Astro's dev toolbar has headings of its own.
await expect(
page.locator("main").getByRole("heading", { level: 1 }),
).toContainText("Select a Card");
});
test("keeps a manually selected earlier step instead of re-forcing it", async ({
page,
}) => {
await mockAuthEndpoints(page);
await page.goto("?code=test-oauth-code");
const heading = page.locator("main").getByRole("heading", { level: 1 });
await expect(heading).toContainText("Select a Card");
// Going back to "Login" while still authenticated must stick: the transition
// fires only on an auth *change*, so a later render must not snap the user
// forward again. A bug that dropped the previous-value guard would bounce
// the heading straight back to "Select a Card" here.
await page.getByRole("button", { name: "Login" }).click();
await expect(heading).toContainText("Login");
});
});
-44
View File
@@ -1,44 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="title" content="GitHub Stats Extended" />
<meta
name="description"
content="Dynamically generate GitHub stats for your READMEs."
/>
<meta property="og:type" content="website" />
<meta
property="og:url"
content="https://github-stats-extended.vercel.app/frontend"
/>
<meta property="og:title" content="GitHub Stats Extended" />
<meta
property="og:description"
content="Dynamically generate GitHub stats for your READMEs."
/>
<!--
https://vite.dev/guide/assets#the-public-directory
-->
<link rel="apple-touch-icon" href="/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="/manifest.json" />
<title>GitHub Stats Extended</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
+23 -20
View File
@@ -4,40 +4,43 @@
"license": "MIT",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"test": "vitest",
"test:e2e": "playwright test",
"lint": "eslint",
"typecheck": "tsc -p tsconfig.typecheck.json"
"lint": "astro sync && eslint",
"typecheck": "astro sync && tsc -p tsconfig.typecheck.json"
},
"dependencies": {
"@reduxjs/toolkit": "^2.11.2",
"@tailwindcss/vite": "^4.2.2",
"axios": "^1",
"axios-cache-interceptor": "^1",
"daisyui": "^5.5.19",
"@astrojs/react": "^6.0.2",
"@astrojs/starlight": "^0.41.7",
"@reduxjs/toolkit": "^2.12.0",
"@stats-organization/github-readme-stats-backend": "workspace:^",
"@stats-organization/github-readme-stats-core": "workspace:^",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-icons": "^5.6.0",
"react-loading-skeleton": "^3.3.1",
"react-redux": "^9.2.0",
"@tailwindcss/vite": "^4.3.3",
"astro": "^7.2.0",
"axios": "catalog:default",
"axios-cache-interceptor": "^1.12.0",
"daisyui": "^5.6.18",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-icons": "^5.7.0",
"react-loading-skeleton": "^3.5.0",
"react-redux": "^9.3.0",
"react-spinners": "^0.17.0",
"react-toastify": "^11.1.0",
"redux": "^5.0.1",
"save-svg-as-png": "^1.4.17",
"uuid": "^14.0.0"
"uuid": "^14.0.1"
},
"devDependencies": {
"@types/react": "19.2.14",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "6.0.1",
"clsx": "2.1.1",
"tailwindcss": "4.2.2",
"vite": "catalog:default",
"jsdom": "catalog:default",
"starlight-links-validator": "0.25.2",
"tailwindcss": "4.3.3",
"vitest": "catalog:default"
},
"browserslist": {
+5 -1
View File
@@ -8,7 +8,7 @@ import { defineConfig, devices } from "@playwright/test";
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
const baseURL = "http://localhost:5173/frontend/";
const baseURL = "http://localhost:4321/frontend/";
/**
* See https://playwright.dev/docs/test-configuration.
@@ -25,6 +25,8 @@ export default defineConfig({
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
// The wizard is a client-only island; on a cold dev server its bundle takes a while.
expect: { timeout: 15_000 },
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('')`. */
@@ -77,5 +79,7 @@ export default defineConfig({
command: "pnpm run dev",
url: baseURL,
reuseExistingServer: !process.env["CI"],
// `astro dev` detaches itself in an AI-agent shell, which reads as exiting early.
env: { ASTRO_DEV_BACKGROUND: "1" },
},
});
+24 -21
View File
@@ -8,17 +8,18 @@ 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 contributions from "./wizard/mockData/contributions.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
@@ -88,9 +89,7 @@ axios.defaults.adapter = async (config) => {
if (
config.url === "https://api.github.com/graphql" &&
params.query?.includes(
"query userInfo($login: String!, $after: String, $includeMergedPullRequests:",
) &&
params.query?.includes("query userInfo(") &&
params.variables?.login === DEMO_USER
) {
return createMockResponse(userStats, config);
@@ -98,9 +97,7 @@ axios.defaults.adapter = async (config) => {
if (
config.url === "https://api.github.com/graphql" &&
params.query?.includes(
"query userInfo($login: String!, $after: String, $ownerAffiliations:",
) &&
params.query?.includes("query userRepos(") &&
params.variables?.login === DEMO_USER
) {
return createMockResponse(additionalUserStars, config);
@@ -108,9 +105,15 @@ axios.defaults.adapter = async (config) => {
if (
config.url === "https://api.github.com/graphql" &&
params.query?.includes(
"query userInfo($login: String!, $ownerAffiliations:",
) &&
params.query?.includes("query userContributions(") &&
params.variables?.login === DEMO_USER
) {
return createMockResponse(contributions, config);
}
if (
config.url === "https://api.github.com/graphql" &&
params.query?.includes("query topLanguages(") &&
params.variables?.login === DEMO_USER
) {
return createMockResponse(topLanguages, config);
@@ -118,7 +121,7 @@ axios.defaults.adapter = async (config) => {
if (
config.url === "https://api.github.com/graphql" &&
params.query?.includes("fragment RepoInfo on Repository {") &&
params.query?.includes("query getRepo(") &&
params.variables &&
params.variables.login === DEMO_REPO.split("/")[0] &&
params.variables.repo === DEMO_REPO.split("/")[1]
@@ -1,43 +0,0 @@
import { clsx } from "clsx";
import type { JSX } from "react";
import { CardImage } from "./CardImage";
interface CardProps {
title: string;
description: string;
imageSrc: string;
stage: number;
selected?: boolean;
compact?: boolean;
fixedSize?: boolean;
}
export const Card = ({
title,
description,
imageSrc,
stage,
selected = false,
compact = false,
fixedSize = false,
}: CardProps): JSX.Element => {
return (
<div
className={clsx("p-6 rounded border-2", {
"h-[370px] w-[510px]": fixedSize,
"border-blue-500 bg-blue-50": selected,
"border-gray-200 bg-white hover:bg-gray-50": !selected,
})}
>
<h2 className="text-xl font-medium title-font text-gray-900">{title}</h2>
<p className="text-base leading-relaxed mt-2 mb-4">{description}</p>
<CardImage
imageSrc={imageSrc}
compact={compact}
className={clsx({ "flex justify-center": fixedSize })}
stage={stage}
/>
</div>
);
};
@@ -1,22 +0,0 @@
import { clsx } from "clsx";
import type { HTMLProps, JSX, ReactNode } from "react";
interface ButtonProps extends HTMLProps<HTMLButtonElement> {
children: ReactNode;
}
export function Button(props: ButtonProps): JSX.Element {
const { className, children, ...rest } = props;
return (
<button
{...rest}
type="button"
className={clsx(
"border-0 py-2 px-6 inline-flex focus:outline-none rounded-[0.25rem] text-lg",
className,
)}
>
{children}
</button>
);
}
@@ -1,77 +0,0 @@
import { useEffect, useRef, useState } from "react";
import type { JSX, ReactNode } from "react";
import { Section } from "./Section";
interface NumericSectionProps {
title: string;
description: ReactNode;
value?: number | undefined;
onValueChange: (value: number | undefined) => void;
min: number;
max: number;
step?: number;
disabled?: boolean;
placeholder?: string;
}
export function NumericSection({
title,
description,
value,
onValueChange,
min,
max,
step = 1,
disabled = false,
placeholder,
}: NumericSectionProps): JSX.Element {
const [internalValue, setInternalValue] = useState(() => value?.toString());
const debounceTimeout = useRef<number | null>(null);
useEffect(() => {
// Debounce onValueChange
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
}
if (internalValue === value) {
return undefined;
}
debounceTimeout.current = window.setTimeout(() => {
const maybeNumber = internalValue && parseInt(internalValue, 10);
if (typeof maybeNumber !== "number" || Number.isNaN(maybeNumber)) {
onValueChange(undefined);
} else {
onValueChange(maybeNumber);
}
}, 700);
return () => {
clearTimeout(debounceTimeout.current as number);
};
}, [internalValue, onValueChange, value]);
useEffect(() => {
setInternalValue(value?.toString());
}, [value]);
return (
<Section title={title}>
<p>{description}</p>
<input
type="number"
className="border border-gray-300 bg-white rounded px-2 py-1 mt-2 w-1/4"
value={internalValue ?? ""}
onChange={(e) => {
setInternalValue(e.target.value);
}}
min={min}
max={max}
step={step}
disabled={disabled}
placeholder={placeholder}
/>
</Section>
);
}
@@ -1,71 +0,0 @@
import { clsx } from "clsx";
import { useEffect, useRef, useState } from "react";
import type { ClipboardEventHandler, JSX, ReactNode } from "react";
import { Section } from "./Section";
interface TextSectionProps {
title: string;
description: ReactNode;
value: string;
onValueChange: (value: string) => void;
disabled?: boolean;
placeholder?: string;
onPaste?: ClipboardEventHandler<HTMLInputElement>;
}
export function TextSection({
title,
description,
value,
onValueChange,
disabled = false,
placeholder,
onPaste,
}: TextSectionProps): JSX.Element {
const [internalValue, setInternalValue] = useState(value);
const debounceTimeout = useRef<number | null>(null);
useEffect(() => {
setInternalValue(value);
}, [value]);
useEffect(() => {
// Debounce onValueChange
if (debounceTimeout.current) {
window.clearTimeout(debounceTimeout.current);
}
if (internalValue === value) {
return undefined;
}
debounceTimeout.current = window.setTimeout(() => {
onValueChange(internalValue);
}, 700);
// return cleanup function:
return () => {
window.clearTimeout(debounceTimeout.current as number);
};
}, [internalValue, onValueChange, value]);
return (
<Section title={title}>
<p>{description}</p>
<input
type="text"
className={clsx(
"border border-gray-300 rounded px-2 py-1 mt-2 w-3/4 min-w-48 max-w-xl",
{ "cursor-not-allowed bg-gray-100": disabled, "bg-white": !disabled },
)}
value={internalValue}
onChange={(e) => {
setInternalValue(e.target.value);
}}
disabled={disabled}
placeholder={placeholder}
onPaste={onPaste}
/>
</Section>
);
}
@@ -0,0 +1,56 @@
---
// Starlight has no nav slot, so the two top-level destinations sit beside the title.
import Default from "@astrojs/starlight/components/SiteTitle.astro";
const base = import.meta.env.BASE_URL.replace(/\/$/, "");
const path = Astro.url.pathname;
const links = [
{ href: `${base}/`, label: "Wizard", current: !path.startsWith(`${base}/docs`) },
{ href: `${base}/docs/`, label: "Docs", current: path.startsWith(`${base}/docs`) },
];
---
<Default><slot /></Default>
<nav class="app-nav">
{
links.map(({ href, label, current }) => (
<a href={href} aria-current={current ? "page" : undefined}>
{label}
</a>
))
}
</nav>
<style>
.app-nav {
display: flex;
align-items: center;
gap: 1rem;
/* The title wrapper clips its overflow, so the links sit outside it. */
margin-inline-start: 1rem;
}
.app-nav a {
color: var(--sl-color-white);
font-size: var(--sl-text-base);
text-decoration: none;
white-space: nowrap;
}
.app-nav a:hover {
color: var(--sl-color-text-accent);
}
.app-nav a[aria-current="page"] {
color: var(--sl-color-text-accent);
font-weight: 600;
}
/* The header gets cramped before the search box collapses. */
@media (max-width: 50rem) {
.app-nav {
display: none;
}
}
</style>
+13
View File
@@ -0,0 +1,13 @@
import { defineCollection } from "astro:content";
import { docsLoader } from "@astrojs/starlight/loaders";
import { docsSchema } from "@astrojs/starlight/schema";
/*
* Starlight fixes this collection at `src/content/docs/` and maps it to the site root,
* which the wizard owns — hence the pages sitting one level down, in `docs/`.
* https://starlight.astro.build/guides/pages/#pages-from-markdown
*/
export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
};
+14
View File
@@ -0,0 +1,14 @@
# Why `docs/docs`
Starlight fixes its content collection at `src/content/docs/`, and maps that folder to the **site root** (`/frontend`), which the card wizard occupies (`src/pages/index.astro`).
The pages therefore sit one level down, in `docs/`, so they publish at `/frontend/docs/`.
Nothing else can be renamed: the outer name is Starlight's, the inner one is the URL segment.
Flattening this needs a custom `generateId` on the loader, which is the one thing `starlight-links-validator` cannot follow.
It derives valid routes from file paths, so every internal link would be reported broken.
The nesting buys build-time dead-link checking.
Starlight ignores files starting with `_`, which is why this note isn't published.
See <https://starlight.astro.build/guides/pages/#pages-from-markdown>.
@@ -0,0 +1,34 @@
---
title: Gist Pin Card
---
GitHub gist pins allow you to pin gists in your GitHub profile using a GitHub profile readme.
## Usage
Copy-paste this code into your readme and change the links.
Endpoint: `api/gist?id=bbfce31e0217a3689c8d961a356cb10d`
```md
[![Gist Card](https://github-stats-extended.vercel.app/api/gist?id=bbfce31e0217a3689c8d961a356cb10d)](https://gist.github.com/Yizack/bbfce31e0217a3689c8d961a356cb10d/)
```
## Options
You can customize the appearance and behavior of the gist card using the [common options](/frontend/docs/customization/common-options/) and exclusive options listed in the table below.
| Name | Description | Type | Default value |
| ------------------- | ------------------------------------------------------------------------------------------------------- | ------- | ------------- |
| `show_owner` | Shows the gist's owner name. | boolean | `false` |
| `browser_rendering` | Compute text wrapping of gist description natively in the browser, instead of computing it server-side. | boolean | `false` |
## Demo
<img class="card-preview-light" src="/api/gist?id=bbfce31e0217a3689c8d961a356cb10d&theme=light_github_repocard" alt="Gist Card" />
<img class="card-preview-dark" src="/api/gist?id=bbfce31e0217a3689c8d961a356cb10d&theme=dark_github_repocard" alt="Gist Card" />
Use [show\_owner](#options) query option to include the gist's owner username
<img class="card-preview-light" src="/api/gist?id=bbfce31e0217a3689c8d961a356cb10d&show_owner=true&theme=light_github_repocard" alt="Gist Card" />
<img class="card-preview-dark" src="/api/gist?id=bbfce31e0217a3689c8d961a356cb10d&show_owner=true&theme=dark_github_repocard" alt="Gist Card" />
@@ -0,0 +1,53 @@
---
title: Repo Pin Card
---
GitHub extra pins allow you to pin more than 6 repositories in your profile using a GitHub profile readme.
Yay! You are no longer limited to 6 pinned repositories.
## Usage
Copy-paste this code into your readme and change the links.
Endpoint: `api/pin?username=anuraghazra&repo=type-trident`
```md
[![Readme Card](https://github-stats-extended.vercel.app/api/pin/?username=anuraghazra&repo=type-trident)](https://github.com/anuraghazra/type-trident)
```
## Options
You can customize the appearance and behavior of the pinned repository card using the [common options](/frontend/docs/customization/common-options/) and exclusive options listed in the table below.
| Name | Description | Type | Default value |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------- | ------------------ |
| `show_owner` | Shows the repo's owner name. | boolean | `false` |
| `browser_rendering` | Compute text wrapping of repository description natively in the browser, instead of computing it server-side. | boolean | `false` |
| `description_lines_count` | Manually set the number of lines for the description. Specified value will be clamped between 1 and 3. If this parameter is not specified, the number of lines will be automatically adjusted according to the actual length of the description. | number | `null` |
| `card_width` | Sets the card's width manually. | number | `400px (approx.)` |
| `show_icons` | Shows icons near all stats enabled via `show`. | boolean | `true` |
| `line_height` | Sets the line height between stats enabled via `show`. | integer | `22` |
| `text_bold` | Uses bold text for all stats enabled via `show`. | boolean | `false` |
| `number_format` | Switches between two available formats for displaying the numbers for all stats enabled via `show`: `short` (i.e. `6.6k`) and `long` (i.e. `6626`). | enum | `short` |
| `show` | Shows [additional items](/frontend/docs/cards/stats/#showing-additional-individual-stats) on stats card (i.e. `prs_authored`, `prs_commented`, `prs_reviewed`, `issues_authored` or `issues_commented`). | string (comma-separated values) | `null` |
## Demo
<img class="card-preview-light" src="/api/pin?username=anuraghazra&repo=github-readme-stats&theme=light_github_repocard" alt="Readme Card" />
<img class="card-preview-dark" src="/api/pin?username=anuraghazra&repo=github-readme-stats&theme=dark_github_repocard" alt="Readme Card" />
Use [show\_owner](#options) query option to include the repo's owner username:
<img class="card-preview-light" src="/api/pin?username=anuraghazra&repo=github-readme-stats&show_owner=true&theme=light_github_repocard" alt="Readme Card" />
<img class="card-preview-dark" src="/api/pin?username=anuraghazra&repo=github-readme-stats&show_owner=true&theme=dark_github_repocard" alt="Readme Card" />
Use [show](#options) query option to display the user's contributions to the repository:
<img class="card-preview-light" src="/api/pin?username=anuraghazra&repo=github-readme-stats&show=prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented&theme=light_github_repocard" alt="Readme Card" />
<img class="card-preview-dark" src="/api/pin?username=anuraghazra&repo=github-readme-stats&show=prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented&theme=dark_github_repocard" alt="Readme Card" />
You can also specify the `repo` parameter in the form `<user_or_organization>/<repository>` to pin a repository from any user or organization, not just your own. This allows you to showcase repositories you contributed to, regardless of ownership.
<img class="card-preview-light" src="/api/pin?username=anuraghazra&repo=statykjs/statyk&show_owner=true&show=prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented&theme=light_github_repocard" alt="Readme Card" />
<img class="card-preview-dark" src="/api/pin?username=anuraghazra&repo=statykjs/statyk&show_owner=true&show=prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented&theme=dark_github_repocard" alt="Readme Card" />
@@ -0,0 +1,101 @@
---
title: Stats Card
---
The stats card shows a summary of your GitHub activity: stars earned, commits, pull requests, issues, contributions and an overall rank.
:::caution[Warning]
By default, the stats card only shows statistics like stars, commits, and pull requests from public repositories. To show private statistics on the stats card, [allow GitHub-Stats-Extended to access your private contributions](/frontend/docs/fork/#private-contributions-support) or [deploy your own instance](/frontend/docs/deploy/).
:::
:::note
Available ranks are S (top 1%), A+ (12.5%), A (25%), A- (37.5%), B+ (50%), B (62.5%), B- (75%), C+ (87.5%) and C (everyone). This ranking scheme is based on the [Japanese academic grading](https://wikipedia.org/wiki/Academic_grading_in_Japan) system. The global percentile is calculated as a weighted sum of percentiles for each statistic (number of commits, pull requests, reviews, issues, stars, and followers), based on the cumulative distribution function of the [exponential](https://wikipedia.org/wiki/exponential_distribution) and the [log-normal](https://wikipedia.org/wiki/Log-normal_distribution) distributions. The implementation can be investigated at [calculateRank.ts](https://github.com/stats-organization/github-stats-extended/blob/master/packages/core/src/calculateRank.ts). The circle around the rank shows 100 minus the global percentile.
:::
## Hiding individual stats
You can pass a query parameter `&hide=` to hide any specific stats with comma-separated values.
> Options: `&hide=stars,commits,prs,issues,contribs`
```md
![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra&hide=contribs,prs)
```
## Showing additional individual stats
You can pass a query parameter `&show=` to show any specific additional stats with comma-separated values.
> Options: `&show=contributions,all_time_contribs,reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented`
```md
![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra&show=contributions,all_time_contribs,reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented)
```
:::note
[`contributions`](/frontend/docs/fork/#new-contributions-stat) counts contributions across all years.
[`all_time_contribs`](/frontend/docs/fork/#new-options-for-contributed-to-stats) counts repositories the user has contributed to across all years.
The `contribs` stat under `&hide=` counts repositories the user has contributed to in the past year.
:::
## Showing icons
To enable icons, you can pass `&show_icons=true` in the query param, like so:
```md
![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true)
```
## Filtering by repository and owner
To compute your stats for only a specific repository, you can pass a query parameter `&repo=<user_or_organization>/<repository>`. You can also specify a comma-separated list of multiple repositories, e.g. `&repo=userA/repositoryA,organizationB/repositoryB`. And you can select all repositories owned by specific organizations or users by providing a comma-separated list of owners via the `owner` query parameter, e.g. `&owner=userA,organizationB,organizationC`. The `repo` and `owner` filters are supported by the following items: `commits` (when used with `&include_all_commits=true`), `prs_authored`, `prs_commented`, `prs_reviewed`, `issues_authored` and `issues_commented`. Note that most of these items are not displayed by default, but [you can enable them individually](#showing-additional-individual-stats).
(Some of these mentioned items are similar to other items which are included by default, e.g. `issues_authored` is similar to `issues`. The difference is how these values are fetched - [via GraphQL or via REST API](https://github.com/anuraghazra/github-readme-stats/discussions/1770#number-of-commits-is-incorrect). The default items use GraphQL, but filtering by repository works better via REST API.)
Alternatively, you can use the `role` parameter to specify a comma-separated list of [roles](https://docs.github.com/en/graphql/reference/repos#enum-repositoryaffiliation). The stats will include all repositories in which the user has the specified role. By default, only repositories where the user is OWNER will be included, but you could e.g. set `&role=OWNER,ORGANIZATION_MEMBER,COLLABORATOR`. The `role` parameter is supported by all items except the following: `commits` (when used with `&include_all_commits=true`), `prs_authored`, `prs_commented`, `prs_reviewed`, `issues_authored` and `issues_commented`.
## Showing commits count for specified year
You can specify a year and fetch only the commits that were made in that year by passing `&commits_year=YYYY` to the parameter.
```md
![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra&commits_year=2020)
```
## Options
You can customize the appearance and behavior of the stats card using the [common options](/frontend/docs/customization/common-options/) and the exclusive options listed in the table below.
| Name | Description | Type | Default value |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ----------------------------------- |
| `hide` | Hides the [specified items](#hiding-individual-stats) from stats. | string (comma-separated values) | `null` |
| `hide_title` | Hides the title of your stats card. | boolean | `false` |
| `card_width` | Sets the card's width manually. | number | `500px (approx.)` |
| `hide_rank` | Hides the rank and automatically resizes the card width. | boolean | `false` |
| `rank_icon` | Shows alternative rank icon (i.e. `github`, `percentile` or `default`). | enum | `default` |
| `show_icons` | Shows icons near all stats. | boolean | `false` |
| `include_all_commits` | Count total commits instead of just the current year commits. | boolean | `false` |
| `line_height` | Sets the line height between text. | integer | `25` |
| `exclude_repo` | Excludes specified repositories. Affects only the count for "Total Stars Earned". | string (comma-separated values) | `null` |
| `repo` | Count only stats from the specified repositories. Affects only [certain items](#filtering-by-repository-and-owner). | string (comma-separated values) | `null` |
| `owner` | Count only stats from the specified organizations or users. Affects only [certain items](#filtering-by-repository-and-owner). | string (comma-separated values) | `null` |
| `role` | Include repositories where the user has one of the specified [roles](https://docs.github.com/en/graphql/reference/repos#enum-repositoryaffiliation) (OWNER, ORGANIZATION_MEMBER, COLLABORATOR). | string (comma-separated values) | `OWNER` |
| `custom_title` | Sets a custom title for the card. | string | `<username> GitHub Stats` |
| `text_bold` | Uses bold text. | boolean | `true` |
| `disable_animations` | Disables all animations in the card. | boolean | `false` |
| `ring_color`<sup>1</sup> | Color of the rank circle. | string (hex color) | `2f80ed` |
| `number_format` | Switches between two available formats for displaying the card values: `short` (i.e. `6.6k`) and `long` (i.e. `6626`). | enum | `short` |
| `number_precision` | Enforce the number of digits after the decimal point for `short` number format. Must be an integer between 0 and 2. Will be ignored for `long` number format. | integer (0, 1 or 2) | `null` |
| `show` | Shows [additional items](#showing-additional-individual-stats) on stats card (i.e. `all_time_contribs`, `contributions`, `reviews`, `discussions_started`, `discussions_answered`, `prs_merged` or `prs_merged_percentage`. And the following, which support the `repo` and `owner` filters: `prs_authored`, `prs_commented`, `prs_reviewed`, `issues_authored` or `issues_commented`). | string (comma-separated values) | `null` |
| `contribs_include_own_repos` | Includes the user's own repositories when calculating the `contribs` and `all_time_contribs` stats. By default, only repositories owned by other users or organizations are counted. | boolean | `false` |
| `commits_year` | Filters and counts only commits made in the specified year. | integer _(YYYY)_ | `<current year> (one year to date)` |
<sup>1</sup>: Supports light and dark mode via `ring_color_light` and `ring_color_dark`.
:::caution[Warning]
Custom title should be URI-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding) (i.e: `Anurag's GitHub Stats` should become `Anurag%27s%20GitHub%20Stats`). You can use [urlencoder.org](https://www.urlencoder.org/) to help you do this automatically.
:::
:::note
When hide\_rank=`true`, the minimum card width is 270 px + the title length and padding.
:::
@@ -0,0 +1,197 @@
---
title: Top Languages Card
---
The top languages card shows your most frequently used languages.
:::caution[Warning]
By default, the language card shows language results only from public repositories. To include languages used in private repositories, [allow GitHub-Stats-Extended to access your private contributions](/frontend/docs/fork/#private-contributions-support) or [deploy your own instance](/frontend/docs/deploy/).
:::
:::caution[Warning]
This card shows language usage only inside your own non-forked repositories, not depending on who the author of the commits is. It does not include your contributions into another users/organizations repositories. Currently there are no way to get this data from GitHub API. If you want this behavior to be improved you can support [this feature request](https://github.com/orgs/community/discussions/18230) created by [@rickstaa](https://github.com/rickstaa) inside GitHub Community.
:::
:::caution[Warning]
Currently this card shows data only about first 1000 repositories. This is because GitHub API limitations which cause downtimes of public instances (see [#1471](https://github.com/anuraghazra/github-readme-stats/issues/1471)). In future this behavior will be improved by releasing GitHub action or providing environment variables for user's own instances.
:::
## Usage
Copy-paste this code into your readme and change the links.
Endpoint: `api/top-langs?username=anuraghazra`
```md
[![Top Langs](https://github-stats-extended.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/stats-organization/github-stats-extended)
```
## Options
You can customize the appearance and behavior of the top languages card using the [common options](/frontend/docs/customization/common-options/) and exclusive options listed in the table below.
| Name | Description | Type | Default value |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | --------------------------------------------------- |
| `hide` | Hides the [specified languages](#hide-individual-languages) from card. | string (comma-separated values) | `null` |
| `hide_title` | Hides the title of your card. | boolean | `false` |
| `layout` | Switches between five available layouts `normal` & `compact` & `donut` & `donut-vertical` & `pie`. | enum | `normal` |
| `card_width` | Sets the card's width manually. | number | `300` |
| `langs_count` | Shows more languages on the card, between 1-20. | integer | `5` for `normal` and `donut`, `6` for other layouts |
| `exclude_repo` | Excludes specified repositories. | string (comma-separated values) | `null` |
| `role` | Include repositories where the user has one of the specified [roles](https://docs.github.com/en/graphql/reference/repos#enum-repositoryaffiliation) (OWNER, ORGANIZATION_MEMBER, COLLABORATOR). | string (comma-separated values) | `OWNER` |
| `custom_title` | Sets a custom title for the card. | string | `Most Used Languages` |
| `disable_animations` | Disables all animations in the card. | boolean | `false` |
| `prog_bar_bg_color`<sup>1</sup> | Background color of the bars. (Applies only to `normal` layout.) | string (hex color) | `#ddd` |
| `hide_progress` | Uses the compact layout option, hides percentages, and removes the bars. | boolean | `false` |
| `hide_values` | Hides language percentages or bytes while keeping the progress bars or chart. | boolean | `false` |
| `size_weight` | Configures language stats algorithm (see [Language stats algorithm](#language-stats-algorithm)). | integer | `1` |
| `count_weight` | Configures language stats algorithm (see [Language stats algorithm](#language-stats-algorithm)). | integer | `0` |
| `stats_format` | Switches between two available formats for language's stats `percentages` and `bytes`. | enum | `percentages` |
<sup>1</sup>: Supports light and dark mode via `prog_bar_bg_color_light` and `prog_bar_bg_color_dark`.
:::caution[Warning]
Language names and custom title should be URI-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding) (i.e: `c++` should become `c%2B%2B`, `jupyter notebook` should become `jupyter%20notebook`, `Most Used Languages` should become `Most%20Used%20Languages`, etc.) You can use [urlencoder.org](https://www.urlencoder.org/) to help you do this automatically.
:::
## Language stats algorithm
We use the following algorithm to calculate the languages percentages on the language card:
```js
ranking_index = (byte_count ^ size_weight) * (repo_count ^ count_weight);
```
By default, only the byte count is used for determining the languages percentages shown on the language card (i.e. `size_weight=1` and `count_weight=0`). You can, however, use the `&size_weight=` and `&count_weight=` options to weight the language usage calculation. The values must be positive real numbers. [More details about the algorithm can be found here](https://github.com/anuraghazra/github-readme-stats/issues/1600#issuecomment-1046056305).
- `&size_weight=1&count_weight=0` - _(default)_ Orders by byte count.
- `&size_weight=0.5&count_weight=0.5` - _(recommended)_ Uses both byte and repo count for ranking
- `&size_weight=0&count_weight=1` - Orders by repo count
```md
![Top Langs](https://github-stats-extended.vercel.app/api/top-langs/?username=anuraghazra&size_weight=0.5&count_weight=0.5)
```
If the percentages still look wrong to you, these two comments cover most of the reasons why, and are worth reading before opening an issue:
- <https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665164174>
- <https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665172181>
## Exclude individual repositories
You can use the `&exclude_repo=repo1,repo2` parameter to exclude individual repositories.
```md
![Top Langs](https://github-stats-extended.vercel.app/api/top-langs/?username=anuraghazra&exclude_repo=github-readme-stats,anuraghazra.github.io)
```
## Hide individual languages
You can use `&hide=language1,language2` parameter to hide individual languages.
```md
![Top Langs](https://github-stats-extended.vercel.app/api/top-langs/?username=anuraghazra&hide=javascript,html)
```
Language names with spaces or symbols need to be [percent-encoded](#options), so Jupyter Notebook becomes `&hide=jupyter%20notebook` and C++ becomes `&hide=c%2B%2B`.
## Show more languages
You can use the `&langs_count=` option to increase or decrease the number of languages shown on the card. Valid values are integers between 1 and 20 (inclusive). By default it was set to `5` for `normal` & `donut` and `6` for other layouts.
```md
![Top Langs](https://github-stats-extended.vercel.app/api/top-langs/?username=anuraghazra&langs_count=8)
```
## Compact Language Card Layout
You can use the `&layout=compact` option to change the card design.
```md
![Top Langs](https://github-stats-extended.vercel.app/api/top-langs/?username=anuraghazra&layout=compact)
```
## Donut Chart Language Card Layout
You can use the `&layout=donut` option to change the card design.
```md
[![Top Langs](https://github-stats-extended.vercel.app/api/top-langs/?username=anuraghazra&layout=donut)](https://github.com/stats-organization/github-stats-extended)
```
## Donut Vertical Chart Language Card Layout
You can use the `&layout=donut-vertical` option to change the card design.
```md
[![Top Langs](https://github-stats-extended.vercel.app/api/top-langs/?username=anuraghazra&layout=donut-vertical)](https://github.com/stats-organization/github-stats-extended)
```
## Pie Chart Language Card Layout
You can use the `&layout=pie` option to change the card design.
```md
[![Top Langs](https://github-stats-extended.vercel.app/api/top-langs/?username=anuraghazra&layout=pie)](https://github.com/stats-organization/github-stats-extended)
```
## Hide Progress Bars
You can use the `&hide_progress=true` option to hide the percentages and the progress bars (layout will be automatically set to `compact`).
```md
![Top Langs](https://github-stats-extended.vercel.app/api/top-langs/?username=anuraghazra&hide_progress=true)
```
## Change format of language's stats
You can use the `&stats_format=bytes` option to display the stats in bytes instead of percentage.
```md
![Top Langs](https://github-stats-extended.vercel.app/api/top-langs/?username=anuraghazra&stats_format=bytes)
```
## Demo
<img class="card-preview-light" src="/api/top-langs?username=anuraghazra&theme=light_github" alt="Top Langs" />
<img class="card-preview-dark" src="/api/top-langs?username=anuraghazra&theme=dark_github" alt="Top Langs" />
### Compact layout
<img class="card-preview-light" src="/api/top-langs?username=anuraghazra&layout=compact&theme=light_github" alt="Top Langs" />
<img class="card-preview-dark" src="/api/top-langs?username=anuraghazra&layout=compact&theme=dark_github" alt="Top Langs" />
### Donut Chart layout
<a href="/api/top-langs?username=anuraghazra&layout=donut">
<img class="card-preview-light" src="/api/top-langs?username=anuraghazra&layout=donut&theme=light_github" alt="Top Langs" />
<img class="card-preview-dark" src="/api/top-langs?username=anuraghazra&layout=donut&theme=dark_github" alt="Top Langs" />
</a>
### Donut Vertical Chart layout
<a href="/api/top-langs?username=anuraghazra&layout=donut-vertical">
<img class="card-preview-light" src="/api/top-langs?username=anuraghazra&layout=donut-vertical&theme=light_github" alt="Top Langs" />
<img class="card-preview-dark" src="/api/top-langs?username=anuraghazra&layout=donut-vertical&theme=dark_github" alt="Top Langs" />
</a>
### Pie Chart layout
<a href="/api/top-langs?username=anuraghazra&layout=pie">
<img class="card-preview-light" src="/api/top-langs?username=anuraghazra&layout=pie&theme=light_github" alt="Top Langs" />
<img class="card-preview-dark" src="/api/top-langs?username=anuraghazra&layout=pie&theme=dark_github" alt="Top Langs" />
</a>
### Hidden progress bars
<a href="/api/top-langs?username=anuraghazra&hide_progress=true">
<img class="card-preview-light" src="/api/top-langs?username=anuraghazra&hide_progress=true&theme=light_github" alt="Top Langs" />
<img class="card-preview-dark" src="/api/top-langs?username=anuraghazra&hide_progress=true&theme=dark_github" alt="Top Langs" />
</a>
### Display bytes instead of percentage
<a href="/api/top-langs?username=anuraghazra&stats_format=bytes">
<img class="card-preview-light" src="/api/top-langs?username=anuraghazra&stats_format=bytes&theme=light_github" alt="Top Langs" />
<img class="card-preview-dark" src="/api/top-langs?username=anuraghazra&stats_format=bytes&theme=dark_github" alt="Top Langs" />
</a>
@@ -0,0 +1,54 @@
---
title: WakaTime Card
---
The WakaTime card shows how much time you have spent coding in each language, taken from your [WakaTime](https://wakatime.com) profile.
:::caution[Warning]
Please be aware that we currently only show data from WakaTime profiles that are public. You therefore have to make sure that **BOTH** `Display code time publicly` and `Display languages, editors, os, categories publicly` are enabled.
:::
:::caution[Warning]
In case you just created a new WakaTime account, then it might take up to 24 hours until your stats will become visible on the WakaTime card.
:::
Change the `?username=` value to your WakaTime username.
```md
[![Alan's WakaTime stats](https://github-stats-extended.vercel.app/api/wakatime?username=alan)](https://wakatime.com/@alan)
```
## Options
You can customize the appearance and behavior of the WakaTime card using the [common options](/frontend/docs/customization/common-options/) and exclusive options listed in the table below.
| Name | Description | Type | Default value |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ---------------- |
| `hide` | Hides the languages specified from the card. | string (comma-separated values) | `null` |
| `hide_title` | Hides the title of your card. | boolean | `false` |
| `card_width` | Sets the card's width manually. | number | `495` |
| `line_height` | Sets the line height between text. | integer | `25` |
| `hide_progress` | Hides the progress bar and percentage. | boolean | `false` |
| `custom_title` | Sets a custom title for the card. | string | `WakaTime Stats` |
| `layout` | Switches between two available layouts `default` & `compact`. | enum | `default` |
| `langs_count` | Limits the number of languages on the card, defaults to all reported languages. | integer | `null` |
| `api_domain` | Sets a custom API domain for the card, e.g. to use services like [Hakatime](https://github.com/mujx/hakatime) or [Wakapi](https://github.com/muety/wakapi) | string | `wakatime.com` |
| `display_format` | Sets the WakaTime stats display format. Choose `time` to display time-based stats or `percent` to show percentages. | enum | `time` |
| `disable_animations` | Disables all animations in the card. | boolean | `false` |
:::caution[Warning]
Custom title should be URI-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding) (i.e: `WakaTime Stats` should become `WakaTime%20Stats`). You can use [urlencoder.org](https://www.urlencoder.org/) to help you do this automatically.
:::
## Demo
<img class="card-preview-light" src="/api/wakatime?username=alan&theme=light_github" alt="Alan's WakaTime stats" />
<img class="card-preview-dark" src="/api/wakatime?username=alan&theme=dark_github" alt="Alan's WakaTime stats" />
<img class="card-preview-light" src="/api/wakatime?username=alan&card_width=315&hide_progress=true&theme=light_github" alt="Alan's WakaTime stats" />
<img class="card-preview-dark" src="/api/wakatime?username=alan&card_width=315&hide_progress=true&theme=dark_github" alt="Alan's WakaTime stats" />
### Compact layout
<img class="card-preview-light" src="/api/wakatime?username=alan&layout=compact&theme=light_github" alt="Alan's WakaTime stats" />
<img class="card-preview-dark" src="/api/wakatime?username=alan&layout=compact&theme=dark_github" alt="Alan's WakaTime stats" />
@@ -0,0 +1,91 @@
---
title: Aligning Cards
---
By default, GitHub does not lay out the cards side by side. To do that, you can use such approaches:
## Stats and top languages cards
<!-- prettier-ignore -->
```html
<a href="https://github-stats-extended.vercel.app/api?username=anuraghazra">
<picture>
<source
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&theme=dark_github"
media="(prefers-color-scheme: dark)"
/>
<img height="200" align="center" src="https://github-stats-extended.vercel.app/api?username=anuraghazra&theme=light_github" />
</picture>
</a>
<a href="https://github-stats-extended.vercel.app/api/top-langs?username=anuraghazra&layout=compact&langs_count=8&card_width=320">
<picture>
<source
srcset="https://github-stats-extended.vercel.app/api/top-langs?username=anuraghazra&layout=compact&langs_count=8&card_width=320&theme=dark_github"
media="(prefers-color-scheme: dark)"
/>
<img height="200" align="center" src="https://github-stats-extended.vercel.app/api/top-langs?username=anuraghazra&layout=compact&langs_count=8&card_width=320&theme=light_github" />
</picture>
</a>
```
<details>
<summary>👀 Show example</summary>
<div class="card-row">
<a href="/api?username=anuraghazra">
<img class="card-preview-light" src="/api?username=anuraghazra&theme=light_github" alt="Anurag's GitHub stats" />
<img class="card-preview-dark" src="/api?username=anuraghazra&theme=dark_github" alt="Anurag's GitHub stats" />
</a>
<a href="/api/top-langs?username=anuraghazra&layout=compact&langs_count=8&card_width=320">
<img class="card-preview-light" src="/api/top-langs?username=anuraghazra&layout=compact&langs_count=8&card_width=320&theme=light_github" alt="Top languages" />
<img class="card-preview-dark" src="/api/top-langs?username=anuraghazra&layout=compact&langs_count=8&card_width=320&theme=dark_github" alt="Top languages" />
</a>
</div>
</details>
## Pinning repositories
<!-- prettier-ignore -->
```html
<a href="https://github.com/anuraghazra/github-readme-stats">
<picture>
<source
srcset="https://github-stats-extended.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&theme=dark_github_repocard"
media="(prefers-color-scheme: dark)"
/>
<img
align="center"
src="https://github-stats-extended.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&theme=light_github_repocard"
/>
</picture>
</a>
<a href="https://github.com/anuraghazra/convoychat">
<picture>
<source
srcset="https://github-stats-extended.vercel.app/api/pin/?username=anuraghazra&repo=convoychat&theme=dark_github_repocard"
media="(prefers-color-scheme: dark)"
/>
<img
align="center"
src="https://github-stats-extended.vercel.app/api/pin/?username=anuraghazra&repo=convoychat&theme=light_github_repocard"
/>
</picture>
</a>
```
<details>
<summary>👀 Show example</summary>
<div class="card-row">
<a href="https://github.com/anuraghazra/github-readme-stats">
<img class="card-preview-light" src="/api/pin?username=anuraghazra&repo=github-readme-stats&theme=light_github_repocard" alt="Readme Card" />
<img class="card-preview-dark" src="/api/pin?username=anuraghazra&repo=github-readme-stats&theme=dark_github_repocard" alt="Readme Card" />
</a>
<a href="https://github.com/anuraghazra/convoychat">
<img class="card-preview-light" src="/api/pin?username=anuraghazra&repo=convoychat&theme=light_github_repocard" alt="Readme Card" />
<img class="card-preview-dark" src="/api/pin?username=anuraghazra&repo=convoychat&theme=dark_github_repocard" alt="Readme Card" />
</a>
</div>
</details>
@@ -0,0 +1,30 @@
---
title: Common Options
---
Every card accepts the options below, on top of the exclusive options listed on its own page.
| Name | Description | Type | Default value |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------- |
| `title_color`<sup>1</sup> | Card's title color. | string (hex color) | `2f80ed` |
| `text_color`<sup>1</sup> | Body text color. | string (hex color) | `434d58` |
| `icon_color`<sup>1</sup> | Icons color if available. | string (hex color) | `4c71f2` |
| `border_color`<sup>1</sup> | Card's border color. Does not apply when `hide_border` is enabled. | string (hex color) | `e4e2e2` |
| `bg_color`<sup>1</sup> | Card's background color. | string (hex color or a gradient in the form of _angle,start,end_) | `fffefe` |
| `hide_border` | Hides the card's border. | boolean | `false` |
| `theme`<sup>1</sup> | Name of the theme, choose from [all available themes](/frontend/docs/customization/themes/). | enum | `default` |
| `cache_seconds` | Sets the cache header manually (min: 21600, max: 86400). | integer | `21600` |
| `locale` | Sets the language in the card, you can check full list of available locales [here](/frontend/docs/customization/locales/). | enum | `en` |
| `border_radius` | Corner rounding on the card. | number | `4.5` |
<sup>1</sup>: Supports light and dark mode via `*_light` / `*_dark` variants (e.g. `title_color_light`). See [Light & Dark Mode Parameters](/frontend/docs/customization/theming/#light--dark-mode-parameters) for details.
:::caution[Warning]
We use caching to decrease the load on our servers (see [this discussion](https://github.com/anuraghazra/github-readme-stats/issues/1471#issuecomment-1271551425)). Cards generated by [https://github-stats-extended.vercel.app/](https://github-stats-extended.vercel.app/frontend) are usually cached for a few days. If you need your card data to update more frequently, you can [deploy your own instance](/frontend/docs/deploy/) and set the [`CACHE_SECONDS`](/frontend/docs/deploy/#available-environment-variables) environment variable to your preferred value. Alternatively, you can use the [GitHub Actions workflow](/frontend/docs/deploy/#github-action) to update your cards on a schedule.
:::
## Gradient in bg\_color
You can provide multiple comma-separated values in the bg\_color option to render a gradient with the following format:
&bg_color=DEG,COLOR1,COLOR2,COLOR3...COLOR10
@@ -0,0 +1,73 @@
---
title: Locales
---
Here is a list of all available locales:
<table>
<tr><td>
| Code | Locale |
| ------- | ---------------- |
| `ar` | Arabic |
| `az` | Azerbaijani |
| `be` | Belarusian |
| `bn` | Bengali |
| `bg` | Bulgarian |
| `my` | Burmese |
| `ca` | Catalan |
| `cn` | Chinese |
| `zh-tw` | Chinese (Taiwan) |
| `cs` | Czech |
| `nl` | Dutch |
| `en` | English |
| `fil` | Filipino |
| `fi` | Finnish |
| `fr` | French |
| `de` | German |
</td><td>
| Code | Locale |
| ------- | --------------------- |
| `el` | Greek |
| `he` | Hebrew |
| `hi` | Hindi |
| `hu` | Hungarian |
| `id` | Indonesian |
| `it` | Italian |
| `ja` | Japanese |
| `kr` | Korean |
| `ml` | Malayalam |
| `np` | Nepali |
| `no` | Norwegian |
| `fa` | Persian (Farsi) |
| `pl` | Polish |
| `pt-br` | Portuguese (Brazil) |
| `pt-pt` | Portuguese (Portugal) |
| `ro` | Romanian |
</td><td>
| Code | Locale |
| --------- | ------------------ |
| `ru` | Russian |
| `sa` | Sanskrit |
| `sr` | Serbian (Cyrillic) |
| `sr-latn` | Serbian (Latin) |
| `sk` | Slovak |
| `es` | Spanish |
| `sw` | Swahili |
| `se` | Swedish |
| `ta` | Tamil |
| `th` | Thai |
| `tr` | Turkish |
| `uk-ua` | Ukrainian |
| `ur` | Urdu |
| `uz` | Uzbek |
| `vi` | Vietnamese |
</td></tr>
</table>
If we don't support your language, please consider contributing! You can find more information about how to do it in our [contributing guidelines](https://github.com/stats-organization/github-stats-extended/blob/master/.github/CONTRIBUTING.md#translations-contribution).
@@ -0,0 +1,238 @@
---
title: Available Themes
---
<!-- DO NOT EDIT THIS FILE DIRECTLY -->
With inbuilt themes, you can customize the look of the card without doing any manual customization.
Use `?theme=THEME_NAME` parameter like so:
```md
![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra&theme=dark&show_icons=true)
```
## Stats
> These themes work with all five of our cards: Stats Card, Repo Card, Gist Card, Top Languages Card, and WakaTime Card.
| | | |
| :------------------------------------------------------------: | :------------------------------------------------------------------: | :------------------------------------------------------------: |
| `default` ![default][default] | `light_github` ![light_github][light_github] | `dark_github` ![dark_github][dark_github] |
| `transparent` ![transparent][transparent] | `shadow_red` ![shadow_red][shadow_red] | `shadow_green` ![shadow_green][shadow_green] |
| `shadow_blue` ![shadow_blue][shadow_blue] | `dark` ![dark][dark] | `radical` ![radical][radical] |
| `merko` ![merko][merko] | `gruvbox` ![gruvbox][gruvbox] | `gruvbox_light` ![gruvbox_light][gruvbox_light] |
| `tokyonight` ![tokyonight][tokyonight] | `onedark` ![onedark][onedark] | `cobalt` ![cobalt][cobalt] |
| `synthwave` ![synthwave][synthwave] | `highcontrast` ![highcontrast][highcontrast] | `dracula` ![dracula][dracula] |
| `prussian` ![prussian][prussian] | `monokai` ![monokai][monokai] | `vue` ![vue][vue] |
| `vue-dark` ![vue-dark][vue-dark] | `shades-of-purple` ![shades-of-purple][shades-of-purple] | `nightowl` ![nightowl][nightowl] |
| `buefy` ![buefy][buefy] | `blue-green` ![blue-green][blue-green] | `algolia` ![algolia][algolia] |
| `great-gatsby` ![great-gatsby][great-gatsby] | `darcula` ![darcula][darcula] | `bear` ![bear][bear] |
| `solarized-dark` ![solarized-dark][solarized-dark] | `solarized-light` ![solarized-light][solarized-light] | `chartreuse-dark` ![chartreuse-dark][chartreuse-dark] |
| `nord` ![nord][nord] | `gotham` ![gotham][gotham] | `material-palenight` ![material-palenight][material-palenight] |
| `graywhite` ![graywhite][graywhite] | `vision-friendly-dark` ![vision-friendly-dark][vision-friendly-dark] | `ayu-mirage` ![ayu-mirage][ayu-mirage] |
| `midnight-purple` ![midnight-purple][midnight-purple] | `calm` ![calm][calm] | `flag-india` ![flag-india][flag-india] |
| `omni` ![omni][omni] | `react` ![react][react] | `jolly` ![jolly][jolly] |
| `maroongold` ![maroongold][maroongold] | `yeblu` ![yeblu][yeblu] | `blueberry` ![blueberry][blueberry] |
| `slateorange` ![slateorange][slateorange] | `kacho_ga` ![kacho_ga][kacho_ga] | `outrun` ![outrun][outrun] |
| `ocean_dark` ![ocean_dark][ocean_dark] | `city_lights` ![city_lights][city_lights] | `github_dark` ![github_dark][github_dark] |
| `github_dark_dimmed` ![github_dark_dimmed][github_dark_dimmed] | `discord_old_blurple` ![discord_old_blurple][discord_old_blurple] | `aura_dark` ![aura_dark][aura_dark] |
| `panda` ![panda][panda] | `noctis_minimus` ![noctis_minimus][noctis_minimus] | `cobalt2` ![cobalt2][cobalt2] |
| `swift` ![swift][swift] | `aura` ![aura][aura] | `apprentice` ![apprentice][apprentice] |
| `moltack` ![moltack][moltack] | `codeSTACKr` ![codeSTACKr][codeSTACKr] | `rose_pine` ![rose_pine][rose_pine] |
| `catppuccin_latte` ![catppuccin_latte][catppuccin_latte] | `catppuccin_mocha` ![catppuccin_mocha][catppuccin_mocha] | `date_night` ![date_night][date_night] |
| `one_dark_pro` ![one_dark_pro][one_dark_pro] | `rose` ![rose][rose] | `holi` ![holi][holi] |
| `neon` ![neon][neon] | `blue_navy` ![blue_navy][blue_navy] | `calm_pink` ![calm_pink][calm_pink] |
| `ambient_gradient` ![ambient_gradient][ambient_gradient] | | |
## Repo Card
> These themes work with all five of our cards: Stats Card, Repo Card, Gist Card, Top Languages Card, and WakaTime Card.
| | | |
| :-----------------------------------------------------------------: | :--------------------------------------------------------------------------: | :-----------------------------------------------------------------------: |
| `default_repocard` ![default_repocard][default_repocard_repo] | `light_github_repocard` ![light_github_repocard][light_github_repocard_repo] | `dark_github_repocard` ![dark_github_repocard][dark_github_repocard_repo] |
| `transparent` ![transparent][transparent_repo] | `shadow_red` ![shadow_red][shadow_red_repo] | `shadow_green` ![shadow_green][shadow_green_repo] |
| `shadow_blue` ![shadow_blue][shadow_blue_repo] | `dark` ![dark][dark_repo] | `radical` ![radical][radical_repo] |
| `merko` ![merko][merko_repo] | `gruvbox` ![gruvbox][gruvbox_repo] | `gruvbox_light` ![gruvbox_light][gruvbox_light_repo] |
| `tokyonight` ![tokyonight][tokyonight_repo] | `onedark` ![onedark][onedark_repo] | `cobalt` ![cobalt][cobalt_repo] |
| `synthwave` ![synthwave][synthwave_repo] | `highcontrast` ![highcontrast][highcontrast_repo] | `dracula` ![dracula][dracula_repo] |
| `prussian` ![prussian][prussian_repo] | `monokai` ![monokai][monokai_repo] | `vue` ![vue][vue_repo] |
| `vue-dark` ![vue-dark][vue-dark_repo] | `shades-of-purple` ![shades-of-purple][shades-of-purple_repo] | `nightowl` ![nightowl][nightowl_repo] |
| `buefy` ![buefy][buefy_repo] | `blue-green` ![blue-green][blue-green_repo] | `algolia` ![algolia][algolia_repo] |
| `great-gatsby` ![great-gatsby][great-gatsby_repo] | `darcula` ![darcula][darcula_repo] | `bear` ![bear][bear_repo] |
| `solarized-dark` ![solarized-dark][solarized-dark_repo] | `solarized-light` ![solarized-light][solarized-light_repo] | `chartreuse-dark` ![chartreuse-dark][chartreuse-dark_repo] |
| `nord` ![nord][nord_repo] | `gotham` ![gotham][gotham_repo] | `material-palenight` ![material-palenight][material-palenight_repo] |
| `graywhite` ![graywhite][graywhite_repo] | `vision-friendly-dark` ![vision-friendly-dark][vision-friendly-dark_repo] | `ayu-mirage` ![ayu-mirage][ayu-mirage_repo] |
| `midnight-purple` ![midnight-purple][midnight-purple_repo] | `calm` ![calm][calm_repo] | `flag-india` ![flag-india][flag-india_repo] |
| `omni` ![omni][omni_repo] | `react` ![react][react_repo] | `jolly` ![jolly][jolly_repo] |
| `maroongold` ![maroongold][maroongold_repo] | `yeblu` ![yeblu][yeblu_repo] | `blueberry` ![blueberry][blueberry_repo] |
| `slateorange` ![slateorange][slateorange_repo] | `kacho_ga` ![kacho_ga][kacho_ga_repo] | `outrun` ![outrun][outrun_repo] |
| `ocean_dark` ![ocean_dark][ocean_dark_repo] | `city_lights` ![city_lights][city_lights_repo] | `github_dark` ![github_dark][github_dark_repo] |
| `github_dark_dimmed` ![github_dark_dimmed][github_dark_dimmed_repo] | `discord_old_blurple` ![discord_old_blurple][discord_old_blurple_repo] | `aura_dark` ![aura_dark][aura_dark_repo] |
| `panda` ![panda][panda_repo] | `noctis_minimus` ![noctis_minimus][noctis_minimus_repo] | `cobalt2` ![cobalt2][cobalt2_repo] |
| `swift` ![swift][swift_repo] | `aura` ![aura][aura_repo] | `apprentice` ![apprentice][apprentice_repo] |
| `moltack` ![moltack][moltack_repo] | `codeSTACKr` ![codeSTACKr][codeSTACKr_repo] | `rose_pine` ![rose_pine][rose_pine_repo] |
| `catppuccin_latte` ![catppuccin_latte][catppuccin_latte_repo] | `catppuccin_mocha` ![catppuccin_mocha][catppuccin_mocha_repo] | `date_night` ![date_night][date_night_repo] |
| `one_dark_pro` ![one_dark_pro][one_dark_pro_repo] | `rose` ![rose][rose_repo] | `holi` ![holi][holi_repo] |
| `neon` ![neon][neon_repo] | `blue_navy` ![blue_navy][blue_navy_repo] | `calm_pink` ![calm_pink][calm_pink_repo] |
| `ambient_gradient` ![ambient_gradient][ambient_gradient_repo] | | |
[default]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=default
[default_repocard]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=default_repocard
[light_github]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=light_github
[dark_github]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=dark_github
[light_github_repocard]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=light_github_repocard
[dark_github_repocard]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=dark_github_repocard
[transparent]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=transparent
[shadow_red]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=shadow_red
[shadow_green]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=shadow_green
[shadow_blue]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=shadow_blue
[dark]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=dark
[radical]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=radical
[merko]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=merko
[gruvbox]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=gruvbox
[gruvbox_light]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=gruvbox_light
[tokyonight]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=tokyonight
[onedark]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=onedark
[cobalt]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=cobalt
[synthwave]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=synthwave
[highcontrast]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=highcontrast
[dracula]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=dracula
[prussian]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=prussian
[monokai]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=monokai
[vue]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=vue
[vue-dark]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=vue-dark
[shades-of-purple]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=shades-of-purple
[nightowl]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=nightowl
[buefy]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=buefy
[blue-green]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=blue-green
[algolia]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=algolia
[great-gatsby]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=great-gatsby
[darcula]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=darcula
[bear]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=bear
[solarized-dark]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=solarized-dark
[solarized-light]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=solarized-light
[chartreuse-dark]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=chartreuse-dark
[nord]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=nord
[gotham]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=gotham
[material-palenight]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=material-palenight
[graywhite]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=graywhite
[vision-friendly-dark]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=vision-friendly-dark
[ayu-mirage]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=ayu-mirage
[midnight-purple]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=midnight-purple
[calm]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=calm
[flag-india]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=flag-india
[omni]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=omni
[react]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=react
[jolly]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=jolly
[maroongold]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=maroongold
[yeblu]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=yeblu
[blueberry]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=blueberry
[slateorange]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=slateorange
[kacho_ga]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=kacho_ga
[outrun]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=outrun
[ocean_dark]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=ocean_dark
[city_lights]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=city_lights
[github_dark]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=github_dark
[github_dark_dimmed]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=github_dark_dimmed
[discord_old_blurple]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=discord_old_blurple
[aura_dark]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=aura_dark
[panda]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=panda
[noctis_minimus]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=noctis_minimus
[cobalt2]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=cobalt2
[swift]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=swift
[aura]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=aura
[apprentice]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=apprentice
[moltack]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=moltack
[codeSTACKr]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=codeSTACKr
[rose_pine]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=rose_pine
[catppuccin_latte]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=catppuccin_latte
[catppuccin_mocha]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=catppuccin_mocha
[date_night]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=date_night
[one_dark_pro]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=one_dark_pro
[rose]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=rose
[holi]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=holi
[neon]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=neon
[blue_navy]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=blue_navy
[calm_pink]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=calm_pink
[ambient_gradient]: /api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=ambient_gradient
[default_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=default
[default_repocard_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=default_repocard
[light_github_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=light_github
[dark_github_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=dark_github
[light_github_repocard_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=light_github_repocard
[dark_github_repocard_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=dark_github_repocard
[transparent_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=transparent
[shadow_red_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=shadow_red
[shadow_green_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=shadow_green
[shadow_blue_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=shadow_blue
[dark_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=dark
[radical_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=radical
[merko_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=merko
[gruvbox_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=gruvbox
[gruvbox_light_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=gruvbox_light
[tokyonight_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=tokyonight
[onedark_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=onedark
[cobalt_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=cobalt
[synthwave_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=synthwave
[highcontrast_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=highcontrast
[dracula_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=dracula
[prussian_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=prussian
[monokai_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=monokai
[vue_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=vue
[vue-dark_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=vue-dark
[shades-of-purple_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=shades-of-purple
[nightowl_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=nightowl
[buefy_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=buefy
[blue-green_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=blue-green
[algolia_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=algolia
[great-gatsby_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=great-gatsby
[darcula_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=darcula
[bear_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=bear
[solarized-dark_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=solarized-dark
[solarized-light_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=solarized-light
[chartreuse-dark_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=chartreuse-dark
[nord_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=nord
[gotham_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=gotham
[material-palenight_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=material-palenight
[graywhite_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=graywhite
[vision-friendly-dark_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=vision-friendly-dark
[ayu-mirage_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=ayu-mirage
[midnight-purple_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=midnight-purple
[calm_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=calm
[flag-india_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=flag-india
[omni_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=omni
[react_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=react
[jolly_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=jolly
[maroongold_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=maroongold
[yeblu_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=yeblu
[blueberry_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=blueberry
[slateorange_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=slateorange
[kacho_ga_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=kacho_ga
[outrun_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=outrun
[ocean_dark_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=ocean_dark
[city_lights_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=city_lights
[github_dark_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=github_dark
[github_dark_dimmed_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=github_dark_dimmed
[discord_old_blurple_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=discord_old_blurple
[aura_dark_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=aura_dark
[panda_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=panda
[noctis_minimus_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=noctis_minimus
[cobalt2_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=cobalt2
[swift_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=swift
[aura_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=aura
[apprentice_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=apprentice
[moltack_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=moltack
[codeSTACKr_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=codeSTACKr
[rose_pine_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=rose_pine
[catppuccin_latte_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=catppuccin_latte
[catppuccin_mocha_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=catppuccin_mocha
[date_night_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=date_night
[one_dark_pro_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=one_dark_pro
[rose_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=rose
[holi_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=holi
[neon_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=neon
[blue_navy_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=blue_navy
[calm_pink_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=calm_pink
[ambient_gradient_repo]: /api/pin?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=ambient_gradient
@@ -0,0 +1,130 @@
---
title: Theming
---
With inbuilt themes, you can customize the look of the card without doing any [manual customization](/frontend/docs/customization/common-options/).
Pass `&theme=THEME_NAME`:
```md
![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical)
```
## All inbuilt themes
GitHub Stats Extended comes with several built-in themes (e.g. `radical`, `merko`, `gruvbox`, `tokyonight`, `onedark`, `cobalt`, `synthwave`, `highcontrast`, `dracula`).
<img src="https://res.cloudinary.com/anuraghazra/image/upload/v1595174536/grs-themes_l4ynja.png" alt="GitHub Stats Extended Themes" width="600px"/>
:::tip
Use `light_github` and `dark_github` to match GitHub's own light and dark themes. For repository and gist cards use `light_github_repocard` and `dark_github_repocard`, which differ only in icon color.
:::
Preview [all available themes](/frontend/docs/customization/themes/) or read the [theme config file](https://github.com/stats-organization/github-stats-extended/blob/master/packages/core/src/themes/index.ts). We have paused the addition of new themes to reduce maintenance effort; pull requests adding one will be closed.
## Light and Dark Mode
<img class="card-preview-light" src="/api?username=anuraghazra&show_icons=true&theme=light_github" alt="Anurag's GitHub stats" />
<img class="card-preview-dark" src="/api?username=anuraghazra&show_icons=true&theme=dark_github" alt="Anurag's GitHub stats" />
There are several ways to switch a card between modes on the client side.
### Use GitHub's media feature (recommended)
[GitHub's media feature](https://github.blog/changelog/2022-05-19-specify-theme-context-for-images-in-markdown-beta/) picks the image from a `<picture>` element using the `prefers-color-scheme` media query.
<!-- prettier-ignore -->
```html
<picture>
<source
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark_github"
media="(prefers-color-scheme: dark)"
/>
<!-- light mode -->
<img src="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=light_github" />
</picture>
```
### Set light and dark mode in one card
`theme_light` / `theme_dark` and the `*_light` / `*_dark` color parameters put both modes in a single card URL, which then follows the viewer's browser or OS setting. See [Light & Dark Mode Parameters](#light--dark-mode-parameters) for the details.
This approach is not GitHub-specific, so it also works outside GitHub — including GitHub sponsorship pages, where the other approaches don't work.
:::note
GitHub serves the card from its [CDN](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-anonymized-urls), so a GitHub theme that differs from the browser/OS setting cannot be detected by this approach.
:::
```md
![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme_light=light_github&theme_dark=dark_github)
```
<details>
<summary>👀 Show example</summary>
![Anurag's GitHub stats](/api?username=anuraghazra&show_icons=true&theme_light=light_github&theme_dark=dark_github)
</details>
### Light & Dark Mode Parameters
These parameters style each mode separately.
**Priority (lowest → highest):**
- default theme
- `theme`
- `theme_light` / `theme_dark`
- general color parameters
- `*_light` / `*_dark` color parameters
For example:
```md
![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme_light=light_github&theme_dark=dark_github)
```
Parameter types can be mixed — for example a light and a dark theme, plus one fixed title color:
```md
![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme_light=light_github&theme_dark=dark_github&title_color=aabbcc)
```
### Use GitHub's theme context tag
Appending [`#gh-dark-mode-only` or `#gh-light-mode-only`](https://github.blog/changelog/2021-11-24-specify-theme-context-for-images-in-markdown/) to an image URL shows it only to viewers on that GitHub mode:
```md
[![Anurag's GitHub stats-Dark](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark_github#gh-dark-mode-only)](https://github.com/stats-organization/github-stats-extended#gh-dark-mode-only)
[![Anurag's GitHub stats-Light](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=light_github#gh-light-mode-only)](https://github.com/stats-organization/github-stats-extended#gh-light-mode-only)
```
### Use the transparent theme
The `transparent` theme has no background, so it sits well on GitHub's light and dark modes:
```md
![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=transparent)
```
<details>
<summary>👀 Show example</summary>
![Anurag's GitHub stats](/api?username=anuraghazra&show_icons=true&theme=transparent)
</details>
### Add transparent alpha channel to a themes bg\_color
Any of [the available themes](/frontend/docs/customization/themes/) turns transparent when `bg_color` carries an alpha channel (i.e. `bg_color=00000000`):
```md
![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&bg_color=00000000)
```
<details>
<summary>👀 Show example</summary>
![Anurag's GitHub stats](/api?username=anuraghazra&show_icons=true&bg_color=00000000)
</details>
@@ -0,0 +1,95 @@
---
title: Demo Gallery
---
## Default
<img class="card-preview-light" src="/api?username=anuraghazra&theme=light_github" alt="Anurag's GitHub stats" />
<img class="card-preview-dark" src="/api?username=anuraghazra&theme=dark_github" alt="Anurag's GitHub stats" />
## Hiding specific stats
<img class="card-preview-light" src="/api?username=anuraghazra&hide=contribs,issues&theme=light_github" alt="Anurag's GitHub stats" />
<img class="card-preview-dark" src="/api?username=anuraghazra&hide=contribs,issues&theme=dark_github" alt="Anurag's GitHub stats" />
## Showing additional stats
<img class="card-preview-light" src="/api?username=anuraghazra&show_icons=true&show=contributions,reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_commented,prs_reviewed,issues_commented&theme=light_github" alt="Anurag's GitHub stats" />
<img class="card-preview-dark" src="/api?username=anuraghazra&show_icons=true&show=contributions,reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_commented,prs_reviewed,issues_commented&theme=dark_github" alt="Anurag's GitHub stats" />
## Showing stats for a specific repository
<img class="card-preview-light" src="/api?username=anuraghazra&repo=anuraghazra/github-readme-stats&hide=prs,issues,stars,commits,contribs&show=prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented&hide_rank=true&custom_title=Anurag%27s%20Stats%20for%20github-readme-stats&card_width=370&theme=light_github" alt="Anurag's GitHub stats for anuraghazra/github-readme-stats" />
<img class="card-preview-dark" src="/api?username=anuraghazra&repo=anuraghazra/github-readme-stats&hide=prs,issues,stars,commits,contribs&show=prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented&hide_rank=true&custom_title=Anurag%27s%20Stats%20for%20github-readme-stats&card_width=370&theme=dark_github" alt="Anurag's GitHub stats for anuraghazra/github-readme-stats" />
## Showing stats for a specific organization
<img class="card-preview-light" src="/api?username=anuraghazra&owner=razorpay&hide=prs,issues,stars,commits,contribs&show=prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented&hide_rank=true&custom_title=Anurag%27s%20Stats%20for%20razorpay&card_width=370&theme=light_github" alt="Anurag's GitHub stats for razorpay" />
<img class="card-preview-dark" src="/api?username=anuraghazra&owner=razorpay&hide=prs,issues,stars,commits,contribs&show=prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented&hide_rank=true&custom_title=Anurag%27s%20Stats%20for%20razorpay&card_width=370&theme=dark_github" alt="Anurag's GitHub stats for razorpay" />
## Showing icons
<img class="card-preview-light" src="/api?username=anuraghazra&hide=issues&show_icons=true&theme=light_github" alt="Anurag's GitHub stats" />
<img class="card-preview-dark" src="/api?username=anuraghazra&hide=issues&show_icons=true&theme=dark_github" alt="Anurag's GitHub stats" />
## Shows GitHub logo instead rank level
<img class="card-preview-light" src="/api?username=anuraghazra&rank_icon=github&theme=light_github" alt="Anurag's GitHub stats" />
<img class="card-preview-dark" src="/api?username=anuraghazra&rank_icon=github&theme=dark_github" alt="Anurag's GitHub stats" />
## Shows user rank percentile instead of rank level
<img class="card-preview-light" src="/api?username=anuraghazra&rank_icon=percentile&theme=light_github" alt="Anurag's GitHub stats" />
<img class="card-preview-dark" src="/api?username=anuraghazra&rank_icon=percentile&theme=dark_github" alt="Anurag's GitHub stats" />
## Customize Border Color
<img class="card-preview-light" src="/api?username=anuraghazra&border_color=2e4058&theme=light_github" alt="Anurag's GitHub stats" />
<img class="card-preview-dark" src="/api?username=anuraghazra&border_color=2e4058&theme=dark_github" alt="Anurag's GitHub stats" />
## Include All Commits
<img class="card-preview-light" src="/api?username=anuraghazra&include_all_commits=true&theme=light_github" alt="Anurag's GitHub stats" />
<img class="card-preview-dark" src="/api?username=anuraghazra&include_all_commits=true&theme=dark_github" alt="Anurag's GitHub stats" />
## Themes
Choose from any of the [default themes](/frontend/docs/customization/themes/)
![Anurag's GitHub stats](/api?username=anuraghazra&show_icons=true&theme=radical)
## Gradient
![Anurag's GitHub stats](/api?username=anuraghazra&bg_color=30,e96443,904e95&title_color=fff&text_color=fff)
## Customizing stats card
![Anurag's GitHub stats](/api?username=anuraghazra&show_icons=true&title_color=fff&icon_color=79ff97&text_color=9f9f9f&bg_color=151515)
## Setting card locale
<img class="card-preview-light" src="/api?username=anuraghazra&locale=es&theme=light_github" alt="Anurag's GitHub stats" />
<img class="card-preview-dark" src="/api?username=anuraghazra&locale=es&theme=dark_github" alt="Anurag's GitHub stats" />
## Customizing repo card
![Customized Card](/api/pin?username=anuraghazra&repo=github-readme-stats&title_color=fff&icon_color=f9f9f9&text_color=9f9f9f&bg_color=151515)
## Gist card
<img class="card-preview-light" src="/api/gist?id=bbfce31e0217a3689c8d961a356cb10d&theme=light_github_repocard" alt="Gist Card" />
<img class="card-preview-dark" src="/api/gist?id=bbfce31e0217a3689c8d961a356cb10d&theme=dark_github_repocard" alt="Gist Card" />
## Customizing gist card
![Gist Card](/api/gist?id=bbfce31e0217a3689c8d961a356cb10d&theme=calm)
## Top languages
<img class="card-preview-light" src="/api/top-langs?username=anuraghazra&theme=light_github" alt="Top Langs" />
<img class="card-preview-dark" src="/api/top-langs?username=anuraghazra&theme=dark_github" alt="Top Langs" />
## WakaTime card
<img class="card-preview-light" src="/api/wakatime?username=alan&theme=light_github" alt="Alan's WakaTime stats" />
<img class="card-preview-dark" src="/api/wakatime?username=alan&theme=dark_github" alt="Alan's WakaTime stats" />
@@ -0,0 +1,208 @@
---
title: Run It Yourself
---
We cache generated cards for a few hours or days to avoid potential rate-limiting in the GitHub API or on Vercel. If you want to set your own cache duration or you want to include private contributions in your stats without granting our hosted version of GitHub-Stats-Extended access to your private contributions, you can run GitHub-Stats-Extended on your own.
GitHub Actions is the simplest setup with static SVGs stored in your repo but less frequent updates, while self-hosting GitHub-Stats-Extended on Vercel takes more work and can serve fresher stats (with caching).
## GitHub Action
With [github-readme-stats-action](https://github.com/stats-organization/github-readme-stats-action) you can generate static cards in your GitHub Actions workflow, commit them to your profile repository, and embed them directly from there. This avoids any per-request API calls.
Create `/.github/workflows/grs.yml` in your profile repo (`USERNAME/USERNAME`):
```yaml
name: Update README cards
on:
schedule:
- cron: "0 0 * * *" # Runs once daily at midnight
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- name: Generate stats card
uses: stats-organization/github-readme-stats-action@v2
with:
card: stats
options: username=${{ github.repository_owner }}&show_icons=true
path: profile/stats.svg
token: ${{ secrets.GITHUB_TOKEN }}
- name: Commit cards
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add profile/*.svg
git commit -m "Update README cards" || exit 0
git push
```
Then embed from your [profile README](https://docs.github.com/en/account-and-profile/how-tos/profile-customization/managing-your-profile-readme#adding-a-profile-readme):
```md
![Stats](./profile/stats.svg)
```
See more options and examples in the [GitHub Readme Stats Action README](https://github.com/stats-organization/github-readme-stats-action#readme).
## Self-hosted on Vercel
Running your own instance avoids public rate limits and gives you full control over caching, tokens, and private stats.
### First step: get your Personal Access Token (PAT)
For deploying your own instance of GitHub Stats Extended, you will need to create a GitHub Personal Access Token (PAT). Below are the steps to create one and the scopes you need to select for both classic and fine-grained tokens.
Selecting the right scopes for your token is important in case you want to display private contributions on your cards.
#### Classic token
- Go to [Account → Settings → Developer Settings → Personal access tokens → Tokens (classic)](https://github.com/settings/tokens).
- Click on `Generate new token → Generate new token (classic)`.
- Scopes to select:
- repo
- read:user
- Click on `Generate token` and copy it.
#### Fine-grained token
:::caution[Warning]
This limits the scope of commits to public repositories only.
:::
- Go to [Account → Settings → Developer Settings → Personal access tokens → Fine-grained tokens](https://github.com/settings/personal-access-tokens).
- Click on `Generate new token → Generate new token`.
- Enter a token name
- Select an expiration date
- Select `All repositories`
- Scopes to select under `Permissions`:
- Commit statuses: read-only
- Contents: read-only
- Issues: read-only
- Metadata: read-only (added automatically when selecting above scopes)
- Pull requests: read-only
- Click on `Generate token` and copy it.
### On Vercel
Click on the deploy button to get started!
[![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/stats-organization/github-stats-extended&root-directory=apps/backend)
<b>Recommended: Step-by-step guide on setting up your own Vercel instance</b>
1. Go to [vercel.com](https://vercel.com/).
2. Click on `Log in`.
![](https://files.catbox.moe/pcxk33.png)
3. Sign in with GitHub by pressing `Continue with GitHub`.
![](https://files.catbox.moe/b9oxey.png)
4. Sign in to GitHub and allow access to all repositories if prompted.
5. Fork this repo. Disable "Copy the `master` branch only".
6. Go back to your [Vercel dashboard](https://vercel.com/dashboard).
7. To import a project, click the `Add New...` button and select the `Project` option.
![](https://files.catbox.moe/3n76fh.png)
8. Search for the forked Git Repository and import it by clicking the `Import` button.
9. Create a Personal Access Token (PAT) as described in the [previous section](#first-step-get-your-personal-access-token-pat).
10. Add the PAT as an environment variable named `PAT_1` (as shown).
![](https://files.catbox.moe/0yclio.png)
Note: For enhanced security, you can add a variable as a sensitive variable. To do this:
1. Go to `Environment Variables` in `Project Settings`, then choose `Add Environment Variable`.
![](https://files.catbox.moe/heprjw.jpg)
2. Uncheck `Deployment` from `Environments`
![](https://files.catbox.moe/tiqgd0.jpg)
3. Now you can make the variable `Sensitive` by checking the checkbox.
![](https://files.catbox.moe/mla5no.jpg)
11. As `Root directory` select the `apps/backend` folder.
12. Click deploy.
13. Point your Vercel project at the `release` branch instead of `master`.
`master` contains unreleased, potentially unstable code, while `release` contains the latest stable release.
1. In your Vercel project settings, go to `Environments → Production` and change the tracked branch from `master` to `release`.
2. Go to `Deployments → ... → Create Deployment`, select `release` and click `Deploy to production`.
14. See your domains to use the API!
### Optional steps
#### Add an SQL database
Add an SQL database, either through an integration such as ["Nile"](https://vercel.com/marketplace/nile), or by manually setting the environment variable `POSTGRES_URL`.
#### Use your own OAuth App
[Create your own OAuth App](https://github.com/settings/developers) and set the environment variables `OAUTH_REDIRECT_URI`, `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` on Vercel accordingly.
#### Silence the turbo build warning
Set the environment variable `TURBO_PLATFORM_ENV_DISABLED` to `true` to disable the build-time warning from [turbo](https://turborepo.dev/) about environment variables missing from `turbo.json`. This warning is not relevant in our project.
### Available environment variables
GitHub Stats Extended provides several environment variables that can be used to customize the behavior of your self-hosted instance. These include:
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Supported values</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CACHE_SECONDS</code></td>
<td>Sets the cache duration in seconds for the generated cards. This variable takes precedence over the default cache timings for the public instance. If this variable is not set, the default cache duration is 24 hours (86,400 seconds).</td>
<td>Any positive integer or <code>0</code> to disable caching</td>
</tr>
<tr>
<td><code>UPDATE_AFTER_HOURS</code></td>
<td>Sets the duration in hours after which the server <a href="/frontend/docs/fork/#improved-performance-and-latency">proactively regenerates</a> a previously requested card. Defaults to 11 hours.</td>
<td>Any int or float</td>
</tr>
<tr>
<td><code>DELETE_AFTER_HOURS</code></td>
<td>Sets the duration in hours after which the server stops <a href="/frontend/docs/fork/#improved-performance-and-latency">proactively regenerating</a> a previously requested card if it hasn't been requested again in the meantime. Defaults to 8 days, i.e. 192 hours.</td>
<td>Any int or float</td>
</tr>
<tr>
<td><code>WHITELIST</code></td>
<td>A comma-separated list of GitHub usernames that are allowed to access your instance. If this variable is not set, all usernames are allowed.</td>
<td>Comma-separated GitHub usernames</td>
</tr>
<tr>
<td><code>GIST_WHITELIST</code></td>
<td>A comma-separated list of GitHub Gist IDs that are allowed to be accessed on your instance. If this variable is not set, all Gist IDs are allowed.</td>
<td>Comma-separated GitHub Gist IDs</td>
</tr>
<tr>
<td><code>EXCLUDE_REPO</code></td>
<td>A comma-separated list of repositories that will be excluded from stats and top languages cards on your instance. This allows repository exclusion without exposing repository names in public URLs. This enhances privacy for self-hosted instances that include private repositories in stats cards.</td>
<td>Comma-separated repository names</td>
</tr>
<tr>
<td><code>FETCH_MULTI_PAGE_STARS</code></td>
<td>Enables fetching all starred repositories for accurate star counts, especially for users with more than 100 repositories. This may increase response times and API points usage, so it is limited to 10 fetches - i.e. 1000 repos - on the public instance.</td>
<td><code>true</code> or <code>false</code> or a maximum number of fetches</td>
</tr>
</tbody>
</table>
See [the Vercel documentation](https://vercel.com/docs/concepts/projects/environment-variables) on adding these environment variables to your Vercel instance.
:::caution[Warning]
Please remember to redeploy your instance after making any changes to the environment variables so that the updates take effect. The changes will not be applied to the previous deployments.
:::
### Keep your fork up to date
You can keep your fork, and thus your private Vercel instance up to date with the upstream using GitHub's [Sync Fork button](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork). You can also use the [pull](https://github.com/wei/pull) package created by [@wei](https://github.com/wei) to automate this process.
As a prerequisite, GitHub has to know that your personal GitHub-Stats-Extended repo is a fork of https://github.com/stats-organization/github-stats-extended. This only works if you follow the "Step-by-step guide on setting up your own Vercel instance" above, instead of clicking the Vercel "Deploy" button above.
+131
View File
@@ -0,0 +1,131 @@
---
title: Fork Information
---
This project is an actively maintained fork and extension of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats).
## Key Differences
Compared to [github-readme-stats](https://github.com/anuraghazra/github-readme-stats), this project adds the following changes:
### Frontend for easy, visual configuration of cards
GitHub-Stats-Extended adds a frontend which allows users to visually configure stats cards. It is hosted at https://github-stats-extended.vercel.app/frontend.
![frontend screenshot](frontend-screenshot.png)
The frontend is based on [GitHub Trends](https://github.com/avgupta456/github-trends) by [@avgupta456](https://github.com/avgupta456).
### Aggregate stats across organizations
To include stars from repos which are not owned by you, but where you are a collaborator or organization member, add `&role=OWNER,ORGANIZATION_MEMBER,COLLABORATOR` to your stats card url. To include such repos in your language stats, you can also add the same parameter to your top languages card url.
See [here](/frontend/docs/cards/stats/#filtering-by-repository-and-owner) for full feature documentation.
The resolution of this most requested feature in github-readme-stats was [originally implemented](https://github.com/anuraghazra/github-readme-stats/issues/1#issuecomment-855681098) by [@developStorm](https://github.com/developStorm).
### Improved performance and latency
GitHub-Stats-Extended proactively precomputes and caches cards. This solves the problem where [cards wouldn't load on the first try](https://github.com/anuraghazra/github-readme-stats/issues/2603). It also gives GitHub-Stats-Extended more time while generating cards in the background, which allows it to fetch more repo data:
### Multi-page fetching for accurate star counts
GitHub-Stats-Extended fetches up to 1000 of your starred repositories to accurately compute your stars count. In github-readme-stats, this is limited to 100 repos because github-readme-stats doesn't have the above-mentioned performance improvements.
### Light and dark mode in a single card URL
GitHub-Stats-Extended adds parameters `theme_light`, `theme_dark` and the `*_light` / `*_dark` color variants (e.g. `title_color_light`), to specify both modes in one card URL. The card then follows the viewer's browser or OS setting.
```md
![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra&theme_light=light_github&theme_dark=dark_github)
```
It works everywhere, including GitHub sponsorship pages, where the other light/dark approaches do not. See [Set light and dark mode in one card](/frontend/docs/customization/theming/#set-light-and-dark-mode-in-one-card) for the details.
### GitHub-themed light and dark themes
GitHub-Stats-Extended adds `light_github` and `dark_github` [themes](/frontend/docs/customization/themes/) that exactly match GitHub's own UI colors. For repo and gist cards use `light_github_repocard` and `dark_github_repocard`, which differ only in icon color.
### New contributions stat
GitHub-Stats-Extended adds an optional stat showing the number of [contributions](https://docs.github.com/en/account-and-profile/reference/profile-contributions-reference#what-counts-as-a-contribution) (commits, pull requests, issues, etc.) across all years of a user's history. Enable it with `&show=contributions`. Whether private contributions are counted depends on [your profile visibility settings](https://docs.github.com/en/account-and-profile/how-tos/contribution-settings/manage-visibility-settings-for-private-contributions-and-achievements#changing-the-visibility-of-your-private-contributions).
:::note
The pre-existing "Contributed to" stat counts repositories a user has contributed to, not contributions.
:::
### New options for "contributed-to" stats
GitHub-Stats-Extended adds an `all_time_contribs` stat that shows the number of repositories a user has contributed to across all years — not just the past year like the default `contribs` stat. Enable it with [`&show=all_time_contribs`](/frontend/docs/cards/stats/#showing-additional-individual-stats).
GitHub-Stats-Extended also adds a parameter [`contribs_include_own_repos`](/frontend/docs/cards/stats/#options) to include the user's own repositories in the `contribs` and `all_time_contribs` stats. By default, both stats exclude them and only count repositories owned by other users or organizations.
### Customization of top languages card
GitHub-Stats-Extended can show your top languages without any numbers via the `hide_values` parameter. And the new `prog_bar_bg_color` parameter sets the background color of progress bars, e.g. to transparent:
<img class="card-preview-light" src="/api/top-langs?username=anuraghazra&langs_count=4&hide_values=true&prog_bar_bg_color=0000&theme=light_github" alt="anuraghazra's top languages without numbers" />
<img class="card-preview-dark" src="/api/top-langs?username=anuraghazra&langs_count=4&hide_values=true&prog_bar_bg_color=0000&theme=dark_github" alt="anuraghazra's top languages without numbers" />
### Private contributions support
GitHub-Stats-Extended can include private contributions in your stats cards. You no longer have to deploy your own instance for that. Just log into the [GitHub-Stats-Extended Wizard](/frontend) via the "GitHub Private Access" button (or click "Upgrade to Private Access" if already logged in). This will allow GitHub-Stats-Extended to see your private contributions.
### Display contributions to specific repositories or organizations
GitHub-Stats-Extended adds the ability to show contribution stats for specific repositories and organizations.
Especially for regular contributors in open source projects it might make sense to display an overview of their own contributions to these projects on their GitHub profile.
See [here](/frontend/docs/cards/stats/#filtering-by-repository-and-owner) for full feature documentation.
---
anuraghazra's contributions to github-readme-stats:
<img class="card-preview-light" src="/api/pin?username=anuraghazra&repo=github-readme-stats&show=prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented&theme=light_github_repocard" alt="anuraghazra's contributions to github-readme-stats" />
<img class="card-preview-dark" src="/api/pin?username=anuraghazra&repo=github-readme-stats&show=prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented&theme=dark_github_repocard" alt="anuraghazra's contributions to github-readme-stats" />
Add `&show=prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented` to your repo card url to display your contributions to the pinned repository.
---
anurag's contributions to razorpay:
<img class="card-preview-light" src="/api?username=anuraghazra&owner=razorpay&hide=prs,issues,stars,commits,contribs&show=prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented&hide_rank=true&custom_title=anurag%27s%20contributions%20to%20razorpay&card_width=333&theme=light_github" alt="anurag's contributions to razorpay" />
<img class="card-preview-dark" src="/api?username=anuraghazra&owner=razorpay&hide=prs,issues,stars,commits,contribs&show=prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented&hide_rank=true&custom_title=anurag%27s%20contributions%20to%20razorpay&card_width=333&theme=dark_github" alt="anurag's contributions to razorpay" />
Add `&repo=userA/repoA,orgB/repoB` or `&owner=userC,orgD` to your profile stats url to filter your contributions by repo or organization. (The screenshot above uses further customization options.)
### Other
GitHub-Stats-Extended adds various other, minor improvements. For example, the repo card now supports the `card_width` parameter.
## Why This Fork Exists
[github-readme-stats](https://github.com/anuraghazra/github-readme-stats) is a great project, which unfortunately saw its development slow down in the past years, with [highly requested features](https://github.com/anuraghazra/github-readme-stats/issues/1935) getting delayed for a long time.
One of the valued maintainers [wrote](https://github.com/anuraghazra/github-readme-stats/pull/3911#issuecomment-3377726545):
> I joined the project as collaborator in the middle of 2023 and there was just a few guys in the team while hundreds of PRs, issues and discussions pending to be reviewed.
>
> The volume is overwhelming for the small team, especially taking into account that right now I'm alone online and working only sometimes when I have a free hours, so it took some time to get to your PR.
So [@martin-mfg](https://github.com/martin-mfg) decided to fork the project, implement some of the highly requested features and make the enhanced project available to everyone. Since the initial release of this fork @martin-mfg joined forces with the maintainers of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) and GitHub-Stats-Extended is now becoming the successor of github-readme-stats.
## Compatibility Notes
GitHub-Stats-Extended aims to be fully compatible with [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). Additional functionality introduced in this fork has to be explicitly enabled via some parameter.
So you can change an existing stats card url from [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) to GitHub-Stats-Extended simply by changing the domain from `github-readme-stats.vercel.app` to `github-stats-extended.vercel.app`. The card will look the same.
There is only one exception to this: GitHub-Stats-Extended improves line wrapping for multi-line gist and repository descriptions.
This should be an improvement for existing cards, but it still changes their appearance a bit.
Previously, line wrapping happened simply after 59 characters, with special handling for Chinese characters:
<img width="400" height="140" alt="character-based" src="https://github.com/user-attachments/assets/1cf7edba-7f6c-4a37-89d7-334cbe54f0f1" />
GitHub-Stats-Extended now takes the actual width of each character into account:
<img width="400" height="150" alt="new-server-side-calculation" src="https://github.com/user-attachments/assets/277b92df-b2a9-48be-bd2a-d28c1d6764c5" />
Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

@@ -0,0 +1,80 @@
---
title: GitHub Stats Extended
---
Dynamically generate GitHub stats for your READMEs.
<img class="card-preview-light" src="/api?username=anuraghazra&theme=light_github" alt="Anurag's GitHub stats" />
<img class="card-preview-dark" src="/api?username=anuraghazra&theme=dark_github" alt="Anurag's GitHub stats" />
GitHub-Stats-Extended is the [extended, actively maintained successor](/frontend/docs/fork/) of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). It generates [various stats cards](#card-types) about your GitHub contributions, your top languages and more. You can [customize](/frontend/docs/customization/common-options/) the cards via multiple parameters.
## Quick Start
Copy and paste this into your markdown, then change the `?username=` value to your GitHub username:
```md
[![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra)](https://github.com/stats-organization/github-stats-extended)
```
As a more comfortable alternative, use the [card wizard](/frontend) to configure your card visually. Then copy the generated markdown 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).
## Migration from github-readme-stats
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
```
GitHub-Stats-Extended aims to be fully compatible with github-readme-stats. For details see [Compatibility Notes](/frontend/docs/fork/#compatibility-notes).
## Card Types
- Show your GitHub statistics:
<img class="card-preview-light" src="/api?username=anuraghazra&theme=light_github" alt="Anurag's GitHub stats" />
<img class="card-preview-dark" src="/api?username=anuraghazra&theme=dark_github" alt="Anurag's GitHub stats" />
- ...your top languages...:
<img class="card-preview-light" src="/api/top-langs?username=anuraghazra&langs_count=4&theme=light_github" alt="Top Langs" />
<img class="card-preview-dark" src="/api/top-langs?username=anuraghazra&langs_count=4&theme=dark_github" alt="Top Langs" />
- ...and development time:
<a href="https://wakatime.com/@alan">
<img class="card-preview-light" src="/api/wakatime?username=alan&langs_count=6&theme=light_github" alt="Alan's WakaTime stats" />
<img class="card-preview-dark" src="/api/wakatime?username=alan&langs_count=6&theme=dark_github" alt="Alan's WakaTime stats" />
</a>
- Pin more than 6 repos in your GitHub profile:
<a href="https://github.com/anuraghazra/github-readme-stats">
<img class="card-preview-light" src="/api/pin?username=anuraghazra&repo=github-readme-stats&theme=light_github_repocard" alt="Readme Card" />
<img class="card-preview-dark" src="/api/pin?username=anuraghazra&repo=github-readme-stats&theme=dark_github_repocard" alt="Readme Card" />
</a>
- Pin Gists in your GitHub profile:
<a href="https://gist.github.com/Yizack/bbfce31e0217a3689c8d961a356cb10d">
<img class="card-preview-light" src="/api/gist?id=bbfce31e0217a3689c8d961a356cb10d&theme=light_github_repocard" alt="Gist Card" />
<img class="card-preview-dark" src="/api/gist?id=bbfce31e0217a3689c8d961a356cb10d&theme=dark_github_repocard" alt="Gist Card" />
</a>
- Customize all the cards:
[![Anurag's GitHub stats](/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)](/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)
## Where to next
- [Cards](/frontend/docs/cards/stats/) — the options each card accepts.
- [Customization](/frontend/docs/customization/common-options/) — options every card shares, plus theming and locales.
- [Available Themes](/frontend/docs/customization/themes/) — the built-in themes, rendered as live examples.
- [Run It Yourself](/frontend/docs/deploy/) — GitHub Actions or a self-hosted Vercel deployment.
- [Fork Information](/frontend/docs/fork/) — 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). The card wizard 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! ❤️
+114 -7
View File
@@ -1,14 +1,121 @@
/* ./src/index.css */
@import "./styles/layer-order.css";
@import "tailwindcss";
@plugin "daisyui";
@plugin "daisyui" {
themes:
light --default,
dark --prefersdark;
}
body {
margin: 0;
font-family: "Segoe UI", Ubuntu, Sans-Serif;
/*
Override primary color with default blue from github-stats-extended.
Override background color in dark mode with GitHub's dark mode background color.
*/
@plugin "daisyui/theme" {
name: "light";
--color-primary: #2f80ed;
--color-error: var(--color-red-700);
--color-error-content: #ffffff;
}
@plugin "daisyui/theme" {
name: "dark";
--color-primary: #2f80ed;
--color-error: var(--color-red-400);
--color-error-content: #0d1117;
--color-base-100: #0d1117;
}
:root {
/* Snappy duration for interactive elements (buttons, hovers). */
--transition-fast: 100ms;
/* Slower, deliberate fade for large surfaces during a theme switch. */
--transition-surface: 300ms;
}
/*
* Everything here is nested so it reaches only the wizard's own nodes:
* the page background, font and reset belong to the site, which shares this page.
*/
/* Scoping only: no background, so overlays mounted on `body` stay see-through. */
.wizard {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: var(--color-base-content);
button {
cursor: pointer;
}
/* Smoothly animate color changes when switching themes / on hover. */
*,
*::before,
*::after {
transition-property: background-color, border-color, color, fill, stroke;
/*
* An ease-out curve: animations start at full speed so quick interactions
* like button hovers feel responsive instead of laggy.
*/
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
transition-duration: var(--transition-fast);
}
/*
* Large surface backgrounds fade more slowly so a theme switch reads as a
* deliberate fade rather than an instant flip. Durations reference the custom
* properties above so the reduced-motion override below zeroes them out
* regardless of selector specificity.
*/
&,
& .bg-base-100,
& .bg-base-200,
& .bg-base-300 {
transition-duration: var(--transition-surface);
}
/*
* DaisyUI's `.btn` ships its own 200ms transition which felt slow. The rule
* above is unlayered, so it would otherwise clobber the button's property list
* and timing function. Restore both here (so fill, text and border animate
* together on outline buttons) and only speed up the duration.
*/
.btn {
transition-property: color, background-color, border-color, box-shadow;
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
transition-duration: var(--transition-fast);
}
/*
* DaisyUI's default hover is very subtle; make it noticeable. Primary always darkens,
* while soft lightens in dark mode and darkens in light mode, so the change shows.
*/
.btn-primary:hover {
background-color: color-mix(in oklab, var(--color-primary), #000 30%);
}
.btn-soft {
background-color: color-mix(in oklab, var(--color-base-100), #888 35%);
}
.btn-soft:hover {
background-color: color-mix(in oklab, var(--color-base-100), #888 60%);
}
.btn-error {
color: var(--color-error-content);
}
.btn-error:hover {
color: var(--color-error-content);
background-color: color-mix(in oklab, var(--color-error), #f00 40%);
}
/* Unscoped, this strips the focus ring from the site's search box. */
input:focus,
select:focus {
outline: 0;
border: var(--color-primary) solid 1px;
}
}
button {
cursor: pointer;
/* Respect users who prefer reduced motion. */
@media (prefers-reduced-motion: reduce) {
:root {
--transition-fast: 0ms;
--transition-surface: 0ms;
}
}
-20
View File
@@ -1,20 +0,0 @@
import "./axios-override";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import { AppTrends } from "./pages/App/AppTrends";
import { store } from "./redux/store";
import "./index.css";
const root = createRoot(document.getElementById("root") as HTMLElement);
root.render(
<StrictMode>
<Provider store={store}>
<AppTrends />
</Provider>
</StrictMode>,
);
@@ -1,84 +0,0 @@
{
"data": {
"user": {
"repositories": {
"totalCount": 139,
"nodes": [
{ "name": "classicLogo", "stargazers": { "totalCount": 1 } },
{ "name": "creativechat", "stargazers": { "totalCount": 1 } },
{ "name": "verlet_tests", "stargazers": { "totalCount": 1 } },
{
"name": "blade-checkout-ui-demo",
"stargazers": { "totalCount": 0 }
},
{
"name": "tslint-plugin-analytics",
"stargazers": { "totalCount": 0 }
},
{ "name": "stargazer", "stargazers": { "totalCount": 0 } },
{
"name": "react-smooth-range-input",
"stargazers": { "totalCount": 0 }
},
{
"name": "csb-git-integration-bug",
"stargazers": { "totalCount": 0 }
},
{ "name": "covid-nepal-web", "stargazers": { "totalCount": 0 } },
{
"name": "shrijan00003.github.io",
"stargazers": { "totalCount": 0 }
},
{ "name": "TheBigCB.com", "stargazers": { "totalCount": 0 } },
{ "name": "html2commonmark", "stargazers": { "totalCount": 0 } },
{ "name": "test-myaction", "stargazers": { "totalCount": 0 } },
{ "name": "react-emoji-render", "stargazers": { "totalCount": 0 } },
{ "name": "sajilo-editor", "stargazers": { "totalCount": 0 } },
{
"name": "coder-aayush.github.io",
"stargazers": { "totalCount": 0 }
},
{ "name": "codesandbox-client", "stargazers": { "totalCount": 0 } },
{ "name": "product-sans", "stargazers": { "totalCount": 0 } },
{ "name": "NodejsBlogSystem", "stargazers": { "totalCount": 0 } },
{ "name": "native-javascript", "stargazers": { "totalCount": 0 } },
{ "name": "p5.js", "stargazers": { "totalCount": 0 } },
{ "name": "gatsbytutorials.com", "stargazers": { "totalCount": 0 } },
{
"name": "hello-world-docker-action",
"stargazers": { "totalCount": 0 }
},
{ "name": "glugmvit.github.io", "stargazers": { "totalCount": 0 } },
{
"name": "react-map-interaction",
"stargazers": { "totalCount": 0 }
},
{ "name": "react-external-image", "stargazers": { "totalCount": 0 } },
{ "name": "gatsby-boilerplate", "stargazers": { "totalCount": 0 } },
{ "name": "YouBackup", "stargazers": { "totalCount": 0 } },
{ "name": "npm-expansions", "stargazers": { "totalCount": 0 } },
{
"name": "Toy-Neural-Network-JS",
"stargazers": { "totalCount": 0 }
},
{ "name": "LSystemCreator", "stargazers": { "totalCount": 0 } },
{ "name": "Happy-2019", "stargazers": { "totalCount": 0 } },
{ "name": "jquery-navScroll", "stargazers": { "totalCount": 0 } },
{ "name": "strml.net", "stargazers": { "totalCount": 0 } },
{ "name": "Logo", "stargazers": { "totalCount": 0 } },
{ "name": "website", "stargazers": { "totalCount": 0 } },
{
"name": "anuraghazra.github.io-old",
"stargazers": { "totalCount": 0 }
},
{ "name": "Quintus", "stargazers": { "totalCount": 0 } },
{ "name": "Electron_experiments", "stargazers": { "totalCount": 0 } }
],
"pageInfo": {
"hasNextPage": false,
"endCursor": "Y3Vyc29yOnYyOpIAzgd5N08="
}
}
}
}
}
-647
View File
@@ -1,647 +0,0 @@
{
"data": {
"user": {
"name": "Anurag Hazra",
"login": "anuraghazra",
"commits": {
"totalCommitContributions": 47
},
"reviews": {
"totalPullRequestReviewContributions": 149
},
"repositoriesContributedTo": {
"totalCount": 1
},
"pullRequests": {
"totalCount": 820
},
"mergedPullRequests": {
"totalCount": 740
},
"openIssues": {
"totalCount": 72
},
"closedIssues": {
"totalCount": 108
},
"followers": {
"totalCount": 14412
},
"repositoryDiscussions": {
"totalCount": 6
},
"repositoryDiscussionComments": {
"totalCount": 7
},
"repositories": {
"totalCount": 139,
"nodes": [
{
"name": "github-readme-stats",
"stargazers": {
"totalCount": 77025
}
},
{
"name": "Verly.js",
"stargazers": {
"totalCount": 696
}
},
{
"name": "anuraghazra.github.io",
"stargazers": {
"totalCount": 608
}
},
{
"name": "anuraghazra",
"stargazers": {
"totalCount": 376
}
},
{
"name": "type-trident",
"stargazers": {
"totalCount": 359
}
},
{
"name": "BugVilla",
"stargazers": {
"totalCount": 232
}
},
{
"name": "convoychat",
"stargazers": {
"totalCount": 214
}
},
{
"name": "CanvasFun",
"stargazers": {
"totalCount": 211
}
},
{
"name": "EvolutionAquerium",
"stargazers": {
"totalCount": 192
}
},
{
"name": "typelevel-parser",
"stargazers": {
"totalCount": 147
}
},
{
"name": "VerlyRangeSlider",
"stargazers": {
"totalCount": 135
}
},
{
"name": "vscode-strip-ts-copy",
"stargazers": {
"totalCount": 72
}
},
{
"name": "react-folder-tree",
"stargazers": {
"totalCount": 54
}
},
{
"name": "Atomic.js",
"stargazers": {
"totalCount": 53
}
},
{
"name": "ToyLang",
"stargazers": {
"totalCount": 51
}
},
{
"name": "design-patterns-everyday",
"stargazers": {
"totalCount": 46
}
},
{
"name": "gatsby",
"stargazers": {
"totalCount": 36
}
},
{
"name": "Slime",
"stargazers": {
"totalCount": 35
}
},
{
"name": "Candy.js",
"stargazers": {
"totalCount": 34
}
},
{
"name": "ShaderExpo",
"stargazers": {
"totalCount": 33
}
},
{
"name": "Nothing",
"stargazers": {
"totalCount": 32
}
},
{
"name": "peerlist-profile-action",
"stargazers": {
"totalCount": 30
}
},
{
"name": "facebook-reaction-animation",
"stargazers": {
"totalCount": 29
}
},
{
"name": "react-stripe-dropdown",
"stargazers": {
"totalCount": 27
}
},
{
"name": "QuickerPoll",
"stargazers": {
"totalCount": 27
}
},
{
"name": "playground-format-on-save",
"stargazers": {
"totalCount": 24
}
},
{
"name": "ParticleBrush",
"stargazers": {
"totalCount": 23
}
},
{
"name": "graphql-oauth",
"stargazers": {
"totalCount": 21
}
},
{
"name": "parasites",
"stargazers": {
"totalCount": 21
}
},
{
"name": "twitter-banner-jokes",
"stargazers": {
"totalCount": 18
}
},
{
"name": "verly-cpp",
"stargazers": {
"totalCount": 16
}
},
{
"name": "gatsby-github-issues-blog",
"stargazers": {
"totalCount": 16
}
},
{
"name": "NikeHyperAce",
"stargazers": {
"totalCount": 12
}
},
{
"name": "rust-particles",
"stargazers": {
"totalCount": 11
}
},
{
"name": "VerletDrawing",
"stargazers": {
"totalCount": 10
}
},
{
"name": "graphql-jwt-auth-test",
"stargazers": {
"totalCount": 9
}
},
{
"name": "anuraghazra.github.io-old-react",
"stargazers": {
"totalCount": 9
}
},
{
"name": "react18-ssr-test",
"stargazers": {
"totalCount": 8
}
},
{
"name": "GyroDodge",
"stargazers": {
"totalCount": 8
}
},
{
"name": "go-phyllotaxis",
"stargazers": {
"totalCount": 6
}
},
{
"name": "graphql-oauth-client",
"stargazers": {
"totalCount": 6
}
},
{
"name": "code-runner-action",
"stargazers": {
"totalCount": 6
}
},
{
"name": "GithubActionsPlayground",
"stargazers": {
"totalCount": 6
}
},
{
"name": "Loader.js",
"stargazers": {
"totalCount": 6
}
},
{
"name": "ts-essentials",
"stargazers": {
"totalCount": 5
}
},
{
"name": "chakra-ui",
"stargazers": {
"totalCount": 5
}
},
{
"name": "text-to-handwriting",
"stargazers": {
"totalCount": 5
}
},
{
"name": "build-your-own-x",
"stargazers": {
"totalCount": 5
}
},
{
"name": "nodejs-dynamic-restapi",
"stargazers": {
"totalCount": 5
}
},
{
"name": "DevYouTubeList",
"stargazers": {
"totalCount": 5
}
},
{
"name": "FlockingBlackHole",
"stargazers": {
"totalCount": 5
}
},
{
"name": "webgpu-boids",
"stargazers": {
"totalCount": 4
}
},
{
"name": "test-githubblock",
"stargazers": {
"totalCount": 4
}
},
{
"name": "renderless-components",
"stargazers": {
"totalCount": 4
}
},
{
"name": "reakit",
"stargazers": {
"totalCount": 4
}
},
{
"name": "micro-open-graph",
"stargazers": {
"totalCount": 4
}
},
{
"name": "rest-and-graphql",
"stargazers": {
"totalCount": 4
}
},
{
"name": "abell-issues-blog",
"stargazers": {
"totalCount": 4
}
},
{
"name": "VanillaMVC",
"stargazers": {
"totalCount": 4
}
},
{
"name": "covid19-sim",
"stargazers": {
"totalCount": 4
}
},
{
"name": "YoutubeDownloader",
"stargazers": {
"totalCount": 4
}
},
{
"name": "verlet.js",
"stargazers": {
"totalCount": 4
}
},
{
"name": "test-github-ts",
"stargazers": {
"totalCount": 3
}
},
{
"name": "tw-dynamic-interpolation",
"stargazers": {
"totalCount": 3
}
},
{
"name": "ts-string-interpolator",
"stargazers": {
"totalCount": 3
}
},
{
"name": "AnuReact",
"stargazers": {
"totalCount": 3
}
},
{
"name": "CanvasBalls",
"stargazers": {
"totalCount": 3
}
},
{
"name": "circleci-test",
"stargazers": {
"totalCount": 3
}
},
{
"name": "gatsby-starter-netlify-cms",
"stargazers": {
"totalCount": 3
}
},
{
"name": "WebGL.js",
"stargazers": {
"totalCount": 3
}
},
{
"name": "test-fork-workflow",
"stargazers": {
"totalCount": 2
}
},
{
"name": "test-codesandbox-projects",
"stargazers": {
"totalCount": 2
}
},
{
"name": "codesandbox-template-astro",
"stargazers": {
"totalCount": 2
}
},
{
"name": "apollo",
"stargazers": {
"totalCount": 2
}
},
{
"name": "api-docs",
"stargazers": {
"totalCount": 2
}
},
{
"name": "Buddies-movie",
"stargazers": {
"totalCount": 2
}
},
{
"name": "2k76",
"stargazers": {
"totalCount": 2
}
},
{
"name": "gatsby-remark-embedder",
"stargazers": {
"totalCount": 2
}
},
{
"name": "weird-hello-worlds",
"stargazers": {
"totalCount": 2
}
},
{
"name": "rn-tabview-test-repro",
"stargazers": {
"totalCount": 1
}
},
{
"name": "tailwindcss-jit",
"stargazers": {
"totalCount": 1
}
},
{
"name": "DefinitelyTyped",
"stargazers": {
"totalCount": 1
}
},
{
"name": "server-components-demo",
"stargazers": {
"totalCount": 1
}
},
{
"name": "volant-vo",
"stargazers": {
"totalCount": 1
}
},
{
"name": "react-spectrum",
"stargazers": {
"totalCount": 1
}
},
{
"name": "webhooks-test",
"stargazers": {
"totalCount": 1
}
},
{
"name": "react-showdown",
"stargazers": {
"totalCount": 1
}
},
{
"name": "transcode",
"stargazers": {
"totalCount": 1
}
},
{
"name": "awake-yet-action",
"stargazers": {
"totalCount": 1
}
},
{
"name": "bradgarropy.com",
"stargazers": {
"totalCount": 1
}
},
{
"name": "gatsby-plugin-social-banners",
"stargazers": {
"totalCount": 1
}
},
{
"name": "Tour360",
"stargazers": {
"totalCount": 1
}
},
{
"name": "blizard.js",
"stargazers": {
"totalCount": 1
}
},
{
"name": "keep-a-changelog",
"stargazers": {
"totalCount": 1
}
},
{
"name": "vulnerabilities",
"stargazers": {
"totalCount": 1
}
},
{
"name": "harleydavidson",
"stargazers": {
"totalCount": 1
}
},
{
"name": "awesome-webgl",
"stargazers": {
"totalCount": 1
}
},
{
"name": "NewtonsInterpolation",
"stargazers": {
"totalCount": 1
}
},
{
"name": "now-github-starter",
"stargazers": {
"totalCount": 1
}
},
{
"name": "simplerockets",
"stargazers": {
"totalCount": 1
}
}
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "Y3Vyc29yOnYyOpIBzgh5q48="
}
}
}
}
}
-8
View File
@@ -1,8 +0,0 @@
export const CardType = {
STATS: "stats",
TOP_LANGS: "top-langs",
PIN: "pin",
GIST: "gist",
WAKATIME: "wakatime",
} as const;
export type CardType = (typeof CardType)[keyof typeof CardType];
-7
View File
@@ -1,10 +1,3 @@
declare module "*.css";
declare module "*.png" {
const img: string;
export default img;
}
// This package doesn't have a @types counter part
declare module "save-svg-as-png" {
const saveSvgAsPng: (
-59
View File
@@ -1,59 +0,0 @@
import type { JSX } from "react";
import { FaGithub as GithubIcon } from "react-icons/fa";
import appIcon from "../../assets/appLogo64.png";
import { ProgressBar } from "../../components/Home/Progress";
import { STAGE_LABELS } from "../../models/Stage";
import type { StageIndex } from "../../models/Stage";
interface HeaderProps {
currStageIndex: StageIndex;
onStageIndexChange: (stageIndex: StageIndex) => void;
}
const items = STAGE_LABELS.map((it) => it.shortTitle);
export function Header({
currStageIndex,
onStageIndexChange,
}: HeaderProps): JSX.Element {
return (
<>
<div className="text-gray-100 bg-gray-800 shadow-md z-50">
<div className="px-5 py-2 flex flex-wrap">
{/* Logo */}
<a
href="/"
className="flex items-center title-font font-medium text-gray-50 mb-0 md:mr-8"
>
<img src={appIcon} alt="logo" className="w-6 h-6" />
<span className="ml-2 text-xl">GitHub Stats Extended</span>
</a>
{/* Star on GitHub */}
<div className="flex ml-auto items-center text-base justify-center">
<a
href="https://github.com/stats-organization/github-stats-extended"
target="_blank"
rel="noopener noreferrer"
>
<button
type="button"
className="rounded-[0.25rem] shadow bg-gray-200 hover:bg-gray-300 text-black px-3 py-1 flex items-center"
>
Star on
<GithubIcon className="ml-1.5 w-5 h-5" />
</button>
</a>
</div>
</div>
</div>
<ProgressBar
items={items}
currItemIndex={currStageIndex}
onItemClick={(itemIndex) => {
onStageIndexChange(itemIndex as StageIndex);
}}
/>
</>
);
}
-397
View File
@@ -1,397 +0,0 @@
import axios from "axios";
import { useEffect, useMemo, useRef, useState } from "react";
import type { JSX } from "react";
import { useDispatch } from "react-redux";
import { BounceLoader } from "react-spinners";
import { v4 as uuidv4 } from "uuid";
import { authenticate } from "../../api/user";
import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../../components/Home/LanguagesLayoutSection";
import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../../components/Home/StatsRankSection";
import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../../components/Home/WakatimeLayoutSection";
import {
DEMO_GIST,
DEMO_REPO,
DEMO_USER,
DEMO_WAKATIME_USER,
HOST,
} from "../../constants";
import { CardType } from "../../models/CardType";
import { STAGE_LABELS } from "../../models/Stage";
import type { StageIndex } from "../../models/Stage";
import {
useIsAuthenticated,
usePrivateAccess,
useUserId,
} from "../../redux/selectors/userSelectors";
import { login } from "../../redux/slices/user";
import { getFullSuffix } from "./getFullSuffix";
import { CustomizeStage } from "./stages/Customize";
import { DisplayStage } from "./stages/Display";
import { LoginStage } from "./stages/Login/Login";
import { SelectCardStage } from "./stages/SelectCard";
import { ThemeStage } from "./stages/Theme";
interface HomeScreenProps {
stage: StageIndex;
setStage: (stageIndex: StageIndex) => void;
}
export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
const [isLoading, setIsLoading] = useState(false);
const userId = useUserId(DEMO_USER);
const privateAccess = usePrivateAccess();
const isAuthenticated = useIsAuthenticated();
const dispatch = useDispatch();
// for stage two
const [selectedUserId, setSelectedUserId] = useState(userId);
const [repo, setRepo] = useState(DEMO_REPO);
const [gist, setGist] = useState(DEMO_GIST);
const [wakatimeUser, setWakatimeUser] = useState(DEMO_WAKATIME_USER);
const [selectedCard, setSelectedCard] = useState<CardType>(CardType.STATS);
useEffect(() => {
setSelectedUserId(userId);
}, [userId]);
// for stage three
const [selectedStatsRank, setSelectedStatsRank] =
useState(STATS_DEFAULT_RANK);
const [selectedLanguagesLayout, setSelectedLanguagesLayout] = useState(
LANGUAGES_DEFAULT_LAYOUT,
);
const [selectedWakatimeLayout, setSelectedWakatimeLayout] = useState(
WAKATIME_DEFAULT_LAYOUT,
);
const [showTitle, setShowTitle] = useState(true);
const [showOwner, setShowOwner] = useState(false);
const [descriptionLines, setDescriptionLines] = useState<
number | undefined
>();
const [customTitle, setCustomTitle] = useState("");
const [langsCount, setLangsCount] = useState<number | undefined>();
const [hideValues, setHideValues] = useState(false);
const [showAllStats, setShowAllStats] = useState(false);
const [showIcons, setShowIcons] = useState(false);
const [includeAllCommits, setIncludeAllCommits] = useState(true);
const [enableAnimations, setEnableAnimations] = useState(true);
const [usePercent, setUsePercent] = useState(false);
const [theme, setTheme] = useState("default");
const handleCardTypeChange = (cardType: CardType) => {
if (cardType === CardType.TOP_LANGS) {
setLangsCount(4);
setSelectedWakatimeLayout(WAKATIME_DEFAULT_LAYOUT);
} else if (cardType === CardType.WAKATIME) {
setLangsCount(6);
setSelectedLanguagesLayout(LANGUAGES_DEFAULT_LAYOUT);
}
if (theme === "default" || theme === "default_repocard") {
if (cardType === CardType.PIN || cardType === CardType.GIST) {
setTheme("default_repocard");
} else {
setTheme("default");
}
}
setSelectedCard(cardType);
// Go to the next stage
setStage(2);
};
const fullSuffix = getFullSuffix({
userId,
selectedCard,
selectedUserId,
repo,
gist,
wakatimeUser,
selectedStatsRank,
selectedLanguagesLayout,
selectedWakatimeLayout,
showTitle,
showOwner,
descriptionLines,
customTitle,
langsCount,
hideValues,
showAllStats,
showIcons,
includeAllCommits,
enableAnimations,
usePercent,
});
// for stage four
let themeSuffix = fullSuffix;
if (
!(
(theme === "default" &&
[CardType.STATS, CardType.TOP_LANGS, CardType.WAKATIME].includes(
selectedCard as never,
)) ||
(theme === "default_repocard" &&
[CardType.PIN, CardType.GIST].includes(selectedCard as never))
)
) {
themeSuffix += `&theme=${theme}`;
}
// for stage five
const [gistUrl, setGistUrl] = useState("");
const guestHint = useMemo(() => {
switch (selectedCard) {
case CardType.STATS:
case CardType.TOP_LANGS:
return `username "${DEMO_USER}"`;
case CardType.PIN:
return `repo "${DEMO_REPO}"`;
case CardType.GIST:
return `Gist ID "${DEMO_GIST}"`;
case CardType.WAKATIME:
return `WakaTime username "${DEMO_WAKATIME_USER}"`;
default:
selectedCard satisfies never;
return "";
}
}, [selectedCard]);
useEffect(() => {
async function fetchGistURL() {
try {
const fullUrl = `https://api.github.com/gists/${gist}`;
const result = await axios.get<{ html_url: string }>(fullUrl);
setGistUrl(result.data.html_url);
} catch (error) {
console.error(error);
}
}
void fetchGistURL();
}, [gist]);
const contentSectionRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
// scroll to top of content section if we're scrolled down
if (contentSectionRef.current) {
const rect = contentSectionRef.current.getBoundingClientRect();
if (rect.top < 0) {
contentSectionRef.current.scrollIntoView();
// additional offset to account for sticky progress bar
window.scrollBy({ top: -80 });
}
}
}, [stage]);
useEffect(() => {
async function redirectCode() {
// After requesting GitHub access, GitHub redirects back to your app with a code parameter
const url = window.location.href;
// If GitHub API returns the code parameter
if (url.includes("code=")) {
const tempPrivateAccess = url.includes("private");
const newUrl = url.split("code=", 2) as [string, string];
const redirect = `${url.split(HOST)[0] as string}${HOST}/frontend`;
window.history.pushState({}, "", redirect);
setIsLoading(true);
const userKey = uuidv4();
const newUserId = await authenticate(
newUrl[1],
tempPrivateAccess,
userKey,
);
dispatch(login({ userId: newUserId, userKey }));
setIsLoading(false);
}
}
void redirectCode();
}, [dispatch]);
if (isLoading) {
return (
<div className="h-full py-8 flex justify-center items-center">
<BounceLoader color="#3B82F6" />
</div>
);
}
return (
<div ref={contentSectionRef} className="h-full px-2 lg:px-8 text-gray-600">
<div className="flex flex-col">
<div className="m-4 rounded-sm">
<div className="lg:p-4">
<h1 className="text-2xl text-gray-600 font-semibold">
{STAGE_LABELS[stage].title}
</h1>
<div>
{stage === 0 && isAuthenticated ? (
<div>
<p>
You are logged in as{" "}
<a
href={`https://github.com/${userId}`}
target="_blank"
className="text-blue-500 hover:underline font-semibold"
>
{userId}
</a>
.
</p>
<p>
{privateAccess ? (
<>
You granted GitHub-Stats-Extended access to both your{" "}
<b>public and private</b> contributions.
</>
) : (
<>
You granted GitHub-Stats-Extended access to your{" "}
<b>public</b> contributions.
</>
)}
</p>
</div>
) : (
[
"",
"You will be able to customize your card in future steps.",
"",
"",
"Display the finished card on GitHub, Twitter/X, LinkedIn, or anywhere else!",
][stage]
)}
</div>
</div>
{stage === 0 && (
<LoginStage
onContinueAsGuest={() => {
setStage(1);
}}
/>
)}
{stage === 1 && (
<SelectCardStage
selectedCardType={selectedCard}
onCardTypeChange={handleCardTypeChange}
/>
)}
{stage === 2 && (
<CustomizeStage
selectedCard={selectedCard}
selectedStatsRank={selectedStatsRank}
setSelectedStatsRank={setSelectedStatsRank}
selectedLanguagesLayout={selectedLanguagesLayout}
setSelectedLanguagesLayout={setSelectedLanguagesLayout}
selectedWakatimeLayout={selectedWakatimeLayout}
setSelectedWakatimeLayout={setSelectedWakatimeLayout}
showTitle={showTitle}
setShowTitle={setShowTitle}
showOwner={showOwner}
setShowOwner={setShowOwner}
descriptionLines={descriptionLines}
setDescriptionLines={setDescriptionLines}
customTitle={customTitle}
setCustomTitle={setCustomTitle}
langsCount={langsCount}
setLangsCount={setLangsCount}
hideValues={hideValues}
setHideValues={setHideValues}
showAllStats={showAllStats}
setShowAllStats={setShowAllStats}
showIcons={showIcons}
setShowIcons={setShowIcons}
includeAllCommits={includeAllCommits}
setIncludeAllCommits={setIncludeAllCommits}
enableAnimations={enableAnimations}
setEnableAnimations={setEnableAnimations}
selectedUserId={selectedUserId}
setSelectedUserId={setSelectedUserId}
repo={repo}
setRepo={setRepo}
gist={gist}
setGist={setGist}
wakatimeUser={wakatimeUser}
setWakatimeUser={setWakatimeUser}
usePercent={usePercent}
setUsePercent={setUsePercent}
fullSuffix={fullSuffix}
setStage={setStage}
/>
)}
{stage === 3 && (
<ThemeStage
fullSuffix={fullSuffix}
theme={theme}
onThemeChange={(theme) => {
setTheme(theme);
setStage(4);
}}
/>
)}
{stage === 4 && (
<DisplayStage
filename={(() => {
switch (selectedCard) {
case CardType.STATS:
case CardType.TOP_LANGS:
return `${selectedUserId}_card`;
case CardType.PIN:
return `${repo}_card`;
case CardType.GIST:
return `gist_card`;
case CardType.WAKATIME:
return `${wakatimeUser}_card`;
default:
selectedCard satisfies never;
return "";
}
})()}
link={(() => {
switch (selectedCard) {
case CardType.STATS:
case CardType.TOP_LANGS:
return `https://${HOST}/api${themeSuffix}`;
case CardType.PIN: {
let myRepo = repo;
if (!myRepo.includes("/")) {
myRepo = `${userId}/${myRepo}`;
}
return `https://github.com/${myRepo}`;
}
case CardType.GIST:
return gistUrl;
case CardType.WAKATIME:
return `https://wakatime.com/@${wakatimeUser}`;
default:
selectedCard satisfies never;
return "";
}
})()}
themeSuffix={themeSuffix}
guestHint={
isAuthenticated
? null
: `Replace the sample ${guestHint} with your own after copying your Markdown or URL!`
}
/>
)}
</div>
</div>
</div>
);
}
@@ -1,173 +0,0 @@
import type { SelectOption } from "../../components/Generic/Select";
import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../../components/Home/LanguagesLayoutSection";
import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../../components/Home/StatsRankSection";
import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../../components/Home/WakatimeLayoutSection";
import { CardType } from "../../models/CardType";
interface Options {
userId: string;
selectedUserId: string;
selectedCard: CardType;
repo: string;
gist: string;
wakatimeUser: string;
selectedStatsRank: SelectOption;
selectedLanguagesLayout: SelectOption;
selectedWakatimeLayout: SelectOption;
showTitle: boolean;
showOwner: boolean;
descriptionLines: number | undefined;
customTitle: string;
langsCount: number | undefined;
hideValues: boolean;
showAllStats: boolean;
showIcons: boolean;
includeAllCommits: boolean;
enableAnimations: boolean;
usePercent: boolean;
}
export function getFullSuffix({
userId,
selectedCard,
selectedUserId,
repo,
gist,
wakatimeUser,
selectedStatsRank,
selectedLanguagesLayout,
selectedWakatimeLayout,
showTitle,
showOwner,
descriptionLines,
customTitle,
langsCount,
hideValues,
showAllStats,
showIcons,
includeAllCommits,
enableAnimations,
usePercent,
}: Options): string {
let fullSuffix = `${selectedCard === CardType.STATS ? "" : "/" + selectedCard}?`;
switch (selectedCard) {
case CardType.STATS:
case CardType.TOP_LANGS:
fullSuffix += `username=${selectedUserId}`;
break;
case CardType.PIN:
/**
* We should use the name of the logged-in user, not the value entered in the
* username field in step 3.
*
* This input is not shown when the PIN card type is selected,
* but it may still contain a different value if the user previously chose another card type.
*
* For example,
* 1. the user could select the STATS card
* 2. enter a username
* 3. then go back and switch to the PIN card, leaving the old value behind.
*
* @see https://github.com/stats-organization/github-stats-extended/pull/73#discussion_r2792177515
*/
fullSuffix += `username=${userId}&repo=${repo}`;
break;
case CardType.GIST:
fullSuffix += `id=${gist}`;
break;
case CardType.WAKATIME:
fullSuffix += `username=${wakatimeUser}`;
break;
default:
selectedCard satisfies never;
}
if (
selectedStatsRank !== STATS_DEFAULT_RANK &&
selectedCard === CardType.STATS
) {
fullSuffix += `&rank_icon=${selectedStatsRank.value}`;
}
if (
selectedLanguagesLayout !== LANGUAGES_DEFAULT_LAYOUT &&
selectedCard === CardType.TOP_LANGS
) {
fullSuffix += `&layout=${selectedLanguagesLayout.value}`;
}
if (
selectedWakatimeLayout !== WAKATIME_DEFAULT_LAYOUT &&
selectedCard === CardType.WAKATIME
) {
fullSuffix += `&layout=${selectedWakatimeLayout.value}`;
}
if (
!showTitle &&
(selectedCard === CardType.STATS ||
selectedCard === CardType.TOP_LANGS ||
selectedCard === CardType.WAKATIME)
) {
fullSuffix += "&hide_title=true";
}
if (
showOwner &&
(selectedCard === CardType.PIN || selectedCard === CardType.GIST)
) {
fullSuffix += "&show_owner=true";
}
if (descriptionLines && selectedCard === CardType.PIN) {
fullSuffix += `&description_lines_count=${descriptionLines}`;
}
if (
customTitle &&
(selectedCard === CardType.STATS || selectedCard === CardType.WAKATIME)
) {
const encodedTitle = encodeURIComponent(customTitle);
fullSuffix += `&custom_title=${encodedTitle}`;
}
if (
langsCount &&
(selectedCard === CardType.TOP_LANGS || selectedCard === CardType.WAKATIME)
) {
fullSuffix += `&langs_count=${langsCount}`;
}
if (hideValues && selectedCard === CardType.TOP_LANGS) {
fullSuffix += `&hide_values=true`;
}
if (showAllStats && selectedCard === CardType.STATS) {
fullSuffix += `&show=reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_commented,prs_reviewed,issues_commented`;
}
if (showIcons && selectedCard === CardType.STATS) {
fullSuffix += `&show_icons=true`;
}
if (includeAllCommits && selectedCard === CardType.STATS) {
fullSuffix += `&include_all_commits=true`;
}
if (
!enableAnimations &&
(selectedCard === CardType.STATS ||
selectedCard === CardType.TOP_LANGS ||
selectedCard === CardType.WAKATIME)
) {
fullSuffix += `&disable_animations=${!enableAnimations}`;
}
if (usePercent && selectedCard === CardType.WAKATIME) {
fullSuffix += `&display_format=percent`;
}
return fullSuffix;
}
@@ -1,61 +0,0 @@
import type { JSX } from "react";
import { CardImage } from "../../../../components/Card/CardImage";
import {
DEMO_GIST,
DEMO_REPO,
DEMO_USER,
DEMO_WAKATIME_USER,
} from "../../../../constants";
const cards: Array<{ demoImageSrc: string }> = [
{
demoImageSrc: `/pin?repo=${DEMO_REPO}&disable_animations=true`,
},
{
demoImageSrc: `/top-langs?username=${DEMO_USER}&langs_count=4&disable_animations=true`,
},
{
demoImageSrc: `?username=${DEMO_USER}&include_all_commits=true&disable_animations=true`,
},
{
demoImageSrc: `/wakatime?username=${DEMO_WAKATIME_USER}&langs_count=6&card_width=450&disable_animations=true`,
},
{
demoImageSrc: `/gist?id=${DEMO_GIST}&disable_animations=true`,
},
];
function getCardXPosition(cardIndex: number): number {
const radius = 60;
const centerX = 70;
const startAngle = Math.PI * 0.75; // 135 degrees
const endAngle = Math.PI * 1.25; // 225 degrees
const angle =
startAngle + (endAngle - startAngle) * (cardIndex / (cards.length - 1));
const x = centerX + radius * Math.cos(angle);
return x;
}
export function LoginBoxDemoCards(): JSX.Element {
return (
<div className="w-full h-full lg:w-2/5 flex lg:flex-col lg:p-8 relative overflow-hidden">
<div className="relative w-full h-full">
{cards.map((card, index) => (
<div
key={card.demoImageSrc}
style={{
left: `${getCardXPosition(index)}%`,
position: "relative",
zoom: "0.5",
marginBottom: "1%",
}}
>
<CardImage imageSrc={card.demoImageSrc} compact={false} stage={0} />
</div>
))}
</div>
</div>
);
}
@@ -1,65 +0,0 @@
import { themes } from "@stats-organization/github-readme-stats-core";
import type { JSX } from "react";
import { Card } from "../../../components/Card/Card";
const excludedThemes = [
"merko",
"blue-green",
"gotham",
"blueberry",
"outrun",
"holi",
];
const themeList = Object.keys(themes).filter(
(myTheme) => !excludedThemes.includes(myTheme),
);
interface ThemeStageProps {
fullSuffix: string;
theme: string;
onThemeChange: (theme: string) => void;
}
export function ThemeStage({
theme,
fullSuffix,
onThemeChange,
}: ThemeStageProps): JSX.Element {
return (
<>
<div className="flex flex-wrap">
{themeList.map((myTheme) => (
<button
className="p-2 lg:p-4"
key={myTheme}
type="button"
onClick={() => {
onThemeChange(myTheme);
}}
>
<Card
title={myTheme}
description=""
imageSrc={`${fullSuffix}&theme=${myTheme}`}
selected={theme === myTheme}
stage={3}
/>
</button>
))}
</div>
<div className="pl-10 pr-10">
For more theme options check the{" "}
<a
href="https://github.com/stats-organization/github-stats-extended/blob/master/docs/advanced_documentation.md#themes"
target="_blank"
className="underline text-blue-900"
>
customization documentation
</a>{" "}
after you copied your card URL in step 5.
</div>
</>
);
}
+39
View File
@@ -0,0 +1,39 @@
---
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
import { Wizard } from "../wizard/Wizard";
// Tailwind and daisyUI, loaded only here: the docs pages are styled by Starlight.
import "../index.css";
---
{/* `splash` drops the sidebar and table of contents, leaving the wizard the full width. */}
<StarlightPage
frontmatter={{
title: "Card wizard",
description: "Build your GitHub stats card and copy its markdown.",
template: "splash",
}}
>
{/* `not-content` opts out of the prose spacing Starlight applies to markdown. */}
<div class="not-content wizard bg-base-100"><Wizard client:only="react" /></div>
</StarlightPage>
{/* Only this page loads these rules, so the docs keep Starlight's reading column. */}
<style is:global>
/* Starlight caps the content at a reading width; the wizard uses the whole viewport. */
:root {
--sl-main-pad: 0rem;
}
main .sl-container {
max-width: none;
}
main .content-panel {
padding: 0;
}
/* Every wizard stage heads its own section, so the page title would be a second `h1`. */
.content-panel:has(> .sl-container > #_top) {
display: none;
}
</style>
+37
View File
@@ -0,0 +1,37 @@
import { clsx } from "clsx";
import type { JSX, ReactNode } from "react";
import { FaExternalLinkAlt as ExternalIcon } from "react-icons/fa";
interface LinkExternalProps {
href: string;
children: ReactNode;
className?: string;
/**
* Set to `false` for links that already carry their own mark, such as the
* branded GitHub button, where a second icon would only add noise.
*/
showIcon?: boolean;
}
/**
* A link that opens in a new tab, with the `rel` that `target="_blank"`
* requires, and a trailing icon so the new tab is announced before the click.
*/
export function LinkExternal({
href,
children,
className,
showIcon = true,
}: LinkExternalProps): JSX.Element {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className={clsx("inline-flex items-center gap-1", className)}
>
{children}
{showIcon && <ExternalIcon className="w-3 h-3 shrink-0" aria-hidden />}
</a>
);
}
+8
View File
@@ -0,0 +1,8 @@
/*
* Tailwind's utilities must outrank Starlight's reset, whose `margin: 0` otherwise beats every margin utility in the wizard.
* Preflight stays ahead so the chrome keeps its styling.
* Imported by both entry sheets:
* only the first `@layer` wins, and dev loads them in the opposite order to a build.
*/
@layer theme, base, components, starlight.base, starlight.reset, starlight.core,
starlight.content, starlight.components, starlight.utils, utilities;
@@ -0,0 +1,44 @@
@import "./layer-order.css";
/*
* Gives the docs the wizard's accent and GitHub's dark background, so the two
* halves read as one site.
*/
:root {
--app-primary: #2f80ed;
--sl-font: "Segoe UI", Ubuntu, Sans-Serif;
--sl-color-accent: var(--app-primary);
--sl-color-accent-low: color-mix(in oklab, var(--app-primary), #000 45%);
--sl-color-accent-high: color-mix(in oklab, var(--app-primary), #fff 45%);
--sl-color-text-accent: var(--app-primary);
}
:root[data-theme="dark"] {
--sl-color-bg: #0d1117;
--sl-color-bg-sidebar: #0d1117;
}
/* Prose styles render images as blocks, so the row is explicit. */
.card-row {
display: flex;
flex-wrap: nowrap;
gap: 0.5rem;
align-items: center;
}
.card-row a {
flex: 0 1 auto;
min-width: 0;
}
.card-row img {
max-width: 100%;
height: auto;
}
/* `prefers-color-scheme` cannot see the site theme, so pick the card by `data-theme`. */
:root[data-theme="dark"] .card-preview-light,
:root[data-theme="light"] .card-preview-dark {
display: none;
}
+285
View File
@@ -0,0 +1,285 @@
import type { ThemeName } from "@stats-organization/github-readme-stats-core";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { JSX } from "react";
import { useDispatch } from "react-redux";
import { BounceLoader } from "react-spinners";
import { v4 as uuidv4 } from "uuid";
import { DEMO_USER } from "../../constants";
import {
useIsAuthenticated,
usePrivateAccess,
useUserId,
} from "../../redux/selectors/userSelectors";
import { login } from "../../redux/slices/user";
import { LinkExternal } from "../../shared/LinkExternal";
import { authenticate } from "../api/user";
import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../components/Home/LanguagesLayoutSection";
import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../components/Home/WakatimeLayoutSection";
import {
CATEGORY_BY_CARD_TYPE,
CardCategory,
CardType,
} from "../models/CardType";
import { STAGE_LABELS } from "../models/Stage";
import type { StageIndex } from "../models/Stage";
import { useIsDarkTheme } from "../useIsDarkTheme";
import { buildCardUrl } from "./buildCardUrl";
import { getDefaultCardOptions } from "./cardOptions";
import type { CardOptions } from "./cardOptions";
import { CustomizeStage } from "./stages/Customize";
import { DisplayStage } from "./stages/Display";
import { LoginStage } from "./stages/Login/Login";
import { SelectCardStage } from "./stages/SelectCard";
import { ThemeStage } from "./stages/Theme";
import { useCardDescriptor } from "./useCardDescriptor";
interface HomeScreenProps {
stage: StageIndex;
setStage: (stageIndex: StageIndex) => void;
}
function getDefaultTheme(isDark: boolean, category: CardCategory): ThemeName {
if (category === CardCategory.REPO) {
return isDark ? "dark_github_repocard" : "light_github_repocard";
}
return isDark ? "dark_github" : "light_github";
}
export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
const [isLoading, setIsLoading] = useState(false);
const userId = useUserId(DEMO_USER);
const privateAccess = usePrivateAccess();
const isAuthenticated = useIsAuthenticated();
const dispatch = useDispatch();
const [selectedCard, setSelectedCard] = useState<CardType>(CardType.STATS);
// for stages two and three
const [cardOptions, setCardOptions] = useState(() =>
getDefaultCardOptions(userId),
);
const setCardOption = useCallback<
<K extends keyof CardOptions>(key: K, value: CardOptions[K]) => void
>((key, value) => {
setCardOptions((prev) => ({ ...prev, [key]: value }));
}, []);
// Reset the selected user to the resolved account id when it changes,
// adjusting state during render rather than in an effect:
// https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
const [prevUserId, setPrevUserId] = useState(userId);
if (userId !== prevUserId) {
setPrevUserId(userId);
setCardOptions((prev) => ({ ...prev, selectedUserId: userId }));
}
const isDark = useIsDarkTheme();
const cardCategory = CATEGORY_BY_CARD_TYPE[selectedCard];
const [theme, setTheme] = useState(() =>
getDefaultTheme(isDark, cardCategory),
);
const handleCardTypeChange = (cardType: CardType) => {
if (cardType === CardType.TOP_LANGS) {
setCardOptions((prev) => ({
...prev,
langsCount: 4,
selectedWakatimeLayout: WAKATIME_DEFAULT_LAYOUT,
}));
} else if (cardType === CardType.WAKATIME) {
setCardOptions((prev) => ({
...prev,
langsCount: 6,
selectedLanguagesLayout: LANGUAGES_DEFAULT_LAYOUT,
}));
}
if (theme === getDefaultTheme(isDark, cardCategory)) {
setTheme(getDefaultTheme(isDark, CATEGORY_BY_CARD_TYPE[cardType]));
}
setSelectedCard(cardType);
// Go to the next stage
setStage(2);
};
// Memoized so the builder keeps a stable reference across renders when its
// inputs are unchanged. Card components consume it by value (they derive a URL
// string), but memoizing keeps it safe to pass as a prop if any of them are
// ever wrapped in React.memo.
const cardBuilder = useMemo(
() => buildCardUrl(userId, selectedCard, cardOptions),
[userId, selectedCard, cardOptions],
);
// Preview builder for the customize stage, dark-themed to match the surroundings.
const customizeCardBuilder = useMemo(
() => cardBuilder.theme(getDefaultTheme(isDark, cardCategory)),
[cardBuilder, isDark, cardCategory],
);
// for stage four
const themeBuilder = useMemo(
() => cardBuilder.theme(theme),
[cardBuilder, theme],
);
// for stage five
const cardDescriptor = useCardDescriptor({
selectedCard,
themeBuilder,
repo: cardOptions.repo,
userId,
wakatimeUser: cardOptions.wakatimeUser,
gist: cardOptions.gist,
});
const contentSectionRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
// scroll to top of content section if we're scrolled down
if (contentSectionRef.current) {
const rect = contentSectionRef.current.getBoundingClientRect();
if (rect.top < 0) {
contentSectionRef.current.scrollIntoView();
// additional offset to account for sticky progress bar
window.scrollBy({ top: -80 });
}
}
}, [stage]);
useEffect(() => {
// After requesting GitHub access, GitHub redirects back to the app with a
// code parameter appended to the redirect URI (which carries mode=private|public).
const url = new URL(window.location.href);
const code = url.searchParams.get("code");
if (code === null) {
return;
}
const tempPrivateAccess = url.searchParams.get("mode") === "private";
window.history.pushState({}, "", `${url.origin}/frontend`);
async function exchangeCode(code: string) {
setIsLoading(true);
try {
const userKey = uuidv4();
const newUserId = await authenticate(code, tempPrivateAccess, userKey);
dispatch(login({ userId: newUserId, userKey }));
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
}
void exchangeCode(code);
}, [dispatch]);
if (isLoading) {
return (
<div className="h-full py-8 flex justify-center items-center">
<BounceLoader color="#3B82F6" />
</div>
);
}
return (
<div
ref={contentSectionRef}
className="h-full px-2 lg:px-8 text-base-content/70"
>
<div className="flex flex-col">
<div className="m-4 rounded-sm">
<div className="lg:p-4">
<h1 className="text-2xl text-base-content/70 font-semibold">
{STAGE_LABELS[stage].title}
</h1>
<div>
{stage === 0 && isAuthenticated ? (
<div>
<p>
You are logged in as{" "}
<LinkExternal
href={`https://github.com/${userId}`}
className="text-primary hover:underline font-semibold"
>
{userId}
</LinkExternal>
.
</p>
<p>
{privateAccess ? (
<>
You granted GitHub-Stats-Extended access to both your{" "}
<b>public and private</b> contributions.
</>
) : (
<>
You granted GitHub-Stats-Extended access to your{" "}
<b>public</b> contributions.
</>
)}
</p>
</div>
) : (
STAGE_LABELS[stage].description
)}
</div>
</div>
{stage === 0 && (
<LoginStage
onContinueAsGuest={() => {
setStage(1);
}}
/>
)}
{stage === 1 && (
<SelectCardStage
selectedCardType={selectedCard}
onCardTypeChange={handleCardTypeChange}
/>
)}
{stage === 2 && (
<CustomizeStage
selectedCard={selectedCard}
options={cardOptions}
onOptionChange={setCardOption}
card={customizeCardBuilder}
setStage={setStage}
/>
)}
{stage === 3 && (
<ThemeStage
card={cardBuilder}
theme={theme}
category={cardCategory}
onThemeChange={(theme) => {
setTheme(theme);
setStage(4);
}}
/>
)}
{stage === 4 && (
<DisplayStage
filename={cardBuilder.filename()}
link={cardDescriptor.link}
theme={theme}
card={themeBuilder}
guestHint={
isAuthenticated
? null
: `Replace the sample ${cardDescriptor.guestHint} with your own after copying your Markdown or URL!`
}
/>
)}
</div>
</div>
</div>
);
}
@@ -1,16 +1,19 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../../components/Home/LanguagesLayoutSection";
import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../../components/Home/StatsRankSection";
import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../../components/Home/WakatimeLayoutSection";
import { CardType } from "../../models/CardType";
import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../components/Home/LanguagesLayoutSection";
import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../components/Home/StatsRankSection";
import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../components/Home/WakatimeLayoutSection";
import { CardType } from "../models/CardType";
import { getFullSuffix } from "./getFullSuffix";
import { buildCardUrl } from "./buildCardUrl";
import type { CardOptions } from "./cardOptions";
const baseOptions = {
selectedCard: CardType.STATS,
const USER_ID = "john-github";
// Built inline instead of via `getDefaultCardOptions`, which imports
// `constants.ts` and needs a `window` global these node-based tests lack.
const baseOptions: CardOptions = {
selectedUserId: "john",
userId: "john-github",
repo: "repo1",
gist: "gist1",
wakatimeUser: "wakaUser",
@@ -30,15 +33,15 @@ const baseOptions = {
usePercent: false,
};
describe("getFullSuffix", () => {
describe("buildCardUrl", () => {
it("builds stats suffix with defaults", () => {
const result = getFullSuffix(baseOptions);
const result = buildCardUrl(USER_ID, CardType.STATS, baseOptions);
expect(result).toBe("?username=john");
expect(result.toString()).toBe("?username=john");
});
it("adds stats options", () => {
const result = getFullSuffix({
const result = buildCardUrl(USER_ID, CardType.STATS, {
...baseOptions,
showIcons: true,
includeAllCommits: true,
@@ -46,73 +49,69 @@ describe("getFullSuffix", () => {
showTitle: false,
});
expect(result).toBe(
expect(result.toString()).toBe(
"?username=john" +
"&hide_title=true" +
"&show=reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_commented,prs_reviewed,issues_commented" +
// commas are percent-encoded (%2C) now that params go through URLSearchParams
"&show=reviews%2Cdiscussions_started%2Cdiscussions_answered%2Cprs_merged%2Cprs_merged_percentage%2Cprs_commented%2Cprs_reviewed%2Cissues_commented%2Ccontributions" +
"&show_icons=true" +
"&include_all_commits=true",
);
});
it("builds top-langs suffix", () => {
const result = getFullSuffix({
const result = buildCardUrl(USER_ID, CardType.TOP_LANGS, {
...baseOptions,
selectedCard: CardType.TOP_LANGS,
langsCount: 5,
showTitle: false,
});
expect(result).toBe(
expect(result.toString()).toBe(
"/top-langs?username=john&hide_title=true&langs_count=5",
);
});
it("builds pin suffix using userId not selectedUserId", () => {
const result = getFullSuffix({
const result = buildCardUrl(USER_ID, CardType.PIN, {
...baseOptions,
selectedCard: CardType.PIN,
showOwner: true,
descriptionLines: 3,
});
expect(result).toBe(
expect(result.toString()).toBe(
"/pin?username=john-github&repo=repo1&show_owner=true&description_lines_count=3",
);
});
it("builds gist suffix", () => {
const result = getFullSuffix({
const result = buildCardUrl(USER_ID, CardType.GIST, {
...baseOptions,
selectedCard: CardType.GIST,
showOwner: true,
});
expect(result).toBe("/gist?id=gist1&show_owner=true");
expect(result.toString()).toBe("/gist?id=gist1&show_owner=true");
});
it("builds wakatime suffix with percent and custom title", () => {
const result = getFullSuffix({
const result = buildCardUrl(USER_ID, CardType.WAKATIME, {
...baseOptions,
selectedCard: CardType.WAKATIME,
wakatimeUser: "waka",
usePercent: true,
customTitle: "My Stats",
showTitle: false,
});
expect(result).toBe(
expect(result.toString()).toBe(
"/wakatime?username=waka&hide_title=true&custom_title=My%20Stats&display_format=percent",
);
});
it("adds non-default layouts", () => {
const result = getFullSuffix({
const result = buildCardUrl(USER_ID, CardType.TOP_LANGS, {
...baseOptions,
selectedCard: CardType.TOP_LANGS,
selectedLanguagesLayout: { id: 2, value: "compact", label: "Compact" },
});
expect(result).toBe("/top-langs?username=john&layout=compact");
expect(result.toString()).toBe("/top-langs?username=john&layout=compact");
});
});
@@ -0,0 +1,175 @@
import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../components/Home/LanguagesLayoutSection";
import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../components/Home/StatsRankSection";
import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../components/Home/WakatimeLayoutSection";
import { CardType } from "../models/CardType";
import { cardUrl } from "../models/CardUrl";
import type { CardUrlBuilder } from "../models/CardUrl";
import type { CardOptions } from "./cardOptions";
export function buildCardUrl(
userId: string,
selectedCard: CardType,
options: CardOptions,
): CardUrlBuilder {
const {
selectedUserId,
repo,
gist,
wakatimeUser,
selectedStatsRank,
selectedLanguagesLayout,
selectedWakatimeLayout,
showTitle,
showOwner,
descriptionLines,
customTitle,
langsCount,
hideValues,
showAllStats,
showIcons,
includeAllCommits,
enableAnimations,
usePercent,
} = options;
switch (selectedCard) {
case CardType.STATS: {
let url = cardUrl(CardType.STATS);
if (selectedUserId) {
url = url.username(selectedUserId);
}
if (selectedStatsRank !== STATS_DEFAULT_RANK) {
if (selectedStatsRank.value === "hide_rank") {
url = url.hideRank();
} else {
url = url.rankIcon(selectedStatsRank.value);
}
}
if (!showTitle) {
url = url.hideTitle();
}
if (customTitle) {
url = url.customTitle(customTitle);
}
if (showAllStats) {
url = url.show(
"reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_commented,prs_reviewed,issues_commented,contributions",
);
}
if (showIcons) {
url = url.showIcons();
}
if (includeAllCommits) {
url = url.includeAllCommits();
}
if (!enableAnimations) {
url = url.disableAnimations();
}
return url;
}
case CardType.TOP_LANGS: {
let url = cardUrl(CardType.TOP_LANGS);
if (selectedUserId) {
url = url.username(selectedUserId);
}
if (selectedLanguagesLayout !== LANGUAGES_DEFAULT_LAYOUT) {
if (selectedLanguagesLayout.value === "hide_progress") {
url = url.hideProgress();
} else {
url = url.layout(selectedLanguagesLayout.value);
}
}
if (!showTitle) {
url = url.hideTitle();
}
if (langsCount) {
url = url.langsCount(langsCount);
}
if (hideValues) {
url = url.hideValues();
}
if (!enableAnimations) {
url = url.disableAnimations();
}
return url;
}
case CardType.PIN: {
/**
* We should use the name of the logged-in user, not the value entered in the
* username field in step 3.
*
* This input is not shown when the PIN card type is selected,
* but it may still contain a different value if the user previously chose another card type.
*
* For example,
* 1. the user could select the STATS card
* 2. enter a username
* 3. then go back and switch to the PIN card, leaving the old value behind.
*
* @see https://github.com/stats-organization/github-stats-extended/pull/73#discussion_r2792177515
*/
let url = cardUrl(CardType.PIN);
if (userId) {
url = url.username(userId);
}
if (repo) {
url = url.repo(repo);
}
if (showOwner) {
url = url.showOwner();
}
if (descriptionLines) {
url = url.descriptionLines(descriptionLines);
}
return url;
}
case CardType.GIST: {
let url = cardUrl(CardType.GIST);
if (gist) {
url = url.gistId(gist);
}
if (showOwner) {
url = url.showOwner();
}
return url;
}
case CardType.WAKATIME: {
let url = cardUrl(CardType.WAKATIME);
if (wakatimeUser) {
url = url.username(wakatimeUser);
}
if (selectedWakatimeLayout !== WAKATIME_DEFAULT_LAYOUT) {
if (selectedWakatimeLayout.value === "hide_progress") {
url = url.hideProgress().cardWidth(315);
} else {
url = url.layout(selectedWakatimeLayout.value);
}
}
if (!showTitle) {
url = url.hideTitle();
}
if (customTitle) {
url = url.customTitle(customTitle);
}
if (langsCount) {
url = url.langsCount(langsCount);
}
if (!enableAnimations) {
url = url.disableAnimations();
}
if (usePercent) {
url = url.displayFormat("percent");
}
return url;
}
default:
selectedCard satisfies never;
throw new Error(`unknown card type: ${selectedCard as string}`);
}
}
@@ -0,0 +1,53 @@
import { DEMO_GIST, DEMO_REPO, DEMO_WAKATIME_USER } from "../../constants";
import type { SelectOption } from "../components/Generic/Select";
import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../components/Home/LanguagesLayoutSection";
import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../components/Home/StatsRankSection";
import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../components/Home/WakatimeLayoutSection";
/**
* All user-tunable card parameters collected during the customize stage.
* Held as a single state object in `HomeScreen` and updated one key at a time.
*/
export interface CardOptions {
selectedUserId: string;
repo: string;
gist: string;
wakatimeUser: string;
selectedStatsRank: SelectOption;
selectedLanguagesLayout: SelectOption;
selectedWakatimeLayout: SelectOption;
showTitle: boolean;
showOwner: boolean;
descriptionLines: number | undefined;
customTitle: string;
langsCount: number | undefined;
hideValues: boolean;
showAllStats: boolean;
showIcons: boolean;
includeAllCommits: boolean;
enableAnimations: boolean;
usePercent: boolean;
}
export function getDefaultCardOptions(userId: string): CardOptions {
return {
selectedUserId: userId,
repo: DEMO_REPO,
gist: DEMO_GIST,
wakatimeUser: DEMO_WAKATIME_USER,
selectedStatsRank: STATS_DEFAULT_RANK,
selectedLanguagesLayout: LANGUAGES_DEFAULT_LAYOUT,
selectedWakatimeLayout: WAKATIME_DEFAULT_LAYOUT,
showTitle: true,
showOwner: false,
descriptionLines: undefined,
customTitle: "",
langsCount: undefined,
hideValues: false,
showAllStats: false,
showIcons: false,
includeAllCommits: true,
enableAnimations: true,
usePercent: false,
};
}
@@ -1,115 +1,91 @@
import type { JSX, default as React } from "react";
import type { JSX } from "react";
import { CardImage } from "../../../components/Card/CardImage";
import type { SelectOption } from "../../../components/Generic/Select";
import { CheckboxSection } from "../../../components/Home/CheckboxSection";
import { LanguagesLayoutSection } from "../../../components/Home/LanguagesLayoutSection";
import { NumericSection } from "../../../components/Home/NumericSection";
import { StatsRankSection } from "../../../components/Home/StatsRankSection";
import { TextSection } from "../../../components/Home/TextSection";
import { WakatimeLayoutSection } from "../../../components/Home/WakatimeLayoutSection";
import {
DEMO_GIST,
DEMO_REPO,
DEMO_USER,
DEMO_WAKATIME_USER,
} from "../../../constants";
import { CardType } from "../../../models/CardType";
import type { StageIndex } from "../../../models/Stage";
import { useIsAuthenticated } from "../../../redux/selectors/userSelectors";
import { LinkExternal } from "../../../shared/LinkExternal";
import { CardImage } from "../../components/Card/CardImage";
import { CheckboxSection } from "../../components/Home/CheckboxSection";
import { LanguagesLayoutSection } from "../../components/Home/LanguagesLayoutSection";
import { NumericSection } from "../../components/Home/NumericSection";
import { StatsRankSection } from "../../components/Home/StatsRankSection";
import { TextSection } from "../../components/Home/TextSection";
import { WakatimeLayoutSection } from "../../components/Home/WakatimeLayoutSection";
import { docsUrl } from "../../docsUrl";
import {
CATEGORY_BY_CARD_TYPE,
CardCategory,
CardType,
} from "../../models/CardType";
import type { CardUrlBuilder } from "../../models/CardUrl";
import type { StageIndex } from "../../models/Stage";
import type { CardOptions } from "../cardOptions";
type Updater<T> = React.Dispatch<React.SetStateAction<T>>;
/**
* Extract the trailing path segments from pasted text, so pasting a full
* GitHub URL yields just the username (1 segment), owner/repo (2), or Gist id (1).
*/
function pastedPathTail(text: string, segments: number): string {
let value = text;
if (value.endsWith("/")) {
value = value.slice(0, -1);
}
const parts = value.split("/");
if (parts.length > segments) {
value = parts.slice(-segments).join("/");
}
return value;
}
/** @todo todo consider using React context API to avoid prop drilling */
interface CustomizeStageProps {
selectedCard: CardType;
selectedStatsRank: SelectOption;
setSelectedStatsRank: Updater<SelectOption>;
selectedLanguagesLayout: SelectOption;
setSelectedLanguagesLayout: Updater<SelectOption>;
selectedWakatimeLayout: SelectOption;
setSelectedWakatimeLayout: Updater<SelectOption>;
selectedUserId: string;
setSelectedUserId: Updater<string>;
repo: string;
setRepo: Updater<string>;
gist: string;
setGist: Updater<string>;
wakatimeUser: string;
setWakatimeUser: Updater<string>;
showTitle: boolean;
setShowTitle: Updater<boolean>;
descriptionLines: number | undefined;
setDescriptionLines: Updater<number | undefined>;
showOwner: boolean;
setShowOwner: Updater<boolean>;
customTitle: string;
setCustomTitle: Updater<string>;
langsCount: number | undefined;
setLangsCount: Updater<number | undefined>;
hideValues: boolean;
setHideValues: Updater<boolean>;
showIcons: boolean;
setShowIcons: Updater<boolean>;
showAllStats: boolean;
setShowAllStats: Updater<boolean>;
includeAllCommits: boolean;
setIncludeAllCommits: Updater<boolean>;
enableAnimations: boolean;
setEnableAnimations: Updater<boolean>;
usePercent: boolean;
setUsePercent: Updater<boolean>;
fullSuffix: string;
options: CardOptions;
onOptionChange: <K extends keyof CardOptions>(
key: K,
value: CardOptions[K],
) => void;
card: CardUrlBuilder;
setStage: (stageIndex: StageIndex) => void;
}
export function CustomizeStage({
selectedCard,
selectedStatsRank,
setSelectedStatsRank,
selectedLanguagesLayout,
setSelectedLanguagesLayout,
selectedWakatimeLayout,
setSelectedWakatimeLayout,
selectedUserId,
setSelectedUserId,
repo,
setRepo,
gist,
setGist,
wakatimeUser,
setWakatimeUser,
showTitle,
setShowTitle,
showOwner,
setShowOwner,
descriptionLines,
setDescriptionLines,
customTitle,
setCustomTitle,
langsCount,
setLangsCount,
hideValues,
setHideValues,
showIcons,
setShowIcons,
showAllStats,
setShowAllStats,
includeAllCommits,
setIncludeAllCommits,
enableAnimations,
setEnableAnimations,
usePercent,
setUsePercent,
fullSuffix,
options,
onOptionChange,
card,
setStage,
}: CustomizeStageProps): JSX.Element {
const cardType = selectedCard;
const isAuthenticated = useIsAuthenticated();
const {
selectedUserId,
repo,
gist,
wakatimeUser,
selectedStatsRank,
selectedLanguagesLayout,
selectedWakatimeLayout,
showTitle,
showOwner,
descriptionLines,
customTitle,
langsCount,
hideValues,
showAllStats,
showIcons,
includeAllCommits,
enableAnimations,
usePercent,
} = options;
return (
<div className="w-full flex flex-wrap">
<div className="h-auto lg:w-2/5 md:w-1/2 p-10 rounded-sm bg-gray-200">
<div className="h-auto lg:w-2/5 md:w-1/2 p-10 rounded-sm bg-base-200">
{(cardType === CardType.STATS || cardType === CardType.TOP_LANGS) && (
<TextSection
title="Username"
@@ -126,7 +102,7 @@ export function CustomizeStage({
e.preventDefault();
setStage(0);
}}
className="underline text-blue-900"
className="underline text-primary"
>
log in
</a>{" "}
@@ -137,19 +113,16 @@ export function CustomizeStage({
}
placeholder={`e.g. "${DEMO_USER}"`}
value={selectedUserId}
onValueChange={setSelectedUserId}
onValueChange={(value) => {
onOptionChange("selectedUserId", value);
}}
onPaste={(e) => {
e.preventDefault();
let newValue = e.clipboardData.getData("text");
// if the user pasted a full GitHub URL, extract username
if (newValue.endsWith("/")) {
newValue = newValue.slice(0, -1);
}
const parts = newValue.split("/");
if (parts.length > 1) {
newValue = parts.slice(-1).join("/");
}
setSelectedUserId(newValue);
onOptionChange(
"selectedUserId",
pastedPathTail(e.clipboardData.getData("text"), 1),
);
}}
disabled={!isAuthenticated}
/>
@@ -170,7 +143,7 @@ export function CustomizeStage({
e.preventDefault();
setStage(0);
}}
className="underline text-blue-900"
className="underline text-primary"
>
log in
</a>{" "}
@@ -181,19 +154,16 @@ export function CustomizeStage({
}
placeholder={`e.g. "${DEMO_REPO}"`}
value={repo}
onValueChange={setRepo}
onValueChange={(value) => {
onOptionChange("repo", value);
}}
onPaste={(e) => {
e.preventDefault();
let newValue = e.clipboardData.getData("text");
// if the user pasted a full GitHub URL, extract owner/repo
if (newValue.endsWith("/")) {
newValue = newValue.slice(0, -1);
}
const parts = newValue.split("/");
if (parts.length > 2) {
newValue = parts.slice(-2).join("/");
}
setRepo(newValue);
onOptionChange(
"repo",
pastedPathTail(e.clipboardData.getData("text"), 2),
);
}}
disabled={!isAuthenticated}
/>
@@ -214,7 +184,7 @@ export function CustomizeStage({
e.preventDefault();
setStage(0);
}}
className="underline text-blue-900"
className="underline text-primary"
>
log in
</a>{" "}
@@ -225,19 +195,16 @@ export function CustomizeStage({
}
placeholder={`e.g. "${DEMO_GIST}"`}
value={gist}
onValueChange={setGist}
onValueChange={(value) => {
onOptionChange("gist", value);
}}
onPaste={(e) => {
e.preventDefault();
let newValue = e.clipboardData.getData("text");
// if the user pasted a full GitHub URL, extract Gist ID
if (newValue.endsWith("/")) {
newValue = newValue.slice(0, -1);
}
const parts = newValue.split("/");
if (parts.length > 1) {
newValue = parts.slice(-1).join("/");
}
setGist(newValue);
onOptionChange(
"gist",
pastedPathTail(e.clipboardData.getData("text"), 1),
);
}}
disabled={!isAuthenticated}
/>
@@ -248,19 +215,20 @@ export function CustomizeStage({
description={
<>
Set your{" "}
<a
<LinkExternal
href="https://wakatime.com/"
target="_blank"
className="underline text-blue-900"
className="underline text-primary"
>
WakaTime
</a>{" "}
</LinkExternal>{" "}
username to fetch your stats.
</>
}
placeholder={`e.g. "${DEMO_WAKATIME_USER}"`}
value={wakatimeUser}
onValueChange={setWakatimeUser}
onValueChange={(value) => {
onOptionChange("wakatimeUser", value);
}}
/>
)}
{cardType === CardType.STATS && (
@@ -269,13 +237,17 @@ export function CustomizeStage({
text="Show all available statistics."
question="Show all stats?"
checked={showAllStats}
onCheckedChange={setShowAllStats}
onCheckedChange={(checked) => {
onOptionChange("showAllStats", checked);
}}
/>
)}
{cardType === CardType.STATS && (
<StatsRankSection
selectedOption={selectedStatsRank}
onOptionChange={setSelectedStatsRank}
onOptionChange={(option) => {
onOptionChange("selectedStatsRank", option);
}}
/>
)}
{cardType === CardType.STATS && (
@@ -284,7 +256,9 @@ export function CustomizeStage({
text="Show icons next to all stats."
question="Show icons?"
checked={showIcons}
onCheckedChange={setShowIcons}
onCheckedChange={(checked) => {
onOptionChange("showIcons", checked);
}}
/>
)}
{cardType === CardType.STATS && (
@@ -293,19 +267,25 @@ export function CustomizeStage({
text="Count total commits or just commits of the last 365 days."
question="Include all commits?"
checked={includeAllCommits}
onCheckedChange={setIncludeAllCommits}
onCheckedChange={(checked) => {
onOptionChange("includeAllCommits", checked);
}}
/>
)}
{cardType === CardType.TOP_LANGS && (
<LanguagesLayoutSection
selectedLanguageLayoutOption={selectedLanguagesLayout}
onLanguageLayoutOptionChange={setSelectedLanguagesLayout}
onLanguageLayoutOptionChange={(option) => {
onOptionChange("selectedLanguagesLayout", option);
}}
/>
)}
{cardType === CardType.WAKATIME && (
<WakatimeLayoutSection
selectedOption={selectedWakatimeLayout}
onOptionChange={setSelectedWakatimeLayout}
onOptionChange={(option) => {
onOptionChange("selectedWakatimeLayout", option);
}}
/>
)}
{(cardType === CardType.TOP_LANGS ||
@@ -320,7 +300,9 @@ export function CustomizeStage({
</>
}
value={langsCount}
onValueChange={setLangsCount}
onValueChange={(value) => {
onOptionChange("langsCount", value);
}}
min={1}
max={20}
/>
@@ -331,7 +313,9 @@ export function CustomizeStage({
text="Hide language percentages or bytes while keeping the selected layout visible."
question="Hide values?"
checked={hideValues}
onCheckedChange={setHideValues}
onCheckedChange={(checked) => {
onOptionChange("hideValues", checked);
}}
/>
)}
{cardType === CardType.WAKATIME && (
@@ -340,7 +324,9 @@ export function CustomizeStage({
text="Show time spent in hours or percentages."
question="Show percentages?"
checked={usePercent}
onCheckedChange={setUsePercent}
onCheckedChange={(checked) => {
onOptionChange("usePercent", checked);
}}
/>
)}
{(cardType === CardType.STATS ||
@@ -351,7 +337,9 @@ export function CustomizeStage({
text="Shows a title at the top of the card."
question="Show title?"
checked={showTitle}
onCheckedChange={setShowTitle}
onCheckedChange={(checked) => {
onOptionChange("showTitle", checked);
}}
/>
)}
{(cardType === CardType.STATS || cardType === CardType.WAKATIME) && (
@@ -366,7 +354,9 @@ export function CustomizeStage({
}
placeholder='e.g. "My GitHub Stats"'
value={customTitle}
onValueChange={setCustomTitle}
onValueChange={(value) => {
onOptionChange("customTitle", value);
}}
/>
)}
{(cardType === CardType.STATS ||
@@ -377,16 +367,20 @@ export function CustomizeStage({
// text="Enable Animations."
question="enable animations?"
checked={enableAnimations}
onCheckedChange={setEnableAnimations}
onCheckedChange={(checked) => {
onOptionChange("enableAnimations", checked);
}}
/>
)}
{(cardType === CardType.PIN || cardType === CardType.GIST) && (
{CATEGORY_BY_CARD_TYPE[cardType] === CardCategory.REPO && (
<CheckboxSection
title="Show Owner?"
text="Shows the repo owner's name next to the repo name."
question="Show owner?"
checked={showOwner}
onCheckedChange={setShowOwner}
onCheckedChange={(checked) => {
onOptionChange("showOwner", checked);
}}
/>
)}
{cardType === CardType.PIN && (
@@ -401,26 +395,27 @@ export function CustomizeStage({
</>
}
value={descriptionLines}
onValueChange={setDescriptionLines}
onValueChange={(value) => {
onOptionChange("descriptionLines", value);
}}
min={1}
max={3}
/>
)}
<div className="pl-10 pr-10">
For more customization options check the{" "}
<a
href="https://github.com/stats-organization/github-stats-extended/blob/master/docs/advanced_documentation.md"
target="_blank"
className="underline text-blue-900"
<LinkExternal
href={docsUrl("customization/common-options")}
className="underline text-primary"
>
customization documentation
</a>{" "}
</LinkExternal>{" "}
after you copied your card URL in step 5.
</div>
</div>
<div className="w-full lg:w-3/5 md:w-1/2 object-center pt-5 md:pt-0 pl-0 md:pl-5 lg:pl-0">
<div className="w-full lg:w-3/5 mx-auto flex flex-col justify-center sticky top-32">
<CardImage imageSrc={fullSuffix} stage={2} />
<CardImage card={card} stage={2} />
</div>
</div>
</div>
@@ -1,25 +1,31 @@
import { clsx } from "clsx";
import type { JSX } from "react";
import { toast } from "react-toastify";
import { saveSvgAsPng } from "save-svg-as-png";
import { CardImage } from "../../../components/Card/CardImage";
import { Button } from "../../../components/Generic/Button";
import { HOST } from "../../../constants";
import { CardImage } from "../../components/Card/CardImage";
import { getCardThemeBackdrop } from "../../components/Card/themeBackdrop";
import { Button } from "../../components/Generic/Button";
import type { CardUrlBuilder } from "../../models/CardUrl";
import { useIsDarkTheme } from "../../useIsDarkTheme";
interface DisplayStageProps {
filename: string;
link: string;
themeSuffix: string;
theme: string;
card: CardUrlBuilder;
guestHint: string | null;
}
export function DisplayStage({
filename,
link,
themeSuffix,
theme,
card,
guestHint,
}: DisplayStageProps): JSX.Element {
const isDark = useIsDarkTheme();
const downloadPNG = () => {
saveSvgAsPng(
document.getElementById("svgWrapper")?.shadowRoot?.firstElementChild
@@ -34,7 +40,7 @@ export function DisplayStage({
const copyMarkdown = () => {
void navigator.clipboard.writeText(
`[![GitHub Stats](https://${HOST}/api${themeSuffix})](${link})`,
`[![GitHub Stats](${card.toApiUrl(HOST)})](${link})`,
);
toast.info("Copied to Clipboard!", {
position: "bottom-right",
@@ -47,7 +53,7 @@ export function DisplayStage({
};
const copyUrl = () => {
void navigator.clipboard.writeText(`https://${HOST}/api${themeSuffix}`);
void navigator.clipboard.writeText(card.toApiUrl(HOST));
toast.info("Copied to Clipboard!", {
position: "bottom-right",
autoClose: 1500,
@@ -61,7 +67,7 @@ export function DisplayStage({
return (
<div className="w-full flex flex-wrap">
<div className="h-auto lg:w-2/5 md:w-1/2">
<div className="p-10 rounded-sm bg-gray-200">
<div className="p-10 rounded-sm bg-base-200">
<div className="flex flex-col items-center">
{[
{
@@ -82,10 +88,8 @@ export function DisplayStage({
].map((item) => (
<Button
key={item.title}
className={clsx("m-4 w-60 flex justify-center", {
"bg-blue-500 hover:bg-blue-600 text-white": item.highlight,
"bg-white hover:bg-gray-100 text-black": !item.highlight,
})}
variant={item.highlight ? "primary" : "soft"}
className="m-4 w-60 flex justify-center"
onClick={item.onClick}
>
{item.title}
@@ -96,10 +100,14 @@ export function DisplayStage({
</div>
</div>
<div className="w-full lg:w-3/5 md:w-1/2 object-center pt-5 md:pt-0 pl-0 md:pl-5 lg:pl-0">
<div className="w-full lg:w-3/5 mx-auto flex flex-col justify-center sticky top-32">
<div
className="w-full lg:w-3/5 mx-auto flex flex-col justify-center sticky top-32 rounded p-4"
style={{ background: getCardThemeBackdrop(theme, isDark) }}
>
<CardImage
imageSrc={`${themeSuffix}&disable_animations=true`}
card={card.disableAnimations()}
stage={4}
className="flex justify-center"
/>
</div>
</div>
@@ -2,7 +2,7 @@ import { useEffect, useRef } from "react";
import type { JSX, RefObject } from "react";
import { createPortal } from "react-dom";
import { Button } from "../../../../components/Generic/Button";
import { Button } from "../../../components/Generic/Button";
function useOutsideAlerter(
ref: RefObject<HTMLElement | null>,
@@ -42,10 +42,11 @@ export function LoginAccountDeleteModal(
useOutsideAlerter(wrapperRef, onClose);
return createPortal(
<div className="fixed left-0 top-0 w-full h-full">
// `wizard` because this mounts on `body`, outside the element the app's styles are scoped to.
<div className="wizard fixed left-0 top-0 w-full h-full bg-black/50 z-50">
<div className="w-full h-full flex justify-center items-center">
<div
className="w-96 p-4 bg-white rounded-sm border-2 border-gray-200 text-gray-700"
className="w-96 p-4 bg-base-100 rounded-sm border-2 border-base-300 text-base-content"
ref={wrapperRef}
>
<p className="mb-1 text-2xl">Delete Account</p>
@@ -57,16 +58,10 @@ export function LoginAccountDeleteModal(
</p>
<br />
<div className="flex flex-wrap">
<Button
className="bg-blue-500 hover:bg-blue-600 text-white rounded-[0.25rem]"
onClick={onClose}
>
<Button variant="primary" onClick={onClose}>
Cancel
</Button>
<Button
className="bg-gray-200 hover:bg-gray-300 ml-auto rounded-[0.25rem] text-red-600 border-2"
onClick={onConfirm}
>
<Button variant="error" className="ml-auto" onClick={onConfirm}>
Delete Account
</Button>
</div>
@@ -3,8 +3,6 @@ import type { JSX } from "react";
import { FaGithub as GithubIcon } from "react-icons/fa";
import { useDispatch } from "react-redux";
import { deleteAccount } from "../../../../api/user";
import { Button } from "../../../../components/Generic/Button";
import {
CLIENT_ID,
GITHUB_PRIVATE_AUTH_URL,
@@ -16,6 +14,8 @@ import {
useUserKey,
} from "../../../../redux/selectors/userSelectors";
import { logout } from "../../../../redux/slices/user";
import { deleteAccount } from "../../../api/user";
import { Button } from "../../../components/Generic/Button";
import { LoginAccountDeleteModal } from "./LoginAccountDeleteModal";
import { LoginBox } from "./LoginBox";
@@ -56,14 +56,17 @@ export function LoginAccountManagement(): JSX.Element {
<a
href={`https://${HOST}/api/downgrade?user_key=${userKey as string}`}
>
<Button className="h-12 flex justify-center items-center w-[320px] text-black border border-black bg-white hover:bg-gray-100">
<Button
variant="soft"
className="h-12 flex justify-center items-center w-[320px]"
>
<GithubIcon className="w-6 h-6" />
<span className="ml-2 xl:text-lg">
Downgrade to Public Access
</span>
</Button>
</a>
<p className="text-sm text-gray-600 flex-1">
<p className="text-sm text-base-content/70 flex-1">
Switch to public access if you prefer not to share private
contributions.
</p>
@@ -71,14 +74,17 @@ export function LoginAccountManagement(): JSX.Element {
) : (
<div className="flex items-center gap-4">
<a href={GITHUB_PRIVATE_AUTH_URL}>
<Button className="h-12 flex justify-center items-center w-[320px] text-white bg-blue-500 hover:bg-blue-600">
<Button
variant="primary"
className="h-12 flex justify-center items-center w-[320px]"
>
<GithubIcon className="w-6 h-6" />
<span className="ml-2 xl:text-lg">
Upgrade to Private Access
</span>
</Button>
</a>
<p className="text-sm text-gray-600 flex-1">
<p className="text-sm text-base-content/70 flex-1">
Upgrade to include contributions in private repositories for more
complete and accurate stats.
</p>
@@ -89,12 +95,15 @@ export function LoginAccountManagement(): JSX.Element {
{/* Delete Account Button */}
<div className="mt-6 flex items-center gap-4">
<Button
className="h-12 flex justify-center items-center w-[320px] text-black border border-black bg-white hover:bg-gray-100"
variant="soft"
className="h-12 flex justify-center items-center w-[320px]"
onClick={openDeleteModal}
>
<span className="xl:text-lg text-red-600">Delete Account</span>
<span className="xl:text-lg text-error text-shadow-none">
Delete Account
</span>
</Button>
<p className="text-sm text-gray-600 flex-1">
<p className="text-sm text-base-content/70 flex-1">
This will delete your GitHub-Stats-Extended account and then redirect
you to a GitHub screen where you can revoke your access token.
</p>
@@ -103,12 +112,13 @@ export function LoginAccountManagement(): JSX.Element {
{/* Logout Button */}
<div className="mt-6 flex items-center gap-4">
<Button
className="h-12 flex justify-center items-center w-[320px] text-black border border-black bg-white hover:bg-gray-100"
variant="soft"
className="h-12 flex justify-center items-center w-[320px]"
onClick={handleLogout}
>
<span className="xl:text-lg">Log Out</span>
</Button>
<p className="text-sm text-gray-600 flex-1">
<p className="text-sm text-base-content/70 flex-1">
Log out from GitHub-Stats-Extended.
</p>
</div>
@@ -16,7 +16,7 @@ export function LoginBox(props: LoginBoxProps): JSX.Element {
<div className="h-full flex flex-wrap">
<div className={clsx("md:flex", { "opacity-25": isOpaque })}>
<div className="lg:block lg:w-3/5 lg:p-8">
<div className="bg-gray-200 rounded-sm w-full h-full m-auto p-8 shadow lg:h-auto">
<div className="bg-base-300 rounded-sm w-full h-full m-auto p-8 shadow lg:h-auto">
{children}
</div>
</div>
@@ -0,0 +1,70 @@
import type { JSX } from "react";
import {
DEMO_GIST,
DEMO_REPO,
DEMO_USER,
DEMO_WAKATIME_USER,
} from "../../../../constants";
import { CardImage } from "../../../components/Card/CardImage";
import { CardType } from "../../../models/CardType";
import { cardUrl } from "../../../models/CardUrl";
import { useIsDarkTheme } from "../../../useIsDarkTheme";
const cards = [
cardUrl(CardType.PIN).repo(DEMO_REPO).disableAnimations(),
cardUrl(CardType.TOP_LANGS)
.username(DEMO_USER)
.langsCount(4)
.disableAnimations(),
cardUrl(CardType.STATS)
.username(DEMO_USER)
.includeAllCommits()
.disableAnimations(),
cardUrl(CardType.WAKATIME)
.username(DEMO_WAKATIME_USER)
.langsCount(6)
.cardWidth(450)
.disableAnimations(),
cardUrl(CardType.GIST).gistId(DEMO_GIST).disableAnimations(),
];
function getCardXPosition(cardIndex: number): number {
const radius = 60;
const centerX = 70;
const startAngle = Math.PI * 0.75; // 135 degrees
const endAngle = Math.PI * 1.25; // 225 degrees
const angle =
startAngle + (endAngle - startAngle) * (cardIndex / (cards.length - 1));
const x = centerX + radius * Math.cos(angle);
return x;
}
export function LoginBoxDemoCards(): JSX.Element {
const isDark = useIsDarkTheme();
return (
<div className="w-full h-full lg:w-2/5 flex lg:flex-col lg:p-8 relative overflow-hidden">
<div className="relative w-full h-full">
{cards.map((card, index) => {
// Show dark-themed demo cards in dark mode so they fit the surroundings.
const demoCard = isDark ? card.theme("github_dark") : card;
return (
<div
key={demoCard.toString()}
style={{
left: `${getCardXPosition(index)}%`,
position: "relative",
zoom: "0.5",
marginBottom: "1%",
}}
>
<CardImage card={demoCard} compact={false} stage={0} />
</div>
);
})}
</div>
</div>
);
}
@@ -1,11 +1,11 @@
import type { JSX } from "react";
import { FaGithub as GithubIcon } from "react-icons/fa";
import { Button } from "../../../../components/Generic/Button";
import {
GITHUB_PRIVATE_AUTH_URL,
GITHUB_PUBLIC_AUTH_URL,
} from "../../../../constants";
import { Button } from "../../../components/Generic/Button";
import { LoginBox } from "./LoginBox";
@@ -20,24 +20,30 @@ export function LoginOptions(props: LoginOptionsProps): JSX.Element {
<LoginBox>
<div className="flex items-center gap-4 mb-4">
<a href={GITHUB_PUBLIC_AUTH_URL}>
<Button className="h-12 flex justify-center items-center w-[260px] text-white bg-blue-500 hover:bg-blue-600">
<Button
variant="primary"
className="h-12 flex justify-center items-center w-[260px]"
>
<GithubIcon className="w-6 h-6" />
<span className="ml-2 xl:text-lg">GitHub Public Access</span>
GitHub Public Access
</Button>
</a>
<p className="text-sm text-gray-600 flex-1">
<p className="text-sm text-base-content/80 flex-1">
Generate stats based on your contributions in public repositories.
</p>
</div>
<div className="flex items-center gap-4 mb-4">
<a href={GITHUB_PRIVATE_AUTH_URL}>
<Button className="h-12 flex justify-center items-center w-[260px] text-black border border-black bg-white hover:bg-gray-100">
<Button
variant="soft"
className="h-12 flex justify-center items-center w-[260px]"
>
<GithubIcon className="w-6 h-6" />
<span className="ml-2 xl:text-lg">GitHub Private Access</span>
GitHub Private Access
</Button>
</a>
<p className="text-sm text-gray-600 flex-1">
<p className="text-sm text-base-content/80 flex-1">
Include contributions from private repositories for more complete and
accurate stats.
</p>
@@ -45,12 +51,13 @@ export function LoginOptions(props: LoginOptionsProps): JSX.Element {
<div className="flex items-center gap-4">
<Button
className="h-12 flex justify-center items-center w-[260px] text-black border border-black bg-white hover:bg-gray-100"
variant="soft"
className="h-12 flex justify-center items-center w-[260px]"
onClick={onContinueAsGuest}
>
<span className="ml-2 xl:text-lg">Continue as Guest</span>
Continue as Guest
</Button>
<p className="text-sm text-gray-600 flex-1">
<p className="text-sm text-base-content/80 flex-1">
Explore options using sample data. Insert your own username in the
last step.
</p>

Some files were not shown because too many files have changed in this diff Show More