[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = LF\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[vercel.json]\nindent_size = 4\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [ryo-ma]\n"
  },
  {
    "path": ".github/workflows/close-old-issues.yml",
    "content": "name: Close inactive issues\non:\n  schedule:\n    - cron: \"30 1 * * *\"\n\njobs:\n  close-issues:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n      pull-requests: write\n    steps:\n      - uses: actions/stale@v5\n        with:\n          days-before-issue-stale: 30\n          days-before-issue-close: 14\n          stale-issue-label: \"stale\"\n          stale-issue-message: \"This issue is stale because it has been open for 30 days with no activity.\"\n          close-issue-message: \"This issue was closed because it has been inactive for 14 days since being marked as stale.\"\n          days-before-pr-stale: -1\n          days-before-pr-close: -1\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/test-repository-action.yml",
    "content": "name: Test GitHub Profile Trophy Action\non: [push, pull_request, workflow_dispatch]\n\njobs:\n  test-trophy:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout this repo\n        uses: actions/checkout@v4\n      - name: Test Action\n        id: trophy\n        uses: ./.\n        with:\n          username: ${{ github.repository_owner }}\n          output_path: ./trophy.svg\n          token: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Verify SVG generated\n        run: |\n          if [ -f trophy.svg ]; then\n            echo \"SVG exsists ($(wc -c < trophy.svg) Bytes)\"\n            file trophy.svg\n          else\n            echo \"SVG failed to generate\"\n            exit 1\n          fi\n"
  },
  {
    "path": ".github/workflows/testing.yml",
    "content": "name: Check PR Test\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n  workflow_dispatch:\n\njobs:\n  install-dependencies:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        deno-version: [\"1.44.4\"]\n\n    steps:\n      # Step 1: Checkout repository\n      - name: Git Checkout Deno Module\n        uses: actions/checkout@v4\n\n      # Step 2: Setup Deno\n      - name: Setup Deno\n        uses: denoland/setup-deno@v1\n        with:\n          deno-version: ${{ matrix.deno-version }}\n\n      # Step 3: Cache / Install dependencies\n      # Change src/mod.ts to main.ts if your project uses main.ts\n      - name: Install dependencies\n        run: deno cache --reload deps.ts\n\n      # Step 4: Format check\n      - name: Deno format check\n        run: deno fmt --check\n\n      # Step 5: Lint check (won't fail workflow)\n      - name: Deno lint check\n        run: deno lint || true\n\n      # Step 6: Run tests\n      - name: Test Deno Module\n        run: deno task test\n"
  },
  {
    "path": ".gitignore",
    "content": ".vscode\n.env\n.idea\ndeno.lock\n*.sh\n**/.DS_Store\n"
  },
  {
    "path": ".vercelignore",
    "content": ".gitignore\n.github\nREADME.md\nLICENSE\ndebug.ts"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contribution Guide\n\n## Environment\n\n- Deno >= v1.36.1\n- [Vercel](https://vercel.com/)\n- GitHub API v4\n- Docker and Docker compose (optional)\n\n## Local Run\n\nCreate `.env` file to project root directory, and write your GitHub token to the\n`.env` file. Please select the authority of `repo` when creating token.\n\n```properties\nGITHUB_TOKEN1=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\nGITHUB_TOKEN2=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n\n# if using GitHub Enterprise:\n# (this env var defaults to https://api.github.com/graphql)\nGITHUB_API=https://github.example.com/api/graphql\n```\n\nRun local server.\n\n```sh\ndeno task start\n```\n\nYou can enable the Redis if you want, but it's not mandatory.\n\n```sh\ndocker compose up -d\n```\n\nRename `env-example` to `.env`, and change ENABLE_REDIS to true\n\nOpen localhost from your browser.\n\nhttp://localhost:8080/?username=ryo-ma\n\n## Editor config\n\nRead the [.editorconfig](./.editorconfig)\n\n## Pull Requests\n\nPull requests are always welcome! In general, they should a single concern in\nthe least number of changed lines as possible. For changes that address core\nfunctionality, it is best to open an issue to discuss your proposal first. I\nlook forward to seeing what you come up with!\n\n## Run deno lint\n\n## What to do before contributing\n\n### 1. Run deno lint\n\n```sh\ndeno task lint\n```\n\n### 2. Run deno format\n\n```sh\ndeno task format\n```\n\n### 3. Run deno test\n\n```sh\ndeno task test\n```\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM denoland/deno:latest\n\n# Create working directory\nWORKDIR /app\n\n# Copy source\nCOPY . .\n\n# Compile the main app\nRUN deno cache main.ts\n\n# Run the app\nCMD [\"deno\", \"run\", \"-A\", \"main.ts\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2020 ryo-ma\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <img width=\"140\" src=\"https://user-images.githubusercontent.com/6661165/91657958-61b4fd00-eb00-11ea-9def-dc7ef5367e34.png\"  alt=\"GitHub Profile Trophy\"/>\n  <h2 align=\"center\">GitHub Profile Trophy</h2>\n  <p align=\"center\">🏆 Add dynamically generated GitHub Stat Trophies on your README</p>\n</div>\n<div align=\"center\">\n\n[![stargazers](https://img.shields.io/github/stars/ryo-ma/github-profile-trophy)](https://github.com/ryo-ma/github-profile-trophy/stargazers)\n[![forks](https://img.shields.io/github/forks/ryo-ma/github-profile-trophy)](https://github.com/ryo-ma/github-profile-trophy/network/members)\n[![issues](https://img.shields.io/github/issues/ryo-ma/github-profile-trophy)](https://github.com/ryo-ma/github-profile-trophy/issues)\n[![license](https://img.shields.io/github/license/ryo-ma/github-profile-trophy)](https://github.com/ryo-ma/github-profile-trophy/blob/master/LICENSE)\n[![share](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2Fryo-ma%2Fgithub-profile-trophy)](https://twitter.com/intent/tweet?text=Add%20dynamically%20generated%20GitHub%20Trophy%20on%20your%20readme%0D%0A&url=https%3A%2F%2Fgithub.com%2Fryo-ma%2Fgithub-profile-trophy)\n\n</div>\n<p align=\"center\">\n  You can use this service for free. I'm looking for sponsors to help us keep up with this service❤️\n</p>\n<div align=\"center\">\n  <a href=\"https://github.com/sponsors/ryo-ma\">\n    <img src=\"https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=ff69b4\" alt=\"Sponsor\"/>\n  </a>\n</div>\n\n> ⚠️ **Notice from the Project Owner**\n>\n> Hello everyone,\\\n> I am the owner of this project.\n>\n> Currently, due to the increase in users and traffic, the cost of maintaining\n> this service has become quite high, and it is becoming financially difficult\n> to sustain.\\\n> As costs continue to rise, there is a possibility that the service may have to\n> be discontinued.\\\n> We are now at a stage where monetization and financial support are essential.\n>\n> Your support in the following ways would be greatly appreciated:\n>\n> 1. Financial support: [GitHub Sponsors](https://github.com/sponsors/ryo-ma)\n> 2. Reducing server load through self-hosting (by forking on GitHub and\n>    deploying to Vercel)\n> 3. Share your github-profile-trophy URL deployed to Vercel for load balancing\n>    (Send email: saka_ro@yahoo.co.jp or Pull Request)\n\n# Load balancing endpoints\n\nThese are endpoints provided by volunteers. Please use these in moderation.\n\n- [https://github-profile-trophy-liard-delta.vercel.app](https://github-profile-trophy-liard-delta.vercel.app/)\n  by [Adwitya](https://github.com/Adwitya)\n- [https://github-profile-trophy-fork-two.vercel.app](https://github-profile-trophy-fork-two.vercel.app)\n  by [hesreallyhim](https://github.com/hesreallyhim)\n- [https://github-profile-trophy-winning.vercel.app](https://github-profile-trophy-winning.vercel.app)\n  by [hongbo-wei](https://github.com/hongbo-wei)\n- [https://github-profile-trophy-kannan.vercel.app](https://github-profile-trophy-kannan.vercel.app)\n  by [kann4n](https://github.com/kann4n)\n- [https://trophy.ryglcloud.net](https://trophy.ryglcloud.net) by\n  [PracticalRyan](https://github.com/PracticalRyan)\n- [https://github-profile-trophy-tawny.vercel.app](https://github-profile-trophy-tawny.vercel.app)\n  by [vijaypurohit322](https://github.com/vijaypurohit322)\n- [https://github-profile-repo.vercel.app](https://github-profile-repo.vercel.app/))\n  by [HackyCoder0951](https://github.com/hackycoder0951)\n- [https://gh-trophy.cdnsoft.net](https://gh-trophy.cdnsoft.net) by\n  [cromatikap](https://github.com/cromatikap)\n- [https://trophygh.kolioaris.xyz](https://trophygh.kolioaris.xyz) by\n  [kolioaris](https://github.com/kolioaris)\n- [https://github-profile-trophy-orcin-eta.vercel.app](https://github-profile-trophy-orcin-eta.vercel.app/)\n  by [manupawick](https://github.com/manupawickramasinghe)\n- [https://github-profile-trophy-reiyua-mirror.vercel.app](https://github-profile-trophy-reiyua-mirror.vercel.app)\n  by [reiyua](https://github.com/reiyua)\n\n# Quick Start\n\nAdd the following code to your readme. When pasting the code into your profile's\nreadme, change the `?username=` value to your GitHub's username.\n\n```\n[![trophy](https://github-profile-trophy.vercel.app/?username=ryo-ma)](https://github.com/ryo-ma/github-profile-trophy)\n```\n\n<p align=\"center\">\n  <img src=\"https://github-profile-trophy.vercel.app/?username=ryo-ma&column=8&rank=SSS,SS,S,AAA,AA,A,B,C\" />\n</p>\n\n## Use theme\n\nAdd optional parameter of the theme.\n\n```\n[![trophy](https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=onedark)](https://github.com/ryo-ma/github-profile-trophy)\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/92327052-d99b9e00-f091-11ea-9a24-c7ec86982370.png\">\n</p>\n\n**[More detail](#apply-theme)**\n\n# About Rank\n\nRanks are `SSS` `SS` `S` `AAA` `AA` `A` `B` `C` `UNKNOWN` `SECRET`.\n\n| Rank       | Description                                                                                |\n| ---------- | ------------------------------------------------------------------------------------------ |\n| SSS, SS, S | You are at a hard to reach rank. You can brag.                                             |\n| AAA, AA, A | You will reach this rank if you do your best. Let's aim here first.                        |\n| B, C       | You are currently making good progress. Let's aim a bit higher.                            |\n| UNKNOWN    | You have not taken action yet. Let's act first.                                            |\n| SECRET     | This rank is very rare. The trophy will not be displayed until certain conditions are met. |\n\n**NOTE: The `UNKNOWN` rank is denoted by `?`**\n\n## Secret Rank\n\nThe acquisition condition is secret, but you can see this.\n\n<p align=\"center\">\n  <img width=\"110\" src=\"https://github.com/user-attachments/assets/40461f38-a317-431c-93d2-a56c2e803cf3\" />\n</p>\n\nThere are only a few secret trophies. Therefore, if you come up with interesting\nconditions, I will consider adding a trophy. I am waiting for contributions.\n\n# About Display details\n\n<p align=\"center\">\n  <img width=\"220\" src=\"https://user-images.githubusercontent.com/6661165/91642962-6333e600-ea6a-11ea-83af-e371e996bfa6.png\" />\n</p>\n\n1. Title name of aggregation target.\n2. Current rank.\n3. Title according to rank.\n4. Target aggregation result.\n5. Rank progress bar.\n\n# Optional Request Parameters\n\n- [title](#filter-by-titles)\n- [rank](#filter-by-ranks)\n- [column](#specify-the-maximum-row--column-size)\n- [row](#specify-the-maximum-row--column-size)\n- [theme](#apply-theme)\n- [margin-w](#margin-width)\n- [margin-h](#margin-height)\n- [no-bg](#transparent-background)\n- [no-frame](#hide-frames)\n\n## Filter by titles\n\nYou can filter the display by specifying the titles of trophy.\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&title=Followers\n```\n\n<p align=\"center\">\n  <img width=\"110\" src=\"https://user-images.githubusercontent.com/6661165/92317141-80ebe700-f038-11ea-8501-4015bfbb2cf4.png\">\n</p>\n\nIf you want to specify multiple titles.\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&title=Stars,Followers\n```\n\n<p align=\"center\">\n  <img width=\"220\" src=\"https://github.com/user-attachments/assets/3b8a1c8b-afcd-49dc-ab18-a439d5c36a83\">\n</p>\n\nYou can also exclude the trophies you don't want to display.\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&title=-Stars,-Followers\n```\n\n## Filter by ranks\n\nYou can filter the display by specifying the ranks.\\\n`Available values: SECRET SSS SS S AAA AA A B C`\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&rank=S\n```\n\n<p align=\"center\">\n  <img width=\"110\" src=\"https://user-images.githubusercontent.com/6661165/91642657-1cdd8780-ea68-11ea-994b-4568a55cd22a.png\" />\n</p>\n\nIf you want to specify multiple ranks.\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&rank=S,AAA\n```\n\n<p align=\"center\">\n  <img width=\"220\" src=\"https://github.com/user-attachments/assets/0c2ffca8-4b03-4d46-b1d7-4e1eb6702f68\">\n</p>\n\nYou can also exclude ranks.\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&rank=-C,-B\n```\n\n**NOTE: Since `UNKNOWN` is denoted by `?`, in order to include or exclude it you\nwill have to use `rank=?` and `rank=-?` respectively**\n\n## Specify the maximum row & column size\n\nYou can specify the maximum row and column size.\\\nTrophy will be hidden if it exceeds the range of both row and column.\n\n`Available value: number type`\\\n`Default: column=6 row=3`\n\nRestrict only row\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&row=2\n```\n\nRestrict only column\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&column=2\n```\n\nRestrict row & column\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&row=2&column=3\n```\n\n<p align=\"center\">\n  <img width=\"330\" src=\"https://user-images.githubusercontent.com/6661165/91659474-c07f7400-eb0a-11ea-84f2-eb6b42547829.png\">\n</p>\n\nAdaptive column\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&column=-1\n```\n\nYou can set `column` to `-1` to adapt the width to the number of trophies, the\nparameter `row` will be ignored.\n\n## Apply theme\n\nAvailable themes.\n\n| theme                       |\n| --------------------------- |\n| [flat](#flat)               |\n| [onedark](#onedark)         |\n| [gruvbox](#gruvbox)         |\n| [dracula](#dracula)         |\n| [monokai](#monokai)         |\n| [chalk](#chalk)             |\n| [nord](#nord)               |\n| [alduin](#alduin)           |\n| [darkhub](#darkhub)         |\n| [juicyfresh](#juicyfresh)   |\n| [buddhism](#buddhism)       |\n| [oldie](#oldie)             |\n| [radical](#radical)         |\n| [onestar](#onestar)         |\n| [discord](#discord)         |\n| [algolia](#algolia)         |\n| [gitdimmed](#gitdimmed)     |\n| [tokyonight](#tokyonight)   |\n| [matrix](#matrix)           |\n| [apprentice](#apprentice)   |\n| [dark_dimmed](#dark_dimmed) |\n| [dark_lover](#dark_lover)   |\n| [kimbie_dark](#kimbie_dark) |\n| [aura](#aura)               |\n\n### flat\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=flat\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/92325601-039b9300-f087-11ea-983a-fce8133549ee.png\">\n</p>\n\n### onedark\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=onedark\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/92327052-d99b9e00-f091-11ea-9a24-c7ec86982370.png\">\n</p>\n\n### gruvbox\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=gruvbox\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/92315152-e9c56600-f01c-11ea-9536-1bfbb158cfcb.png\">\n</p>\n\n### dracula\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=dracula\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/92490273-c91f2b00-f22b-11ea-9481-b5daae4d7bc3.png\">\n</p>\n\n### monokai\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=monokai\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/93725426-2c289e80-fbea-11ea-96a4-f6490ccf2126.png\">\n</p>\n\n### chalk\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=chalk\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/94294003-1de7d300-ff9a-11ea-91d1-60417a4d919b.png\">\n</p>\n\n### nord\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=nord\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/94346857-7ab2be80-006a-11eb-9082-36d377ae2531.png\">\n</p>\n\n### alduin\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=alduin\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/99085932-2a88bf00-260c-11eb-9b26-d2f125773831.png\">\n</p>\n\n### darkhub\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=darkhub\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/102801126-249ab080-43f8-11eb-91c8-f56f94c35777.png\">\n</p>\n\n### juicyfresh\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=juicyfresh\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/104810094-edbc8c80-5835-11eb-8c20-a76192a00728.png\">\n</p>\n\n### buddhism\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=buddhism\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/113709167-2412f500-971d-11eb-9ee5-0ab292cf8b4c.png\">\n</p>\n\n### oldie\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=oldie\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/113709581-a0a5d380-971d-11eb-8583-770dc4091ebf.png\">\n</p>\n\n### radical\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=radical\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/116633521-adbc8800-a994-11eb-97c4-e45a32721491.png\">\n</p>\n\n### onestar\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=onestar\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/122048400-2af46d00-ce1c-11eb-94e0-c2c6ddaf6819.png\">\n</p>\n\n### discord\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=discord\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/122048628-7dce2480-ce1c-11eb-9792-1e600b384c4d.png\">\n</p>\n\n### algolia\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=algolia\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/131685203-92a31101-2d93-4d18-b24a-d81a8bb012c5.png\">\n</p>\n\n### gitdimmed\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=gitdimmed\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/131685406-799a864f-2691-4840-bb71-1db9c087a507.png\">\n</p>\n\n### tokyonight\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=tokyonight\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/6661165/135482087-27764d6f-53b4-4c2a-8473-32431d12660c.png\">\n</p>\n\n### matrix\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=matrix\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/31789752/141647414-15cfe279-af12-4746-a886-f494c25c096d.png\">\n</p>\n\n### apprentice\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=apprentice\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/31789752/144701036-285cdd4b-d687-4ddc-95c2-7ccae9e25a1f.png\">\n</p>\n\n### dark_dimmed\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=dark_dimmed\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/31789752/147340893-655b9fa5-138f-4f29-91ec-2a17c93822d1.png\">\n</p>\n\n### dark_lover\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=dark_lover\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/31789752/152659041-de5b23cb-1be8-4e6b-b07b-726127ab8c3a.png\">\n</p>\n\n### kimbie_dark\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=kimbie_dark\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://user-images.githubusercontent.com/8161064/288417332-408705a4-ae9c-47fe-af1a-9fb08555f526.png\">\n</p>\n\n### aura\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&theme=aura\n```\n\n<p align=\"center\">\n  <img width=\"660\" src=\"https://github.com/user-attachments/assets/18a2266c-9a88-4882-940d-162c0c4d36e0\">\n</p>\n\n## Margin Width\n\nYou can put a margin in the width between trophies.\\\n`Available value: number type`\\\n`Default: margin-w=0`\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&margin-w=15\n```\n\n<p align=\"center\">\n  <img width=\"735\" src=\"https://user-images.githubusercontent.com/6661165/93668661-e0ca9f00-fac8-11ea-9bec-325454f49fb4.png\">\n</p>\n\n## Margin Height\n\nYou can put a margin in the height between trophies.\\\n`Available value: number type`\\\n`Default: margin-h=0`\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&margin-h=15\n```\n\n<p align=\"center\">\n  <img width=\"110\" height=\"330\" src=\"https://github.com/user-attachments/assets/233dee5b-4491-46cc-884a-39d0aa928752\">\n</p>\n\n## Example layout\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&column=3&margin-w=15&margin-h=15\n```\n\n<p align=\"center\">\n  <img width=\"360\" src=\"https://user-images.githubusercontent.com/6661165/93668677-ff309a80-fac8-11ea-8ae3-3e3e8adbef39.png\">\n</p>\n\n## Transparent background\n\nYou can turn the background transparent.\\\n`Available value: boolean type (true or false)`\\\n`Default: no-bg=false`\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&no-bg=true\n```\n\n<p align=\"center\">\n  <img width=\"969\" src=\"https://github.com/user-attachments/assets/32d3b63b-7845-42cb-b71c-31abaa673bcb\">\n</p>\n\n## Hide frames\n\nYou can hide the frames around the trophies.\\\n`Available value: boolean type (true or false)`\\\n`Default: no-frame=false`\n\n```\nhttps://github-profile-trophy.vercel.app/?username=ryo-ma&no-frame=true\n```\n\n<p align=\"center\">\n  <img width=\"936\" src=\"https://github.com/user-attachments/assets/54de15a3-d907-4a50-8117-170aae74d1cd\">\n</p>\n\n## Generate an svg file localy\n\nUsing the render_svg.ts script you can generate your trophys as an svg file\ngiven your username, (Enviroment Vars: See [env-example](env-example)).\n\nUsage:\n\n```bash\ndeno run --allow-net --allow-env --allow-read --allow-write ./render_svg.ts USERNAME OUTPUT_DIR THEME\n```\n\n## Generate an svg inside Github CI (Workflow)\n\nUsing the provided github action you can easly generate the trophy inside an\ngithub workflow. This eliminates the needs of an online service running but you\nhave to manualy update rerun the action to update the file.\n\nUsage:\n\n```yaml\n- name: Generate trophy\n  uses: Erik-Donath/github-profile-trophy@feature/generate-svg\n  with:\n    username: your-username\n    output_path: trophy.svg\n    token: ${{ secrets.GITHUB_TOKEN }}\n```\n\n# Contribution Guide\n\nCheck [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.\n\n# License\n\nThis product is licensed under the\n[MIT License](https://github.com/ryo-ma/github-profile-trophy/blob/master/LICENSE).\n\n# 🙏 Sponsors\n\nThank you so much to all the amazing sponsors who support this project! Your\ncontributions help keep development going and make this work possible.\n\n## 💖 Monthly Sponsors\n\n- [@Leay15](https://github.com/Leay15) - $10 / month\n- [@hesreallyhim](https://github.com/hesreallyhim) - $10 / month\n- [@pmsosa](https://github.com/pmsosa) — $10 / month\n- [@chenfeng-huang](https://github.com/chenfeng-huang) — $10 / month\n- [@holly-hacker](https://github.com/holly-hacker) — $2 / month\n- [@skillerious](https://github.com/skillerious) — $2 / month (since Aug\n  17, 2024)\n- [@LudovicGardy](https://github.com/LudovicGardy) — $10 / month (since Aug\n  15, 2024)\n- [@alexcastrodev](https://github.com/alexcastrodev) — $10 / month (since Oct\n  13, 2023, previously $50 / month)\n- [@great-work-told-is](https://github.com/great-work-told-is) — $10 / month\n  (since Apr 12, 2023)\n- [@Ilithy](https://github.com/Ilithy) — $5 / month (since Jul 21, 2022)\n- [@weakish](https://github.com/weakish) — $2 / month (since Jan 22, 2022)\n- [@Kazuhito00](https://github.com/Kazuhito00) — $2 / month (since Jan 19, 2022)\n- [@KATO-Hiro](https://github.com/KATO-Hiro) — $2 / month (since Jan 5, 2022)\n- [@batazor](https://github.com/batazor) — $2 / month (since Oct 28, 2021)\n- [@port19x](https://github.com/port19x) — $2 / month (since Jan 27, 2022)\n\n## 🎁 One-Time Sponsors\n\n- [@pronoym99](https://github.com/pronoym99) - $20 (Feb 8, 2026)\n- [@hesreallyhim](https://github.com/hesreallyhim) - $100 (Jan 11, 2026)\n- [@Seo-4d696b75](https://github.com/Seo-4d696b75) — $10 (Jan 4, 2026)\n- [@massif-01](https://github.com/massif-01) — $5 (December 17, 2025)\n- [@tapegram](https://github.com/tapegram) — $20 (December 6, 2025)\n- [@WilliamCorotan](https://github.com/WilliamCorotan) — $5 (November 5, 2025)\n- [@arnabnandy7](https://github.com/arnabnandy7) — $10 (Oct 3, 2025)\n- [@JoqarSabon](https://github.com/JoqarSabon) — $5 (May 24, 2024)\n- [@syaghoubi00](https://github.com/syaghoubi00) — $5 (Jan 28, 2024)\n- [@pylapp](https://github.com/pylapp) — $20 (Jan 15, 2024)\n- [@Dobefu](https://github.com/Dobefu) — $10 (Dec 22, 2024)\n- [@michele-lorenzoni](https://github.com/michele-lorenzoni) — $10 (Nov\n  26, 2024)\n- [@skillerious](https://github.com/skillerious) — $10 (Aug 17, 2024)\n"
  },
  {
    "path": "action.yml",
    "content": "name: Generate GitHub Profile Trophy SVG\ndescription: Run the local generator script to produce an SVG.\ninputs:\n  username:\n    description: \"GitHub username to generate the trophy for\"\n    required: true\n  output_path:\n    description: \"Output path to write the SVG\"\n    required: true\n    default: \"trophy.svg\"\n  token:\n    description: \"PAT or token to use for GitHub API\"\n    required: true\n  theme:\n    description: \"Theme for the card\"\n    required: false\n    default: \"default\"\nruns:\n  using: \"composite\"\n  steps:\n    - name: Setup Deno\n      uses: denoland/setup-deno@v1\n      with:\n        deno-version: v1.x\n\n    - name: Generate trophy\n      shell: bash\n      env:\n        GITHUB_TOKEN1: ${{ inputs.token }}\n      run: |\n        deno run --allow-net --allow-env --allow-read --allow-write $GITHUB_ACTION_PATH/render_svg.ts \"${{ inputs.username }}\" \"${{ inputs.output_path }}\" \"${{ inputs.theme }}\"\n"
  },
  {
    "path": "api/index.ts",
    "content": "import { Card } from \"../src/card.ts\";\nimport { CONSTANTS, parseParams } from \"../src/utils.ts\";\nimport { COLORS, Theme } from \"../src/theme.ts\";\nimport { Error400 } from \"../src/error_page.ts\";\nimport \"https://deno.land/x/dotenv@v0.5.0/load.ts\";\nimport { staticRenderRegeneration } from \"../src/StaticRenderRegeneration/index.ts\";\nimport { GithubRepositoryService } from \"../src/Repository/GithubRepository.ts\";\nimport { GithubApiService } from \"../src/Services/GithubApiService.ts\";\nimport { ServiceError } from \"../src/Types/index.ts\";\nimport { ErrorPage } from \"../src/pages/Error.ts\";\nimport { cacheProvider } from \"../src/config/cache.ts\";\n\nconst serviceProvider = new GithubApiService();\nconst client = new GithubRepositoryService(serviceProvider).repository;\n\n// Build cache control header with optimized caching strategy\nconst cacheControlHeader = [\n  \"public\",\n  `max-age=${CONSTANTS.CACHE_MAX_AGE}`,\n  `s-maxage=${CONSTANTS.CDN_CACHE_MAX_AGE}`,\n  `stale-while-revalidate=${CONSTANTS.STALE_WHILE_REVALIDATE}`,\n].join(\", \");\n\nconst defaultHeaders = new Headers(\n  {\n    \"Content-Type\": \"image/svg+xml\",\n    \"Cache-Control\": cacheControlHeader,\n  },\n);\n\nexport default (request: Request) =>\n  staticRenderRegeneration(request, {\n    revalidate: CONSTANTS.REVALIDATE_TIME,\n    headers: defaultHeaders,\n  }, function (req: Request) {\n    return app(req);\n  });\n\nasync function app(req: Request): Promise<Response> {\n  const params = parseParams(req);\n  const username = params.get(\"username\");\n  const row = params.getNumberValue(\"row\", CONSTANTS.DEFAULT_MAX_ROW);\n  const column = params.getNumberValue(\"column\", CONSTANTS.DEFAULT_MAX_COLUMN);\n  const themeParam: string = params.getStringValue(\"theme\", \"default\");\n  if (username === null) {\n    const [base] = req.url.split(\"?\");\n    const error = new Error400(\n      `<section>\n      <div>\n        <h2>\"username\" is a required query parameter</h2>\n        <p>The URL should look like\n        <div>\n          <p id=\"base-show\">${base}?username=USERNAME</p>\n          <button>Copy Base Url</button>\n          <span id=\"temporary-span\"></span>\n        </div>where\n        <code>USERNAME</code> is <em>your GitHub username.</em>\n      </div>\n      <div>\n        <h2>You can use this form: </h2>\n        <p>Enter your username and click \"Get Trophies\"</p>\n        <form action=\"${base}\" method=\"get\">\n          <label for=\"username\">GitHub Username</label>\n          <input type=\"text\" name=\"username\" id=\"username\" placeholder=\"Ex. gabriel-logan\" required>\n          <label for=\"theme\">Theme (Optional)</label>\n          <input type=\"text\" name=\"theme\" id=\"theme\" placeholder=\"Ex. onedark\" value=\"light\">\n          <text>\n            See all the available themes\n            <a href=\"https://github.com/ryo-ma/github-profile-trophy?tab=readme-ov-file#apply-theme\" target=\"_blank\">here</a>\n          </text>\n          <br>\n          <button type=\"submit\">Get Trophies</button>\n        </form>\n      </div>\n      <script>\n        const button = document.querySelector(\"button\");\n        const input = document.querySelector(\"input\");\n        const temporarySpan = document.querySelector(\"#temporary-span\");\n\n        button.addEventListener(\"click\", () => {\n          navigator.clipboard.writeText(document.querySelector(\"#base-show\").textContent);\n          temporarySpan.textContent = \"Copied!\";\n          setTimeout(() => {\n            temporarySpan.textContent = \"\";\n          }, 1500);\n        });\n      </script>\n    </section>`,\n    );\n    return new Response(\n      error.render(),\n      {\n        status: error.status,\n        headers: new Headers({\n          \"Content-Type\": \"text/html\",\n          \"Cache-Control\": cacheControlHeader,\n        }),\n      },\n    );\n  }\n  let theme: Theme = COLORS.default;\n  if (Object.keys(COLORS).includes(themeParam)) {\n    theme = COLORS[themeParam];\n  }\n  const marginWidth = params.getNumberValue(\n    \"margin-w\",\n    CONSTANTS.DEFAULT_MARGIN_W,\n  );\n  const paddingHeight = params.getNumberValue(\n    \"margin-h\",\n    CONSTANTS.DEFAULT_MARGIN_H,\n  );\n  const noBackground = params.getBooleanValue(\n    \"no-bg\",\n    CONSTANTS.DEFAULT_NO_BACKGROUND,\n  );\n  const noFrame = params.getBooleanValue(\n    \"no-frame\",\n    CONSTANTS.DEFAULT_NO_FRAME,\n  );\n  const titles: Array<string> = params.getAll(\"title\").flatMap((r) =>\n    r.split(\",\")\n  ).map((r) => r.trim());\n  const ranks: Array<string> = params.getAll(\"rank\").flatMap((r) =>\n    r.split(\",\")\n  ).map((r) => r.trim());\n\n  const userKeyCache = [\"v1\", username].join(\"-\");\n  const userInfoCached = await cacheProvider.get(userKeyCache) || \"{}\";\n  let userInfo = JSON.parse(userInfoCached);\n  const hasCache = !!Object.keys(userInfo).length;\n\n  if (!hasCache) {\n    const userResponseInfo = await client.requestUserInfo(username);\n    if (userResponseInfo instanceof ServiceError) {\n      return new Response(\n        ErrorPage({ error: userResponseInfo }).render(),\n        {\n          status: userResponseInfo.code,\n          headers: new Headers({\n            \"Content-Type\": \"text/html\",\n            \"Cache-Control\": cacheControlHeader,\n          }),\n        },\n      );\n    }\n    userInfo = userResponseInfo;\n    await cacheProvider.set(userKeyCache, JSON.stringify(userInfo));\n  }\n  // Success Response\n  return new Response(\n    new Card(\n      titles,\n      ranks,\n      column,\n      row,\n      CONSTANTS.DEFAULT_PANEL_SIZE,\n      marginWidth,\n      paddingHeight,\n      noBackground,\n      noFrame,\n    ).render(userInfo, theme),\n    {\n      headers: defaultHeaders,\n    },\n  );\n}\n"
  },
  {
    "path": "deno.json",
    "content": "{\n  \"tasks\": {\n    \"start\": \"deno run -A main.ts\",\n    \"debug\": \"deno run --inspect-brk -A main.ts\",\n    \"format\": \"deno fmt\",\n    \"lint\": \"deno lint\",\n    \"test\": \"ENV_TYPE=test deno test --allow-env\"\n  }\n}\n"
  },
  {
    "path": "deps.ts",
    "content": "import { Soxa as ServiceProvider } from \"https://deno.land/x/soxa@1.4/src/core/Soxa.ts\";\nimport { defaults } from \"https://deno.land/x/soxa@1.4/src/defaults.ts\";\nimport {\n  assertEquals,\n  assertRejects,\n} from \"https://deno.land/std@0.203.0/assert/mod.ts\";\nimport {\n  assertSpyCalls,\n  returnsNext,\n  spy,\n  stub,\n} from \"https://deno.land/std@0.203.0/testing/mock.ts\";\n\nexport {\n  type Bulk,\n  connect,\n  type Redis,\n} from \"https://deno.land/x/redis@v0.31.0/mod.ts\";\n\nimport { CONSTANTS } from \"./src/utils.ts\";\n\nconst baseURL = Deno.env.get(\"GITHUB_API\") || CONSTANTS.DEFAULT_GITHUB_API;\n\nconst soxa = new ServiceProvider({\n  ...defaults,\n  baseURL,\n});\n\nexport {\n  assertEquals,\n  assertRejects,\n  assertSpyCalls,\n  returnsNext,\n  soxa,\n  spy,\n  stub,\n};\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: \"3\"\nservices:\n  redis:\n    container_name: trophy-redis\n    image: redis:latest\n    ports:\n      - \"6379:6379\"\n  deno-app:\n    build: .\n    volumes:\n      - .:/app\n    ports:\n      - \"80:8080\"\n    environment:\n      - DENO_ENV=development\n    command: [\"deno\", \"run\", \"--watch\", \"-A\", \"main.ts\"]\n"
  },
  {
    "path": "env-example",
    "content": "PORT=8080\nGITHUB_TOKEN1=\nGITHUB_TOKEN2=\nGITHUB_API=https://api.github.com/graphql\nENABLE_REDIS=\nREDIS_PORT=6379\nREDIS_HOST=\nREDIS_USERNAME=\nREDIS_PASSWORD=\n"
  },
  {
    "path": "main.ts",
    "content": "import { serve } from \"https://deno.land/std@0.125.0/http/server.ts\";\nimport requestHandler from \"./api/index.ts\";\n\nserve(requestHandler, { port: Number(Deno.env.get(\"PORT\")) || 8080 });\n"
  },
  {
    "path": "render_svg.ts",
    "content": "import \"https://deno.land/x/dotenv@v0.5.0/load.ts\";\n\nconst username = Deno.args[0];\nconst outputPath = Deno.args[1] ?? \"./assets/trophy.svg\";\nconst themeName = Deno.args[2] ?? \"default\";\n\nif (!username) {\n  console.error(\n    \"Usage: deno run --allow-net --allow-env --allow-read --allow-write ./render_svg.ts USERNAME [OUTPUT_PATH] [THEME]\",\n  );\n  Deno.exit(1);\n}\n\nimport { GithubApiService } from \"./src/Services/GithubApiService.ts\";\nimport { Card } from \"./src/card.ts\";\nimport { COLORS } from \"./src/theme.ts\";\n\nasync function main() {\n  console.log(\"Starting trophy render...\");\n  console.log(\"Username:\", username);\n  console.log(\"Output path:\", outputPath);\n  console.log(\"Theme:\", themeName);\n\n  const svc = new GithubApiService();\n\n  const userInfoOrError = await svc.requestUserInfo(username);\n\n  if (\n    !(userInfoOrError && (userInfoOrError as any).totalCommits !== undefined)\n  ) {\n    console.error(\n      \"Failed to fetch user info. Check token, username and rate limits.\",\n    );\n    Deno.exit(2);\n  }\n\n  const userInfo = userInfoOrError as any;\n\n  const panelSize = 115;\n  const maxRow = 10;\n  const maxColumn = -1; // auto\n  const marginWidth = 10;\n  const marginHeight = 10;\n  const noBackground = false;\n  const noFrame = false;\n\n  const card = new Card(\n    [],\n    [],\n    maxColumn,\n    maxRow,\n    panelSize,\n    marginWidth,\n    marginHeight,\n    noBackground,\n    noFrame,\n  );\n  const theme = (COLORS as any)[themeName] ?? (COLORS as any).default;\n  const svg = card.render(userInfo, theme);\n\n  try {\n    const dir = outputPath.replace(/\\/[^/]+$/, \"\");\n    if (dir) await Deno.mkdir(dir, { recursive: true });\n  } catch {\n    console.error(\"Failed to create directory. No permission?\");\n    Deno.exit(3);\n  }\n\n  await Deno.writeTextFile(outputPath, svg);\n  console.log(`Wrote ${outputPath}`);\n}\n\nawait main();\n"
  },
  {
    "path": "src/Helpers/Logger.ts",
    "content": "const enableLogging = Deno.env.get(\"ENV_TYPE\") !== \"test\";\n\nexport class Logger {\n  public static log(message: unknown): void {\n    if (!enableLogging) return;\n    console.log(message);\n  }\n\n  public static error(message: unknown): void {\n    if (!enableLogging) return;\n\n    console.error(message);\n  }\n  public static warn(message: unknown): void {\n    if (!enableLogging) return;\n\n    console.warn(message);\n  }\n}\n"
  },
  {
    "path": "src/Helpers/Retry.ts",
    "content": "import { ServiceError } from \"../Types/index.ts\";\nimport { Logger } from \"./Logger.ts\";\n\nexport type RetryCallbackProps = {\n  attempt: number;\n};\n\ntype callbackType<T = unknown> = (data: RetryCallbackProps) => Promise<T> | T;\n\nasync function* createAsyncIterable<T>(\n  callback: callbackType<T>,\n  retries: number,\n  delay: number,\n) {\n  for (let i = 0; i < retries; i++) {\n    const isLastAttempt = i === retries - 1;\n    try {\n      const data = await callback({ attempt: i });\n      yield data;\n      return;\n    } catch (e) {\n      if (e instanceof ServiceError && isLastAttempt) {\n        yield e;\n        return;\n      }\n\n      yield null;\n      Logger.error(e);\n      await new Promise((resolve) => setTimeout(resolve, delay));\n    }\n  }\n}\n\nexport class Retry {\n  constructor(private maxRetries = 2, private retryDelay = 1000) {}\n  async fetch<T = unknown>(\n    callback: callbackType<T>,\n  ) {\n    let lastError = null;\n    for await (\n      const callbackResult of createAsyncIterable<T>(\n        callback,\n        this.maxRetries,\n        this.retryDelay,\n      )\n    ) {\n      const isError = callbackResult instanceof Error;\n\n      if (callbackResult && !isError) {\n        return callbackResult as T;\n      }\n\n      if (isError) {\n        lastError = callbackResult;\n      }\n    }\n\n    throw new Error(`Max retries (${this.maxRetries}) exceeded.`, {\n      cause: lastError,\n    });\n  }\n}\n"
  },
  {
    "path": "src/Helpers/__tests__/Retry.test.ts",
    "content": "import { Retry } from \"../Retry.ts\";\nimport {\n  assertEquals,\n  assertRejects,\n  assertSpyCalls,\n  spy,\n} from \"../../../deps.ts\";\n\ntype MockResponse = {\n  value: number;\n};\n\nDeno.test(\"Retry.fetch\", () => {\n  const retryInstance = new Retry();\n  const callback = spy(retryInstance, \"fetch\");\n\n  retryInstance.fetch<MockResponse>(() => {\n    return { value: 1 };\n  });\n\n  assertSpyCalls(callback, 1);\n});\n\nDeno.test(\"Should retry\", async () => {\n  let countErrors = 0;\n\n  const callbackError = () => {\n    countErrors++;\n    throw new Error(\"Panic! Threw Error\");\n  };\n  const retries = 3;\n  const retryInstance = new Retry(retries);\n\n  await assertRejects(\n    () => {\n      return retryInstance.fetch<MockResponse>(callbackError);\n    },\n    Error,\n    `Max retries (${retries}) exceeded.`,\n  );\n\n  assertEquals(countErrors, 3);\n});\n\nDeno.test(\"Should retry the asyncronous callback\", async () => {\n  let countErrors = 0;\n  const callbackError = async () => {\n    countErrors++;\n    // Mock request in callback\n    await new Promise((_, reject) => setTimeout(reject, 100));\n  };\n\n  const retries = 3;\n  const retryInstance = new Retry(retries);\n\n  await assertRejects(\n    () => {\n      return retryInstance.fetch(callbackError);\n    },\n    Error,\n    `Max retries (${retries}) exceeded.`,\n  );\n\n  assertEquals(countErrors, 3);\n});\n"
  },
  {
    "path": "src/Repository/GithubRepository.ts",
    "content": "import { ServiceError } from \"../Types/index.ts\";\nimport {\n  GitHubUserActivity,\n  GitHubUserAll,\n  GitHubUserIssue,\n  GitHubUserPullRequest,\n  GitHubUserRepository,\n  UserInfo,\n} from \"../user_info.ts\";\n\nexport abstract class GithubRepository {\n  abstract requestUserInfo(username: string): Promise<UserInfo | ServiceError>;\n  abstract requestUserAll(\n    username: string,\n  ): Promise<GitHubUserAll | ServiceError>;\n  abstract requestUserActivity(\n    username: string,\n  ): Promise<GitHubUserActivity | ServiceError>;\n  abstract requestUserIssue(\n    username: string,\n  ): Promise<GitHubUserIssue | ServiceError>;\n  abstract requestUserPullRequest(\n    username: string,\n  ): Promise<GitHubUserPullRequest | ServiceError>;\n  abstract requestUserRepository(\n    username: string,\n  ): Promise<GitHubUserRepository | ServiceError>;\n}\n\nexport class GithubRepositoryService {\n  constructor(public repository: GithubRepository) {}\n}\n"
  },
  {
    "path": "src/Schemas/index.ts",
    "content": "export const queryUserActivity = `\n    query userInfo($username: String!) {\n      user(login: $username) {\n        createdAt\n        contributionsCollection {\n          totalCommitContributions\n          restrictedContributionsCount\n          totalPullRequestReviewContributions\n        }\n        organizations(first: 1) {\n          totalCount\n        }\n        followers(first: 1) {\n          totalCount\n        }\n      }\n    }\n`;\n\nexport const queryUserIssue = `\n  query userInfo($username: String!) {\n    user(login: $username) {\n      openIssues: issues(states: OPEN) {\n        totalCount\n      }\n      closedIssues: issues(states: CLOSED) {\n        totalCount\n      }\n    }\n  }\n`;\n\nexport const queryUserPullRequest = `\n  query userInfo($username: String!) {\n    user(login: $username) {\n      pullRequests(first: 1) {\n        totalCount\n      }\n    }\n  }\n`;\n\nexport const queryUserRepository = `\n  query userInfo($username: String!) {\n    user(login: $username) {\n      repositories(first: 50, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}) {\n        totalCount\n        nodes {\n          languages(first: 2, orderBy: {direction:DESC, field: SIZE}) {\n            nodes {\n              name\n            }\n          }\n          stargazers {\n            totalCount\n          }\n          createdAt\n        }\n      }\n    }\n  }\n`;\n\nexport const queryUserAll = `\n  query userInfo($username: String!) {\n    user(login: $username) {\n      createdAt\n      contributionsCollection {\n        totalCommitContributions\n        restrictedContributionsCount\n        totalPullRequestReviewContributions\n      }\n      organizations(first: 1) {\n        totalCount\n      }\n      followers(first: 1) {\n        totalCount\n      }\n      openIssues: issues(states: OPEN) {\n        totalCount\n      }\n      closedIssues: issues(states: CLOSED) {\n        totalCount\n      }\n      pullRequests(first: 1) {\n        totalCount\n      }\n      repositories(first: 50, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}) {\n        totalCount\n        nodes {\n          languages(first: 2, orderBy: {direction:DESC, field: SIZE}) {\n            nodes {\n              name\n            }\n          }\n          stargazers {\n            totalCount\n          }\n          createdAt\n        }\n      }\n    }\n  }\n`;\n"
  },
  {
    "path": "src/Services/GithubApiService.ts",
    "content": "import { GithubRepository } from \"../Repository/GithubRepository.ts\";\nimport {\n  GitHubUserActivity,\n  GitHubUserAll,\n  GitHubUserIssue,\n  GitHubUserPullRequest,\n  GitHubUserRepository,\n  UserInfo,\n} from \"../user_info.ts\";\nimport {\n  queryUserActivity,\n  queryUserAll,\n  queryUserIssue,\n  queryUserPullRequest,\n  queryUserRepository,\n} from \"../Schemas/index.ts\";\nimport { Retry } from \"../Helpers/Retry.ts\";\nimport { CONSTANTS } from \"../utils.ts\";\nimport { EServiceKindError, ServiceError } from \"../Types/index.ts\";\nimport { Logger } from \"../Helpers/Logger.ts\";\nimport { requestGithubData } from \"./request.ts\";\n\n// Need to be here - Exporting from another file makes array of null\nexport const TOKENS = [\n  Deno.env.get(\"GITHUB_TOKEN1\"),\n  Deno.env.get(\"GITHUB_TOKEN2\"),\n];\n\nexport class GithubApiService extends GithubRepository {\n  async requestUserAll(\n    username: string,\n  ): Promise<GitHubUserAll | ServiceError> {\n    return await this.executeQuery<GitHubUserAll>(queryUserAll, {\n      username,\n    });\n  }\n  async requestUserRepository(\n    username: string,\n  ): Promise<GitHubUserRepository | ServiceError> {\n    return await this.executeQuery<GitHubUserRepository>(queryUserRepository, {\n      username,\n    });\n  }\n  async requestUserActivity(\n    username: string,\n  ): Promise<GitHubUserActivity | ServiceError> {\n    return await this.executeQuery<GitHubUserActivity>(queryUserActivity, {\n      username,\n    });\n  }\n  async requestUserIssue(\n    username: string,\n  ): Promise<GitHubUserIssue | ServiceError> {\n    return await this.executeQuery<GitHubUserIssue>(queryUserIssue, {\n      username,\n    });\n  }\n  async requestUserPullRequest(\n    username: string,\n  ): Promise<GitHubUserPullRequest | ServiceError> {\n    return await this.executeQuery<GitHubUserPullRequest>(\n      queryUserPullRequest,\n      { username },\n    );\n  }\n  async requestUserInfo(username: string): Promise<UserInfo | ServiceError> {\n    // Use single combined query instead of 4 separate queries to reduce Function Duration\n    try {\n      const result = await this.requestUserAll(username);\n      if (result instanceof ServiceError) {\n        return result;\n      }\n      return UserInfo.fromCombined(result);\n    } catch {\n      Logger.error(`Error fetching user info for username: ${username}`);\n      return new ServiceError(\"Not found\", EServiceKindError.NOT_FOUND);\n    }\n  }\n\n  async executeQuery<T = unknown>(\n    query: string,\n    variables: { [key: string]: string },\n  ) {\n    try {\n      const retry = new Retry(\n        TOKENS.length,\n        CONSTANTS.DEFAULT_GITHUB_RETRY_DELAY,\n      );\n      return await retry.fetch<Promise<T>>(async ({ attempt }) => {\n        return await requestGithubData(\n          query,\n          variables,\n          TOKENS[attempt],\n        );\n      });\n    } catch (error) {\n      if (error.cause instanceof ServiceError) {\n        Logger.error(error.cause.message);\n        return error.cause;\n      }\n      if (error instanceof Error && error.cause) {\n        Logger.error(JSON.stringify(error.cause, null, 2));\n      } else {\n        Logger.error(error);\n      }\n      return new ServiceError(\"not found\", EServiceKindError.NOT_FOUND);\n    }\n  }\n}\n"
  },
  {
    "path": "src/Services/__mocks__/notFoundUserMock.json",
    "content": "{\n  \"data\": {\n    \"data\": {\n      \"user\": null\n    },\n    \"errors\": [\n      {\n        \"type\": \"NOT_FOUND\",\n        \"path\": [\n          \"user\"\n        ],\n        \"locations\": [\n          {\n            \"line\": 2,\n            \"column\": 5\n          }\n        ],\n        \"message\": \"Could not resolve to a User with the login of 'alekinho'.\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "src/Services/__mocks__/rateLimitMock.json",
    "content": "{\n  \"exceeded\": {\n    \"data\": {\n      \"documentation_url\": \"https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#secondary-rate-limits\",\n      \"message\": \"You have exceeded a secondary rate limit. Please wait a few minutes before you try again. If you reach out to GitHub Support for help, please include the request ID DBD8:FB98:31801A8:3222432:65195FDB.\"\n    }\n  },\n  \"rate_limit\": {\n    \"data\": {\n      \"data\": {\n        \"user\": null\n      },\n      \"errors\": [\n        {\n          \"type\": \"RATE_LIMITED\",\n          \"message\": \"API rate limit exceeded for user ID 10711649.\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "src/Services/__mocks__/successGithubResponse.json",
    "content": "{\n  \"data\": {\n    \"data\": {\n      \"user\": {\n        \"repositories\": {\n          \"totalCount\": 128,\n          \"nodes\": [\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 23\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 11\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 9\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 6\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 6\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"Java\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 5\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 5\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"Jupyter Notebook\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 5\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 4\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 3\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 2\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 2\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 2\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": []\n              },\n              \"stargazers\": {\n                \"totalCount\": 1\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 1\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"PHP\"\n                  },\n                  {\n                    \"name\": \"Go\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 1\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 1\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 1\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"Dockerfile\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 1\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"Dockerfile\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 1\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"Dart\"\n                  },\n                  {\n                    \"name\": \"Swift\"\n                  },\n                  {\n                    \"name\": \"Kotlin\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 1\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"HTML\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 1\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"Vue\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 1\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"Jupyter Notebook\"\n                  },\n                  {\n                    \"name\": \"Python\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 1\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"Dart\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  },\n                  {\n                    \"name\": \"Swift\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 1\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"Vue\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 1\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"PHP\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 1\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": []\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": []\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"Shell\"\n                  },\n                  {\n                    \"name\": \"Dockerfile\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"CSS\"\n                  },\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"Dockerfile\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": []\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"TypeScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  },\n                  {\n                    \"name\": \"C#\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"PHP\"\n                  },\n                  {\n                    \"name\": \"Vue\"\n                  },\n                  {\n                    \"name\": \"Blade\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"CSS\"\n                  },\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"C\"\n                  },\n                  {\n                    \"name\": \"C++\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"C++\"\n                  },\n                  {\n                    \"name\": \"Makefile\"\n                  },\n                  {\n                    \"name\": \"CMake\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"C++\"\n                  },\n                  {\n                    \"name\": \"Makefile\"\n                  },\n                  {\n                    \"name\": \"CMake\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"TypeScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"Vue\"\n                  },\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": []\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"PHP\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"Blade\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"Rust\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"Svelte\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"CSS\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"Dockerfile\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"Rust\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"Dockerfile\"\n                  },\n                  {\n                    \"name\": \"Shell\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"Shell\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"HTML\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"Dart\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  },\n                  {\n                    \"name\": \"Swift\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"Dart\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"Rust\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  },\n                  {\n                    \"name\": \"Swift\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"SCSS\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"Shell\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"SCSS\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"SCSS\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"SCSS\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"JavaScript\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  },\n                  {\n                    \"name\": \"SCSS\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"Swift\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            },\n            {\n              \"languages\": {\n                \"nodes\": [\n                  {\n                    \"name\": \"TypeScript\"\n                  },\n                  {\n                    \"name\": \"HTML\"\n                  },\n                  {\n                    \"name\": \"CSS\"\n                  }\n                ]\n              },\n              \"stargazers\": {\n                \"totalCount\": 0\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Services/__tests__/githubApiService.test.ts",
    "content": "import { GithubApiService } from \"../GithubApiService.ts\";\nimport { assertEquals, returnsNext, soxa, stub } from \"../../../deps.ts\";\nimport { GitHubUserRepository } from \"../../user_info.ts\";\n\nconst rateLimitMock = await import(\"../__mocks__/rateLimitMock.json\", {\n  with: { type: \"json\" },\n});\n\nconst successGithubResponseMock = await import(\n  \"../__mocks__/successGithubResponse.json\",\n  { with: { type: \"json\" } }\n);\n\nconst notFoundGithubResponseMock = await import(\n  \"../__mocks__/notFoundUserMock.json\",\n  { with: { type: \"json\" } }\n);\n\nimport { ServiceError } from \"../../Types/index.ts\";\n\n// Unfortunatelly, The spy is a global instance\n// We can't reset mock as Jest does.\nstub(\n  soxa,\n  \"post\",\n  returnsNext([\n    // Should get data in first try\n    new Promise((resolve) => {\n      resolve(successGithubResponseMock.default);\n    }),\n    // Should throw NOT FOUND (requestUserInfo makes 1 combined API call)\n    // Each call makes 2 attempts (one per token), so 2 promises total\n    new Promise((resolve) => {\n      resolve(notFoundGithubResponseMock.default);\n    }),\n    new Promise((resolve) => {\n      resolve(notFoundGithubResponseMock.default);\n    }),\n    // Should throw NOT FOUND even if request the user only\n    new Promise((resolve) => {\n      resolve(notFoundGithubResponseMock.default);\n    }),\n    new Promise((resolve) => {\n      resolve(notFoundGithubResponseMock.default);\n    }),\n    // Should throw RATE LIMIT\n    new Promise((resolve) => {\n      resolve(rateLimitMock.default.rate_limit);\n    }),\n    new Promise((resolve) => {\n      resolve(rateLimitMock.default.rate_limit);\n    }),\n    // Should throw RATE LIMIT Exceed\n    new Promise((resolve) => {\n      resolve(rateLimitMock.default.rate_limit);\n    }),\n    new Promise((resolve) => {\n      resolve(rateLimitMock.default.exceeded);\n    }),\n  ]),\n);\n\nDeno.test(\"Should get data in first try\", async () => {\n  const provider = new GithubApiService();\n\n  const data = await provider.requestUserRepository(\n    \"test\",\n  ) as GitHubUserRepository;\n\n  assertEquals(data.repositories.totalCount, 128);\n});\n\n//Deno.test(\"Should get data in second Retry\", async () => {\n//  const provider = new GithubApiService();\n//\n//  const data = await provider.requestUserRepository(\n//    \"test\",\n//  ) as GitHubUserRepository;\n//\n//  assertEquals(data.repositories.totalCount, 128);\n//});\n\nDeno.test(\"Should throw NOT FOUND\", async () => {\n  const provider = new GithubApiService();\n  let error = null;\n\n  try {\n    error = await provider.requestUserInfo(\"test\");\n  } catch (e) {\n    error = e;\n  }\n\n  assertEquals(error.code, 404);\n  assertEquals(error instanceof ServiceError, true);\n});\nDeno.test(\"Should throw NOT FOUND even if request the user only\", async () => {\n  const provider = new GithubApiService();\n  let error = null;\n\n  try {\n    error = await provider.requestUserRepository(\"test\");\n  } catch (e) {\n    error = e;\n  }\n\n  assertEquals(error.code, 404);\n  assertEquals(error instanceof ServiceError, true);\n});\n\n// The assertRejects() assertion is a little more complicated\n// mainly because it deals with Promises.\n// https://docs.deno.com/runtime/manual/basics/testing/assertions#throws\nDeno.test(\"Should throw RATE LIMIT\", async () => {\n  const provider = new GithubApiService();\n  let error = null;\n\n  try {\n    error = await provider.requestUserRepository(\"test\");\n  } catch (e) {\n    error = e;\n  }\n\n  assertEquals(error.code, 419);\n  assertEquals(error instanceof ServiceError, true);\n});\n\nDeno.test(\"Should throw RATE LIMIT Exceed\", async () => {\n  const provider = new GithubApiService();\n  let error = null;\n\n  try {\n    error = await provider.requestUserRepository(\"test\");\n  } catch (e) {\n    error = e;\n  }\n\n  assertEquals(error.code, 419);\n  assertEquals(error instanceof ServiceError, true);\n});\n"
  },
  {
    "path": "src/Services/request.ts",
    "content": "import { soxa } from \"../../deps.ts\";\nimport {\n  EServiceKindError,\n  GithubError,\n  GithubErrorResponse,\n  GithubExceedError,\n  QueryDefaultResponse,\n  ServiceError,\n} from \"../Types/index.ts\";\n\nexport async function requestGithubData<T = unknown>(\n  query: string,\n  variables: { [key: string]: string },\n  token = \"\",\n) {\n  const response = await soxa.post(\"\", {}, {\n    data: { query, variables },\n    headers: {\n      Authorization: `bearer ${token}`,\n    },\n  }) as QueryDefaultResponse<{ user: T }>;\n  const responseData = response.data;\n\n  if (responseData?.data?.user) {\n    return responseData.data.user;\n  }\n\n  throw handleError(responseData);\n}\n\nfunction handleError(\n  responseData: {\n    data?: unknown;\n    errors?: GithubError[];\n    message?: string;\n    documentation_url?: string;\n  },\n): ServiceError {\n  let isRateLimitExceeded = false;\n  const arrayErrors = responseData?.errors || [];\n\n  if (Array.isArray(arrayErrors) && arrayErrors.length > 0) {\n    isRateLimitExceeded = arrayErrors.some((error) =>\n      error.type.includes(EServiceKindError.RATE_LIMIT)\n    );\n  }\n\n  if (responseData?.message) {\n    isRateLimitExceeded = responseData.message.toLowerCase().includes(\n      \"rate limit\",\n    );\n  }\n\n  if (isRateLimitExceeded) {\n    throw new ServiceError(\n      \"Rate limit exceeded\",\n      EServiceKindError.RATE_LIMIT,\n    );\n  }\n\n  throw new ServiceError(\n    \"unknown error\",\n    EServiceKindError.NOT_FOUND,\n  );\n}\n"
  },
  {
    "path": "src/StaticRenderRegeneration/cache_manager.ts",
    "content": "import { Logger } from \"../Helpers/Logger.ts\";\nimport { existsSync } from \"./utils.ts\";\n\nexport class CacheManager {\n  constructor(private revalidateTime: number, private cacheFile: string) {}\n\n  // Reason to use /tmp/:\n  // https://github.com/orgs/vercel/discussions/314\n  get cacheFilePath(): string {\n    return `/tmp/${this.cacheFile}`;\n  }\n  get cacheFileExists(): boolean {\n    return existsSync(this.cacheFilePath);\n  }\n\n  get cacheFileLastModified(): Date | null {\n    if (!this.cacheFileExists) {\n      return null;\n    }\n    const fileInfo = Deno.statSync(this.cacheFilePath);\n    return fileInfo.mtime ?? null;\n  }\n\n  get cacheFileLastModifiedGetTime(): number | null {\n    const lastModified = this.cacheFileLastModified;\n    if (lastModified === null) {\n      return null;\n    }\n    return lastModified.getTime();\n  }\n\n  get isCacheValid(): boolean {\n    if (this.cacheFileLastModifiedGetTime === null) {\n      return false;\n    }\n    const currentTime = new Date().getTime();\n    return currentTime - this.cacheFileLastModifiedGetTime <\n      this.revalidateTime;\n  }\n\n  async save(response: Response): Promise<void> {\n    if (response === null) return;\n    // Prevent TypeError: ReadableStream is locked\n    const text = await response.clone().text();\n    const data = new TextEncoder().encode(text);\n\n    Deno.writeFile(this.cacheFilePath, data, { create: true }).catch(() => {\n      Logger.warn(\"Failed to save cache file\");\n    });\n  }\n}\n"
  },
  {
    "path": "src/StaticRenderRegeneration/index.ts",
    "content": "import { CacheManager } from \"./cache_manager.ts\";\nimport { StaticRegenerationOptions } from \"./types.ts\";\nimport { getUrl, hashString, readCache } from \"./utils.ts\";\n\nexport async function staticRenderRegeneration(\n  request: Request,\n  options: StaticRegenerationOptions,\n  render: (request: Request) => Promise<Response>,\n) {\n  // avoid TypeError: Invalid URL at deno:core\n  const url = getUrl(request);\n\n  // if more conditions are added, make sure to create a variable to skipCache\n  if (url.pathname === \"/favicon.ico\") {\n    return await render(request);\n  }\n\n  const cacheFile = await hashString(url.pathname + (url.search ?? \"\"));\n  const cacheManager = new CacheManager(options.revalidate ?? 0, cacheFile);\n  if (cacheManager.isCacheValid) {\n    const cache = readCache(cacheManager.cacheFilePath);\n    if (cache !== null) {\n      return new Response(cache, {\n        headers: options.headers ?? new Headers({}),\n      });\n    }\n  }\n\n  const response = await render(request);\n\n  if (response.status >= 200 && response.status < 300) {\n    void cacheManager.save(response);\n  }\n\n  return response;\n}\n"
  },
  {
    "path": "src/StaticRenderRegeneration/types.ts",
    "content": "export interface StaticRegenerationOptions {\n  // The number of milliseconds before the page should be revalidated\n  revalidate?: number;\n  // The headers to be sent with the response\n  headers?: Headers;\n}\n"
  },
  {
    "path": "src/StaticRenderRegeneration/utils.ts",
    "content": "export function getUrl(request: Request) {\n  try {\n    return new URL(request.url);\n  } catch {\n    return {\n      pathname: request.url,\n      search: request.url,\n    };\n  }\n}\n\nexport function readCache(cacheFilePath: string): Uint8Array | null {\n  try {\n    return Deno.readFileSync(cacheFilePath);\n  } catch {\n    return null;\n  }\n}\n\n// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest\nexport async function hashString(message: string): Promise<string> {\n  const encoder = new TextEncoder();\n  const data = encoder.encode(message);\n  const hashBuffer = await crypto.subtle.digest(\"SHA-256\", data);\n\n  const hashArray = Array.from(new Uint8Array(hashBuffer));\n  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, \"0\")).join(\n    \"\",\n  );\n\n  return hashHex;\n}\n\nexport const existsSync = (filename: string): boolean => {\n  try {\n    Deno.statSync(filename);\n    // successful, file or directory must exist\n    return true;\n  } catch {\n    return false;\n  }\n};\n"
  },
  {
    "path": "src/Types/EServiceKindError.ts",
    "content": "export const enum EServiceKindError {\n  RATE_LIMIT = \"RATE_LIMITED\",\n  NOT_FOUND = \"NOT_FOUND\",\n}\n"
  },
  {
    "path": "src/Types/Request.ts",
    "content": "export type GithubError = {\n  message: string;\n  type: string;\n};\n\nexport type GithubErrorResponse = {\n  errors: GithubError[];\n};\n\nexport type GithubExceedError = {\n  documentation_url: string;\n  message: string;\n};\n\nexport type QueryDefaultResponse<T = unknown> = {\n  data: {\n    data: T;\n    errors?: GithubError[];\n    message?: string;\n    documentation_url?: string;\n  };\n};\n"
  },
  {
    "path": "src/Types/ServiceError.ts",
    "content": "import { EServiceKindError } from \"./EServiceKindError.ts\";\n\nexport class ServiceError extends Error {\n  constructor(message: string, kind: EServiceKindError) {\n    super(message);\n    this.message = message;\n    this.name = \"ServiceError\";\n    this.cause = kind;\n  }\n\n  get code(): number {\n    switch (this.cause) {\n      case EServiceKindError.RATE_LIMIT:\n        return 419;\n      case EServiceKindError.NOT_FOUND:\n        return 404;\n      default:\n        return 400;\n    }\n  }\n}\n"
  },
  {
    "path": "src/Types/index.ts",
    "content": "export * from \"./Request.ts\";\nexport * from \"./ServiceError.ts\";\nexport * from \"./EServiceKindError.ts\";\n"
  },
  {
    "path": "src/card.ts",
    "content": "import { UserInfo } from \"./user_info.ts\";\nimport { TrophyList } from \"./trophy_list.ts\";\nimport { Trophy } from \"./trophy.ts\";\nimport { Theme } from \"./theme.ts\";\n\nexport class Card {\n  private width = 0;\n  private height = 0;\n  constructor(\n    private titles: Array<string>,\n    private ranks: Array<string>,\n    private maxColumn: number,\n    private maxRow: number,\n    private panelSize: number,\n    private marginWidth: number,\n    private marginHeight: number,\n    private noBackground: boolean,\n    private noFrame: boolean,\n  ) {\n    this.width = panelSize * this.maxColumn +\n      this.marginWidth * (this.maxColumn - 1);\n  }\n  render(\n    userInfo: UserInfo,\n    theme: Theme,\n  ): string {\n    const trophyList = new TrophyList(userInfo);\n\n    trophyList.filterByHidden();\n\n    if (this.titles.length != 0) {\n      const includeTitles = this.titles.filter((title) =>\n        !title.startsWith(\"-\")\n      );\n      if (includeTitles.length > 0) {\n        trophyList.filterByTitles(includeTitles);\n      }\n      trophyList.filterByExclusionTitles(this.titles);\n    }\n\n    if (this.ranks.length != 0) {\n      trophyList.filterByRanks(this.ranks);\n    }\n\n    trophyList.sortByRank();\n\n    if (this.maxColumn == -1) {\n      this.maxColumn = trophyList.length;\n      this.width = this.panelSize * this.maxColumn +\n        this.marginWidth * (this.maxColumn - 1);\n    }\n\n    const row = this.getRow(trophyList);\n    this.height = this.getHeight(row);\n\n    return `\n    <svg\n      width=\"${this.width}\"\n      height=\"${this.height}\"\n      viewBox=\"0 0 ${this.width} ${this.height}\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      ${this.renderTrophy(trophyList, theme)}\n    </svg>`;\n  }\n  private getRow(trophyList: TrophyList) {\n    let row = Math.floor((trophyList.length - 1) / this.maxColumn) + 1;\n    if (row > this.maxRow) {\n      row = this.maxRow;\n    }\n    return row;\n  }\n  private getHeight(row: number) {\n    // Calculate the height of the card from turns\n    return this.panelSize * row + this.marginHeight * (row - 1);\n  }\n\n  private renderTrophy(trophyList: TrophyList, theme: Theme) {\n    return trophyList.getArray.reduce(\n      (sum: string, trophy: Trophy, i: number) => {\n        const currentColumn = i % this.maxColumn;\n        const currentRow = Math.floor(i / this.maxColumn);\n        const x = this.panelSize * currentColumn +\n          this.marginWidth * currentColumn;\n        const y = this.panelSize * currentRow + this.marginHeight * currentRow;\n        return sum +\n          trophy.render(\n            theme,\n            x,\n            y,\n            this.panelSize,\n            this.noBackground,\n            this.noFrame,\n          );\n      },\n      \"\",\n    );\n  }\n}\n"
  },
  {
    "path": "src/config/cache.ts",
    "content": "import { Bulk, connect, Redis } from \"../../deps.ts\";\nimport { Logger } from \"../Helpers/Logger.ts\";\nimport { CONSTANTS } from \"../utils.ts\";\n\nconst enableCache: boolean = Deno.env.get(\"ENABLE_REDIS\") === \"true\";\n\n// https://developer.redis.com/develop/deno/\nclass CacheProvider {\n  private static instance: CacheProvider;\n  public client: Redis | null = null;\n\n  private constructor() {}\n\n  static getInstance(): CacheProvider {\n    if (!CacheProvider.instance) {\n      CacheProvider.instance = new CacheProvider();\n    }\n    return CacheProvider.instance;\n  }\n\n  async connect(): Promise<void> {\n    if (!enableCache) return;\n    this.client = await connect({\n      hostname: Deno.env.get(\"REDIS_HOST\") || \"\",\n      port: Number(Deno.env.get(\"REDIS_PORT\")) || 6379,\n      username: Deno.env.get(\"REDIS_USERNAME\") || undefined,\n      password: Deno.env.get(\"REDIS_PASSWORD\") || undefined,\n    });\n  }\n\n  async get(key: string): Promise<Bulk | undefined> {\n    if (!enableCache) return undefined;\n\n    try {\n      if (!this.client) {\n        await this.connect();\n      }\n\n      return await this.client?.get(key);\n    } catch {\n      return undefined;\n    }\n  }\n\n  async set(key: string, value: string): Promise<void> {\n    if (!enableCache) return;\n\n    try {\n      if (!this.client) {\n        await this.connect();\n      }\n      await this.client?.set(key, value, {\n        px: CONSTANTS.REDIS_TTL,\n      });\n    } catch (e) {\n      Logger.error(`Failed to set cache: ${e.message}`);\n    }\n  }\n\n  async del(key: string): Promise<void> {\n    if (!enableCache) return;\n\n    try {\n      if (!this.client) {\n        await this.connect();\n      }\n      await this.client?.del(key);\n    } catch (e) {\n      Logger.error(`Failed to delete cache: ${e.message}`);\n    }\n  }\n}\n\nexport const cacheProvider = CacheProvider.getInstance();\n"
  },
  {
    "path": "src/error_page.ts",
    "content": "abstract class BaseError {\n  readonly status!: number;\n  readonly message!: string;\n  constructor(readonly content?: string) {}\n  render() {\n    return this.renderPage();\n  }\n\n  private renderPage() {\n    return `<!DOCTYPE html>\n    <html lang=\"en\"><head>\n      <meta charset=\"UTF-8\">\n      <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n      <title>GitHub Profile Trophy</title>\n      <meta name=\"description\" content=\"🏆 Add dynamically generated GitHub Stat Trophies on your readme\">\n      <style>\n        body {\n          font-family: Arial, sans-serif;\n          margin: 0;\n          padding: 0;\n          background-color: #f4f4f4;\n        }\n        h1,\n        h2 {\n          color: #333;\n        }\n        p {\n          color: #666;\n        }\n        #back-link {\n          display: flex;\n          justify-content: center;\n          text-decoration: none;\n        }\n        #back-link:hover {\n          text-decoration: underline;\n        }\n        section {\n          width: 80%;\n          margin: 0 auto;\n          padding: 20px;\n        }\n        div {\n          background-color: #fff;\n          border-radius: 5px;\n          padding: 20px;\n          margin-bottom: 20px;\n          box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);\n        }\n        form {\n          display: flex;\n          flex-direction: column;\n        }\n        label {\n          margin-bottom: 10px;\n        }\n        input {\n          padding: 12px;\n          margin-bottom: 20px;\n          border-radius: 5px;\n          border: 1px solid #ddd;\n        }\n        button {\n          padding: 10px 20px;\n          background-color: #333;\n          color: #fff;\n          border: none;\n          border-radius: 5px;\n          cursor: pointer;\n        }\n        #base-show {\n          font-size: 16px;\n          color: #333;\n          background-color: #f4f4f4;\n          padding: 10px;\n          border-radius: 5px;\n          text-align: center;\n          margin: 10px 0;\n        }\n        button:hover {\n          background-color: #444;\n        }\n        @media (max-width: 768px) {\n          #base-show {\n            font-size: 14px;\n          }\n        }\n        @media (max-width: 480px) {\n          #base-show {\n            font-size: 8px;\n          }\n        }\n        @media (min-width: 768px) {\n          section {\n            width: 60%;\n          }\n        }\n        @media (min-width: 1024px) {\n          section {\n            width: 50%;\n          }\n        }\n      </style>\n    </head>\n    <body>\n      <h1 style=\"text-align: center;\">${this.status} - ${this.message}</h1>\n      <p style=\"text-align: center;\">${this.content ?? \"\"}</p>\n      ${\n      this.content &&\n      '<a id=\"back-link\" href=\"/\">Go back</a>'\n    }\n    </body>\n    </html>`;\n  }\n}\n\nexport class Error400 extends BaseError {\n  readonly status = 400;\n  readonly message = \"Bad Request\";\n}\n\nexport class Error419 extends BaseError {\n  readonly status = 419;\n  readonly message = \"Rate Limit Exceeded\";\n}\n\nexport class Error404 extends BaseError {\n  readonly status = 404;\n  readonly message = \"Not Found\";\n}\n"
  },
  {
    "path": "src/icons.ts",
    "content": "import { RANK } from \"./utils.ts\";\nimport { Theme } from \"./theme.ts\";\n\nconst leafIcon = (laurel: string): string => {\n  return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"90pt\" height=\"90pt\" viewBox=\"0 0 100 100\" preserveAspectRatio=\"xMidYMid meet\">\n<metadata>\nCreated by potrace 1.15, written by Peter Selinger 2001-2017\n</metadata>\n<g transform=\"translate(20.000000,60.000000) scale(0.00400000,-0.00400000)\" fill=\"${laurel}\" stroke=\"none\">\n<path d=\"M200 5103 c0 -2 18 -40 41 -84 47 -95 62 -132 50 -125 -15 10 -18 -39 -6 -87 31 -121 265 -468 412 -608 124 -119 281 -222 383 -251 36 -10 49 -16 30 -13 -19 3 -78 12 -130 20 -117 17 -353 35 -477 35 l-93 0 53 -82 c72 -112 72 -112 59 -104 -18 11 -26 -41 -13 -84 25 -84 261 -376 404 -502 95 -83 222 -168 304 -205 98 -43 194 -73 242 -74 l46 -1 -45 -8 c-25 -5 -124 -21 -220 -36 -96 -15 -177 -29 -180 -31 -2 -2 0 -7 5 -11 27 -19 138 -144 123 -139 -18 6 -28 -10 -28 -47 0 -38 53 -108 141 -187 349 -313 631 -450 939 -453 63 0 131 2 150 7 19 4 -35 -17 -120 -46 -236 -82 -310 -110 -310 -117 0 -3 29 -28 65 -54 55 -40 102 -84 67 -62 -13 8 -32 -24 -32 -54 0 -99 486 -361 790 -426 125 -27 327 -25 444 4 113 28 261 98 309 145 39 40 56 92 38 124 -8 17 -4 24 29 49 22 16 40 32 40 36 0 4 -26 40 -58 80 -162 203 -368 328 -608 369 -89 15 -368 6 -474 -15 -131 -26 -147 -26 -59 -3 51 13 102 34 122 50 38 29 61 84 51 123 -5 18 1 26 25 39 17 8 31 19 31 23 0 4 -16 38 -35 75 -163 317 -424 501 -781 548 -113 15 -127 19 -91 30 51 14 84 58 89 118 1 20 9 33 20 37 23 7 23 6 -12 114 -108 329 -305 534 -640 662 -41 15 -59 25 -40 21 19 -5 82 -8 140 -8 81 0 113 4 142 18 39 20 73 76 65 107 -3 12 2 20 14 23 23 6 23 21 4 124 -61 320 -249 568 -544 718 -157 79 -394 147 -666 190 -88 13 -170 26 -182 29 -13 2 -23 2 -23 -1z\"/>\n<path d=\"M12550 5099 c-232 -36 -334 -55 -445 -84 -484 -122 -761 -346 -880 -712 -26 -79 -57 -242 -48 -255 2 -5 14 -8 26 -9 12 0 16 -3 10 -6 -17 -6 -16 -38 2 -72 25 -49 75 -66 200 -66 61 0 124 4 140 8 17 5 -13 -9 -66 -31 -136 -55 -250 -126 -341 -211 -128 -120 -217 -263 -272 -439 -32 -101 -32 -110 -3 -122 12 -5 17 -9 11 -9 -8 -1 -9 -12 -5 -34 15 -66 41 -95 94 -107 31 -7 31 -7 7 -12 -14 -3 -72 -13 -130 -22 -322 -51 -553 -206 -714 -479 -25 -42 -52 -92 -60 -111 -14 -33 -14 -33 23 -54 20 -12 31 -22 26 -22 -15 0 -17 -39 -4 -78 14 -42 76 -88 130 -97 22 -4 39 -9 36 -11 -2 -2 -55 3 -118 12 -154 22 -395 15 -494 -13 -216 -62 -391 -184 -545 -380 l-41 -52 40 -32 c34 -27 39 -35 30 -51 -17 -33 -1 -85 38 -125 48 -47 196 -117 309 -145 117 -29 319 -31 444 -4 300 64 790 328 790 425 0 29 -18 63 -31 56 -5 -4 -9 -4 -9 -2 0 3 34 29 75 59 41 29 75 56 75 59 0 3 -21 13 -47 22 -349 120 -422 146 -388 140 59 -12 241 -8 310 6 208 43 437 158 636 322 241 199 314 293 265 342 -13 12 -6 24 51 86 36 40 64 73 62 75 -4 2 -107 20 -359 60 -70 12 -77 14 -39 15 48 1 144 31 241 74 139 62 318 202 451 352 104 117 225 279 249 333 21 47 21 99 0 94 -16 -3 -10 9 58 116 l50 77 -72 3 c-91 4 -362 -14 -488 -33 -179 -26 -179 -26 -116 -9 93 26 244 120 365 230 193 174 467 605 443 696 -2 10 -8 15 -13 12 -12 -7 3 30 51 126 23 45 40 85 38 89 -2 4 -23 4 -48 0z m-933 -1185 c-3 -3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z m-1290 -1860 c-3 -3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z m40 -10 c-3 -3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z\"/>\n<path d=\"M10242 4632 c-46 -140 -92 -319 -118 -457 -18 -94 -28 -519 -12 -509 4 3 5 -8 2 -25 -8 -40 21 -179 58 -274 75 -195 297 -437 400 -437 57 0 124 70 177 185 53 112 67 224 65 510 -1 120 -5 166 -23 233 -66 252 -206 515 -423 795 -39 51 -74 95 -77 99 -4 4 -26 -50 -49 -120z\"/>\n<path d=\"M2407 4612 c-305 -408 -443 -757 -422 -1067 3 -55 8 -140 10 -190 5 -113 30 -204 83 -293 44 -74 88 -118 128 -128 102 -26 339 218 422 433 34 87 67 235 58 258 -3 9 -1 39 5 68 5 28 10 117 10 197 1 155 -15 284 -57 455 -33 137 -117 405 -126 405 -5 0 -55 -62 -111 -138z\"/>\n<path d=\"M2970 3839 c-189 -385 -254 -632 -248 -941 2 -86 6 -151 10 -145 5 7 5 -2 2 -19 -19 -98 77 -354 181 -487 76 -96 141 -120 210 -77 70 43 195 240 239 375 20 63 46 197 40 207 -3 4 0 54 6 111 26 257 -51 553 -240 922 -42 83 -87 165 -98 184 l-21 35 -81 -165z\"/>\n<path d=\"M9711 3929 c-124 -219 -230 -466 -276 -642 -39 -151 -47 -234 -41 -426 7 -237 39 -357 136 -517 136 -225 233 -251 355 -97 94 121 171 313 181 453 2 36 8 111 13 167 26 288 -56 600 -266 1017 l-60 120 -42 -75z\"/>\n<path d=\"M3645 3278 c-2 -7 -14 -78 -27 -158 -19 -123 -22 -188 -23 -430 -2 -309 7 -401 50 -555 66 -232 204 -430 388 -552 120 -80 189 -70 245 34 36 69 74 205 78 283 1 30 5 109 9 175 9 135 -1 213 -41 339 -66 208 -198 406 -429 645 -147 152 -244 237 -250 219z\"/>\n<path d=\"M8994 3143 c-289 -284 -435 -492 -514 -732 -32 -100 -55 -261 -45 -330 3 -25 8 -91 10 -146 7 -168 66 -347 129 -387 77 -48 196 11 347 170 94 98 156 199 205 331 37 99 67 234 59 262 -4 11 -2 19 5 19 8 0 10 8 7 21 -3 11 1 77 10 147 10 89 13 172 9 277 -7 191 -48 515 -65 515 -3 0 -74 -66 -157 -147z\"/>\n<path d=\"M4501 2358 c52 -129 69 -179 59 -173 -6 4 -10 -12 -10 -42 0 -78 115 -313 252 -514 215 -317 529 -509 832 -509 124 0 166 27 180 112 1 11 8 23 15 27 10 7 11 27 2 98 -66 545 -401 836 -1164 1012 -97 23 -179 41 -182 41 -3 0 5 -24 16 -52z\"/>\n<path d=\"M8159 2375 c-609 -138 -940 -344 -1096 -683 -59 -127 -110 -377 -88 -429 5 -10 10 -29 12 -42 13 -75 59 -101 179 -101 316 0 625 196 854 543 110 165 222 395 228 465 3 40 2 53 -7 48 -11 -7 -3 15 53 153 28 68 31 81 19 80 -5 0 -74 -16 -154 -34z\"/>\n<path d=\"M4032 1479 c-193 -25 -435 -124 -667 -274 -108 -69 -314 -218 -315 -226 0 -4 28 -16 63 -29 66 -24 92 -40 65 -40 -10 0 -19 -12 -23 -30 -14 -65 45 -105 226 -154 572 -155 982 -93 1270 194 75 74 101 131 81 174 -9 21 -6 31 29 77 21 29 39 55 39 58 0 3 -25 24 -55 46 -229 167 -469 236 -713 204z\"/>\n<path d=\"M8513 1476 c-155 -30 -317 -101 -446 -196 l-67 -50 40 -54 c37 -49 40 -56 29 -80 -21 -46 4 -101 80 -176 288 -287 698 -349 1270 -194 180 49 240 89 226 153 -3 16 -14 31 -23 33 -9 2 18 18 61 35 l79 30 -126 92 c-304 223 -550 347 -780 395 -113 23 -257 28 -343 12z\"/>\n<path d=\"M6324 1249 c-48 -14 -120 -83 -139 -134 -13 -34 -16 -60 -11 -112 4 -37 6 -84 6 -103 -2 -95 62 -193 145 -220 138 -46 285 52 292 195 1 28 8 71 14 97 10 37 10 60 0 101 -31 139 -167 217 -307 176z\"/>\n<path d=\"M5255 1054 c-276 -46 -587 -227 -935 -541 l-54 -50 74 -23 c41 -13 67 -25 58 -27 -23 -6 -34 -48 -20 -78 16 -35 75 -61 188 -84 568 -115 968 -37 1250 243 69 69 110 130 114 171 5 42 0 66 -12 59 -12 -8 -12 -7 39 73 18 29 33 56 33 62 0 15 -49 46 -147 95 -207 104 -386 135 -588 100z\"/>\n<path d=\"M7249 1054 c-42 -7 -109 -25 -150 -40 -76 -27 -226 -101 -267 -133 l-24 -18 41 -66 c22 -37 36 -67 31 -67 -18 0 -11 -80 11 -121 11 -22 53 -73 92 -113 279 -282 682 -361 1251 -245 113 23 172 49 188 84 14 30 3 72 -20 77 -9 3 18 15 61 29 l78 24 -93 81 c-362 313 -622 460 -902 509 -106 18 -194 18 -297 -1z\"/>\n</g>\n</svg>`;\n};\n\nexport const getNextRankBar = (\n  title: string,\n  percentage: number,\n  color: string,\n): string => {\n  const maxWidth = 80;\n  return `\n    <style>\n    @keyframes ${title}RankAnimation {\n      from {\n        width: 0px;\n      }\n      to {\n        width: ${maxWidth * percentage}px;\n      }\n    }\n    #${title}-rank-progress{\n      animation: ${title}RankAnimation 1s forwards ease-in-out;\n    }\n    </style>\n    <rect\n      x=\"15\"\n      y=\"101\"\n      rx=\"1\"\n      width=\"${maxWidth}\"\n      height=\"3.2\"\n      opacity=\"0.3\"\n      fill=\"${color}\"\n    />\n    <rect\n      id=\"${title}-rank-progress\"\n      x=\"15\"\n      y=\"101\"\n      rx=\"1\"\n      height=\"3.2\"\n      fill=\"${color}\"\n    />\n  `;\n};\n\nconst getSmallTrophyIcon = (\n  icon: string,\n  color: string,\n  count: number,\n): string => {\n  const leftXPosition = 7;\n  const rightXPosition = 68;\n  const getIcon = (x: number) => {\n    return `<svg x=\"${x}\" y=\"35\" width=\"65\" height=\"65\" viewBox=\"0 0 30 30\" fill=\"${color}\" xmlns=\"http://www.w3.org/2000/svg\">\n      ${icon}\n    </svg>`;\n  };\n  if (count == 1) {\n    // Double Rank\n    return getIcon(rightXPosition);\n  } else if (count == 2) {\n    // Triple Rank\n    return `${getIcon(leftXPosition)}${getIcon(rightXPosition)}`;\n  }\n  // Single Rank\n  return \"\";\n};\nexport const getTrophyIcon = (theme: Theme, rank = RANK.UNKNOWN) => {\n  let color = theme.DEFAULT_RANK_BASE;\n  let rankColor = theme.DEFAULT_RANK_TEXT;\n  let backgroundIcon = \"\";\n  let gradationColor = `\n      <stop offset=\"0%\" stop-color=\"${theme.DEFAULT_RANK_BASE}\"/>\n      <stop offset=\"50%\" stop-color=\"${theme.DEFAULT_RANK_BASE}\"/>\n      <stop offset=\"100%\" stop-color=\"${theme.DEFAULT_RANK_SHADOW}\"/>\n  `;\n  const { ICON_CIRCLE } = theme;\n  if (rank === RANK.SECRET) {\n    rankColor = theme.SECRET_RANK_TEXT;\n    gradationColor = `\n    <stop offset=\"0%\" stop-color=\"${theme.SECRET_RANK_1}\"/>\n    <stop offset=\"50%\" stop-color=\"${theme.SECRET_RANK_2}\"/>\n    <stop offset=\"100%\" stop-color=\"${theme.SECRET_RANK_3}\"/>\n    `;\n  } else if (rank.slice(0, 1) === RANK.S) {\n    color = theme.S_RANK_BASE;\n    rankColor = theme.S_RANK_TEXT;\n    backgroundIcon = leafIcon(theme.LAUREL);\n    gradationColor = `\n    <stop offset=\"0%\" stop-color=\"${color}\"/>\n    <stop offset=\"70%\" stop-color=\"${color}\"/>\n    <stop offset=\"100%\" stop-color=\"${theme.S_RANK_SHADOW}\"/>\n    `;\n  } else if (rank.slice(0, 1) === RANK.A) {\n    color = theme.A_RANK_BASE;\n    rankColor = theme.A_RANK_TEXT;\n    backgroundIcon = leafIcon(theme.LAUREL);\n    gradationColor = `\n    <stop offset=\"0%\" stop-color=\"${color}\"/>\n    <stop offset=\"70%\" stop-color=\"${color}\"/>\n    <stop offset=\"100%\" stop-color=\"${theme.A_RANK_SHADOW}\"/>\n    `;\n  } else if (rank === RANK.B) {\n    color = theme.B_RANK_BASE;\n    rankColor = theme.B_RANK_TEXT;\n    gradationColor = `\n    <stop offset=\"0%\" stop-color=\"${color}\"/>\n    <stop offset=\"70%\" stop-color=\"${color}\"/>\n    <stop offset=\"100%\" stop-color=\"${theme.B_RANK_SHADOW}\"/>\n    `;\n  }\n  const icon = `\n    <path d=\"M7 10h2v4H7v-4z\"/>\n    <path d=\"M10 11c0 .552-.895 1-2 1s-2-.448-2-1 .895-1 2-1 2 .448 2 1z\"/>\n    <path fill-rule=\"evenodd\" d=\"M12.5 3a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm-3 2a3 3 0 1 1 6 0 3 3 0 0 1-6 0zm-6-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm-3 2a3 3 0 1 1 6 0 3 3 0 0 1-6 0z\"/>\n    <path d=\"M3 1h10c-.495 3.467-.5 10-5 10S3.495 4.467 3 1zm0 15a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1H3zm2-1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5z\"/>\n    <circle cx=\"8\" cy=\"6\" r=\"4\" fill=\"${ICON_CIRCLE}\" />\n    <text x=\"6\" y=\"8\" font-family=\"Courier, Monospace\" font-size=\"7\" fill=\"${rankColor}\">${\n    rank.slice(0, 1)\n  }</text>\n  `;\n  const optionRankIcon = getSmallTrophyIcon(icon, color, rank.length - 1);\n  return `\n  ${backgroundIcon}\n  ${optionRankIcon}\n  <defs>\n    <linearGradient id=\"${rank}\" gradientTransform=\"rotate(45)\">\n    ${gradationColor}\n    </linearGradient>\n  </defs>\n  <svg x=\"28\" y=\"20\" width=\"100\" height=\"100\" viewBox=\"0 0 30 30\" fill=\"url(#${rank})\" xmlns=\"http://www.w3.org/2000/svg\">\n    ${icon}\n  </svg>\n  `;\n};\n"
  },
  {
    "path": "src/pages/Error.ts",
    "content": "import { EServiceKindError, ServiceError } from \"../Types/index.ts\";\nimport { Error400, Error404, Error419 } from \"../error_page.ts\";\n\ninterface ErrorPageProps {\n  error: ServiceError;\n}\n\nexport function ErrorPage({ error }: ErrorPageProps) {\n  let cause: Error400 | Error404 | Error419 = new Error400();\n\n  if (error.cause === EServiceKindError.RATE_LIMIT) {\n    cause = new Error419();\n  }\n\n  if (error.cause === EServiceKindError.NOT_FOUND) {\n    cause = new Error404(\n      \"Sorry, the user you are looking for was not found.\",\n    );\n  }\n\n  return cause;\n}\n"
  },
  {
    "path": "src/theme.ts",
    "content": "export const COLORS: { [name: string]: Theme } = {\n  default: {\n    BACKGROUND: \"#FFF\",\n    TITLE: \"#000\",\n    ICON_CIRCLE: \"#FFF\",\n    TEXT: \"#666\",\n    LAUREL: \"#009366\",\n    SECRET_RANK_1: \"red\",\n    SECRET_RANK_2: \"fuchsia\",\n    SECRET_RANK_3: \"blue\",\n    SECRET_RANK_TEXT: \"fuchsia\",\n    NEXT_RANK_BAR: \"#0366d6\",\n    S_RANK_BASE: \"#FAD200\",\n    S_RANK_SHADOW: \"#C8A090\",\n    S_RANK_TEXT: \"#886000\",\n    A_RANK_BASE: \"#B0B0B0\",\n    A_RANK_SHADOW: \"#9090C0\",\n    A_RANK_TEXT: \"#505050\",\n    B_RANK_BASE: \"#A18D66\",\n    B_RANK_SHADOW: \"#816D96\",\n    B_RANK_TEXT: \"#412D06\",\n    DEFAULT_RANK_BASE: \"#777\",\n    DEFAULT_RANK_SHADOW: \"#333\",\n    DEFAULT_RANK_TEXT: \"#333\",\n  },\n  dracula: {\n    BACKGROUND: \"#282a36\",\n    TITLE: \"#ff79c6\",\n    ICON_CIRCLE: \"#f8f8f2\",\n    TEXT: \"#f8f8f2\",\n    LAUREL: \"#50fa7b\",\n    SECRET_RANK_1: \"#ff5555\",\n    SECRET_RANK_2: \"#ff79c6\",\n    SECRET_RANK_3: \"#bd93f9\",\n    SECRET_RANK_TEXT: \"#bd93f9\",\n    NEXT_RANK_BAR: \"#ff79c6\",\n    S_RANK_BASE: \"#ffb86c\",\n    S_RANK_SHADOW: \"#ffb86c\",\n    S_RANK_TEXT: \"#6272a4\",\n    A_RANK_BASE: \"#8be9fd\",\n    A_RANK_SHADOW: \"#8be9fd\",\n    A_RANK_TEXT: \"#6272a4\",\n    B_RANK_BASE: \"#ff5555\",\n    B_RANK_SHADOW: \"#ff5555\",\n    B_RANK_TEXT: \"#6272a4\",\n    DEFAULT_RANK_BASE: \"#6272a4\",\n    DEFAULT_RANK_SHADOW: \"#6272a4\",\n    DEFAULT_RANK_TEXT: \"#6272a4\",\n  },\n  flat: {\n    BACKGROUND: \"#FFF\",\n    TITLE: \"#000\",\n    ICON_CIRCLE: \"#FFF\",\n    TEXT: \"#666\",\n    LAUREL: \"#009366\",\n    SECRET_RANK_1: \"red\",\n    SECRET_RANK_2: \"fuchsia\",\n    SECRET_RANK_3: \"blue\",\n    SECRET_RANK_TEXT: \"fuchsia\",\n    NEXT_RANK_BAR: \"#0366d6\",\n    S_RANK_BASE: \"#eac200\",\n    S_RANK_SHADOW: \"#eac200\",\n    S_RANK_TEXT: \"#886000\",\n    A_RANK_BASE: \"#B0B0B0\",\n    A_RANK_SHADOW: \"#B0B0B0\",\n    A_RANK_TEXT: \"#505050\",\n    B_RANK_BASE: \"#A18D66\",\n    B_RANK_SHADOW: \"#A18D66\",\n    B_RANK_TEXT: \"#412D06\",\n    DEFAULT_RANK_BASE: \"#777\",\n    DEFAULT_RANK_SHADOW: \"#777\",\n    DEFAULT_RANK_TEXT: \"#333\",\n  },\n  onedark: {\n    BACKGROUND: \"#282c34\",\n    TITLE: \"#e5c07b\",\n    ICON_CIRCLE: \"#FFF\",\n    TEXT: \"#e06c75\",\n    LAUREL: \"#98c379\",\n    SECRET_RANK_1: \"#e06c75\",\n    SECRET_RANK_2: \"#c678dd\",\n    SECRET_RANK_3: \"#61afef\",\n    SECRET_RANK_TEXT: \"#c678dd\",\n    NEXT_RANK_BAR: \"#e5c07b\",\n    S_RANK_BASE: \"#e5c07b\",\n    S_RANK_SHADOW: \"#e5c07b\",\n    S_RANK_TEXT: \"#282c34\",\n    A_RANK_BASE: \"#56b6c2\",\n    A_RANK_SHADOW: \"#56b6c2\",\n    A_RANK_TEXT: \"#282c34\",\n    B_RANK_BASE: \"#c678dd\",\n    B_RANK_SHADOW: \"#c678dd\",\n    B_RANK_TEXT: \"#282c34\",\n    DEFAULT_RANK_BASE: \"#abb2bf\",\n    DEFAULT_RANK_SHADOW: \"#abb2bf\",\n    DEFAULT_RANK_TEXT: \"#282c34\",\n  },\n  gruvbox: {\n    BACKGROUND: \"#282828\",\n    TITLE: \"#ebdbb2\",\n    ICON_CIRCLE: \"#ebdbb2\",\n    TEXT: \"#98971a\",\n    LAUREL: \"#689d6a\",\n    SECRET_RANK_1: \"#fb4934\",\n    SECRET_RANK_2: \"#d3869b\",\n    SECRET_RANK_3: \"#458588\",\n    SECRET_RANK_TEXT: \"#b16286\",\n    NEXT_RANK_BAR: \"#fabd26\",\n    S_RANK_BASE: \"#fabd2f\",\n    S_RANK_SHADOW: \"#fabd2f\",\n    S_RANK_TEXT: \"#322301\",\n    A_RANK_BASE: \"#83a598\",\n    A_RANK_SHADOW: \"#83a598\",\n    A_RANK_TEXT: \"#151e1a\",\n    B_RANK_BASE: \"#d65d0e\",\n    B_RANK_SHADOW: \"#d65d0e\",\n    B_RANK_TEXT: \"#301503\",\n    DEFAULT_RANK_BASE: \"#928374\",\n    DEFAULT_RANK_SHADOW: \"#928374\",\n    DEFAULT_RANK_TEXT: \"#282828\",\n  },\n  monokai: {\n    BACKGROUND: \"#272822\",\n    TITLE: \"#f92672\",\n    ICON_CIRCLE: \"#fff\",\n    TEXT: \"#fff\",\n    LAUREL: \"#a6e22e\",\n    SECRET_RANK_1: \"#f92672\",\n    SECRET_RANK_2: \"#ae81ff\",\n    SECRET_RANK_3: \"#66d9ef\",\n    SECRET_RANK_TEXT: \"#b16286\",\n    NEXT_RANK_BAR: \"#f92672\",\n    S_RANK_BASE: \"#e6db74\",\n    S_RANK_SHADOW: \"#e6db74\",\n    S_RANK_TEXT: \"#272822\",\n    A_RANK_BASE: \"#66d9ef\",\n    A_RANK_SHADOW: \"#66d9ef\",\n    A_RANK_TEXT: \"#272822\",\n    B_RANK_BASE: \"#fd971f\",\n    B_RANK_SHADOW: \"#fd971f\",\n    B_RANK_TEXT: \"#272822\",\n    DEFAULT_RANK_BASE: \"#75715e\",\n    DEFAULT_RANK_SHADOW: \"#75715e\",\n    DEFAULT_RANK_TEXT: \"#282828\",\n  },\n  nord: {\n    BACKGROUND: \"#2E3440\",\n    TITLE: \"#81A1C1\",\n    ICON_CIRCLE: \"#D8DEE9\",\n    TEXT: \"#ECEFF4\",\n    LAUREL: \"#A3BE8C\",\n    SECRET_RANK_1: \"#BF616A\",\n    SECRET_RANK_2: \"#B48EAD\",\n    SECRET_RANK_3: \"#81A1C1\",\n    SECRET_RANK_TEXT: \"#B48EAD\",\n    NEXT_RANK_BAR: \"#81A1C1\",\n    S_RANK_BASE: \"#EBCB8B\",\n    S_RANK_SHADOW: \"#EBCB8B\",\n    S_RANK_TEXT: \"#3B4252\",\n    A_RANK_BASE: \"#8FBCBB\",\n    A_RANK_SHADOW: \"#8FBCBB\",\n    A_RANK_TEXT: \"#3B4252\",\n    B_RANK_BASE: \"#D08770\",\n    B_RANK_SHADOW: \"#D08770\",\n    B_RANK_TEXT: \"#3B4252\",\n    DEFAULT_RANK_BASE: \"#5E81AC\",\n    DEFAULT_RANK_SHADOW: \"#5E81AC\",\n    DEFAULT_RANK_TEXT: \"#3B4252\",\n  },\n  discord: {\n    BACKGROUND: \"#23272A\",\n    TITLE: \"#7289DA\",\n    ICON_CIRCLE: \"#FFFFFF\",\n    TEXT: \"#FFFFFF\",\n    LAUREL: \"#57F287\",\n    SECRET_RANK_1: \"#ED4245\",\n    SECRET_RANK_2: \"#57F287\",\n    SECRET_RANK_3: \"#5865F2\",\n    SECRET_RANK_TEXT: \"#000000\",\n    NEXT_RANK_BAR: \"#5865F2\",\n    S_RANK_BASE: \"#FEE75C\",\n    S_RANK_SHADOW: \"#FEE75C\",\n    S_RANK_TEXT: \"#000000\",\n    A_RANK_BASE: \"#EB459E\",\n    A_RANK_SHADOW: \"#ED4245\",\n    A_RANK_TEXT: \"#000000\",\n    B_RANK_BASE: \"#ED4245\",\n    B_RANK_SHADOW: \"#ED4245\",\n    B_RANK_TEXT: \"#000000\",\n    DEFAULT_RANK_BASE: \"#5865F2\",\n    DEFAULT_RANK_SHADOW: \"#5865F2\",\n    DEFAULT_RANK_TEXT: \"#000000\",\n  },\n  chalk: {\n    BACKGROUND: \"#2d2d2d\",\n    TITLE: \"#fed37e\",\n    ICON_CIRCLE: \"#e4e4e4\",\n    TEXT: \"#d4d4d4\",\n    LAUREL: \"#a9d3ab\",\n    SECRET_RANK_1: \"#f58e8e\",\n    SECRET_RANK_2: \"#d6add5\",\n    SECRET_RANK_3: \"#66d9ef\",\n    SECRET_RANK_TEXT: \"#f58e8e\",\n    NEXT_RANK_BAR: \"#7aabd4\",\n    S_RANK_BASE: \"#fed37e\",\n    S_RANK_SHADOW: \"#fed37e\",\n    S_RANK_TEXT: \"#2d2d2d\",\n    A_RANK_BASE: \"#79D4D5\",\n    A_RANK_SHADOW: \"#79D4D5\",\n    A_RANK_TEXT: \"#2d2d2d\",\n    B_RANK_BASE: \"#f58e8e\",\n    B_RANK_SHADOW: \"#f58e8e\",\n    B_RANK_TEXT: \"#2d2d2d\",\n    DEFAULT_RANK_BASE: \"#75715e\",\n    DEFAULT_RANK_SHADOW: \"#75715e\",\n    DEFAULT_RANK_TEXT: \"#2d2d2d\",\n  },\n  alduin: {\n    BACKGROUND: \"#1c1c1c\",\n    TITLE: \"#dfd7af\",\n    ICON_CIRCLE: \"#e3e3e3\",\n    TEXT: \"#dfd7af\",\n    LAUREL: \"#a9d3ab\",\n    SECRET_RANK_1: \"#f58e8e\",\n    SECRET_RANK_2: \"#d6add5\",\n    SECRET_RANK_3: \"#66d9ef\",\n    SECRET_RANK_TEXT: \"#f58e8e\",\n    NEXT_RANK_BAR: \"#dfd7af\",\n    S_RANK_BASE: \"#fed37e\",\n    S_RANK_SHADOW: \"#fed37e\",\n    S_RANK_TEXT: \"#2d2d2d\",\n    A_RANK_BASE: \"#79D4D5\",\n    A_RANK_SHADOW: \"#79D4D5\",\n    A_RANK_TEXT: \"#2d2d2d\",\n    B_RANK_BASE: \"#f58e8e\",\n    B_RANK_SHADOW: \"#f58e8e\",\n    B_RANK_TEXT: \"#2d2d2d\",\n    DEFAULT_RANK_BASE: \"#75715e\",\n    DEFAULT_RANK_SHADOW: \"#75715e\",\n    DEFAULT_RANK_TEXT: \"#2d2d2d\",\n  },\n  darkhub: {\n    BACKGROUND: \"#0d1117\",\n    TITLE: \"#c9d1d9\",\n    ICON_CIRCLE: \"#f0f6fb\",\n    TEXT: \"#8b949e\",\n    LAUREL: \"#178600\",\n    SECRET_RANK_1: \"#ff5555\",\n    SECRET_RANK_2: \"#ff79c6\",\n    SECRET_RANK_3: \"#388bfd\",\n    SECRET_RANK_TEXT: \"#ff79c6\",\n    NEXT_RANK_BAR: \"#ff79c6\",\n    S_RANK_BASE: \"#ffb86c\",\n    S_RANK_SHADOW: \"#ffb86c\",\n    S_RANK_TEXT: \"#0d1117\",\n    A_RANK_BASE: \"#8be9fd\",\n    A_RANK_SHADOW: \"#8be9fd\",\n    A_RANK_TEXT: \"#0d1117\",\n    B_RANK_BASE: \"#ff5555\",\n    B_RANK_SHADOW: \"#ff5555\",\n    B_RANK_TEXT: \"#0d1117\",\n    DEFAULT_RANK_BASE: \"#6272a4\",\n    DEFAULT_RANK_SHADOW: \"#6272a4\",\n    DEFAULT_RANK_TEXT: \"#0d1117\",\n  },\n  juicyfresh: {\n    BACKGROUND: \"#0d0c15\",\n    TITLE: \"#f7d745\",\n    ICON_CIRCLE: \"#FFF\",\n    TEXT: \"#b2d76c\",\n    LAUREL: \"#8bb071\",\n    SECRET_RANK_1: \"#a8d937\",\n    SECRET_RANK_2: \"#f7e662\",\n    SECRET_RANK_3: \"#4d9b1c\",\n    SECRET_RANK_TEXT: \"#ff5700\",\n    NEXT_RANK_BAR: \"#6562af\",\n    S_RANK_BASE: \"#f7d644\",\n    S_RANK_SHADOW: \"#f69e44\",\n    S_RANK_TEXT: \"#ff5700\",\n    A_RANK_BASE: \"#f69e44\",\n    A_RANK_SHADOW: \"#f46d5a\",\n    A_RANK_TEXT: \"#ff5700\",\n    B_RANK_BASE: \"#f46d5a\",\n    B_RANK_SHADOW: \"#f73155\",\n    B_RANK_TEXT: \"#ff5700\",\n    DEFAULT_RANK_BASE: \"#f0d7d6\",\n    DEFAULT_RANK_SHADOW: \"#f58867\",\n    DEFAULT_RANK_TEXT: \"#ff5700\",\n  },\n  oldie: {\n    BACKGROUND: \"#F0F0F0\",\n    TITLE: \"#111\",\n    ICON_CIRCLE: \"#FFF\",\n    TEXT: \"#666\",\n    LAUREL: \"#535353\",\n    SECRET_RANK_1: \"#738986\",\n    SECRET_RANK_2: \"#B36154\",\n    SECRET_RANK_3: \"#91A16A\",\n    SECRET_RANK_TEXT: \"#4D4D4D\",\n    NEXT_RANK_BAR: \"#8E8680\",\n    S_RANK_BASE: \"#8E8E8E\",\n    S_RANK_SHADOW: \"#8E8E8E\",\n    S_RANK_TEXT: \"#4D4D4D\",\n    A_RANK_BASE: \"#AFAFAF\",\n    A_RANK_SHADOW: \"#AFAFAF\",\n    A_RANK_TEXT: \"#4D4D4D\",\n    B_RANK_BASE: \"#858585\",\n    B_RANK_SHADOW: \"#858585\",\n    B_RANK_TEXT: \"#4D4D4D\",\n    DEFAULT_RANK_BASE: \"#535353\",\n    DEFAULT_RANK_SHADOW: \"#535353\",\n    DEFAULT_RANK_TEXT: \"#4D4D4D\",\n  },\n  buddhism: {\n    BACKGROUND: \"#ffc20e\",\n    TITLE: \"#FFF\",\n    ICON_CIRCLE: \"#FFF\",\n    TEXT: \"#FFF\",\n    LAUREL: \"#27c5ff\",\n    SECRET_RANK_1: \"#FFF\",\n    SECRET_RANK_2: \"#f73155\",\n    SECRET_RANK_3: \"#fff\",\n    SECRET_RANK_TEXT: \"#f73155\",\n    NEXT_RANK_BAR: \"#f73155\",\n    S_RANK_BASE: \"#ff8400\",\n    S_RANK_SHADOW: \"#ff8400\",\n    S_RANK_TEXT: \"#ffc20e\",\n    A_RANK_BASE: \"#fff\",\n    A_RANK_SHADOW: \"#fff\",\n    A_RANK_TEXT: \"#ffc20e\",\n    B_RANK_BASE: \"#f73155\",\n    B_RANK_SHADOW: \"#f73155\",\n    B_RANK_TEXT: \"#ffc20e\",\n    DEFAULT_RANK_BASE: \"#27c5ff\",\n    DEFAULT_RANK_SHADOW: \"#27c5ff\",\n    DEFAULT_RANK_TEXT: \"#ffc20e\",\n  },\n  radical: {\n    BACKGROUND: \"#141321\",\n    ICON_CIRCLE: \"#EEEEEE\",\n    TITLE: \"#fe428e\",\n    TEXT: \"#a9fef7\",\n    LAUREL: \"#50fa7b\",\n    SECRET_RANK_1: \"#ff5555\",\n    SECRET_RANK_2: \"#ff15d9\",\n    SECRET_RANK_3: \"#1E65F5\",\n    SECRET_RANK_TEXT: \"#ff61c6\",\n    NEXT_RANK_BAR: \"#fe428e\",\n    S_RANK_BASE: \"#ffce32\",\n    S_RANK_SHADOW: \"#ffce32\",\n    S_RANK_TEXT: \"#CB8A30\",\n    A_RANK_BASE: \"#8DF7B5\",\n    A_RANK_SHADOW: \"#8DF7B5\",\n    A_RANK_TEXT: \"#3A3A3A\",\n    B_RANK_BASE: \"#EA3F25\",\n    B_RANK_SHADOW: \"#EA3F25\",\n    B_RANK_TEXT: \"#3A3A3A\",\n    DEFAULT_RANK_BASE: \"#1E65F5\",\n    DEFAULT_RANK_SHADOW: \"#1E65F5\",\n    DEFAULT_RANK_TEXT: \"#3A3A3A\",\n  },\n  onestar: {\n    BACKGROUND: \"#0d1117\",\n    ICON_CIRCLE: \"#EEEEEE\",\n    TITLE: \"#EEEEEE\",\n    TEXT: \"#c7c7c7\",\n    LAUREL: \"#0dbc79\",\n    SECRET_RANK_1: \"#ff5555\",\n    SECRET_RANK_2: \"#d861d8\",\n    SECRET_RANK_3: \"#3b8eea\",\n    SECRET_RANK_TEXT: \"#ff61c6\",\n    NEXT_RANK_BAR: \"#9e9e9e\",\n    S_RANK_BASE: \"#FFD54F\",\n    S_RANK_SHADOW: \"#FFE082\",\n    S_RANK_TEXT: \"#CB8A30\",\n    A_RANK_BASE: \"#23d18b\",\n    A_RANK_SHADOW: \"#8DF7B5\",\n    A_RANK_TEXT: \"#3A3A3A\",\n    B_RANK_BASE: \"#d13b3b\",\n    B_RANK_SHADOW: \"#fa4b4b\",\n    B_RANK_TEXT: \"#3A3A3A\",\n    DEFAULT_RANK_BASE: \"#2472c8\",\n    DEFAULT_RANK_SHADOW: \"#3b8eea\",\n    DEFAULT_RANK_TEXT: \"#3A3A3A\",\n  },\n  algolia: {\n    BACKGROUND: \"#050f2c\",\n    TITLE: \"#00aeff\",\n    ICON_CIRCLE: \"#f0f6fb\",\n    TEXT: \"#7eace9\",\n    LAUREL: \"#178600\",\n    SECRET_RANK_1: \"#ff5555\",\n    SECRET_RANK_2: \"#ff79c6\",\n    SECRET_RANK_3: \"#388bfd\",\n    SECRET_RANK_TEXT: \"#ff79c6\",\n    NEXT_RANK_BAR: \"#00aeff\",\n    S_RANK_BASE: \"#ffb86c\",\n    S_RANK_SHADOW: \"#ffb86c\",\n    S_RANK_TEXT: \"#0d1117\",\n    A_RANK_BASE: \"#2dde98\",\n    A_RANK_TEXT: \"#0d1117\",\n    A_RANK_SHADOW: \"#2dde98\",\n    B_RANK_BASE: \"#8be9fd\",\n    B_RANK_SHADOW: \"#8be9fd\",\n    B_RANK_TEXT: \"#0d1117\",\n    DEFAULT_RANK_BASE: \"#5c75c3\",\n    DEFAULT_RANK_SHADOW: \"#6272a4\",\n    DEFAULT_RANK_TEXT: \"#0d1117\",\n  },\n  gitdimmed: {\n    BACKGROUND: \"#333\",\n    TITLE: \"#f0f6fb\",\n    ICON_CIRCLE: \"#f0f6fb\",\n    TEXT: \"#FFF\",\n    LAUREL: \"#178600\",\n    SECRET_RANK_1: \"#ff5555\",\n    SECRET_RANK_2: \"#ff79c6\",\n    SECRET_RANK_3: \"#388bfd\",\n    SECRET_RANK_TEXT: \"#ff79c6\",\n    NEXT_RANK_BAR: \"#00aeff\",\n    S_RANK_BASE: \"#ffb86c\",\n    S_RANK_SHADOW: \"#ffb86c\",\n    S_RANK_TEXT: \"#0d1117\",\n    A_RANK_BASE: \"#2dde98\",\n    A_RANK_TEXT: \"#0d1117\",\n    A_RANK_SHADOW: \"#2dde98\",\n    B_RANK_BASE: \"#8be9fd\",\n    B_RANK_SHADOW: \"#8be9fd\",\n    B_RANK_TEXT: \"#0d1117\",\n    DEFAULT_RANK_BASE: \"#5c75c3\",\n    DEFAULT_RANK_SHADOW: \"#6272a4\",\n    DEFAULT_RANK_TEXT: \"#0d1117\",\n  },\n  tokyonight: {\n    BACKGROUND: \"#1a1b27\",\n    TITLE: \"#70a5fd\",\n    ICON_CIRCLE: \"#bf91f3\",\n    TEXT: \"#38bdae\",\n    LAUREL: \"#178600\",\n    SECRET_RANK_1: \"#ff5555\",\n    SECRET_RANK_2: \"#ff79c6\",\n    SECRET_RANK_3: \"#388bfd\",\n    SECRET_RANK_TEXT: \"#ff79c6\",\n    NEXT_RANK_BAR: \"#00aeff\",\n    S_RANK_BASE: \"#ffb86c\",\n    S_RANK_SHADOW: \"#ffb86c\",\n    S_RANK_TEXT: \"#0d1117\",\n    A_RANK_BASE: \"#2dde98\",\n    A_RANK_TEXT: \"#0d1117\",\n    A_RANK_SHADOW: \"#2dde98\",\n    B_RANK_BASE: \"#8be9fd\",\n    B_RANK_SHADOW: \"#8be9fd\",\n    B_RANK_TEXT: \"#0d1117\",\n    DEFAULT_RANK_BASE: \"#5c75c3\",\n    DEFAULT_RANK_SHADOW: \"#6272a4\",\n    DEFAULT_RANK_TEXT: \"#0d1117\",\n  },\n  matrix: {\n    BACKGROUND: \"#000000\",\n    TITLE: \"#00cc00\",\n    ICON_CIRCLE: \"#002200\",\n    TEXT: \"#00cc00\",\n    LAUREL: \"#178600\",\n    SECRET_RANK_1: \"#ffd700\",\n    SECRET_RANK_2: \"#ffffff\",\n    SECRET_RANK_3: \"#ffd700\",\n    SECRET_RANK_TEXT: \"#00ff00\",\n    NEXT_RANK_BAR: \"#00ff00\",\n    S_RANK_BASE: \"#ffd700\",\n    S_RANK_SHADOW: \"#ffd700\",\n    S_RANK_TEXT: \"#00ff00\",\n    A_RANK_BASE: \"#c0c0c0\",\n    A_RANK_TEXT: \"#00ff00\",\n    A_RANK_SHADOW: \"#c0c0c0\",\n    B_RANK_BASE: \"#b08d57\",\n    B_RANK_SHADOW: \"#b08d57\",\n    B_RANK_TEXT: \"#00ff00\",\n    DEFAULT_RANK_BASE: \"#b08d57\",\n    DEFAULT_RANK_SHADOW: \"#b08d57\",\n    DEFAULT_RANK_TEXT: \"#00ff00\",\n  },\n  apprentice: {\n    BACKGROUND: \"#262626\",\n    TITLE: \"#BCBCBC\",\n    ICON_CIRCLE: \"#BCBCBC\",\n    TEXT: \"#5F875F\",\n    LAUREL: \"#5F8787\",\n    SECRET_RANK_1: \"#FF8700\",\n    SECRET_RANK_2: \"#8787AF\",\n    SECRET_RANK_3: \"#5F87AF\",\n    SECRET_RANK_TEXT: \"#5F5F87\",\n    NEXT_RANK_BAR: \"#FFFFA9\",\n    S_RANK_BASE: \"#FFFFAF\",\n    S_RANK_SHADOW: \"#FFFFAF\",\n    S_RANK_TEXT: \"#87875F\",\n    A_RANK_BASE: \"#8FAFD7\",\n    A_RANK_SHADOW: \"#8FAFD7\",\n    A_RANK_TEXT: \"#5F875F\",\n    B_RANK_BASE: \"#AF5F5F\",\n    B_RANK_SHADOW: \"#AF5F5F\",\n    B_RANK_TEXT: \"#AF5F5F\",\n    DEFAULT_RANK_BASE: \"#6C6C6C\",\n    DEFAULT_RANK_SHADOW: \"#6C6C6C\",\n    DEFAULT_RANK_TEXT: \"#1C1C1C\",\n  },\n  dark_dimmed: {\n    BACKGROUND: \"#22272e\",\n    TITLE: \"#adbac7\",\n    ICON_CIRCLE: \"#002200\",\n    TEXT: \"#adbac7\",\n    LAUREL: \"#178600\",\n    SECRET_RANK_1: \"red\",\n    SECRET_RANK_2: \"fuchsia\",\n    SECRET_RANK_3: \"blue\",\n    SECRET_RANK_TEXT: \"fuchsia\",\n    NEXT_RANK_BAR: \"#0366d6\",\n    S_RANK_BASE: \"#FAD200\",\n    S_RANK_SHADOW: \"#C8A090\",\n    S_RANK_TEXT: \"#886000\",\n    A_RANK_BASE: \"#B0B0B0\",\n    A_RANK_SHADOW: \"#9090C0\",\n    A_RANK_TEXT: \"#505050\",\n    B_RANK_BASE: \"#A18D66\",\n    B_RANK_SHADOW: \"#816D96\",\n    B_RANK_TEXT: \"#412D06\",\n    DEFAULT_RANK_BASE: \"#777\",\n    DEFAULT_RANK_SHADOW: \"#333\",\n    DEFAULT_RANK_TEXT: \"#333\",\n  },\n  dark_lover: {\n    BACKGROUND: \"#0d0d0d\",\n    TITLE: \"#e8aa64\",\n    ICON_CIRCLE: \"white\",\n    TEXT: \"#e8aa64\",\n    LAUREL: \"#e86464\",\n    SECRET_RANK_1: \"#e05555\",\n    SECRET_RANK_2: \"#e05555\",\n    SECRET_RANK_3: \"#e05555\",\n    SECRET_RANK_TEXT: \"#e05555\",\n    NEXT_RANK_BAR: \"#e05555\",\n    S_RANK_BASE: \"#f2c635\",\n    S_RANK_SHADOW: \"#e0d7b8\",\n    S_RANK_TEXT: \"#b35707\",\n    A_RANK_BASE: \"#f25755\",\n    A_RANK_SHADOW: \"#e69493\",\n    A_RANK_TEXT: \"#f5352f\",\n    B_RANK_BASE: \"#63db93\",\n    B_RANK_SHADOW: \"#8cd1a8\",\n    B_RANK_TEXT: \"#07b84e\",\n    DEFAULT_RANK_BASE: \"#7f6ceb\",\n    DEFAULT_RANK_SHADOW: \"#a598ed\",\n    DEFAULT_RANK_TEXT: \"#7f6ceb\",\n  },\n  kimbie_dark: {\n    BACKGROUND: \"#221a0f\",\n    TITLE: \"#d3af86\",\n    ICON_CIRCLE: \"#7e602c\",\n    TEXT: \"#d3af86\",\n    LAUREL: \"#889b4a\",\n    SECRET_RANK_1: \"#f14a68\",\n    SECRET_RANK_2: \"#f14a68\",\n    SECRET_RANK_3: \"#dc3958\",\n    SECRET_RANK_TEXT: \"#dc3958\",\n    NEXT_RANK_BAR: \"#dc3958\",\n    S_RANK_BASE: \"#fcac51\",\n    S_RANK_SHADOW: \"#f79a32\",\n    S_RANK_TEXT: \"#d3af86\",\n    A_RANK_BASE: \"#a3B95a\",\n    A_RANK_SHADOW: \"#889b4a\",\n    A_RANK_TEXT: \"#d3af86\",\n    B_RANK_BASE: \"#4c96a8\",\n    B_RANK_SHADOW: \"#418292\",\n    B_RANK_TEXT: \"#d3af86\",\n    DEFAULT_RANK_BASE: \"#8ab1b0\",\n    DEFAULT_RANK_SHADOW: \"#719190\",\n    DEFAULT_RANK_TEXT: \"#d3af86\",\n  },\n  aura: {\n    BACKGROUND: \"#1E1D26\",\n    TITLE: \"#FFFFFF\",\n    ICON_CIRCLE: \"#FFFFFF\",\n    TEXT: \"#dbffe6\",\n    LAUREL: \"#a9fcca\",\n    SECRET_RANK_1: \"#c273ff\",\n    SECRET_RANK_2: \"#c273ff\",\n    SECRET_RANK_3: \"#c273ff\",\n    SECRET_RANK_TEXT: \"#bd93f9\",\n    NEXT_RANK_BAR: \"#715df5\",\n    S_RANK_BASE: \"#8e57ff\",\n    S_RANK_SHADOW: \"#2361ad\",\n    S_RANK_TEXT: \"#6272a4\",\n    A_RANK_BASE: \"#7c71f5\",\n    A_RANK_SHADOW: \"#3ae056\",\n    A_RANK_TEXT: \"#6272a4\",\n    B_RANK_BASE: \"#226a80\",\n    B_RANK_SHADOW: \"#226a80\",\n    B_RANK_TEXT: \"#6272a4\",\n    DEFAULT_RANK_BASE: \"#5e8c2a\",\n    DEFAULT_RANK_SHADOW: \"#5e8c2a\",\n    DEFAULT_RANK_TEXT: \"#5e8c2a\",\n  },\n};\n\nexport interface Theme {\n  BACKGROUND: string;\n  TITLE: string;\n  ICON_CIRCLE: string;\n  TEXT: string;\n  LAUREL: string;\n  SECRET_RANK_1: string;\n  SECRET_RANK_2: string;\n  SECRET_RANK_3: string;\n  SECRET_RANK_TEXT: string;\n  NEXT_RANK_BAR: string;\n  S_RANK_BASE: string;\n  S_RANK_SHADOW: string;\n  S_RANK_TEXT: string;\n  A_RANK_BASE: string;\n  A_RANK_SHADOW: string;\n  A_RANK_TEXT: string;\n  B_RANK_BASE: string;\n  B_RANK_SHADOW: string;\n  B_RANK_TEXT: string;\n  DEFAULT_RANK_BASE: string;\n  DEFAULT_RANK_SHADOW: string;\n  DEFAULT_RANK_TEXT: string;\n}\n"
  },
  {
    "path": "src/trophy.ts",
    "content": "import { getNextRankBar, getTrophyIcon } from \"./icons.ts\";\nimport { abridgeScore, CONSTANTS, RANK, RANK_ORDER } from \"./utils.ts\";\nimport { Theme } from \"./theme.ts\";\n\nclass RankCondition {\n  constructor(\n    readonly rank: RANK,\n    readonly message: string,\n    readonly requiredScore: number,\n  ) {}\n}\n\nexport class Trophy {\n  rankCondition: RankCondition | null = null;\n  rank: RANK = RANK.UNKNOWN;\n  topMessage = \"Unknown\";\n  bottomMessage = \"0\";\n  title = \"\";\n  filterTitles: Array<string> = [];\n  hidden = false;\n  constructor(\n    private score: number,\n    private rankConditions: Array<RankCondition>,\n  ) {\n    this.bottomMessage = abridgeScore(score);\n    this.setRank();\n  }\n  setRank() {\n    const sortedRankConditions = this.rankConditions.toSorted((a, b) =>\n      RANK_ORDER.indexOf(a.rank) - RANK_ORDER.indexOf(b.rank)\n    );\n    // Set the rank that hit the first condition\n    const rankCondition = sortedRankConditions.find((r) =>\n      this.score >= r.requiredScore\n    );\n    if (rankCondition != null) {\n      this.rank = rankCondition.rank;\n      this.rankCondition = rankCondition;\n      this.topMessage = rankCondition.message;\n    }\n  }\n  private calculateNextRankPercentage() {\n    if (this.rank === RANK.UNKNOWN) {\n      return 0;\n    }\n    const nextRankIndex = RANK_ORDER.indexOf(this.rank) - 1;\n    // When got the max rank\n    if (nextRankIndex < 0 || this.rank === RANK.SSS) {\n      return 1;\n    }\n    const nextRank = RANK_ORDER[nextRankIndex];\n    const nextRankCondition = this.rankConditions.find((r) =>\n      r.rank == nextRank\n    );\n    const distance = nextRankCondition!.requiredScore -\n      this.rankCondition!.requiredScore;\n    const progress = this.score - this.rankCondition!.requiredScore;\n    const result = progress / distance;\n    return result;\n  }\n  render(\n    theme: Theme,\n    x = 0,\n    y = 0,\n    panelSize = CONSTANTS.DEFAULT_PANEL_SIZE,\n    noBackground = CONSTANTS.DEFAULT_NO_BACKGROUND,\n    noFrame = CONSTANTS.DEFAULT_NO_FRAME,\n  ): string {\n    const { BACKGROUND: PRIMARY, TITLE: SECONDARY, TEXT, NEXT_RANK_BAR } =\n      theme;\n    const nextRankBar = getNextRankBar(\n      this.title,\n      this.calculateNextRankPercentage(),\n      NEXT_RANK_BAR,\n    );\n    return `\n        <svg\n          x=\"${x}\"\n          y=\"${y}\"\n          width=\"${panelSize}\"\n          height=\"${panelSize}\"\n          viewBox=\"0 0 ${panelSize} ${panelSize}\"\n          fill=\"none\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n        >\n          <rect\n            x=\"0.5\"\n            y=\"0.5\"\n            rx=\"4.5\"\n            width=\"${panelSize - 1}\"\n            height=\"${panelSize - 1}\"\n            stroke=\"#e1e4e8\"\n            fill=\"${PRIMARY}\"\n            stroke-opacity=\"${noFrame ? \"0\" : \"1\"}\"\n            fill-opacity=\"${noBackground ? \"0\" : \"1\"}\"\n          />\n          ${getTrophyIcon(theme, this.rank)}\n          <text x=\"50%\" y=\"18\" text-anchor=\"middle\" font-family=\"Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji\" font-weight=\"bold\" font-size=\"13\" fill=\"${SECONDARY}\">${this.title}</text>\n          <text x=\"50%\" y=\"85\" text-anchor=\"middle\" font-family=\"Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji\" font-weight=\"bold\" font-size=\"10.5\" fill=\"${TEXT}\">${this.topMessage}</text>\n          <text x=\"50%\" y=\"97\" text-anchor=\"middle\" font-family=\"Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji\" font-weight=\"bold\" font-size=\"10\" fill=\"${TEXT}\">${this.bottomMessage}</text>\n          ${nextRankBar}\n        </svg>\n        `;\n  }\n}\n\nexport class MultipleLangTrophy extends Trophy {\n  constructor(score: number) {\n    const rankConditions = [\n      new RankCondition(\n        RANK.SECRET,\n        \"Rainbow Lang User\",\n        10,\n      ),\n    ];\n    super(score, rankConditions);\n    this.title = \"MultiLanguage\";\n    this.filterTitles = [\"MultipleLang\", \"MultiLanguage\"];\n    this.hidden = true;\n  }\n}\n\nexport class AllSuperRankTrophy extends Trophy {\n  constructor(score: number) {\n    const rankConditions = [\n      new RankCondition(\n        RANK.SECRET,\n        \"S Rank Hacker\",\n        1,\n      ),\n    ];\n    super(score, rankConditions);\n    this.title = \"AllSuperRank\";\n    this.filterTitles = [\"AllSuperRank\"];\n    this.bottomMessage = \"All S Rank\";\n    this.hidden = true;\n  }\n}\nexport class Joined2020Trophy extends Trophy {\n  constructor(score: number) {\n    const rankConditions = [\n      new RankCondition(\n        RANK.SECRET,\n        \"Everything started...\",\n        1,\n      ),\n    ];\n    super(score, rankConditions);\n    this.title = \"Joined2020\";\n    this.filterTitles = [\"Joined2020\"];\n    this.bottomMessage = \"Joined 2020\";\n    this.hidden = true;\n  }\n}\nexport class AncientAccountTrophy extends Trophy {\n  constructor(score: number) {\n    const rankConditions = [\n      new RankCondition(\n        RANK.SECRET,\n        \"Ancient User\",\n        1,\n      ),\n    ];\n    super(score, rankConditions);\n    this.title = \"AncientUser\";\n    this.filterTitles = [\"AncientUser\"];\n    this.bottomMessage = \"Before 2010\";\n    this.hidden = true;\n  }\n}\nexport class LongTimeAccountTrophy extends Trophy {\n  constructor(score: number) {\n    const rankConditions = [\n      new RankCondition(\n        RANK.SECRET,\n        \"Village Elder\",\n        10,\n      ),\n    ];\n    super(score, rankConditions);\n    this.title = \"LongTimeUser\";\n    this.filterTitles = [\"LongTimeUser\"];\n    this.hidden = true;\n  }\n}\nexport class MultipleOrganizationsTrophy extends Trophy {\n  constructor(score: number) {\n    const rankConditions = [\n      new RankCondition(\n        RANK.SECRET,\n        // or if this doesn't render well: \"Factorum\"\n        \"Jack of all Trades\",\n        3,\n      ),\n    ];\n    super(score, rankConditions);\n    this.title = \"Organizations\";\n    this.filterTitles = [\"Organizations\", \"Orgs\", \"Teams\"];\n    this.hidden = true;\n  }\n}\n\nexport class OGAccountTrophy extends Trophy {\n  constructor(score: number) {\n    const rankConditions = [\n      new RankCondition(\n        RANK.SECRET,\n        \"OG User\",\n        1,\n      ),\n    ];\n    super(score, rankConditions);\n    this.title = \"OGUser\";\n    this.filterTitles = [\"OGUser\"];\n    this.bottomMessage = \"Joined 2008\";\n    this.hidden = true;\n  }\n}\n\nexport class TotalReviewsTrophy extends Trophy {\n  constructor(score: number) {\n    const rankConditions = [\n      new RankCondition(\n        RANK.SSS,\n        \"God Reviewer\",\n        70,\n      ),\n      new RankCondition(\n        RANK.SS,\n        \"Deep Reviewer\",\n        57,\n      ),\n      new RankCondition(\n        RANK.S,\n        \"Super Reviewer\",\n        45,\n      ),\n      new RankCondition(\n        RANK.AAA,\n        \"Ultra Reviewer\",\n        30,\n      ),\n      new RankCondition(\n        RANK.AA,\n        \"Hyper Reviewer\",\n        20,\n      ),\n      new RankCondition(\n        RANK.A,\n        \"Active Reviewer\",\n        8,\n      ),\n      new RankCondition(\n        RANK.B,\n        \"Intermediate Reviewer\",\n        3,\n      ),\n      new RankCondition(\n        RANK.C,\n        \"New Reviewer\",\n        1,\n      ),\n    ];\n    super(score, rankConditions);\n    this.title = \"Reviews\";\n    this.filterTitles = [\"Review\", \"Reviews\"];\n  }\n}\n\nexport class AccountDurationTrophy extends Trophy {\n  constructor(score: number) {\n    const rankConditions = [\n      new RankCondition(\n        RANK.SSS,\n        \"Seasoned Veteran\",\n        70, // 20 years\n      ),\n      new RankCondition(\n        RANK.SS,\n        \"Grandmaster\",\n        55, // 15 years\n      ),\n      new RankCondition(\n        RANK.S,\n        \"Master Dev\",\n        40, // 10 years\n      ),\n      new RankCondition(\n        RANK.AAA,\n        \"Expert Dev\",\n        28, // 7.5 years\n      ),\n      new RankCondition(\n        RANK.AA,\n        \"Experienced Dev\",\n        18, // 5 years\n      ),\n      new RankCondition(\n        RANK.A,\n        \"Intermediate Dev\",\n        11, // 3 years\n      ),\n      new RankCondition(\n        RANK.B,\n        \"Junior Dev\",\n        6, // 1.5 years\n      ),\n      new RankCondition(\n        RANK.C,\n        \"Newbie\",\n        2, // 0.5 year\n      ),\n    ];\n    super(score, rankConditions);\n    this.title = \"Experience\";\n    this.filterTitles = [\"Experience\", \"Duration\", \"Since\"];\n  }\n}\n\nexport class TotalStarTrophy extends Trophy {\n  constructor(score: number) {\n    const rankConditions = [\n      new RankCondition(\n        RANK.SSS,\n        \"Super Stargazer\",\n        2000,\n      ),\n      new RankCondition(\n        RANK.SS,\n        \"High Stargazer\",\n        700,\n      ),\n      new RankCondition(\n        RANK.S,\n        \"Stargazer\",\n        200,\n      ),\n      new RankCondition(\n        RANK.AAA,\n        \"Super Star\",\n        100,\n      ),\n      new RankCondition(\n        RANK.AA,\n        \"High Star\",\n        50,\n      ),\n      new RankCondition(\n        RANK.A,\n        \"You are a Star\",\n        30,\n      ),\n      new RankCondition(\n        RANK.B,\n        \"Middle Star\",\n        10,\n      ),\n      new RankCondition(\n        RANK.C,\n        \"First Star\",\n        1,\n      ),\n    ];\n    super(score, rankConditions);\n    this.title = \"Stars\";\n    this.filterTitles = [\"Star\", \"Stars\"];\n  }\n}\n\nexport class TotalCommitTrophy extends Trophy {\n  constructor(score: number) {\n    const rankConditions = [\n      new RankCondition(\n        RANK.SSS,\n        \"God Committer\",\n        4000,\n      ),\n      new RankCondition(\n        RANK.SS,\n        \"Deep Committer\",\n        2000,\n      ),\n      new RankCondition(\n        RANK.S,\n        \"Super Committer\",\n        1000,\n      ),\n      new RankCondition(\n        RANK.AAA,\n        \"Ultra Committer\",\n        500,\n      ),\n      new RankCondition(\n        RANK.AA,\n        \"Hyper Committer\",\n        200,\n      ),\n      new RankCondition(\n        RANK.A,\n        \"High Committer\",\n        100,\n      ),\n      new RankCondition(\n        RANK.B,\n        \"Middle Committer\",\n        10,\n      ),\n      new RankCondition(\n        RANK.C,\n        \"First Commit\",\n        1,\n      ),\n    ];\n    super(score, rankConditions);\n    this.title = \"Commits\";\n    this.filterTitles = [\"Commit\", \"Commits\"];\n  }\n}\n\nexport class TotalFollowerTrophy extends Trophy {\n  constructor(score: number) {\n    const rankConditions = [\n      new RankCondition(\n        RANK.SSS,\n        \"Super Celebrity\",\n        1000,\n      ),\n      new RankCondition(\n        RANK.SS,\n        \"Ultra Celebrity\",\n        400,\n      ),\n      new RankCondition(\n        RANK.S,\n        \"Hyper Celebrity\",\n        200,\n      ),\n      new RankCondition(\n        RANK.AAA,\n        \"Famous User\",\n        100,\n      ),\n      new RankCondition(\n        RANK.AA,\n        \"Active User\",\n        50,\n      ),\n      new RankCondition(\n        RANK.A,\n        \"Dynamic User\",\n        20,\n      ),\n      new RankCondition(\n        RANK.B,\n        \"Many Friends\",\n        10,\n      ),\n      new RankCondition(\n        RANK.C,\n        \"First Friend\",\n        1,\n      ),\n    ];\n    super(score, rankConditions);\n    this.title = \"Followers\";\n    this.filterTitles = [\"Follower\", \"Followers\"];\n  }\n}\n\nexport class TotalIssueTrophy extends Trophy {\n  constructor(score: number) {\n    const rankConditions = [\n      new RankCondition(\n        RANK.SSS,\n        \"God Issuer\",\n        1000,\n      ),\n      new RankCondition(\n        RANK.SS,\n        \"Deep Issuer\",\n        500,\n      ),\n      new RankCondition(\n        RANK.S,\n        \"Super Issuer\",\n        200,\n      ),\n      new RankCondition(\n        RANK.AAA,\n        \"Ultra Issuer\",\n        100,\n      ),\n      new RankCondition(\n        RANK.AA,\n        \"Hyper Issuer\",\n        50,\n      ),\n      new RankCondition(\n        RANK.A,\n        \"High Issuer\",\n        20,\n      ),\n      new RankCondition(\n        RANK.B,\n        \"Middle Issuer\",\n        10,\n      ),\n      new RankCondition(\n        RANK.C,\n        \"First Issue\",\n        1,\n      ),\n    ];\n    super(score, rankConditions);\n    this.title = \"Issues\";\n    this.filterTitles = [\"Issue\", \"Issues\"];\n  }\n}\n\nexport class TotalPullRequestTrophy extends Trophy {\n  constructor(score: number) {\n    const rankConditions = [\n      new RankCondition(\n        RANK.SSS,\n        \"God Puller\",\n        1000,\n      ),\n      new RankCondition(\n        RANK.SS,\n        \"Deep Puller\",\n        500,\n      ),\n      new RankCondition(\n        RANK.S,\n        \"Super Puller\",\n        200,\n      ),\n      new RankCondition(\n        RANK.AAA,\n        \"Ultra Puller\",\n        100,\n      ),\n      new RankCondition(\n        RANK.AA,\n        \"Hyper Puller\",\n        50,\n      ),\n      new RankCondition(\n        RANK.A,\n        \"High Puller\",\n        20,\n      ),\n      new RankCondition(\n        RANK.B,\n        \"Middle Puller\",\n        10,\n      ),\n      new RankCondition(\n        RANK.C,\n        \"First Pull\",\n        1,\n      ),\n    ];\n    super(score, rankConditions);\n    this.title = \"PullRequest\";\n    this.filterTitles = [\"PR\", \"PullRequest\", \"Pulls\", \"Puller\"];\n  }\n}\n\nexport class TotalRepositoryTrophy extends Trophy {\n  constructor(score: number) {\n    const rankConditions = [\n      new RankCondition(\n        RANK.SSS,\n        \"God Repo Creator\",\n        50,\n      ),\n      new RankCondition(\n        RANK.SS,\n        \"Deep Repo Creator\",\n        45,\n      ),\n      new RankCondition(\n        RANK.S,\n        \"Super Repo Creator\",\n        40,\n      ),\n      new RankCondition(\n        RANK.AAA,\n        \"Ultra Repo Creator\",\n        35,\n      ),\n      new RankCondition(\n        RANK.AA,\n        \"Hyper Repo Creator\",\n        30,\n      ),\n      new RankCondition(\n        RANK.A,\n        \"High Repo Creator\",\n        20,\n      ),\n      new RankCondition(\n        RANK.B,\n        \"Middle Repo Creator\",\n        10,\n      ),\n      new RankCondition(\n        RANK.C,\n        \"First Repository\",\n        1,\n      ),\n    ];\n    super(score, rankConditions);\n    this.title = \"Repositories\";\n    this.filterTitles = [\"Repo\", \"Repository\", \"Repositories\"];\n  }\n}\n"
  },
  {
    "path": "src/trophy_list.ts",
    "content": "import {\n  AccountDurationTrophy,\n  AllSuperRankTrophy,\n  AncientAccountTrophy,\n  Joined2020Trophy,\n  LongTimeAccountTrophy,\n  MultipleLangTrophy,\n  MultipleOrganizationsTrophy,\n  OGAccountTrophy,\n  TotalCommitTrophy,\n  TotalFollowerTrophy,\n  TotalIssueTrophy,\n  TotalPullRequestTrophy,\n  TotalRepositoryTrophy,\n  TotalReviewsTrophy,\n  TotalStarTrophy,\n  Trophy,\n} from \"./trophy.ts\";\nimport { UserInfo } from \"./user_info.ts\";\nimport { RANK, RANK_ORDER } from \"./utils.ts\";\n\nexport class TrophyList {\n  private trophies = new Array<Trophy>();\n  constructor(userInfo: UserInfo) {\n    // Base trophies\n    this.trophies.push(\n      new TotalStarTrophy(userInfo.totalStargazers),\n      new TotalCommitTrophy(userInfo.totalCommits),\n      new TotalFollowerTrophy(userInfo.totalFollowers),\n      new TotalIssueTrophy(userInfo.totalIssues),\n      new TotalPullRequestTrophy(userInfo.totalPullRequests),\n      new TotalRepositoryTrophy(userInfo.totalRepositories),\n      new TotalReviewsTrophy(userInfo.totalReviews),\n    );\n    // Secret trophies\n    this.trophies.push(\n      new AllSuperRankTrophy(this.isAllSRank),\n      new MultipleLangTrophy(userInfo.languageCount),\n      new LongTimeAccountTrophy(userInfo.durationYear),\n      new AncientAccountTrophy(userInfo.ancientAccount),\n      new OGAccountTrophy(userInfo.ogAccount),\n      new Joined2020Trophy(userInfo.joined2020),\n      new MultipleOrganizationsTrophy(userInfo.totalOrganizations),\n      new AccountDurationTrophy(userInfo.durationDays),\n    );\n  }\n  get length() {\n    return this.trophies.length;\n  }\n  get getArray() {\n    return this.trophies;\n  }\n  private get isAllSRank() {\n    return this.trophies.every((trophy) => trophy.rank.slice(0, 1) == RANK.S)\n      ? 1\n      : 0;\n  }\n  filterByHidden() {\n    this.trophies = this.trophies.filter((trophy) =>\n      !trophy.hidden || trophy.rank !== RANK.UNKNOWN\n    );\n  }\n  filterByTitles(titles: Array<string>) {\n    this.trophies = this.trophies.filter((trophy) => {\n      return trophy.filterTitles.some((title) => titles.includes(title));\n    });\n  }\n  filterByRanks(ranks: Array<string>) {\n    if (ranks.filter((rank) => rank.includes(\"-\")).length !== 0) {\n      this.trophies = this.trophies.filter((trophy) =>\n        !ranks.map((rank) => rank.substring(1)).includes(trophy.rank)\n      );\n      return;\n    }\n    this.trophies = this.trophies.filter((trophy) =>\n      ranks.includes(trophy.rank)\n    );\n  }\n  filterByExclusionTitles(titles: Array<string>) {\n    const excludeTitles = titles.filter((title) => title.startsWith(\"-\")).map(\n      (title) => title.substring(1),\n    );\n    if (excludeTitles.length > 0) {\n      this.trophies = this.trophies.filter((trophy) =>\n        !excludeTitles.includes(trophy.title)\n      );\n    }\n  }\n  sortByRank() {\n    this.trophies = this.trophies.toSorted((a: Trophy, b: Trophy) =>\n      RANK_ORDER.indexOf(a.rank) - RANK_ORDER.indexOf(b.rank)\n    );\n  }\n}\n"
  },
  {
    "path": "src/user_info.ts",
    "content": "type Language = { name: string };\ntype Stargazers = { totalCount: number };\ntype Repository = {\n  languages: { nodes: Language[] };\n  stargazers: Stargazers;\n  createdAt: string;\n};\nexport type GitHubUserRepository = {\n  repositories: {\n    totalCount: number;\n    nodes: Repository[];\n  };\n};\n\nexport type GitHubUserIssue = {\n  openIssues: {\n    totalCount: number;\n  };\n  closedIssues: {\n    totalCount: number;\n  };\n};\n\nexport type GitHubUserPullRequest = {\n  pullRequests: {\n    totalCount: number;\n  };\n};\n\nexport type GitHubUserActivity = {\n  createdAt: string;\n  contributionsCollection: {\n    totalCommitContributions: number;\n    restrictedContributionsCount: number;\n    totalPullRequestReviewContributions: number;\n  };\n  organizations: {\n    totalCount: number;\n  };\n  followers: {\n    totalCount: number;\n  };\n};\n\nexport type GitHubUserAll =\n  & GitHubUserActivity\n  & GitHubUserIssue\n  & GitHubUserPullRequest\n  & GitHubUserRepository;\nexport class UserInfo {\n  public readonly totalCommits: number;\n  public readonly totalFollowers: number;\n  public readonly totalIssues: number;\n  public readonly totalOrganizations: number;\n  public readonly totalPullRequests: number;\n  public readonly totalReviews: number;\n  public readonly totalStargazers: number;\n  public readonly totalRepositories: number;\n  public readonly languageCount: number;\n  public readonly durationYear: number;\n  public readonly durationDays: number;\n  public readonly ancientAccount: number;\n  public readonly joined2020: number;\n  public readonly ogAccount: number;\n\n  static fromCombined(data: GitHubUserAll): UserInfo {\n    return new UserInfo(data, data, data, data);\n  }\n\n  constructor(\n    userActivity: GitHubUserActivity,\n    userIssue: GitHubUserIssue,\n    userPullRequest: GitHubUserPullRequest,\n    userRepository: GitHubUserRepository,\n  ) {\n    const totalCommits =\n      userActivity.contributionsCollection.restrictedContributionsCount +\n      userActivity.contributionsCollection.totalCommitContributions;\n    const totalStargazers = userRepository.repositories.nodes.reduce(\n      (prev: number, node: Repository) => {\n        return prev + node.stargazers.totalCount;\n      },\n      0,\n    );\n\n    const languages = new Set<string>();\n    userRepository.repositories.nodes.forEach((node: Repository) => {\n      if (node.languages.nodes != undefined) {\n        node.languages.nodes.forEach((node: Language) => {\n          if (node != undefined) {\n            languages.add(node.name);\n          }\n        });\n      }\n    });\n\n    // Find the earliest repository creation date\n    let earliestRepoDate = userActivity.createdAt; // start with the oldest possible\n\n    earliestRepoDate = userRepository.repositories.nodes.reduce(\n      (earliest, node) => {\n        return new Date(node.createdAt).getTime() < new Date(earliest).getTime()\n          ? node.createdAt\n          : earliest;\n      },\n      earliestRepoDate,\n    );\n\n    const durationTime = new Date().getTime() -\n      new Date(earliestRepoDate).getTime();\n    const durationYear = new Date(durationTime).getUTCFullYear() - 1970;\n    const durationDays = Math.floor(\n      durationTime / (1000 * 60 * 60 * 24) / 100,\n    );\n    const ancientAccount = new Date(earliestRepoDate).getFullYear() <= 2010\n      ? 1\n      : 0;\n    const joined2020 = new Date(earliestRepoDate).getFullYear() == 2020 ? 1 : 0;\n    const ogAccount = new Date(earliestRepoDate).getFullYear() <= 2008 ? 1 : 0;\n\n    this.totalCommits = totalCommits;\n    this.totalFollowers = userActivity.followers.totalCount;\n    this.totalIssues = userIssue.openIssues.totalCount +\n      userIssue.closedIssues.totalCount;\n    this.totalOrganizations = userActivity.organizations.totalCount;\n    this.totalPullRequests = userPullRequest.pullRequests.totalCount;\n    this.totalReviews =\n      userActivity.contributionsCollection.totalPullRequestReviewContributions;\n    this.totalStargazers = totalStargazers;\n    this.totalRepositories = userRepository.repositories.totalCount;\n    this.languageCount = languages.size;\n    this.durationYear = durationYear;\n    this.durationDays = durationDays;\n    this.ancientAccount = ancientAccount;\n    this.joined2020 = joined2020;\n    this.ogAccount = ogAccount;\n  }\n}\n"
  },
  {
    "path": "src/utils.ts",
    "content": "export class CustomURLSearchParams extends URLSearchParams {\n  getStringValue(key: string, defaultValue: string): string {\n    if (super.has(key)) {\n      const param = super.get(key);\n      if (param !== null) {\n        return param.toString();\n      }\n    }\n    return defaultValue.toString();\n  }\n  getNumberValue(key: string, defaultValue: number): number {\n    if (super.has(key)) {\n      const param = super.get(key);\n      if (param !== null) {\n        const parsedValue = parseInt(param);\n        if (isNaN(parsedValue)) {\n          return defaultValue;\n        }\n        return parsedValue;\n      }\n    }\n    return defaultValue;\n  }\n  getBooleanValue(key: string, defaultValue: boolean): boolean {\n    if (super.has(key)) {\n      const param = super.get(key);\n      return param !== null && param.toString() === \"true\";\n    }\n    return defaultValue;\n  }\n}\n\nexport function parseParams(req: Request): CustomURLSearchParams {\n  const splittedURL = req.url.split(\"?\");\n  if (splittedURL.length < 2) {\n    return new CustomURLSearchParams();\n  }\n  return new CustomURLSearchParams(splittedURL[1]);\n}\n\nexport function abridgeScore(score: number): string {\n  if (Math.abs(score) < 1) {\n    return \"0pt\";\n  }\n  if (Math.abs(score) > 999) {\n    return (Math.sign(score) * (Math.abs(score) / 1000)).toFixed(1) + \"kpt\";\n  }\n  return (Math.sign(score) * Math.abs(score)).toString() + \"pt\";\n}\n\nconst HOUR_IN_MILLISECONDS = 60 * 60 * 1000;\n\nexport const CONSTANTS = {\n  CACHE_MAX_AGE: 18800,\n  CDN_CACHE_MAX_AGE: 28800, // 8 hours for CDN edge cache\n  STALE_WHILE_REVALIDATE: 86400, // 24 hours - serve stale while revalidating\n  DEFAULT_PANEL_SIZE: 110,\n  DEFAULT_MAX_COLUMN: 8,\n  DEFAULT_MAX_ROW: 3,\n  DEFAULT_MARGIN_W: 0,\n  DEFAULT_MARGIN_H: 0,\n  DEFAULT_NO_BACKGROUND: false,\n  DEFAULT_NO_FRAME: false,\n  DEFAULT_GITHUB_API: \"https://api.github.com/graphql\",\n  DEFAULT_GITHUB_RETRY_DELAY: 500,\n  REVALIDATE_TIME: HOUR_IN_MILLISECONDS * 6,\n  REDIS_TTL: HOUR_IN_MILLISECONDS * 4,\n};\n\nexport enum RANK {\n  SECRET = \"SECRET\",\n  SSS = \"SSS\",\n  SS = \"SS\",\n  S = \"S\",\n  AAA = \"AAA\",\n  AA = \"AA\",\n  A = \"A\",\n  B = \"B\",\n  C = \"C\",\n  UNKNOWN = \"?\",\n}\n\nexport const RANK_ORDER = Object.values(RANK);\n"
  },
  {
    "path": "test/test.ts",
    "content": ""
  },
  {
    "path": "vercel.json",
    "content": "{\n  \"public\": true,\n  \"functions\": {\n    \"api/**/*.[jt]s\": {\n      \"runtime\": \"vercel-deno@3.1.1\",\n      \"maxDuration\": 5\n    }\n  },\n  \"rewrites\": [\n    {\n      \"source\": \"/(.*)\",\n      \"destination\": \"/api/$1\"\n    }\n  ]\n}\n"
  }
]