├── test └── test.ts ├── .vercelignore ├── .gitignore ├── .github ├── FUNDING.yml ├── logo.png └── workflows │ ├── testing.yml │ └── close-old-issues.yml ├── src ├── Types │ ├── index.ts │ ├── EServiceKindError.ts │ ├── Request.ts │ └── ServiceError.ts ├── StaticRenderRegeneration │ ├── types.ts │ ├── utils.ts │ ├── index.ts │ └── cache_manager.ts ├── Services │ ├── __mocks__ │ │ ├── notFoundUserMock.json │ │ ├── rateLimitMock.json │ │ └── successGithubResponse.json │ ├── request.ts │ ├── GithubApiService.ts │ └── __tests__ │ │ └── githubApiService.test.ts ├── Helpers │ ├── Logger.ts │ ├── __tests__ │ │ └── Retry.test.ts │ └── Retry.ts ├── pages │ └── Error.ts ├── error_page.ts ├── Repository │ └── GithubRepository.ts ├── Schemas │ └── index.ts ├── config │ └── cache.ts ├── utils.ts ├── card.ts ├── trophy_list.ts ├── user_info.ts ├── icons.ts ├── trophy.ts └── theme.ts ├── docker-compose.yml ├── debug.ts ├── env-example ├── deno.json ├── vercel.json ├── .editorconfig ├── deps.ts ├── LICENSE ├── CONTRIBUTING.md ├── api └── index.ts └── README.md /test/test.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vercelignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .github 3 | README.md 4 | LICENSE 5 | debug.ts -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .env 3 | .idea 4 | deno.lock 5 | *.sh 6 | **/.DS_Store 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ryo-ma] 4 | -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iqtidartara/github-profile-trophy/HEAD/.github/logo.png -------------------------------------------------------------------------------- /src/Types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Request.ts"; 2 | export * from "./ServiceError.ts"; 3 | export * from "./EServiceKindError.ts"; 4 | -------------------------------------------------------------------------------- /src/Types/EServiceKindError.ts: -------------------------------------------------------------------------------- 1 | export const enum EServiceKindError { 2 | RATE_LIMIT = "RATE_LIMITED", 3 | NOT_FOUND = "NOT_FOUND", 4 | } 5 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | redis: 4 | container_name: trophy-redis 5 | image: redis:latest 6 | ports: 7 | - "6379:6379" 8 | -------------------------------------------------------------------------------- /debug.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "https://deno.land/std@0.125.0/http/server.ts"; 2 | import requestHandler from "./api/index.ts"; 3 | 4 | serve(requestHandler, { port: 8080 }); 5 | -------------------------------------------------------------------------------- /env-example: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN1= 2 | GITHUB_TOKEN2= 3 | GITHUB_API=https://api.github.com/graphql 4 | ENABLE_REDIS= 5 | REDIS_PORT=6379 6 | REDIS_HOST= 7 | REDIS_USERNAME= 8 | REDIS_PASSWORD= 9 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "start": "deno run -A debug.ts", 4 | "debug": "deno run --inspect-brk -A debug.ts", 5 | "format": "deno fmt", 6 | "lint": "deno lint", 7 | "test": "ENV_TYPE=test deno test --allow-env" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/StaticRenderRegeneration/types.ts: -------------------------------------------------------------------------------- 1 | export interface StaticRegenerationOptions { 2 | // The number of milliseconds before the page should be revalidated 3 | revalidate?: number; 4 | // The headers to be sent with the response 5 | headers?: Headers; 6 | } 7 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "public": true, 3 | "functions": { 4 | "api/**/*.[jt]s": { 5 | "runtime": "vercel-deno@3.0.4" 6 | } 7 | }, 8 | "rewrites": [ 9 | { 10 | "source": "/(.*)", 11 | "destination": "/api/$1" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = LF 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [vercel.json] 12 | indent_size = 4 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /src/Services/__mocks__/notFoundUserMock.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "user": null 4 | }, 5 | "errors": [ 6 | { 7 | "type": "NOT_FOUND", 8 | "path": [ 9 | "user" 10 | ], 11 | "locations": [ 12 | { 13 | "line": 2, 14 | "column": 5 15 | } 16 | ], 17 | "message": "Could not resolve to a User with the login of 'alekinho'." 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/Types/Request.ts: -------------------------------------------------------------------------------- 1 | export type GithubError = { 2 | message: string; 3 | type: string; 4 | }; 5 | 6 | export type GithubErrorResponse = { 7 | errors: GithubError[]; 8 | }; 9 | 10 | export type GithubExceedError = { 11 | documentation_url: string; 12 | message: string; 13 | }; 14 | 15 | export type QueryDefaultResponse = { 16 | data: { 17 | data: T; 18 | errors?: GithubErrorResponse; 19 | message?: string; 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/Helpers/Logger.ts: -------------------------------------------------------------------------------- 1 | const enableLogging = Deno.env.get("ENV_TYPE") !== "test"; 2 | 3 | export class Logger { 4 | public static log(message: unknown): void { 5 | if (!enableLogging) return; 6 | console.log(message); 7 | } 8 | 9 | public static error(message: unknown): void { 10 | if (!enableLogging) return; 11 | 12 | console.error(message); 13 | } 14 | public static warn(message: unknown): void { 15 | if (!enableLogging) return; 16 | 17 | console.warn(message); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Types/ServiceError.ts: -------------------------------------------------------------------------------- 1 | import { EServiceKindError } from "./EServiceKindError.ts"; 2 | 3 | export class ServiceError extends Error { 4 | constructor(message: string, kind: EServiceKindError) { 5 | super(message); 6 | this.message = message; 7 | this.name = "ServiceError"; 8 | this.cause = kind; 9 | } 10 | 11 | get code(): number { 12 | switch (this.cause) { 13 | case EServiceKindError.RATE_LIMIT: 14 | return 419; 15 | case EServiceKindError.NOT_FOUND: 16 | return 404; 17 | default: 18 | return 400; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/Error.ts: -------------------------------------------------------------------------------- 1 | import { EServiceKindError, ServiceError } from "../Types/index.ts"; 2 | import { Error400, Error404, Error419 } from "../error_page.ts"; 3 | 4 | interface ErrorPageProps { 5 | error: ServiceError; 6 | username: string; 7 | } 8 | 9 | export function ErrorPage({ error, username }: ErrorPageProps) { 10 | let cause: Error400 | Error404 | Error419 = new Error400(); 11 | 12 | if (error.cause === EServiceKindError.RATE_LIMIT) { 13 | cause = new Error419(); 14 | } 15 | 16 | if (error.cause === EServiceKindError.NOT_FOUND) { 17 | cause = new Error404( 18 | "Can not find a user with username: " + username, 19 | ); 20 | } 21 | 22 | return cause; 23 | } 24 | -------------------------------------------------------------------------------- /src/Services/__mocks__/rateLimitMock.json: -------------------------------------------------------------------------------- 1 | { 2 | "exceeded": { 3 | "data": { 4 | "documentation_url": "https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#secondary-rate-limits", 5 | "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." 6 | } 7 | }, 8 | "rate_limit": { 9 | "data": { 10 | "errors": [ 11 | { 12 | "type": "RATE_LIMITED", 13 | "message": "API rate limit exceeded for user ID 10711649." 14 | } 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/error_page.ts: -------------------------------------------------------------------------------- 1 | abstract class BaseError { 2 | readonly status!: number; 3 | readonly message!: string; 4 | constructor(readonly content?: string) {} 5 | render(): string { 6 | return `

${this.status} - ${this.message}

${ 7 | this.content ?? "" 8 | }`; 9 | } 10 | } 11 | 12 | export class Error400 extends BaseError { 13 | readonly status = 400; 14 | readonly message = "Bad Request"; 15 | } 16 | 17 | export class Error419 extends BaseError { 18 | readonly status = 419; 19 | readonly message = "Rate Limit Exceeded"; 20 | } 21 | 22 | export class Error404 extends BaseError { 23 | readonly status = 404; 24 | readonly message = "Not Found"; 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Check PR Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | jobs: 11 | install-dependencies: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | deno-version: [1.36.1] 17 | 18 | steps: 19 | - name: Git Checkout Deno Module 20 | uses: actions/checkout@v2 21 | - name: Use Deno Version ${{ matrix.deno-version }} 22 | uses: denolib/setup-deno@v2 23 | with: 24 | deno-version: ${{ matrix.deno-version }} 25 | - name: Deno format check 26 | run: deno fmt --check 27 | - name: Deno lint check 28 | run: deno task lint 29 | - name: Test Deno Module 30 | run: deno task test 31 | -------------------------------------------------------------------------------- /.github/workflows/close-old-issues.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v5 14 | with: 15 | days-before-issue-stale: 30 16 | days-before-issue-close: 14 17 | stale-issue-label: "stale" 18 | stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." 19 | close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." 20 | days-before-pr-stale: -1 21 | days-before-pr-close: -1 22 | repo-token: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | import { Soxa as ServiceProvider } from "https://deno.land/x/soxa@1.4/src/core/Soxa.ts"; 2 | import { defaults } from "https://deno.land/x/soxa@1.4/src/defaults.ts"; 3 | import { 4 | assertEquals, 5 | assertRejects, 6 | } from "https://deno.land/std@0.203.0/assert/mod.ts"; 7 | import { 8 | assertSpyCalls, 9 | returnsNext, 10 | spy, 11 | stub, 12 | } from "https://deno.land/std@0.203.0/testing/mock.ts"; 13 | 14 | export { 15 | type Bulk, 16 | connect, 17 | type Redis, 18 | } from "https://deno.land/x/redis@v0.31.0/mod.ts"; 19 | 20 | import { CONSTANTS } from "./src/utils.ts"; 21 | 22 | const baseURL = Deno.env.get("GITHUB_API") || CONSTANTS.DEFAULT_GITHUB_API; 23 | 24 | const soxa = new ServiceProvider({ 25 | ...defaults, 26 | baseURL, 27 | }); 28 | 29 | export { 30 | assertEquals, 31 | assertRejects, 32 | assertSpyCalls, 33 | returnsNext, 34 | soxa, 35 | spy, 36 | stub, 37 | }; 38 | -------------------------------------------------------------------------------- /src/Repository/GithubRepository.ts: -------------------------------------------------------------------------------- 1 | import { ServiceError } from "../Types/index.ts"; 2 | import { 3 | GitHubUserActivity, 4 | GitHubUserIssue, 5 | GitHubUserPullRequest, 6 | GitHubUserRepository, 7 | UserInfo, 8 | } from "../user_info.ts"; 9 | 10 | export abstract class GithubRepository { 11 | abstract requestUserInfo(username: string): Promise; 12 | abstract requestUserActivity( 13 | username: string, 14 | ): Promise; 15 | abstract requestUserIssue( 16 | username: string, 17 | ): Promise; 18 | abstract requestUserPullRequest( 19 | username: string, 20 | ): Promise; 21 | abstract requestUserRepository( 22 | username: string, 23 | ): Promise; 24 | } 25 | 26 | export class GithubRepositoryService { 27 | constructor(public repository: GithubRepository) {} 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 ryo-ma 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/StaticRenderRegeneration/utils.ts: -------------------------------------------------------------------------------- 1 | export function getUrl(request: Request) { 2 | try { 3 | return new URL(request.url); 4 | } catch { 5 | return { 6 | pathname: request.url, 7 | search: request.url, 8 | }; 9 | } 10 | } 11 | 12 | export function readCache(cacheFilePath: string): Uint8Array | null { 13 | try { 14 | return Deno.readFileSync(cacheFilePath); 15 | } catch { 16 | return null; 17 | } 18 | } 19 | 20 | // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest 21 | export async function hashString(message: string): Promise { 22 | const encoder = new TextEncoder(); 23 | const data = encoder.encode(message); 24 | const hashBuffer = await crypto.subtle.digest("SHA-256", data); 25 | 26 | const hashArray = Array.from(new Uint8Array(hashBuffer)); 27 | const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join( 28 | "", 29 | ); 30 | 31 | return hashHex; 32 | } 33 | 34 | export const existsSync = (filename: string): boolean => { 35 | try { 36 | Deno.statSync(filename); 37 | // successful, file or directory must exist 38 | return true; 39 | } catch { 40 | return false; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/StaticRenderRegeneration/index.ts: -------------------------------------------------------------------------------- 1 | import { CacheManager } from "./cache_manager.ts"; 2 | import { StaticRegenerationOptions } from "./types.ts"; 3 | import { getUrl, hashString, readCache } from "./utils.ts"; 4 | 5 | export async function staticRenderRegeneration( 6 | request: Request, 7 | options: StaticRegenerationOptions, 8 | render: (request: Request) => Promise, 9 | ) { 10 | // avoid TypeError: Invalid URL at deno:core 11 | const url = getUrl(request); 12 | 13 | // if more conditions are added, make sure to create a variable to skipCache 14 | if (url.pathname === "/favicon.ico") { 15 | return await render(request); 16 | } 17 | 18 | const cacheFile = await hashString(url.pathname + (url.search ?? "")); 19 | const cacheManager = new CacheManager(options.revalidate ?? 0, cacheFile); 20 | if (cacheManager.isCacheValid) { 21 | const cache = readCache(cacheManager.cacheFilePath); 22 | if (cache !== null) { 23 | return new Response(cache, { 24 | headers: options.headers ?? new Headers({}), 25 | }); 26 | } 27 | } 28 | 29 | const response = await render(request); 30 | 31 | if (response.status >= 200 && response.status < 300) { 32 | cacheManager.save(response); 33 | } 34 | 35 | return response; 36 | } 37 | -------------------------------------------------------------------------------- /src/Schemas/index.ts: -------------------------------------------------------------------------------- 1 | export const queryUserActivity = ` 2 | query userInfo($username: String!) { 3 | user(login: $username) { 4 | createdAt 5 | contributionsCollection { 6 | totalCommitContributions 7 | restrictedContributionsCount 8 | totalPullRequestReviewContributions 9 | } 10 | organizations(first: 1) { 11 | totalCount 12 | } 13 | followers(first: 1) { 14 | totalCount 15 | } 16 | } 17 | } 18 | `; 19 | 20 | export const queryUserIssue = ` 21 | query userInfo($username: String!) { 22 | user(login: $username) { 23 | openIssues: issues(states: OPEN) { 24 | totalCount 25 | } 26 | closedIssues: issues(states: CLOSED) { 27 | totalCount 28 | } 29 | } 30 | } 31 | `; 32 | 33 | export const queryUserPullRequest = ` 34 | query userInfo($username: String!) { 35 | user(login: $username) { 36 | pullRequests(first: 1) { 37 | totalCount 38 | } 39 | } 40 | } 41 | `; 42 | 43 | export const queryUserRepository = ` 44 | query userInfo($username: String!) { 45 | user(login: $username) { 46 | repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}) { 47 | totalCount 48 | nodes { 49 | languages(first: 3, orderBy: {direction:DESC, field: SIZE}) { 50 | nodes { 51 | name 52 | } 53 | } 54 | stargazers { 55 | totalCount 56 | } 57 | } 58 | } 59 | } 60 | } 61 | `; 62 | -------------------------------------------------------------------------------- /src/Helpers/__tests__/Retry.test.ts: -------------------------------------------------------------------------------- 1 | import { Retry } from "../Retry.ts"; 2 | import { 3 | assertEquals, 4 | assertRejects, 5 | assertSpyCalls, 6 | spy, 7 | } from "../../../deps.ts"; 8 | 9 | type MockResponse = { 10 | value: number; 11 | }; 12 | 13 | Deno.test("Retry.fetch", () => { 14 | const retryInstance = new Retry(); 15 | const callback = spy(retryInstance, "fetch"); 16 | 17 | retryInstance.fetch(() => { 18 | return { value: 1 }; 19 | }); 20 | 21 | assertSpyCalls(callback, 1); 22 | }); 23 | 24 | Deno.test("Should retry", async () => { 25 | let countErrors = 0; 26 | 27 | const callbackError = () => { 28 | countErrors++; 29 | throw new Error("Panic! Threw Error"); 30 | }; 31 | const retries = 3; 32 | const retryInstance = new Retry(retries); 33 | 34 | await assertRejects( 35 | () => { 36 | return retryInstance.fetch(callbackError); 37 | }, 38 | Error, 39 | `Max retries (${retries}) exceeded.`, 40 | ); 41 | 42 | assertEquals(countErrors, 3); 43 | }); 44 | 45 | Deno.test("Should retry the asyncronous callback", async () => { 46 | let countErrors = 0; 47 | const callbackError = async () => { 48 | countErrors++; 49 | // Mock request in callback 50 | await new Promise((_, reject) => setTimeout(reject, 100)); 51 | }; 52 | 53 | const retries = 3; 54 | const retryInstance = new Retry(retries); 55 | 56 | await assertRejects( 57 | () => { 58 | return retryInstance.fetch(callbackError); 59 | }, 60 | Error, 61 | `Max retries (${retries}) exceeded.`, 62 | ); 63 | 64 | assertEquals(countErrors, 3); 65 | }); 66 | -------------------------------------------------------------------------------- /src/Helpers/Retry.ts: -------------------------------------------------------------------------------- 1 | import { ServiceError } from "../Types/index.ts"; 2 | import { Logger } from "./Logger.ts"; 3 | 4 | export type RetryCallbackProps = { 5 | attempt: number; 6 | }; 7 | 8 | type callbackType = (data: RetryCallbackProps) => Promise | T; 9 | 10 | async function* createAsyncIterable( 11 | callback: callbackType, 12 | retries: number, 13 | delay: number, 14 | ) { 15 | for (let i = 0; i < retries; i++) { 16 | const isLastAttempt = i === retries - 1; 17 | try { 18 | const data = await callback({ attempt: i }); 19 | yield data; 20 | return; 21 | } catch (e) { 22 | if (e instanceof ServiceError && isLastAttempt) { 23 | yield e; 24 | return; 25 | } 26 | 27 | yield null; 28 | Logger.error(e); 29 | await new Promise((resolve) => setTimeout(resolve, delay)); 30 | } 31 | } 32 | } 33 | 34 | export class Retry { 35 | constructor(private maxRetries = 2, private retryDelay = 1000) {} 36 | async fetch( 37 | callback: callbackType, 38 | ) { 39 | let lastError = null; 40 | for await ( 41 | const callbackResult of createAsyncIterable( 42 | callback, 43 | this.maxRetries, 44 | this.retryDelay, 45 | ) 46 | ) { 47 | const isError = callbackResult instanceof Error; 48 | 49 | if (callbackResult && !isError) { 50 | return callbackResult as T; 51 | } 52 | 53 | if (isError) { 54 | lastError = callbackResult; 55 | } 56 | } 57 | 58 | throw new Error(`Max retries (${this.maxRetries}) exceeded.`, { 59 | cause: lastError, 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/StaticRenderRegeneration/cache_manager.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "../Helpers/Logger.ts"; 2 | import { existsSync } from "./utils.ts"; 3 | 4 | export class CacheManager { 5 | constructor(private revalidateTime: number, private cacheFile: string) {} 6 | 7 | // Reason to use /tmp/: 8 | // https://github.com/orgs/vercel/discussions/314 9 | get cacheFilePath(): string { 10 | return `/tmp/${this.cacheFile}`; 11 | } 12 | get cacheFileExists(): boolean { 13 | return existsSync(this.cacheFilePath); 14 | } 15 | 16 | get cacheFileLastModified(): Date | null { 17 | if (!this.cacheFileExists) { 18 | return null; 19 | } 20 | const fileInfo = Deno.statSync(this.cacheFilePath); 21 | return fileInfo.mtime ?? null; 22 | } 23 | 24 | get cacheFileLastModifiedGetTime(): number | null { 25 | const lastModified = this.cacheFileLastModified; 26 | if (lastModified === null) { 27 | return null; 28 | } 29 | return lastModified.getTime(); 30 | } 31 | 32 | get isCacheValid(): boolean { 33 | if (this.cacheFileLastModifiedGetTime === null) { 34 | return false; 35 | } 36 | const currentTime = new Date().getTime(); 37 | return currentTime - this.cacheFileLastModifiedGetTime < 38 | this.revalidateTime; 39 | } 40 | 41 | async save(response: Response): Promise { 42 | if (response === null) return; 43 | // Prevent TypeError: ReadableStream is locked 44 | const text = await response.clone().text(); 45 | const data = new TextEncoder().encode(text); 46 | 47 | Deno.writeFile(this.cacheFilePath, data, { create: true }).catch(() => { 48 | Logger.warn("Failed to save cache file"); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | ## Environment 4 | 5 | - Deno >= v1.36.1 6 | - [Vercel](https://vercel.com/) 7 | - GitHub API v4 8 | - Docker and Docker compose (optional) 9 | 10 | ## Local Run 11 | 12 | Create `.env` file to project root directory, and write your GitHub token to the 13 | `.env` file. Please select the authority of `repo` when creating token. 14 | 15 | ```properties 16 | GITHUB_TOKEN1=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 17 | GITHUB_TOKEN2=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 18 | 19 | # if using GitHub Enterprise: 20 | # (this env var defaults to https://api.github.com/graphql) 21 | GITHUB_API=https://github.example.com/api/graphql 22 | ``` 23 | 24 | Run local server. 25 | 26 | ```sh 27 | deno task start 28 | ``` 29 | 30 | You can enable the Redis if you want, but it's not mandatory. 31 | 32 | ```sh 33 | docker compose up -d 34 | ``` 35 | 36 | Rename `env-example` to `.env`, and change ENABLE_REDIS to true 37 | 38 | Open localhost from your browser. 39 | 40 | http://localhost:8080/?username=ryo-ma 41 | 42 | ## Editor config 43 | 44 | Read the [.editorconfig](./.editorconfig) 45 | 46 | ## Pull Requests 47 | 48 | Pull requests are are always welcome! In general, they should a single concern 49 | in the least number of changed lines as possible. For changes that address core 50 | functionality, it is best to open an issue to discuss your proposal first. I 51 | look forward to seeing what you come up with! 52 | 53 | ## Run deno lint 54 | 55 | ## What to do before contributing 56 | 57 | ### 1. Run deno lint 58 | 59 | ```sh 60 | deno task lint 61 | ``` 62 | 63 | ### 2. Run deno format 64 | 65 | ```sh 66 | deno task format 67 | ``` 68 | 69 | ### 3. Run deno test 70 | 71 | ```sh 72 | deno task test 73 | ``` 74 | -------------------------------------------------------------------------------- /src/Services/request.ts: -------------------------------------------------------------------------------- 1 | import { soxa } from "../../deps.ts"; 2 | import { 3 | EServiceKindError, 4 | GithubErrorResponse, 5 | GithubExceedError, 6 | QueryDefaultResponse, 7 | ServiceError, 8 | } from "../Types/index.ts"; 9 | 10 | export async function requestGithubData( 11 | query: string, 12 | variables: { [key: string]: string }, 13 | token = "", 14 | ) { 15 | const response = await soxa.post("", {}, { 16 | data: { query, variables }, 17 | headers: { 18 | Authorization: `bearer ${token}`, 19 | }, 20 | }) as QueryDefaultResponse<{ user: T }>; 21 | const responseData = response.data; 22 | 23 | if (responseData?.data?.user) { 24 | return responseData.data.user; 25 | } 26 | 27 | throw handleError( 28 | responseData as unknown as GithubErrorResponse | GithubExceedError, 29 | ); 30 | } 31 | 32 | function handleError( 33 | reponseErrors: GithubErrorResponse | GithubExceedError, 34 | ): ServiceError { 35 | let isRateLimitExceeded = false; 36 | const arrayErrors = (reponseErrors as GithubErrorResponse)?.errors || []; 37 | const objectError = (reponseErrors as GithubExceedError) || {}; 38 | 39 | if (Array.isArray(arrayErrors)) { 40 | isRateLimitExceeded = arrayErrors.some((error) => 41 | error.type.includes(EServiceKindError.RATE_LIMIT) 42 | ); 43 | } 44 | 45 | if (objectError?.message) { 46 | isRateLimitExceeded = objectError?.message.includes( 47 | "rate limit", 48 | ); 49 | } 50 | 51 | if (isRateLimitExceeded) { 52 | throw new ServiceError( 53 | "Rate limit exceeded", 54 | EServiceKindError.RATE_LIMIT, 55 | ); 56 | } 57 | 58 | throw new ServiceError( 59 | "unknown error", 60 | EServiceKindError.NOT_FOUND, 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/config/cache.ts: -------------------------------------------------------------------------------- 1 | import { Bulk, connect, Redis } from "../../deps.ts"; 2 | import { Logger } from "../Helpers/Logger.ts"; 3 | import { CONSTANTS } from "../utils.ts"; 4 | 5 | const enableCache = Deno.env.get("ENABLE_REDIS") || false; 6 | 7 | // https://developer.redis.com/develop/deno/ 8 | class CacheProvider { 9 | private static instance: CacheProvider; 10 | public client: Redis | null = null; 11 | 12 | private constructor() {} 13 | 14 | static getInstance(): CacheProvider { 15 | if (!CacheProvider.instance) { 16 | CacheProvider.instance = new CacheProvider(); 17 | } 18 | return CacheProvider.instance; 19 | } 20 | 21 | async connect(): Promise { 22 | if (!enableCache) return; 23 | this.client = await connect({ 24 | hostname: Deno.env.get("REDIS_HOST") || "", 25 | port: Number(Deno.env.get("REDIS_PORT")) || 6379, 26 | username: Deno.env.get("REDIS_USERNAME") || "", 27 | password: Deno.env.get("REDIS_PASSWORD") || "", 28 | }); 29 | } 30 | 31 | async get(key: string): Promise { 32 | if (!enableCache) return undefined; 33 | 34 | try { 35 | if (!this.client) { 36 | await this.connect(); 37 | } 38 | 39 | return await this.client?.get(key); 40 | } catch { 41 | return undefined; 42 | } 43 | } 44 | 45 | async set(key: string, value: string): Promise { 46 | if (!enableCache) return; 47 | 48 | try { 49 | if (!this.client) { 50 | await this.connect(); 51 | } 52 | await this.client?.set(key, value, { 53 | px: CONSTANTS.REDIS_TTL, 54 | }); 55 | } catch (e) { 56 | Logger.error(`Failed to set cache: ${e.message}`); 57 | } 58 | } 59 | 60 | async del(key: string): Promise { 61 | if (!enableCache) return; 62 | 63 | try { 64 | if (!this.client) { 65 | await this.connect(); 66 | } 67 | await this.client?.del(key); 68 | } catch (e) { 69 | Logger.error(`Failed to delete cache: ${e.message}`); 70 | } 71 | } 72 | } 73 | 74 | export const cacheProvider = CacheProvider.getInstance(); 75 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export class CustomURLSearchParams extends URLSearchParams { 2 | constructor( 3 | init?: string[][] | Record | string | URLSearchParams, 4 | ) { 5 | super(init); 6 | } 7 | getStringValue(key: string, defaultValue: string): string { 8 | if (super.has(key)) { 9 | const param = super.get(key); 10 | if (param !== null) { 11 | return param.toString(); 12 | } 13 | } 14 | return defaultValue.toString(); 15 | } 16 | getNumberValue(key: string, defaultValue: number): number { 17 | if (super.has(key)) { 18 | const param = super.get(key); 19 | if (param !== null) { 20 | const parsedValue = parseInt(param); 21 | if (isNaN(parsedValue)) { 22 | return defaultValue; 23 | } 24 | return parsedValue; 25 | } 26 | } 27 | return defaultValue; 28 | } 29 | getBooleanValue(key: string, defaultValue: boolean): boolean { 30 | if (super.has(key)) { 31 | const param = super.get(key); 32 | return param !== null && param.toString() === "true"; 33 | } 34 | return defaultValue; 35 | } 36 | } 37 | 38 | export function parseParams(req: Request): CustomURLSearchParams { 39 | const splittedURL = req.url.split("?"); 40 | if (splittedURL.length < 2) { 41 | return new CustomURLSearchParams(); 42 | } 43 | return new CustomURLSearchParams(splittedURL[1]); 44 | } 45 | 46 | export function abridgeScore(score: number): string { 47 | if (Math.abs(score) < 1) { 48 | return "0pt"; 49 | } 50 | if (Math.abs(score) > 999) { 51 | return (Math.sign(score) * (Math.abs(score) / 1000)).toFixed(1) + "kpt"; 52 | } 53 | return (Math.sign(score) * Math.abs(score)).toString() + "pt"; 54 | } 55 | 56 | const HOUR_IN_MILLISECONDS = 60 * 60 * 1000; 57 | 58 | export const CONSTANTS = { 59 | CACHE_MAX_AGE: 7200, 60 | DEFAULT_PANEL_SIZE: 110, 61 | DEFAULT_MAX_COLUMN: 8, 62 | DEFAULT_MAX_ROW: 3, 63 | DEFAULT_MARGIN_W: 0, 64 | DEFAULT_MARGIN_H: 0, 65 | DEFAULT_NO_BACKGROUND: false, 66 | DEFAULT_NO_FRAME: false, 67 | DEFAULT_GITHUB_API: "https://api.github.com/graphql", 68 | DEFAULT_GITHUB_RETRY_DELAY: 1000, 69 | REVALIDATE_TIME: HOUR_IN_MILLISECONDS, 70 | REDIS_TTL: HOUR_IN_MILLISECONDS * 4, 71 | }; 72 | 73 | export enum RANK { 74 | SECRET = "SECRET", 75 | SSS = "SSS", 76 | SS = "SS", 77 | S = "S", 78 | AAA = "AAA", 79 | AA = "AA", 80 | A = "A", 81 | B = "B", 82 | C = "C", 83 | UNKNOWN = "?", 84 | } 85 | 86 | export const RANK_ORDER = Object.values(RANK); 87 | -------------------------------------------------------------------------------- /src/card.ts: -------------------------------------------------------------------------------- 1 | import { UserInfo } from "./user_info.ts"; 2 | import { TrophyList } from "./trophy_list.ts"; 3 | import { Trophy } from "./trophy.ts"; 4 | import { Theme } from "./theme.ts"; 5 | 6 | export class Card { 7 | private width = 0; 8 | private height = 0; 9 | constructor( 10 | private titles: Array, 11 | private ranks: Array, 12 | private maxColumn: number, 13 | private maxRow: number, 14 | private panelSize: number, 15 | private marginWidth: number, 16 | private marginHeight: number, 17 | private noBackground: boolean, 18 | private noFrame: boolean, 19 | ) { 20 | this.width = panelSize * this.maxColumn + 21 | this.marginWidth * (this.maxColumn - 1); 22 | } 23 | render( 24 | userInfo: UserInfo, 25 | theme: Theme, 26 | ): string { 27 | const trophyList = new TrophyList(userInfo); 28 | 29 | trophyList.filterByHidden(); 30 | 31 | if (this.titles.length != 0) { 32 | trophyList.filterByTitles(this.titles); 33 | } 34 | 35 | if (this.ranks.length != 0) { 36 | trophyList.filterByRanks(this.ranks); 37 | } 38 | 39 | trophyList.sortByRank(); 40 | 41 | if (this.maxColumn == -1) { 42 | this.maxColumn = trophyList.length; 43 | this.width = this.panelSize * this.maxColumn + 44 | this.marginWidth * (this.maxColumn - 1); 45 | } 46 | 47 | const row = this.getRow(trophyList); 48 | this.height = this.getHeight(row); 49 | 50 | return ` 51 | 58 | ${this.renderTrophy(trophyList, theme)} 59 | `; 60 | } 61 | private getRow(trophyList: TrophyList) { 62 | let row = Math.floor((trophyList.length - 1) / this.maxColumn) + 1; 63 | if (row > this.maxRow) { 64 | row = this.maxRow; 65 | } 66 | return row; 67 | } 68 | private getHeight(row: number) { 69 | // Calculate the height of card from turns 70 | return this.panelSize * row + this.marginHeight * (row - 1); 71 | } 72 | 73 | private renderTrophy(trophyList: TrophyList, theme: Theme) { 74 | return trophyList.getArray.reduce( 75 | (sum: string, trophy: Trophy, i: number) => { 76 | const currentColumn = i % this.maxColumn; 77 | const currentRow = Math.floor(i / this.maxColumn); 78 | const x = this.panelSize * currentColumn + 79 | this.marginWidth * currentColumn; 80 | const y = this.panelSize * currentRow + this.marginHeight * currentRow; 81 | return sum + 82 | trophy.render( 83 | theme, 84 | x, 85 | y, 86 | this.panelSize, 87 | this.noBackground, 88 | this.noFrame, 89 | ); 90 | }, 91 | "", 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/trophy_list.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountDurationTrophy, 3 | AllSuperRankTrophy, 4 | AncientAccountTrophy, 5 | Joined2020Trophy, 6 | LongTimeAccountTrophy, 7 | MultipleLangTrophy, 8 | MultipleOrganizationsTrophy, 9 | OGAccountTrophy, 10 | TotalCommitTrophy, 11 | TotalFollowerTrophy, 12 | TotalIssueTrophy, 13 | TotalPullRequestTrophy, 14 | TotalRepositoryTrophy, 15 | TotalReviewsTrophy, 16 | TotalStarTrophy, 17 | Trophy, 18 | } from "./trophy.ts"; 19 | import { UserInfo } from "./user_info.ts"; 20 | import { RANK, RANK_ORDER } from "./utils.ts"; 21 | 22 | export class TrophyList { 23 | private trophies = new Array(); 24 | constructor(userInfo: UserInfo) { 25 | // Base trophies 26 | this.trophies.push( 27 | new TotalStarTrophy(userInfo.totalStargazers), 28 | new TotalCommitTrophy(userInfo.totalCommits), 29 | new TotalFollowerTrophy(userInfo.totalFollowers), 30 | new TotalIssueTrophy(userInfo.totalIssues), 31 | new TotalPullRequestTrophy(userInfo.totalPullRequests), 32 | new TotalRepositoryTrophy(userInfo.totalRepositories), 33 | new TotalReviewsTrophy(userInfo.totalReviews), 34 | ); 35 | // Secret trophies 36 | this.trophies.push( 37 | new AllSuperRankTrophy(this.isAllSRank), 38 | new MultipleLangTrophy(userInfo.languageCount), 39 | new LongTimeAccountTrophy(userInfo.durationYear), 40 | new AncientAccountTrophy(userInfo.ancientAccount), 41 | new OGAccountTrophy(userInfo.ogAccount), 42 | new Joined2020Trophy(userInfo.joined2020), 43 | new MultipleOrganizationsTrophy(userInfo.totalOrganizations), 44 | new AccountDurationTrophy(userInfo.durationDays), 45 | ); 46 | } 47 | get length() { 48 | return this.trophies.length; 49 | } 50 | get getArray() { 51 | return this.trophies; 52 | } 53 | private get isAllSRank() { 54 | return this.trophies.every((trophy) => trophy.rank.slice(0, 1) == RANK.S) 55 | ? 1 56 | : 0; 57 | } 58 | filterByHidden() { 59 | this.trophies = this.trophies.filter((trophy) => 60 | !trophy.hidden || trophy.rank !== RANK.UNKNOWN 61 | ); 62 | } 63 | filterByTitles(titles: Array) { 64 | this.trophies = this.trophies.filter((trophy) => { 65 | return trophy.filterTitles.some((title) => titles.includes(title)); 66 | }); 67 | } 68 | filterByRanks(ranks: Array) { 69 | if (ranks.filter((rank) => rank.includes("-")).length !== 0) { 70 | this.trophies = this.trophies.filter((trophy) => 71 | !ranks.map((rank) => rank.substring(1)).includes(trophy.rank) 72 | ); 73 | return; 74 | } 75 | this.trophies = this.trophies.filter((trophy) => 76 | ranks.includes(trophy.rank) 77 | ); 78 | } 79 | sortByRank() { 80 | this.trophies = this.trophies.sort((a: Trophy, b: Trophy) => 81 | RANK_ORDER.indexOf(a.rank) - RANK_ORDER.indexOf(b.rank) 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/user_info.ts: -------------------------------------------------------------------------------- 1 | type Language = { name: string }; 2 | type Stargazers = { totalCount: number }; 3 | type Repository = { 4 | languages: { nodes: Language[] }; 5 | stargazers: Stargazers; 6 | }; 7 | export type GitHubUserRepository = { 8 | repositories: { 9 | totalCount: number; 10 | nodes: Repository[]; 11 | }; 12 | }; 13 | 14 | export type GitHubUserIssue = { 15 | openIssues: { 16 | totalCount: number; 17 | }; 18 | closedIssues: { 19 | totalCount: number; 20 | }; 21 | }; 22 | 23 | export type GitHubUserPullRequest = { 24 | pullRequests: { 25 | totalCount: number; 26 | }; 27 | }; 28 | 29 | export type GitHubUserActivity = { 30 | createdAt: string; 31 | contributionsCollection: { 32 | totalCommitContributions: number; 33 | restrictedContributionsCount: number; 34 | totalPullRequestReviewContributions: number; 35 | }; 36 | organizations: { 37 | totalCount: number; 38 | }; 39 | followers: { 40 | totalCount: number; 41 | }; 42 | }; 43 | export class UserInfo { 44 | public readonly totalCommits: number; 45 | public readonly totalFollowers: number; 46 | public readonly totalIssues: number; 47 | public readonly totalOrganizations: number; 48 | public readonly totalPullRequests: number; 49 | public readonly totalReviews: number; 50 | public readonly totalStargazers: number; 51 | public readonly totalRepositories: number; 52 | public readonly languageCount: number; 53 | public readonly durationYear: number; 54 | public readonly durationDays: number; 55 | public readonly ancientAccount: number; 56 | public readonly joined2020: number; 57 | public readonly ogAccount: number; 58 | constructor( 59 | userActivity: GitHubUserActivity, 60 | userIssue: GitHubUserIssue, 61 | userPullRequest: GitHubUserPullRequest, 62 | userRepository: GitHubUserRepository, 63 | ) { 64 | const totalCommits = 65 | userActivity.contributionsCollection.restrictedContributionsCount + 66 | userActivity.contributionsCollection.totalCommitContributions; 67 | const totalStargazers = userRepository.repositories.nodes.reduce( 68 | (prev: number, node: Repository) => { 69 | return prev + node.stargazers.totalCount; 70 | }, 71 | 0, 72 | ); 73 | 74 | const languages = new Set(); 75 | userRepository.repositories.nodes.forEach((node: Repository) => { 76 | if (node.languages.nodes != undefined) { 77 | node.languages.nodes.forEach((node: Language) => { 78 | if (node != undefined) { 79 | languages.add(node.name); 80 | } 81 | }); 82 | } 83 | }); 84 | const durationTime = new Date().getTime() - 85 | new Date(userActivity.createdAt).getTime(); 86 | const durationYear = new Date(durationTime).getUTCFullYear() - 1970; 87 | const durationDays = Math.floor( 88 | durationTime / (1000 * 60 * 60 * 24) / 100, 89 | ); 90 | const ancientAccount = 91 | new Date(userActivity.createdAt).getFullYear() <= 2010 ? 1 : 0; 92 | const joined2020 = new Date(userActivity.createdAt).getFullYear() == 2020 93 | ? 1 94 | : 0; 95 | const ogAccount = new Date(userActivity.createdAt).getFullYear() <= 2008 96 | ? 1 97 | : 0; 98 | 99 | this.totalCommits = totalCommits; 100 | this.totalFollowers = userActivity.followers.totalCount; 101 | this.totalIssues = userIssue.openIssues.totalCount + 102 | userIssue.closedIssues.totalCount; 103 | this.totalOrganizations = userActivity.organizations.totalCount; 104 | this.totalPullRequests = userPullRequest.pullRequests.totalCount; 105 | this.totalReviews = 106 | userActivity.contributionsCollection.totalPullRequestReviewContributions; 107 | this.totalStargazers = totalStargazers; 108 | this.totalRepositories = userRepository.repositories.totalCount; 109 | this.languageCount = languages.size; 110 | this.durationYear = durationYear; 111 | this.durationDays = durationDays; 112 | this.ancientAccount = ancientAccount; 113 | this.joined2020 = joined2020; 114 | this.ogAccount = ogAccount; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /api/index.ts: -------------------------------------------------------------------------------- 1 | import { Card } from "../src/card.ts"; 2 | import { CONSTANTS, parseParams } from "../src/utils.ts"; 3 | import { COLORS, Theme } from "../src/theme.ts"; 4 | import { Error400 } from "../src/error_page.ts"; 5 | import "https://deno.land/x/dotenv@v0.5.0/load.ts"; 6 | import { staticRenderRegeneration } from "../src/StaticRenderRegeneration/index.ts"; 7 | import { GithubRepositoryService } from "../src/Repository/GithubRepository.ts"; 8 | import { GithubApiService } from "../src/Services/GithubApiService.ts"; 9 | import { ServiceError } from "../src/Types/index.ts"; 10 | import { ErrorPage } from "../src/pages/Error.ts"; 11 | import { cacheProvider } from "../src/config/cache.ts"; 12 | 13 | const serviceProvider = new GithubApiService(); 14 | const client = new GithubRepositoryService(serviceProvider).repository; 15 | 16 | const defaultHeaders = new Headers( 17 | { 18 | "Content-Type": "image/svg+xml", 19 | "Cache-Control": `public, max-age=${CONSTANTS.CACHE_MAX_AGE}`, 20 | }, 21 | ); 22 | 23 | export default (request: Request) => 24 | staticRenderRegeneration(request, { 25 | revalidate: CONSTANTS.REVALIDATE_TIME, 26 | headers: defaultHeaders, 27 | }, function (req: Request) { 28 | return app(req); 29 | }); 30 | 31 | async function app(req: Request): Promise { 32 | const params = parseParams(req); 33 | const username = params.get("username"); 34 | const row = params.getNumberValue("row", CONSTANTS.DEFAULT_MAX_ROW); 35 | const column = params.getNumberValue("column", CONSTANTS.DEFAULT_MAX_COLUMN); 36 | const themeParam: string = params.getStringValue("theme", "default"); 37 | let theme: Theme = COLORS.default; 38 | if (Object.keys(COLORS).includes(themeParam)) { 39 | theme = COLORS[themeParam]; 40 | } 41 | const marginWidth = params.getNumberValue( 42 | "margin-w", 43 | CONSTANTS.DEFAULT_MARGIN_W, 44 | ); 45 | const paddingHeight = params.getNumberValue( 46 | "margin-h", 47 | CONSTANTS.DEFAULT_MARGIN_H, 48 | ); 49 | const noBackground = params.getBooleanValue( 50 | "no-bg", 51 | CONSTANTS.DEFAULT_NO_BACKGROUND, 52 | ); 53 | const noFrame = params.getBooleanValue( 54 | "no-frame", 55 | CONSTANTS.DEFAULT_NO_FRAME, 56 | ); 57 | const titles: Array = params.getAll("title").flatMap((r) => 58 | r.split(",") 59 | ).map((r) => r.trim()); 60 | const ranks: Array = params.getAll("rank").flatMap((r) => 61 | r.split(",") 62 | ).map((r) => r.trim()); 63 | 64 | if (username === null) { 65 | const [base] = req.url.split("?"); 66 | const error = new Error400( 67 | `

"username" is a required query parameter

68 |

The URL should look like ${base}?username=USERNAME, where 69 | USERNAME is your GitHub username.`, 70 | ); 71 | return new Response( 72 | error.render(), 73 | { 74 | status: error.status, 75 | headers: new Headers({ "Content-Type": "text" }), 76 | }, 77 | ); 78 | } 79 | const userKeyCache = ["v1", username].join("-"); 80 | const userInfoCached = await cacheProvider.get(userKeyCache) || "{}"; 81 | let userInfo = JSON.parse(userInfoCached); 82 | const hasCache = !!Object.keys(userInfo).length; 83 | 84 | if (!hasCache) { 85 | const userResponseInfo = await client.requestUserInfo(username); 86 | if (userResponseInfo instanceof ServiceError) { 87 | return new Response( 88 | ErrorPage({ error: userInfo, username }).render(), 89 | { 90 | status: userResponseInfo.code, 91 | headers: new Headers({ "Content-Type": "text" }), 92 | }, 93 | ); 94 | } 95 | userInfo = userResponseInfo; 96 | await cacheProvider.set(userKeyCache, JSON.stringify(userInfo)); 97 | } 98 | // Success Response 99 | return new Response( 100 | new Card( 101 | titles, 102 | ranks, 103 | column, 104 | row, 105 | CONSTANTS.DEFAULT_PANEL_SIZE, 106 | marginWidth, 107 | paddingHeight, 108 | noBackground, 109 | noFrame, 110 | ).render(userInfo, theme), 111 | { 112 | headers: defaultHeaders, 113 | }, 114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /src/Services/GithubApiService.ts: -------------------------------------------------------------------------------- 1 | import { GithubRepository } from "../Repository/GithubRepository.ts"; 2 | import { 3 | GitHubUserActivity, 4 | GitHubUserIssue, 5 | GitHubUserPullRequest, 6 | GitHubUserRepository, 7 | UserInfo, 8 | } from "../user_info.ts"; 9 | import { 10 | queryUserActivity, 11 | queryUserIssue, 12 | queryUserPullRequest, 13 | queryUserRepository, 14 | } from "../Schemas/index.ts"; 15 | import { Retry } from "../Helpers/Retry.ts"; 16 | import { CONSTANTS } from "../utils.ts"; 17 | import { EServiceKindError } from "../Types/EServiceKindError.ts"; 18 | import { ServiceError } from "../Types/ServiceError.ts"; 19 | import { Logger } from "../Helpers/Logger.ts"; 20 | import { requestGithubData } from "./request.ts"; 21 | 22 | // Need to be here - Exporting from another file makes array of null 23 | export const TOKENS = [ 24 | Deno.env.get("GITHUB_TOKEN1"), 25 | Deno.env.get("GITHUB_TOKEN2"), 26 | ]; 27 | 28 | export class GithubApiService extends GithubRepository { 29 | async requestUserRepository( 30 | username: string, 31 | ): Promise { 32 | return await this.executeQuery(queryUserRepository, { 33 | username, 34 | }); 35 | } 36 | async requestUserActivity( 37 | username: string, 38 | ): Promise { 39 | return await this.executeQuery(queryUserActivity, { 40 | username, 41 | }); 42 | } 43 | async requestUserIssue( 44 | username: string, 45 | ): Promise { 46 | return await this.executeQuery(queryUserIssue, { 47 | username, 48 | }); 49 | } 50 | async requestUserPullRequest( 51 | username: string, 52 | ): Promise { 53 | return await this.executeQuery( 54 | queryUserPullRequest, 55 | { username }, 56 | ); 57 | } 58 | async requestUserInfo(username: string): Promise { 59 | // Avoid to call others if one of them is null 60 | const repository = await this.requestUserRepository(username); 61 | 62 | if (repository instanceof ServiceError) { 63 | Logger.error(repository); 64 | return repository; 65 | } 66 | 67 | const promises = Promise.allSettled([ 68 | this.requestUserActivity(username), 69 | this.requestUserIssue(username), 70 | this.requestUserPullRequest(username), 71 | ]); 72 | const [activity, issue, pullRequest] = await promises; 73 | const status = [ 74 | activity.status, 75 | issue.status, 76 | pullRequest.status, 77 | ]; 78 | 79 | if (status.includes("rejected")) { 80 | Logger.error(`Can not find a user with username:' ${username}'`); 81 | return new ServiceError("Not found", EServiceKindError.NOT_FOUND); 82 | } 83 | 84 | return new UserInfo( 85 | (activity as PromiseFulfilledResult).value, 86 | (issue as PromiseFulfilledResult).value, 87 | (pullRequest as PromiseFulfilledResult).value, 88 | repository as GitHubUserRepository, 89 | ); 90 | } 91 | 92 | async executeQuery( 93 | query: string, 94 | variables: { [key: string]: string }, 95 | ) { 96 | try { 97 | const retry = new Retry( 98 | TOKENS.length, 99 | CONSTANTS.DEFAULT_GITHUB_RETRY_DELAY, 100 | ); 101 | const response = await retry.fetch>(async ({ attempt }) => { 102 | return await requestGithubData( 103 | query, 104 | variables, 105 | TOKENS[attempt], 106 | ); 107 | }); 108 | 109 | return response; 110 | } catch (error) { 111 | if (error.cause instanceof ServiceError) { 112 | Logger.error(error.cause.message); 113 | return error.cause; 114 | } 115 | if (error instanceof Error && error.cause) { 116 | Logger.error(JSON.stringify(error.cause, null, 2)); 117 | } else { 118 | Logger.error(error); 119 | } 120 | return new ServiceError("not found", EServiceKindError.NOT_FOUND); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Services/__tests__/githubApiService.test.ts: -------------------------------------------------------------------------------- 1 | import { GithubApiService } from "../GithubApiService.ts"; 2 | import { assertEquals, returnsNext, soxa, stub } from "../../../deps.ts"; 3 | import { GitHubUserRepository } from "../../user_info.ts"; 4 | 5 | const rateLimitMock = await import("../__mocks__/rateLimitMock.json", { 6 | assert: { type: "json" }, 7 | }); 8 | 9 | const successGithubResponseMock = await import( 10 | "../__mocks__/successGithubResponse.json", 11 | { assert: { type: "json" } } 12 | ); 13 | 14 | const notFoundGithubResponseMock = await import( 15 | "../__mocks__/notFoundUserMock.json", 16 | { assert: { type: "json" } } 17 | ); 18 | 19 | import { ServiceError } from "../../Types/index.ts"; 20 | 21 | // Unfortunatelly, The spy is a global instance 22 | // We can't reset mock as Jest does. 23 | stub( 24 | soxa, 25 | "post", 26 | returnsNext([ 27 | // Should get data in first try 28 | new Promise((resolve) => { 29 | resolve(successGithubResponseMock.default); 30 | }), 31 | // // Should get data in second Retry 32 | new Promise((resolve) => { 33 | resolve(rateLimitMock.default.rate_limit); 34 | }), 35 | new Promise((resolve) => { 36 | resolve(successGithubResponseMock.default); 37 | }), 38 | // Should throw NOT FOUND 39 | new Promise((resolve) => { 40 | resolve(notFoundGithubResponseMock.default); 41 | }), 42 | new Promise((resolve) => { 43 | resolve(notFoundGithubResponseMock.default); 44 | }), 45 | // Should throw NOT FOUND even if request the user only 46 | new Promise((resolve) => { 47 | resolve(notFoundGithubResponseMock.default); 48 | }), 49 | new Promise((resolve) => { 50 | resolve(notFoundGithubResponseMock.default); 51 | }), 52 | // Should throw RATE LIMIT 53 | new Promise((resolve) => { 54 | resolve(rateLimitMock.default.rate_limit); 55 | }), 56 | new Promise((resolve) => { 57 | resolve(rateLimitMock.default.rate_limit); 58 | }), 59 | // Should throw RATE LIMIT Exceed 60 | new Promise((resolve) => { 61 | resolve(rateLimitMock.default.rate_limit); 62 | }), 63 | new Promise((resolve) => { 64 | resolve(rateLimitMock.default.exceeded); 65 | }), 66 | ]), 67 | ); 68 | 69 | Deno.test("Should get data in first try", async () => { 70 | const provider = new GithubApiService(); 71 | 72 | const data = await provider.requestUserRepository( 73 | "test", 74 | ) as GitHubUserRepository; 75 | 76 | assertEquals(data.repositories.totalCount, 128); 77 | }); 78 | 79 | Deno.test("Should get data in second Retry", async () => { 80 | const provider = new GithubApiService(); 81 | 82 | const data = await provider.requestUserRepository( 83 | "test", 84 | ) as GitHubUserRepository; 85 | 86 | assertEquals(data.repositories.totalCount, 128); 87 | }); 88 | 89 | Deno.test("Should throw NOT FOUND", async () => { 90 | const provider = new GithubApiService(); 91 | let error = null; 92 | 93 | try { 94 | error = await provider.requestUserInfo("test"); 95 | } catch (e) { 96 | error = e; 97 | } 98 | 99 | assertEquals(error.code, 404); 100 | assertEquals(error instanceof ServiceError, true); 101 | }); 102 | Deno.test("Should throw NOT FOUND even if request the user only", async () => { 103 | const provider = new GithubApiService(); 104 | let error = null; 105 | 106 | try { 107 | error = await provider.requestUserRepository("test"); 108 | } catch (e) { 109 | error = e; 110 | } 111 | 112 | assertEquals(error.code, 404); 113 | assertEquals(error instanceof ServiceError, true); 114 | }); 115 | 116 | // The assertRejects() assertion is a little more complicated 117 | // mainly because it deals with Promises. 118 | // https://docs.deno.com/runtime/manual/basics/testing/assertions#throws 119 | Deno.test("Should throw RATE LIMIT", async () => { 120 | const provider = new GithubApiService(); 121 | let error = null; 122 | 123 | try { 124 | error = await provider.requestUserRepository("test"); 125 | } catch (e) { 126 | error = e; 127 | } 128 | 129 | assertEquals(error.code, 419); 130 | assertEquals(error instanceof ServiceError, true); 131 | }); 132 | 133 | Deno.test("Should throw RATE LIMIT Exceed", async () => { 134 | const provider = new GithubApiService(); 135 | let error = null; 136 | 137 | try { 138 | error = await provider.requestUserRepository("test"); 139 | } catch (e) { 140 | error = e; 141 | } 142 | 143 | assertEquals(error.code, 419); 144 | assertEquals(error instanceof ServiceError, true); 145 | }); 146 | -------------------------------------------------------------------------------- /src/icons.ts: -------------------------------------------------------------------------------- 1 | import { RANK } from "./utils.ts"; 2 | import { Theme } from "./theme.ts"; 3 | 4 | const leafIcon = (laurel: string): string => { 5 | return ` 6 | 7 | Created by potrace 1.15, written by Peter Selinger 2001-2017 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | `; 27 | }; 28 | 29 | export const getNextRankBar = ( 30 | title: string, 31 | percentage: number, 32 | color: string, 33 | ): string => { 34 | const maxWidth = 80; 35 | return ` 36 | 49 | 58 | 66 | `; 67 | }; 68 | 69 | const getSmallTrophyIcon = ( 70 | icon: string, 71 | color: string, 72 | count: number, 73 | ): string => { 74 | const leftXPosition = 7; 75 | const rightXPosition = 68; 76 | const getIcon = (x: number) => { 77 | return ` 78 | ${icon} 79 | `; 80 | }; 81 | if (count == 1) { 82 | // Double Rank 83 | return getIcon(rightXPosition); 84 | } else if (count == 2) { 85 | // Triple Rank 86 | return `${getIcon(leftXPosition)}${getIcon(rightXPosition)}`; 87 | } 88 | // Single Rank 89 | return ""; 90 | }; 91 | export const getTrophyIcon = (theme: Theme, rank = RANK.UNKNOWN) => { 92 | let color = theme.DEFAULT_RANK_BASE; 93 | let rankColor = theme.DEFAULT_RANK_TEXT; 94 | let backgroundIcon = ""; 95 | let gradationColor = ` 96 | 97 | 98 | 99 | `; 100 | const { ICON_CIRCLE } = theme; 101 | if (rank === RANK.SECRET) { 102 | rankColor = theme.SECRET_RANK_TEXT; 103 | gradationColor = ` 104 | 105 | 106 | 107 | `; 108 | } else if (rank.slice(0, 1) === RANK.S) { 109 | color = theme.S_RANK_BASE; 110 | rankColor = theme.S_RANK_TEXT; 111 | backgroundIcon = leafIcon(theme.LAUREL); 112 | gradationColor = ` 113 | 114 | 115 | 116 | `; 117 | } else if (rank.slice(0, 1) === RANK.A) { 118 | color = theme.A_RANK_BASE; 119 | rankColor = theme.A_RANK_TEXT; 120 | backgroundIcon = leafIcon(theme.LAUREL); 121 | gradationColor = ` 122 | 123 | 124 | 125 | `; 126 | } else if (rank === RANK.B) { 127 | color = theme.B_RANK_BASE; 128 | rankColor = theme.B_RANK_TEXT; 129 | gradationColor = ` 130 | 131 | 132 | 133 | `; 134 | } 135 | const icon = ` 136 | 137 | 138 | 139 | 140 | 141 | ${ 142 | rank.slice(0, 1) 143 | } 144 | `; 145 | const optionRankIcon = getSmallTrophyIcon(icon, color, rank.length - 1); 146 | return ` 147 | ${backgroundIcon} 148 | ${optionRankIcon} 149 | 150 | 151 | ${gradationColor} 152 | 153 | 154 | 155 | ${icon} 156 | 157 | `; 158 | }; 159 | -------------------------------------------------------------------------------- /src/trophy.ts: -------------------------------------------------------------------------------- 1 | import { getNextRankBar, getTrophyIcon } from "./icons.ts"; 2 | import { abridgeScore, CONSTANTS, RANK, RANK_ORDER } from "./utils.ts"; 3 | import { Theme } from "./theme.ts"; 4 | 5 | class RankCondition { 6 | constructor( 7 | readonly rank: RANK, 8 | readonly message: string, 9 | readonly requiredScore: number, 10 | ) {} 11 | } 12 | 13 | export class Trophy { 14 | rankCondition: RankCondition | null = null; 15 | rank: RANK = RANK.UNKNOWN; 16 | topMessage = "Unknown"; 17 | bottomMessage = "0"; 18 | title = ""; 19 | filterTitles: Array = []; 20 | hidden = false; 21 | constructor( 22 | private score: number, 23 | private rankConditions: Array, 24 | ) { 25 | this.bottomMessage = abridgeScore(score); 26 | this.setRank(); 27 | } 28 | setRank() { 29 | const sortedRankConditions = this.rankConditions.sort((a, b) => 30 | RANK_ORDER.indexOf(a.rank) - RANK_ORDER.indexOf(b.rank) 31 | ); 32 | // Set the rank that hit the first condition 33 | const rankCondition = sortedRankConditions.find((r) => 34 | this.score >= r.requiredScore 35 | ); 36 | if (rankCondition != null) { 37 | this.rank = rankCondition.rank; 38 | this.rankCondition = rankCondition; 39 | this.topMessage = rankCondition.message; 40 | } 41 | } 42 | private calculateNextRankPercentage() { 43 | if (this.rank === RANK.UNKNOWN) { 44 | return 0; 45 | } 46 | const nextRankIndex = RANK_ORDER.indexOf(this.rank) - 1; 47 | // When got the max rank 48 | if (nextRankIndex < 0 || this.rank === RANK.SSS) { 49 | return 1; 50 | } 51 | const nextRank = RANK_ORDER[nextRankIndex]; 52 | const nextRankCondition = this.rankConditions.find((r) => 53 | r.rank == nextRank 54 | ); 55 | const distance = nextRankCondition!.requiredScore - 56 | this.rankCondition!.requiredScore; 57 | const progress = this.score - this.rankCondition!.requiredScore; 58 | const result = progress / distance; 59 | return result; 60 | } 61 | render( 62 | theme: Theme, 63 | x = 0, 64 | y = 0, 65 | panelSize = CONSTANTS.DEFAULT_PANEL_SIZE, 66 | noBackground = CONSTANTS.DEFAULT_NO_BACKGROUND, 67 | noFrame = CONSTANTS.DEFAULT_NO_FRAME, 68 | ): string { 69 | const { BACKGROUND: PRIMARY, TITLE: SECONDARY, TEXT, NEXT_RANK_BAR } = 70 | theme; 71 | const nextRankBar = getNextRankBar( 72 | this.title, 73 | this.calculateNextRankPercentage(), 74 | NEXT_RANK_BAR, 75 | ); 76 | return ` 77 | 86 | 97 | ${getTrophyIcon(theme, this.rank)} 98 | ${this.title} 99 | ${this.topMessage} 100 | ${this.bottomMessage} 101 | ${nextRankBar} 102 | 103 | `; 104 | } 105 | } 106 | 107 | export class MultipleLangTrophy extends Trophy { 108 | constructor(score: number) { 109 | const rankConditions = [ 110 | new RankCondition( 111 | RANK.SECRET, 112 | "Rainbow Lang User", 113 | 10, 114 | ), 115 | ]; 116 | super(score, rankConditions); 117 | this.title = "MultiLanguage"; 118 | this.filterTitles = ["MultipleLang", "MultiLanguage"]; 119 | this.hidden = true; 120 | } 121 | } 122 | 123 | export class AllSuperRankTrophy extends Trophy { 124 | constructor(score: number) { 125 | const rankConditions = [ 126 | new RankCondition( 127 | RANK.SECRET, 128 | "S Rank Hacker", 129 | 1, 130 | ), 131 | ]; 132 | super(score, rankConditions); 133 | this.title = "AllSuperRank"; 134 | this.filterTitles = ["AllSuperRank"]; 135 | this.bottomMessage = "All S Rank"; 136 | this.hidden = true; 137 | } 138 | } 139 | export class Joined2020Trophy extends Trophy { 140 | constructor(score: number) { 141 | const rankConditions = [ 142 | new RankCondition( 143 | RANK.SECRET, 144 | "Everything started...", 145 | 1, 146 | ), 147 | ]; 148 | super(score, rankConditions); 149 | this.title = "Joined2020"; 150 | this.filterTitles = ["Joined2020"]; 151 | this.bottomMessage = "Joined 2020"; 152 | this.hidden = true; 153 | } 154 | } 155 | export class AncientAccountTrophy extends Trophy { 156 | constructor(score: number) { 157 | const rankConditions = [ 158 | new RankCondition( 159 | RANK.SECRET, 160 | "Ancient User", 161 | 1, 162 | ), 163 | ]; 164 | super(score, rankConditions); 165 | this.title = "AncientUser"; 166 | this.filterTitles = ["AncientUser"]; 167 | this.bottomMessage = "Before 2010"; 168 | this.hidden = true; 169 | } 170 | } 171 | export class LongTimeAccountTrophy extends Trophy { 172 | constructor(score: number) { 173 | const rankConditions = [ 174 | new RankCondition( 175 | RANK.SECRET, 176 | "Village Elder", 177 | 10, 178 | ), 179 | ]; 180 | super(score, rankConditions); 181 | this.title = "LongTimeUser"; 182 | this.filterTitles = ["LongTimeUser"]; 183 | this.hidden = true; 184 | } 185 | } 186 | export class MultipleOrganizationsTrophy extends Trophy { 187 | constructor(score: number) { 188 | const rankConditions = [ 189 | new RankCondition( 190 | RANK.SECRET, 191 | // or if this doesn't render well: "Factorum" 192 | "Jack of all Trades", 193 | 3, 194 | ), 195 | ]; 196 | super(score, rankConditions); 197 | this.title = "Organizations"; 198 | this.filterTitles = ["Organizations", "Orgs", "Teams"]; 199 | this.hidden = true; 200 | } 201 | } 202 | 203 | export class OGAccountTrophy extends Trophy { 204 | constructor(score: number) { 205 | const rankConditions = [ 206 | new RankCondition( 207 | RANK.SECRET, 208 | "OG User", 209 | 1, 210 | ), 211 | ]; 212 | super(score, rankConditions); 213 | this.title = "OGUser"; 214 | this.filterTitles = ["OGUser"]; 215 | this.bottomMessage = "Joined 2008"; 216 | this.hidden = true; 217 | } 218 | } 219 | 220 | export class TotalReviewsTrophy extends Trophy { 221 | constructor(score: number) { 222 | const rankConditions = [ 223 | new RankCondition( 224 | RANK.SSS, 225 | "God Reviewer", 226 | 70, 227 | ), 228 | new RankCondition( 229 | RANK.SS, 230 | "Deep Reviewer", 231 | 57, 232 | ), 233 | new RankCondition( 234 | RANK.S, 235 | "Super Reviewer", 236 | 45, 237 | ), 238 | new RankCondition( 239 | RANK.AAA, 240 | "Ultra Reviewer", 241 | 30, 242 | ), 243 | new RankCondition( 244 | RANK.AA, 245 | "Hyper Reviewer", 246 | 20, 247 | ), 248 | new RankCondition( 249 | RANK.A, 250 | "Active Reviewer", 251 | 8, 252 | ), 253 | new RankCondition( 254 | RANK.B, 255 | "Intermediate Reviewer", 256 | 3, 257 | ), 258 | new RankCondition( 259 | RANK.C, 260 | "New Reviewer", 261 | 1, 262 | ), 263 | ]; 264 | super(score, rankConditions); 265 | this.title = "Reviews"; 266 | this.filterTitles = ["Review", "Reviews"]; 267 | } 268 | } 269 | 270 | export class AccountDurationTrophy extends Trophy { 271 | constructor(score: number) { 272 | const rankConditions = [ 273 | new RankCondition( 274 | RANK.SSS, 275 | "Seasoned Veteran", 276 | 70, // 20 years 277 | ), 278 | new RankCondition( 279 | RANK.SS, 280 | "Grandmaster", 281 | 55, // 15 years 282 | ), 283 | new RankCondition( 284 | RANK.S, 285 | "Master Dev", 286 | 40, // 10 years 287 | ), 288 | new RankCondition( 289 | RANK.AAA, 290 | "Expert Dev", 291 | 28, // 7.5 years 292 | ), 293 | new RankCondition( 294 | RANK.AA, 295 | "Experienced Dev", 296 | 18, // 5 years 297 | ), 298 | new RankCondition( 299 | RANK.A, 300 | "Intermediate Dev", 301 | 11, // 3 years 302 | ), 303 | new RankCondition( 304 | RANK.B, 305 | "Junior Dev", 306 | 6, // 1.5 years 307 | ), 308 | new RankCondition( 309 | RANK.C, 310 | "Newbie", 311 | 2, // 0.5 year 312 | ), 313 | ]; 314 | super(score, rankConditions); 315 | this.title = "Experience"; 316 | this.filterTitles = ["Experience", "Duration", "Since"]; 317 | // this.hidden = true; 318 | } 319 | } 320 | 321 | export class TotalStarTrophy extends Trophy { 322 | constructor(score: number) { 323 | const rankConditions = [ 324 | new RankCondition( 325 | RANK.SSS, 326 | "Super Stargazer", 327 | 2000, 328 | ), 329 | new RankCondition( 330 | RANK.SS, 331 | "High Stargazer", 332 | 700, 333 | ), 334 | new RankCondition( 335 | RANK.S, 336 | "Stargazer", 337 | 200, 338 | ), 339 | new RankCondition( 340 | RANK.AAA, 341 | "Super Star", 342 | 100, 343 | ), 344 | new RankCondition( 345 | RANK.AA, 346 | "High Star", 347 | 50, 348 | ), 349 | new RankCondition( 350 | RANK.A, 351 | "You are a Star", 352 | 30, 353 | ), 354 | new RankCondition( 355 | RANK.B, 356 | "Middle Star", 357 | 10, 358 | ), 359 | new RankCondition( 360 | RANK.C, 361 | "First Star", 362 | 1, 363 | ), 364 | ]; 365 | super(score, rankConditions); 366 | this.title = "Stars"; 367 | this.filterTitles = ["Star", "Stars"]; 368 | } 369 | } 370 | 371 | export class TotalCommitTrophy extends Trophy { 372 | constructor(score: number) { 373 | const rankConditions = [ 374 | new RankCondition( 375 | RANK.SSS, 376 | "God Committer", 377 | 4000, 378 | ), 379 | new RankCondition( 380 | RANK.SS, 381 | "Deep Committer", 382 | 2000, 383 | ), 384 | new RankCondition( 385 | RANK.S, 386 | "Super Committer", 387 | 1000, 388 | ), 389 | new RankCondition( 390 | RANK.AAA, 391 | "Ultra Committer", 392 | 500, 393 | ), 394 | new RankCondition( 395 | RANK.AA, 396 | "Hyper Committer", 397 | 200, 398 | ), 399 | new RankCondition( 400 | RANK.A, 401 | "High Committer", 402 | 100, 403 | ), 404 | new RankCondition( 405 | RANK.B, 406 | "Middle Committer", 407 | 10, 408 | ), 409 | new RankCondition( 410 | RANK.C, 411 | "First Commit", 412 | 1, 413 | ), 414 | ]; 415 | super(score, rankConditions); 416 | this.title = "Commits"; 417 | this.filterTitles = ["Commit", "Commits"]; 418 | } 419 | } 420 | 421 | export class TotalFollowerTrophy extends Trophy { 422 | constructor(score: number) { 423 | const rankConditions = [ 424 | new RankCondition( 425 | RANK.SSS, 426 | "Super Celebrity", 427 | 1000, 428 | ), 429 | new RankCondition( 430 | RANK.SS, 431 | "Ultra Celebrity", 432 | 400, 433 | ), 434 | new RankCondition( 435 | RANK.S, 436 | "Hyper Celebrity", 437 | 200, 438 | ), 439 | new RankCondition( 440 | RANK.AAA, 441 | "Famous User", 442 | 100, 443 | ), 444 | new RankCondition( 445 | RANK.AA, 446 | "Active User", 447 | 50, 448 | ), 449 | new RankCondition( 450 | RANK.A, 451 | "Dynamic User", 452 | 20, 453 | ), 454 | new RankCondition( 455 | RANK.B, 456 | "Many Friends", 457 | 10, 458 | ), 459 | new RankCondition( 460 | RANK.C, 461 | "First Friend", 462 | 1, 463 | ), 464 | ]; 465 | super(score, rankConditions); 466 | this.title = "Followers"; 467 | this.filterTitles = ["Follower", "Followers"]; 468 | } 469 | } 470 | 471 | export class TotalIssueTrophy extends Trophy { 472 | constructor(score: number) { 473 | const rankConditions = [ 474 | new RankCondition( 475 | RANK.SSS, 476 | "God Issuer", 477 | 1000, 478 | ), 479 | new RankCondition( 480 | RANK.SS, 481 | "Deep Issuer", 482 | 500, 483 | ), 484 | new RankCondition( 485 | RANK.S, 486 | "Super Issuer", 487 | 200, 488 | ), 489 | new RankCondition( 490 | RANK.AAA, 491 | "Ultra Issuer", 492 | 100, 493 | ), 494 | new RankCondition( 495 | RANK.AA, 496 | "Hyper Issuer", 497 | 50, 498 | ), 499 | new RankCondition( 500 | RANK.A, 501 | "High Issuer", 502 | 20, 503 | ), 504 | new RankCondition( 505 | RANK.B, 506 | "Middle Issuer", 507 | 10, 508 | ), 509 | new RankCondition( 510 | RANK.C, 511 | "First Issue", 512 | 1, 513 | ), 514 | ]; 515 | super(score, rankConditions); 516 | this.title = "Issues"; 517 | this.filterTitles = ["Issue", "Issues"]; 518 | } 519 | } 520 | 521 | export class TotalPullRequestTrophy extends Trophy { 522 | constructor(score: number) { 523 | const rankConditions = [ 524 | new RankCondition( 525 | RANK.SSS, 526 | "God Puller", 527 | 1000, 528 | ), 529 | new RankCondition( 530 | RANK.SS, 531 | "Deep Puller", 532 | 500, 533 | ), 534 | new RankCondition( 535 | RANK.S, 536 | "Super Puller", 537 | 200, 538 | ), 539 | new RankCondition( 540 | RANK.AAA, 541 | "Ultra Puller", 542 | 100, 543 | ), 544 | new RankCondition( 545 | RANK.AA, 546 | "Hyper Puller", 547 | 50, 548 | ), 549 | new RankCondition( 550 | RANK.A, 551 | "High Puller", 552 | 20, 553 | ), 554 | new RankCondition( 555 | RANK.B, 556 | "Middle Puller", 557 | 10, 558 | ), 559 | new RankCondition( 560 | RANK.C, 561 | "First Pull", 562 | 1, 563 | ), 564 | ]; 565 | super(score, rankConditions); 566 | this.title = "PullRequest"; 567 | this.filterTitles = ["PR", "PullRequest", "Pulls", "Puller"]; 568 | } 569 | } 570 | 571 | export class TotalRepositoryTrophy extends Trophy { 572 | constructor(score: number) { 573 | const rankConditions = [ 574 | new RankCondition( 575 | RANK.SSS, 576 | "God Repo Creator", 577 | 100, 578 | ), 579 | new RankCondition( 580 | RANK.SS, 581 | "Deep Repo Creator", 582 | 90, 583 | ), 584 | new RankCondition( 585 | RANK.S, 586 | "Super Repo Creator", 587 | 80, 588 | ), 589 | new RankCondition( 590 | RANK.AAA, 591 | "Ultra Repo Creator", 592 | 50, 593 | ), 594 | new RankCondition( 595 | RANK.AA, 596 | "Hyper Repo Creator", 597 | 30, 598 | ), 599 | new RankCondition( 600 | RANK.A, 601 | "High Repo Creator", 602 | 20, 603 | ), 604 | new RankCondition( 605 | RANK.B, 606 | "Middle Repo Creator", 607 | 10, 608 | ), 609 | new RankCondition( 610 | RANK.C, 611 | "First Repository", 612 | 1, 613 | ), 614 | ]; 615 | super(score, rankConditions); 616 | this.title = "Repositories"; 617 | this.filterTitles = ["Repo", "Repository", "Repositories"]; 618 | } 619 | } 620 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

GitHub Profile Trophy

4 |

🏆 Add dynamically generated GitHub Stat Trophies on your readme

5 |

6 |

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

20 |

21 | 22 | 23 | 24 | 25 |

26 |

27 | You can use this service for free. I'm looking for sponsors to help us keep up with this service❤️ 28 |

29 |

30 | 31 | 32 | 33 |

34 | 35 | # Quick Start 36 | 37 | Add the following code to your readme. When pasting the code into your profile's 38 | readme, change the `?username=` value to your GitHub's username. 39 | 40 | ``` 41 | [![trophy](https://github-profile-trophy.vercel.app/?username=ryo-ma)](https://github.com/ryo-ma/github-profile-trophy) 42 | ``` 43 | 44 |

45 | 46 |

47 | 48 | ## Use theme 49 | 50 | Add optional parameter of the theme. 51 | 52 | ``` 53 | [![trophy](https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=onedark)](https://github.com/ryo-ma/github-profile-trophy) 54 | ``` 55 | 56 |

57 | 58 |

59 | 60 | **[More detail](#apply-theme)** 61 | 62 | # About Rank 63 | 64 | Ranks are `SSS` `SS` `S` `AAA` `AA` `A` `B` `C` `UNKNOWN` `SECRET`. 65 | 66 | | Rank | Description | 67 | | ---------- | ------------------------------------------------------------------------------------------ | 68 | | SSS, SS, S | You are at a hard to reach rank. You can brag. | 69 | | AAA, AA, A | You will reach this rank if you do your best. Let's aim here first. | 70 | | B, C | You are currently making good progress. Let's aim a bit higher. | 71 | | UNKNOWN | You have not taken action yet. Let's act first. | 72 | | SECRET | This rank is very rare. The trophy will not be displayed until certain conditions are met. | 73 | 74 | ## Secret Rank 75 | 76 | The acquisition condition is secret, but you can know the condition by reading 77 | this code. 78 | 79 |

80 | 81 |

82 | 83 | There are only a few secret trophies. Therefore, if you come up with interesting 84 | conditions, I will consider adding a trophy. I am waiting for contributions. 85 | 86 | # About Display details 87 | 88 |

89 | 90 |

91 | 92 | 1. Title name of aggregation target. 93 | 2. Current Rank. 94 | 3. Title according to rank. 95 | 4. Target aggregation result. 96 | 5. Next Rank Bar. The road from the current rank to the next rank. 97 | 98 | # Optional Request Parameters 99 | 100 | - [title](#filter-by-titles) 101 | - [rank](#filter-by-ranks) 102 | - [column](#specify-the-maximum-row--column-size) 103 | - [row](#specify-the-maximum-row--column-size) 104 | - [theme](#apply-theme) 105 | - [margin-w](#margin-width) 106 | - [margin-h](#margin-height) 107 | - [no-bg](#transparent-background) 108 | - [no-frame](#hide-frames) 109 | 110 | ## Filter by titles 111 | 112 | You can filter the display by specifying the titles of trophy. 113 | 114 | ``` 115 | https://github-profile-trophy.vercel.app/?username=ryo-ma&title=Followers 116 | ``` 117 | 118 |

119 | 120 |

121 | 122 | If you want to specify multiple titles. 123 | 124 | ``` 125 | https://github-profile-trophy.vercel.app/?username=ryo-ma&title=Stars,Followers 126 | ``` 127 | 128 | ## Filter by ranks 129 | 130 | You can filter the display by specifying the ranks.\ 131 | `Available values: SECRET SSS SS S AAA AA A B C` 132 | 133 | ``` 134 | https://github-profile-trophy.vercel.app/?username=ryo-ma&rank=S 135 | ``` 136 | 137 |

138 | 139 |

140 | 141 | If you want to specify multiple ranks. 142 | 143 | ``` 144 | https://github-profile-trophy.vercel.app/?username=ryo-ma&rank=S,AAA 145 | ``` 146 | 147 | You can also exclude ranks. 148 | 149 | ``` 150 | https://github-profile-trophy.vercel.app/?username=ryo-ma&rank=-C,-B 151 | ``` 152 | 153 | ## Specify the maximum row & column size 154 | 155 | You can specify the maximum row and column size.\ 156 | Trophy will be hidden if it exceeds the range of both row and column. 157 | 158 | `Available value: number type`\ 159 | `Default: column=6 row=3` 160 | 161 | Restrict only row 162 | 163 | ``` 164 | https://github-profile-trophy.vercel.app/?username=ryo-ma&row=2 165 | ``` 166 | 167 | Restrict only column 168 | 169 | ``` 170 | https://github-profile-trophy.vercel.app/?username=ryo-ma&column=2 171 | ``` 172 | 173 | Restrict row & column 174 | 175 | ``` 176 | https://github-profile-trophy.vercel.app/?username=ryo-ma&row=2&column=3 177 | ``` 178 | 179 |

180 | 181 |

182 | 183 | Adaptive column 184 | 185 | ``` 186 | https://github-profile-trophy.vercel.app/?username=ryo-ma&column=-1 187 | ``` 188 | 189 | You can set `columns` to `-1` to adapt the width to the number of trophies, the 190 | parameter `row` will be ignored. 191 | 192 | ## Apply theme 193 | 194 | Available themes. 195 | 196 | | theme | 197 | | --------------------------- | 198 | | [flat](#flat) | 199 | | [onedark](#onedark) | 200 | | [gruvbox](#gruvbox) | 201 | | [dracula](#dracula) | 202 | | [monokai](#monokai) | 203 | | [chalk](#chalk) | 204 | | [nord](#nord) | 205 | | [alduin](#alduin) | 206 | | [darkhub](#darkhub) | 207 | | [juicyfresh](#juicyfresh) | 208 | | [buddhism](#buddhism) | 209 | | [oldie](#oldie) | 210 | | [radical](#radical) | 211 | | [onestar](#onestar) | 212 | | [discord](#discord) | 213 | | [algolia](#algolia) | 214 | | [gitdimmed](#gitdimmed) | 215 | | [tokyonight](#tokyonight) | 216 | | [matrix](#matrix) | 217 | | [apprentice](#apprentice) | 218 | | [dark_dimmed](#dark_dimmed) | 219 | | [dark_lover](#dark_lover) | 220 | | [kimbie_dark](#kimbie_dark) | 221 | 222 | ### flat 223 | 224 | ``` 225 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=flat 226 | ``` 227 | 228 |

229 | 230 |

231 | 232 | ### onedark 233 | 234 | ``` 235 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=onedark 236 | ``` 237 | 238 |

239 | 240 |

241 | 242 | ### gruvbox 243 | 244 | ``` 245 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=gruvbox 246 | ``` 247 | 248 |

249 | 250 |

251 | 252 | ### dracula 253 | 254 | ``` 255 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=dracula 256 | ``` 257 | 258 |

259 | 260 |

261 | 262 | ### monokai 263 | 264 | ``` 265 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=monokai 266 | ``` 267 | 268 |

269 | 270 |

271 | 272 | ### chalk 273 | 274 | ``` 275 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=chalk 276 | ``` 277 | 278 |

279 | 280 |

281 | 282 | ### nord 283 | 284 | ``` 285 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=nord 286 | ``` 287 | 288 |

289 | 290 |

291 | 292 | ### alduin 293 | 294 | ``` 295 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=alduin 296 | ``` 297 | 298 |

299 | 300 |

301 | 302 | ### darkhub 303 | 304 | ``` 305 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=darkhub 306 | ``` 307 | 308 |

309 | 310 |

311 | 312 | ### juicyfresh 313 | 314 | ``` 315 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=juicyfresh 316 | ``` 317 | 318 |

319 | 320 |

321 | 322 | ### buddhism 323 | 324 | ``` 325 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=buddhism 326 | ``` 327 | 328 |

329 | 330 |

331 | 332 | ### oldie 333 | 334 | ``` 335 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=oldie 336 | ``` 337 | 338 |

339 | 340 |

341 | 342 | ### radical 343 | 344 | ``` 345 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=radical 346 | ``` 347 | 348 |

349 | 350 |

351 | 352 | ### onestar 353 | 354 | ``` 355 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=onestar 356 | ``` 357 | 358 |

359 | 360 |

361 | 362 | ### discord 363 | 364 | ``` 365 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=discord 366 | ``` 367 | 368 |

369 | 370 |

371 | 372 | ### algolia 373 | 374 | ``` 375 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=algolia 376 | ``` 377 | 378 |

379 | 380 |

381 | 382 | ### gitdimmed 383 | 384 | ``` 385 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=gitdimmed 386 | ``` 387 | 388 |

389 | 390 |

391 | 392 | ### tokyonight 393 | 394 | ``` 395 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=tokyonight 396 | ``` 397 | 398 |

399 | 400 |

401 | 402 | ### matrix 403 | 404 | ``` 405 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=matrix 406 | ``` 407 | 408 |

409 | 410 |

411 | 412 | ### apprentice 413 | 414 | ``` 415 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=apprentice 416 | ``` 417 | 418 |

419 | 420 |

421 | 422 | ### dark_dimmed 423 | 424 | ``` 425 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=dark_dimmed 426 | ``` 427 | 428 |

429 | 430 |

431 | 432 | ### dark_lover 433 | 434 | ``` 435 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=dark_lover 436 | ``` 437 | 438 |

439 | 440 |

441 | 442 | ### kimbie_dark 443 | 444 | ``` 445 | https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=kimbie_dark 446 | ``` 447 | 448 |

449 | 450 |

451 | 452 | ## Margin Width 453 | 454 | You can put a margin in the width between trophies.\ 455 | `Available value: number type`\ 456 | `Default: margin-w=0` 457 | 458 | ``` 459 | https://github-profile-trophy.vercel.app/?username=ryo-ma&margin-w=15 460 | ``` 461 | 462 |

463 | 464 |

465 | 466 | ## Margin Height 467 | 468 | You can put a margin in the height between trophies.\ 469 | `Available value: number type`\ 470 | `Default: margin-h=0` 471 | 472 | ``` 473 | https://github-profile-trophy.vercel.app/?username=ryo-ma&margin-h=15 474 | ``` 475 | 476 | ## Example layout 477 | 478 | ``` 479 | https://github-profile-trophy.vercel.app/?username=ryo-ma&column=3&margin-w=15&margin-h=15 480 | ``` 481 | 482 |

483 | 484 |

485 | 486 | ## Transparent background 487 | 488 | You can turn the background transparent.\ 489 | `Available value: boolean type (true or false)`\ 490 | `Default: no-bg=false` 491 | 492 | ``` 493 | https://github-profile-trophy.vercel.app/?username=ryo-ma&no-bg=true 494 | ``` 495 | 496 |

497 | 498 |

499 | 500 | ## Hide frames 501 | 502 | You can hide the frames around the trophies.\ 503 | `Available value: boolean type (true or false)`\ 504 | `Default: no-frame=false` 505 | 506 | ``` 507 | https://github-profile-trophy.vercel.app/?username=ryo-ma&no-frame=true 508 | ``` 509 | 510 |

511 | 512 |

513 | 514 | # Contribution Guide 515 | 516 | Check [CONTRIBUTING.md](./CONTRIBUTING.md) for more details. 517 | 518 | # License 519 | 520 | This product is licensed under the 521 | [MIT License](https://github.com/ryo-ma/github-profile-trophy/blob/master/LICENSE). 522 | -------------------------------------------------------------------------------- /src/theme.ts: -------------------------------------------------------------------------------- 1 | export const COLORS: { [name: string]: Theme } = { 2 | default: { 3 | BACKGROUND: "#FFF", 4 | TITLE: "#000", 5 | ICON_CIRCLE: "#FFF", 6 | TEXT: "#666", 7 | LAUREL: "#009366", 8 | SECRET_RANK_1: "red", 9 | SECRET_RANK_2: "fuchsia", 10 | SECRET_RANK_3: "blue", 11 | SECRET_RANK_TEXT: "fuchsia", 12 | NEXT_RANK_BAR: "#0366d6", 13 | S_RANK_BASE: "#FAD200", 14 | S_RANK_SHADOW: "#C8A090", 15 | S_RANK_TEXT: "#886000", 16 | A_RANK_BASE: "#B0B0B0", 17 | A_RANK_SHADOW: "#9090C0", 18 | A_RANK_TEXT: "#505050", 19 | B_RANK_BASE: "#A18D66", 20 | B_RANK_SHADOW: "#816D96", 21 | B_RANK_TEXT: "#412D06", 22 | DEFAULT_RANK_BASE: "#777", 23 | DEFAULT_RANK_SHADOW: "#333", 24 | DEFAULT_RANK_TEXT: "#333", 25 | }, 26 | dracula: { 27 | BACKGROUND: "#282a36", 28 | TITLE: "#ff79c6", 29 | ICON_CIRCLE: "#f8f8f2", 30 | TEXT: "#f8f8f2", 31 | LAUREL: "#50fa7b", 32 | SECRET_RANK_1: "#ff5555", 33 | SECRET_RANK_2: "#ff79c6", 34 | SECRET_RANK_3: "#bd93f9", 35 | SECRET_RANK_TEXT: "#bd93f9", 36 | NEXT_RANK_BAR: "#ff79c6", 37 | S_RANK_BASE: "#ffb86c", 38 | S_RANK_SHADOW: "#ffb86c", 39 | S_RANK_TEXT: "#6272a4", 40 | A_RANK_BASE: "#8be9fd", 41 | A_RANK_SHADOW: "#8be9fd", 42 | A_RANK_TEXT: "#6272a4", 43 | B_RANK_BASE: "#ff5555", 44 | B_RANK_SHADOW: "#ff5555", 45 | B_RANK_TEXT: "#6272a4", 46 | DEFAULT_RANK_BASE: "#6272a4", 47 | DEFAULT_RANK_SHADOW: "#6272a4", 48 | DEFAULT_RANK_TEXT: "#6272a4", 49 | }, 50 | flat: { 51 | BACKGROUND: "#FFF", 52 | TITLE: "#000", 53 | ICON_CIRCLE: "#FFF", 54 | TEXT: "#666", 55 | LAUREL: "#009366", 56 | SECRET_RANK_1: "red", 57 | SECRET_RANK_2: "fuchsia", 58 | SECRET_RANK_3: "blue", 59 | SECRET_RANK_TEXT: "fuchsia", 60 | NEXT_RANK_BAR: "#0366d6", 61 | S_RANK_BASE: "#eac200", 62 | S_RANK_SHADOW: "#eac200", 63 | S_RANK_TEXT: "#886000", 64 | A_RANK_BASE: "#B0B0B0", 65 | A_RANK_SHADOW: "#B0B0B0", 66 | A_RANK_TEXT: "#505050", 67 | B_RANK_BASE: "#A18D66", 68 | B_RANK_SHADOW: "#A18D66", 69 | B_RANK_TEXT: "#412D06", 70 | DEFAULT_RANK_BASE: "#777", 71 | DEFAULT_RANK_SHADOW: "#777", 72 | DEFAULT_RANK_TEXT: "#333", 73 | }, 74 | onedark: { 75 | BACKGROUND: "#282c34", 76 | TITLE: "#e5c07b", 77 | ICON_CIRCLE: "#FFF", 78 | TEXT: "#e06c75", 79 | LAUREL: "#98c379", 80 | SECRET_RANK_1: "#e06c75", 81 | SECRET_RANK_2: "#c678dd", 82 | SECRET_RANK_3: "#61afef", 83 | SECRET_RANK_TEXT: "#c678dd", 84 | NEXT_RANK_BAR: "#e5c07b", 85 | S_RANK_BASE: "#e5c07b", 86 | S_RANK_SHADOW: "#e5c07b", 87 | S_RANK_TEXT: "#282c34", 88 | A_RANK_BASE: "#56b6c2", 89 | A_RANK_SHADOW: "#56b6c2", 90 | A_RANK_TEXT: "#282c34", 91 | B_RANK_BASE: "#c678dd", 92 | B_RANK_SHADOW: "#c678dd", 93 | B_RANK_TEXT: "#282c34", 94 | DEFAULT_RANK_BASE: "#abb2bf", 95 | DEFAULT_RANK_SHADOW: "#abb2bf", 96 | DEFAULT_RANK_TEXT: "#282c34", 97 | }, 98 | gruvbox: { 99 | BACKGROUND: "#282828", 100 | TITLE: "#ebdbb2", 101 | ICON_CIRCLE: "#ebdbb2", 102 | TEXT: "#98971a", 103 | LAUREL: "#689d6a", 104 | SECRET_RANK_1: "#fb4934", 105 | SECRET_RANK_2: "#d3869b", 106 | SECRET_RANK_3: "#458588", 107 | SECRET_RANK_TEXT: "#b16286", 108 | NEXT_RANK_BAR: "#fabd26", 109 | S_RANK_BASE: "#fabd2f", 110 | S_RANK_SHADOW: "#fabd2f", 111 | S_RANK_TEXT: "#322301", 112 | A_RANK_BASE: "#83a598", 113 | A_RANK_SHADOW: "#83a598", 114 | A_RANK_TEXT: "#151e1a", 115 | B_RANK_BASE: "#d65d0e", 116 | B_RANK_SHADOW: "#d65d0e", 117 | B_RANK_TEXT: "#301503", 118 | DEFAULT_RANK_BASE: "#928374", 119 | DEFAULT_RANK_SHADOW: "#928374", 120 | DEFAULT_RANK_TEXT: "#282828", 121 | }, 122 | monokai: { 123 | BACKGROUND: "#272822", 124 | TITLE: "#f92672", 125 | ICON_CIRCLE: "#fff", 126 | TEXT: "#fff", 127 | LAUREL: "#a6e22e", 128 | SECRET_RANK_1: "#f92672", 129 | SECRET_RANK_2: "#ae81ff", 130 | SECRET_RANK_3: "#66d9ef", 131 | SECRET_RANK_TEXT: "#b16286", 132 | NEXT_RANK_BAR: "#f92672", 133 | S_RANK_BASE: "#e6db74", 134 | S_RANK_SHADOW: "#e6db74", 135 | S_RANK_TEXT: "#272822", 136 | A_RANK_BASE: "#66d9ef", 137 | A_RANK_SHADOW: "#66d9ef", 138 | A_RANK_TEXT: "#272822", 139 | B_RANK_BASE: "#fd971f", 140 | B_RANK_SHADOW: "#fd971f", 141 | B_RANK_TEXT: "#272822", 142 | DEFAULT_RANK_BASE: "#75715e", 143 | DEFAULT_RANK_SHADOW: "#75715e", 144 | DEFAULT_RANK_TEXT: "#282828", 145 | }, 146 | nord: { 147 | BACKGROUND: "#2E3440", 148 | TITLE: "#81A1C1", 149 | ICON_CIRCLE: "#D8DEE9", 150 | TEXT: "#ECEFF4", 151 | LAUREL: "#A3BE8C", 152 | SECRET_RANK_1: "#BF616A", 153 | SECRET_RANK_2: "#B48EAD", 154 | SECRET_RANK_3: "#81A1C1", 155 | SECRET_RANK_TEXT: "#B48EAD", 156 | NEXT_RANK_BAR: "#81A1C1", 157 | S_RANK_BASE: "#EBCB8B", 158 | S_RANK_SHADOW: "#EBCB8B", 159 | S_RANK_TEXT: "#3B4252", 160 | A_RANK_BASE: "#8FBCBB", 161 | A_RANK_SHADOW: "#8FBCBB", 162 | A_RANK_TEXT: "#3B4252", 163 | B_RANK_BASE: "#D08770", 164 | B_RANK_SHADOW: "#D08770", 165 | B_RANK_TEXT: "#3B4252", 166 | DEFAULT_RANK_BASE: "#5E81AC", 167 | DEFAULT_RANK_SHADOW: "#5E81AC", 168 | DEFAULT_RANK_TEXT: "#3B4252", 169 | }, 170 | discord: { 171 | BACKGROUND: "#23272A", 172 | TITLE: "#7289DA", 173 | ICON_CIRCLE: "#FFFFFF", 174 | TEXT: "#FFFFFF", 175 | LAUREL: "#57F287", 176 | SECRET_RANK_1: "#ED4245", 177 | SECRET_RANK_2: "#57F287", 178 | SECRET_RANK_3: "#5865F2", 179 | SECRET_RANK_TEXT: "#000000", 180 | NEXT_RANK_BAR: "#5865F2", 181 | S_RANK_BASE: "#FEE75C", 182 | S_RANK_SHADOW: "#FEE75C", 183 | S_RANK_TEXT: "#000000", 184 | A_RANK_BASE: "#EB459E", 185 | A_RANK_SHADOW: "#ED4245", 186 | A_RANK_TEXT: "#000000", 187 | B_RANK_BASE: "#ED4245", 188 | B_RANK_SHADOW: "#ED4245", 189 | B_RANK_TEXT: "#000000", 190 | DEFAULT_RANK_BASE: "#5865F2", 191 | DEFAULT_RANK_SHADOW: "#5865F2", 192 | DEFAULT_RANK_TEXT: "#000000", 193 | }, 194 | chalk: { 195 | BACKGROUND: "#2d2d2d", 196 | TITLE: "#fed37e", 197 | ICON_CIRCLE: "#e4e4e4", 198 | TEXT: "#d4d4d4", 199 | LAUREL: "#a9d3ab", 200 | SECRET_RANK_1: "#f58e8e", 201 | SECRET_RANK_2: "#d6add5", 202 | SECRET_RANK_3: "#66d9ef", 203 | SECRET_RANK_TEXT: "#f58e8e", 204 | NEXT_RANK_BAR: "#7aabd4", 205 | S_RANK_BASE: "#fed37e", 206 | S_RANK_SHADOW: "#fed37e", 207 | S_RANK_TEXT: "#2d2d2d", 208 | A_RANK_BASE: "#79D4D5", 209 | A_RANK_SHADOW: "#79D4D5", 210 | A_RANK_TEXT: "#2d2d2d", 211 | B_RANK_BASE: "#f58e8e", 212 | B_RANK_SHADOW: "#f58e8e", 213 | B_RANK_TEXT: "#2d2d2d", 214 | DEFAULT_RANK_BASE: "#75715e", 215 | DEFAULT_RANK_SHADOW: "#75715e", 216 | DEFAULT_RANK_TEXT: "#2d2d2d", 217 | }, 218 | alduin: { 219 | BACKGROUND: "#1c1c1c", 220 | TITLE: "#dfd7af", 221 | ICON_CIRCLE: "#e3e3e3", 222 | TEXT: "#dfd7af", 223 | LAUREL: "#a9d3ab", 224 | SECRET_RANK_1: "#f58e8e", 225 | SECRET_RANK_2: "#d6add5", 226 | SECRET_RANK_3: "#66d9ef", 227 | SECRET_RANK_TEXT: "#f58e8e", 228 | NEXT_RANK_BAR: "#dfd7af", 229 | S_RANK_BASE: "#fed37e", 230 | S_RANK_SHADOW: "#fed37e", 231 | S_RANK_TEXT: "#2d2d2d", 232 | A_RANK_BASE: "#79D4D5", 233 | A_RANK_SHADOW: "#79D4D5", 234 | A_RANK_TEXT: "#2d2d2d", 235 | B_RANK_BASE: "#f58e8e", 236 | B_RANK_SHADOW: "#f58e8e", 237 | B_RANK_TEXT: "#2d2d2d", 238 | DEFAULT_RANK_BASE: "#75715e", 239 | DEFAULT_RANK_SHADOW: "#75715e", 240 | DEFAULT_RANK_TEXT: "#2d2d2d", 241 | }, 242 | darkhub: { 243 | BACKGROUND: "#0d1117", 244 | TITLE: "#c9d1d9", 245 | ICON_CIRCLE: "#f0f6fb", 246 | TEXT: "#8b949e", 247 | LAUREL: "#178600", 248 | SECRET_RANK_1: "#ff5555", 249 | SECRET_RANK_2: "#ff79c6", 250 | SECRET_RANK_3: "#388bfd", 251 | SECRET_RANK_TEXT: "#ff79c6", 252 | NEXT_RANK_BAR: "#ff79c6", 253 | S_RANK_BASE: "#ffb86c", 254 | S_RANK_SHADOW: "#ffb86c", 255 | S_RANK_TEXT: "#0d1117", 256 | A_RANK_BASE: "#8be9fd", 257 | A_RANK_SHADOW: "#8be9fd", 258 | A_RANK_TEXT: "#0d1117", 259 | B_RANK_BASE: "#ff5555", 260 | B_RANK_SHADOW: "#ff5555", 261 | B_RANK_TEXT: "#0d1117", 262 | DEFAULT_RANK_BASE: "#6272a4", 263 | DEFAULT_RANK_SHADOW: "#6272a4", 264 | DEFAULT_RANK_TEXT: "#0d1117", 265 | }, 266 | juicyfresh: { 267 | BACKGROUND: "#0d0c15", 268 | TITLE: "#f7d745", 269 | ICON_CIRCLE: "#FFF", 270 | TEXT: "#b2d76c", 271 | LAUREL: "#8bb071", 272 | SECRET_RANK_1: "#a8d937", 273 | SECRET_RANK_2: "#f7e662", 274 | SECRET_RANK_3: "#4d9b1c", 275 | SECRET_RANK_TEXT: "#ff5700", 276 | NEXT_RANK_BAR: "#6562af", 277 | S_RANK_BASE: "#f7d644", 278 | S_RANK_SHADOW: "#f69e44", 279 | S_RANK_TEXT: "#ff5700", 280 | A_RANK_BASE: "#f69e44", 281 | A_RANK_SHADOW: "#f46d5a", 282 | A_RANK_TEXT: "#ff5700", 283 | B_RANK_BASE: "#f46d5a", 284 | B_RANK_SHADOW: "#f73155", 285 | B_RANK_TEXT: "#ff5700", 286 | DEFAULT_RANK_BASE: "#f0d7d6", 287 | DEFAULT_RANK_SHADOW: "#f58867", 288 | DEFAULT_RANK_TEXT: "#ff5700", 289 | }, 290 | oldie: { 291 | BACKGROUND: "#F0F0F0", 292 | TITLE: "#111", 293 | ICON_CIRCLE: "#FFF", 294 | TEXT: "#666", 295 | LAUREL: "#535353", 296 | SECRET_RANK_1: "#738986", 297 | SECRET_RANK_2: "#B36154", 298 | SECRET_RANK_3: "#91A16A", 299 | SECRET_RANK_TEXT: "#4D4D4D", 300 | NEXT_RANK_BAR: "#8E8680", 301 | S_RANK_BASE: "#8E8E8E", 302 | S_RANK_SHADOW: "#8E8E8E", 303 | S_RANK_TEXT: "#4D4D4D", 304 | A_RANK_BASE: "#AFAFAF", 305 | A_RANK_SHADOW: "#AFAFAF", 306 | A_RANK_TEXT: "#4D4D4D", 307 | B_RANK_BASE: "#858585", 308 | B_RANK_SHADOW: "#858585", 309 | B_RANK_TEXT: "#4D4D4D", 310 | DEFAULT_RANK_BASE: "#535353", 311 | DEFAULT_RANK_SHADOW: "#535353", 312 | DEFAULT_RANK_TEXT: "#4D4D4D", 313 | }, 314 | buddhism: { 315 | BACKGROUND: "#ffc20e", 316 | TITLE: "#FFF", 317 | ICON_CIRCLE: "#FFF", 318 | TEXT: "#FFF", 319 | LAUREL: "#27c5ff", 320 | SECRET_RANK_1: "#FFF", 321 | SECRET_RANK_2: "#f73155", 322 | SECRET_RANK_3: "#fff", 323 | SECRET_RANK_TEXT: "#f73155", 324 | NEXT_RANK_BAR: "#f73155", 325 | S_RANK_BASE: "#ff8400", 326 | S_RANK_SHADOW: "#ff8400", 327 | S_RANK_TEXT: "#ffc20e", 328 | A_RANK_BASE: "#fff", 329 | A_RANK_SHADOW: "#fff", 330 | A_RANK_TEXT: "#ffc20e", 331 | B_RANK_BASE: "#f73155", 332 | B_RANK_SHADOW: "#f73155", 333 | B_RANK_TEXT: "#ffc20e", 334 | DEFAULT_RANK_BASE: "#27c5ff", 335 | DEFAULT_RANK_SHADOW: "#27c5ff", 336 | DEFAULT_RANK_TEXT: "#ffc20e", 337 | }, 338 | radical: { 339 | BACKGROUND: "#141321", 340 | ICON_CIRCLE: "#EEEEEE", 341 | TITLE: "#fe428e", 342 | TEXT: "#a9fef7", 343 | LAUREL: "#50fa7b", 344 | SECRET_RANK_1: "#ff5555", 345 | SECRET_RANK_2: "#ff15d9", 346 | SECRET_RANK_3: "#1E65F5", 347 | SECRET_RANK_TEXT: "#ff61c6", 348 | NEXT_RANK_BAR: "#fe428e", 349 | S_RANK_BASE: "#ffce32", 350 | S_RANK_SHADOW: "#ffce32", 351 | S_RANK_TEXT: "#CB8A30", 352 | A_RANK_BASE: "#8DF7B5", 353 | A_RANK_SHADOW: "#8DF7B5", 354 | A_RANK_TEXT: "#3A3A3A", 355 | B_RANK_BASE: "#EA3F25", 356 | B_RANK_SHADOW: "#EA3F25", 357 | B_RANK_TEXT: "#3A3A3A", 358 | DEFAULT_RANK_BASE: "#1E65F5", 359 | DEFAULT_RANK_SHADOW: "#1E65F5", 360 | DEFAULT_RANK_TEXT: "#3A3A3A", 361 | }, 362 | onestar: { 363 | BACKGROUND: "#0d1117", 364 | ICON_CIRCLE: "#EEEEEE", 365 | TITLE: "#EEEEEE", 366 | TEXT: "#c7c7c7", 367 | LAUREL: "#0dbc79", 368 | SECRET_RANK_1: "#ff5555", 369 | SECRET_RANK_2: "#d861d8", 370 | SECRET_RANK_3: "#3b8eea", 371 | SECRET_RANK_TEXT: "#ff61c6", 372 | NEXT_RANK_BAR: "#9e9e9e", 373 | S_RANK_BASE: "#FFD54F", 374 | S_RANK_SHADOW: "#FFE082", 375 | S_RANK_TEXT: "#CB8A30", 376 | A_RANK_BASE: "#23d18b", 377 | A_RANK_SHADOW: "#8DF7B5", 378 | A_RANK_TEXT: "#3A3A3A", 379 | B_RANK_BASE: "#d13b3b", 380 | B_RANK_SHADOW: "#fa4b4b", 381 | B_RANK_TEXT: "#3A3A3A", 382 | DEFAULT_RANK_BASE: "#2472c8", 383 | DEFAULT_RANK_SHADOW: "#3b8eea", 384 | DEFAULT_RANK_TEXT: "#3A3A3A", 385 | }, 386 | algolia: { 387 | BACKGROUND: "#050f2c", 388 | TITLE: "#00aeff", 389 | ICON_CIRCLE: "#f0f6fb", 390 | TEXT: "#7eace9", 391 | LAUREL: "#178600", 392 | SECRET_RANK_1: "#ff5555", 393 | SECRET_RANK_2: "#ff79c6", 394 | SECRET_RANK_3: "#388bfd", 395 | SECRET_RANK_TEXT: "#ff79c6", 396 | NEXT_RANK_BAR: "#00aeff", 397 | S_RANK_BASE: "#ffb86c", 398 | S_RANK_SHADOW: "#ffb86c", 399 | S_RANK_TEXT: "#0d1117", 400 | A_RANK_BASE: "#2dde98", 401 | A_RANK_TEXT: "#0d1117", 402 | A_RANK_SHADOW: "#2dde98", 403 | B_RANK_BASE: "#8be9fd", 404 | B_RANK_SHADOW: "#8be9fd", 405 | B_RANK_TEXT: "#0d1117", 406 | DEFAULT_RANK_BASE: "#5c75c3", 407 | DEFAULT_RANK_SHADOW: "#6272a4", 408 | DEFAULT_RANK_TEXT: "#0d1117", 409 | }, 410 | gitdimmed: { 411 | BACKGROUND: "#333", 412 | TITLE: "#f0f6fb", 413 | ICON_CIRCLE: "#f0f6fb", 414 | TEXT: "#FFF", 415 | LAUREL: "#178600", 416 | SECRET_RANK_1: "#ff5555", 417 | SECRET_RANK_2: "#ff79c6", 418 | SECRET_RANK_3: "#388bfd", 419 | SECRET_RANK_TEXT: "#ff79c6", 420 | NEXT_RANK_BAR: "#00aeff", 421 | S_RANK_BASE: "#ffb86c", 422 | S_RANK_SHADOW: "#ffb86c", 423 | S_RANK_TEXT: "#0d1117", 424 | A_RANK_BASE: "#2dde98", 425 | A_RANK_TEXT: "#0d1117", 426 | A_RANK_SHADOW: "#2dde98", 427 | B_RANK_BASE: "#8be9fd", 428 | B_RANK_SHADOW: "#8be9fd", 429 | B_RANK_TEXT: "#0d1117", 430 | DEFAULT_RANK_BASE: "#5c75c3", 431 | DEFAULT_RANK_SHADOW: "#6272a4", 432 | DEFAULT_RANK_TEXT: "#0d1117", 433 | }, 434 | tokyonight: { 435 | BACKGROUND: "#1a1b27", 436 | TITLE: "#70a5fd", 437 | ICON_CIRCLE: "#bf91f3", 438 | TEXT: "#38bdae", 439 | LAUREL: "#178600", 440 | SECRET_RANK_1: "#ff5555", 441 | SECRET_RANK_2: "#ff79c6", 442 | SECRET_RANK_3: "#388bfd", 443 | SECRET_RANK_TEXT: "#ff79c6", 444 | NEXT_RANK_BAR: "#00aeff", 445 | S_RANK_BASE: "#ffb86c", 446 | S_RANK_SHADOW: "#ffb86c", 447 | S_RANK_TEXT: "#0d1117", 448 | A_RANK_BASE: "#2dde98", 449 | A_RANK_TEXT: "#0d1117", 450 | A_RANK_SHADOW: "#2dde98", 451 | B_RANK_BASE: "#8be9fd", 452 | B_RANK_SHADOW: "#8be9fd", 453 | B_RANK_TEXT: "#0d1117", 454 | DEFAULT_RANK_BASE: "#5c75c3", 455 | DEFAULT_RANK_SHADOW: "#6272a4", 456 | DEFAULT_RANK_TEXT: "#0d1117", 457 | }, 458 | matrix: { 459 | BACKGROUND: "#000000", 460 | TITLE: "#00cc00", 461 | ICON_CIRCLE: "#002200", 462 | TEXT: "#00cc00", 463 | LAUREL: "#178600", 464 | SECRET_RANK_1: "#ffd700", 465 | SECRET_RANK_2: "#ffffff", 466 | SECRET_RANK_3: "#ffd700", 467 | SECRET_RANK_TEXT: "#00ff00", 468 | NEXT_RANK_BAR: "#00ff00", 469 | S_RANK_BASE: "#ffd700", 470 | S_RANK_SHADOW: "#ffd700", 471 | S_RANK_TEXT: "#00ff00", 472 | A_RANK_BASE: "#c0c0c0", 473 | A_RANK_TEXT: "#00ff00", 474 | A_RANK_SHADOW: "#c0c0c0", 475 | B_RANK_BASE: "#b08d57", 476 | B_RANK_SHADOW: "#b08d57", 477 | B_RANK_TEXT: "#00ff00", 478 | DEFAULT_RANK_BASE: "#b08d57", 479 | DEFAULT_RANK_SHADOW: "#b08d57", 480 | DEFAULT_RANK_TEXT: "#00ff00", 481 | }, 482 | apprentice: { 483 | BACKGROUND: "#262626", 484 | TITLE: "#BCBCBC", 485 | ICON_CIRCLE: "#BCBCBC", 486 | TEXT: "#5F875F", 487 | LAUREL: "#5F8787", 488 | SECRET_RANK_1: "#FF8700", 489 | SECRET_RANK_2: "#8787AF", 490 | SECRET_RANK_3: "#5F87AF", 491 | SECRET_RANK_TEXT: "#5F5F87", 492 | NEXT_RANK_BAR: "#FFFFA9", 493 | S_RANK_BASE: "#FFFFAF", 494 | S_RANK_SHADOW: "#FFFFAF", 495 | S_RANK_TEXT: "#87875F", 496 | A_RANK_BASE: "#8FAFD7", 497 | A_RANK_SHADOW: "#8FAFD7", 498 | A_RANK_TEXT: "#5F875F", 499 | B_RANK_BASE: "#AF5F5F", 500 | B_RANK_SHADOW: "#AF5F5F", 501 | B_RANK_TEXT: "#AF5F5F", 502 | DEFAULT_RANK_BASE: "#6C6C6C", 503 | DEFAULT_RANK_SHADOW: "#6C6C6C", 504 | DEFAULT_RANK_TEXT: "#1C1C1C", 505 | }, 506 | dark_dimmed: { 507 | BACKGROUND: "#22272e", 508 | TITLE: "#adbac7", 509 | ICON_CIRCLE: "#002200", 510 | TEXT: "#adbac7", 511 | LAUREL: "#178600", 512 | SECRET_RANK_1: "red", 513 | SECRET_RANK_2: "fuchsia", 514 | SECRET_RANK_3: "blue", 515 | SECRET_RANK_TEXT: "fuchsia", 516 | NEXT_RANK_BAR: "#0366d6", 517 | S_RANK_BASE: "#FAD200", 518 | S_RANK_SHADOW: "#C8A090", 519 | S_RANK_TEXT: "#886000", 520 | A_RANK_BASE: "#B0B0B0", 521 | A_RANK_SHADOW: "#9090C0", 522 | A_RANK_TEXT: "#505050", 523 | B_RANK_BASE: "#A18D66", 524 | B_RANK_SHADOW: "#816D96", 525 | B_RANK_TEXT: "#412D06", 526 | DEFAULT_RANK_BASE: "#777", 527 | DEFAULT_RANK_SHADOW: "#333", 528 | DEFAULT_RANK_TEXT: "#333", 529 | }, 530 | dark_lover: { 531 | BACKGROUND: "#0d0d0d", 532 | TITLE: "#e8aa64", 533 | ICON_CIRCLE: "white", 534 | TEXT: "#e8aa64", 535 | LAUREL: "#e86464", 536 | SECRET_RANK_1: "#e05555", 537 | SECRET_RANK_2: "#e05555", 538 | SECRET_RANK_3: "#e05555", 539 | SECRET_RANK_TEXT: "#e05555", 540 | NEXT_RANK_BAR: "#e05555", 541 | S_RANK_BASE: "#f2c635", 542 | S_RANK_SHADOW: "#e0d7b8", 543 | S_RANK_TEXT: "#b35707", 544 | A_RANK_BASE: "#f25755", 545 | A_RANK_SHADOW: "#e69493", 546 | A_RANK_TEXT: "#f5352f", 547 | B_RANK_BASE: "#63db93", 548 | B_RANK_SHADOW: "#8cd1a8", 549 | B_RANK_TEXT: "#07b84e", 550 | DEFAULT_RANK_BASE: "#7f6ceb", 551 | DEFAULT_RANK_SHADOW: "#a598ed", 552 | DEFAULT_RANK_TEXT: "#7f6ceb", 553 | }, 554 | kimbie_dark: { 555 | BACKGROUND: "#221a0f", 556 | TITLE: "#d3af86", 557 | ICON_CIRCLE: "#7e602c", 558 | TEXT: "#d3af86", 559 | LAUREL: "#889b4a", 560 | SECRET_RANK_1: "#f14a68", 561 | SECRET_RANK_2: "#f14a68", 562 | SECRET_RANK_3: "#dc3958", 563 | SECRET_RANK_TEXT: "#dc3958", 564 | NEXT_RANK_BAR: "#dc3958", 565 | S_RANK_BASE: "#fcac51", 566 | S_RANK_SHADOW: "#f79a32", 567 | S_RANK_TEXT: "#d3af86", 568 | A_RANK_BASE: "#a3B95a", 569 | A_RANK_SHADOW: "#889b4a", 570 | A_RANK_TEXT: "#d3af86", 571 | B_RANK_BASE: "#4c96a8", 572 | B_RANK_SHADOW: "#418292", 573 | B_RANK_TEXT: "#d3af86", 574 | DEFAULT_RANK_BASE: "#8ab1b0", 575 | DEFAULT_RANK_SHADOW: "#719190", 576 | DEFAULT_RANK_TEXT: "#d3af86", 577 | }, 578 | }; 579 | 580 | export interface Theme { 581 | BACKGROUND: string; 582 | TITLE: string; 583 | ICON_CIRCLE: string; 584 | TEXT: string; 585 | LAUREL: string; 586 | SECRET_RANK_1: string; 587 | SECRET_RANK_2: string; 588 | SECRET_RANK_3: string; 589 | SECRET_RANK_TEXT: string; 590 | NEXT_RANK_BAR: string; 591 | S_RANK_BASE: string; 592 | S_RANK_SHADOW: string; 593 | S_RANK_TEXT: string; 594 | A_RANK_BASE: string; 595 | A_RANK_SHADOW: string; 596 | A_RANK_TEXT: string; 597 | B_RANK_BASE: string; 598 | B_RANK_SHADOW: string; 599 | B_RANK_TEXT: string; 600 | DEFAULT_RANK_BASE: string; 601 | DEFAULT_RANK_SHADOW: string; 602 | DEFAULT_RANK_TEXT: string; 603 | } 604 | -------------------------------------------------------------------------------- /src/Services/__mocks__/successGithubResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "data": { 4 | "user": { 5 | "repositories": { 6 | "totalCount": 128, 7 | "nodes": [ 8 | { 9 | "languages": { 10 | "nodes": [ 11 | { 12 | "name": "JavaScript" 13 | } 14 | ] 15 | }, 16 | "stargazers": { 17 | "totalCount": 23 18 | } 19 | }, 20 | { 21 | "languages": { 22 | "nodes": [ 23 | { 24 | "name": "JavaScript" 25 | } 26 | ] 27 | }, 28 | "stargazers": { 29 | "totalCount": 11 30 | } 31 | }, 32 | { 33 | "languages": { 34 | "nodes": [ 35 | { 36 | "name": "TypeScript" 37 | } 38 | ] 39 | }, 40 | "stargazers": { 41 | "totalCount": 9 42 | } 43 | }, 44 | { 45 | "languages": { 46 | "nodes": [ 47 | { 48 | "name": "JavaScript" 49 | } 50 | ] 51 | }, 52 | "stargazers": { 53 | "totalCount": 6 54 | } 55 | }, 56 | { 57 | "languages": { 58 | "nodes": [ 59 | { 60 | "name": "JavaScript" 61 | } 62 | ] 63 | }, 64 | "stargazers": { 65 | "totalCount": 6 66 | } 67 | }, 68 | { 69 | "languages": { 70 | "nodes": [ 71 | { 72 | "name": "Java" 73 | } 74 | ] 75 | }, 76 | "stargazers": { 77 | "totalCount": 5 78 | } 79 | }, 80 | { 81 | "languages": { 82 | "nodes": [ 83 | { 84 | "name": "TypeScript" 85 | } 86 | ] 87 | }, 88 | "stargazers": { 89 | "totalCount": 5 90 | } 91 | }, 92 | { 93 | "languages": { 94 | "nodes": [ 95 | { 96 | "name": "Jupyter Notebook" 97 | } 98 | ] 99 | }, 100 | "stargazers": { 101 | "totalCount": 5 102 | } 103 | }, 104 | { 105 | "languages": { 106 | "nodes": [ 107 | { 108 | "name": "TypeScript" 109 | } 110 | ] 111 | }, 112 | "stargazers": { 113 | "totalCount": 4 114 | } 115 | }, 116 | { 117 | "languages": { 118 | "nodes": [ 119 | { 120 | "name": "JavaScript" 121 | } 122 | ] 123 | }, 124 | "stargazers": { 125 | "totalCount": 3 126 | } 127 | }, 128 | { 129 | "languages": { 130 | "nodes": [ 131 | { 132 | "name": "TypeScript" 133 | } 134 | ] 135 | }, 136 | "stargazers": { 137 | "totalCount": 2 138 | } 139 | }, 140 | { 141 | "languages": { 142 | "nodes": [ 143 | { 144 | "name": "JavaScript" 145 | } 146 | ] 147 | }, 148 | "stargazers": { 149 | "totalCount": 2 150 | } 151 | }, 152 | { 153 | "languages": { 154 | "nodes": [ 155 | { 156 | "name": "JavaScript" 157 | } 158 | ] 159 | }, 160 | "stargazers": { 161 | "totalCount": 2 162 | } 163 | }, 164 | { 165 | "languages": { 166 | "nodes": [] 167 | }, 168 | "stargazers": { 169 | "totalCount": 1 170 | } 171 | }, 172 | { 173 | "languages": { 174 | "nodes": [ 175 | { 176 | "name": "JavaScript" 177 | } 178 | ] 179 | }, 180 | "stargazers": { 181 | "totalCount": 1 182 | } 183 | }, 184 | { 185 | "languages": { 186 | "nodes": [ 187 | { 188 | "name": "PHP" 189 | }, 190 | { 191 | "name": "Go" 192 | }, 193 | { 194 | "name": "HTML" 195 | } 196 | ] 197 | }, 198 | "stargazers": { 199 | "totalCount": 1 200 | } 201 | }, 202 | { 203 | "languages": { 204 | "nodes": [ 205 | { 206 | "name": "TypeScript" 207 | }, 208 | { 209 | "name": "JavaScript" 210 | }, 211 | { 212 | "name": "CSS" 213 | } 214 | ] 215 | }, 216 | "stargazers": { 217 | "totalCount": 1 218 | } 219 | }, 220 | { 221 | "languages": { 222 | "nodes": [ 223 | { 224 | "name": "TypeScript" 225 | } 226 | ] 227 | }, 228 | "stargazers": { 229 | "totalCount": 1 230 | } 231 | }, 232 | { 233 | "languages": { 234 | "nodes": [ 235 | { 236 | "name": "TypeScript" 237 | }, 238 | { 239 | "name": "JavaScript" 240 | }, 241 | { 242 | "name": "Dockerfile" 243 | } 244 | ] 245 | }, 246 | "stargazers": { 247 | "totalCount": 1 248 | } 249 | }, 250 | { 251 | "languages": { 252 | "nodes": [ 253 | { 254 | "name": "TypeScript" 255 | }, 256 | { 257 | "name": "JavaScript" 258 | }, 259 | { 260 | "name": "Dockerfile" 261 | } 262 | ] 263 | }, 264 | "stargazers": { 265 | "totalCount": 1 266 | } 267 | }, 268 | { 269 | "languages": { 270 | "nodes": [ 271 | { 272 | "name": "Dart" 273 | }, 274 | { 275 | "name": "Swift" 276 | }, 277 | { 278 | "name": "Kotlin" 279 | } 280 | ] 281 | }, 282 | "stargazers": { 283 | "totalCount": 1 284 | } 285 | }, 286 | { 287 | "languages": { 288 | "nodes": [ 289 | { 290 | "name": "HTML" 291 | }, 292 | { 293 | "name": "CSS" 294 | } 295 | ] 296 | }, 297 | "stargazers": { 298 | "totalCount": 1 299 | } 300 | }, 301 | { 302 | "languages": { 303 | "nodes": [ 304 | { 305 | "name": "JavaScript" 306 | }, 307 | { 308 | "name": "Vue" 309 | }, 310 | { 311 | "name": "HTML" 312 | } 313 | ] 314 | }, 315 | "stargazers": { 316 | "totalCount": 1 317 | } 318 | }, 319 | { 320 | "languages": { 321 | "nodes": [ 322 | { 323 | "name": "Jupyter Notebook" 324 | }, 325 | { 326 | "name": "Python" 327 | } 328 | ] 329 | }, 330 | "stargazers": { 331 | "totalCount": 1 332 | } 333 | }, 334 | { 335 | "languages": { 336 | "nodes": [ 337 | { 338 | "name": "Dart" 339 | }, 340 | { 341 | "name": "HTML" 342 | }, 343 | { 344 | "name": "Swift" 345 | } 346 | ] 347 | }, 348 | "stargazers": { 349 | "totalCount": 1 350 | } 351 | }, 352 | { 353 | "languages": { 354 | "nodes": [ 355 | { 356 | "name": "JavaScript" 357 | }, 358 | { 359 | "name": "Vue" 360 | }, 361 | { 362 | "name": "HTML" 363 | } 364 | ] 365 | }, 366 | "stargazers": { 367 | "totalCount": 1 368 | } 369 | }, 370 | { 371 | "languages": { 372 | "nodes": [ 373 | { 374 | "name": "PHP" 375 | } 376 | ] 377 | }, 378 | "stargazers": { 379 | "totalCount": 1 380 | } 381 | }, 382 | { 383 | "languages": { 384 | "nodes": [ 385 | { 386 | "name": "TypeScript" 387 | }, 388 | { 389 | "name": "CSS" 390 | }, 391 | { 392 | "name": "JavaScript" 393 | } 394 | ] 395 | }, 396 | "stargazers": { 397 | "totalCount": 0 398 | } 399 | }, 400 | { 401 | "languages": { 402 | "nodes": [ 403 | { 404 | "name": "TypeScript" 405 | }, 406 | { 407 | "name": "HTML" 408 | }, 409 | { 410 | "name": "JavaScript" 411 | } 412 | ] 413 | }, 414 | "stargazers": { 415 | "totalCount": 0 416 | } 417 | }, 418 | { 419 | "languages": { 420 | "nodes": [ 421 | { 422 | "name": "TypeScript" 423 | }, 424 | { 425 | "name": "CSS" 426 | }, 427 | { 428 | "name": "JavaScript" 429 | } 430 | ] 431 | }, 432 | "stargazers": { 433 | "totalCount": 0 434 | } 435 | }, 436 | { 437 | "languages": { 438 | "nodes": [] 439 | }, 440 | "stargazers": { 441 | "totalCount": 0 442 | } 443 | }, 444 | { 445 | "languages": { 446 | "nodes": [] 447 | }, 448 | "stargazers": { 449 | "totalCount": 0 450 | } 451 | }, 452 | { 453 | "languages": { 454 | "nodes": [ 455 | { 456 | "name": "JavaScript" 457 | }, 458 | { 459 | "name": "Shell" 460 | }, 461 | { 462 | "name": "Dockerfile" 463 | } 464 | ] 465 | }, 466 | "stargazers": { 467 | "totalCount": 0 468 | } 469 | }, 470 | { 471 | "languages": { 472 | "nodes": [ 473 | { 474 | "name": "JavaScript" 475 | } 476 | ] 477 | }, 478 | "stargazers": { 479 | "totalCount": 0 480 | } 481 | }, 482 | { 483 | "languages": { 484 | "nodes": [ 485 | { 486 | "name": "TypeScript" 487 | } 488 | ] 489 | }, 490 | "stargazers": { 491 | "totalCount": 0 492 | } 493 | }, 494 | { 495 | "languages": { 496 | "nodes": [ 497 | { 498 | "name": "CSS" 499 | }, 500 | { 501 | "name": "TypeScript" 502 | }, 503 | { 504 | "name": "JavaScript" 505 | } 506 | ] 507 | }, 508 | "stargazers": { 509 | "totalCount": 0 510 | } 511 | }, 512 | { 513 | "languages": { 514 | "nodes": [ 515 | { 516 | "name": "TypeScript" 517 | }, 518 | { 519 | "name": "JavaScript" 520 | }, 521 | { 522 | "name": "Dockerfile" 523 | } 524 | ] 525 | }, 526 | "stargazers": { 527 | "totalCount": 0 528 | } 529 | }, 530 | { 531 | "languages": { 532 | "nodes": [] 533 | }, 534 | "stargazers": { 535 | "totalCount": 0 536 | } 537 | }, 538 | { 539 | "languages": { 540 | "nodes": [ 541 | { 542 | "name": "JavaScript" 543 | }, 544 | { 545 | "name": "TypeScript" 546 | } 547 | ] 548 | }, 549 | "stargazers": { 550 | "totalCount": 0 551 | } 552 | }, 553 | { 554 | "languages": { 555 | "nodes": [ 556 | { 557 | "name": "TypeScript" 558 | }, 559 | { 560 | "name": "CSS" 561 | }, 562 | { 563 | "name": "JavaScript" 564 | } 565 | ] 566 | }, 567 | "stargazers": { 568 | "totalCount": 0 569 | } 570 | }, 571 | { 572 | "languages": { 573 | "nodes": [ 574 | { 575 | "name": "JavaScript" 576 | }, 577 | { 578 | "name": "HTML" 579 | }, 580 | { 581 | "name": "C#" 582 | } 583 | ] 584 | }, 585 | "stargazers": { 586 | "totalCount": 0 587 | } 588 | }, 589 | { 590 | "languages": { 591 | "nodes": [ 592 | { 593 | "name": "JavaScript" 594 | }, 595 | { 596 | "name": "HTML" 597 | } 598 | ] 599 | }, 600 | "stargazers": { 601 | "totalCount": 0 602 | } 603 | }, 604 | { 605 | "languages": { 606 | "nodes": [ 607 | { 608 | "name": "TypeScript" 609 | }, 610 | { 611 | "name": "JavaScript" 612 | }, 613 | { 614 | "name": "CSS" 615 | } 616 | ] 617 | }, 618 | "stargazers": { 619 | "totalCount": 0 620 | } 621 | }, 622 | { 623 | "languages": { 624 | "nodes": [ 625 | { 626 | "name": "PHP" 627 | }, 628 | { 629 | "name": "Vue" 630 | }, 631 | { 632 | "name": "Blade" 633 | } 634 | ] 635 | }, 636 | "stargazers": { 637 | "totalCount": 0 638 | } 639 | }, 640 | { 641 | "languages": { 642 | "nodes": [ 643 | { 644 | "name": "CSS" 645 | }, 646 | { 647 | "name": "TypeScript" 648 | }, 649 | { 650 | "name": "JavaScript" 651 | } 652 | ] 653 | }, 654 | "stargazers": { 655 | "totalCount": 0 656 | } 657 | }, 658 | { 659 | "languages": { 660 | "nodes": [ 661 | { 662 | "name": "C" 663 | }, 664 | { 665 | "name": "C++" 666 | }, 667 | { 668 | "name": "HTML" 669 | } 670 | ] 671 | }, 672 | "stargazers": { 673 | "totalCount": 0 674 | } 675 | }, 676 | { 677 | "languages": { 678 | "nodes": [ 679 | { 680 | "name": "C++" 681 | }, 682 | { 683 | "name": "Makefile" 684 | }, 685 | { 686 | "name": "CMake" 687 | } 688 | ] 689 | }, 690 | "stargazers": { 691 | "totalCount": 0 692 | } 693 | }, 694 | { 695 | "languages": { 696 | "nodes": [ 697 | { 698 | "name": "JavaScript" 699 | } 700 | ] 701 | }, 702 | "stargazers": { 703 | "totalCount": 0 704 | } 705 | }, 706 | { 707 | "languages": { 708 | "nodes": [ 709 | { 710 | "name": "TypeScript" 711 | }, 712 | { 713 | "name": "JavaScript" 714 | } 715 | ] 716 | }, 717 | "stargazers": { 718 | "totalCount": 0 719 | } 720 | }, 721 | { 722 | "languages": { 723 | "nodes": [ 724 | { 725 | "name": "C++" 726 | }, 727 | { 728 | "name": "Makefile" 729 | }, 730 | { 731 | "name": "CMake" 732 | } 733 | ] 734 | }, 735 | "stargazers": { 736 | "totalCount": 0 737 | } 738 | }, 739 | { 740 | "languages": { 741 | "nodes": [ 742 | { 743 | "name": "JavaScript" 744 | } 745 | ] 746 | }, 747 | "stargazers": { 748 | "totalCount": 0 749 | } 750 | }, 751 | { 752 | "languages": { 753 | "nodes": [ 754 | { 755 | "name": "JavaScript" 756 | }, 757 | { 758 | "name": "TypeScript" 759 | } 760 | ] 761 | }, 762 | "stargazers": { 763 | "totalCount": 0 764 | } 765 | }, 766 | { 767 | "languages": { 768 | "nodes": [ 769 | { 770 | "name": "JavaScript" 771 | } 772 | ] 773 | }, 774 | "stargazers": { 775 | "totalCount": 0 776 | } 777 | }, 778 | { 779 | "languages": { 780 | "nodes": [ 781 | { 782 | "name": "JavaScript" 783 | } 784 | ] 785 | }, 786 | "stargazers": { 787 | "totalCount": 0 788 | } 789 | }, 790 | { 791 | "languages": { 792 | "nodes": [ 793 | { 794 | "name": "TypeScript" 795 | }, 796 | { 797 | "name": "CSS" 798 | }, 799 | { 800 | "name": "HTML" 801 | } 802 | ] 803 | }, 804 | "stargazers": { 805 | "totalCount": 0 806 | } 807 | }, 808 | { 809 | "languages": { 810 | "nodes": [ 811 | { 812 | "name": "JavaScript" 813 | }, 814 | { 815 | "name": "TypeScript" 816 | }, 817 | { 818 | "name": "CSS" 819 | } 820 | ] 821 | }, 822 | "stargazers": { 823 | "totalCount": 0 824 | } 825 | }, 826 | { 827 | "languages": { 828 | "nodes": [ 829 | { 830 | "name": "JavaScript" 831 | } 832 | ] 833 | }, 834 | "stargazers": { 835 | "totalCount": 0 836 | } 837 | }, 838 | { 839 | "languages": { 840 | "nodes": [ 841 | { 842 | "name": "TypeScript" 843 | }, 844 | { 845 | "name": "JavaScript" 846 | } 847 | ] 848 | }, 849 | "stargazers": { 850 | "totalCount": 0 851 | } 852 | }, 853 | { 854 | "languages": { 855 | "nodes": [ 856 | { 857 | "name": "Vue" 858 | }, 859 | { 860 | "name": "TypeScript" 861 | }, 862 | { 863 | "name": "CSS" 864 | } 865 | ] 866 | }, 867 | "stargazers": { 868 | "totalCount": 0 869 | } 870 | }, 871 | { 872 | "languages": { 873 | "nodes": [ 874 | { 875 | "name": "TypeScript" 876 | }, 877 | { 878 | "name": "CSS" 879 | }, 880 | { 881 | "name": "HTML" 882 | } 883 | ] 884 | }, 885 | "stargazers": { 886 | "totalCount": 0 887 | } 888 | }, 889 | { 890 | "languages": { 891 | "nodes": [ 892 | { 893 | "name": "TypeScript" 894 | } 895 | ] 896 | }, 897 | "stargazers": { 898 | "totalCount": 0 899 | } 900 | }, 901 | { 902 | "languages": { 903 | "nodes": [] 904 | }, 905 | "stargazers": { 906 | "totalCount": 0 907 | } 908 | }, 909 | { 910 | "languages": { 911 | "nodes": [ 912 | { 913 | "name": "JavaScript" 914 | }, 915 | { 916 | "name": "HTML" 917 | } 918 | ] 919 | }, 920 | "stargazers": { 921 | "totalCount": 0 922 | } 923 | }, 924 | { 925 | "languages": { 926 | "nodes": [ 927 | { 928 | "name": "PHP" 929 | }, 930 | { 931 | "name": "JavaScript" 932 | }, 933 | { 934 | "name": "Blade" 935 | } 936 | ] 937 | }, 938 | "stargazers": { 939 | "totalCount": 0 940 | } 941 | }, 942 | { 943 | "languages": { 944 | "nodes": [ 945 | { 946 | "name": "JavaScript" 947 | }, 948 | { 949 | "name": "Rust" 950 | }, 951 | { 952 | "name": "HTML" 953 | } 954 | ] 955 | }, 956 | "stargazers": { 957 | "totalCount": 0 958 | } 959 | }, 960 | { 961 | "languages": { 962 | "nodes": [ 963 | { 964 | "name": "JavaScript" 965 | }, 966 | { 967 | "name": "Svelte" 968 | }, 969 | { 970 | "name": "HTML" 971 | } 972 | ] 973 | }, 974 | "stargazers": { 975 | "totalCount": 0 976 | } 977 | }, 978 | { 979 | "languages": { 980 | "nodes": [ 981 | { 982 | "name": "CSS" 983 | }, 984 | { 985 | "name": "HTML" 986 | }, 987 | { 988 | "name": "JavaScript" 989 | } 990 | ] 991 | }, 992 | "stargazers": { 993 | "totalCount": 0 994 | } 995 | }, 996 | { 997 | "languages": { 998 | "nodes": [ 999 | { 1000 | "name": "JavaScript" 1001 | } 1002 | ] 1003 | }, 1004 | "stargazers": { 1005 | "totalCount": 0 1006 | } 1007 | }, 1008 | { 1009 | "languages": { 1010 | "nodes": [ 1011 | { 1012 | "name": "TypeScript" 1013 | }, 1014 | { 1015 | "name": "Dockerfile" 1016 | }, 1017 | { 1018 | "name": "JavaScript" 1019 | } 1020 | ] 1021 | }, 1022 | "stargazers": { 1023 | "totalCount": 0 1024 | } 1025 | }, 1026 | { 1027 | "languages": { 1028 | "nodes": [ 1029 | { 1030 | "name": "Rust" 1031 | } 1032 | ] 1033 | }, 1034 | "stargazers": { 1035 | "totalCount": 0 1036 | } 1037 | }, 1038 | { 1039 | "languages": { 1040 | "nodes": [ 1041 | { 1042 | "name": "JavaScript" 1043 | }, 1044 | { 1045 | "name": "Dockerfile" 1046 | }, 1047 | { 1048 | "name": "Shell" 1049 | } 1050 | ] 1051 | }, 1052 | "stargazers": { 1053 | "totalCount": 0 1054 | } 1055 | }, 1056 | { 1057 | "languages": { 1058 | "nodes": [ 1059 | { 1060 | "name": "TypeScript" 1061 | }, 1062 | { 1063 | "name": "Shell" 1064 | } 1065 | ] 1066 | }, 1067 | "stargazers": { 1068 | "totalCount": 0 1069 | } 1070 | }, 1071 | { 1072 | "languages": { 1073 | "nodes": [ 1074 | { 1075 | "name": "TypeScript" 1076 | }, 1077 | { 1078 | "name": "JavaScript" 1079 | } 1080 | ] 1081 | }, 1082 | "stargazers": { 1083 | "totalCount": 0 1084 | } 1085 | }, 1086 | { 1087 | "languages": { 1088 | "nodes": [ 1089 | { 1090 | "name": "HTML" 1091 | }, 1092 | { 1093 | "name": "JavaScript" 1094 | }, 1095 | { 1096 | "name": "CSS" 1097 | } 1098 | ] 1099 | }, 1100 | "stargazers": { 1101 | "totalCount": 0 1102 | } 1103 | }, 1104 | { 1105 | "languages": { 1106 | "nodes": [ 1107 | { 1108 | "name": "TypeScript" 1109 | }, 1110 | { 1111 | "name": "CSS" 1112 | }, 1113 | { 1114 | "name": "HTML" 1115 | } 1116 | ] 1117 | }, 1118 | "stargazers": { 1119 | "totalCount": 0 1120 | } 1121 | }, 1122 | { 1123 | "languages": { 1124 | "nodes": [ 1125 | { 1126 | "name": "Dart" 1127 | }, 1128 | { 1129 | "name": "HTML" 1130 | }, 1131 | { 1132 | "name": "Swift" 1133 | } 1134 | ] 1135 | }, 1136 | "stargazers": { 1137 | "totalCount": 0 1138 | } 1139 | }, 1140 | { 1141 | "languages": { 1142 | "nodes": [ 1143 | { 1144 | "name": "Dart" 1145 | } 1146 | ] 1147 | }, 1148 | "stargazers": { 1149 | "totalCount": 0 1150 | } 1151 | }, 1152 | { 1153 | "languages": { 1154 | "nodes": [ 1155 | { 1156 | "name": "Rust" 1157 | }, 1158 | { 1159 | "name": "JavaScript" 1160 | }, 1161 | { 1162 | "name": "HTML" 1163 | } 1164 | ] 1165 | }, 1166 | "stargazers": { 1167 | "totalCount": 0 1168 | } 1169 | }, 1170 | { 1171 | "languages": { 1172 | "nodes": [ 1173 | { 1174 | "name": "JavaScript" 1175 | } 1176 | ] 1177 | }, 1178 | "stargazers": { 1179 | "totalCount": 0 1180 | } 1181 | }, 1182 | { 1183 | "languages": { 1184 | "nodes": [ 1185 | { 1186 | "name": "JavaScript" 1187 | }, 1188 | { 1189 | "name": "CSS" 1190 | }, 1191 | { 1192 | "name": "Swift" 1193 | } 1194 | ] 1195 | }, 1196 | "stargazers": { 1197 | "totalCount": 0 1198 | } 1199 | }, 1200 | { 1201 | "languages": { 1202 | "nodes": [ 1203 | { 1204 | "name": "TypeScript" 1205 | }, 1206 | { 1207 | "name": "JavaScript" 1208 | }, 1209 | { 1210 | "name": "SCSS" 1211 | } 1212 | ] 1213 | }, 1214 | "stargazers": { 1215 | "totalCount": 0 1216 | } 1217 | }, 1218 | { 1219 | "languages": { 1220 | "nodes": [ 1221 | { 1222 | "name": "TypeScript" 1223 | } 1224 | ] 1225 | }, 1226 | "stargazers": { 1227 | "totalCount": 0 1228 | } 1229 | }, 1230 | { 1231 | "languages": { 1232 | "nodes": [ 1233 | { 1234 | "name": "JavaScript" 1235 | }, 1236 | { 1237 | "name": "Shell" 1238 | } 1239 | ] 1240 | }, 1241 | "stargazers": { 1242 | "totalCount": 0 1243 | } 1244 | }, 1245 | { 1246 | "languages": { 1247 | "nodes": [ 1248 | { 1249 | "name": "JavaScript" 1250 | } 1251 | ] 1252 | }, 1253 | "stargazers": { 1254 | "totalCount": 0 1255 | } 1256 | }, 1257 | { 1258 | "languages": { 1259 | "nodes": [ 1260 | { 1261 | "name": "TypeScript" 1262 | }, 1263 | { 1264 | "name": "JavaScript" 1265 | }, 1266 | { 1267 | "name": "SCSS" 1268 | } 1269 | ] 1270 | }, 1271 | "stargazers": { 1272 | "totalCount": 0 1273 | } 1274 | }, 1275 | { 1276 | "languages": { 1277 | "nodes": [ 1278 | { 1279 | "name": "TypeScript" 1280 | }, 1281 | { 1282 | "name": "HTML" 1283 | } 1284 | ] 1285 | }, 1286 | "stargazers": { 1287 | "totalCount": 0 1288 | } 1289 | }, 1290 | { 1291 | "languages": { 1292 | "nodes": [ 1293 | { 1294 | "name": "TypeScript" 1295 | }, 1296 | { 1297 | "name": "JavaScript" 1298 | }, 1299 | { 1300 | "name": "HTML" 1301 | } 1302 | ] 1303 | }, 1304 | "stargazers": { 1305 | "totalCount": 0 1306 | } 1307 | }, 1308 | { 1309 | "languages": { 1310 | "nodes": [ 1311 | { 1312 | "name": "JavaScript" 1313 | }, 1314 | { 1315 | "name": "CSS" 1316 | }, 1317 | { 1318 | "name": "HTML" 1319 | } 1320 | ] 1321 | }, 1322 | "stargazers": { 1323 | "totalCount": 0 1324 | } 1325 | }, 1326 | { 1327 | "languages": { 1328 | "nodes": [ 1329 | { 1330 | "name": "TypeScript" 1331 | }, 1332 | { 1333 | "name": "JavaScript" 1334 | }, 1335 | { 1336 | "name": "CSS" 1337 | } 1338 | ] 1339 | }, 1340 | "stargazers": { 1341 | "totalCount": 0 1342 | } 1343 | }, 1344 | { 1345 | "languages": { 1346 | "nodes": [ 1347 | { 1348 | "name": "TypeScript" 1349 | }, 1350 | { 1351 | "name": "JavaScript" 1352 | }, 1353 | { 1354 | "name": "HTML" 1355 | } 1356 | ] 1357 | }, 1358 | "stargazers": { 1359 | "totalCount": 0 1360 | } 1361 | }, 1362 | { 1363 | "languages": { 1364 | "nodes": [ 1365 | { 1366 | "name": "TypeScript" 1367 | }, 1368 | { 1369 | "name": "JavaScript" 1370 | }, 1371 | { 1372 | "name": "HTML" 1373 | } 1374 | ] 1375 | }, 1376 | "stargazers": { 1377 | "totalCount": 0 1378 | } 1379 | }, 1380 | { 1381 | "languages": { 1382 | "nodes": [ 1383 | { 1384 | "name": "TypeScript" 1385 | }, 1386 | { 1387 | "name": "JavaScript" 1388 | }, 1389 | { 1390 | "name": "HTML" 1391 | } 1392 | ] 1393 | }, 1394 | "stargazers": { 1395 | "totalCount": 0 1396 | } 1397 | }, 1398 | { 1399 | "languages": { 1400 | "nodes": [ 1401 | { 1402 | "name": "TypeScript" 1403 | }, 1404 | { 1405 | "name": "SCSS" 1406 | }, 1407 | { 1408 | "name": "JavaScript" 1409 | } 1410 | ] 1411 | }, 1412 | "stargazers": { 1413 | "totalCount": 0 1414 | } 1415 | }, 1416 | { 1417 | "languages": { 1418 | "nodes": [ 1419 | { 1420 | "name": "TypeScript" 1421 | }, 1422 | { 1423 | "name": "JavaScript" 1424 | }, 1425 | { 1426 | "name": "SCSS" 1427 | } 1428 | ] 1429 | }, 1430 | "stargazers": { 1431 | "totalCount": 0 1432 | } 1433 | }, 1434 | { 1435 | "languages": { 1436 | "nodes": [ 1437 | { 1438 | "name": "JavaScript" 1439 | }, 1440 | { 1441 | "name": "HTML" 1442 | }, 1443 | { 1444 | "name": "CSS" 1445 | } 1446 | ] 1447 | }, 1448 | "stargazers": { 1449 | "totalCount": 0 1450 | } 1451 | }, 1452 | { 1453 | "languages": { 1454 | "nodes": [ 1455 | { 1456 | "name": "JavaScript" 1457 | }, 1458 | { 1459 | "name": "HTML" 1460 | }, 1461 | { 1462 | "name": "CSS" 1463 | } 1464 | ] 1465 | }, 1466 | "stargazers": { 1467 | "totalCount": 0 1468 | } 1469 | }, 1470 | { 1471 | "languages": { 1472 | "nodes": [ 1473 | { 1474 | "name": "JavaScript" 1475 | }, 1476 | { 1477 | "name": "HTML" 1478 | }, 1479 | { 1480 | "name": "SCSS" 1481 | } 1482 | ] 1483 | }, 1484 | "stargazers": { 1485 | "totalCount": 0 1486 | } 1487 | }, 1488 | { 1489 | "languages": { 1490 | "nodes": [ 1491 | { 1492 | "name": "Swift" 1493 | } 1494 | ] 1495 | }, 1496 | "stargazers": { 1497 | "totalCount": 0 1498 | } 1499 | }, 1500 | { 1501 | "languages": { 1502 | "nodes": [ 1503 | { 1504 | "name": "TypeScript" 1505 | }, 1506 | { 1507 | "name": "HTML" 1508 | }, 1509 | { 1510 | "name": "CSS" 1511 | } 1512 | ] 1513 | }, 1514 | "stargazers": { 1515 | "totalCount": 0 1516 | } 1517 | }, 1518 | { 1519 | "languages": { 1520 | "nodes": [ 1521 | { 1522 | "name": "TypeScript" 1523 | }, 1524 | { 1525 | "name": "HTML" 1526 | }, 1527 | { 1528 | "name": "CSS" 1529 | } 1530 | ] 1531 | }, 1532 | "stargazers": { 1533 | "totalCount": 0 1534 | } 1535 | } 1536 | ] 1537 | } 1538 | } 1539 | } 1540 | } 1541 | } 1542 | --------------------------------------------------------------------------------