Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc9c926800 | ||
|
|
986eaad1a8 | ||
|
|
5403eefbd3 | ||
|
|
12b27566be | ||
|
|
40097713b9 | ||
|
|
2168c1f91e | ||
|
|
fa82216dfe | ||
|
|
9cf716c85f | ||
|
|
ef3f207dbf | ||
|
|
35fbec8004 | ||
|
|
3ef27f872c | ||
|
|
9f4ad921aa | ||
|
|
5557e9c772 | ||
|
|
570fc4aaed | ||
|
|
b6f4183ddd | ||
|
|
2ac454a177 | ||
|
|
b837c50885 | ||
|
|
dfb07a69b9 | ||
|
|
41b51dcfd9 | ||
|
|
7687c47d85 | ||
|
|
7917d54fdf | ||
|
|
7771a34790 | ||
|
|
1c266ad51e | ||
|
|
6258d90604 | ||
|
|
6d857d0f34 | ||
|
|
e15deeac76 | ||
|
|
b5d50fc28f | ||
|
|
1d394c7fad | ||
|
|
76e09ce3b0 | ||
|
|
96d141e8c7 | ||
|
|
6310073428 | ||
|
|
1d8aaf0fc8 | ||
|
|
4a2785e286 |
@@ -14,11 +14,11 @@ runs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
||||||
|
|
||||||
- name: Setup Node.js (via input)
|
- name: Setup Node.js (via input)
|
||||||
if: ${{ inputs.node-version }}
|
if: ${{ inputs.node-version }}
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node-version }}
|
node-version: ${{ inputs.node-version }}
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
@@ -26,7 +26,7 @@ runs:
|
|||||||
|
|
||||||
- name: Setup Node.js (via .nvmrc)
|
- name: Setup Node.js (via .nvmrc)
|
||||||
if: ${{ !inputs.node-version }}
|
if: ${{ !inputs.node-version }}
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
|
|||||||
@@ -10,11 +10,15 @@ on:
|
|||||||
# │ │ │ │ │
|
# │ │ │ │ │
|
||||||
# │ │ │ │ │
|
# │ │ │ │ │
|
||||||
# * * * * *
|
# * * * * *
|
||||||
- cron: "45 * * * *"
|
- cron: "15,45 * * * *"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
triggerRepeatRecent:
|
triggerRepeatRecent:
|
||||||
|
if: |
|
||||||
|
github.repository == 'anuraghazra/github-readme-stats' ||
|
||||||
|
github.repository == 'stats-organization/github-readme-stats' ||
|
||||||
|
github.repository == 'stats-organization/github-stats-extended'
|
||||||
name: Trigger server to update cached data.
|
name: Trigger server to update cached data.
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- uses: haya14busa/action-update-semver@7d2c558640ea49e798d46539536190aff8c18715 # v1.5.1
|
- uses: haya14busa/action-update-semver@7d2c558640ea49e798d46539536190aff8c18715 # v1.5.1
|
||||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"yzhang.markdown-all-in-one",
|
"yzhang.markdown-all-in-one",
|
||||||
"prettier.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"github.vscode-github-actions"
|
"github.vscode-github-actions"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
<a href="https://github-stats-extended.vercel.app/api?username=anuraghazra"><img src="https://github-stats-extended.vercel.app/api?username=anuraghazra"></a>
|
<a href="https://github-stats-extended.vercel.app/api?username=anuraghazra"><img src="https://github-stats-extended.vercel.app/api?username=anuraghazra"></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
This project is an [extended version](docs/fork.md) of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). It generates [various stats cards](#card-types), e.g. about your GitHub contributions, your top languages, etc. You can [customize](#advanced-customization) the cards via multiple parameters.
|
This project is the [extended, actively maintained successor](docs/fork.md) of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). It generates [various stats cards](#card-types), e.g. about your GitHub contributions, your top languages, etc. You can [customize](#advanced-customization) the cards via multiple parameters.
|
||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
- [Quick Start](#quick-start)
|
- [Quick Start](#quick-start)
|
||||||
|
- [Migration from github-readme-stats](#migration-from-github-readme-stats)
|
||||||
- [Card Types](#card-types)
|
- [Card Types](#card-types)
|
||||||
- [Advanced Customization](#advanced-customization)
|
- [Advanced Customization](#advanced-customization)
|
||||||
- [Self-Hosting](#self-hosting)
|
- [Run It Yourself](#run-it-yourself)
|
||||||
- [Acknowledgements](#acknowledgements)
|
- [Acknowledgements](#acknowledgements)
|
||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
|
|
||||||
@@ -27,6 +28,16 @@ This project is an [extended version](docs/fork.md) of [github-readme-stats](htt
|
|||||||
|
|
||||||
As more comfortable alternative, use the [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) to create your custom stats card. Copy the generated markdown code and paste it into your [GitHub profile README](https://docs.github.com/en/account-and-profile/how-tos/profile-customization/managing-your-profile-readme#adding-a-profile-readme). Done!
|
As more comfortable alternative, use the [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) to create your custom stats card. Copy the generated markdown code and paste it into your [GitHub profile README](https://docs.github.com/en/account-and-profile/how-tos/profile-customization/managing-your-profile-readme#adding-a-profile-readme). Done!
|
||||||
|
|
||||||
|
# Migration from github-readme-stats
|
||||||
|
To migrate from [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) you only need to change the domain from `github-readme-stats.vercel.app` to `github-stats-extended.vercel.app`:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- https://github-readme-stats.vercel.app/api?username=octocat&theme=radical
|
||||||
|
+ https://github-stats-extended.vercel.app/api?username=octocat&theme=radical
|
||||||
|
```
|
||||||
|
|
||||||
|
GitHub-Stats-Extended aims to be fully compatible with github-readme-stats. For more details see [Compatibility Notes](docs/fork.md#compatibility-notes).
|
||||||
|
|
||||||
# Card Types
|
# Card Types
|
||||||
- Show your GitHub statistics:
|
- Show your GitHub statistics:
|
||||||
|
|
||||||
@@ -55,13 +66,13 @@ As more comfortable alternative, use the [GitHub-Stats-Extended Wizard](https://
|
|||||||
# Advanced Customization
|
# Advanced Customization
|
||||||
The [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) offers some essential customization options. For more advanced customization check out the [advanced documentation](docs/advanced_documentation.md).
|
The [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) offers some essential customization options. For more advanced customization check out the [advanced documentation](docs/advanced_documentation.md).
|
||||||
|
|
||||||
|
# Run It Yourself
|
||||||
|
If you want to run GitHub-Stats-Extended on your own, there are two main deployment options: you can use [github-readme-stats-action](https://github.com/stats-organization/github-readme-stats-action) to generate cards in your own GitHub Actions workflow. Or you can self-host GitHub-Stats-Extended on Vercel.
|
||||||
|
|
||||||
|
See [Run It Yourself](docs/deploy.md) for detailed instructions.
|
||||||
|
|
||||||
# Acknowledgements
|
# Acknowledgements
|
||||||
This project is based on [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). On top of their functionality I added several new features and improvements. See [Fork Information](docs/fork.md) for a list of changes. The frontend I added to the project is based on [GitHub Trends](https://github.com/avgupta456/github-trends). Big thanks to [@anuraghazra](https://github.com/anuraghazra), [@avgupta456](https://github.com/avgupta456), [@rickstaa](https://github.com/rickstaa), [@qwerty541](https://github.com/qwerty541) and everyone else who worked on these projects! ❤️
|
This project is based on [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). On top of that project's functionality GitHub-Stats-Extended adds several new features and improvements. See [Fork Information](docs/fork.md) for a list of changes. The frontend added to GitHub-Stats-Extended is based on [GitHub Trends](https://github.com/avgupta456/github-trends). Big thanks to [@anuraghazra](https://github.com/anuraghazra), [@avgupta456](https://github.com/avgupta456), [@rickstaa](https://github.com/rickstaa), [@qwerty541](https://github.com/qwerty541) and everyone else who worked on these projects! ❤️
|
||||||
|
|
||||||
# Self-Hosting
|
|
||||||
Since the GitHub API only allows a limited number of requests per hour, the public instance of GitHub-Stats-Extended at https://github-stats-extended.vercel.app/api could possibly hit the rate limiter. If you host your own instance you do not have to worry about anything. Also, if you don't want to give my GitHub-Stats-Extended instance access to your private contributions but still want to include these contributions in your stats, you can simply host your own instance.
|
|
||||||
|
|
||||||
See [Deploy on your own](docs/deploy.md) for various deployment options.
|
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
Contributions are welcome!
|
Contributions are welcome!
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"expiration": 39600,
|
"expiration": 255600,
|
||||||
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
|
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
|
||||||
"passQuery": true
|
"passQuery": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"expiration": 39600,
|
"expiration": 255600,
|
||||||
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
|
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
|
||||||
"passQuery": true
|
"passQuery": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"expiration": 39600,
|
"expiration": 255600,
|
||||||
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
|
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
|
||||||
"passQuery": true
|
"passQuery": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"expiration": 39600,
|
"expiration": 255600,
|
||||||
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
|
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
|
||||||
"passQuery": true
|
"passQuery": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"expiration": 39600,
|
"expiration": 255600,
|
||||||
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
|
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
|
||||||
"passQuery": true
|
"passQuery": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,12 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"axios-mock-adapter": "2.1.0",
|
"axios-mock-adapter": "2.1.0",
|
||||||
"express": "5.2.1",
|
"express": "5.2.1",
|
||||||
"jsdom": "29.0.2",
|
"jsdom": "catalog:default",
|
||||||
"vitest": "catalog:default"
|
"vitest": "catalog:default"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.15.2",
|
"axios": "catalog:default",
|
||||||
"@stats-organization/github-readme-stats-core": "workspace:^",
|
"@stats-organization/github-readme-stats-core": "workspace:^",
|
||||||
"pg": "^8.20.0"
|
"pg": "^8.21.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,6 @@ export async function repeatRecentRequests() {
|
|||||||
if (urls.length === 0) {
|
if (urls.length === 0) {
|
||||||
console.log("No recent requests found.");
|
console.log("No recent requests found.");
|
||||||
} else {
|
} else {
|
||||||
await makeRequests(urls, 5);
|
await makeRequests(urls, 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const STATS_MOCK_RESPONSE = {
|
|||||||
followers: { totalCount: 0 },
|
followers: { totalCount: 0 },
|
||||||
repositories: {
|
repositories: {
|
||||||
totalCount: 1,
|
totalCount: 1,
|
||||||
nodes: [{ name: REPO, stargazers: { totalCount: 1 } }],
|
nodes: [{ name: REPO, stargazerCount: 1 }],
|
||||||
pageInfo: {
|
pageInfo: {
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
endCursor: "cursor",
|
endCursor: "cursor",
|
||||||
@@ -110,9 +110,7 @@ const REPO_MOCK_RESPONSE = {
|
|||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
isTemplate: false,
|
isTemplate: false,
|
||||||
stargazers: {
|
stargazerCount: 1,
|
||||||
totalCount: 1,
|
|
||||||
},
|
|
||||||
description: "Simple cra test repo.",
|
description: "Simple cra test repo.",
|
||||||
primaryLanguage: {
|
primaryLanguage: {
|
||||||
color: "#f1e05a",
|
color: "#f1e05a",
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ exports[`Test /api contract > should match the public many-params response snaps
|
|||||||
|
|
||||||
</g>
|
</g>
|
||||||
</svg>",
|
</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": [
|
"headers": [
|
||||||
[
|
[
|
||||||
"Cache-Control",
|
"Cache-Control",
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ exports[`Test /api/pin contract > should match the public many-params response s
|
|||||||
|
|
||||||
</g>
|
</g>
|
||||||
</svg>",
|
</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": [
|
"headers": [
|
||||||
[
|
[
|
||||||
"Cache-Control",
|
"Cache-Control",
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ export const data_stats = {
|
|||||||
repositories: {
|
repositories: {
|
||||||
totalCount: 3,
|
totalCount: 3,
|
||||||
nodes: [
|
nodes: [
|
||||||
{ name: "repo-keep-1", stargazers: { totalCount: 1500 } },
|
{ name: "repo-keep-1", stargazerCount: 1500 },
|
||||||
{ name: "repo-exclude-me", stargazers: { totalCount: 9999 } },
|
{ name: "repo-exclude-me", stargazerCount: 9999 },
|
||||||
{ name: "repo-keep-2", stargazers: { totalCount: 2600 } },
|
{ name: "repo-keep-2", stargazerCount: 2600 },
|
||||||
],
|
],
|
||||||
pageInfo: {
|
pageInfo: {
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
@@ -73,9 +73,7 @@ export const data_user = {
|
|||||||
username: "anuraghazra",
|
username: "anuraghazra",
|
||||||
name: "convoychat",
|
name: "convoychat",
|
||||||
nameWithOwner: "anuraghazra/convoychat",
|
nameWithOwner: "anuraghazra/convoychat",
|
||||||
stargazers: {
|
stargazerCount: 38000,
|
||||||
totalCount: 38000,
|
|
||||||
},
|
|
||||||
description:
|
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.",
|
"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: {
|
primaryLanguage: {
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ test("load initial page correctly", async ({ page }) => {
|
|||||||
// Logo exists
|
// Logo exists
|
||||||
await expect(page.locator('img[alt="logo"]')).toBeVisible();
|
await expect(page.locator('img[alt="logo"]')).toBeVisible();
|
||||||
|
|
||||||
// Star on GitHub button
|
// Star on GitHub link
|
||||||
const starButton = page.getByRole("button", { name: /star on/i });
|
const starButton = page.getByRole("link", { name: /star on/i });
|
||||||
await expect(starButton).toBeVisible();
|
await expect(starButton).toBeVisible();
|
||||||
|
|
||||||
const githubLink = page.locator(
|
const githubLink = page.locator(
|
||||||
@@ -39,6 +39,45 @@ test("load initial page correctly", async ({ page }) => {
|
|||||||
await expect(guestBtn).toBeEnabled();
|
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 }) => {
|
test("navigates between steps", async ({ page }) => {
|
||||||
await page.goto("");
|
await page.goto("");
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,24 @@
|
|||||||
<title>GitHub Stats Extended</title>
|
<title>GitHub Stats Extended</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/index.tsx"></script>
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
|||||||
+10
-10
@@ -13,18 +13,18 @@
|
|||||||
"typecheck": "tsc -p tsconfig.typecheck.json"
|
"typecheck": "tsc -p tsconfig.typecheck.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^2.11.2",
|
"@reduxjs/toolkit": "^2.12.0",
|
||||||
"@tailwindcss/vite": "^4.2.2",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"axios": "^1",
|
"axios": "catalog:default",
|
||||||
"axios-cache-interceptor": "^1",
|
"axios-cache-interceptor": "^1",
|
||||||
"daisyui": "^5.5.19",
|
"daisyui": "^5.5.23",
|
||||||
"@stats-organization/github-readme-stats-backend": "workspace:^",
|
"@stats-organization/github-readme-stats-backend": "workspace:^",
|
||||||
"@stats-organization/github-readme-stats-core": "workspace:^",
|
"@stats-organization/github-readme-stats-core": "workspace:^",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.7",
|
||||||
"react-icons": "^5.6.0",
|
"react-icons": "^5.6.0",
|
||||||
"react-loading-skeleton": "^3.3.1",
|
"react-loading-skeleton": "^3.3.1",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.3.0",
|
||||||
"react-spinners": "^0.17.0",
|
"react-spinners": "^0.17.0",
|
||||||
"react-toastify": "^11.1.0",
|
"react-toastify": "^11.1.0",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
@@ -32,11 +32,11 @@
|
|||||||
"uuid": "^14.0.0"
|
"uuid": "^14.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "19.2.17",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"@vitejs/plugin-react": "6.0.1",
|
"@vitejs/plugin-react": "6.0.2",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"tailwindcss": "4.2.2",
|
"tailwindcss": "4.3.0",
|
||||||
"vite": "catalog:default",
|
"vite": "catalog:default",
|
||||||
"vitest": "catalog:default"
|
"vitest": "catalog:default"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import type { JSX } from "react";
|
import type { CSSProperties, JSX } from "react";
|
||||||
|
|
||||||
import { CardImage } from "./CardImage";
|
import { CardImage } from "./CardImage";
|
||||||
|
import { LIGHT_CARD_BG } from "./themeBackdrop";
|
||||||
|
|
||||||
interface CardProps {
|
interface CardProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -11,6 +12,10 @@ interface CardProps {
|
|||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
fixedSize?: 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 = ({
|
export const Card = ({
|
||||||
@@ -21,16 +26,46 @@ export const Card = ({
|
|||||||
selected = false,
|
selected = false,
|
||||||
compact = false,
|
compact = false,
|
||||||
fixedSize = false,
|
fixedSize = false,
|
||||||
|
backgroundColor,
|
||||||
|
titleColor,
|
||||||
}: CardProps): JSX.Element => {
|
}: 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx("p-6 rounded border-2", {
|
className={clsx("p-6 rounded border-4", {
|
||||||
"h-[370px] w-[510px]": fixedSize,
|
"h-[370px] w-[510px]": fixedSize,
|
||||||
"border-blue-500 bg-blue-50": selected,
|
"border-primary": selected,
|
||||||
"border-gray-200 bg-white hover:bg-gray-50": !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>
|
<p className="text-base leading-relaxed mt-2 mb-4">{description}</p>
|
||||||
<CardImage
|
<CardImage
|
||||||
imageSrc={imageSrc}
|
imageSrc={imageSrc}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function SvgInline(props: SvgInlineProps): JSX.Element {
|
|||||||
FETCH_MULTI_PAGE_STARS: "10",
|
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
|
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);
|
loadConfigFromEnv(config);
|
||||||
|
|
||||||
setLoaded(false);
|
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 { clsx } from "clsx";
|
||||||
import type { HTMLProps, JSX, ReactNode } from "react";
|
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;
|
children: ReactNode;
|
||||||
|
/** DaisyUI color variant. Omit for the default neutral button. */
|
||||||
|
variant?: ButtonVariant | undefined;
|
||||||
|
size?: ButtonSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Button(props: ButtonProps): JSX.Element {
|
export function Button(props: ButtonProps): JSX.Element {
|
||||||
const { className, children, ...rest } = props;
|
const { className, children, variant, size = "md", ...rest } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
{...rest}
|
{...rest}
|
||||||
type="button"
|
type="button"
|
||||||
className={clsx(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function Checkbox({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
className="checkbox bg-white text-[#18181b] mr-2"
|
className="checkbox checkbox-primary mr-2"
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
onCheckedChange(!checked);
|
onCheckedChange(!checked);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -27,10 +27,7 @@ export function Select({
|
|||||||
}: SelectProps): JSX.Element {
|
}: SelectProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
className={clsx(
|
className={clsx("text-base select select-sm w-40 mt-4", className)}
|
||||||
"text-gray-700 text-base bg-white select select-sm w-40 rounded-sm mt-4",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
value={selectedOption.value}
|
value={selectedOption.value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
onOptionChange(options[e.target.selectedIndex] as SelectOption);
|
onOptionChange(options[e.target.selectedIndex] as SelectOption);
|
||||||
@@ -43,7 +40,7 @@ export function Select({
|
|||||||
value={option.value}
|
value={option.value}
|
||||||
disabled={option.disabled}
|
disabled={option.disabled}
|
||||||
className={clsx({
|
className={clsx({
|
||||||
"bg-blue-200": option.value === selectedOption.value,
|
"bg-primary/20": option.value === selectedOption.value,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{option.label}
|
{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>
|
<p>{description}</p>
|
||||||
<input
|
<input
|
||||||
type="number"
|
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 ?? ""}
|
value={internalValue ?? ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setInternalValue(e.target.value);
|
setInternalValue(e.target.value);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function ProgressSection({
|
|||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-1/4 flex flex-col mx-2 p-2 cursor-pointer",
|
"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",
|
isActive ? "border-t-[14px] -mt-[5px]" : "border-t-4",
|
||||||
)}
|
)}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -32,14 +32,16 @@ function ProgressSection({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx("text-lg font-bold", {
|
className={clsx("text-lg font-bold", {
|
||||||
"text-blue-500": passed,
|
"text-primary": passed,
|
||||||
"text-gray-500": !passed,
|
"text-base-content/60": !passed,
|
||||||
"-mt-[4px]": isActive,
|
"-mt-[4px]": isActive,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{`Step ${num + 1}`}
|
{`Step ${num + 1}`}
|
||||||
</div>
|
</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>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -59,16 +61,23 @@ export function ProgressBar({
|
|||||||
const rightDisabled = currItemIndex === items.length - 1;
|
const rightDisabled = currItemIndex === items.length - 1;
|
||||||
|
|
||||||
return (
|
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">
|
<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">
|
||||||
<LeftArrowIcon
|
<button
|
||||||
className={clsx("w-8 h-8", {
|
type="button"
|
||||||
"text-gray-400 cursor-not-allowed": leftDisabled,
|
aria-label="Previous step"
|
||||||
"text-gray-700 cursor-pointer": !leftDisabled,
|
disabled={leftDisabled}
|
||||||
})}
|
className="btn btn-ghost btn-circle"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onItemClick(Math.max(currItemIndex - 1, 0));
|
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">
|
<div className="px-2 flex-grow flex flex-row">
|
||||||
{items.map((item, index) => {
|
{items.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
@@ -85,15 +94,22 @@ export function ProgressBar({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<RightArrowIcon
|
<button
|
||||||
className={clsx("w-8 h-8", {
|
type="button"
|
||||||
"text-gray-400 cursor-not-allowed": rightDisabled,
|
aria-label="Next step"
|
||||||
"text-gray-700 cursor-pointer": !rightDisabled,
|
disabled={rightDisabled}
|
||||||
})}
|
className="btn btn-ghost btn-circle"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onItemClick(Math.min(currItemIndex + 1, items.length - 1));
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ export function Section({ title, children }: SectionProps): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<div className="flex relative pb-12">
|
<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-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>
|
||||||
<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" />
|
<LightningIcon className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-grow pl-4">
|
<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}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ export function TextSection({
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"border border-gray-300 rounded px-2 py-1 mt-2 w-3/4 min-w-48 max-w-xl",
|
"border border-base-content/20 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 },
|
disabled ? "cursor-not-allowed bg-base-200" : "bg-base-100",
|
||||||
)}
|
)}
|
||||||
value={internalValue}
|
value={internalValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
+103
-2
@@ -1,14 +1,115 @@
|
|||||||
/* ./src/index.css */
|
|
||||||
@import "tailwindcss";
|
@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 {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: "Segoe UI", Ubuntu, Sans-Serif;
|
font-family: "Segoe UI", Ubuntu, Sans-Serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: var(--color-base-100);
|
||||||
|
color: var(--color-base-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
cursor: pointer;
|
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": {
|
"repositories": {
|
||||||
"totalCount": 139,
|
"totalCount": 139,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{ "name": "classicLogo", "stargazers": { "totalCount": 1 } },
|
{ "name": "classicLogo", "stargazerCount": 1 },
|
||||||
{ "name": "creativechat", "stargazers": { "totalCount": 1 } },
|
{ "name": "creativechat", "stargazerCount": 1 },
|
||||||
{ "name": "verlet_tests", "stargazers": { "totalCount": 1 } },
|
{ "name": "verlet_tests", "stargazerCount": 1 },
|
||||||
{
|
{ "name": "blade-checkout-ui-demo", "stargazerCount": 0 },
|
||||||
"name": "blade-checkout-ui-demo",
|
{ "name": "tslint-plugin-analytics", "stargazerCount": 0 },
|
||||||
"stargazers": { "totalCount": 0 }
|
{ "name": "stargazer", "stargazerCount": 0 },
|
||||||
},
|
{ "name": "react-smooth-range-input", "stargazerCount": 0 },
|
||||||
{
|
{ "name": "csb-git-integration-bug", "stargazerCount": 0 },
|
||||||
"name": "tslint-plugin-analytics",
|
{ "name": "covid-nepal-web", "stargazerCount": 0 },
|
||||||
"stargazers": { "totalCount": 0 }
|
{ "name": "shrijan00003.github.io", "stargazerCount": 0 },
|
||||||
},
|
{ "name": "TheBigCB.com", "stargazerCount": 0 },
|
||||||
{ "name": "stargazer", "stargazers": { "totalCount": 0 } },
|
{ "name": "html2commonmark", "stargazerCount": 0 },
|
||||||
{
|
{ "name": "test-myaction", "stargazerCount": 0 },
|
||||||
"name": "react-smooth-range-input",
|
{ "name": "react-emoji-render", "stargazerCount": 0 },
|
||||||
"stargazers": { "totalCount": 0 }
|
{ "name": "sajilo-editor", "stargazerCount": 0 },
|
||||||
},
|
{ "name": "coder-aayush.github.io", "stargazerCount": 0 },
|
||||||
{
|
{ "name": "codesandbox-client", "stargazerCount": 0 },
|
||||||
"name": "csb-git-integration-bug",
|
{ "name": "product-sans", "stargazerCount": 0 },
|
||||||
"stargazers": { "totalCount": 0 }
|
{ "name": "NodejsBlogSystem", "stargazerCount": 0 },
|
||||||
},
|
{ "name": "native-javascript", "stargazerCount": 0 },
|
||||||
{ "name": "covid-nepal-web", "stargazers": { "totalCount": 0 } },
|
{ "name": "p5.js", "stargazerCount": 0 },
|
||||||
{
|
{ "name": "gatsbytutorials.com", "stargazerCount": 0 },
|
||||||
"name": "shrijan00003.github.io",
|
{ "name": "hello-world-docker-action", "stargazerCount": 0 },
|
||||||
"stargazers": { "totalCount": 0 }
|
{ "name": "glugmvit.github.io", "stargazerCount": 0 },
|
||||||
},
|
{ "name": "react-map-interaction", "stargazerCount": 0 },
|
||||||
{ "name": "TheBigCB.com", "stargazers": { "totalCount": 0 } },
|
{ "name": "react-external-image", "stargazerCount": 0 },
|
||||||
{ "name": "html2commonmark", "stargazers": { "totalCount": 0 } },
|
{ "name": "gatsby-boilerplate", "stargazerCount": 0 },
|
||||||
{ "name": "test-myaction", "stargazers": { "totalCount": 0 } },
|
{ "name": "YouBackup", "stargazerCount": 0 },
|
||||||
{ "name": "react-emoji-render", "stargazers": { "totalCount": 0 } },
|
{ "name": "npm-expansions", "stargazerCount": 0 },
|
||||||
{ "name": "sajilo-editor", "stargazers": { "totalCount": 0 } },
|
{ "name": "Toy-Neural-Network-JS", "stargazerCount": 0 },
|
||||||
{
|
{ "name": "LSystemCreator", "stargazerCount": 0 },
|
||||||
"name": "coder-aayush.github.io",
|
{ "name": "Happy-2019", "stargazerCount": 0 },
|
||||||
"stargazers": { "totalCount": 0 }
|
{ "name": "jquery-navScroll", "stargazerCount": 0 },
|
||||||
},
|
{ "name": "strml.net", "stargazerCount": 0 },
|
||||||
{ "name": "codesandbox-client", "stargazers": { "totalCount": 0 } },
|
{ "name": "Logo", "stargazerCount": 0 },
|
||||||
{ "name": "product-sans", "stargazers": { "totalCount": 0 } },
|
{ "name": "website", "stargazerCount": 0 },
|
||||||
{ "name": "NodejsBlogSystem", "stargazers": { "totalCount": 0 } },
|
{ "name": "anuraghazra.github.io-old", "stargazerCount": 0 },
|
||||||
{ "name": "native-javascript", "stargazers": { "totalCount": 0 } },
|
{ "name": "Quintus", "stargazerCount": 0 },
|
||||||
{ "name": "p5.js", "stargazers": { "totalCount": 0 } },
|
{ "name": "Electron_experiments", "stargazerCount": 0 }
|
||||||
{ "name": "gatsbytutorials.com", "stargazers": { "totalCount": 0 } },
|
|
||||||
{
|
|
||||||
"name": "hello-world-docker-action",
|
|
||||||
"stargazers": { "totalCount": 0 }
|
|
||||||
},
|
|
||||||
{ "name": "glugmvit.github.io", "stargazers": { "totalCount": 0 } },
|
|
||||||
{
|
|
||||||
"name": "react-map-interaction",
|
|
||||||
"stargazers": { "totalCount": 0 }
|
|
||||||
},
|
|
||||||
{ "name": "react-external-image", "stargazers": { "totalCount": 0 } },
|
|
||||||
{ "name": "gatsby-boilerplate", "stargazers": { "totalCount": 0 } },
|
|
||||||
{ "name": "YouBackup", "stargazers": { "totalCount": 0 } },
|
|
||||||
{ "name": "npm-expansions", "stargazers": { "totalCount": 0 } },
|
|
||||||
{
|
|
||||||
"name": "Toy-Neural-Network-JS",
|
|
||||||
"stargazers": { "totalCount": 0 }
|
|
||||||
},
|
|
||||||
{ "name": "LSystemCreator", "stargazers": { "totalCount": 0 } },
|
|
||||||
{ "name": "Happy-2019", "stargazers": { "totalCount": 0 } },
|
|
||||||
{ "name": "jquery-navScroll", "stargazers": { "totalCount": 0 } },
|
|
||||||
{ "name": "strml.net", "stargazers": { "totalCount": 0 } },
|
|
||||||
{ "name": "Logo", "stargazers": { "totalCount": 0 } },
|
|
||||||
{ "name": "website", "stargazers": { "totalCount": 0 } },
|
|
||||||
{
|
|
||||||
"name": "anuraghazra.github.io-old",
|
|
||||||
"stargazers": { "totalCount": 0 }
|
|
||||||
},
|
|
||||||
{ "name": "Quintus", "stargazers": { "totalCount": 0 } },
|
|
||||||
{ "name": "Electron_experiments", "stargazers": { "totalCount": 0 } }
|
|
||||||
],
|
],
|
||||||
"pageInfo": {
|
"pageInfo": {
|
||||||
"hasNextPage": false,
|
"hasNextPage": false,
|
||||||
|
|||||||
@@ -7,9 +7,7 @@
|
|||||||
"isPrivate": false,
|
"isPrivate": false,
|
||||||
"isArchived": false,
|
"isArchived": false,
|
||||||
"isTemplate": false,
|
"isTemplate": false,
|
||||||
"stargazers": {
|
"stargazerCount": 76994,
|
||||||
"totalCount": 76994
|
|
||||||
},
|
|
||||||
"description": ":zap: Dynamically generated stats for your github readmes",
|
"description": ":zap: Dynamically generated stats for your github readmes",
|
||||||
"primaryLanguage": {
|
"primaryLanguage": {
|
||||||
"color": "#f1e05a",
|
"color": "#f1e05a",
|
||||||
|
|||||||
@@ -36,606 +36,109 @@
|
|||||||
"repositories": {
|
"repositories": {
|
||||||
"totalCount": 139,
|
"totalCount": 139,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{ "name": "github-readme-stats", "stargazerCount": 77025 },
|
||||||
"name": "github-readme-stats",
|
{ "name": "Verly.js", "stargazerCount": 696 },
|
||||||
"stargazers": {
|
{ "name": "anuraghazra.github.io", "stargazerCount": 608 },
|
||||||
"totalCount": 77025
|
{ "name": "anuraghazra", "stargazerCount": 376 },
|
||||||
}
|
{ "name": "type-trident", "stargazerCount": 359 },
|
||||||
},
|
{ "name": "BugVilla", "stargazerCount": 232 },
|
||||||
{
|
{ "name": "convoychat", "stargazerCount": 214 },
|
||||||
"name": "Verly.js",
|
{ "name": "CanvasFun", "stargazerCount": 211 },
|
||||||
"stargazers": {
|
{ "name": "EvolutionAquerium", "stargazerCount": 192 },
|
||||||
"totalCount": 696
|
{ "name": "typelevel-parser", "stargazerCount": 147 },
|
||||||
}
|
{ "name": "VerlyRangeSlider", "stargazerCount": 135 },
|
||||||
},
|
{ "name": "vscode-strip-ts-copy", "stargazerCount": 72 },
|
||||||
{
|
{ "name": "react-folder-tree", "stargazerCount": 54 },
|
||||||
"name": "anuraghazra.github.io",
|
{ "name": "Atomic.js", "stargazerCount": 53 },
|
||||||
"stargazers": {
|
{ "name": "ToyLang", "stargazerCount": 51 },
|
||||||
"totalCount": 608
|
{ "name": "design-patterns-everyday", "stargazerCount": 46 },
|
||||||
}
|
{ "name": "gatsby", "stargazerCount": 36 },
|
||||||
},
|
{ "name": "Slime", "stargazerCount": 35 },
|
||||||
{
|
{ "name": "Candy.js", "stargazerCount": 34 },
|
||||||
"name": "anuraghazra",
|
{ "name": "ShaderExpo", "stargazerCount": 33 },
|
||||||
"stargazers": {
|
{ "name": "Nothing", "stargazerCount": 32 },
|
||||||
"totalCount": 376
|
{ "name": "peerlist-profile-action", "stargazerCount": 30 },
|
||||||
}
|
{ "name": "facebook-reaction-animation", "stargazerCount": 29 },
|
||||||
},
|
{ "name": "react-stripe-dropdown", "stargazerCount": 27 },
|
||||||
{
|
{ "name": "QuickerPoll", "stargazerCount": 27 },
|
||||||
"name": "type-trident",
|
{ "name": "playground-format-on-save", "stargazerCount": 24 },
|
||||||
"stargazers": {
|
{ "name": "ParticleBrush", "stargazerCount": 23 },
|
||||||
"totalCount": 359
|
{ "name": "graphql-oauth", "stargazerCount": 21 },
|
||||||
}
|
{ "name": "parasites", "stargazerCount": 21 },
|
||||||
},
|
{ "name": "twitter-banner-jokes", "stargazerCount": 18 },
|
||||||
{
|
{ "name": "verly-cpp", "stargazerCount": 16 },
|
||||||
"name": "BugVilla",
|
{ "name": "gatsby-github-issues-blog", "stargazerCount": 16 },
|
||||||
"stargazers": {
|
{ "name": "NikeHyperAce", "stargazerCount": 12 },
|
||||||
"totalCount": 232
|
{ "name": "rust-particles", "stargazerCount": 11 },
|
||||||
}
|
{ "name": "VerletDrawing", "stargazerCount": 10 },
|
||||||
},
|
{ "name": "graphql-jwt-auth-test", "stargazerCount": 9 },
|
||||||
{
|
|
||||||
"name": "convoychat",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 214
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "CanvasFun",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 211
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "EvolutionAquerium",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 192
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "typelevel-parser",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 147
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "VerlyRangeSlider",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 135
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "vscode-strip-ts-copy",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 72
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "react-folder-tree",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 54
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Atomic.js",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 53
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ToyLang",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 51
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "design-patterns-everyday",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 46
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "gatsby",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 36
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Slime",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 35
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Candy.js",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 34
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ShaderExpo",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 33
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Nothing",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 32
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "peerlist-profile-action",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 30
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "facebook-reaction-animation",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 29
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "react-stripe-dropdown",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 27
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "QuickerPoll",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 27
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "playground-format-on-save",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 24
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ParticleBrush",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 23
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "graphql-oauth",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 21
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "parasites",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 21
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "twitter-banner-jokes",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verly-cpp",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 16
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "gatsby-github-issues-blog",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 16
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "NikeHyperAce",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 12
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "rust-particles",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 11
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "VerletDrawing",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 10
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "graphql-jwt-auth-test",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 9
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "anuraghazra.github.io-old-react",
|
"name": "anuraghazra.github.io-old-react",
|
||||||
"stargazers": {
|
"stargazerCount": 9
|
||||||
"totalCount": 9
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{ "name": "react18-ssr-test", "stargazerCount": 8 },
|
||||||
"name": "react18-ssr-test",
|
{ "name": "GyroDodge", "stargazerCount": 8 },
|
||||||
"stargazers": {
|
{ "name": "go-phyllotaxis", "stargazerCount": 6 },
|
||||||
"totalCount": 8
|
{ "name": "graphql-oauth-client", "stargazerCount": 6 },
|
||||||
}
|
{ "name": "code-runner-action", "stargazerCount": 6 },
|
||||||
},
|
{ "name": "GithubActionsPlayground", "stargazerCount": 6 },
|
||||||
{
|
{ "name": "Loader.js", "stargazerCount": 6 },
|
||||||
"name": "GyroDodge",
|
{ "name": "ts-essentials", "stargazerCount": 5 },
|
||||||
"stargazers": {
|
{ "name": "chakra-ui", "stargazerCount": 5 },
|
||||||
"totalCount": 8
|
{ "name": "text-to-handwriting", "stargazerCount": 5 },
|
||||||
}
|
{ "name": "build-your-own-x", "stargazerCount": 5 },
|
||||||
},
|
{ "name": "nodejs-dynamic-restapi", "stargazerCount": 5 },
|
||||||
{
|
{ "name": "DevYouTubeList", "stargazerCount": 5 },
|
||||||
"name": "go-phyllotaxis",
|
{ "name": "FlockingBlackHole", "stargazerCount": 5 },
|
||||||
"stargazers": {
|
{ "name": "webgpu-boids", "stargazerCount": 4 },
|
||||||
"totalCount": 6
|
{ "name": "test-githubblock", "stargazerCount": 4 },
|
||||||
}
|
{ "name": "renderless-components", "stargazerCount": 4 },
|
||||||
},
|
{ "name": "reakit", "stargazerCount": 4 },
|
||||||
{
|
{ "name": "micro-open-graph", "stargazerCount": 4 },
|
||||||
"name": "graphql-oauth-client",
|
{ "name": "rest-and-graphql", "stargazerCount": 4 },
|
||||||
"stargazers": {
|
{ "name": "abell-issues-blog", "stargazerCount": 4 },
|
||||||
"totalCount": 6
|
{ "name": "VanillaMVC", "stargazerCount": 4 },
|
||||||
}
|
{ "name": "covid19-sim", "stargazerCount": 4 },
|
||||||
},
|
{ "name": "YoutubeDownloader", "stargazerCount": 4 },
|
||||||
{
|
{ "name": "verlet.js", "stargazerCount": 4 },
|
||||||
"name": "code-runner-action",
|
{ "name": "test-github-ts", "stargazerCount": 3 },
|
||||||
"stargazers": {
|
{ "name": "tw-dynamic-interpolation", "stargazerCount": 3 },
|
||||||
"totalCount": 6
|
{ "name": "ts-string-interpolator", "stargazerCount": 3 },
|
||||||
}
|
{ "name": "AnuReact", "stargazerCount": 3 },
|
||||||
},
|
{ "name": "CanvasBalls", "stargazerCount": 3 },
|
||||||
{
|
{ "name": "circleci-test", "stargazerCount": 3 },
|
||||||
"name": "GithubActionsPlayground",
|
{ "name": "gatsby-starter-netlify-cms", "stargazerCount": 3 },
|
||||||
"stargazers": {
|
{ "name": "WebGL.js", "stargazerCount": 3 },
|
||||||
"totalCount": 6
|
{ "name": "test-fork-workflow", "stargazerCount": 2 },
|
||||||
}
|
{ "name": "test-codesandbox-projects", "stargazerCount": 2 },
|
||||||
},
|
{ "name": "codesandbox-template-astro", "stargazerCount": 2 },
|
||||||
{
|
{ "name": "apollo", "stargazerCount": 2 },
|
||||||
"name": "Loader.js",
|
{ "name": "api-docs", "stargazerCount": 2 },
|
||||||
"stargazers": {
|
{ "name": "Buddies-movie", "stargazerCount": 2 },
|
||||||
"totalCount": 6
|
{ "name": "2k76", "stargazerCount": 2 },
|
||||||
}
|
{ "name": "gatsby-remark-embedder", "stargazerCount": 2 },
|
||||||
},
|
{ "name": "weird-hello-worlds", "stargazerCount": 2 },
|
||||||
{
|
{ "name": "rn-tabview-test-repro", "stargazerCount": 1 },
|
||||||
"name": "ts-essentials",
|
{ "name": "tailwindcss-jit", "stargazerCount": 1 },
|
||||||
"stargazers": {
|
{ "name": "DefinitelyTyped", "stargazerCount": 1 },
|
||||||
"totalCount": 5
|
{ "name": "server-components-demo", "stargazerCount": 1 },
|
||||||
}
|
{ "name": "volant-vo", "stargazerCount": 1 },
|
||||||
},
|
{ "name": "react-spectrum", "stargazerCount": 1 },
|
||||||
{
|
{ "name": "webhooks-test", "stargazerCount": 1 },
|
||||||
"name": "chakra-ui",
|
{ "name": "react-showdown", "stargazerCount": 1 },
|
||||||
"stargazers": {
|
{ "name": "transcode", "stargazerCount": 1 },
|
||||||
"totalCount": 5
|
{ "name": "awake-yet-action", "stargazerCount": 1 },
|
||||||
}
|
{ "name": "bradgarropy.com", "stargazerCount": 1 },
|
||||||
},
|
{ "name": "gatsby-plugin-social-banners", "stargazerCount": 1 },
|
||||||
{
|
{ "name": "Tour360", "stargazerCount": 1 },
|
||||||
"name": "text-to-handwriting",
|
{ "name": "blizard.js", "stargazerCount": 1 },
|
||||||
"stargazers": {
|
{ "name": "keep-a-changelog", "stargazerCount": 1 },
|
||||||
"totalCount": 5
|
{ "name": "vulnerabilities", "stargazerCount": 1 },
|
||||||
}
|
{ "name": "harleydavidson", "stargazerCount": 1 },
|
||||||
},
|
{ "name": "awesome-webgl", "stargazerCount": 1 },
|
||||||
{
|
{ "name": "NewtonsInterpolation", "stargazerCount": 1 },
|
||||||
"name": "build-your-own-x",
|
{ "name": "now-github-starter", "stargazerCount": 1 },
|
||||||
"stargazers": {
|
{ "name": "simplerockets", "stargazerCount": 1 }
|
||||||
"totalCount": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "nodejs-dynamic-restapi",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "DevYouTubeList",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "FlockingBlackHole",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "webgpu-boids",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "test-githubblock",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "renderless-components",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "reakit",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "micro-open-graph",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "rest-and-graphql",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "abell-issues-blog",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "VanillaMVC",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "covid19-sim",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "YoutubeDownloader",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "verlet.js",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "test-github-ts",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tw-dynamic-interpolation",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ts-string-interpolator",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "AnuReact",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "CanvasBalls",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "circleci-test",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "gatsby-starter-netlify-cms",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "WebGL.js",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "test-fork-workflow",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "test-codesandbox-projects",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "codesandbox-template-astro",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "apollo",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "api-docs",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Buddies-movie",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "2k76",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "gatsby-remark-embedder",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "weird-hello-worlds",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "rn-tabview-test-repro",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tailwindcss-jit",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "DefinitelyTyped",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "server-components-demo",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "volant-vo",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "react-spectrum",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "webhooks-test",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "react-showdown",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "transcode",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "awake-yet-action",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "bradgarropy.com",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "gatsby-plugin-social-banners",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Tour360",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "blizard.js",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "keep-a-changelog",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "vulnerabilities",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "harleydavidson",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "awesome-webgl",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "NewtonsInterpolation",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "now-github-starter",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "simplerockets",
|
|
||||||
"stargazers": {
|
|
||||||
"totalCount": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"pageInfo": {
|
"pageInfo": {
|
||||||
"hasNextPage": true,
|
"hasNextPage": true,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ToastContainer, toast } from "react-toastify";
|
|||||||
import { getUserMetadata } from "../../api/user";
|
import { getUserMetadata } from "../../api/user";
|
||||||
import { clearAxiosCache } from "../../axios-override";
|
import { clearAxiosCache } from "../../axios-override";
|
||||||
import type { StageIndex } from "../../models/Stage";
|
import type { StageIndex } from "../../models/Stage";
|
||||||
|
import { useTheme } from "../../redux/selectors/themeSelectors";
|
||||||
import {
|
import {
|
||||||
useIsAuthenticated,
|
useIsAuthenticated,
|
||||||
useUserKey,
|
useUserKey,
|
||||||
@@ -56,6 +57,7 @@ export function AppTrends() {
|
|||||||
const userKey = useUserKey();
|
const userKey = useUserKey();
|
||||||
const userToken = useUserToken();
|
const userToken = useUserToken();
|
||||||
const isAuthenticated = useIsAuthenticated();
|
const isAuthenticated = useIsAuthenticated();
|
||||||
|
const { isDark } = useTheme();
|
||||||
|
|
||||||
const [stage, setStage] = useState<StageIndex>(isAuthenticated ? 1 : 0);
|
const [stage, setStage] = useState<StageIndex>(isAuthenticated ? 1 : 0);
|
||||||
|
|
||||||
@@ -116,9 +118,9 @@ export function AppTrends() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
<Header currStageIndex={stage} onStageIndexChange={setStage} />
|
<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} />
|
<HomeScreen stage={stage} setStage={setStage} />
|
||||||
<ToastContainer />
|
<ToastContainer theme={isDark ? "dark" : "light"} />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { JSX } from "react";
|
|||||||
import { FaGithub as GithubIcon } from "react-icons/fa";
|
import { FaGithub as GithubIcon } from "react-icons/fa";
|
||||||
|
|
||||||
import appIcon from "../../assets/appLogo64.png";
|
import appIcon from "../../assets/appLogo64.png";
|
||||||
|
import { ThemePicker } from "../../components/Generic/ThemePicker";
|
||||||
import { ProgressBar } from "../../components/Home/Progress";
|
import { ProgressBar } from "../../components/Home/Progress";
|
||||||
import { STAGE_LABELS } from "../../models/Stage";
|
import { STAGE_LABELS } from "../../models/Stage";
|
||||||
import type { StageIndex } from "../../models/Stage";
|
import type { StageIndex } from "../../models/Stage";
|
||||||
@@ -19,31 +20,34 @@ export function Header({
|
|||||||
}: HeaderProps): JSX.Element {
|
}: HeaderProps): JSX.Element {
|
||||||
return (
|
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">
|
<div className="px-5 py-2 flex flex-wrap">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<a
|
<a
|
||||||
href="/"
|
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" />
|
<img src={appIcon} alt="logo" className="w-6 h-6" />
|
||||||
<span className="ml-2 text-xl">GitHub Stats Extended</span>
|
<span className="ml-2 text-xl">GitHub Stats Extended</span>
|
||||||
</a>
|
</a>
|
||||||
{/* Star on GitHub */}
|
{/* Star on GitHub + theme toggle */}
|
||||||
<div className="flex ml-auto items-center text-base justify-center">
|
<div className="flex ml-auto items-center gap-2 text-base justify-center">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/stats-organization/github-stats-extended"
|
href="https://github.com/stats-organization/github-stats-extended"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
className="rounded-[0.25rem] shadow bg-neutral-content hover:opacity-90 text-neutral px-3 py-1 flex items-center"
|
||||||
>
|
>
|
||||||
<button
|
Star on
|
||||||
type="button"
|
<GithubIcon className="ml-1.5 w-5 h-5" />
|
||||||
className="rounded-[0.25rem] shadow bg-gray-200 hover:bg-gray-300 text-black px-3 py-1 flex items-center"
|
|
||||||
>
|
|
||||||
Star on
|
|
||||||
<GithubIcon className="ml-1.5 w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</a>
|
</a>
|
||||||
|
<ThemePicker />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import { CardType } from "../../models/CardType";
|
import { CardType } from "../../models/CardType";
|
||||||
import { STAGE_LABELS } from "../../models/Stage";
|
import { STAGE_LABELS } from "../../models/Stage";
|
||||||
import type { StageIndex } from "../../models/Stage";
|
import type { StageIndex } from "../../models/Stage";
|
||||||
|
import { useTheme } from "../../redux/selectors/themeSelectors";
|
||||||
import {
|
import {
|
||||||
useIsAuthenticated,
|
useIsAuthenticated,
|
||||||
usePrivateAccess,
|
usePrivateAccess,
|
||||||
@@ -83,7 +84,9 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
|
|||||||
const [enableAnimations, setEnableAnimations] = useState(true);
|
const [enableAnimations, setEnableAnimations] = useState(true);
|
||||||
const [usePercent, setUsePercent] = useState(false);
|
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) => {
|
const handleCardTypeChange = (cardType: CardType) => {
|
||||||
if (cardType === CardType.TOP_LANGS) {
|
if (cardType === CardType.TOP_LANGS) {
|
||||||
@@ -230,11 +233,14 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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="flex flex-col">
|
||||||
<div className="m-4 rounded-sm">
|
<div className="m-4 rounded-sm">
|
||||||
<div className="lg:p-4">
|
<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}
|
{STAGE_LABELS[stage].title}
|
||||||
</h1>
|
</h1>
|
||||||
<div>
|
<div>
|
||||||
@@ -245,7 +251,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
|
|||||||
<a
|
<a
|
||||||
href={`https://github.com/${userId}`}
|
href={`https://github.com/${userId}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-blue-500 hover:underline font-semibold"
|
className="text-primary hover:underline font-semibold"
|
||||||
>
|
>
|
||||||
{userId}
|
{userId}
|
||||||
</a>
|
</a>
|
||||||
@@ -328,7 +334,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
|
|||||||
setWakatimeUser={setWakatimeUser}
|
setWakatimeUser={setWakatimeUser}
|
||||||
usePercent={usePercent}
|
usePercent={usePercent}
|
||||||
setUsePercent={setUsePercent}
|
setUsePercent={setUsePercent}
|
||||||
fullSuffix={fullSuffix}
|
fullSuffix={fullSuffix + themeParam}
|
||||||
setStage={setStage}
|
setStage={setStage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -382,6 +388,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
|
theme={theme}
|
||||||
themeSuffix={themeSuffix}
|
themeSuffix={themeSuffix}
|
||||||
guestHint={
|
guestHint={
|
||||||
isAuthenticated
|
isAuthenticated
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export function CustomizeStage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-wrap">
|
<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) && (
|
{(cardType === CardType.STATS || cardType === CardType.TOP_LANGS) && (
|
||||||
<TextSection
|
<TextSection
|
||||||
title="Username"
|
title="Username"
|
||||||
@@ -126,7 +126,7 @@ export function CustomizeStage({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setStage(0);
|
setStage(0);
|
||||||
}}
|
}}
|
||||||
className="underline text-blue-900"
|
className="underline text-primary"
|
||||||
>
|
>
|
||||||
log in
|
log in
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
@@ -170,7 +170,7 @@ export function CustomizeStage({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setStage(0);
|
setStage(0);
|
||||||
}}
|
}}
|
||||||
className="underline text-blue-900"
|
className="underline text-primary"
|
||||||
>
|
>
|
||||||
log in
|
log in
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
@@ -214,7 +214,7 @@ export function CustomizeStage({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setStage(0);
|
setStage(0);
|
||||||
}}
|
}}
|
||||||
className="underline text-blue-900"
|
className="underline text-primary"
|
||||||
>
|
>
|
||||||
log in
|
log in
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
@@ -251,7 +251,7 @@ export function CustomizeStage({
|
|||||||
<a
|
<a
|
||||||
href="https://wakatime.com/"
|
href="https://wakatime.com/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="underline text-blue-900"
|
className="underline text-primary"
|
||||||
>
|
>
|
||||||
WakaTime
|
WakaTime
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
@@ -411,7 +411,7 @@ export function CustomizeStage({
|
|||||||
<a
|
<a
|
||||||
href="https://github.com/stats-organization/github-stats-extended/blob/master/docs/advanced_documentation.md"
|
href="https://github.com/stats-organization/github-stats-extended/blob/master/docs/advanced_documentation.md"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="underline text-blue-900"
|
className="underline text-primary"
|
||||||
>
|
>
|
||||||
customization documentation
|
customization documentation
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { clsx } from "clsx";
|
|
||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { saveSvgAsPng } from "save-svg-as-png";
|
import { saveSvgAsPng } from "save-svg-as-png";
|
||||||
|
|
||||||
import { CardImage } from "../../../components/Card/CardImage";
|
import { CardImage } from "../../../components/Card/CardImage";
|
||||||
|
import { getCardThemeBackdrop } from "../../../components/Card/themeBackdrop";
|
||||||
import { Button } from "../../../components/Generic/Button";
|
import { Button } from "../../../components/Generic/Button";
|
||||||
import { HOST } from "../../../constants";
|
import { HOST } from "../../../constants";
|
||||||
|
import { useTheme } from "../../../redux/selectors/themeSelectors";
|
||||||
|
|
||||||
interface DisplayStageProps {
|
interface DisplayStageProps {
|
||||||
filename: string;
|
filename: string;
|
||||||
link: string;
|
link: string;
|
||||||
|
theme: string;
|
||||||
themeSuffix: string;
|
themeSuffix: string;
|
||||||
guestHint: string | null;
|
guestHint: string | null;
|
||||||
}
|
}
|
||||||
@@ -17,9 +19,12 @@ interface DisplayStageProps {
|
|||||||
export function DisplayStage({
|
export function DisplayStage({
|
||||||
filename,
|
filename,
|
||||||
link,
|
link,
|
||||||
|
theme,
|
||||||
themeSuffix,
|
themeSuffix,
|
||||||
guestHint,
|
guestHint,
|
||||||
}: DisplayStageProps): JSX.Element {
|
}: DisplayStageProps): JSX.Element {
|
||||||
|
const { isDark } = useTheme();
|
||||||
|
|
||||||
const downloadPNG = () => {
|
const downloadPNG = () => {
|
||||||
saveSvgAsPng(
|
saveSvgAsPng(
|
||||||
document.getElementById("svgWrapper")?.shadowRoot?.firstElementChild
|
document.getElementById("svgWrapper")?.shadowRoot?.firstElementChild
|
||||||
@@ -61,7 +66,7 @@ export function DisplayStage({
|
|||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-wrap">
|
<div className="w-full flex flex-wrap">
|
||||||
<div className="h-auto lg:w-2/5 md:w-1/2">
|
<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">
|
<div className="flex flex-col items-center">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
@@ -82,10 +87,8 @@ export function DisplayStage({
|
|||||||
].map((item) => (
|
].map((item) => (
|
||||||
<Button
|
<Button
|
||||||
key={item.title}
|
key={item.title}
|
||||||
className={clsx("m-4 w-60 flex justify-center", {
|
variant={item.highlight ? "primary" : "soft"}
|
||||||
"bg-blue-500 hover:bg-blue-600 text-white": item.highlight,
|
className="m-4 w-60 flex justify-center"
|
||||||
"bg-white hover:bg-gray-100 text-black": !item.highlight,
|
|
||||||
})}
|
|
||||||
onClick={item.onClick}
|
onClick={item.onClick}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
@@ -96,10 +99,14 @@ export function DisplayStage({
|
|||||||
</div>
|
</div>
|
||||||
</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 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
|
<CardImage
|
||||||
imageSrc={`${themeSuffix}&disable_animations=true`}
|
imageSrc={`${themeSuffix}&disable_animations=true`}
|
||||||
stage={4}
|
stage={4}
|
||||||
|
className="flex justify-center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function LoginAccountDeleteModal(
|
|||||||
<div className="fixed left-0 top-0 w-full h-full">
|
<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-full h-full flex justify-center items-center">
|
||||||
<div
|
<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}
|
ref={wrapperRef}
|
||||||
>
|
>
|
||||||
<p className="mb-1 text-2xl">Delete Account</p>
|
<p className="mb-1 text-2xl">Delete Account</p>
|
||||||
@@ -57,16 +57,10 @@ export function LoginAccountDeleteModal(
|
|||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
<div className="flex flex-wrap">
|
<div className="flex flex-wrap">
|
||||||
<Button
|
<Button variant="primary" onClick={onClose}>
|
||||||
className="bg-blue-500 hover:bg-blue-600 text-white rounded-[0.25rem]"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="error" className="ml-auto" onClick={onConfirm}>
|
||||||
className="bg-gray-200 hover:bg-gray-300 ml-auto rounded-[0.25rem] text-red-600 border-2"
|
|
||||||
onClick={onConfirm}
|
|
||||||
>
|
|
||||||
Delete Account
|
Delete Account
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,14 +56,17 @@ export function LoginAccountManagement(): JSX.Element {
|
|||||||
<a
|
<a
|
||||||
href={`https://${HOST}/api/downgrade?user_key=${userKey as string}`}
|
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" />
|
<GithubIcon className="w-6 h-6" />
|
||||||
<span className="ml-2 xl:text-lg">
|
<span className="ml-2 xl:text-lg">
|
||||||
Downgrade to Public Access
|
Downgrade to Public Access
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</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
|
Switch to public access if you prefer not to share private
|
||||||
contributions.
|
contributions.
|
||||||
</p>
|
</p>
|
||||||
@@ -71,14 +74,17 @@ export function LoginAccountManagement(): JSX.Element {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<a href={GITHUB_PRIVATE_AUTH_URL}>
|
<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" />
|
<GithubIcon className="w-6 h-6" />
|
||||||
<span className="ml-2 xl:text-lg">
|
<span className="ml-2 xl:text-lg">
|
||||||
Upgrade to Private Access
|
Upgrade to Private Access
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</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
|
Upgrade to include contributions in private repositories for more
|
||||||
complete and accurate stats.
|
complete and accurate stats.
|
||||||
</p>
|
</p>
|
||||||
@@ -89,12 +95,15 @@ export function LoginAccountManagement(): JSX.Element {
|
|||||||
{/* Delete Account Button */}
|
{/* Delete Account Button */}
|
||||||
<div className="mt-6 flex items-center gap-4">
|
<div className="mt-6 flex items-center gap-4">
|
||||||
<Button
|
<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}
|
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>
|
</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
|
This will delete your GitHub-Stats-Extended account and then redirect
|
||||||
you to a GitHub screen where you can revoke your access token.
|
you to a GitHub screen where you can revoke your access token.
|
||||||
</p>
|
</p>
|
||||||
@@ -103,12 +112,13 @@ export function LoginAccountManagement(): JSX.Element {
|
|||||||
{/* Logout Button */}
|
{/* Logout Button */}
|
||||||
<div className="mt-6 flex items-center gap-4">
|
<div className="mt-6 flex items-center gap-4">
|
||||||
<Button
|
<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}
|
onClick={handleLogout}
|
||||||
>
|
>
|
||||||
<span className="xl:text-lg">Log Out</span>
|
<span className="xl:text-lg">Log Out</span>
|
||||||
</Button>
|
</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.
|
Log out from GitHub-Stats-Extended.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function LoginBox(props: LoginBoxProps): JSX.Element {
|
|||||||
<div className="h-full flex flex-wrap">
|
<div className="h-full flex flex-wrap">
|
||||||
<div className={clsx("md:flex", { "opacity-25": isOpaque })}>
|
<div className={clsx("md:flex", { "opacity-25": isOpaque })}>
|
||||||
<div className="lg:block lg:w-3/5 lg:p-8">
|
<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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
DEMO_USER,
|
DEMO_USER,
|
||||||
DEMO_WAKATIME_USER,
|
DEMO_WAKATIME_USER,
|
||||||
} from "../../../../constants";
|
} from "../../../../constants";
|
||||||
|
import { useTheme } from "../../../../redux/selectors/themeSelectors";
|
||||||
|
|
||||||
const cards: Array<{ demoImageSrc: string }> = [
|
const cards: Array<{ demoImageSrc: string }> = [
|
||||||
{
|
{
|
||||||
@@ -39,6 +40,10 @@ function getCardXPosition(cardIndex: number): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LoginBoxDemoCards(): JSX.Element {
|
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 (
|
return (
|
||||||
<div className="w-full h-full lg:w-2/5 flex lg:flex-col lg:p-8 relative overflow-hidden">
|
<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">
|
<div className="relative w-full h-full">
|
||||||
@@ -52,7 +57,11 @@ export function LoginBoxDemoCards(): JSX.Element {
|
|||||||
marginBottom: "1%",
|
marginBottom: "1%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardImage imageSrc={card.demoImageSrc} compact={false} stage={0} />
|
<CardImage
|
||||||
|
imageSrc={`${card.demoImageSrc}${themeParam}`}
|
||||||
|
compact={false}
|
||||||
|
stage={0}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,24 +20,30 @@ export function LoginOptions(props: LoginOptionsProps): JSX.Element {
|
|||||||
<LoginBox>
|
<LoginBox>
|
||||||
<div className="flex items-center gap-4 mb-4">
|
<div className="flex items-center gap-4 mb-4">
|
||||||
<a href={GITHUB_PUBLIC_AUTH_URL}>
|
<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" />
|
<GithubIcon className="w-6 h-6" />
|
||||||
<span className="ml-2 xl:text-lg">GitHub Public Access</span>
|
GitHub Public Access
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</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.
|
Generate stats based on your contributions in public repositories.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 mb-4">
|
<div className="flex items-center gap-4 mb-4">
|
||||||
<a href={GITHUB_PRIVATE_AUTH_URL}>
|
<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" />
|
<GithubIcon className="w-6 h-6" />
|
||||||
<span className="ml-2 xl:text-lg">GitHub Private Access</span>
|
GitHub Private Access
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</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
|
Include contributions from private repositories for more complete and
|
||||||
accurate stats.
|
accurate stats.
|
||||||
</p>
|
</p>
|
||||||
@@ -45,12 +51,13 @@ export function LoginOptions(props: LoginOptionsProps): JSX.Element {
|
|||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button
|
<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}
|
onClick={onContinueAsGuest}
|
||||||
>
|
>
|
||||||
<span className="ml-2 xl:text-lg">Continue as Guest</span>
|
Continue as Guest
|
||||||
</Button>
|
</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
|
Explore options using sample data. Insert your own username in the
|
||||||
last step.
|
last step.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
DEMO_WAKATIME_USER,
|
DEMO_WAKATIME_USER,
|
||||||
} from "../../../constants";
|
} from "../../../constants";
|
||||||
import { CardType } from "../../../models/CardType";
|
import { CardType } from "../../../models/CardType";
|
||||||
|
import { useTheme } from "../../../redux/selectors/themeSelectors";
|
||||||
import { useUserId } from "../../../redux/selectors/userSelectors";
|
import { useUserId } from "../../../redux/selectors/userSelectors";
|
||||||
|
|
||||||
interface SelectCardStageProps {
|
interface SelectCardStageProps {
|
||||||
@@ -21,6 +22,9 @@ export function SelectCardStage({
|
|||||||
onCardTypeChange,
|
onCardTypeChange,
|
||||||
}: SelectCardStageProps): JSX.Element {
|
}: SelectCardStageProps): JSX.Element {
|
||||||
const userId = useUserId(DEMO_USER);
|
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<
|
const options = useMemo<
|
||||||
Array<{
|
Array<{
|
||||||
@@ -34,37 +38,37 @@ export function SelectCardStage({
|
|||||||
{
|
{
|
||||||
title: "GitHub Stats Card",
|
title: "GitHub Stats Card",
|
||||||
description: "your overall GitHub statistics",
|
description: "your overall GitHub statistics",
|
||||||
demoImageSrc: `?username=${userId}&include_all_commits=true`,
|
demoImageSrc: `?username=${userId}&include_all_commits=true${themeParam}`,
|
||||||
cardType: CardType.STATS,
|
cardType: CardType.STATS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Top Languages Card",
|
title: "Top Languages Card",
|
||||||
description: "your most frequently used languages",
|
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,
|
cardType: CardType.TOP_LANGS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "GitHub Extra Pin",
|
title: "GitHub Extra Pin",
|
||||||
description:
|
description:
|
||||||
"pin more than 6 repositories in your profile using a GitHub profile readme",
|
"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,
|
cardType: CardType.PIN,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "GitHub Gist Pin",
|
title: "GitHub Gist Pin",
|
||||||
description:
|
description:
|
||||||
"pin gists in your GitHub profile using a GitHub profile readme",
|
"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,
|
cardType: CardType.GIST,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "WakaTime Stats Card",
|
title: "WakaTime Stats Card",
|
||||||
description: "your coding activity from WakaTime",
|
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,
|
cardType: CardType.WAKATIME,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[userId],
|
[userId, themeParam],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { themes } from "@stats-organization/github-readme-stats-core";
|
|||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
|
|
||||||
import { Card } from "../../../components/Card/Card";
|
import { Card } from "../../../components/Card/Card";
|
||||||
|
import {
|
||||||
|
getCardThemeBackdrop,
|
||||||
|
getThemeSortRank,
|
||||||
|
} from "../../../components/Card/themeBackdrop";
|
||||||
|
import { useTheme } from "../../../redux/selectors/themeSelectors";
|
||||||
|
|
||||||
const excludedThemes = [
|
const excludedThemes = [
|
||||||
"merko",
|
"merko",
|
||||||
@@ -12,9 +17,10 @@ const excludedThemes = [
|
|||||||
"holi",
|
"holi",
|
||||||
];
|
];
|
||||||
|
|
||||||
const themeList = Object.keys(themes).filter(
|
// Light themes first, adaptive themes in the middle, dark themes last.
|
||||||
(myTheme) => !excludedThemes.includes(myTheme),
|
const themeList = Object.keys(themes)
|
||||||
);
|
.filter((myTheme) => !excludedThemes.includes(myTheme))
|
||||||
|
.sort((a, b) => getThemeSortRank(a) - getThemeSortRank(b));
|
||||||
|
|
||||||
interface ThemeStageProps {
|
interface ThemeStageProps {
|
||||||
fullSuffix: string;
|
fullSuffix: string;
|
||||||
@@ -27,38 +33,49 @@ export function ThemeStage({
|
|||||||
fullSuffix,
|
fullSuffix,
|
||||||
onThemeChange,
|
onThemeChange,
|
||||||
}: ThemeStageProps): JSX.Element {
|
}: ThemeStageProps): JSX.Element {
|
||||||
|
const { isDark } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-wrap">
|
<div className="flex flex-wrap">
|
||||||
{themeList.map((myTheme) => (
|
{themeList.map((myTheme) => {
|
||||||
<button
|
const themeColors = themes[myTheme as keyof typeof themes];
|
||||||
className="p-2 lg:p-4"
|
return (
|
||||||
key={myTheme}
|
<button
|
||||||
type="button"
|
className="p-2 lg:p-4"
|
||||||
onClick={() => {
|
key={myTheme}
|
||||||
onThemeChange(myTheme);
|
type="button"
|
||||||
}}
|
onClick={() => {
|
||||||
>
|
onThemeChange(myTheme);
|
||||||
<Card
|
}}
|
||||||
title={myTheme}
|
>
|
||||||
description=""
|
<Card
|
||||||
imageSrc={`${fullSuffix}&theme=${myTheme}`}
|
title={myTheme}
|
||||||
selected={theme === myTheme}
|
description=""
|
||||||
stage={3}
|
imageSrc={`${fullSuffix}&theme=${myTheme}`}
|
||||||
/>
|
selected={theme === myTheme}
|
||||||
</button>
|
stage={3}
|
||||||
))}
|
backgroundColor={getCardThemeBackdrop(myTheme, isDark)}
|
||||||
|
titleColor={`#${
|
||||||
|
myTheme === "ambient_gradient" && !isDark
|
||||||
|
? (themes["ambient_gradient"].bg_color.split(",")[1] ?? "")
|
||||||
|
: themeColors.title_color
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="pl-10 pr-10">
|
<div className="pl-10 pr-10">
|
||||||
For more theme options check the{" "}
|
{"For more theme options check the "}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/stats-organization/github-stats-extended/blob/master/docs/advanced_documentation.md#themes"
|
href="https://github.com/stats-organization/github-stats-extended/blob/master/docs/advanced_documentation.md#themes"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="underline text-blue-900"
|
className="underline text-primary"
|
||||||
>
|
>
|
||||||
customization documentation
|
customization documentation
|
||||||
</a>{" "}
|
</a>
|
||||||
after you copied your card URL in step 5.
|
{" after you copied your card URL in step 5."}
|
||||||
</div>
|
</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 };
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -3,11 +3,13 @@ import { configureStore } from "@reduxjs/toolkit";
|
|||||||
import { USE_LOGGER } from "../constants";
|
import { USE_LOGGER } from "../constants";
|
||||||
|
|
||||||
import { loggerMiddleware } from "./logger";
|
import { loggerMiddleware } from "./logger";
|
||||||
|
import theme from "./slices/theme";
|
||||||
import user from "./slices/user";
|
import user from "./slices/user";
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
user,
|
user,
|
||||||
|
theme,
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) => {
|
middleware: (getDefaultMiddleware) => {
|
||||||
const middleware = getDefaultMiddleware();
|
const middleware = getDefaultMiddleware();
|
||||||
|
|||||||
@@ -85,6 +85,50 @@ You can look at a preview for [all available themes](../packages/core/src/themes
|
|||||||
|
|
||||||
Since GitHub will re-upload the cards and serve them from their [CDN](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-anonymized-urls), we can not infer the browser/GitHub theme on the server side. There are, however, four methods you can use to create dynamics themes on the client side.
|
Since GitHub will re-upload the cards and serve them from their [CDN](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-anonymized-urls), we can not infer the browser/GitHub theme on the server side. There are, however, four methods you can use to create dynamics themes on the client side.
|
||||||
|
|
||||||
|
##### Use GitHub's new media feature (recommended)
|
||||||
|
|
||||||
|
You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-19-specify-theme-context-for-images-in-markdown-beta/) in HTML to specify whether to display images for light or dark themes. This is done using the HTML `<picture>` element in combination with the `prefers-color-scheme` media feature.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<picture>
|
||||||
|
<source
|
||||||
|
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
/>
|
||||||
|
<img src="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true" /> <!-- light mode -->
|
||||||
|
</picture>
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>:eyes: Show example</summary>
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<source
|
||||||
|
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
/>
|
||||||
|
<img src="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true" /> <!-- light mode -->
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
##### Use GitHub's theme context tag
|
||||||
|
|
||||||
|
You can use [GitHub's theme context](https://github.blog/changelog/2021-11-24-specify-theme-context-for-images-in-markdown/) tags to switch the theme based on the user GitHub theme automatically. This is done by appending `#gh-dark-mode-only` or `#gh-light-mode-only` to the end of an image URL. This tag will define whether the image specified in the markdown is only shown to viewers using a light or a dark GitHub theme:
|
||||||
|
|
||||||
|
```md
|
||||||
|
[](https://github.com/stats-organization/github-stats-extended#gh-dark-mode-only)
|
||||||
|
[](https://github.com/stats-organization/github-stats-extended#gh-light-mode-only)
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>:eyes: Show example</summary>
|
||||||
|
|
||||||
|
[](https://github.com/stats-organization/github-stats-extended#gh-dark-mode-only)
|
||||||
|
[](https://github.com/stats-organization/github-stats-extended#gh-light-mode-only)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
##### Use the transparent theme
|
##### Use the transparent theme
|
||||||
|
|
||||||
We have included a `transparent` theme that has a transparent background. This theme is optimized to look good on GitHub's dark and light default themes. You can enable this theme using the `&theme=transparent` parameter like so:
|
We have included a `transparent` theme that has a transparent background. This theme is optimized to look good on GitHub's dark and light default themes. You can enable this theme using the `&theme=transparent` parameter like so:
|
||||||
@@ -115,58 +159,6 @@ You can use the `bg_color` parameter to make any of [the available themes](../pa
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
##### Use GitHub's theme context tag
|
|
||||||
|
|
||||||
You can use [GitHub's theme context](https://github.blog/changelog/2021-11-24-specify-theme-context-for-images-in-markdown/) tags to switch the theme based on the user GitHub theme automatically. This is done by appending `#gh-dark-mode-only` or `#gh-light-mode-only` to the end of an image URL. This tag will define whether the image specified in the markdown is only shown to viewers using a light or a dark GitHub theme:
|
|
||||||
|
|
||||||
```md
|
|
||||||
[](https://github.com/stats-organization/github-stats-extended#gh-dark-mode-only)
|
|
||||||
[](https://github.com/stats-organization/github-stats-extended#gh-light-mode-only)
|
|
||||||
```
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>:eyes: Show example</summary>
|
|
||||||
|
|
||||||
[](https://github.com/stats-organization/github-stats-extended#gh-dark-mode-only)
|
|
||||||
[](https://github.com/stats-organization/github-stats-extended#gh-light-mode-only)
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
##### Use GitHub's new media feature
|
|
||||||
|
|
||||||
You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-19-specify-theme-context-for-images-in-markdown-beta/) in HTML to specify whether to display images for light or dark themes. This is done using the HTML `<picture>` element in combination with the `prefers-color-scheme` media feature.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<picture>
|
|
||||||
<source
|
|
||||||
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
|
|
||||||
media="(prefers-color-scheme: dark)"
|
|
||||||
/>
|
|
||||||
<source
|
|
||||||
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true"
|
|
||||||
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
|
|
||||||
/>
|
|
||||||
<img src="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true" />
|
|
||||||
</picture>
|
|
||||||
```
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>:eyes: Show example</summary>
|
|
||||||
|
|
||||||
<picture>
|
|
||||||
<source
|
|
||||||
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
|
|
||||||
media="(prefers-color-scheme: dark)"
|
|
||||||
/>
|
|
||||||
<source
|
|
||||||
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true"
|
|
||||||
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
|
|
||||||
/>
|
|
||||||
<img src="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true" />
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Customization
|
### Customization
|
||||||
|
|
||||||
You can customize the appearance of all your cards however you wish with URL parameters.
|
You can customize the appearance of all your cards however you wish with URL parameters.
|
||||||
|
|||||||
+18
-37
@@ -1,12 +1,12 @@
|
|||||||
# Deploy on your own
|
# Run It Yourself
|
||||||
|
|
||||||
Since the GitHub API only allows a limited number of requests per hour, my `https://github-stats-extended.vercel.app/api` could possibly hit the rate limiter. If you deploy it yourself via GitHub Actions or your own hosted instance, then you do not have to worry about anything. Also, if you don't want to give my GitHub-Stats-Extended instance access to your private contributions but still want to include these contributions in your stats, you can simply host your own instance.
|
We cache generated cards for a few hours to avoid potential rate-limiting in the GitHub API or on Vercel. If you want to set your own cache duration or you want to include private contributions in your stats without granting our hosted version of GitHub-Stats-Extended access to your private contributions, you can run GitHub-Stats-Extended on your own.
|
||||||
|
|
||||||
GitHub Actions is the simplest setup with static SVGs stored in your repo but less frequent updates, while self-hosting takes more work and can serve fresher stats (with caching).
|
GitHub Actions is the simplest setup with static SVGs stored in your repo but less frequent updates, while self-hosting GitHub-Stats-Extended on Vercel takes more work and can serve fresher stats (with caching).
|
||||||
|
|
||||||
## GitHub Actions
|
## GitHub Action
|
||||||
|
|
||||||
GitHub Actions generates static SVGs and avoids per-request API calls. By default it uses `GITHUB_TOKEN` (public stats only), for private stats, set a [PAT](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) as a secret and pass it to the action instead.
|
With [github-readme-stats-action](https://github.com/stats-organization/github-readme-stats-action) you can generate static cards in your GitHub Actions workflow, commit them to your profile repository, and embed them directly from there. This avoids any per-request API calls.
|
||||||
|
|
||||||
Create `/.github/workflows/grs.yml` in your profile repo (`USERNAME/USERNAME`):
|
Create `/.github/workflows/grs.yml` in your profile repo (`USERNAME/USERNAME`):
|
||||||
|
|
||||||
@@ -15,17 +15,21 @@ name: Update README cards
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 3 * * *"
|
- cron: "0 0 * * *" # Runs once daily at midnight
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Generate stats card
|
- name: Generate stats card
|
||||||
uses: readme-tools/github-readme-stats-action@v1
|
uses: stats-organization/github-readme-stats-action@v2
|
||||||
with:
|
with:
|
||||||
card: stats
|
card: stats
|
||||||
options: username=${{ github.repository_owner }}&show_icons=true
|
options: username=${{ github.repository_owner }}&show_icons=true
|
||||||
@@ -34,14 +38,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Commit cards
|
- name: Commit cards
|
||||||
run: |
|
run: |
|
||||||
git config user.name "github-actions"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions@users.noreply.github.com"
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
git add profile/*.svg
|
git add profile/*.svg
|
||||||
git commit -m "Update README cards" || exit 0
|
git commit -m "Update README cards" || exit 0
|
||||||
git push
|
git push
|
||||||
```
|
```
|
||||||
|
|
||||||
Then embed from your profile README:
|
Then embed from your [profile README](https://docs.github.com/en/account-and-profile/how-tos/profile-customization/managing-your-profile-readme#adding-a-profile-readme):
|
||||||
|
|
||||||
```md
|
```md
|
||||||

|

|
||||||
@@ -49,7 +53,7 @@ Then embed from your profile README:
|
|||||||
|
|
||||||
See more options and examples in the [GitHub Readme Stats Action README](https://github.com/stats-organization/github-readme-stats-action#readme).
|
See more options and examples in the [GitHub Readme Stats Action README](https://github.com/stats-organization/github-readme-stats-action#readme).
|
||||||
|
|
||||||
## Self-hosted (Vercel/Other)
|
## Self-hosted on Vercel
|
||||||
|
|
||||||
Running your own instance avoids public rate limits and gives you full control over caching, tokens, and private stats.
|
Running your own instance avoids public rate limits and gives you full control over caching, tokens, and private stats.
|
||||||
|
|
||||||
@@ -88,14 +92,11 @@ Selecting the right scopes for your token is important in case you want to displ
|
|||||||
|
|
||||||
### On Vercel
|
### On Vercel
|
||||||
|
|
||||||
<b>:film\_projector: [Check Out Step By Step Video Tutorial By @codeSTACKr](https://youtu.be/n6d4KHSKqGk?t=107)</b>
|
|
||||||
|
|
||||||
Click on the deploy button to get started!
|
Click on the deploy button to get started!
|
||||||
|
|
||||||
[](https://vercel.com/import/project?template=https://github.com/stats-organization/github-stats-extended)
|
[](https://vercel.com/import/project?template=https://github.com/stats-organization/github-stats-extended)
|
||||||
|
|
||||||
<details>
|
<b>Recommended: Step-by-step guide on setting up your own Vercel instance</b>
|
||||||
<summary><b>:hammer_and_wrench: Recommended: Step-by-step guide on setting up your own Vercel instance</b></summary>
|
|
||||||
|
|
||||||
1. Go to [vercel.com](https://vercel.com/).
|
1. Go to [vercel.com](https://vercel.com/).
|
||||||
2. Click on `Log in`.
|
2. Click on `Log in`.
|
||||||
@@ -122,27 +123,7 @@ Click on the deploy button to get started!
|
|||||||
12. Click deploy, and you're good to go. See your domains to use the API!
|
12. Click deploy, and you're good to go. See your domains to use the API!
|
||||||
13. optional: add an SQL database; by using e.g. the ["Nile" integration](https://vercel.com/marketplace/nile) or by manually setting the environment variable `POSTGRES_URL`
|
13. optional: add an SQL database; by using e.g. the ["Nile" integration](https://vercel.com/marketplace/nile) or by manually setting the environment variable `POSTGRES_URL`
|
||||||
14. optional: [create your own OAuth App](https://github.com/settings/developers) and set environment variables `OAUTH_REDIRECT_URI`, `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` on Vercel accordingly
|
14. optional: [create your own OAuth App](https://github.com/settings/developers) and set environment variables `OAUTH_REDIRECT_URI`, `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` on Vercel accordingly
|
||||||
15. optional: in addition to the Vercel project based on the `apps/backend` folder, create a second project based on the `apps/frontend` folder. No environment variables needed.
|
15. optional: set the environment variable `TURBO_PLATFORM_ENV_DISABLED` to `true` to disable the build-time warning from [turbo](https://turborepo.dev/) about environment variables missing from "turbo.json" - This warning is not relevant in our project.
|
||||||
16. optional: set the environment variable `TURBO_PLATFORM_ENV_DISABLED` to `true` to disable the build-time warning from [turbo](https://turborepo.dev/) about environment variables missing from "turbo.json" - This warning is not relevant in our project.
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### On other platforms
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> This way of using GitHub-Stats-Extended is not officially supported and was added to cater to some particular use cases where Vercel could not be used (e.g. [#2341](https://github.com/anuraghazra/github-readme-stats/discussions/2341)). The support for this method, therefore, is limited.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><b>:hammer_and_wrench: Step-by-step guide for deploying on other platforms</b></summary>
|
|
||||||
|
|
||||||
1. Fork or clone this repo as per your needs
|
|
||||||
2. Move `express` from the devDependencies to the dependencies section of `package.json`
|
|
||||||
<https://github.com/anuraghazra/github-readme-stats/blob/ba7c2f8b55eac8452e479c8bd38b044d204d0424/package.json#L54-L61>
|
|
||||||
3. Run `npm i` if needed (initial setup)
|
|
||||||
4. Run `node express.js` to start the server, or set the entry point to `express.js` in `package.json` if you're deploying on a managed service
|
|
||||||
<https://github.com/anuraghazra/github-readme-stats/blob/ba7c2f8b55eac8452e479c8bd38b044d204d0424/package.json#L11>
|
|
||||||
5. You're done 🎉
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Available environment variables
|
### Available environment variables
|
||||||
|
|
||||||
|
|||||||
+16
-7
@@ -25,14 +25,14 @@ The resolution of this most requested feature in github-readme-stats was [origin
|
|||||||
|
|
||||||
GitHub-Stats-Extended proactively precomputes and caches cards. This solves the problem where [cards wouldn't load on the first try](https://github.com/anuraghazra/github-readme-stats/issues/2603). It also gives GitHub-Stats-Extended more time while generating cards in the background, which allows it to fetch more repo data:
|
GitHub-Stats-Extended proactively precomputes and caches cards. This solves the problem where [cards wouldn't load on the first try](https://github.com/anuraghazra/github-readme-stats/issues/2603). It also gives GitHub-Stats-Extended more time while generating cards in the background, which allows it to fetch more repo data:
|
||||||
|
|
||||||
|
### Multi-page fetching for accurate star counts
|
||||||
|
GitHub-Stats-Extended fetches up to 1000 of your starred repositories to accurately compute your stars count. In github-readme-stats, this is limited to 100 repos because github-readme-stats doesn't have the above-mentioned performance improvements.
|
||||||
|
|
||||||
### Customization of top languages card
|
### Customization of top languages card
|
||||||
GitHub-Stats-Extended allows you to display your top languages without any numbers via the `hide_values` parameter. And the `prog_bar_bg_color` parameter allows you to customize the background color of the progress bars, e.g. by setting it to white:
|
GitHub-Stats-Extended allows you to display your top languages without any numbers via the `hide_values` parameter. And the `prog_bar_bg_color` parameter allows you to customize the background color of the progress bars, e.g. by setting it to white:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Multi-page fetching for accurate star counts
|
|
||||||
GitHub-Stats-Extended fetches up to 1000 of your starred repositories to accurately compute your stars count. In github-readme-stats, this is limited to 100 repos because github-readme-stats doesn't have the above-mentioned performance improvements.
|
|
||||||
|
|
||||||
### Private contributions support
|
### Private contributions support
|
||||||
GitHub-Stats-Extended can include private contributions in your stats cards. You no longer have to deploy your own instance for that. Just log into the [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) via the "GitHub Private Access" button (or click "Upgrade to Private Access" if already logged in). This will allow GitHub-Stats-Extended to see your private contributions.
|
GitHub-Stats-Extended can include private contributions in your stats cards. You no longer have to deploy your own instance for that. Just log into the [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) via the "GitHub Private Access" button (or click "Upgrade to Private Access" if already logged in). This will allow GitHub-Stats-Extended to see your private contributions.
|
||||||
|
|
||||||
@@ -72,12 +72,21 @@ One of the valued maintainers [wrote](https://github.com/anuraghazra/github-read
|
|||||||
>
|
>
|
||||||
> The volume is overwhelming for the small team, especially taking into account that right now I'm alone online and working only sometimes when I have a free hours, so it took some time to get to your PR.
|
> The volume is overwhelming for the small team, especially taking into account that right now I'm alone online and working only sometimes when I have a free hours, so it took some time to get to your PR.
|
||||||
|
|
||||||
So I decided to implement some of the highly requested features and make the enhanced project available to everyone. Since the initial release of this fork I have been in contact with the maintainers of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) and we decided to join forces. github-stats-extended is now in the process of becoming the successor of github-readme-stats.
|
So [@martin-mfg](https://github.com/martin-mfg) decided to fork the project, implement some of the highly requested features and make the enhanced project available to everyone. Since the initial release of this fork @martin-mfg joined forces with the maintainers of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) and GitHub-Stats-Extended is now becoming the successor of github-readme-stats.
|
||||||
|
|
||||||
## Compatibility Notes
|
## Compatibility Notes
|
||||||
|
|
||||||
If there should be any changes in github-readme-stats in the future, they will be synced into this fork.
|
GitHub-Stats-Extended aims to be fully compatible with [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). Additional functionality introduced in this fork has to be explicitly enabled via some parameter.
|
||||||
|
|
||||||
GitHub-Stats-Extended aims to be fully compatible with [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). Generally, additional functionality introduced in this fork has to be explicitly enabled via some parameter.
|
|
||||||
|
|
||||||
So you can change an existing stats card url from [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) to GitHub-Stats-Extended simply by changing the domain from `github-readme-stats.vercel.app` to `github-stats-extended.vercel.app`. The card will look the same.
|
So you can change an existing stats card url from [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) to GitHub-Stats-Extended simply by changing the domain from `github-readme-stats.vercel.app` to `github-stats-extended.vercel.app`. The card will look the same.
|
||||||
|
|
||||||
|
There is only one exception to this: GitHub-Stats-Extended improves line wrapping for multi-line gist and repository descriptions.
|
||||||
|
This should be an improvement for existing cards, but it still changes their appearance a bit.
|
||||||
|
|
||||||
|
Previously, line wrapping happened simply after 59 characters, with special handling for Chinese characters:
|
||||||
|
|
||||||
|
<img width="400" height="140" alt="character-based" src="https://github.com/user-attachments/assets/1cf7edba-7f6c-4a37-89d7-334cbe54f0f1" />
|
||||||
|
|
||||||
|
GitHub-Stats-Extended now takes the actual width of each character into account:
|
||||||
|
|
||||||
|
<img width="400" height="150" alt="new-server-side-calculation" src="https://github.com/user-attachments/assets/277b92df-b2a9-48be-bd2a-d28c1d6764c5" />
|
||||||
@@ -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
|
// We don't need this we have typescript
|
||||||
"jsdoc/require-returns": "off",
|
"jsdoc/require-returns": "off",
|
||||||
"jsdoc/require-returns-description": "off",
|
"jsdoc/require-returns-description": "off",
|
||||||
|
|||||||
+12
-12
@@ -4,25 +4,25 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
|
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "2.0.5",
|
"@eslint/compat": "2.1.0",
|
||||||
"@eslint/js": "10.0.1",
|
"@eslint/js": "10.0.1",
|
||||||
"@playwright/test": "1.59.1",
|
"@playwright/test": "1.60.0",
|
||||||
"@types/node": "24.12.2",
|
"@types/node": "24.13.2",
|
||||||
"@vitest/coverage-v8": "catalog:default",
|
"@vitest/coverage-v8": "catalog:default",
|
||||||
"eslint": "10.2.1",
|
"eslint": "10.4.1",
|
||||||
"eslint-import-resolver-typescript": "4.4.4",
|
"eslint-import-resolver-typescript": "4.4.5",
|
||||||
"eslint-plugin-import-x": "4.16.2",
|
"eslint-plugin-import-x": "4.16.2",
|
||||||
"eslint-plugin-jsdoc": "62.9.0",
|
"eslint-plugin-jsdoc": "63.0.2",
|
||||||
"eslint-plugin-react": "7.37.5",
|
"eslint-plugin-react": "7.37.5",
|
||||||
"eslint-plugin-react-hooks": "7.1.1",
|
"eslint-plugin-react-hooks": "7.1.1",
|
||||||
"globals": "17.5.0",
|
"globals": "17.6.0",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"knip": "6.7.0",
|
"knip": "6.16.1",
|
||||||
"lint-staged": "16.4.0",
|
"lint-staged": "17.0.7",
|
||||||
"prettier": "3.8.3",
|
"prettier": "3.8.4",
|
||||||
"turbo": "2.9.6",
|
"turbo": "2.9.18",
|
||||||
"typescript": "6.0.3",
|
"typescript": "6.0.3",
|
||||||
"typescript-eslint": "8.59.0",
|
"typescript-eslint": "8.61.0",
|
||||||
"vitest": "catalog:default"
|
"vitest": "catalog:default"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Core library powering [GitHub Stats Extended](https://github.com/stats-organization/github-stats-extended) and [GitHub Readme Stats Action](https://github.com/readme-tools/github-readme-stats-action).
|
Core library powering [GitHub Stats Extended](https://github.com/stats-organization/github-stats-extended) and [GitHub Readme Stats Action](https://github.com/stats-organization/github-readme-stats-action).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@stats-organization/github-readme-stats-core",
|
"name": "@stats-organization/github-readme-stats-core",
|
||||||
"version": "2.1.2",
|
"version": "2.1.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"homepage": "https://github-stats-extended.vercel.app/frontend",
|
"homepage": "https://github-stats-extended.vercel.app/frontend",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
@@ -83,12 +83,12 @@
|
|||||||
"@testing-library/jest-dom": "6.9.1",
|
"@testing-library/jest-dom": "6.9.1",
|
||||||
"@uppercod/css-to-object": "1.1.1",
|
"@uppercod/css-to-object": "1.1.1",
|
||||||
"axios-mock-adapter": "2.1.0",
|
"axios-mock-adapter": "2.1.0",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.2.0",
|
||||||
"jsdom": "29.0.2",
|
"jsdom": "catalog:default",
|
||||||
"vitest": "catalog:default"
|
"vitest": "catalog:default"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.15.2",
|
"axios": "catalog:default",
|
||||||
"emoji-name-map": "^2.0.3",
|
"emoji-name-map": "^2.0.3",
|
||||||
"github-username-regex": "^1.0.0"
|
"github-username-regex": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { renderGistCard } from "../cards/gist.js";
|
import { renderGistCard } from "../cards/gist.js";
|
||||||
import {
|
import {
|
||||||
MissingParamError,
|
MissingParamError,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { renderStatsCard } from "../cards/stats.js";
|
import { renderStatsCard } from "../cards/stats.js";
|
||||||
import {
|
import {
|
||||||
MissingParamError,
|
MissingParamError,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { renderRepoCard } from "../cards/repo.js";
|
import { renderRepoCard } from "../cards/repo.js";
|
||||||
import {
|
import {
|
||||||
MissingParamError,
|
MissingParamError,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { renderTopLanguages } from "../cards/top-languages.js";
|
import { renderTopLanguages } from "../cards/top-languages.js";
|
||||||
import {
|
import {
|
||||||
MissingParamError,
|
MissingParamError,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { renderWakatimeCard } from "../cards/wakatime.js";
|
import { renderWakatimeCard } from "../cards/wakatime.js";
|
||||||
import {
|
import {
|
||||||
MissingParamError,
|
MissingParamError,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { default as Card } from "../common/Card.js";
|
import { default as Card } from "../common/Card.js";
|
||||||
import { getCardColors } from "../common/color.js";
|
import { getCardColors } from "../common/color.js";
|
||||||
import { kFormatter, wrapTextMultiline } from "../common/fmt.js";
|
import { kFormatter, wrapTextMultiline } from "../common/fmt.js";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { Card } from "../common/Card.js";
|
import { Card } from "../common/Card.js";
|
||||||
import { I18n } from "../common/I18n.js";
|
import { I18n } from "../common/I18n.js";
|
||||||
import { getCardColors } from "../common/color.js";
|
import { getCardColors } from "../common/color.js";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { Card } from "../common/Card.js";
|
import { Card } from "../common/Card.js";
|
||||||
import { I18n } from "../common/I18n.js";
|
import { I18n } from "../common/I18n.js";
|
||||||
import { getCardColors } from "../common/color.js";
|
import { getCardColors } from "../common/color.js";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { Card } from "../common/Card.js";
|
import { Card } from "../common/Card.js";
|
||||||
import { I18n } from "../common/I18n.js";
|
import { I18n } from "../common/I18n.js";
|
||||||
import { fallbackColor, getCardColors } from "../common/color.js";
|
import { fallbackColor, getCardColors } from "../common/color.js";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { Card } from "../common/Card.js";
|
import { Card } from "../common/Card.js";
|
||||||
import { I18n } from "../common/I18n.js";
|
import { I18n } from "../common/I18n.js";
|
||||||
import { getCardColors } from "../common/color.js";
|
import { getCardColors } from "../common/color.js";
|
||||||
|
|||||||
@@ -1,25 +1,46 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { encodeHTML } from "./html.js";
|
import { encodeHTML } from "./html.js";
|
||||||
import { flexLayout } from "./render.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 {
|
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.
|
* Creates a new card instance.
|
||||||
*
|
*
|
||||||
* @param {object} args Card arguments.
|
* @param props Card arguments.
|
||||||
* @param {number=} args.width Card width.
|
* @param props.width Card width.
|
||||||
* @param {number=} args.height Card height.
|
* @param props.height Card height.
|
||||||
* @param {number=} args.border_radius Card border radius.
|
* @param props.border_radius Card border radius.
|
||||||
* @param {string=} args.customTitle Card custom title.
|
* @param props.colors Card colors arguments.
|
||||||
* @param {string=} args.defaultTitle Card default title.
|
* @param props.customTitle Card custom title.
|
||||||
* @param {string=} args.titlePrefixIcon Card title prefix icon.
|
* @param props.defaultTitle Card default title.
|
||||||
* @param {object} [args.colors={}] Card colors arguments.
|
* @param props.titlePrefixIcon Card title prefix icon.
|
||||||
* @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.
|
|
||||||
*/
|
*/
|
||||||
constructor({
|
constructor({
|
||||||
width = 100,
|
width = 100,
|
||||||
@@ -29,6 +50,14 @@ class Card {
|
|||||||
customTitle,
|
customTitle,
|
||||||
defaultTitle = "",
|
defaultTitle = "",
|
||||||
titlePrefixIcon,
|
titlePrefixIcon,
|
||||||
|
}: {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
border_radius?: number;
|
||||||
|
colors?: CardColors;
|
||||||
|
customTitle?: string;
|
||||||
|
defaultTitle?: string;
|
||||||
|
titlePrefixIcon?: string;
|
||||||
}) {
|
}) {
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
@@ -40,10 +69,9 @@ class Card {
|
|||||||
|
|
||||||
// returns theme based colors with proper overrides and defaults
|
// returns theme based colors with proper overrides and defaults
|
||||||
this.colors = colors;
|
this.colors = colors;
|
||||||
this.title =
|
this.title = encodeHTML(
|
||||||
customTitle === undefined
|
customTitle === undefined ? defaultTitle : customTitle,
|
||||||
? encodeHTML(defaultTitle)
|
);
|
||||||
: encodeHTML(customTitle);
|
|
||||||
|
|
||||||
this.css = "";
|
this.css = "";
|
||||||
|
|
||||||
@@ -55,45 +83,44 @@ class Card {
|
|||||||
this.a11yDesc = "";
|
this.a11yDesc = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
disableAnimations(): void {
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
disableAnimations() {
|
|
||||||
this.animations = false;
|
this.animations = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} props The props object.
|
* @param props The props object.
|
||||||
* @param {string} props.title Accessibility title.
|
* @param props.title Accessibility title.
|
||||||
* @param {string} props.desc Accessibility description.
|
* @param props.desc Accessibility description.
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
setAccessibilityLabel({ title, desc }) {
|
setAccessibilityLabel({
|
||||||
|
title,
|
||||||
|
desc,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
}): void {
|
||||||
this.a11yTitle = title;
|
this.a11yTitle = title;
|
||||||
this.a11yDesc = desc;
|
this.a11yDesc = desc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} value The CSS to add to the card.
|
* @param value The CSS to add to the card.
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
setCSS(value) {
|
setCSS(value: string): void {
|
||||||
this.css = value;
|
this.css = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} value Whether to hide the border or not.
|
* @param value Whether to hide the border or not.
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
setHideBorder(value) {
|
setHideBorder(value: boolean): void {
|
||||||
this.hideBorder = value;
|
this.hideBorder = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} value Whether to hide the title or not.
|
* @param value Whether to hide the title or not.
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
setHideTitle(value) {
|
setHideTitle(value: boolean): void {
|
||||||
this.hideTitle = value;
|
this.hideTitle = value;
|
||||||
if (value) {
|
if (value) {
|
||||||
this.height -= 30;
|
this.height -= 30;
|
||||||
@@ -101,17 +128,16 @@ class Card {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} text The title to set.
|
* @param text The title to set.
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
setTitle(text) {
|
setTitle(text: string): void {
|
||||||
this.title = text;
|
this.title = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string} The rendered card title.
|
* @returns The rendered card title.
|
||||||
*/
|
*/
|
||||||
renderTitle() {
|
renderTitle(): string {
|
||||||
const titleText = `
|
const titleText = `
|
||||||
<text
|
<text
|
||||||
x="0"
|
x="0"
|
||||||
@@ -131,7 +157,7 @@ class Card {
|
|||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
>
|
>
|
||||||
${this.titlePrefixIcon}
|
${String(this.titlePrefixIcon)}
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
return `
|
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") {
|
if (typeof this.colors.bgColor !== "object") {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -161,13 +187,15 @@ class Card {
|
|||||||
<defs>
|
<defs>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="gradient"
|
id="gradient"
|
||||||
gradientTransform="rotate(${this.colors.bgColor[0]})"
|
gradientTransform="rotate(${String(this.colors.bgColor[0])})"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
${gradients.map((grad, index) => {
|
${gradients
|
||||||
let offset = (index * 100) / (gradients.length - 1);
|
.map((grad, index) => {
|
||||||
return `<stop offset="${offset}%" stop-color="#${grad}" />`;
|
const offset = (index * 100) / (gradients.length - 1);
|
||||||
})}
|
return `<stop offset="${offset}%" stop-color="#${grad}" />`;
|
||||||
|
})
|
||||||
|
.join(",")}
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
`
|
`
|
||||||
@@ -177,9 +205,9 @@ class Card {
|
|||||||
/**
|
/**
|
||||||
* Retrieves css animations for a card.
|
* Retrieves css animations for a card.
|
||||||
*
|
*
|
||||||
* @returns {string} Animation css.
|
* @returns Animation css.
|
||||||
*/
|
*/
|
||||||
getAnimations = () => {
|
getAnimations = (): string => {
|
||||||
return `
|
return `
|
||||||
/* Animations */
|
/* Animations */
|
||||||
@keyframes scaleInAnimation {
|
@keyframes scaleInAnimation {
|
||||||
@@ -202,10 +230,10 @@ class Card {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} body The inner body of the card.
|
* @param body The inner body of the card.
|
||||||
* @returns {string} The rendered card.
|
* @returns The rendered card.
|
||||||
*/
|
*/
|
||||||
render(body) {
|
render(body: string): string {
|
||||||
return `
|
return `
|
||||||
<svg
|
<svg
|
||||||
width="${this.width}"
|
width="${this.width}"
|
||||||
@@ -221,7 +249,7 @@ class Card {
|
|||||||
<style>
|
<style>
|
||||||
.header {
|
.header {
|
||||||
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
|
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;
|
animation: fadeInAnimation 0.8s ease-in-out forwards;
|
||||||
}
|
}
|
||||||
@supports(-moz-appearance: auto) {
|
@supports(-moz-appearance: auto) {
|
||||||
@@ -232,9 +260,9 @@ class Card {
|
|||||||
|
|
||||||
${this.getAnimations()}
|
${this.getAnimations()}
|
||||||
${
|
${
|
||||||
this.animations === false
|
this.animations
|
||||||
? `* { animation-duration: 0s !important; animation-delay: 0s !important; }`
|
? ""
|
||||||
: ""
|
: `* { animation-duration: 0s !important; animation-delay: 0s !important; }`
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -246,12 +274,12 @@ class Card {
|
|||||||
y="0.5"
|
y="0.5"
|
||||||
rx="${this.border_radius}"
|
rx="${this.border_radius}"
|
||||||
height="99%"
|
height="99%"
|
||||||
stroke="${this.colors.borderColor}"
|
stroke="${String(this.colors.borderColor)}"
|
||||||
width="${this.width - 1}"
|
width="${this.width - 1}"
|
||||||
fill="${
|
fill="${
|
||||||
typeof this.colors.bgColor === "object"
|
typeof this.colors.bgColor === "object"
|
||||||
? "url(#gradient)"
|
? "url(#gradient)"
|
||||||
: this.colors.bgColor
|
: String(this.colors.bgColor)
|
||||||
}"
|
}"
|
||||||
stroke-opacity="${this.hideBorder ? 0 : 1}"
|
stroke-opacity="${this.hideBorder ? 0 : 1}"
|
||||||
/>
|
/>
|
||||||
@@ -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 };
|
|
||||||
@@ -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";
|
import { themes } from "../themes/index.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a string is a valid hex color.
|
* Checks if a string is a valid hex color.
|
||||||
*
|
*
|
||||||
* @param {string} hexColor String to check.
|
* @param hexColor String to check.
|
||||||
* @returns {boolean} True if the given string is a valid hex color.
|
* @returns True if the given string is a valid hex color.
|
||||||
*/
|
*/
|
||||||
const isValidHexColor = (hexColor) => {
|
const isValidHexColor = (hexColor: string): boolean => {
|
||||||
return new RegExp(
|
return new RegExp(
|
||||||
/^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4})$/,
|
/^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4})$/,
|
||||||
).test(hexColor);
|
).test(hexColor);
|
||||||
@@ -17,10 +15,10 @@ const isValidHexColor = (hexColor) => {
|
|||||||
/**
|
/**
|
||||||
* Check if the given string is a valid gradient.
|
* Check if the given string is a valid gradient.
|
||||||
*
|
*
|
||||||
* @param {string[]} colors Array of colors.
|
* @param colors Array of colors.
|
||||||
* @returns {boolean} True if the given string is a valid gradient.
|
* @returns True if the given string is a valid gradient.
|
||||||
*/
|
*/
|
||||||
const isValidGradient = (colors) => {
|
const isValidGradient = (colors: Array<string>): boolean => {
|
||||||
return (
|
return (
|
||||||
colors.length > 2 &&
|
colors.length > 2 &&
|
||||||
colors.slice(1).every((color) => isValidHexColor(color))
|
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.
|
* Retrieves a gradient if color has more than one valid hex codes else a single color.
|
||||||
*
|
*
|
||||||
* @param {string} color The color to parse.
|
* @param color The color to parse.
|
||||||
* @param {string | string[]} fallbackColor The fallback color.
|
* @param fallbackColor The fallback color.
|
||||||
* @returns {string | string[]} The gradient or color.
|
* @returns The gradient or color.
|
||||||
*/
|
*/
|
||||||
const fallbackColor = (color, fallbackColor) => {
|
const fallbackColor = (
|
||||||
let gradient = null;
|
color: string | undefined,
|
||||||
|
fallbackColor: string | Array<string>,
|
||||||
let colors = color ? color.split(",") : [];
|
): string | Array<string> => {
|
||||||
|
const colors = color ? color.split(",") : [];
|
||||||
if (colors.length > 1 && isValidGradient(colors)) {
|
if (colors.length > 1 && isValidGradient(colors)) {
|
||||||
gradient = colors;
|
return colors;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (color !== undefined && isValidHexColor(color)) {
|
||||||
(gradient ? gradient : isValidHexColor(color) && `#${color}`) ||
|
return `#${color}`;
|
||||||
fallbackColor
|
}
|
||||||
);
|
|
||||||
|
return fallbackColor;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object containing card colors.
|
* 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.
|
* Returns theme based colors with proper overrides and defaults.
|
||||||
*
|
*
|
||||||
* @param {Object} args Function arguments.
|
* @param props Function arguments.
|
||||||
* @param {string=} args.title_color Card title color.
|
* @param props.title_color Card title color.
|
||||||
* @param {string=} args.text_color Card text color.
|
* @param props.text_color Card text color.
|
||||||
* @param {string=} args.icon_color Card icon color.
|
* @param props.icon_color Card icon color.
|
||||||
* @param {string=} args.bg_color Card background color.
|
* @param props.bg_color Card background color.
|
||||||
* @param {string=} args.border_color Card border color.
|
* @param props.border_color Card border color.
|
||||||
* @param {string=} args.ring_color Card ring color.
|
* @param props.ring_color Card ring color.
|
||||||
* @param {string=} args.theme Card theme.
|
* @param props.theme Card theme.
|
||||||
* @returns {CardColors} Card colors.
|
* @returns Card colors.
|
||||||
*/
|
*/
|
||||||
const getCardColors = ({
|
const getCardColors = ({
|
||||||
title_color,
|
title_color,
|
||||||
@@ -81,19 +81,26 @@ const getCardColors = ({
|
|||||||
border_color,
|
border_color,
|
||||||
ring_color,
|
ring_color,
|
||||||
theme,
|
theme,
|
||||||
}) => {
|
}: {
|
||||||
const defaultTheme = themes["default"];
|
title_color?: string;
|
||||||
const isThemeProvided =
|
text_color?: string;
|
||||||
theme !== null && theme !== undefined && theme in themes;
|
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
|
||||||
const selectedTheme = isThemeProvided ? themes[theme] : defaultTheme;
|
? themes[theme as keyof typeof themes]
|
||||||
|
: defaultTheme;
|
||||||
|
|
||||||
const defaultBorderColor =
|
const defaultBorderColor =
|
||||||
"border_color" in selectedTheme
|
"border_color" in selectedTheme
|
||||||
? selectedTheme.border_color
|
? selectedTheme.border_color
|
||||||
: // @ts-ignore
|
: defaultTheme.border_color;
|
||||||
defaultTheme.border_color;
|
|
||||||
|
|
||||||
// get the color provided by the user else the theme color
|
// get the color provided by the user else the theme color
|
||||||
// finally if both colors are invalid fallback to default theme
|
// 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
|
// get the color provided by the user else the theme color
|
||||||
// finally if both colors are invalid we use the titleColor
|
// finally if both colors are invalid we use the titleColor
|
||||||
const ringColor = fallbackColor(
|
// NOTE: no built-in theme defines `ring_color`, so it falls back to the title color.
|
||||||
// @ts-ignore
|
const ringColor = fallbackColor(ring_color, titleColor);
|
||||||
ring_color || selectedTheme.ring_color,
|
|
||||||
titleColor,
|
|
||||||
);
|
|
||||||
const iconColor = fallbackColor(
|
const iconColor = fallbackColor(
|
||||||
icon_color || selectedTheme.icon_color,
|
icon_color || selectedTheme.icon_color,
|
||||||
"#" + defaultTheme.icon_color,
|
"#" + defaultTheme.icon_color,
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
||||||
@@ -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 };
|
|
||||||
@@ -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";
|
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";
|
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 = {
|
const SECONDARY_ERROR_MESSAGES = {
|
||||||
MAX_RETRY:
|
MAX_RETRY:
|
||||||
@@ -28,16 +26,21 @@ const SECONDARY_ERROR_MESSAGES = {
|
|||||||
* Custom error class to handle custom GRS errors.
|
* Custom error class to handle custom GRS errors.
|
||||||
*/
|
*/
|
||||||
class CustomError extends Error {
|
class CustomError extends Error {
|
||||||
|
type: string;
|
||||||
|
secondaryMessage: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom error constructor.
|
* Custom error constructor.
|
||||||
*
|
*
|
||||||
* @param {string} message Error message.
|
* @param message Error message.
|
||||||
* @param {string} type Error type.
|
* @param type Error type.
|
||||||
*/
|
*/
|
||||||
constructor(message, type) {
|
constructor(message: string, type: string) {
|
||||||
super(message);
|
super(message);
|
||||||
this.type = type;
|
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";
|
static MAX_RETRY = "MAX_RETRY";
|
||||||
@@ -53,13 +56,16 @@ class CustomError extends Error {
|
|||||||
* Missing query parameter class.
|
* Missing query parameter class.
|
||||||
*/
|
*/
|
||||||
class MissingParamError extends Error {
|
class MissingParamError extends Error {
|
||||||
|
missedParams: Array<string>;
|
||||||
|
secondaryMessage: string | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Missing query parameter error constructor.
|
* Missing query parameter error constructor.
|
||||||
*
|
*
|
||||||
* @param {string[]} missedParams An array of missing parameters names.
|
* @param missedParams An array of missing parameters names.
|
||||||
* @param {string=} secondaryMessage Optional secondary message to display.
|
* @param secondaryMessage Optional secondary message to display.
|
||||||
*/
|
*/
|
||||||
constructor(missedParams, secondaryMessage) {
|
constructor(missedParams: Array<string>, secondaryMessage?: string) {
|
||||||
const msg = `Missing params ${missedParams
|
const msg = `Missing params ${missedParams
|
||||||
.map((p) => `"${p}"`)
|
.map((p) => `"${p}"`)
|
||||||
.join(", ")} make sure you pass the parameters in URL`;
|
.join(", ")} make sure you pass the parameters in URL`;
|
||||||
@@ -72,10 +78,10 @@ class MissingParamError extends Error {
|
|||||||
/**
|
/**
|
||||||
* Retrieve secondary message from an error object.
|
* Retrieve secondary message from an error object.
|
||||||
*
|
*
|
||||||
* @param {Error} err The error object.
|
* @param err The error object.
|
||||||
* @returns {string|undefined} The secondary message if available, otherwise undefined.
|
* @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"
|
return "secondaryMessage" in err && typeof err.secondaryMessage === "string"
|
||||||
? err.secondaryMessage
|
? err.secondaryMessage
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { encodeHTML } from "./html.js";
|
import { encodeHTML } from "./html.js";
|
||||||
import { splitWrappedText } from "./render.js";
|
import { splitWrappedText } from "./render.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves num with suffix k(thousands) precise to given decimal places.
|
* Retrieves num with suffix k(thousands) precise to given decimal places.
|
||||||
*
|
*
|
||||||
* @param {number} num The number to format.
|
* @param num The number to format.
|
||||||
* @param {number=} precision The number of decimal places to include.
|
* @param precision The number of decimal places to include.
|
||||||
* @returns {string|number} The formatted number.
|
* @returns The formatted number.
|
||||||
*/
|
*/
|
||||||
const kFormatter = (num, precision) => {
|
const kFormatter = (num: number, precision?: number): string | number => {
|
||||||
const abs = Math.abs(num);
|
const abs = Math.abs(num);
|
||||||
const sign = Math.sign(num);
|
const sign = Math.sign(num);
|
||||||
|
|
||||||
@@ -22,17 +20,17 @@ const kFormatter = (num, precision) => {
|
|||||||
return sign * abs;
|
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.
|
* Convert bytes to a human-readable string representation.
|
||||||
*
|
*
|
||||||
* @param {number} bytes The number of bytes to convert.
|
* @param bytes The number of bytes to convert.
|
||||||
* @returns {string} The human-readable representation of bytes.
|
* @returns The human-readable representation of bytes.
|
||||||
* @throws {Error} If bytes is negative or too large.
|
* @throws {Error} If bytes is negative or too large.
|
||||||
*/
|
*/
|
||||||
const formatBytes = (bytes) => {
|
const formatBytes = (bytes: number): string => {
|
||||||
if (bytes < 0) {
|
if (bytes < 0) {
|
||||||
throw new Error("Bytes must be a non-negative number");
|
throw new Error("Bytes must be a non-negative number");
|
||||||
}
|
}
|
||||||
@@ -45,23 +43,29 @@ const formatBytes = (bytes) => {
|
|||||||
const base = 1024;
|
const base = 1024;
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(base));
|
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");
|
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.
|
* Split text over multiple lines based on the card width.
|
||||||
*
|
*
|
||||||
* @param {string} text Text to split.
|
* @param text Text to split.
|
||||||
* @param {number} width Available wrap width in px.
|
* @param width Available wrap width in px.
|
||||||
* @param {number} fontSize Font size in px.
|
* @param fontSize Font size in px.
|
||||||
* @param {number} maxLines Maximum number of lines.
|
* @param maxLines Maximum number of lines.
|
||||||
* @returns {string[]} Array 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 wrapped = splitWrappedText(text, fontSize, width);
|
||||||
const lines = wrapped
|
const lines = wrapped
|
||||||
.map((line) => encodeHTML(line.trim()))
|
.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
|
// Add "..." to the last line if the text exceeds maxLines
|
||||||
if (wrapped.length > 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
|
// Remove empty lines if text fits in less than maxLines lines
|
||||||
@@ -1,18 +1,13 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode string as HTML.
|
* Encode string as HTML.
|
||||||
*
|
*
|
||||||
* @see https://stackoverflow.com/a/48073476/10629172
|
* @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 (
|
return (
|
||||||
str
|
str
|
||||||
.replace(/[\u00A0-\u9999<>&](?!#)/gim, (i) => {
|
.replace(/[\u00A0-\u9999<>&](?!#)/gim, (i: string) => {
|
||||||
return "&#" + i.charCodeAt(0) + ";";
|
return `&#${i.charCodeAt(0)};`;
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line no-control-regex
|
// eslint-disable-next-line no-control-regex
|
||||||
.replace(/\u0008/gim, "")
|
.replace(/\u0008/gim, "")
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import axios from "axios";
|
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:
|
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
|
* Get rank icon
|
||||||
*
|
*
|
||||||
* @param {string} rankIcon - The rank icon type.
|
* @param rankIcon - The rank icon type.
|
||||||
* @param {string} rankLevel - The rank level.
|
* @param rankLevel - The rank level.
|
||||||
* @param {number} percentile - The rank percentile.
|
* @param percentile - The rank percentile.
|
||||||
* @returns {string} - The SVG code of the rank icon
|
* @returns The SVG code of the rank icon
|
||||||
*/
|
*/
|
||||||
const rankIcon = (rankIcon, rankLevel, percentile) => {
|
const rankIcon = (
|
||||||
|
rankIcon: string,
|
||||||
|
rankLevel: string,
|
||||||
|
percentile: number,
|
||||||
|
): string => {
|
||||||
switch (rankIcon) {
|
switch (rankIcon) {
|
||||||
case "github":
|
case "github":
|
||||||
return `
|
return `
|
||||||
@@ -254,6 +254,7 @@
|
|||||||
"Hy": "#7790B2",
|
"Hy": "#7790B2",
|
||||||
"IDL": "#a3522f",
|
"IDL": "#a3522f",
|
||||||
"IGOR Pro": "#0000cc",
|
"IGOR Pro": "#0000cc",
|
||||||
|
"IL Assembly": "#512BD4",
|
||||||
"INI": "#d1dbe0",
|
"INI": "#d1dbe0",
|
||||||
"ISPC": "#2D68B1",
|
"ISPC": "#2D68B1",
|
||||||
"Idris": "#b30000",
|
"Idris": "#b30000",
|
||||||
@@ -467,6 +468,7 @@
|
|||||||
"Quake": "#882233",
|
"Quake": "#882233",
|
||||||
"QuakeC": "#975777",
|
"QuakeC": "#975777",
|
||||||
"QuickBASIC": "#008080",
|
"QuickBASIC": "#008080",
|
||||||
|
"Quint": "#9d6ce5",
|
||||||
"R": "#198CE7",
|
"R": "#198CE7",
|
||||||
"RAML": "#77d9fb",
|
"RAML": "#77d9fb",
|
||||||
"RAScript": "#2C97FA",
|
"RAScript": "#2C97FA",
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import toEmoji from "emoji-name-map";
|
import toEmoji from "emoji-name-map";
|
||||||
|
|
||||||
import { OWNER_AFFILIATIONS } from "./constants.js";
|
import { OWNER_AFFILIATIONS } from "./constants.js";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { getCardColors } from "./color.js";
|
import { getCardColors } from "./color.js";
|
||||||
import { SECONDARY_ERROR_MESSAGES, TRY_AGAIN_LATER } from "./error.js";
|
import { SECONDARY_ERROR_MESSAGES, TRY_AGAIN_LATER } from "./error.js";
|
||||||
import { encodeHTML } from "./html.js";
|
import { encodeHTML } from "./html.js";
|
||||||
|
|||||||
@@ -1,43 +1,56 @@
|
|||||||
// @ts-check
|
import type { AxiosResponse } from "axios";
|
||||||
|
|
||||||
import { getConfig } from "./config.js";
|
import { getConfig } from "./config.js";
|
||||||
import { CustomError } from "./error.js";
|
import { CustomError } from "./error.js";
|
||||||
import { logger } from "./log.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).
|
* Returns a random integer from 0 (inclusive) to `max` (exclusive).
|
||||||
*
|
*
|
||||||
* The value is generated using `Math.random()` and uniformly distributed
|
* The value is generated using `Math.random()` and uniformly distributed
|
||||||
* across the range.
|
* 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);
|
return Math.floor(Math.random() * max);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
type FetcherResponse = AxiosResponse<ResponseData>;
|
||||||
* @typedef {import("axios").AxiosResponse} AxiosResponse Axios response.
|
|
||||||
* @typedef {(variables: any, token: string, retriesForTests?: number) => Promise<AxiosResponse>} FetcherFunction Fetcher function.
|
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.
|
* Try to execute the fetcher function until it succeeds or the max number of retries is reached.
|
||||||
*
|
*
|
||||||
* @param {FetcherFunction} fetcher The fetcher function.
|
* @param fetcher The fetcher function.
|
||||||
* @param {any} variables Object with arguments to pass to the fetcher function.
|
* @param variables Object with arguments to pass to the fetcher function.
|
||||||
* @param {string | null} pat Optional PAT override.
|
* @param pat Optional PAT override.
|
||||||
* @returns {Promise<any>} The response from the fetcher function.
|
* @returns The response from the fetcher function.
|
||||||
*/
|
*/
|
||||||
const retryer = async (fetcher, variables, pat = null) => {
|
const retryer = async (
|
||||||
let PATs;
|
fetcher: FetcherFunction,
|
||||||
if (pat) {
|
variables: Record<string, unknown>,
|
||||||
PATs = [{ name: "user PAT from database", value: pat }];
|
pat: string | null = null,
|
||||||
} else {
|
): Promise<FetcherResponse> => {
|
||||||
PATs = getConfig().pats;
|
const PATs = pat
|
||||||
}
|
? [{ name: "user PAT from database", value: pat }]
|
||||||
|
: getConfig().pats;
|
||||||
|
|
||||||
if (!PATs.length) {
|
if (!PATs.length) {
|
||||||
throw new CustomError("No GitHub API tokens found", CustomError.NO_TOKENS);
|
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++) {
|
for (let retries = 0; retries < PATs.length; retries++) {
|
||||||
const currentPAT = PATs[(startPAT + retries) % PATs.length];
|
const currentPAT = PATs[(startPAT + retries) % PATs.length];
|
||||||
|
if (!currentPAT) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let response = await fetcher(
|
const response = await fetcher(
|
||||||
variables,
|
variables,
|
||||||
// @ts-ignore
|
|
||||||
currentPAT.value,
|
currentPAT.value,
|
||||||
// used in tests for faking rate limit
|
// used in tests for faking rate limit
|
||||||
retries,
|
retries,
|
||||||
@@ -58,11 +73,11 @@ const retryer = async (fetcher, variables, pat = null) => {
|
|||||||
|
|
||||||
// react on both type and message-based rate-limit signals.
|
// react on both type and message-based rate-limit signals.
|
||||||
// https://github.com/anuraghazra/github-readme-stats/issues/4425
|
// 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 errorType = errors?.[0]?.type;
|
||||||
const errorMsg = errors?.[0]?.message || "";
|
const errorMsg = errors?.[0]?.message ?? "";
|
||||||
const isRateLimited =
|
const isRateLimited =
|
||||||
(errors && errorType === "RATE_LIMITED") ||
|
(!!errors && errorType === "RATE_LIMITED") ||
|
||||||
/rate limit/i.test(errorMsg);
|
/rate limit/i.test(errorMsg);
|
||||||
|
|
||||||
if (isRateLimited) {
|
if (isRateLimited) {
|
||||||
@@ -71,20 +86,18 @@ const retryer = async (fetcher, variables, pat = null) => {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
/** @type {any} */
|
const e = err as { response?: FetcherResponse };
|
||||||
const e = err;
|
|
||||||
|
|
||||||
// network/unexpected error → let caller treat as failure
|
// network/unexpected error → let caller treat as failure
|
||||||
if (!e?.response) {
|
if (!e.response) {
|
||||||
throw e;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
// also checking for bad credentials if any tokens gets invalidated
|
// also checking for bad credentials if any tokens gets invalidated
|
||||||
const isBadCredential =
|
const message = e.response.data.message;
|
||||||
e?.response?.data?.message === "Bad credentials";
|
const isBadCredential = message === "Bad credentials";
|
||||||
const isAccountSuspended =
|
const isAccountSuspended =
|
||||||
e?.response?.data?.message === "Sorry. Your account was suspended.";
|
message === "Sorry. Your account was suspended.";
|
||||||
|
|
||||||
if (isBadCredential || isAccountSuspended) {
|
if (isBadCredential || isAccountSuspended) {
|
||||||
logger.log(`${currentPAT.name} Failed due to bad credentials`);
|
logger.log(`${currentPAT.name} Failed due to bad credentials`);
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { MissingParamError } from "../common/error.js";
|
import { MissingParamError } from "../common/error.js";
|
||||||
import { request } from "../common/http.js";
|
import { request } from "../common/http.js";
|
||||||
import { retryer } from "../common/retryer.js";
|
import { retryer } from "../common/retryer.js";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { MissingParamError } from "../common/error.js";
|
import { MissingParamError } from "../common/error.js";
|
||||||
import { request } from "../common/http.js";
|
import { request } from "../common/http.js";
|
||||||
import { retryer } from "../common/retryer.js";
|
import { retryer } from "../common/retryer.js";
|
||||||
@@ -23,9 +21,7 @@ const fetcher = (variables, token) => {
|
|||||||
isPrivate
|
isPrivate
|
||||||
isArchived
|
isArchived
|
||||||
isTemplate
|
isTemplate
|
||||||
stargazers {
|
stargazerCount
|
||||||
totalCount
|
|
||||||
}
|
|
||||||
description
|
description
|
||||||
primaryLanguage {
|
primaryLanguage {
|
||||||
color
|
color
|
||||||
@@ -130,7 +126,7 @@ const fetchRepo = async (
|
|||||||
return {
|
return {
|
||||||
...repoUserStats,
|
...repoUserStats,
|
||||||
...data.user.repository,
|
...data.user.repository,
|
||||||
starCount: data.user.repository.stargazers.totalCount,
|
starCount: data.user.repository.stargazerCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +151,7 @@ const fetchRepo = async (
|
|||||||
return {
|
return {
|
||||||
...repoUserStats,
|
...repoUserStats,
|
||||||
...data.organization.repository,
|
...data.organization.repository,
|
||||||
starCount: data.organization.repository.stargazers.totalCount,
|
starCount: data.organization.repository.stargazerCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import githubUsernameRegex from "github-username-regex";
|
import githubUsernameRegex from "github-username-regex";
|
||||||
|
|
||||||
@@ -18,9 +16,7 @@ const GRAPHQL_REPOS_FIELD = `
|
|||||||
totalCount
|
totalCount
|
||||||
nodes {
|
nodes {
|
||||||
name
|
name
|
||||||
stargazers {
|
stargazerCount
|
||||||
totalCount
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pageInfo {
|
pageInfo {
|
||||||
hasNextPage
|
hasNextPage
|
||||||
@@ -159,7 +155,7 @@ const statsFetcher = async ({
|
|||||||
// Disable multi page fetching on public Vercel instance due to rate limits.
|
// Disable multi page fetching on public Vercel instance due to rate limits.
|
||||||
fetchedPages++;
|
fetchedPages++;
|
||||||
const repoNodesWithStars = repoNodes.filter(
|
const repoNodesWithStars = repoNodes.filter(
|
||||||
(node) => node.stargazers.totalCount !== 0,
|
(node) => node.stargazerCount !== 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
hasNextPage =
|
hasNextPage =
|
||||||
@@ -458,7 +454,7 @@ const fetchStats = async (
|
|||||||
return !repoToHide.has(data.name);
|
return !repoToHide.has(data.name);
|
||||||
})
|
})
|
||||||
.reduce((prev, curr) => {
|
.reduce((prev, curr) => {
|
||||||
return prev + curr.stargazers.totalCount;
|
return prev + curr.stargazerCount;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
stats.rank = calculateRank({
|
stats.rank = calculateRank({
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { getConfig } from "../common/config.js";
|
import { getConfig } from "../common/config.js";
|
||||||
import { CustomError, MissingParamError } from "../common/error.js";
|
import { CustomError, MissingParamError } from "../common/error.js";
|
||||||
import { wrapTextMultiline } from "../common/fmt.js";
|
import { wrapTextMultiline } from "../common/fmt.js";
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ export interface RepositoryData {
|
|||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
isArchived: boolean;
|
isArchived: boolean;
|
||||||
isTemplate: boolean;
|
isTemplate: boolean;
|
||||||
stargazers: { totalCount: number };
|
stargazerCount: number;
|
||||||
description: string;
|
description: string;
|
||||||
primaryLanguage: {
|
primaryLanguage: {
|
||||||
color: string;
|
color: string;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
import { CustomError, MissingParamError } from "../common/error.js";
|
import { CustomError, MissingParamError } from "../common/error.js";
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { encodeHTML } from "./common/html.js";
|
import { encodeHTML } from "./common/html.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves stat card labels in the available locales.
|
* Retrieves stat card labels in the available locales.
|
||||||
*
|
*
|
||||||
* @param {object} props Function arguments.
|
* @param props Function arguments.
|
||||||
* @param {string} props.name The name of the locale.
|
* @param props.name The name of the locale.
|
||||||
* @param {string} props.apostrophe Whether to use apostrophe or not.
|
* @param props.apostrophe Whether to use apostrophe or not.
|
||||||
* @returns {object} The locales object.
|
* @returns The locales object.
|
||||||
*
|
*
|
||||||
* @see https://www.andiamo.co.uk/resources/iso-language-codes/ for language codes.
|
* @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);
|
const encodedName = encodeHTML(name);
|
||||||
return {
|
return {
|
||||||
"statcard.title": {
|
"statcard.title": {
|
||||||
@@ -1140,10 +1144,10 @@ const availableLocales = Object.keys(repoCardLocales["repocard.archived"]);
|
|||||||
/**
|
/**
|
||||||
* Checks whether the locale is available or not.
|
* Checks whether the locale is available or not.
|
||||||
*
|
*
|
||||||
* @param {string} locale The locale to check.
|
* @param locale The locale to check.
|
||||||
* @returns {boolean} Boolean specifying whether the locale is available or not.
|
* @returns Boolean specifying whether the locale is available or not.
|
||||||
*/
|
*/
|
||||||
const isLocaleAvailable = (locale) => {
|
const isLocaleAvailable = (locale: string): boolean => {
|
||||||
return availableLocales.includes(locale.toLowerCase());
|
return availableLocales.includes(locale.toLowerCase());
|
||||||
};
|
};
|
||||||
|
|
||||||
+12
@@ -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;
|
||||||
|
}
|
||||||
@@ -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";
|
|
||||||
@@ -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 { queryByTestId } from "@testing-library/dom";
|
||||||
import { cssToObject } from "@uppercod/css-to-object";
|
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 { Card } from "../src/common/Card.js";
|
||||||
import { getCardColors } from "../src/common/color.js";
|
import { getCardColors } from "../src/common/color.js";
|
||||||
import { icons } from "../src/common/icons.js";
|
import { icons } from "../src/common/icons.js";
|
||||||
|
|
||||||
describe("Card", () => {
|
describe("Card", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
it("should hide border", () => {
|
it("should hide border", () => {
|
||||||
const card = new Card({});
|
const card = new Card({});
|
||||||
card.setHideBorder(true);
|
card.setHideBorder(true);
|
||||||
@@ -68,21 +72,21 @@ describe("Card", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("title should have prefix icon", () => {
|
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(``);
|
document.body.innerHTML = card.render(``);
|
||||||
expect(document.getElementsByClassName("icon")[0]).toBeInTheDocument();
|
expect(document.getElementsByClassName("icon")[0]).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("title should not have prefix icon", () => {
|
it("title should not have prefix icon", () => {
|
||||||
const card = new Card({ title: "ok" });
|
const card = new Card({});
|
||||||
|
|
||||||
document.body.innerHTML = card.render(``);
|
document.body.innerHTML = card.render(``);
|
||||||
expect(document.getElementsByClassName("icon")[0]).toBeUndefined();
|
expect(document.getElementsByClassName("icon")[0]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have proper height, width", () => {
|
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(``);
|
document.body.innerHTML = card.render(``);
|
||||||
expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
|
expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
|
||||||
"height",
|
"height",
|
||||||
@@ -95,7 +99,7 @@ describe("Card", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should have less height after title is hidden", () => {
|
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);
|
card.setHideTitle(true);
|
||||||
|
|
||||||
document.body.innerHTML = card.render(``);
|
document.body.innerHTML = card.render(``);
|
||||||
@@ -147,10 +151,10 @@ describe("Card", () => {
|
|||||||
document.body.innerHTML = card.render(``);
|
document.body.innerHTML = card.render(``);
|
||||||
|
|
||||||
const styleTag = document.querySelector("style");
|
const styleTag = document.querySelector("style");
|
||||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
const stylesObject = cssToObject(styleTag?.innerHTML ?? "");
|
||||||
const headerClassStyles = stylesObject[":host"][".header "];
|
const headerClassStyles = stylesObject[":host"]?.[".header "];
|
||||||
|
|
||||||
expect(headerClassStyles["fill"].trim()).toBe("#f00");
|
expect(headerClassStyles?.["fill"]?.trim()).toBe("#f00");
|
||||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||||
"fill",
|
"fill",
|
||||||
"#fff",
|
"#fff",
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { getCardColors } from "../src/common/color";
|
import { getCardColors } from "../src/common/color.js";
|
||||||
|
|
||||||
describe("Test color.js", () => {
|
describe("Test color.js", () => {
|
||||||
it("getCardColors: should return expected values", () => {
|
it("getCardColors: should return expected values", () => {
|
||||||
let colors = getCardColors({
|
const colors = getCardColors({
|
||||||
title_color: "f00",
|
title_color: "f00",
|
||||||
text_color: "0f0",
|
text_color: "0f0",
|
||||||
ring_color: "0000ff",
|
ring_color: "0000ff",
|
||||||
@@ -24,7 +24,7 @@ describe("Test color.js", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("getCardColors: should fallback to default colors if color is invalid", () => {
|
it("getCardColors: should fallback to default colors if color is invalid", () => {
|
||||||
let colors = getCardColors({
|
const colors = getCardColors({
|
||||||
title_color: "invalidcolor",
|
title_color: "invalidcolor",
|
||||||
text_color: "0f0",
|
text_color: "0f0",
|
||||||
icon_color: "00f",
|
icon_color: "00f",
|
||||||
@@ -43,7 +43,7 @@ describe("Test color.js", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("getCardColors: should fallback to specified theme colors if is not defined", () => {
|
it("getCardColors: should fallback to specified theme colors if is not defined", () => {
|
||||||
let colors = getCardColors({
|
const colors = getCardColors({
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
});
|
});
|
||||||
expect(colors).toStrictEqual({
|
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", () => {
|
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",
|
title_color: "f00",
|
||||||
text_color: "0f0",
|
text_color: "0f0",
|
||||||
icon_color: "00f",
|
icon_color: "00f",
|
||||||
@@ -76,7 +76,7 @@ describe("Test color.js", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("getCardColors: should fallback to default theme if theme is invalid", () => {
|
it("getCardColors: should fallback to default theme if theme is invalid", () => {
|
||||||
let colors = getCardColors({
|
const colors = getCardColors({
|
||||||
theme: "invalidTheme",
|
theme: "invalidTheme",
|
||||||
});
|
});
|
||||||
expect(colors).toStrictEqual({
|
expect(colors).toStrictEqual({
|
||||||
@@ -7,7 +7,7 @@ import { fetchRepo } from "../src/fetchers/repo.js";
|
|||||||
const data_repo = {
|
const data_repo = {
|
||||||
repository: {
|
repository: {
|
||||||
name: "convoychat",
|
name: "convoychat",
|
||||||
stargazers: { totalCount: 38000 },
|
stargazerCount: 38000,
|
||||||
description: "Help us take over the world! React + TS + GraphQL Chat App",
|
description: "Help us take over the world! React + TS + GraphQL Chat App",
|
||||||
primaryLanguage: {
|
primaryLanguage: {
|
||||||
color: "#2b7489",
|
color: "#2b7489",
|
||||||
@@ -46,7 +46,7 @@ describe("Test fetchRepo", () => {
|
|||||||
|
|
||||||
expect(repo).toStrictEqual({
|
expect(repo).toStrictEqual({
|
||||||
...data_repo.repository,
|
...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");
|
let repo = await fetchRepo("anuraghazra", "convoychat");
|
||||||
expect(repo).toStrictEqual({
|
expect(repo).toStrictEqual({
|
||||||
...data_repo.repository,
|
...data_repo.repository,
|
||||||
starCount: data_repo.repository.stargazers.totalCount,
|
starCount: data_repo.repository.stargazerCount,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user