Compare commits

...
36 Commits
Author SHA1 Message Date
Martin dc9c926800 bump core version to 2.1.4 (#398) 2026-07-24 20:24:20 +02:00
Martin 986eaad1a8 release: longer prerender caching 2 (#394)
see https://github.com/stats-organization/github-stats-extended/pull/393
2026-07-23 21:34:31 +02:00
Martin 5403eefbd3 release: switch from stargazers.totalCount to stargazerCount (#392)
release
https://github.com/stats-organization/github-stats-extended/pull/391 as
hotfix
2026-07-23 20:43:32 +02:00
Martin 12b27566be release: longer prerender caching (#361)
see https://github.com/stats-organization/github-stats-extended/pull/360
2026-07-13 10:48:31 +02:00
Martin 40097713b9 release: adjust repeat-recent (#350)
Cherry-pick of
https://github.com/stats-organization/github-stats-extended/pull/341.

no GitHub release planned for this merge
2026-07-08 20:16:06 +02:00
Martin 2168c1f91e release: fix primary color in step 4 (#318)
no GitHub release planned for this merge
2026-06-30 09:10:34 +02:00
Martin fa82216dfe fix primary color in step 4 (#317) 2026-06-30 09:04:48 +02:00
Martin 9cf716c85f bring latest changes to github-stats-extended.vercel.app (#316)
no GitHub release planned for this merge
2026-06-30 08:30:38 +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 9f4ad921aa release v2.1.3, again (#291) 2026-06-19 00:11:49 +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
MartinMarco Pasqualettigithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b6f4183ddd release v2.1.3 (#288)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 21:32:46 +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
Martin 73189400ee set correct version for 'core' package (#219) 2026-05-08 12:58:37 +02:00
Martin 8a239313d3 ignore git checks when publishing (#217) 2026-05-08 12:20:04 +02:00
Martin a3dbb37dd2 start publishing "core" npm package (#215) 2026-05-08 12:00:49 +02:00
martin-mfg 9c4e631bc7 Merge remote-tracking branch 'origin/master' into release 2026-05-08 11:57:09 +02:00
Martin 6ebd9a082f release v2.0.0 (#209) 2026-05-06 18:49:05 +02:00
Martin 9236acd99d release 1.2.0 (#204) 2026-05-03 18:28:19 +02:00
martin-mfg 12b4e979a8 Merge branch 'master' into release-1.2.0 2026-05-03 14:26:37 +02:00
Martin 5c4ea2a797 merge master: documentation updates (#127) 2026-03-07 20:52:27 +01:00
Martin 7d666e2510 merge master: fix dark mode colors in delete modal (#125) 2026-03-07 16:18:04 +01:00
Martin 96a18562f0 update release (#122)
Merging the latest `master` into `release` in order to create a new
release.
2026-03-07 14:36:04 +01:00
martin-mfg b64d601d47 Merge remote-tracking branch 'origin/master' into update-release 2026-03-07 14:34:01 +01:00
martin-mfg 26aec2a4af Merge remote-tracking branch 'origin/master' into release 2026-03-07 12:55:13 +01:00
martin-mfg ad23ab39ed Merge branch 'master' into release 2026-03-07 12:23:50 +01:00
martin-mfg d982bef3c5 Revert "fix problem on simultaneous login"
This reverts commit 23ba727b9a.
2026-01-08 18:57:49 +01:00
martin-mfg 2463f0900d Merge branch 'master' into release 2026-01-08 18:51:11 +01:00
martin-mfg 23ba727b9a fix problem on simultaneous login 2026-01-07 11:55:03 +01:00
martin-mfg 6c625fb76b Merge branch 'master' into release 2026-01-07 11:44:26 +01:00
martin-mfg 0aafbbbdb5 back to release rewrite 2026-01-06 20:21:14 +01:00
martin-mfg 8e6e0fac47 back to relase env vars 2026-01-06 20:00:49 +01:00
99 changed files with 1437 additions and 1329 deletions
@@ -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"
+1 -1
View File
@@ -10,7 +10,7 @@ on:
# │ │ │ │ │
# │ │ │ │ │
# * * * * *
- cron: "45 * * * *"
- cron: "15,45 * * * *"
workflow_dispatch:
jobs:
+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,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
}
+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);
}
}
+2 -4
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",
@@ -443,7 +443,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":"\\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 stargazerCount\\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"]}}",
"headers": [
[
"Cache-Control",
@@ -341,7 +341,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":"\\n fragment 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 }\\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"}}",
"headers": [
[
"Cache-Control",
+4 -6
View File
@@ -26,9 +26,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 +73,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: {
+41 -2
View File
@@ -12,8 +12,8 @@ test("load initial page correctly", async ({ page }) => {
// Logo exists
await expect(page.locator('img[alt="logo"]')).toBeVisible();
// Star on GitHub button
const starButton = page.getByRole("button", { name: /star on/i });
// Star on GitHub link
const starButton = page.getByRole("link", { name: /star on/i });
await expect(starButton).toBeVisible();
const githubLink = page.locator(
@@ -39,6 +39,45 @@ test("load initial page correctly", async ({ page }) => {
await expect(guestBtn).toBeEnabled();
});
test("theme picker switches and persists the theme", async ({ page }) => {
await page.goto("");
const html = page.locator("html");
const trigger = page.getByRole("button", { name: /choose theme/i });
const options = page.getByRole("menuitemradio");
// The menu is closed initially.
await expect(trigger).toBeVisible();
await expect(trigger).toHaveAttribute("aria-expanded", "false");
await expect(options).toHaveCount(0);
// Opening it reveals the options (portaled to body, above the stepper).
await trigger.click();
await expect(trigger).toHaveAttribute("aria-expanded", "true");
// Read the themes off the options instead of hardcoding their names.
const firstTheme = await options.first().getAttribute("data-theme-value");
const lastTheme = await options.last().getAttribute("data-theme-value");
// Selecting an option applies the theme, persists it, and closes the menu.
await options.last().click();
await expect(html).toHaveAttribute("data-theme", lastTheme ?? "");
await expect(options).toHaveCount(0);
await expect(
page.evaluate(() => localStorage.getItem("theme")),
).resolves.toBe(lastTheme);
// The choice survives a reload and is marked as selected on reopen.
await page.reload();
await expect(html).toHaveAttribute("data-theme", lastTheme ?? "");
await trigger.click();
await expect(options.last()).toHaveAttribute("aria-checked", "true");
// Switching to another option works too.
await options.first().click();
await expect(html).toHaveAttribute("data-theme", firstTheme ?? "");
});
test("navigates between steps", async ({ page }) => {
await page.goto("");
+18
View File
@@ -37,6 +37,24 @@
<title>GitHub Stats Extended</title>
</head>
<body>
<script>
// Apply the persisted theme from localStorage before first paint. This is
// what makes a saved theme survive a reload when it differs from the OS
// preference (DaisyUI's `--prefersdark` only covers the OS default, and
// nothing in React sets `data-theme` on mount).
// Also suppress color transitions until after the first paint so elements
// don't animate from their unstyled state on load.
(function () {
try {
var stored = localStorage.getItem("theme");
const matchesDarkTheme = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
var theme = stored || (matchesDarkTheme ? "dark" : "light");
document.documentElement.setAttribute("data-theme", theme);
} catch (e) {}
})();
</script>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
+40 -5
View File
@@ -1,7 +1,8 @@
import { clsx } from "clsx";
import type { JSX } from "react";
import type { CSSProperties, JSX } from "react";
import { CardImage } from "./CardImage";
import { LIGHT_CARD_BG } from "./themeBackdrop";
interface CardProps {
title: string;
@@ -11,6 +12,10 @@ interface CardProps {
selected?: boolean;
compact?: boolean;
fixedSize?: boolean;
/** CSS background for the card surface (e.g. a theme's own background). */
backgroundColor?: string;
/** Title color, used when the card sits on a custom background. */
titleColor?: string;
}
export const Card = ({
@@ -21,16 +26,46 @@ export const Card = ({
selected = false,
compact = false,
fixedSize = false,
backgroundColor,
titleColor,
}: CardProps): JSX.Element => {
const customBackground = backgroundColor
? selected
? `color-mix(in oklab, var(--color-primary) ${backgroundColor === LIGHT_CARD_BG ? "10%" : "25%"}, ${backgroundColor})`
: backgroundColor
: undefined;
const cardStyle = customBackground
? ({ "--card-bg": customBackground } as CSSProperties)
: undefined;
return (
<div
className={clsx("p-6 rounded border-2", {
className={clsx("p-6 rounded border-4", {
"h-[370px] w-[510px]": fixedSize,
"border-blue-500 bg-blue-50": selected,
"border-gray-200 bg-white hover:bg-gray-50": !selected,
"border-primary": selected,
"border-base-300": !selected,
"bg-[var(--card-bg)]": !!customBackground,
// Token backgrounds only apply when no custom background is given.
"bg-primary/10": selected && !customBackground,
"bg-base-100 hover:bg-base-200": !selected && !customBackground,
"hover:bg-base-300": !selected && !!customBackground,
})}
style={cardStyle}
data-theme={
stage == 3
? backgroundColor === LIGHT_CARD_BG || !backgroundColor
? "light"
: "dark"
: undefined
}
>
<h2 className="text-xl font-medium title-font text-gray-900">{title}</h2>
<h2
className="text-xl font-medium title-font text-base-content"
style={titleColor ? { color: titleColor } : undefined}
>
{title}
</h2>
<p className="text-base leading-relaxed mt-2 mb-4">{description}</p>
<CardImage
imageSrc={imageSrc}
@@ -50,7 +50,7 @@ export function SvgInline(props: SvgInlineProps): JSX.Element {
FETCH_MULTI_PAGE_STARS: "10",
PAT_1: userToken as string, // even if it's null, core's retryer.js sees there is 1 PAT and sets `RETRIES` accordingly
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
loadConfigFromEnv(config);
setLoaded(false);
@@ -0,0 +1,90 @@
import { themes } from "@stats-organization/github-readme-stats-core";
// We use the same background colors here which GitHub uses.
export const LIGHT_CARD_BG = "#ffffff";
const DARK_CARD_BG = "#0d1117";
// Themes that look good in either mode (their text/icon colors read on both a
// light and a dark backdrop), so their backdrop follows the app theme. Note we
// only list themes that are genuinely mode-agnostic here: other transparent
// themes (e.g. shadow_*) have dark text and must stay on a light backdrop.
const ADAPTIVE_THEMES = ["transparent", "ambient_gradient"];
/** Expand a shorthand `#abc` hex to `#aabbcc`. */
function expandHex(hex: string): string {
if (hex.length === 3) {
return hex
.split("")
.map((char) => char + char)
.join("");
}
return hex;
}
/** Whether a hex color (3-, 6- or 8-digit) is dark by perceived luminance. */
function isDarkHex(hex: string): boolean {
const normalized = expandHex(hex);
if (normalized.length < 6) {
return false;
}
const r = parseInt(normalized.slice(0, 2), 16);
const g = parseInt(normalized.slice(2, 4), 16);
const b = parseInt(normalized.slice(4, 6), 16);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance < 0.5;
}
/** Whether a theme's `bg_color` (hex or `angle,c1,c2,…` gradient) is dark. */
function isDarkBg(bgColor: string): boolean {
const parts = bgColor.split(",");
// For gradients use the first color stop (index 0 is the angle).
return isDarkHex((parts.length > 1 ? parts[1] : parts[0]) ?? "");
}
/** A theme whose backdrop should follow the app mode rather than its own bg. */
function isAdaptiveTheme(themeName: string): boolean {
return ADAPTIVE_THEMES.includes(themeName);
}
/**
* Whether the backdrop a card actually gets is dark, for the given app mode.
* Adaptive themes follow the app mode; the rest follow their own background.
*/
function isCardBackdropDark(themeName: string, appIsDark: boolean): boolean {
const theme = themes[themeName as keyof typeof themes];
if (isAdaptiveTheme(themeName)) {
return appIsDark;
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!theme) {
return appIsDark;
}
return isDarkBg(theme.bg_color);
}
/**
* Sort rank for the theme grid: light themes first (0), adaptive themes in the
* middle (1), dark themes last (2). Adaptive cards sit between the groups so
* they blend with whichever side matches their backdrop in the current mode.
*/
export function getThemeSortRank(themeName: string): number {
if (isAdaptiveTheme(themeName)) {
return 1;
}
const theme = themes[themeName as keyof typeof themes];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!theme) {
return 0;
}
return isDarkBg(theme.bg_color) ? 2 : 0;
}
/** The backdrop color a card rendered with the given theme should sit on. */
export function getCardThemeBackdrop(
themeName: string,
appIsDark: boolean,
): string {
return isCardBackdropDark(themeName, appIsDark)
? DARK_CARD_BG
: LIGHT_CARD_BG;
}
@@ -1,18 +1,32 @@
import { clsx } from "clsx";
import type { HTMLProps, JSX, ReactNode } from "react";
interface ButtonProps extends HTMLProps<HTMLButtonElement> {
type ButtonVariant = "primary" | "soft" | "error";
type ButtonSize = "sm" | "md" | "lg";
interface ButtonProps extends Omit<HTMLProps<HTMLButtonElement>, "size"> {
children: ReactNode;
/** DaisyUI color variant. Omit for the default neutral button. */
variant?: ButtonVariant | undefined;
size?: ButtonSize;
}
export function Button(props: ButtonProps): JSX.Element {
const { className, children, ...rest } = props;
const { className, children, variant, size = "md", ...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",
"btn text-lg",
{
"btn-primary": variant === "primary",
"btn-soft": variant === "soft",
"btn-error": variant === "error",
"btn-sm": size === "sm",
"btn-lg": size === "lg",
},
className,
)}
>
@@ -19,7 +19,7 @@ export function Checkbox({
type="checkbox"
disabled={disabled}
checked={checked}
className="checkbox bg-white text-[#18181b] mr-2"
className="checkbox checkbox-primary mr-2"
onChange={() => {
onCheckedChange(!checked);
}}
@@ -27,10 +27,7 @@ export function Select({
}: SelectProps): JSX.Element {
return (
<select
className={clsx(
"text-gray-700 text-base bg-white select select-sm w-40 rounded-sm mt-4",
className,
)}
className={clsx("text-base select select-sm w-40 mt-4", className)}
value={selectedOption.value}
onChange={(e) => {
onOptionChange(options[e.target.selectedIndex] as SelectOption);
@@ -43,7 +40,7 @@ export function Select({
value={option.value}
disabled={option.disabled}
className={clsx({
"bg-blue-200": option.value === selectedOption.value,
"bg-primary/20": option.value === selectedOption.value,
})}
>
{option.label}
@@ -0,0 +1,131 @@
import { useCallback, useEffect, useRef, useState } from "react";
import type { JSX } from "react";
import { createPortal } from "react-dom";
import {
FaCheck as CheckIcon,
FaMoon as MoonIcon,
FaSun as SunIcon,
} from "react-icons/fa";
import { useTheme } from "../../redux/selectors/themeSelectors";
import { THEMES } from "../../redux/slices/theme";
interface ThemeIconProps {
isDark: boolean;
className: string;
}
/** Sun for light themes, moon for dark ones. */
function ThemeIcon({ isDark, className }: ThemeIconProps): JSX.Element {
return isDark ? (
<MoonIcon className={className} />
) : (
<SunIcon className={className} />
);
}
export function ThemePicker(): JSX.Element {
const { theme, isDark, setTheme } = useTheme();
const [open, setOpen] = useState(false);
const [position, setPosition] = useState({ top: 0, right: 0 });
const buttonRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<HTMLUListElement>(null);
const handleToggle = useCallback(() => {
if (!open && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
setPosition({
top: rect.bottom + 8,
right: window.innerWidth - rect.right,
});
}
setOpen((value) => !value);
}, [open]);
useEffect(() => {
if (!open) {
return undefined;
}
const close = () => {
setOpen(false);
};
const handlePointerDown = (event: MouseEvent) => {
const target = event.target as Node;
if (
!menuRef.current?.contains(target) &&
!buttonRef.current?.contains(target)
) {
setOpen(false);
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setOpen(false);
}
};
document.addEventListener("mousedown", handlePointerDown);
document.addEventListener("keydown", handleKeyDown);
window.addEventListener("resize", close);
// Close on scroll so the fixed menu can't detach from its button.
window.addEventListener("scroll", close, true);
return () => {
document.removeEventListener("mousedown", handlePointerDown);
document.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("resize", close);
window.removeEventListener("scroll", close, true);
};
}, [open]);
return (
<>
<button
ref={buttonRef}
type="button"
className="btn btn-ghost btn-circle"
aria-label="Choose theme"
aria-haspopup="menu"
aria-expanded={open}
title="Choose theme"
onClick={handleToggle}
>
<ThemeIcon isDark={isDark} className="w-4 h-4" />
</button>
{open &&
createPortal(
<ul
ref={menuRef}
role="menu"
className="menu bg-base-200 text-base-content rounded-box fixed z-[9999] w-48 p-2 shadow-lg"
style={{ top: position.top, right: position.right }}
>
{THEMES.map((option) => (
<li key={option.name}>
<button
type="button"
role="menuitemradio"
aria-checked={option.name === theme}
data-theme-value={option.name}
className="flex items-center justify-between"
onClick={() => {
setTheme(option.name);
setOpen(false);
}}
>
<span className="flex items-center gap-2">
<ThemeIcon isDark={option.isDark} className="w-4 h-4" />
{option.label}
</span>
{option.name === theme && <CheckIcon className="w-3 h-3" />}
</button>
</li>
))}
</ul>,
document.body,
)}
</>
);
}
@@ -61,7 +61,7 @@ export function NumericSection({
<p>{description}</p>
<input
type="number"
className="border border-gray-300 bg-white rounded px-2 py-1 mt-2 w-1/4"
className="border border-base-content/20 rounded px-2 py-1 mt-2 w-1/4 bg-base-100"
value={internalValue ?? ""}
onChange={(e) => {
setInternalValue(e.target.value);
+33 -17
View File
@@ -24,7 +24,7 @@ function ProgressSection({
<button
className={clsx(
"w-1/4 flex flex-col mx-2 p-2 cursor-pointer",
passed ? "border-blue-500" : "border-gray-500",
passed ? "border-primary" : "border-base-content/60",
isActive ? "border-t-[14px] -mt-[5px]" : "border-t-4",
)}
type="button"
@@ -32,14 +32,16 @@ function ProgressSection({
>
<div
className={clsx("text-lg font-bold", {
"text-blue-500": passed,
"text-gray-500": !passed,
"text-primary": passed,
"text-base-content/60": !passed,
"-mt-[4px]": isActive,
})}
>
{`Step ${num + 1}`}
</div>
<div className={passed ? "text-gray-700" : "text-gray-500"}>{item}</div>
<div className={passed ? "text-base-content/80" : "text-base-content/60"}>
{item}
</div>
</button>
);
}
@@ -59,16 +61,23 @@ export function ProgressBar({
const rightDisabled = currItemIndex === items.length - 1;
return (
<div className="w-full flex items-center sticky top-0 bg-gray-200 z-50 pt-3 pb-1 px-1 md:px-20 shadow-md">
<LeftArrowIcon
className={clsx("w-8 h-8", {
"text-gray-400 cursor-not-allowed": leftDisabled,
"text-gray-700 cursor-pointer": !leftDisabled,
})}
<div className="w-full flex items-center sticky top-0 bg-base-300 z-50 pt-3 pb-1 px-1 md:px-20 shadow-md">
<button
type="button"
aria-label="Previous step"
disabled={leftDisabled}
className="btn btn-ghost btn-circle"
onClick={() => {
onItemClick(Math.max(currItemIndex - 1, 0));
}}
/>
>
<LeftArrowIcon
className={clsx("w-8 h-8", {
"text-base-content/30": leftDisabled,
"text-base-content/70": !leftDisabled,
})}
/>
</button>
<div className="px-2 flex-grow flex flex-row">
{items.map((item, index) => {
return (
@@ -85,15 +94,22 @@ export function ProgressBar({
);
})}
</div>
<RightArrowIcon
className={clsx("w-8 h-8", {
"text-gray-400 cursor-not-allowed": rightDisabled,
"text-gray-700 cursor-pointer": !rightDisabled,
})}
<button
type="button"
aria-label="Next step"
disabled={rightDisabled}
className="btn btn-ghost btn-circle"
onClick={() => {
onItemClick(Math.min(currItemIndex + 1, items.length - 1));
}}
/>
>
<RightArrowIcon
className={clsx("w-8 h-8", {
"text-base-content/30": rightDisabled,
"text-base-content/70": !rightDisabled,
})}
/>
</button>
</div>
);
}
@@ -10,14 +10,14 @@ export function Section({ title, children }: SectionProps): JSX.Element {
return (
<div className="flex relative pb-12">
<div className="h-full w-10 absolute inset-0 flex items-center justify-center">
<div className="h-full w-1 bg-gray-200 pointer-events-none" />
<div className="h-full w-1 bg-base-300 pointer-events-none" />
</div>
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-blue-500 inline-flex items-center justify-center text-white relative z-10">
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-primary inline-flex items-center justify-center text-primary-content relative z-10">
<LightningIcon className="w-5 h-5" />
</div>
<div className="flex-grow pl-4">
<h2 className="font-medium title-font text-sm text-gray-900 mb-1 tracking-wider">
<h2 className="font-medium title-font text-sm text-base-content mb-1 tracking-wider">
{title}
</h2>
{children}
@@ -55,8 +55,8 @@ export function TextSection({
<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 },
"border border-base-content/20 rounded px-2 py-1 mt-2 w-3/4 min-w-48 max-w-xl",
disabled ? "cursor-not-allowed bg-base-200" : "bg-base-100",
)}
value={internalValue}
onChange={(e) => {
+1 -1
View File
@@ -1,6 +1,6 @@
export const USE_LOGGER = true as boolean;
export const CLIENT_ID = "Ov23lilAc5biyyRY0K1u";
export const CLIENT_ID = "Ov23liZSweT9LJrck9i8";
export const HOST = window.location.host;
+103 -2
View File
@@ -1,14 +1,115 @@
/* ./src/index.css */
@import "tailwindcss";
@plugin "daisyui";
@plugin "daisyui" {
themes:
light --default,
dark --prefersdark;
}
/*
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);
}
@plugin "daisyui/theme" {
name: "dark";
--color-primary: #2f80ed;
--color-error: var(--color-red-900);
--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;
}
body {
margin: 0;
font-family: "Segoe UI", Ubuntu, Sans-Serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--color-base-100);
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.
*/
body,
.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 global
* 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-neutral-content);
}
.btn-error:hover {
color: var(--color-neutral-content);
background-color: color-mix(in oklab, var(--color-error), #f00 40%);
}
input:focus,
select:focus {
outline: 0;
border: var(--color-primary) solid 1px;
}
/* Respect users who prefer reduced motion. */
@media (prefers-reduced-motion: reduce) {
:root {
--transition-fast: 0ms;
--transition-surface: 0ms;
}
}
@@ -4,75 +4,45 @@
"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 } }
{ "name": "classicLogo", "stargazerCount": 1 },
{ "name": "creativechat", "stargazerCount": 1 },
{ "name": "verlet_tests", "stargazerCount": 1 },
{ "name": "blade-checkout-ui-demo", "stargazerCount": 0 },
{ "name": "tslint-plugin-analytics", "stargazerCount": 0 },
{ "name": "stargazer", "stargazerCount": 0 },
{ "name": "react-smooth-range-input", "stargazerCount": 0 },
{ "name": "csb-git-integration-bug", "stargazerCount": 0 },
{ "name": "covid-nepal-web", "stargazerCount": 0 },
{ "name": "shrijan00003.github.io", "stargazerCount": 0 },
{ "name": "TheBigCB.com", "stargazerCount": 0 },
{ "name": "html2commonmark", "stargazerCount": 0 },
{ "name": "test-myaction", "stargazerCount": 0 },
{ "name": "react-emoji-render", "stargazerCount": 0 },
{ "name": "sajilo-editor", "stargazerCount": 0 },
{ "name": "coder-aayush.github.io", "stargazerCount": 0 },
{ "name": "codesandbox-client", "stargazerCount": 0 },
{ "name": "product-sans", "stargazerCount": 0 },
{ "name": "NodejsBlogSystem", "stargazerCount": 0 },
{ "name": "native-javascript", "stargazerCount": 0 },
{ "name": "p5.js", "stargazerCount": 0 },
{ "name": "gatsbytutorials.com", "stargazerCount": 0 },
{ "name": "hello-world-docker-action", "stargazerCount": 0 },
{ "name": "glugmvit.github.io", "stargazerCount": 0 },
{ "name": "react-map-interaction", "stargazerCount": 0 },
{ "name": "react-external-image", "stargazerCount": 0 },
{ "name": "gatsby-boilerplate", "stargazerCount": 0 },
{ "name": "YouBackup", "stargazerCount": 0 },
{ "name": "npm-expansions", "stargazerCount": 0 },
{ "name": "Toy-Neural-Network-JS", "stargazerCount": 0 },
{ "name": "LSystemCreator", "stargazerCount": 0 },
{ "name": "Happy-2019", "stargazerCount": 0 },
{ "name": "jquery-navScroll", "stargazerCount": 0 },
{ "name": "strml.net", "stargazerCount": 0 },
{ "name": "Logo", "stargazerCount": 0 },
{ "name": "website", "stargazerCount": 0 },
{ "name": "anuraghazra.github.io-old", "stargazerCount": 0 },
{ "name": "Quintus", "stargazerCount": 0 },
{ "name": "Electron_experiments", "stargazerCount": 0 }
],
"pageInfo": {
"hasNextPage": false,
+1 -3
View File
@@ -7,9 +7,7 @@
"isPrivate": false,
"isArchived": false,
"isTemplate": false,
"stargazers": {
"totalCount": 76994
},
"stargazerCount": 76994,
"description": ":zap: Dynamically generated stats for your github readmes",
"primaryLanguage": {
"color": "#f1e05a",
+100 -597
View File
@@ -36,606 +36,109 @@
"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": "github-readme-stats", "stargazerCount": 77025 },
{ "name": "Verly.js", "stargazerCount": 696 },
{ "name": "anuraghazra.github.io", "stargazerCount": 608 },
{ "name": "anuraghazra", "stargazerCount": 376 },
{ "name": "type-trident", "stargazerCount": 359 },
{ "name": "BugVilla", "stargazerCount": 232 },
{ "name": "convoychat", "stargazerCount": 214 },
{ "name": "CanvasFun", "stargazerCount": 211 },
{ "name": "EvolutionAquerium", "stargazerCount": 192 },
{ "name": "typelevel-parser", "stargazerCount": 147 },
{ "name": "VerlyRangeSlider", "stargazerCount": 135 },
{ "name": "vscode-strip-ts-copy", "stargazerCount": 72 },
{ "name": "react-folder-tree", "stargazerCount": 54 },
{ "name": "Atomic.js", "stargazerCount": 53 },
{ "name": "ToyLang", "stargazerCount": 51 },
{ "name": "design-patterns-everyday", "stargazerCount": 46 },
{ "name": "gatsby", "stargazerCount": 36 },
{ "name": "Slime", "stargazerCount": 35 },
{ "name": "Candy.js", "stargazerCount": 34 },
{ "name": "ShaderExpo", "stargazerCount": 33 },
{ "name": "Nothing", "stargazerCount": 32 },
{ "name": "peerlist-profile-action", "stargazerCount": 30 },
{ "name": "facebook-reaction-animation", "stargazerCount": 29 },
{ "name": "react-stripe-dropdown", "stargazerCount": 27 },
{ "name": "QuickerPoll", "stargazerCount": 27 },
{ "name": "playground-format-on-save", "stargazerCount": 24 },
{ "name": "ParticleBrush", "stargazerCount": 23 },
{ "name": "graphql-oauth", "stargazerCount": 21 },
{ "name": "parasites", "stargazerCount": 21 },
{ "name": "twitter-banner-jokes", "stargazerCount": 18 },
{ "name": "verly-cpp", "stargazerCount": 16 },
{ "name": "gatsby-github-issues-blog", "stargazerCount": 16 },
{ "name": "NikeHyperAce", "stargazerCount": 12 },
{ "name": "rust-particles", "stargazerCount": 11 },
{ "name": "VerletDrawing", "stargazerCount": 10 },
{ "name": "graphql-jwt-auth-test", "stargazerCount": 9 },
{
"name": "anuraghazra.github.io-old-react",
"stargazers": {
"totalCount": 9
}
"stargazerCount": 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
}
}
{ "name": "react18-ssr-test", "stargazerCount": 8 },
{ "name": "GyroDodge", "stargazerCount": 8 },
{ "name": "go-phyllotaxis", "stargazerCount": 6 },
{ "name": "graphql-oauth-client", "stargazerCount": 6 },
{ "name": "code-runner-action", "stargazerCount": 6 },
{ "name": "GithubActionsPlayground", "stargazerCount": 6 },
{ "name": "Loader.js", "stargazerCount": 6 },
{ "name": "ts-essentials", "stargazerCount": 5 },
{ "name": "chakra-ui", "stargazerCount": 5 },
{ "name": "text-to-handwriting", "stargazerCount": 5 },
{ "name": "build-your-own-x", "stargazerCount": 5 },
{ "name": "nodejs-dynamic-restapi", "stargazerCount": 5 },
{ "name": "DevYouTubeList", "stargazerCount": 5 },
{ "name": "FlockingBlackHole", "stargazerCount": 5 },
{ "name": "webgpu-boids", "stargazerCount": 4 },
{ "name": "test-githubblock", "stargazerCount": 4 },
{ "name": "renderless-components", "stargazerCount": 4 },
{ "name": "reakit", "stargazerCount": 4 },
{ "name": "micro-open-graph", "stargazerCount": 4 },
{ "name": "rest-and-graphql", "stargazerCount": 4 },
{ "name": "abell-issues-blog", "stargazerCount": 4 },
{ "name": "VanillaMVC", "stargazerCount": 4 },
{ "name": "covid19-sim", "stargazerCount": 4 },
{ "name": "YoutubeDownloader", "stargazerCount": 4 },
{ "name": "verlet.js", "stargazerCount": 4 },
{ "name": "test-github-ts", "stargazerCount": 3 },
{ "name": "tw-dynamic-interpolation", "stargazerCount": 3 },
{ "name": "ts-string-interpolator", "stargazerCount": 3 },
{ "name": "AnuReact", "stargazerCount": 3 },
{ "name": "CanvasBalls", "stargazerCount": 3 },
{ "name": "circleci-test", "stargazerCount": 3 },
{ "name": "gatsby-starter-netlify-cms", "stargazerCount": 3 },
{ "name": "WebGL.js", "stargazerCount": 3 },
{ "name": "test-fork-workflow", "stargazerCount": 2 },
{ "name": "test-codesandbox-projects", "stargazerCount": 2 },
{ "name": "codesandbox-template-astro", "stargazerCount": 2 },
{ "name": "apollo", "stargazerCount": 2 },
{ "name": "api-docs", "stargazerCount": 2 },
{ "name": "Buddies-movie", "stargazerCount": 2 },
{ "name": "2k76", "stargazerCount": 2 },
{ "name": "gatsby-remark-embedder", "stargazerCount": 2 },
{ "name": "weird-hello-worlds", "stargazerCount": 2 },
{ "name": "rn-tabview-test-repro", "stargazerCount": 1 },
{ "name": "tailwindcss-jit", "stargazerCount": 1 },
{ "name": "DefinitelyTyped", "stargazerCount": 1 },
{ "name": "server-components-demo", "stargazerCount": 1 },
{ "name": "volant-vo", "stargazerCount": 1 },
{ "name": "react-spectrum", "stargazerCount": 1 },
{ "name": "webhooks-test", "stargazerCount": 1 },
{ "name": "react-showdown", "stargazerCount": 1 },
{ "name": "transcode", "stargazerCount": 1 },
{ "name": "awake-yet-action", "stargazerCount": 1 },
{ "name": "bradgarropy.com", "stargazerCount": 1 },
{ "name": "gatsby-plugin-social-banners", "stargazerCount": 1 },
{ "name": "Tour360", "stargazerCount": 1 },
{ "name": "blizard.js", "stargazerCount": 1 },
{ "name": "keep-a-changelog", "stargazerCount": 1 },
{ "name": "vulnerabilities", "stargazerCount": 1 },
{ "name": "harleydavidson", "stargazerCount": 1 },
{ "name": "awesome-webgl", "stargazerCount": 1 },
{ "name": "NewtonsInterpolation", "stargazerCount": 1 },
{ "name": "now-github-starter", "stargazerCount": 1 },
{ "name": "simplerockets", "stargazerCount": 1 }
],
"pageInfo": {
"hasNextPage": true,
+4 -2
View File
@@ -5,6 +5,7 @@ import { ToastContainer, toast } from "react-toastify";
import { getUserMetadata } from "../../api/user";
import { clearAxiosCache } from "../../axios-override";
import type { StageIndex } from "../../models/Stage";
import { useTheme } from "../../redux/selectors/themeSelectors";
import {
useIsAuthenticated,
useUserKey,
@@ -56,6 +57,7 @@ export function AppTrends() {
const userKey = useUserKey();
const userToken = useUserToken();
const isAuthenticated = useIsAuthenticated();
const { isDark } = useTheme();
const [stage, setStage] = useState<StageIndex>(isAuthenticated ? 1 : 0);
@@ -116,9 +118,9 @@ export function AppTrends() {
return (
<div className="min-h-screen flex flex-col">
<Header currStageIndex={stage} onStageIndexChange={setStage} />
<section className="bg-white text-gray-700 flex-grow">
<section className="bg-base-100 text-base-content flex-grow">
<HomeScreen stage={stage} setStage={setStage} />
<ToastContainer />
<ToastContainer theme={isDark ? "dark" : "light"} />
</section>
</div>
);
+15 -11
View File
@@ -2,6 +2,7 @@ import type { JSX } from "react";
import { FaGithub as GithubIcon } from "react-icons/fa";
import appIcon from "../../assets/appLogo64.png";
import { ThemePicker } from "../../components/Generic/ThemePicker";
import { ProgressBar } from "../../components/Home/Progress";
import { STAGE_LABELS } from "../../models/Stage";
import type { StageIndex } from "../../models/Stage";
@@ -19,31 +20,34 @@ export function Header({
}: HeaderProps): JSX.Element {
return (
<>
<div className="text-gray-100 bg-gray-800 shadow-md z-50">
<div
className="text-neutral-content shadow-md z-50"
style={{
backgroundColor:
"color-mix(in oklab, var(--color-primary), #000 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"
className="flex items-center title-font font-medium text-neutral-content 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">
{/* Star on GitHub + theme toggle */}
<div className="flex ml-auto items-center gap-2 text-base justify-center">
<a
href="https://github.com/stats-organization/github-stats-extended"
target="_blank"
rel="noopener noreferrer"
className="rounded-[0.25rem] shadow bg-neutral-content hover:opacity-90 text-neutral px-3 py-1 flex items-center"
>
<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>
Star on
<GithubIcon className="ml-1.5 w-5 h-5" />
</a>
<ThemePicker />
</div>
</div>
</div>
+12 -5
View File
@@ -19,6 +19,7 @@ import {
import { CardType } from "../../models/CardType";
import { STAGE_LABELS } from "../../models/Stage";
import type { StageIndex } from "../../models/Stage";
import { useTheme } from "../../redux/selectors/themeSelectors";
import {
useIsAuthenticated,
usePrivateAccess,
@@ -83,7 +84,9 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
const [enableAnimations, setEnableAnimations] = useState(true);
const [usePercent, setUsePercent] = useState(false);
const [theme, setTheme] = useState("default");
const { isDark } = useTheme();
const themeParam = isDark ? "&theme=github_dark" : "";
const [theme, setTheme] = useState(isDark ? "dark" : "default");
const handleCardTypeChange = (cardType: CardType) => {
if (cardType === CardType.TOP_LANGS) {
@@ -230,11 +233,14 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
}
return (
<div ref={contentSectionRef} className="h-full px-2 lg:px-8 text-gray-600">
<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-gray-600 font-semibold">
<h1 className="text-2xl text-base-content/70 font-semibold">
{STAGE_LABELS[stage].title}
</h1>
<div>
@@ -245,7 +251,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
<a
href={`https://github.com/${userId}`}
target="_blank"
className="text-blue-500 hover:underline font-semibold"
className="text-primary hover:underline font-semibold"
>
{userId}
</a>
@@ -328,7 +334,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
setWakatimeUser={setWakatimeUser}
usePercent={usePercent}
setUsePercent={setUsePercent}
fullSuffix={fullSuffix}
fullSuffix={fullSuffix + themeParam}
setStage={setStage}
/>
)}
@@ -382,6 +388,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
return "";
}
})()}
theme={theme}
themeSuffix={themeSuffix}
guestHint={
isAuthenticated
@@ -109,7 +109,7 @@ export function CustomizeStage({
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 +126,7 @@ export function CustomizeStage({
e.preventDefault();
setStage(0);
}}
className="underline text-blue-900"
className="underline text-primary"
>
log in
</a>{" "}
@@ -170,7 +170,7 @@ export function CustomizeStage({
e.preventDefault();
setStage(0);
}}
className="underline text-blue-900"
className="underline text-primary"
>
log in
</a>{" "}
@@ -214,7 +214,7 @@ export function CustomizeStage({
e.preventDefault();
setStage(0);
}}
className="underline text-blue-900"
className="underline text-primary"
>
log in
</a>{" "}
@@ -251,7 +251,7 @@ export function CustomizeStage({
<a
href="https://wakatime.com/"
target="_blank"
className="underline text-blue-900"
className="underline text-primary"
>
WakaTime
</a>{" "}
@@ -411,7 +411,7 @@ export function CustomizeStage({
<a
href="https://github.com/stats-organization/github-stats-extended/blob/master/docs/advanced_documentation.md"
target="_blank"
className="underline text-blue-900"
className="underline text-primary"
>
customization documentation
</a>{" "}
@@ -1,15 +1,17 @@
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 { getCardThemeBackdrop } from "../../../components/Card/themeBackdrop";
import { Button } from "../../../components/Generic/Button";
import { HOST } from "../../../constants";
import { useTheme } from "../../../redux/selectors/themeSelectors";
interface DisplayStageProps {
filename: string;
link: string;
theme: string;
themeSuffix: string;
guestHint: string | null;
}
@@ -17,9 +19,12 @@ interface DisplayStageProps {
export function DisplayStage({
filename,
link,
theme,
themeSuffix,
guestHint,
}: DisplayStageProps): JSX.Element {
const { isDark } = useTheme();
const downloadPNG = () => {
saveSvgAsPng(
document.getElementById("svgWrapper")?.shadowRoot?.firstElementChild
@@ -61,7 +66,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 +87,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 +99,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`}
stage={4}
className="flex justify-center"
/>
</div>
</div>
@@ -45,7 +45,7 @@ export function LoginAccountDeleteModal(
<div className="fixed left-0 top-0 w-full h-full">
<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 +57,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>
@@ -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>
@@ -7,6 +7,7 @@ import {
DEMO_USER,
DEMO_WAKATIME_USER,
} from "../../../../constants";
import { useTheme } from "../../../../redux/selectors/themeSelectors";
const cards: Array<{ demoImageSrc: string }> = [
{
@@ -39,6 +40,10 @@ function getCardXPosition(cardIndex: number): number {
}
export function LoginBoxDemoCards(): JSX.Element {
const { isDark } = useTheme();
// Show dark-themed demo cards in dark mode so they fit the surroundings.
const themeParam = isDark ? "&theme=github_dark" : "";
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">
@@ -52,7 +57,11 @@ export function LoginBoxDemoCards(): JSX.Element {
marginBottom: "1%",
}}
>
<CardImage imageSrc={card.demoImageSrc} compact={false} stage={0} />
<CardImage
imageSrc={`${card.demoImageSrc}${themeParam}`}
compact={false}
stage={0}
/>
</div>
))}
</div>
@@ -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>
@@ -9,6 +9,7 @@ import {
DEMO_WAKATIME_USER,
} from "../../../constants";
import { CardType } from "../../../models/CardType";
import { useTheme } from "../../../redux/selectors/themeSelectors";
import { useUserId } from "../../../redux/selectors/userSelectors";
interface SelectCardStageProps {
@@ -21,6 +22,9 @@ export function SelectCardStage({
onCardTypeChange,
}: SelectCardStageProps): JSX.Element {
const userId = useUserId(DEMO_USER);
const { isDark } = useTheme();
// Show dark-themed demo cards in dark mode so they fit the surroundings.
const themeParam = isDark ? "&theme=github_dark" : "";
const options = useMemo<
Array<{
@@ -34,37 +38,37 @@ export function SelectCardStage({
{
title: "GitHub Stats Card",
description: "your overall GitHub statistics",
demoImageSrc: `?username=${userId}&include_all_commits=true`,
demoImageSrc: `?username=${userId}&include_all_commits=true${themeParam}`,
cardType: CardType.STATS,
},
{
title: "Top Languages Card",
description: "your most frequently used languages",
demoImageSrc: `/top-langs?username=${userId}&langs_count=4`,
demoImageSrc: `/top-langs?username=${userId}&langs_count=4${themeParam}`,
cardType: CardType.TOP_LANGS,
},
{
title: "GitHub Extra Pin",
description:
"pin more than 6 repositories in your profile using a GitHub profile readme",
demoImageSrc: `/pin?repo=${DEMO_REPO}`,
demoImageSrc: `/pin?repo=${DEMO_REPO}${themeParam}`,
cardType: CardType.PIN,
},
{
title: "GitHub Gist Pin",
description:
"pin gists in your GitHub profile using a GitHub profile readme",
demoImageSrc: `/gist?id=${DEMO_GIST}`,
demoImageSrc: `/gist?id=${DEMO_GIST}${themeParam}`,
cardType: CardType.GIST,
},
{
title: "WakaTime Stats Card",
description: "your coding activity from WakaTime",
demoImageSrc: `/wakatime?username=${DEMO_WAKATIME_USER}&langs_count=6&card_width=450`,
demoImageSrc: `/wakatime?username=${DEMO_WAKATIME_USER}&langs_count=6&card_width=450${themeParam}`,
cardType: CardType.WAKATIME,
},
],
[userId],
[userId, themeParam],
);
return (
+42 -25
View File
@@ -2,6 +2,11 @@ import { themes } from "@stats-organization/github-readme-stats-core";
import type { JSX } from "react";
import { Card } from "../../../components/Card/Card";
import {
getCardThemeBackdrop,
getThemeSortRank,
} from "../../../components/Card/themeBackdrop";
import { useTheme } from "../../../redux/selectors/themeSelectors";
const excludedThemes = [
"merko",
@@ -12,9 +17,10 @@ const excludedThemes = [
"holi",
];
const themeList = Object.keys(themes).filter(
(myTheme) => !excludedThemes.includes(myTheme),
);
// Light themes first, adaptive themes in the middle, dark themes last.
const themeList = Object.keys(themes)
.filter((myTheme) => !excludedThemes.includes(myTheme))
.sort((a, b) => getThemeSortRank(a) - getThemeSortRank(b));
interface ThemeStageProps {
fullSuffix: string;
@@ -27,38 +33,49 @@ export function ThemeStage({
fullSuffix,
onThemeChange,
}: ThemeStageProps): JSX.Element {
const { isDark } = useTheme();
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>
))}
{themeList.map((myTheme) => {
const themeColors = themes[myTheme as keyof typeof themes];
return (
<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}
backgroundColor={getCardThemeBackdrop(myTheme, isDark)}
titleColor={`#${
myTheme === "ambient_gradient" && !isDark
? (themes["ambient_gradient"].bg_color.split(",")[1] ?? "")
: themeColors.title_color
}`}
/>
</button>
);
})}
</div>
<div className="pl-10 pr-10">
For more theme options check the{" "}
{"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"
className="underline text-primary"
>
customization documentation
</a>{" "}
after you copied your card URL in step 5.
</a>
{" after you copied your card URL in step 5."}
</div>
</>
);
@@ -0,0 +1,27 @@
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { isDarkTheme, setTheme as setThemeAction } from "../slices/theme";
import type { StoreState } from "../store";
/**
* Reads the active DaisyUI theme from the Redux store and exposes a setter
* that persists the choice and updates the `data-theme` attribute.
*/
export function useTheme(): {
theme: string;
isDark: boolean;
setTheme: (theme: string) => void;
} {
const dispatch = useDispatch();
const theme = useSelector((state: StoreState) => state.theme.theme);
const setTheme = useCallback(
(next: string) => {
dispatch(setThemeAction(next));
},
[dispatch],
);
return { theme, isDark: isDarkTheme(theme), setTheme };
}
+69
View File
@@ -0,0 +1,69 @@
import { createSlice } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
interface ThemeOption {
/** DaisyUI theme name, used as the `data-theme` value. */
name: string;
label: string;
/** Whether the theme has a dark background (drives toast styling, etc.). */
isDark: boolean;
}
/**
* Themes exposed in the theme picker.
* Keep in sync with the `themes:` list in `index.css`.
*/
export const THEMES: ReadonlyArray<ThemeOption> = [
{ name: "light", label: "Light", isDark: false },
{ name: "dark", label: "Dark", isDark: true },
];
const DEFAULT_LIGHT_THEME = "light";
const DEFAULT_DARK_THEME = "dark";
function isValidTheme(name: string | null): name is string {
return !!name && THEMES.some((theme) => theme.name === name);
}
export function isDarkTheme(name: string): boolean {
return THEMES.find((theme) => theme.name === name)?.isDark ?? false;
}
/**
* @public
* Exported so the inferred store type can name it (see the note in `user.ts`).
*/
export interface ThemeState {
theme: string;
}
function getInitialTheme(): string {
const stored = localStorage.getItem("theme");
if (isValidTheme(stored)) {
return stored;
}
return window.matchMedia("(prefers-color-scheme: dark)").matches
? DEFAULT_DARK_THEME
: DEFAULT_LIGHT_THEME;
}
const initialState: ThemeState = {
theme: getInitialTheme(),
};
const themeSlice = createSlice({
name: "theme",
initialState,
reducers: {
setTheme: (state, action: PayloadAction<string>) => {
const theme = action.payload;
localStorage.setItem("theme", theme);
document.documentElement.setAttribute("data-theme", theme);
state.theme = theme;
},
},
});
export const { setTheme } = themeSlice.actions;
export default themeSlice.reducer;
+2
View File
@@ -3,11 +3,13 @@ import { configureStore } from "@reduxjs/toolkit";
import { USE_LOGGER } from "../constants";
import { loggerMiddleware } from "./logger";
import theme from "./slices/theme";
import user from "./slices/user";
const store = configureStore({
reducer: {
user,
theme,
},
middleware: (getDefaultMiddleware) => {
const middleware = getDefaultMiddleware();
+4
View File
@@ -150,6 +150,10 @@ export default defineConfig(
},
],
// Keep `@param props.x` doc names in sync with the destructured
// parameters so they cannot silently drift on rename.
"jsdoc/check-param-names": "error",
// We don't need this we have typescript
"jsdoc/require-returns": "off",
"jsdoc/require-returns-description": "off",
+2 -2
View File
@@ -7,7 +7,7 @@
"@eslint/compat": "2.1.0",
"@eslint/js": "10.0.1",
"@playwright/test": "1.60.0",
"@types/node": "24.13.1",
"@types/node": "24.13.2",
"@vitest/coverage-v8": "catalog:default",
"eslint": "10.4.1",
"eslint-import-resolver-typescript": "4.4.5",
@@ -20,7 +20,7 @@
"knip": "6.16.1",
"lint-staged": "17.0.7",
"prettier": "3.8.4",
"turbo": "2.9.17",
"turbo": "2.9.18",
"typescript": "6.0.3",
"typescript-eslint": "8.61.0",
"vitest": "catalog:default"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@stats-organization/github-readme-stats-core",
"version": "2.1.2",
"version": "2.1.4",
"type": "module",
"homepage": "https://github-stats-extended.vercel.app/frontend",
"bugs": {
-2
View File
@@ -1,5 +1,3 @@
// @ts-check
import { renderGistCard } from "../cards/gist.js";
import {
MissingParamError,
-2
View File
@@ -1,5 +1,3 @@
// @ts-check
import { renderStatsCard } from "../cards/stats.js";
import {
MissingParamError,
-2
View File
@@ -1,5 +1,3 @@
// @ts-check
import { renderRepoCard } from "../cards/repo.js";
import {
MissingParamError,
-2
View File
@@ -1,5 +1,3 @@
// @ts-check
import { renderTopLanguages } from "../cards/top-languages.js";
import {
MissingParamError,
-2
View File
@@ -1,5 +1,3 @@
// @ts-check
import { renderWakatimeCard } from "../cards/wakatime.js";
import {
MissingParamError,
-2
View File
@@ -1,5 +1,3 @@
// @ts-check
import { default as Card } from "../common/Card.js";
import { getCardColors } from "../common/color.js";
import { kFormatter, wrapTextMultiline } from "../common/fmt.js";
-2
View File
@@ -1,5 +1,3 @@
// @ts-check
import { Card } from "../common/Card.js";
import { I18n } from "../common/I18n.js";
import { getCardColors } from "../common/color.js";
-2
View File
@@ -1,5 +1,3 @@
// @ts-check
import { Card } from "../common/Card.js";
import { I18n } from "../common/I18n.js";
import { getCardColors } from "../common/color.js";
-2
View File
@@ -1,5 +1,3 @@
// @ts-check
import { Card } from "../common/Card.js";
import { I18n } from "../common/I18n.js";
import { fallbackColor, getCardColors } from "../common/color.js";
-2
View File
@@ -1,5 +1,3 @@
// @ts-check
import { Card } from "../common/Card.js";
import { I18n } from "../common/I18n.js";
import { getCardColors } from "../common/color.js";
@@ -1,25 +1,46 @@
// @ts-check
import { encodeHTML } from "./html.js";
import { flexLayout } from "./render.js";
interface CardColors {
/** Card title color. */
titleColor?: string;
/** Card text color. */
textColor?: string;
/** Card icon color. */
iconColor?: string;
/** Card background color. */
bgColor?: string | Array<string>;
/** Card border color. */
borderColor?: string;
}
class Card {
width: number;
height: number;
hideBorder: boolean;
hideTitle: boolean;
border_radius: number;
colors: CardColors;
title: string;
css: string;
paddingX: number;
paddingY: number;
titlePrefixIcon: string | undefined;
animations: boolean;
a11yTitle: string;
a11yDesc: string;
/**
* Creates a new card instance.
*
* @param {object} args Card arguments.
* @param {number=} args.width Card width.
* @param {number=} args.height Card height.
* @param {number=} args.border_radius Card border radius.
* @param {string=} args.customTitle Card custom title.
* @param {string=} args.defaultTitle Card default title.
* @param {string=} args.titlePrefixIcon Card title prefix icon.
* @param {object} [args.colors={}] Card colors arguments.
* @param {string=} args.colors.titleColor Card title color.
* @param {string=} args.colors.textColor Card text color.
* @param {string=} args.colors.iconColor Card icon color.
* @param {string|string[]=} args.colors.bgColor Card background color.
* @param {string=} args.colors.borderColor Card border color.
* @param props Card arguments.
* @param props.width Card width.
* @param props.height Card height.
* @param props.border_radius Card border radius.
* @param props.colors Card colors arguments.
* @param props.customTitle Card custom title.
* @param props.defaultTitle Card default title.
* @param props.titlePrefixIcon Card title prefix icon.
*/
constructor({
width = 100,
@@ -29,6 +50,14 @@ class Card {
customTitle,
defaultTitle = "",
titlePrefixIcon,
}: {
width?: number;
height?: number;
border_radius?: number;
colors?: CardColors;
customTitle?: string;
defaultTitle?: string;
titlePrefixIcon?: string;
}) {
this.width = width;
this.height = height;
@@ -40,10 +69,9 @@ class Card {
// returns theme based colors with proper overrides and defaults
this.colors = colors;
this.title =
customTitle === undefined
? encodeHTML(defaultTitle)
: encodeHTML(customTitle);
this.title = encodeHTML(
customTitle === undefined ? defaultTitle : customTitle,
);
this.css = "";
@@ -55,45 +83,44 @@ class Card {
this.a11yDesc = "";
}
/**
* @returns {void}
*/
disableAnimations() {
disableAnimations(): void {
this.animations = false;
}
/**
* @param {Object} props The props object.
* @param {string} props.title Accessibility title.
* @param {string} props.desc Accessibility description.
* @returns {void}
* @param props The props object.
* @param props.title Accessibility title.
* @param props.desc Accessibility description.
*/
setAccessibilityLabel({ title, desc }) {
setAccessibilityLabel({
title,
desc,
}: {
title: string;
desc: string;
}): void {
this.a11yTitle = title;
this.a11yDesc = desc;
}
/**
* @param {string} value The CSS to add to the card.
* @returns {void}
* @param value The CSS to add to the card.
*/
setCSS(value) {
setCSS(value: string): void {
this.css = value;
}
/**
* @param {boolean} value Whether to hide the border or not.
* @returns {void}
* @param value Whether to hide the border or not.
*/
setHideBorder(value) {
setHideBorder(value: boolean): void {
this.hideBorder = value;
}
/**
* @param {boolean} value Whether to hide the title or not.
* @returns {void}
* @param value Whether to hide the title or not.
*/
setHideTitle(value) {
setHideTitle(value: boolean): void {
this.hideTitle = value;
if (value) {
this.height -= 30;
@@ -101,17 +128,16 @@ class Card {
}
/**
* @param {string} text The title to set.
* @returns {void}
* @param text The title to set.
*/
setTitle(text) {
setTitle(text: string): void {
this.title = text;
}
/**
* @returns {string} The rendered card title.
* @returns The rendered card title.
*/
renderTitle() {
renderTitle(): string {
const titleText = `
<text
x="0"
@@ -131,7 +157,7 @@ class Card {
width="16"
height="16"
>
${this.titlePrefixIcon}
${String(this.titlePrefixIcon)}
</svg>
`;
return `
@@ -148,9 +174,9 @@ class Card {
}
/**
* @returns {string} The rendered card gradient.
* @returns The rendered card gradient.
*/
renderGradient() {
renderGradient(): string {
if (typeof this.colors.bgColor !== "object") {
return "";
}
@@ -161,13 +187,15 @@ class Card {
<defs>
<linearGradient
id="gradient"
gradientTransform="rotate(${this.colors.bgColor[0]})"
gradientTransform="rotate(${String(this.colors.bgColor[0])})"
gradientUnits="userSpaceOnUse"
>
${gradients.map((grad, index) => {
let offset = (index * 100) / (gradients.length - 1);
return `<stop offset="${offset}%" stop-color="#${grad}" />`;
})}
${gradients
.map((grad, index) => {
const offset = (index * 100) / (gradients.length - 1);
return `<stop offset="${offset}%" stop-color="#${grad}" />`;
})
.join(",")}
</linearGradient>
</defs>
`
@@ -177,9 +205,9 @@ class Card {
/**
* Retrieves css animations for a card.
*
* @returns {string} Animation css.
* @returns Animation css.
*/
getAnimations = () => {
getAnimations = (): string => {
return `
/* Animations */
@keyframes scaleInAnimation {
@@ -202,10 +230,10 @@ class Card {
};
/**
* @param {string} body The inner body of the card.
* @returns {string} The rendered card.
* @param body The inner body of the card.
* @returns The rendered card.
*/
render(body) {
render(body: string): string {
return `
<svg
width="${this.width}"
@@ -221,7 +249,7 @@ class Card {
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: ${this.colors.titleColor};
fill: ${String(this.colors.titleColor)};
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
@@ -232,9 +260,9 @@ class Card {
${this.getAnimations()}
${
this.animations === false
? `* { animation-duration: 0s !important; animation-delay: 0s !important; }`
: ""
this.animations
? ""
: `* { animation-duration: 0s !important; animation-delay: 0s !important; }`
}
</style>
@@ -246,12 +274,12 @@ class Card {
y="0.5"
rx="${this.border_radius}"
height="99%"
stroke="${this.colors.borderColor}"
stroke="${String(this.colors.borderColor)}"
width="${this.width - 1}"
fill="${
typeof this.colors.bgColor === "object"
? "url(#gradient)"
: this.colors.bgColor
: String(this.colors.bgColor)
}"
stroke-opacity="${this.hideBorder ? 0 : 1}"
/>
-42
View File
@@ -1,42 +0,0 @@
// @ts-check
const FALLBACK_LOCALE = "en";
/**
* I18n translation class.
*/
class I18n {
/**
* Constructor.
*
* @param {Object} options Options.
* @param {string=} options.locale Locale.
* @param {any} options.translations Translations.
*/
constructor({ locale, translations }) {
this.locale = locale || FALLBACK_LOCALE;
this.translations = translations;
}
/**
* Get translation.
*
* @param {string} str String to translate.
* @returns {string} Translated string.
*/
t(str) {
if (!this.translations[str]) {
throw new Error(`${str} Translation string not found`);
}
if (!this.translations[str][this.locale]) {
throw new Error(
`'${str}' translation not found for locale '${this.locale}'`,
);
}
return this.translations[str][this.locale];
}
}
export { I18n };
+49
View File
@@ -0,0 +1,49 @@
const FALLBACK_LOCALE = "en";
/**
* I18n translation class.
*/
class I18n<Translations extends Record<string, Record<string, string>>> {
locale: string;
translations: Translations;
/**
* @param props Constructor arguments.
* @param props.locale Locale.
* @param props.translations Translations.
*/
constructor({
locale,
translations,
}: {
locale?: string;
translations: Translations;
}) {
this.locale = locale || FALLBACK_LOCALE;
this.translations = translations;
}
/**
* Get translation.
*
* @param str String to translate.
* @returns Translated string.
*/
t(str: keyof Translations & string): string {
const translation = this.translations[str];
if (!translation) {
throw new Error(`${str} Translation string not found`);
}
const localized = translation[this.locale];
if (!localized) {
throw new Error(
`'${str}' translation not found for locale '${this.locale}'`,
);
}
return localized;
}
}
export { I18n };
@@ -1,14 +1,12 @@
// @ts-check
import { themes } from "../themes/index.js";
/**
* Checks if a string is a valid hex color.
*
* @param {string} hexColor String to check.
* @returns {boolean} True if the given string is a valid hex color.
* @param hexColor String to check.
* @returns True if the given string is a valid hex color.
*/
const isValidHexColor = (hexColor) => {
const isValidHexColor = (hexColor: string): boolean => {
return new RegExp(
/^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4})$/,
).test(hexColor);
@@ -17,10 +15,10 @@ const isValidHexColor = (hexColor) => {
/**
* Check if the given string is a valid gradient.
*
* @param {string[]} colors Array of colors.
* @returns {boolean} True if the given string is a valid gradient.
* @param colors Array of colors.
* @returns True if the given string is a valid gradient.
*/
const isValidGradient = (colors) => {
const isValidGradient = (colors: Array<string>): boolean => {
return (
colors.length > 2 &&
colors.slice(1).every((color) => isValidHexColor(color))
@@ -30,48 +28,50 @@ const isValidGradient = (colors) => {
/**
* Retrieves a gradient if color has more than one valid hex codes else a single color.
*
* @param {string} color The color to parse.
* @param {string | string[]} fallbackColor The fallback color.
* @returns {string | string[]} The gradient or color.
* @param color The color to parse.
* @param fallbackColor The fallback color.
* @returns The gradient or color.
*/
const fallbackColor = (color, fallbackColor) => {
let gradient = null;
let colors = color ? color.split(",") : [];
const fallbackColor = (
color: string | undefined,
fallbackColor: string | Array<string>,
): string | Array<string> => {
const colors = color ? color.split(",") : [];
if (colors.length > 1 && isValidGradient(colors)) {
gradient = colors;
return colors;
}
return (
(gradient ? gradient : isValidHexColor(color) && `#${color}`) ||
fallbackColor
);
if (color !== undefined && isValidHexColor(color)) {
return `#${color}`;
}
return fallbackColor;
};
/**
* Object containing card colors.
* @typedef {{
* titleColor: string;
* iconColor: string;
* textColor: string;
* bgColor: string | string[];
* borderColor: string;
* ringColor: string;
* }} CardColors
*/
interface CardColors {
titleColor: string;
iconColor: string;
textColor: string;
bgColor: string | Array<string>;
borderColor: string;
ringColor: string;
}
/**
* Returns theme based colors with proper overrides and defaults.
*
* @param {Object} args Function arguments.
* @param {string=} args.title_color Card title color.
* @param {string=} args.text_color Card text color.
* @param {string=} args.icon_color Card icon color.
* @param {string=} args.bg_color Card background color.
* @param {string=} args.border_color Card border color.
* @param {string=} args.ring_color Card ring color.
* @param {string=} args.theme Card theme.
* @returns {CardColors} Card colors.
* @param props Function arguments.
* @param props.title_color Card title color.
* @param props.text_color Card text color.
* @param props.icon_color Card icon color.
* @param props.bg_color Card background color.
* @param props.border_color Card border color.
* @param props.ring_color Card ring color.
* @param props.theme Card theme.
* @returns Card colors.
*/
const getCardColors = ({
title_color,
@@ -81,19 +81,26 @@ const getCardColors = ({
border_color,
ring_color,
theme,
}) => {
const defaultTheme = themes["default"];
const isThemeProvided =
theme !== null && theme !== undefined && theme in themes;
}: {
title_color?: string;
text_color?: string;
icon_color?: string;
bg_color?: string;
border_color?: string;
ring_color?: string;
theme?: string;
}): CardColors => {
const defaultTheme = themes.default;
const isThemeProvided = theme !== undefined && theme in themes;
// @ts-ignore
const selectedTheme = isThemeProvided ? themes[theme] : defaultTheme;
const selectedTheme = isThemeProvided
? themes[theme as keyof typeof themes]
: defaultTheme;
const defaultBorderColor =
"border_color" in selectedTheme
? selectedTheme.border_color
: // @ts-ignore
defaultTheme.border_color;
: defaultTheme.border_color;
// get the color provided by the user else the theme color
// finally if both colors are invalid fallback to default theme
@@ -104,11 +111,8 @@ const getCardColors = ({
// get the color provided by the user else the theme color
// finally if both colors are invalid we use the titleColor
const ringColor = fallbackColor(
// @ts-ignore
ring_color || selectedTheme.ring_color,
titleColor,
);
// NOTE: no built-in theme defines `ring_color`, so it falls back to the title color.
const ringColor = fallbackColor(ring_color, titleColor);
const iconColor = fallbackColor(
icon_color || selectedTheme.icon_color,
"#" + defaultTheme.icon_color,
-78
View File
@@ -1,78 +0,0 @@
// @ts-check
/**
* @param {string | undefined} value Comma-separated string.
* @returns {string[] | undefined} Parsed string values.
*/
const parseCsv = (value) => {
if (!value) {
return undefined;
}
return value.split(",");
};
/**
* @param {Record<string, string | undefined>} env Environment variables to inspect.
* @returns {{name: string, value: string}[]} Personal access tokens found in the environment.
*/
const parsePATsFromEnv = (env) => {
return Object.keys(env)
.filter((key) => /PAT_\d*$/.exec(key))
.map((name) => ({
name,
value: env[name],
}));
};
/**
* @returns {Record<string, string | undefined>} `process.env` if available, otherwise `{}`.
*/
const getDefaultEnv = () => {
if (typeof process !== "undefined" && process?.env) {
return process.env;
}
return {};
};
/**
* @param {Partial<ReturnType<typeof loadConfigFromEnv>>} config (Partial) config values to normalize.
* @returns {{
* whitelist: string[] | undefined,
* gistWhitelist: string[] | undefined,
* excludeRepositories: string[],
* fetchMultiPageStars: string | undefined,
* pats: {name: string, value: string}[],
* }} Normalized config object with defaults applied.
*/
const normalizeConfig = (config = {}) => {
return {
whitelist: config.whitelist,
gistWhitelist: config.gistWhitelist,
excludeRepositories: config.excludeRepositories || [],
fetchMultiPageStars: config.fetchMultiPageStars,
pats: config.pats || [],
};
};
let currentConfig;
/**
* @param {Record<string, string | undefined>} env Environment variables used to build the runtime config.
*/
export const loadConfigFromEnv = (env = getDefaultEnv()) => {
const whitelist = parseCsv(env.WHITELIST);
const gistWhitelist = parseCsv(env.GIST_WHITELIST);
const excludeRepositories = parseCsv(env.EXCLUDE_REPO) || [];
currentConfig = normalizeConfig({
whitelist,
gistWhitelist,
excludeRepositories,
fetchMultiPageStars: env.FETCH_MULTI_PAGE_STARS,
pats: parsePATsFromEnv(env),
});
};
loadConfigFromEnv();
export const getConfig = () => currentConfig;
+83
View File
@@ -0,0 +1,83 @@
type Env = Record<string, string | undefined>;
interface PersonalAccessToken {
name: string;
value: string;
}
interface Config {
whitelist: Array<string> | undefined;
gistWhitelist: Array<string> | undefined;
excludeRepositories: Array<string>;
fetchMultiPageStars: string | undefined;
pats: Array<PersonalAccessToken>;
}
/**
* @param value Comma-separated string.
* @returns Parsed string values.
*/
const parseCsv = (value: string | undefined): Array<string> | undefined => {
if (!value) {
return undefined;
}
return value.split(",");
};
/**
* @param env Environment variables to inspect.
* @returns Personal access tokens found in the environment.
*/
const parsePATsFromEnv = (env: Env): Array<PersonalAccessToken> => {
return Object.keys(env)
.filter((key) => /PAT_\d*$/.exec(key))
.map((name) => ({
name,
value: env[name] ?? "",
}));
};
/**
* @returns `process.env` if available, otherwise `{}`.
*/
const getDefaultEnv = (): Env => {
const processEnv = (globalThis as { process?: { env?: Env } }).process?.env;
return processEnv ?? {};
};
/**
* @param config (Partial) config values to normalize.
* @returns Normalized config object with defaults applied.
*/
const normalizeConfig = (config: Partial<Config> = {}): Config => {
return {
whitelist: config.whitelist,
gistWhitelist: config.gistWhitelist,
excludeRepositories: config.excludeRepositories ?? [],
fetchMultiPageStars: config.fetchMultiPageStars,
pats: config.pats ?? [],
};
};
let currentConfig: Config;
/**
* @param env Environment variables used to build the runtime config.
*/
export const loadConfigFromEnv = (env: Env = getDefaultEnv()): void => {
const whitelist = parseCsv(env["WHITELIST"]);
const gistWhitelist = parseCsv(env["GIST_WHITELIST"]);
const excludeRepositories = parseCsv(env["EXCLUDE_REPO"]) ?? [];
currentConfig = normalizeConfig({
whitelist,
gistWhitelist,
excludeRepositories,
fetchMultiPageStars: env["FETCH_MULTI_PAGE_STARS"],
pats: parsePATsFromEnv(env),
});
};
loadConfigFromEnv();
export const getConfig = (): Config => currentConfig;
-9
View File
@@ -1,9 +0,0 @@
// @ts-check
/**
* Valid owner affiliations for GitHub API queries.
* @type {Array<string>}
*/
const OWNER_AFFILIATIONS = ["OWNER", "COLLABORATOR", "ORGANIZATION_MEMBER"];
export { OWNER_AFFILIATIONS };
+10
View File
@@ -0,0 +1,10 @@
/**
* Valid owner affiliations for GitHub API queries.
*/
const OWNER_AFFILIATIONS: Array<string> = [
"OWNER",
"COLLABORATOR",
"ORGANIZATION_MEMBER",
];
export { OWNER_AFFILIATIONS };
@@ -1,14 +1,12 @@
// @ts-check
import { OWNER_AFFILIATIONS } from "./constants.js";
/**
* @type {string} A general message to ask user to try again later.
* A general message to ask user to try again later.
*/
const TRY_AGAIN_LATER = "Please try again later";
/**
* @type {Object<string, string>} A map of error types to secondary error messages.
* A map of error types to secondary error messages.
*/
const SECONDARY_ERROR_MESSAGES = {
MAX_RETRY:
@@ -28,16 +26,21 @@ const SECONDARY_ERROR_MESSAGES = {
* Custom error class to handle custom GRS errors.
*/
class CustomError extends Error {
type: string;
secondaryMessage: string;
/**
* Custom error constructor.
*
* @param {string} message Error message.
* @param {string} type Error type.
* @param message Error message.
* @param type Error type.
*/
constructor(message, type) {
constructor(message: string, type: string) {
super(message);
this.type = type;
this.secondaryMessage = SECONDARY_ERROR_MESSAGES[type] || type;
type PartialRecord = Partial<Record<string, string>>;
this.secondaryMessage =
(SECONDARY_ERROR_MESSAGES as PartialRecord)[type] ?? type;
}
static MAX_RETRY = "MAX_RETRY";
@@ -53,13 +56,16 @@ class CustomError extends Error {
* Missing query parameter class.
*/
class MissingParamError extends Error {
missedParams: Array<string>;
secondaryMessage: string | undefined;
/**
* Missing query parameter error constructor.
*
* @param {string[]} missedParams An array of missing parameters names.
* @param {string=} secondaryMessage Optional secondary message to display.
* @param missedParams An array of missing parameters names.
* @param secondaryMessage Optional secondary message to display.
*/
constructor(missedParams, secondaryMessage) {
constructor(missedParams: Array<string>, secondaryMessage?: string) {
const msg = `Missing params ${missedParams
.map((p) => `"${p}"`)
.join(", ")} make sure you pass the parameters in URL`;
@@ -72,10 +78,10 @@ class MissingParamError extends Error {
/**
* Retrieve secondary message from an error object.
*
* @param {Error} err The error object.
* @returns {string|undefined} The secondary message if available, otherwise undefined.
* @param err The error object.
* @returns The secondary message if available, otherwise undefined.
*/
const retrieveSecondaryMessage = (err) => {
const retrieveSecondaryMessage = (err: Error): string | undefined => {
return "secondaryMessage" in err && typeof err.secondaryMessage === "string"
? err.secondaryMessage
: undefined;
@@ -1,16 +1,14 @@
// @ts-check
import { encodeHTML } from "./html.js";
import { splitWrappedText } from "./render.js";
/**
* Retrieves num with suffix k(thousands) precise to given decimal places.
*
* @param {number} num The number to format.
* @param {number=} precision The number of decimal places to include.
* @returns {string|number} The formatted number.
* @param num The number to format.
* @param precision The number of decimal places to include.
* @returns The formatted number.
*/
const kFormatter = (num, precision) => {
const kFormatter = (num: number, precision?: number): string | number => {
const abs = Math.abs(num);
const sign = Math.sign(num);
@@ -22,17 +20,17 @@ const kFormatter = (num, precision) => {
return sign * abs;
}
return sign * parseFloat((abs / 1000).toFixed(1)) + "k";
return `${sign * parseFloat((abs / 1000).toFixed(1))}k`;
};
/**
* Convert bytes to a human-readable string representation.
*
* @param {number} bytes The number of bytes to convert.
* @returns {string} The human-readable representation of bytes.
* @param bytes The number of bytes to convert.
* @returns The human-readable representation of bytes.
* @throws {Error} If bytes is negative or too large.
*/
const formatBytes = (bytes) => {
const formatBytes = (bytes: number): string => {
if (bytes < 0) {
throw new Error("Bytes must be a non-negative number");
}
@@ -45,23 +43,29 @@ const formatBytes = (bytes) => {
const base = 1024;
const i = Math.floor(Math.log(bytes) / Math.log(base));
if (i >= sizes.length) {
const unit = sizes[i];
if (unit === undefined) {
throw new Error("Bytes is too large to convert to a human-readable string");
}
return `${(bytes / Math.pow(base, i)).toFixed(1)} ${sizes[i]}`;
return `${(bytes / Math.pow(base, i)).toFixed(1)} ${unit}`;
};
/**
* Split text over multiple lines based on the card width.
*
* @param {string} text Text to split.
* @param {number} width Available wrap width in px.
* @param {number} fontSize Font size in px.
* @param {number} maxLines Maximum number of lines.
* @returns {string[]} Array of lines.
* @param text Text to split.
* @param width Available wrap width in px.
* @param fontSize Font size in px.
* @param maxLines Maximum number of lines.
* @returns Array of lines.
*/
const wrapTextMultiline = (text, width, fontSize, maxLines = 3) => {
const wrapTextMultiline = (
text: string,
width: number,
fontSize: number,
maxLines = 3,
): Array<string> => {
const wrapped = splitWrappedText(text, fontSize, width);
const lines = wrapped
.map((line) => encodeHTML(line.trim()))
@@ -69,7 +73,11 @@ const wrapTextMultiline = (text, width, fontSize, maxLines = 3) => {
// Add "..." to the last line if the text exceeds maxLines
if (wrapped.length > maxLines) {
lines[maxLines - 1] += "...";
const lastIndex = maxLines - 1;
const lastLine = lines[lastIndex];
if (lastLine !== undefined) {
lines[lastIndex] = `${lastLine}...`;
}
}
// Remove empty lines if text fits in less than maxLines lines
@@ -1,18 +1,13 @@
// @ts-check
/**
* Encode string as HTML.
*
* @see https://stackoverflow.com/a/48073476/10629172
*
* @param {string} str String to encode.
* @returns {string} Encoded string.
*/
const encodeHTML = (str) => {
const encodeHTML = (str: string): string => {
return (
str
.replace(/[\u00A0-\u9999<>&](?!#)/gim, (i) => {
return "&#" + i.charCodeAt(0) + ";";
.replace(/[\u00A0-\u9999<>&](?!#)/gim, (i: string) => {
return `&#${i.charCodeAt(0)};`;
})
// eslint-disable-next-line no-control-regex
.replace(/\u0008/gim, "")
-2
View File
@@ -1,5 +1,3 @@
// @ts-check
import axios from "axios";
/**
@@ -1,5 +1,3 @@
// @ts-check
/*
The icons in this file are based on https://github.com/primer/octicons which is released under the MIT license:
@@ -46,12 +44,16 @@ const icons = {
/**
* Get rank icon
*
* @param {string} rankIcon - The rank icon type.
* @param {string} rankLevel - The rank level.
* @param {number} percentile - The rank percentile.
* @returns {string} - The SVG code of the rank icon
* @param rankIcon - The rank icon type.
* @param rankLevel - The rank level.
* @param percentile - The rank percentile.
* @returns The SVG code of the rank icon
*/
const rankIcon = (rankIcon, rankLevel, percentile) => {
const rankIcon = (
rankIcon: string,
rankLevel: string,
percentile: number,
): string => {
switch (rankIcon) {
case "github":
return `
-2
View File
@@ -1,5 +1,3 @@
// @ts-check
import toEmoji from "emoji-name-map";
import { OWNER_AFFILIATIONS } from "./constants.js";
-2
View File
@@ -1,5 +1,3 @@
// @ts-check
import { getCardColors } from "./color.js";
import { SECONDARY_ERROR_MESSAGES, TRY_AGAIN_LATER } from "./error.js";
import { encodeHTML } from "./html.js";
@@ -1,43 +1,56 @@
// @ts-check
import type { AxiosResponse } from "axios";
import { getConfig } from "./config.js";
import { CustomError } from "./error.js";
import { logger } from "./log.js";
/**
* Subset of the response payload the retryer inspects to detect
* rate-limiting and credential failures.
*/
interface ResponseData {
errors?: Array<{ type?: string; message?: string }>;
message?: string;
}
/**
* Returns a random integer from 0 (inclusive) to `max` (exclusive).
*
* The value is generated using `Math.random()` and uniformly distributed
* across the range.
*
* @param {number} max The upper bound (exclusive). Must be a positive number.
* @param max The upper bound (exclusive). Must be a positive number.
*
* @returns {number} A random integer `n` such that `0 <= n < max`.
* @returns A random integer `n` such that `0 <= n < max`.
*/
function getRandomInt(max) {
function getRandomInt(max: number): number {
return Math.floor(Math.random() * max);
}
/**
* @typedef {import("axios").AxiosResponse} AxiosResponse Axios response.
* @typedef {(variables: any, token: string, retriesForTests?: number) => Promise<AxiosResponse>} FetcherFunction Fetcher function.
*/
type FetcherResponse = AxiosResponse<ResponseData>;
type FetcherFunction = (
variables: Record<string, unknown>,
token: string,
retriesForTests?: number,
) => Promise<FetcherResponse>;
/**
* Try to execute the fetcher function until it succeeds or the max number of retries is reached.
*
* @param {FetcherFunction} fetcher The fetcher function.
* @param {any} variables Object with arguments to pass to the fetcher function.
* @param {string | null} pat Optional PAT override.
* @returns {Promise<any>} The response from the fetcher function.
* @param fetcher The fetcher function.
* @param variables Object with arguments to pass to the fetcher function.
* @param pat Optional PAT override.
* @returns The response from the fetcher function.
*/
const retryer = async (fetcher, variables, pat = null) => {
let PATs;
if (pat) {
PATs = [{ name: "user PAT from database", value: pat }];
} else {
PATs = getConfig().pats;
}
const retryer = async (
fetcher: FetcherFunction,
variables: Record<string, unknown>,
pat: string | null = null,
): Promise<FetcherResponse> => {
const PATs = pat
? [{ name: "user PAT from database", value: pat }]
: getConfig().pats;
if (!PATs.length) {
throw new CustomError("No GitHub API tokens found", CustomError.NO_TOKENS);
@@ -46,11 +59,13 @@ const retryer = async (fetcher, variables, pat = null) => {
for (let retries = 0; retries < PATs.length; retries++) {
const currentPAT = PATs[(startPAT + retries) % PATs.length];
if (!currentPAT) {
continue;
}
try {
let response = await fetcher(
const response = await fetcher(
variables,
// @ts-ignore
currentPAT.value,
// used in tests for faking rate limit
retries,
@@ -58,11 +73,11 @@ const retryer = async (fetcher, variables, pat = null) => {
// react on both type and message-based rate-limit signals.
// https://github.com/anuraghazra/github-readme-stats/issues/4425
const errors = response?.data?.errors;
const errors = response.data.errors;
const errorType = errors?.[0]?.type;
const errorMsg = errors?.[0]?.message || "";
const errorMsg = errors?.[0]?.message ?? "";
const isRateLimited =
(errors && errorType === "RATE_LIMITED") ||
(!!errors && errorType === "RATE_LIMITED") ||
/rate limit/i.test(errorMsg);
if (isRateLimited) {
@@ -71,20 +86,18 @@ const retryer = async (fetcher, variables, pat = null) => {
return response;
}
} catch (err) {
/** @type {any} */
const e = err;
const e = err as { response?: FetcherResponse };
// network/unexpected error → let caller treat as failure
if (!e?.response) {
throw e;
if (!e.response) {
throw err;
}
// prettier-ignore
// also checking for bad credentials if any tokens gets invalidated
const isBadCredential =
e?.response?.data?.message === "Bad credentials";
const message = e.response.data.message;
const isBadCredential = message === "Bad credentials";
const isAccountSuspended =
e?.response?.data?.message === "Sorry. Your account was suspended.";
message === "Sorry. Your account was suspended.";
if (isBadCredential || isAccountSuspended) {
logger.log(`${currentPAT.name} Failed due to bad credentials`);
-2
View File
@@ -1,5 +1,3 @@
// @ts-check
import { MissingParamError } from "../common/error.js";
import { request } from "../common/http.js";
import { retryer } from "../common/retryer.js";
+3 -7
View File
@@ -1,5 +1,3 @@
// @ts-check
import { MissingParamError } from "../common/error.js";
import { request } from "../common/http.js";
import { retryer } from "../common/retryer.js";
@@ -23,9 +21,7 @@ const fetcher = (variables, token) => {
isPrivate
isArchived
isTemplate
stargazers {
totalCount
}
stargazerCount
description
primaryLanguage {
color
@@ -130,7 +126,7 @@ const fetchRepo = async (
return {
...repoUserStats,
...data.user.repository,
starCount: data.user.repository.stargazers.totalCount,
starCount: data.user.repository.stargazerCount,
};
}
@@ -155,7 +151,7 @@ const fetchRepo = async (
return {
...repoUserStats,
...data.organization.repository,
starCount: data.organization.repository.stargazers.totalCount,
starCount: data.organization.repository.stargazerCount,
};
}
+3 -7
View File
@@ -1,5 +1,3 @@
// @ts-check
import axios from "axios";
import githubUsernameRegex from "github-username-regex";
@@ -18,9 +16,7 @@ const GRAPHQL_REPOS_FIELD = `
totalCount
nodes {
name
stargazers {
totalCount
}
stargazerCount
}
pageInfo {
hasNextPage
@@ -159,7 +155,7 @@ const statsFetcher = async ({
// Disable multi page fetching on public Vercel instance due to rate limits.
fetchedPages++;
const repoNodesWithStars = repoNodes.filter(
(node) => node.stargazers.totalCount !== 0,
(node) => node.stargazerCount !== 0,
);
hasNextPage =
@@ -458,7 +454,7 @@ const fetchStats = async (
return !repoToHide.has(data.name);
})
.reduce((prev, curr) => {
return prev + curr.stargazers.totalCount;
return prev + curr.stargazerCount;
}, 0);
stats.rank = calculateRank({
@@ -1,5 +1,3 @@
// @ts-check
import { getConfig } from "../common/config.js";
import { CustomError, MissingParamError } from "../common/error.js";
import { wrapTextMultiline } from "../common/fmt.js";
+1 -1
View File
@@ -13,7 +13,7 @@ export interface RepositoryData {
isPrivate: boolean;
isArchived: boolean;
isTemplate: boolean;
stargazers: { totalCount: number };
stargazerCount: number;
description: string;
primaryLanguage: {
color: string;
-2
View File
@@ -1,5 +1,3 @@
// @ts-check
import axios from "axios";
import { CustomError, MissingParamError } from "../common/error.js";
@@ -1,18 +1,22 @@
// @ts-check
import { encodeHTML } from "./common/html.js";
/**
* Retrieves stat card labels in the available locales.
*
* @param {object} props Function arguments.
* @param {string} props.name The name of the locale.
* @param {string} props.apostrophe Whether to use apostrophe or not.
* @returns {object} The locales object.
* @param props Function arguments.
* @param props.name The name of the locale.
* @param props.apostrophe Whether to use apostrophe or not.
* @returns The locales object.
*
* @see https://www.andiamo.co.uk/resources/iso-language-codes/ for language codes.
*/
const statCardLocales = ({ name, apostrophe }) => {
const statCardLocales = ({
name,
apostrophe,
}: {
name: string;
apostrophe: string;
}) => {
const encodedName = encodeHTML(name);
return {
"statcard.title": {
@@ -1140,10 +1144,10 @@ const availableLocales = Object.keys(repoCardLocales["repocard.archived"]);
/**
* Checks whether the locale is available or not.
*
* @param {string} locale The locale to check.
* @returns {boolean} Boolean specifying whether the locale is available or not.
* @param locale The locale to check.
* @returns Boolean specifying whether the locale is available or not.
*/
const isLocaleAvailable = (locale) => {
const isLocaleAvailable = (locale: string): boolean => {
return availableLocales.includes(locale.toLowerCase());
};
+12
View File
@@ -0,0 +1,12 @@
/**
* `@uppercod/css-to-object` ships type definitions but does not expose them
* through its package.json `exports` map, so they cannot be resolved under
* `nodenext`. Declare the minimal surface used in tests here.
*/
declare module "@uppercod/css-to-object" {
export type CssObject = Record<
string,
Record<string, Record<string, string>>
>;
export function cssToObject(css: string): CssObject;
}
-7
View File
@@ -1,7 +0,0 @@
import * as matchers from "@testing-library/jest-dom/matchers";
import { expect } from "vitest";
expect.extend(matchers);
process.env.PAT_1 = "dummyPAT1";
process.env.PAT_2 = "dummyPAT2";
+4
View File
@@ -0,0 +1,4 @@
import "@testing-library/jest-dom/vitest";
process.env["PAT_1"] = "dummyPAT1";
process.env["PAT_2"] = "dummyPAT2";
@@ -1,12 +1,16 @@
import { queryByTestId } from "@testing-library/dom";
import { cssToObject } from "@uppercod/css-to-object";
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it } from "vitest";
import { Card } from "../src/common/Card.js";
import { getCardColors } from "../src/common/color.js";
import { icons } from "../src/common/icons.js";
describe("Card", () => {
afterEach(() => {
document.body.innerHTML = "";
});
it("should hide border", () => {
const card = new Card({});
card.setHideBorder(true);
@@ -68,21 +72,21 @@ describe("Card", () => {
});
it("title should have prefix icon", () => {
const card = new Card({ title: "ok", titlePrefixIcon: icons.contribs });
const card = new Card({ titlePrefixIcon: icons.contribs });
document.body.innerHTML = card.render(``);
expect(document.getElementsByClassName("icon")[0]).toBeInTheDocument();
});
it("title should not have prefix icon", () => {
const card = new Card({ title: "ok" });
const card = new Card({});
document.body.innerHTML = card.render(``);
expect(document.getElementsByClassName("icon")[0]).toBeUndefined();
});
it("should have proper height, width", () => {
const card = new Card({ height: 200, width: 200, title: "ok" });
const card = new Card({ height: 200, width: 200 });
document.body.innerHTML = card.render(``);
expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
"height",
@@ -95,7 +99,7 @@ describe("Card", () => {
});
it("should have less height after title is hidden", () => {
const card = new Card({ height: 200, title: "ok" });
const card = new Card({ height: 200 });
card.setHideTitle(true);
document.body.innerHTML = card.render(``);
@@ -147,10 +151,10 @@ describe("Card", () => {
document.body.innerHTML = card.render(``);
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[":host"][".header "];
const stylesObject = cssToObject(styleTag?.innerHTML ?? "");
const headerClassStyles = stylesObject[":host"]?.[".header "];
expect(headerClassStyles["fill"].trim()).toBe("#f00");
expect(headerClassStyles?.["fill"]?.trim()).toBe("#f00");
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
"#fff",
@@ -1,10 +1,10 @@
import { describe, expect, it } from "vitest";
import { getCardColors } from "../src/common/color";
import { getCardColors } from "../src/common/color.js";
describe("Test color.js", () => {
it("getCardColors: should return expected values", () => {
let colors = getCardColors({
const colors = getCardColors({
title_color: "f00",
text_color: "0f0",
ring_color: "0000ff",
@@ -24,7 +24,7 @@ describe("Test color.js", () => {
});
it("getCardColors: should fallback to default colors if color is invalid", () => {
let colors = getCardColors({
const colors = getCardColors({
title_color: "invalidcolor",
text_color: "0f0",
icon_color: "00f",
@@ -43,7 +43,7 @@ describe("Test color.js", () => {
});
it("getCardColors: should fallback to specified theme colors if is not defined", () => {
let colors = getCardColors({
const colors = getCardColors({
theme: "dark",
});
expect(colors).toStrictEqual({
@@ -57,7 +57,7 @@ describe("Test color.js", () => {
});
it("getCardColors: should return ring color equal to title color if not ring color is defined", () => {
let colors = getCardColors({
const colors = getCardColors({
title_color: "f00",
text_color: "0f0",
icon_color: "00f",
@@ -76,7 +76,7 @@ describe("Test color.js", () => {
});
it("getCardColors: should fallback to default theme if theme is invalid", () => {
let colors = getCardColors({
const colors = getCardColors({
theme: "invalidTheme",
});
expect(colors).toStrictEqual({
+3 -3
View File
@@ -7,7 +7,7 @@ import { fetchRepo } from "../src/fetchers/repo.js";
const data_repo = {
repository: {
name: "convoychat",
stargazers: { totalCount: 38000 },
stargazerCount: 38000,
description: "Help us take over the world! React + TS + GraphQL Chat App",
primaryLanguage: {
color: "#2b7489",
@@ -46,7 +46,7 @@ describe("Test fetchRepo", () => {
expect(repo).toStrictEqual({
...data_repo.repository,
starCount: data_repo.repository.stargazers.totalCount,
starCount: data_repo.repository.stargazerCount,
});
});
@@ -56,7 +56,7 @@ describe("Test fetchRepo", () => {
let repo = await fetchRepo("anuraghazra", "convoychat");
expect(repo).toStrictEqual({
...data_repo.repository,
starCount: data_repo.repository.stargazers.totalCount,
starCount: data_repo.repository.stargazerCount,
});
});
+10 -10
View File
@@ -33,9 +33,9 @@ const data_stats = {
repositories: {
totalCount: 5,
nodes: [
{ name: "test-repo-1", stargazers: { totalCount: 100 } },
{ name: "test-repo-2", stargazers: { totalCount: 100 } },
{ name: "test-repo-3", stargazers: { totalCount: 100 } },
{ name: "test-repo-1", stargazerCount: 100 },
{ name: "test-repo-2", stargazerCount: 100 },
{ name: "test-repo-3", stargazerCount: 100 },
],
pageInfo: {
hasNextPage: true,
@@ -64,8 +64,8 @@ const data_repo = {
user: {
repositories: {
nodes: [
{ name: "test-repo-4", stargazers: { totalCount: 50 } },
{ name: "test-repo-5", stargazers: { totalCount: 50 } },
{ name: "test-repo-4", stargazerCount: 50 },
{ name: "test-repo-5", stargazerCount: 50 },
],
pageInfo: {
hasNextPage: false,
@@ -81,11 +81,11 @@ const data_repo_zero_stars = {
user: {
repositories: {
nodes: [
{ name: "test-repo-1", stargazers: { totalCount: 100 } },
{ name: "test-repo-2", stargazers: { totalCount: 100 } },
{ name: "test-repo-3", stargazers: { totalCount: 100 } },
{ name: "test-repo-4", stargazers: { totalCount: 0 } },
{ name: "test-repo-5", stargazers: { totalCount: 0 } },
{ name: "test-repo-1", stargazerCount: 100 },
{ name: "test-repo-2", stargazerCount: 100 },
{ name: "test-repo-3", stargazerCount: 100 },
{ name: "test-repo-4", stargazerCount: 0 },
{ name: "test-repo-5", stargazerCount: 0 },
],
pageInfo: {
hasNextPage: true,
@@ -72,7 +72,7 @@ describe("Test fmt.js", () => {
it("wrapTextMultiline: should not wrap small texts", () => {
{
let multiLineText = wrapTextMultiline(
const multiLineText = wrapTextMultiline(
"Small text should not wrap",
130,
11,
@@ -83,7 +83,7 @@ describe("Test fmt.js", () => {
});
it("wrapTextMultiline: should wrap large texts", () => {
let multiLineText = wrapTextMultiline(
const multiLineText = wrapTextMultiline(
"Hello world long long long text",
130,
11,
@@ -93,7 +93,7 @@ describe("Test fmt.js", () => {
});
it("wrapTextMultiline: should wrap large texts and limit max lines", () => {
let multiLineText = wrapTextMultiline(
const multiLineText = wrapTextMultiline(
"Hello world long long long text",
53,
11,
@@ -103,13 +103,13 @@ describe("Test fmt.js", () => {
});
it("wrapTextMultiline: should handle chinese characters", () => {
let multiLineText = wrapTextMultiline(
const multiLineText = wrapTextMultiline(
"专门为刚开始刷题的同学准备的算法基地,没有最细只有更细,立志用动画将晦涩难懂的算法说的通俗易懂!",
130,
11,
3,
);
expect(multiLineText.length).toEqual(3);
expect(multiLineText[0].length).toEqual(11 * 8); // &#xxxxx; x 8
expect(multiLineText).toHaveLength(3);
expect(multiLineText[0]).toHaveLength(11 * 8); // &#xxxxx; x 8
});
});
@@ -17,9 +17,10 @@ describe("I18n", () => {
locale: "en",
translations: statCardLocales({ name: "Anurag Hazra", apostrophe: "s" }),
});
expect(() => i18n.t("statcard.title1")).toThrow(
"statcard.title1 Translation string not found",
);
expect(
// @ts-expect-error using a non-existing key should be reported by ts
() => i18n.t("statcard.title1"),
).toThrow("statcard.title1 Translation string not found");
});
it("should throw error if translation not found for locale", () => {
-2
View File
@@ -1,5 +1,3 @@
// @ts-check
import { queryByTestId } from "@testing-library/dom";
import { describe, expect, it } from "vitest";
@@ -1,9 +1,9 @@
// @ts-check
import { describe, expect, it, vi } from "vitest";
import { retryer } from "../src/common/retryer.js";
type Fetcher = Parameters<typeof retryer>[0];
vi.mock(import("../src/common/log.js"), async () => {
const { createLoggerMock } = await import("./utils.js");
return createLoggerMock();
@@ -13,14 +13,14 @@ const fetcher = vi.fn().mockResolvedValue({ data: "ok" });
const fetcherFail = vi.fn().mockResolvedValue({
data: { errors: [{ type: "RATE_LIMITED" }] },
});
}) as unknown as Fetcher;
const fetcherFailOnSecondTry = vi.fn((_vars, _token, retries) => {
if (retries < 1) {
return Promise.resolve({ data: { errors: [{ type: "RATE_LIMITED" }] } });
}
return Promise.resolve({ data: "ok" });
});
}) as unknown as Fetcher;
const fetcherFailWithMessageBasedRateLimitErr = vi.fn(
(_vars, _token, retries) => {
@@ -38,29 +38,29 @@ const fetcherFailWithMessageBasedRateLimitErr = vi.fn(
}
return Promise.resolve({ data: "ok" });
},
);
) as unknown as Fetcher;
const customFetcher = vi.fn((variables, token) => {
const customFetcher = vi.fn((_variables: unknown, token: string) => {
return Promise.resolve({ data: { token } });
});
}) as unknown as Fetcher;
describe("Test Retryer", () => {
it("retryer should return value and have zero retries on first try", async () => {
let res = await retryer(fetcher, {});
const res = await retryer(fetcher, {});
expect(fetcher).toHaveBeenCalledTimes(1);
expect(res).toStrictEqual({ data: "ok" });
});
it("retryer should return value and have 2 retries", async () => {
let res = await retryer(fetcherFailOnSecondTry, {});
const res = await retryer(fetcherFailOnSecondTry, {});
expect(fetcherFailOnSecondTry).toHaveBeenCalledTimes(2);
expect(res).toStrictEqual({ data: "ok" });
});
it("retryer should return value and have 2 retries with message based rate limit error", async () => {
let res = await retryer(fetcherFailWithMessageBasedRateLimitErr, {});
const res = await retryer(fetcherFailWithMessageBasedRateLimitErr, {});
expect(fetcherFailWithMessageBasedRateLimitErr).toHaveBeenCalledTimes(2);
expect(res).toStrictEqual({ data: "ok" });
+1 -2
View File
@@ -16,8 +16,7 @@ import type * as loggerModule from "../src/common/log.js";
* For example, with `precision = 3`, values must be within `0.001`.
*
* @param expected The expected numeric value to compare against.
*
* @param
* @param precision
* The number of decimal places of tolerance. Higher values mean stricter
* comparison. Internally converted to epsilon = 10^-precision.
*
+4 -1
View File
@@ -5,6 +5,9 @@
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"customConditions": []
"customConditions": [],
// Typecheck is performed in a separate task
"noCheck": true
}
}
-10
View File
@@ -2,17 +2,7 @@
"extends": ["../../tsconfig.base.json"],
"include": ["src", "tests", "vitest.config.ts"],
"compilerOptions": {
/**
* This workspace contains a lot of type errors.
* We will add typecheck later:
* @see https://github.com/stats-organization/github-stats-extended/issues/140
*
* At the moment we just need to output `js` and `.d.ts` files
*/
"noCheck": true,
"allowJs": true,
"checkJs": true,
"module": "nodenext",
"moduleResolution": "nodenext",
+1 -1
View File
@@ -4,6 +4,6 @@ export default defineProject({
test: {
environment: "jsdom",
include: ["./tests/*.test.{ts,js}"],
setupFiles: ["./tests/_setup.js"],
setupFiles: ["./tests/_setup.ts"],
},
});
+54 -54
View File
@@ -36,8 +36,8 @@ importers:
specifier: 1.60.0
version: 1.60.0
'@types/node':
specifier: 24.13.1
version: 24.13.1
specifier: 24.13.2
version: 24.13.2
'@vitest/coverage-v8':
specifier: catalog:default
version: 4.1.8(vitest@4.1.8)
@@ -75,8 +75,8 @@ importers:
specifier: 3.8.4
version: 3.8.4
turbo:
specifier: 2.9.17
version: 2.9.17
specifier: 2.9.18
version: 2.9.18
typescript:
specifier: 6.0.3
version: 6.0.3
@@ -85,7 +85,7 @@ importers:
version: 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
vitest:
specifier: catalog:default
version: 4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))
version: 4.1.8(@types/node@24.13.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
apps/backend:
dependencies:
@@ -110,7 +110,7 @@ importers:
version: 29.1.1
vitest:
specifier: catalog:default
version: 4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))
version: 4.1.8(@types/node@24.13.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
apps/frontend:
dependencies:
@@ -125,7 +125,7 @@ importers:
version: link:../../packages/core
'@tailwindcss/vite':
specifier: ^4.3.0
version: 4.3.0(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))
version: 4.3.0(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
axios:
specifier: catalog:default
version: 1.17.0
@@ -174,7 +174,7 @@ importers:
version: 19.2.3(@types/react@19.2.17)
'@vitejs/plugin-react':
specifier: 6.0.2
version: 6.0.2(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))
version: 6.0.2(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
clsx:
specifier: 2.1.1
version: 2.1.1
@@ -183,10 +183,10 @@ importers:
version: 4.3.0
vite:
specifier: catalog:default
version: 8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)
version: 8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)
vitest:
specifier: catalog:default
version: 4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))
version: 4.1.8(@types/node@24.13.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
packages/core:
dependencies:
@@ -220,7 +220,7 @@ importers:
version: 29.1.1
vitest:
specifier: catalog:default
version: 4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))
version: 4.1.8(@types/node@24.13.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
packages:
@@ -938,33 +938,33 @@ packages:
resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==}
engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
'@turbo/darwin-64@2.9.17':
resolution: {integrity: sha512-io5jn5RDeU+9YV78rWhwG++HD/OZ/Lxg1sg93+jDGKQNP3UDxY6RX2dmarbCILhNxNuAM8FH3WgGMY9E96Mf8w==}
'@turbo/darwin-64@2.9.18':
resolution: {integrity: sha512-9f27peFu16ur8c0v9nUFUEyBnbKuuFsUTjHFWfmwGfzySBXbHwzU44QhZon6Mznz0cHsIr3984NQj/bVrnGSRw==}
cpu: [x64]
os: [darwin]
'@turbo/darwin-arm64@2.9.17':
resolution: {integrity: sha512-83YZTYmN2sxFWf2LTMOwqbOvR3qZMa/TSFwnB6BHVBbIWyoPPe+TAdSTd8KevEx8ml8KkycJ/9A70DFVReyUww==}
'@turbo/darwin-arm64@2.9.18':
resolution: {integrity: sha512-9A6TMRq/Ib+QnbhLlgkhOm+624wO4pzSQ/yQviQfWHOlFvaYxdnIAYmu2H6TS6y7kSVL0DvzNe04NbESTOzFVQ==}
cpu: [arm64]
os: [darwin]
'@turbo/linux-64@2.9.17':
resolution: {integrity: sha512-teKfwJg0zSC+C2ZSOsX3VnAJGVgcN+pgKNmnGWzcpXQ9eIkAQtYP+getrQ2f1Tw/ePudnreQhq8tVP8S73Vy6Q==}
'@turbo/linux-64@2.9.18':
resolution: {integrity: sha512-zCdIDtz69AnbYh913elJRRoF3QY5aa2HNnf+4rAkc7bQ+tWujiDkCNV7stazOUPggaDvhKIf2Z87qHftTeXSkw==}
cpu: [x64]
os: [linux]
'@turbo/linux-arm64@2.9.17':
resolution: {integrity: sha512-mqO36x2CNtJ9CCbEf5xOqH662tVSc1wB0mxh7dopBpgXg0fEifdzwX0IEAUW1WUAaNH986L7iEUUgw3HNqUWgg==}
'@turbo/linux-arm64@2.9.18':
resolution: {integrity: sha512-Va1kXI04naMgYwqv/5Dfa36dTDx8015U7oaQAjrXa45ua9OoFjSV4OmvkML4EmXvUclQHCiBRbY8bvd0jV7eAg==}
cpu: [arm64]
os: [linux]
'@turbo/windows-64@2.9.17':
resolution: {integrity: sha512-jbyoNePufyMoSSrvVr+/mglcjmya/MOgoIrSHPr67iZ1VFgrlMQXHXtptR2lR48gi+86b1XBvsviJZ9A7zrydg==}
'@turbo/windows-64@2.9.18':
resolution: {integrity: sha512-m0kDhZANxSNz9ck1ybogFscHabriAsp4eDFNrN/1H5WrgTF7b3VlcPZnhuO3v2+E2KnCbeAc+UUT10BZZHdDKw==}
cpu: [x64]
os: [win32]
'@turbo/windows-arm64@2.9.17':
resolution: {integrity: sha512-+ql0wYc99Y2AMvyHCcC/P+xtyV4nz522L+C9HDnyi7ryHXBGM+ZjBP28M7SLBGMDImgpN8sk2szpgbvreMeXVA==}
'@turbo/windows-arm64@2.9.18':
resolution: {integrity: sha512-nUdR8WqoomUys9iIQmG45TMiizJ+5BV8egSeLLZba/AWblyp3fVBcIH1kSE58OtK4g2YzbMJEth6Ttv9w5rqMA==}
cpu: [arm64]
os: [win32]
@@ -992,8 +992,8 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/node@24.13.1':
resolution: {integrity: sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg==}
'@types/node@24.13.2':
resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==}
'@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
@@ -2917,8 +2917,8 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
turbo@2.9.17:
resolution: {integrity: sha512-91Q3KxfHJn7esFu2Ic6j9pkvQqWjncQCOp7r1gCKChRSb/+T/yIjsavAmbGLmFRKAzSjmWW/FMrcknmJ4hEOPA==}
turbo@2.9.18:
resolution: {integrity: sha512-bwabv6PupzeavybzEoArBAkwq5fnzwf8OFnRtpHwnviFWuwJPFxtyH+aVp36TmIqK3aYYgtTJ3J0m2ysxxSzQg==}
hasBin: true
type-check@0.4.0:
@@ -3714,12 +3714,12 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.3.0
'@tailwindcss/oxide-win32-x64-msvc': 4.3.0
'@tailwindcss/vite@4.3.0(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))':
'@tailwindcss/vite@4.3.0(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))':
dependencies:
'@tailwindcss/node': 4.3.0
'@tailwindcss/oxide': 4.3.0
tailwindcss: 4.3.0
vite: 8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)
vite: 8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)
'@testing-library/dom@10.4.1':
dependencies:
@@ -3741,22 +3741,22 @@ snapshots:
picocolors: 1.1.1
redent: 3.0.0
'@turbo/darwin-64@2.9.17':
'@turbo/darwin-64@2.9.18':
optional: true
'@turbo/darwin-arm64@2.9.17':
'@turbo/darwin-arm64@2.9.18':
optional: true
'@turbo/linux-64@2.9.17':
'@turbo/linux-64@2.9.18':
optional: true
'@turbo/linux-arm64@2.9.17':
'@turbo/linux-arm64@2.9.18':
optional: true
'@turbo/windows-64@2.9.17':
'@turbo/windows-64@2.9.18':
optional: true
'@turbo/windows-arm64@2.9.17':
'@turbo/windows-arm64@2.9.18':
optional: true
'@tybys/wasm-util@0.10.1':
@@ -3781,7 +3781,7 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@types/node@24.13.1':
'@types/node@24.13.2':
dependencies:
undici-types: 7.18.2
@@ -3949,10 +3949,10 @@ snapshots:
'@uppercod/css-to-object@1.1.1': {}
'@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))':
'@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))':
dependencies:
'@rolldown/pluginutils': 1.0.1
vite: 8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)
vite: 8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)
'@vitest/coverage-v8@4.1.8(vitest@4.1.8)':
dependencies:
@@ -3966,7 +3966,7 @@ snapshots:
obug: 2.1.1
std-env: 4.1.0
tinyrainbow: 3.1.0
vitest: 4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))
vitest: 4.1.8(@types/node@24.13.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
'@vitest/expect@4.1.8':
dependencies:
@@ -3977,13 +3977,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.0
'@vitest/mocker@4.1.8(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))':
'@vitest/mocker@4.1.8(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))':
dependencies:
'@vitest/spy': 4.1.8
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)
vite: 8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)
'@vitest/pretty-format@4.1.8':
dependencies:
@@ -5922,14 +5922,14 @@ snapshots:
tslib@2.8.1:
optional: true
turbo@2.9.17:
turbo@2.9.18:
optionalDependencies:
'@turbo/darwin-64': 2.9.17
'@turbo/darwin-arm64': 2.9.17
'@turbo/linux-64': 2.9.17
'@turbo/linux-arm64': 2.9.17
'@turbo/windows-64': 2.9.17
'@turbo/windows-arm64': 2.9.17
'@turbo/darwin-64': 2.9.18
'@turbo/darwin-arm64': 2.9.18
'@turbo/linux-64': 2.9.18
'@turbo/linux-arm64': 2.9.18
'@turbo/windows-64': 2.9.18
'@turbo/windows-arm64': 2.9.18
type-check@0.4.0:
dependencies:
@@ -6044,7 +6044,7 @@ snapshots:
vary@1.1.2: {}
vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0):
vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
@@ -6052,15 +6052,15 @@ snapshots:
rolldown: 1.0.3
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 24.13.1
'@types/node': 24.13.2
fsevents: 2.3.3
jiti: 2.7.0
yaml: 2.9.0
vitest@4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)):
vitest@4.1.8(@types/node@24.13.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.8
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.8
'@vitest/runner': 4.1.8
'@vitest/snapshot': 4.1.8
@@ -6077,10 +6077,10 @@ snapshots:
tinyexec: 1.2.3
tinyglobby: 0.2.16
tinyrainbow: 3.1.0
vite: 8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)
vite: 8.0.16(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.13.1
'@types/node': 24.13.2
'@vitest/coverage-v8': 4.1.8(vitest@4.1.8)
jsdom: 29.1.1
transitivePeerDependencies: