[
  {
    "path": ".github/workflows/webapp-linting-and-unit-tests.yaml",
    "content": "name: Frontent linting and unit tests\n \non:\n  push:\n    branches:\n      - main\n  pull_request:\n \nenv:\n  NODE_VERSION: 16\n \njobs:\n  linting-and-tests:\n    name: Webapp linting and unit tests\n    runs-on: ubuntu-latest\n    steps:\n      - name: Install NodeJS\n        uses: actions/setup-node@v2\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n \n      - name: Code Checkout\n        uses: actions/checkout@v2\n \n      - name: Install webapp dependencies\n        run: yarn --cwd ./packages/webapp-next install --frozen-lockfile\n\n      - name: Build webapp\n        run: yarn --cwd ./packages/webapp-next build\n \n      - name: Webapp code linting\n        run: yarn --cwd ./packages/webapp-next lint --quiet\n\n      - name: Webapp unit test\n        run: echo \"no tests to run\"\n"
  },
  {
    "path": ".gitignore",
    "content": "# Notes\n.mind\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Deploys\n.netlify\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\ndist/\nartifacts/\ntmp/\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n\n# Local Netlify folder\n.netlify\n\n# development\n.idea\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\n*This is a work in progress.*\n\n### **Table of Contents**\n- [Required](#required) \n- [Running Speedtyper.dev](#running-speedtyperdev)\n    - [Backend](#backend)\n    - [Frontend](#frontend)\n\n## Required\n\n|Prerequisite                               |Link                                                                   |\n|-------------------------------------------|-----------------------------------------------------------------------|\n|Git                                        |[🔗](https://git-scm.com/downloads)                                   |\n|Node 20                                    |[🔗](https://nodejs.org/en/)                                          |\n| Yarn                                      |[🔗](https://classic.yarnpkg.com/lang/en/docs/install/#windows-stable)|\n|PostgreSQL                                 |            |\n|build-essential (or equivalent for your OS)|                                                                       |\n| Docker (Optional)                         |[🔗](https://www.docker.com/)                                         |\n\n## Running Speedtyper.dev\n\n### Backend\n\n1. Install dependencies:\n\n    ```\n    make install-backend-dependencies\n    ```\n1. Copy over path of env file:\n\n    ```\n    cp ./packages/back-nest/.env.development ./packages/back-nest/.env\n    ```\n\n1. Generate [Github Access Token (classic)](https://github.com/settings/tokens) with `public_repo` permissions and update `GITHUB_ACCESS_TOKEN` variable in `./packages/back-nest/.env` with the token value. It is used to download seed data from GitHub.\n\n1. Start Docker Compose in the background:\n\n    ```\n    make run-dev-db\n    ```\n\n1. Seed the db with example challenges:\n\n    ```\n    make run-seed-codesources\n    ```\n\n1. Run the backend:\n\n    ```\n    make run-backend-dev\n    ```\n\n### Frontend\n\n1. Install dependencies:\n\n    ```\n    make install-webapp-dependencies\n    ```\n\n1. Run the frontend:\n\n    ```\n    make run-webapp-dev\n    ```\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 codico <codicocodes@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "# backend\n\ninstall-backend-dependencies:\n\tyarn --cwd ./packages/back-nest\n\nrun-backend-dev:\n\tyarn --cwd ./packages/back-nest start:dev\n\nrun-dev-db:\n\tdocker compose -f ./packages/back-nest/docker-compose.yml up -d\n\nrun-seed-codesources:\n\tyarn --cwd ./packages/back-nest command seed-challenges\n\n# webapp\n\ninstall-webapp-dependencies:\n\tyarn --cwd ./packages/webapp-next\n\nrun-webapp-dev:\n\tyarn --cwd ./packages/webapp-next dev\n"
  },
  {
    "path": "README.md",
    "content": "\r\n<br>\r\n<div align=\"center\">\r\n  <a href=\"https://speedtyper.dev\" target=\"_blank\">\r\n    <img src=\"https://www.speedtyper.dev/logo.png\" alt=\"Speedtyper\" height=\"100\" width=\"auto\"/>\r\n  </a>\r\n  <h1><i>speedtyper.dev</i></h1>\r\n</div>\r\n\r\n<p align=\"center\">\r\n  <b>\r\n      Typing competitions for programmers 🧑‍💻👩‍💻👨‍💻\r\n  </b>\r\n</p>\r\n<p align=\"center\">\r\n  <a href=\"https://github.com/codicocodes/speedtyper.dev\" target=\"__blank\"><img alt=\"GitHub stars\" src=\"https://img.shields.io/github/stars/codicocodes/speedtyper.dev?style=social\"></a>\r\n</p>\r\n\r\n### **Table of Contents**\r\n- [Features](#features-🎉)\r\n- [Contribute](#contribute-👷)\r\n- [Community](#community-☕)\r\n- [License](#license-📜)\r\n- [Project Contributors](#project-contributors⭐)\r\n\r\n## Features 🎉\r\n\r\n- ✍️ [**Practice**](https://speedtyper.dev/play?mode=private) - type code snippets from real open source projects\r\n- 🏎️ [**Battle**](https://speedtyper.dev/play?mode=private) - play with your friends in real time with the private race mode\r\n- 🏅 [**Compete**](https://speedtyper.dev) - get on the global leaderboard\r\n\r\n## Contribute 👷\r\n- 🦄 **Pull requests are very appreciated!**\r\n- 📚 Read the [contributor introduction (wip)](https://github.com/codicocodes/speedtyper.dev/blob/main/CONTRIBUTING.md)\r\n- 🐛 If you encounter a bug, please [open an issue](https://github.com/codicocodes/speedtyper.dev/issues/new)\r\n- 🗨️ If you want to make a large change, please [open an issue](https://github.com/codicocodes/speedtyper.dev/issues/new) so we can discuss it!\r\n\r\n## Community ☕\r\n<a href=\"https://discord.gg/AMbnnN5eep\" target=\"__blank\">\r\n  <img src=\"https://discordapp.com/api/guilds/774781405506568202/widget.png?style=banner2\" alt=\"SpeedTyper Discord\" width=\"auto\" height=\"50px\"/>\r\n</a>\r\n<a href=\"https://twitch.tv/codico\" target=\"__blank\">\r\n  <img src=\"https://user-images.githubusercontent.com/76068197/187993983-6133fe16-46ed-45f7-a459-fa798bda4a92.png\" alt=\"Twitch Stream\" width=\"auto\" height=\"50px\"/>\r\n</a>\r\n\r\n## License 📜\r\n\r\nspeedtyper.dev is open source software licensed as [MIT](https://github.com/codicocodes/speedtyper.dev/blob/main/LICENSE).\r\n\r\nThe [logo](https://github.com/codicocodes/speedtyper.dev/blob/main/packages/webapp/public/images/logo.png) is made by [astrocanyounaut](https://www.twitch.tv/astrocanyounaut) 🧑‍🚀 and is not licensed under MIT.\r\n\r\n\r\n## Project Contributors⭐ \r\n\r\n<a href=\"https://github.com/codicocodes/speedtyper.dev/graphs/contributors\" align=\"center\">\r\n  <img src=\"https://contrib.rocks/image?repo=codicocodes/speedtyper.dev\" /> \r\n</a>\r\n"
  },
  {
    "path": "packages/back-nest/.eslintrc.js",
    "content": "module.exports = {\n  parser: '@typescript-eslint/parser',\n  parserOptions: {\n    project: 'tsconfig.json',\n    tsconfigRootDir : __dirname, \n    sourceType: 'module',\n  },\n  plugins: ['@typescript-eslint/eslint-plugin'],\n  extends: [\n    'plugin:@typescript-eslint/recommended',\n    'plugin:prettier/recommended',\n  ],\n  root: true,\n  env: {\n    node: true,\n    jest: true,\n  },\n  ignorePatterns: ['.eslintrc.js', 'node_modules', 'dist'],\n  rules: {\n    '@typescript-eslint/interface-name-prefix': 'off',\n    '@typescript-eslint/explicit-function-return-type': 'off',\n    '@typescript-eslint/explicit-module-boundary-types': 'off',\n    '@typescript-eslint/no-explicit-any': 'off',\n  },\n};\n"
  },
  {
    "path": "packages/back-nest/.gitignore",
    "content": "# compiled output\n/dist\n/node_modules\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\npnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# OS\n.DS_Store\n\n# Tests\n/coverage\n/.nyc_output\n\n# IDEs and editors\n/.idea\n.project\n.classpath\n.c9/\n*.launch\n.settings/\n*.sublime-workspace\n\n# IDE - VSCode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json"
  },
  {
    "path": "packages/back-nest/.prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}"
  },
  {
    "path": "packages/back-nest/Dockerfile",
    "content": "FROM node:20\n\n# Create app directory\nRUN mkdir -p /app\nWORKDIR /app\n\n# Install app dependencies\nCOPY package.json /app\nCOPY yarn.lock /app\nRUN yarn install --frozen-lockfile\n\n# Bundle app source\nCOPY . /app\nRUN yarn build\n\nEXPOSE 80\n\nCMD [ \"node\", \"dist/main.js\" ]\n"
  },
  {
    "path": "packages/back-nest/README.md",
    "content": "## Seed challenge data\n\n### Seed test challenges\n\n`yarn command seed-challenges`\n\n### Seed production challenges\n\nRequires configuring a personal `GITHUB_ACCESS_TOKEN` in your .env file\n\n`yarn command import-projects`\n`yarn command sync-projects`\n`yarn command import-files`\n`yarn command import-challenges`\n"
  },
  {
    "path": "packages/back-nest/docker-compose.yml",
    "content": "# Use postgres/example user/password credentials\nversion: \"3.1\"\n\nservices:\n  db:\n    image: postgres\n    restart: always\n\n    ports:\n      - 5432:5432\n\n    environment:\n      POSTGRES_USERNAME: postgres\n      POSTGRES_PASSWORD: postgres\n      POSTGRES_DB: speedtyper\n\n  adminer:\n    image: adminer\n    restart: always\n    ports:\n      - 8080:8080\n"
  },
  {
    "path": "packages/back-nest/nest-cli.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/nest-cli\",\n  \"collection\": \"@nestjs/schematics\",\n  \"sourceRoot\": \"src\"\n}\n"
  },
  {
    "path": "packages/back-nest/package.json",
    "content": "{\n  \"name\": \"back-nest\",\n  \"version\": \"0.0.1\",\n  \"description\": \"\",\n  \"author\": \"\",\n  \"private\": true,\n  \"license\": \"UNLICENSED\",\n  \"scripts\": {\n    \"command\": \"TS_NODE_PROJECT=./tsconfig.json ts-node -r tsconfig-paths/register ./src/commands.ts\",\n    \"prebuild\": \"rimraf dist\",\n    \"build\": \"nest build\",\n    \"format\": \"prettier --write \\\"src/**/*.ts\\\" \\\"test/**/*.ts\\\"\",\n    \"start\": \"yarn start:prod\",\n    \"start:dev\": \"nest start --watch\",\n    \"start:debug\": \"nest start --debug --watch\",\n    \"start:prod\": \"node dist/main\",\n    \"lint\": \"eslint \\\"{src,apps,libs,test}/**/*.ts\\\" --fix\",\n    \"test\": \"jest\",\n    \"test:watch\": \"jest --watch\",\n    \"test:cov\": \"jest --coverage\",\n    \"test:debug\": \"node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand\",\n    \"test:e2e\": \"jest --config ./test/jest-e2e.json\"\n  },\n  \"dependencies\": {\n    \"@nestjs/axios\": \"^0.1.0\",\n    \"@nestjs/common\": \"^9.0.0\",\n    \"@nestjs/config\": \"^2.2.0\",\n    \"@nestjs/core\": \"^9.0.0\",\n    \"@nestjs/passport\": \"^9.0.0\",\n    \"@nestjs/platform-express\": \"^9.0.0\",\n    \"@nestjs/platform-socket.io\": \"^9.1.4\",\n    \"@nestjs/typeorm\": \"^10.0.1\",\n    \"@nestjs/websockets\": \"^9.1.4\",\n    \"@sentry/node\": \"^7.37.2\",\n    \"@types/passport-github\": \"^1.1.7\",\n    \"class-transformer\": \"^0.5.1\",\n    \"class-validator\": \"^0.13.2\",\n    \"connect-typeorm\": \"^2.0.0\",\n    \"express-session\": \"^1.17.3\",\n    \"express-socket.io-session\": \"^1.3.5\",\n    \"nest-commander\": \"^3.1.0\",\n    \"passport\": \"^0.6.0\",\n    \"passport-github\": \"^1.1.0\",\n    \"pg\": \"^8.8.0\",\n    \"pg-query-stream\": \"^4.3.0\",\n    \"reflect-metadata\": \"^0.1.13\",\n    \"rimraf\": \"^3.0.2\",\n    \"rxjs\": \"^7.2.0\",\n    \"socket.io\": \"^4.5.2\",\n    \"tree-sitter\": \"^0.20.0\",\n    \"tree-sitter-c\": \"^0.19.0\",\n    \"tree-sitter-c-sharp\": \"^0.19.0\",\n    \"tree-sitter-cpp\": \"^0.19.0\",\n    \"tree-sitter-css\": \"^0.19.0\",\n    \"tree-sitter-go\": \"^0.19.1\",\n    \"tree-sitter-java\": \"^0.19.1\",\n    \"tree-sitter-javascript\": \"^0.19.0\",\n    \"tree-sitter-lua\": \"^1.6.2\",\n    \"tree-sitter-ocaml\": \"^0.19.0\",\n    \"tree-sitter-php\": \"^0.19.0\",\n    \"tree-sitter-python\": \"^0.19.0\",\n    \"tree-sitter-ruby\": \"^0.19.0\",\n    \"tree-sitter-rust\": \"^0.19.1\",\n    \"tree-sitter-scala\": \"^0.19.0\",\n    \"tree-sitter-typescript\": \"^0.19.0\",\n    \"typeorm\": \"^0.3.10\",\n    \"unique-names-generator\": \"^4.7.1\"\n  },\n  \"devDependencies\": {\n    \"@nestjs/cli\": \"^9.0.0\",\n    \"@nestjs/schematics\": \"^9.0.0\",\n    \"@nestjs/testing\": \"^9.0.0\",\n    \"@types/express\": \"^4.17.13\",\n    \"@types/express-session\": \"^1.17.5\",\n    \"@types/express-socket.io-session\": \"^1.3.6\",\n    \"@types/jest\": \"28.1.8\",\n    \"@types/node\": \"^20.0.0\",\n    \"@types/supertest\": \"^2.0.11\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.0.0\",\n    \"@typescript-eslint/parser\": \"^5.0.0\",\n    \"eslint\": \"^8.0.1\",\n    \"eslint-config-prettier\": \"^8.3.0\",\n    \"eslint-plugin-prettier\": \"^4.0.0\",\n    \"jest\": \"28.1.3\",\n    \"prettier\": \"^2.3.2\",\n    \"source-map-support\": \"^0.5.20\",\n    \"supertest\": \"^6.1.3\",\n    \"ts-jest\": \"28.0.8\",\n    \"ts-loader\": \"^9.2.3\",\n    \"ts-node\": \"^10.0.0\",\n    \"tsconfig-paths\": \"4.1.0\",\n    \"typescript\": \"^4.7.4\"\n  },\n  \"jest\": {\n    \"moduleFileExtensions\": [\n      \"node\",\n      \"js\",\n      \"json\",\n      \"ts\"\n    ],\n    \"rootDir\": \"src\",\n    \"testRegex\": \".*\\\\.spec\\\\.ts$\",\n    \"transform\": {\n      \"^.+\\\\.(t|j)s$\": \"ts-jest\"\n    },\n    \"collectCoverageFrom\": [\n      \"**/*.(t|j)s\"\n    ],\n    \"coverageDirectory\": \"../coverage\",\n    \"testEnvironment\": \"node\",\n    \"moduleNameMapper\": {\n      \"^src/(.*)$\": \"<rootDir>/$1\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/scripts/seed-local.sh",
    "content": "#!/bin/bash\n\nyarn command import-projects &&\nyarn command sync-projects &&\nyarn command import-files &&\nyarn command import-challenges\n"
  },
  {
    "path": "packages/back-nest/scripts/seed-production.sh",
    "content": "#!/bin/bash\nrailway run ./seed-local.sh\n"
  },
  {
    "path": "packages/back-nest/src/app.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { ConfigModule } from '@nestjs/config';\nimport { GithubConnectorModule } from './connectors/github/github.module';\nimport { ProjectsModule } from './projects/projects.module';\nimport { ChallengesModule } from './challenges/challenges.module';\nimport { UsersModule } from './users/users.module';\nimport { PostgresModule } from './database.module';\nimport { RacesModule } from './races/races.module';\nimport { SeederModule } from './seeder/seeder.module';\nimport { ResultsModule } from './results/results.module';\nimport { AuthModule } from './auth/auth.module';\n\n@Module({\n  imports: [\n    ChallengesModule,\n    ConfigModule.forRoot(),\n    GithubConnectorModule,\n    PostgresModule,\n    ProjectsModule,\n    RacesModule,\n    ResultsModule,\n    SeederModule,\n    UsersModule,\n    AuthModule,\n  ],\n  controllers: [],\n  providers: [],\n})\nexport class AppModule {}\n"
  },
  {
    "path": "packages/back-nest/src/auth/auth.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { PassportModule } from '@nestjs/passport';\nimport { ConfigModule } from '@nestjs/config';\nimport {\n  AuthController,\n  GithubAuthController,\n} from './github/github.controller';\nimport { GithubStrategy } from './github/github.strategy';\nimport { UsersModule } from 'src/users/users.module';\nimport { RacesModule } from 'src/races/races.module';\n\n@Module({\n  imports: [\n    PassportModule.register({\n      // session: true,\n    }),\n    ConfigModule,\n    UsersModule,\n    RacesModule,\n  ],\n  controllers: [GithubAuthController, AuthController],\n  providers: [GithubStrategy],\n})\nexport class AuthModule {}\n"
  },
  {
    "path": "packages/back-nest/src/auth/github/github.controller.ts",
    "content": "import {\n  Controller,\n  Delete,\n  Get,\n  HttpException,\n  Req,\n  Res,\n  UseGuards,\n} from '@nestjs/common';\nimport { Request, Response } from 'express';\nimport { cookieName } from 'src/sessions/session.middleware';\nimport { User } from 'src/users/entities/user.entity';\nimport { GithubOauthGuard } from './github.guard';\n\n@Controller('auth')\nexport class AuthController {\n  @Delete()\n  async logout(@Req() request: Request, @Res() response: Response) {\n    await new Promise<void>((resolve, reject) =>\n      request.session?.destroy((err) => {\n        console.log('session destroyed', { err });\n        if (err) {\n          return reject(err);\n        }\n        return resolve();\n      }),\n    );\n    response.clearCookie(cookieName);\n    return response.send({\n      ok: true,\n    });\n  }\n}\n\n@Controller('auth/github')\nexport class GithubAuthController {\n  @Get()\n  @UseGuards(GithubOauthGuard)\n  async githubLogin() {\n    // NOTE: the GithubOauthGuard initiates the authentication flow\n  }\n\n  @Get('callback')\n  @UseGuards(GithubOauthGuard)\n  async githubCallback(\n    @Req() request: Request,\n    @Res({ passthrough: true }) response: Response,\n  ) {\n    if (!request.session) {\n      throw new HttpException('Internal server error', 500);\n    }\n    request.session.user = request.user as User;\n    const next =\n      process.env.NODE_ENV === 'production'\n        ? 'https://www.speedtyper.dev'\n        : 'http://localhost:3001';\n    response.redirect(next);\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/auth/github/github.guard.ts",
    "content": "import { ExecutionContext, Injectable } from '@nestjs/common';\nimport { AuthGuard, IAuthModuleOptions } from '@nestjs/passport';\nimport { Request } from 'express';\n\n@Injectable()\nexport class GithubOauthGuard extends AuthGuard('github') {\n  getAuthenticateOptions(context: ExecutionContext): IAuthModuleOptions {\n    const request = context.switchToHttp().getRequest<Request>();\n    return {\n      state: this.getState(request),\n      session: true,\n    };\n  }\n\n  getState(request: Request) {\n    const { next } = request.query as Record<string, string>;\n    const queryParams = next\n      ? {\n          next,\n        }\n      : {};\n    const state = new URLSearchParams(queryParams).toString();\n    return state;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/auth/github/github.strategy.ts",
    "content": "import { PassportStrategy } from '@nestjs/passport';\nimport { Injectable } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport { Profile, Strategy } from 'passport-github';\nimport { UserService } from 'src/users/services/user.service';\nimport { UpsertGithubUserDTO } from 'src/users/entities/upsertGithubUserDTO';\n\n@Injectable()\nexport class GithubStrategy extends PassportStrategy(Strategy, 'github') {\n  constructor(cfg: ConfigService, private userService: UserService) {\n    const BASE_URL =\n      process.env.NODE_ENV === 'production'\n        ? 'https://v3.speedtyper.dev'\n        : 'http://localhost:1337';\n    super({\n      clientID: cfg.get<string>('GITHUB_CLIENT_ID'),\n      clientSecret: cfg.get<string>('GITHUB_CLIENT_SECRET'),\n      callbackURL: `${BASE_URL}/api/auth/github/callback`,\n      scope: ['public_profile'],\n    });\n  }\n\n  async validate(\n    _accessToken: string,\n    _refreshToken: string,\n    profile: Profile,\n  ) {\n    const upsertUserDTO = UpsertGithubUserDTO.fromGithubProfile(profile);\n    const user = await this.userService.upsertGithubUser(\n      upsertUserDTO.toUser(),\n    );\n    return user;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/challenges/challenges.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { GithubConnectorModule } from 'src/connectors/github/github.module';\nimport { ProjectsModule } from 'src/projects/projects.module';\nimport { CalculateLanguageRunner } from './commands/calculate-language-runner';\nimport { ChallengeImportRunner } from './commands/challenge-import-runner';\nimport { ReformatChallengesRunner } from './commands/reformat-challenges-runner';\nimport { UnsyncedFileImportRunner } from './commands/unsynced-file-import-runner';\nimport { Challenge } from './entities/challenge.entity';\nimport { UnsyncedFile } from './entities/unsynced-file.entity';\nimport { LanguageController } from './languages.controller';\nimport { ChallengeService } from './services/challenge.service';\nimport { LiteralService } from './services/literal.service';\nimport { ParserService } from './services/parser.service';\nimport { UnsyncedFileFilterer } from './services/unsynced-file-filterer';\nimport { UnsyncedFileImporter } from './services/unsynced-file-importer';\nimport { UnsyncedFileService } from './services/unsynced-file.service';\n\n@Module({\n  imports: [\n    TypeOrmModule.forFeature([UnsyncedFile, Challenge]),\n    GithubConnectorModule,\n    ProjectsModule,\n  ],\n  controllers: [LanguageController],\n  providers: [\n    ParserService,\n    ChallengeService,\n    LiteralService,\n    ChallengeImportRunner,\n    UnsyncedFileFilterer,\n    UnsyncedFileImporter,\n    UnsyncedFileImportRunner,\n    UnsyncedFileService,\n    CalculateLanguageRunner,\n    ReformatChallengesRunner,\n  ],\n  exports: [ChallengeService, LiteralService],\n})\nexport class ChallengesModule {}\n"
  },
  {
    "path": "packages/back-nest/src/challenges/commands/calculate-language-runner.ts",
    "content": "import { InjectRepository } from '@nestjs/typeorm';\nimport { Command, CommandRunner } from 'nest-commander';\nimport { Repository } from 'typeorm';\nimport { Challenge } from '../entities/challenge.entity';\n\n@Command({\n  name: 'calculate-language',\n  arguments: '',\n  options: {},\n})\nexport class CalculateLanguageRunner extends CommandRunner {\n  constructor(\n    @InjectRepository(Challenge)\n    private repository: Repository<Challenge>,\n  ) {\n    super();\n  }\n\n  async run(): Promise<void> {\n    const stream = await this.repository\n      .createQueryBuilder('ch')\n      .select('id, path')\n      .where('ch.language IS NULL')\n      .stream();\n\n    const updatesByLanguage: Record<string, string[]> = {};\n\n    for await (const { id, path } of stream) {\n      const dotSplitted = path.split('.');\n      const language = dotSplitted[dotSplitted.length - 1];\n      if (!updatesByLanguage[language]) {\n        updatesByLanguage[language] = [];\n      }\n      updatesByLanguage[language].push(id);\n    }\n\n    await Promise.all(\n      Object.entries(updatesByLanguage).map(async ([language, ids]) => {\n        await this.repository.update(ids, { language });\n      }),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/challenges/commands/challenge-import-runner.ts",
    "content": "import { Command, CommandRunner } from 'nest-commander';\nimport { GithubAPI } from 'src/connectors/github/services/github-api';\nimport { Challenge } from '../entities/challenge.entity';\nimport { UnsyncedFile } from '../entities/unsynced-file.entity';\nimport { ChallengeService } from '../services/challenge.service';\nimport { ParserService } from '../services/parser.service';\nimport { UnsyncedFileService } from '../services/unsynced-file.service';\n\n@Command({\n  name: 'import-challenges',\n  arguments: '',\n  options: {},\n})\nexport class ChallengeImportRunner extends CommandRunner {\n  constructor(\n    private api: GithubAPI,\n    private unsynced: UnsyncedFileService,\n    private parserService: ParserService,\n    private challengeService: ChallengeService,\n  ) {\n    super();\n  }\n  async run(): Promise<void> {\n    let filesSynced = 0;\n    const files = await this.unsynced.findAllWithProject();\n    for (const file of files) {\n      const challenges = await this.syncChallengesFromFile(file);\n      filesSynced++;\n      console.info(\n        `[challenge-import]: ${filesSynced}/${files.length} synced. Challenges added=${challenges.length}`,\n      );\n    }\n  }\n\n  private async syncChallengesFromFile(file: UnsyncedFile) {\n    const blob = await this.api.fetchBlob(\n      file.project.fullName,\n      file.currentSha,\n    );\n    const nodes = this.parseNodesFromContent(file.path, blob.content);\n    const challenges = nodes.map((node) =>\n      Challenge.fromTSNode(file.project, file, node),\n    );\n    await this.challengeService.upsert(challenges);\n    await this.unsynced.remove([file]);\n    return challenges;\n  }\n\n  private parseNodesFromContent(path: string, base64Content: string) {\n    const fileExtension = path.split('.').pop();\n    const parser = this.parserService.getParser(fileExtension);\n    const content = Buffer.from(base64Content, 'base64').toString();\n    const nodes = parser.parseTrackedNodes(content);\n    return nodes;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/challenges/commands/reformat-challenges-runner.ts",
    "content": "import { InjectRepository } from '@nestjs/typeorm';\nimport { Command, CommandRunner } from 'nest-commander';\nimport { Repository } from 'typeorm';\nimport { Challenge } from '../entities/challenge.entity';\nimport { getFormattedText } from '../services/parser.service';\n\n@Command({\n  name: 'reformat-challenges',\n  arguments: '',\n  options: {},\n})\nexport class ReformatChallengesRunner extends CommandRunner {\n  constructor(\n    @InjectRepository(Challenge)\n    private repository: Repository<Challenge>,\n  ) {\n    super();\n  }\n\n  async run(): Promise<void> {\n    const stream = await this.repository\n      .createQueryBuilder('ch')\n      .select('id, content')\n      .stream();\n\n    const pendingUpdates = [];\n    for await (const { id, content } of stream) {\n      const formattedContent = getFormattedText(content);\n      if (formattedContent !== content) {\n        pendingUpdates.push(\n          this.repository.update({ id }, { content: formattedContent }),\n        );\n      }\n    }\n\n    await Promise.all(pendingUpdates);\n\n    console.log(`Reformatted ${pendingUpdates.length} challenges`);\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/challenges/commands/unsynced-file-import-runner.ts",
    "content": "import { Command, CommandRunner } from 'nest-commander';\nimport { ProjectService } from 'src/projects/services/project.service';\nimport { UnsyncedFileImporter } from '../services/unsynced-file-importer';\n\n@Command({\n  name: 'import-files',\n  arguments: '',\n  options: {},\n})\nexport class UnsyncedFileImportRunner extends CommandRunner {\n  constructor(\n    private projectService: ProjectService,\n    private importer: UnsyncedFileImporter,\n  ) {\n    super();\n  }\n  async run(): Promise<void> {\n    const projects = await this.projectService.findAll();\n    for (const project of projects) {\n      // Only sync unsynced projects for now\n      if (!project.syncedSha) {\n        const sha = await this.importer.import(project);\n        await this.projectService.updateSyncedSha(project.id, sha);\n        console.info(`[FileImport]: Imported files for ${project.fullName}`);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/challenges/entities/challenge.entity.ts",
    "content": "import TSParser from 'tree-sitter';\nimport { Project } from 'src/projects/entities/project.entity';\nimport {\n  Entity,\n  PrimaryGeneratedColumn,\n  ManyToOne,\n  Column,\n  OneToMany,\n} from 'typeorm';\nimport { UnsyncedFile } from './unsynced-file.entity';\nimport { GithubAPI } from 'src/connectors/github/services/github-api';\nimport { Result } from 'src/results/entities/result.entity';\nimport { getFormattedText } from '../services/parser.service';\n\n@Entity()\nexport class Challenge {\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n  @Column({ select: false })\n  sha: string;\n  @Column({ select: false })\n  treeSha: string;\n  @Column({ nullable: false })\n  language: string;\n  @Column()\n  path: string;\n  @Column({ unique: true })\n  url: string;\n  @Column({ unique: true })\n  content: string;\n  @ManyToOne(() => Project, (project) => project.files)\n  project: Project;\n  @OneToMany(() => Result, (result) => result.user)\n  results: Result[];\n\n  static fromTSNode(\n    project: Project,\n    file: UnsyncedFile,\n    node: TSParser.SyntaxNode,\n  ) {\n    const challenge = new Challenge();\n    challenge.path = file.path;\n    challenge.sha = file.currentSha;\n    challenge.treeSha = file.currentTreeSha;\n    challenge.project = project;\n    challenge.content = getFormattedText(node.text);\n    challenge.url = GithubAPI.getBlobPermaLink(\n      project.fullName,\n      file.currentTreeSha,\n      file.path,\n      // NOTE: row is 0 indexed, while #L is 1 indexed\n      node.startPosition.row + 1,\n      node.endPosition.row + 1,\n    );\n    const dotSplitPath = file.path.split('.');\n    challenge.language = dotSplitPath[dotSplitPath.length - 1];\n    return challenge;\n  }\n  static getStrippedCode(code: string) {\n    const strippedCode = code\n      .split('\\n')\n      .map((subText) => subText.trimStart())\n      .join('\\n');\n    return strippedCode;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/challenges/entities/language.dto.ts",
    "content": "import { IsString } from 'class-validator';\n\nexport class LanguageDTO {\n  @IsString()\n  language: string;\n  @IsString()\n  name: string;\n}\n"
  },
  {
    "path": "packages/back-nest/src/challenges/entities/unsynced-file.entity.ts",
    "content": "import { GithubNode } from 'src/connectors/github/schemas/github-tree.dto';\nimport { Project } from 'src/projects/entities/project.entity';\nimport {\n  Entity,\n  PrimaryGeneratedColumn,\n  ManyToOne,\n  Column,\n  Index,\n} from 'typeorm';\n\n@Entity()\n@Index(['path', 'project'], { unique: true })\nexport class UnsyncedFile {\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n  @Column()\n  path: string;\n  @Column()\n  currentSha: string;\n  @Column()\n  currentTreeSha: string;\n  @Column({ nullable: true })\n  syncedSha?: string;\n\n  @ManyToOne(() => Project, (project) => project.files)\n  project: Project;\n\n  static fromGithubNode(project: Project, treeSha: string, node: GithubNode) {\n    const file = new UnsyncedFile();\n    file.path = node.path;\n    file.currentSha = node.sha;\n    file.currentTreeSha = treeSha;\n    file.project = project;\n    return file;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/challenges/languages.controller.ts",
    "content": "import { Controller, Get } from '@nestjs/common';\nimport { LanguageDTO } from './entities/language.dto';\nimport { ChallengeService } from './services/challenge.service';\n\n@Controller('languages')\nexport class LanguageController {\n  constructor(private service: ChallengeService) {}\n  @Get()\n  getLeaderboard(): Promise<LanguageDTO[]> {\n    return this.service.getLanguages();\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/challenges/services/challenge.service.ts",
    "content": "import { BadRequestException, Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { Challenge } from '../entities/challenge.entity';\nimport { LanguageDTO } from '../entities/language.dto';\n\n@Injectable()\nexport class ChallengeService {\n  private static UpsertOptions = {\n    conflictPaths: ['content'],\n    skipUpdateIfNoValuesChanged: true,\n  };\n  constructor(\n    @InjectRepository(Challenge)\n    private challengeRepository: Repository<Challenge>,\n  ) {}\n\n  async upsert(challenges: Challenge[]): Promise<void> {\n    await this.challengeRepository.upsert(\n      challenges,\n      ChallengeService.UpsertOptions,\n    );\n  }\n\n  async getRandom(language?: string): Promise<Challenge> {\n    let query = this.challengeRepository\n      .createQueryBuilder('challenge')\n      .leftJoinAndSelect('challenge.project', 'project');\n\n    if (language) {\n      query = query.where('challenge.language = :language', {\n        language,\n      });\n    }\n\n    const randomChallenge = await query.orderBy('RANDOM()').getOne();\n\n    if (!randomChallenge)\n      throw new BadRequestException(`No challenges for language: ${language}`);\n\n    return randomChallenge;\n  }\n\n  async getLanguages(): Promise<LanguageDTO[]> {\n    const selectedLanguages = await this.challengeRepository\n      .createQueryBuilder()\n      .select('language')\n      .distinct()\n      .execute();\n\n    const languages = selectedLanguages.map(\n      ({ language }: { language: string }) => ({\n        language,\n        name: this.getLanguageName(language),\n      }),\n    );\n\n    languages.sort((a, b) => a.name.localeCompare(b.name));\n\n    return languages;\n  }\n\n  private getLanguageName(language: string): string {\n    const allLanguages = {\n      js: 'JavaScript',\n      ts: 'TypeScript',\n      rs: 'Rust',\n      c: 'C',\n      java: 'Java',\n      cpp: 'C++',\n      go: 'Go',\n      lua: 'Lua',\n      php: 'PHP',\n      py: 'Python',\n      rb: 'Ruby',\n      cs: 'C-Sharp',\n      scala: 'Scala',\n    };\n    return allLanguages[language];\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/challenges/services/literal.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\n\n@Injectable()\nexport class LiteralService {\n  calculateLiterals(code: string) {\n    const literals = code\n      .substring(0)\n      .split(/[.\\-=/_\\:\\;\\,\\}\\{\\)\\(\\\"\\'\\]\\[\\/\\#\\?\\>\\<\\&\\*]/)\n      .flatMap((r) => {\n        return r.split(/[\\n\\r\\s\\t]+/);\n      })\n      .filter(Boolean);\n    return literals;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/challenges/services/parser.service.ts",
    "content": "import * as TSParser from 'tree-sitter';\nimport { Injectable } from '@nestjs/common';\nimport { getTSLanguageParser } from './ts-parser.factory';\n\n// TODO: Chars like ♡ should be filtered out\n@Injectable()\nexport class ParserService {\n  getParser(language: string) {\n    const tsParser = getTSLanguageParser(language);\n    return new Parser(tsParser);\n  }\n}\n\nexport enum NodeTypes {\n  ClassDeclaration = 'class_declaration',\n  ClassDefinition = 'class_definition',\n  FunctionDeclaration = 'function_declaration',\n  FunctionDefinition = 'function_definition',\n  FunctionItem = 'function_item',\n  MethodDeclaration = 'method_declaration',\n  Module = 'module',\n  Call = 'call',\n  UsingDirective = 'using_directive',\n  NamespaceDeclaration = 'namespace_declaration',\n}\n\nexport class Parser {\n  private MAX_NODE_LENGTH = 300;\n  private MIN_NODE_LENGTH = 100;\n  private MAX_NUM_LINES = 11;\n  private MAX_LINE_LENGTH = 55;\n\n  constructor(private ts: TSParser) {}\n\n  parseTrackedNodes(content: string) {\n    const root = this.ts.parse(content).rootNode;\n    return this.filterNodes(root);\n  }\n\n  private filterNodes(root: TSParser.SyntaxNode) {\n    const nodes = root.children\n      .filter((n) => this.filterValidNodeTypes(n))\n      .filter((n) => this.filterLongNodes(n))\n      .filter((n) => this.filterShortNodes(n))\n      .filter((n) => this.filterTooLongLines(n))\n      .filter((n) => this.filterTooManyLines(n));\n    return nodes;\n  }\n\n  private filterValidNodeTypes(node: TSParser.SyntaxNode) {\n    switch (node.type) {\n      case NodeTypes.ClassDeclaration:\n      case NodeTypes.ClassDefinition:\n      case NodeTypes.FunctionDeclaration:\n      case NodeTypes.FunctionDefinition:\n      case NodeTypes.FunctionItem:\n      case NodeTypes.MethodDeclaration:\n      case NodeTypes.Module:\n      case NodeTypes.Call:\n      case NodeTypes.UsingDirective:\n      case NodeTypes.NamespaceDeclaration:\n        // We want method declarations if they are on the root node (i.e. golang)\n        return true;\n      default:\n        console.log(node.type);\n        return false;\n    }\n  }\n\n  private filterLongNodes(node: TSParser.SyntaxNode) {\n    return this.MAX_NODE_LENGTH > node.text.length;\n  }\n\n  private filterShortNodes(node: TSParser.SyntaxNode) {\n    return node.text.length > this.MIN_NODE_LENGTH;\n  }\n\n  private filterTooManyLines(node: TSParser.SyntaxNode) {\n    const lines = node.text.split('\\n');\n    return lines.length <= this.MAX_NUM_LINES;\n  }\n\n  private filterTooLongLines(node: TSParser.SyntaxNode) {\n    for (const line of node.text.split('\\n')) {\n      if (line.length > this.MAX_LINE_LENGTH) {\n        return false;\n      }\n    }\n    return true;\n  }\n}\n\nexport function removeDuplicateNewLines(rawText: string) {\n  const newLine = '\\n';\n  const duplicateNewLine = '\\n\\n';\n  let newRawText = rawText;\n  let prevRawText = rawText;\n  do {\n    prevRawText = newRawText;\n    newRawText = newRawText.replaceAll(duplicateNewLine, newLine);\n  } while (newRawText !== prevRawText);\n  return newRawText;\n}\n\nexport function replaceTabsWithSpaces(rawText: string) {\n  const tab = '\\t';\n  const spaces = '  ';\n  return rawText.replaceAll(tab, spaces);\n}\n\nexport function removeTrailingSpaces(rawText: string) {\n  return rawText\n    .split('\\n')\n    .map((line) => line.trimEnd())\n    .join('\\n');\n}\n\nexport function dedupeInnerSpaces(rawText: string) {\n  const innerSpaces = /(?<=\\S+)\\s+(?=\\S+)/g;\n  const space = ' ';\n  return rawText\n    .split('\\n')\n    .map((line) => line.replaceAll(innerSpaces, space))\n    .join('\\n');\n}\n\nexport function getFormattedText(rawText: string) {\n  rawText = replaceTabsWithSpaces(rawText);\n  rawText = removeTrailingSpaces(rawText);\n  rawText = removeDuplicateNewLines(rawText);\n  rawText = dedupeInnerSpaces(rawText);\n  return rawText;\n}\n"
  },
  {
    "path": "packages/back-nest/src/challenges/services/tests/parser.service.spec.ts",
    "content": "import { getFormattedText } from '../parser.service';\n\nconst dubbleNewLineInput = `func newGRPCProxyCommand() *cobra.Command {\n  lpc := &cobra.Command{\n    Use:   \"grpc-proxy <subcommand>\",\n    Short: \"grpc-proxy related command\",\n  }\n  lpc.AddCommand(newGRPCProxyStartCommand())\n\n  return lpc\n}`;\n\nconst trippleNewLineInput = `func newGRPCProxyCommand() *cobra.Command {\n  lpc := &cobra.Command{\n    Use:   \"grpc-proxy <subcommand>\",\n    Short: \"grpc-proxy related command\",\n  }\n  lpc.AddCommand(newGRPCProxyStartCommand())\n\n\n  return lpc\n}`;\n\nconst inputWithTabs = `func newGRPCProxyCommand() *cobra.Command {\n\\tlpc := &cobra.Command{\n\\t\\tUse:   \"grpc-proxy <subcommand>\",\n\\t\\tShort: \"grpc-proxy related command\",\n\\t}\n\\tlpc.AddCommand(newGRPCProxyStartCommand())\n\\treturn lpc\n}`;\n\nconst inputWithEmptyLineWithSpaces = `func newGRPCProxyCommand() *cobra.Command {\n  lpc := &cobra.Command{   \n    Use:   \"grpc-proxy <subcommand>\",\n    Short: \"grpc-proxy related command\",\n  }     \n   \n  lpc.AddCommand(newGRPCProxyStartCommand())\n  return lpc\n}`;\n\nconst inputWithTrailingSpaces = `func newGRPCProxyCommand() *cobra.Command {\n  lpc := &cobra.Command{   \n    Use:   \"grpc-proxy <subcommand>\",\n    Short: \"grpc-proxy related command\",\n  }     \n  lpc.AddCommand(newGRPCProxyStartCommand())\n  return lpc\n}`;\n\nconst inputWithStructAlignment = `func newGRPCProxyCommand() *cobra.Command {\n  lpc := &cobra.Command{\n    Use:   \"grpc-proxy <subcommand>\",\n    Short: \"grpc-proxy related command\",\n  }\n  lpc.AddCommand(newGRPCProxyStartCommand())\n  return lpc\n}`;\n\nconst output = `func newGRPCProxyCommand() *cobra.Command {\n  lpc := &cobra.Command{\n    Use: \"grpc-proxy <subcommand>\",\n    Short: \"grpc-proxy related command\",\n  }\n  lpc.AddCommand(newGRPCProxyStartCommand())\n  return lpc\n}`;\n\ndescribe('getFormattedText', () => {\n  it('should remove double newlines', () => {\n    const parsed = getFormattedText(dubbleNewLineInput);\n    expect(parsed).toEqual(output);\n  });\n  it('should remove tripple newlines', () => {\n    const parsed = getFormattedText(trippleNewLineInput);\n    expect(parsed).toEqual(output);\n  });\n  it('should replace tabs with spaces', () => {\n    const parsed = getFormattedText(inputWithTabs);\n    expect(parsed).toEqual(output);\n  });\n  it('should return the same if called twice', () => {\n    const firstParsed = getFormattedText(inputWithTabs);\n    const parsed = getFormattedText(firstParsed);\n    expect(parsed).toEqual(output);\n  });\n  it('should remove trailing spaces', () => {\n    const parsed = getFormattedText(inputWithTrailingSpaces);\n    expect(parsed).toEqual(output);\n  });\n  it('should remove empty line with spaces', () => {\n    const parsed = getFormattedText(inputWithEmptyLineWithSpaces);\n    expect(parsed).toEqual(output);\n  });\n  it('should dedupe multiple interior spaces', () => {\n    const parsed = getFormattedText(inputWithStructAlignment);\n    expect(parsed).toEqual(output);\n  });\n});\n"
  },
  {
    "path": "packages/back-nest/src/challenges/services/ts-parser.factory.ts",
    "content": "import * as TSParser from 'tree-sitter';\n\nimport * as js from 'tree-sitter-javascript';\nimport * as ts from 'tree-sitter-typescript/typescript';\nimport * as java from 'tree-sitter-java';\nimport * as c from 'tree-sitter-c';\nimport * as cpp from 'tree-sitter-cpp';\nimport * as lua from 'tree-sitter-lua';\nimport * as php from 'tree-sitter-php';\nimport * as py from 'tree-sitter-python';\nimport * as rb from 'tree-sitter-ruby';\nimport * as cs from 'tree-sitter-c-sharp';\nimport * as go from 'tree-sitter-go';\nimport * as rs from 'tree-sitter-rust';\nimport * as scala from 'tree-sitter-scala';\n\nconst languageParserMap: { [key: string]: any } = {\n  js,\n  ts,\n  rs,\n  c,\n  java,\n  cpp,\n  go,\n  lua,\n  php,\n  py,\n  rb,\n  cs,\n  scala,\n};\n\nexport const getSupportFileExtensions = () => {\n  return Object.keys(languageParserMap).map((ext) => `.${ext}`);\n};\n\nexport class InvalidLanguage extends Error {\n  constructor(language: string) {\n    super(`Error getting parser for language='${language}'`);\n    Object.setPrototypeOf(this, InvalidLanguage.prototype);\n  }\n}\n\nexport const getTSLanguageParser = (language: string) => {\n  const langParser = languageParserMap[language];\n  if (!langParser) throw new InvalidLanguage(language);\n  const parser = new TSParser();\n  parser.setLanguage(langParser);\n  return parser;\n};\n"
  },
  {
    "path": "packages/back-nest/src/challenges/services/unsynced-file-filterer.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { GithubNode } from 'src/connectors/github/schemas/github-tree.dto';\nimport { getSupportFileExtensions } from './ts-parser.factory';\n\n@Injectable()\nexport class UnsyncedFileFilterer {\n  filter(nodes: GithubNode[]) {\n    return nodes\n      .filter(isBlobNode)\n      .filter(hasTrackedFileExt)\n      .filter(isNotExcludedPath);\n  }\n}\n\nfunction isBlobNode(node: GithubNode) {\n  return node.type === 'blob';\n}\n\nfunction hasTrackedFileExt(node: GithubNode) {\n  const trackedFileExtensions = getSupportFileExtensions();\n  for (const includedExt of trackedFileExtensions) {\n    if (node.path.endsWith(includedExt)) {\n      // ends with tracked file extension\n      return true;\n    }\n  }\n  // untracked file extension\n  return false;\n}\n\nfunction isNotExcludedPath(node: GithubNode) {\n  const excludedSubStrings = [\n    '.ci',\n    '.jenkins',\n    '.build',\n    '.idea',\n    '.devcontainer',\n    'migrations',\n    'benchmarks',\n    'build-tools',\n    'conventions',\n    'licenses',\n    'requirements',\n    '.svg',\n    'docs',\n    '.github',\n    'example',\n    'types',\n    'test',\n    '.pb.',\n    '.proto',\n    'doc',\n  ];\n  for (const excludeStr of excludedSubStrings) {\n    if (node.path.includes(excludeStr)) {\n      // is excluded path\n      return false;\n    }\n  }\n  // is not excluded path\n  return true;\n}\n"
  },
  {
    "path": "packages/back-nest/src/challenges/services/unsynced-file-importer.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { GithubAPI } from 'src/connectors/github/services/github-api';\nimport { Project } from 'src/projects/entities/project.entity';\nimport { UnsyncedFile } from '../entities/unsynced-file.entity';\nimport { UnsyncedFileFilterer } from './unsynced-file-filterer';\nimport { UnsyncedFileService } from './unsynced-file.service';\n\n@Injectable()\nexport class UnsyncedFileImporter {\n  constructor(\n    private api: GithubAPI,\n    private filterer: UnsyncedFileFilterer,\n    private svc: UnsyncedFileService,\n  ) {}\n  async import(project: Project) {\n    const root = await this.api.fetchTree(\n      project.fullName,\n      project.defaultBranch,\n    );\n    const nodes = this.filterer.filter(root.tree);\n    const files = nodes.map((node) =>\n      UnsyncedFile.fromGithubNode(project, root.sha, node),\n    );\n    await this.svc.bulkUpsert(files);\n    return root.sha;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/challenges/services/unsynced-file.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { UnsyncedFile } from '../entities/unsynced-file.entity';\n\n@Injectable()\nexport class UnsyncedFileService {\n  private static UpsertOptions = {\n    conflictPaths: ['path', 'project'],\n    skipUpdateIfNoValuesChanged: true,\n  };\n  constructor(\n    @InjectRepository(UnsyncedFile)\n    private filesRepository: Repository<UnsyncedFile>,\n  ) {}\n\n  async bulkUpsert(files: UnsyncedFile[]): Promise<void> {\n    await this.filesRepository.upsert(files, UnsyncedFileService.UpsertOptions);\n  }\n\n  async findAllWithProject(): Promise<UnsyncedFile[]> {\n    const files = await this.filesRepository.find({\n      relations: {\n        project: true,\n      },\n    });\n    return files;\n  }\n\n  async remove(files: UnsyncedFile[]): Promise<void> {\n    await this.filesRepository.remove(files);\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/commands.ts",
    "content": "import { CommandFactory } from 'nest-commander';\nimport { AppModule } from './app.module';\n\nasync function runCommand() {\n  await CommandFactory.run(AppModule, ['warn', 'error']);\n}\n\nrunCommand();\n"
  },
  {
    "path": "packages/back-nest/src/config/cors.ts",
    "content": "import { GatewayMetadata } from '@nestjs/websockets';\n\nexport const getAllowedOrigins = () => {\n  return process.env.NODE_ENV === 'production'\n    ? ['https://speedtyper.dev', 'https://www.speedtyper.dev']\n    : ['http://localhost:3001'];\n};\n\nexport const gatewayMetadata: GatewayMetadata = {\n  cors: {\n    origin: getAllowedOrigins(),\n    methods: ['GET', 'POST'],\n    credentials: true,\n  },\n};\n"
  },
  {
    "path": "packages/back-nest/src/config/postgres.ts",
    "content": "import * as dotenv from 'dotenv';\nimport { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';\ndotenv.config({ path: __dirname + '/../../.env' });\n\nexport const pgOptions: Partial<PostgresConnectionOptions> = {\n  url: process.env.DATABASE_PRIVATE_URL,\n  extra: {\n    // 120 seconds idle timeout\n    idleTimeoutMillis: 120000,\n    max: 10,\n  },\n};\n"
  },
  {
    "path": "packages/back-nest/src/connectors/github/github.module.ts",
    "content": "import { HttpModule } from '@nestjs/axios';\nimport { Module } from '@nestjs/common';\nimport { ConfigModule } from '@nestjs/config';\nimport { GithubAPI } from './services/github-api';\n\n@Module({\n  imports: [HttpModule, ConfigModule],\n  providers: [GithubAPI],\n  exports: [GithubAPI],\n})\nexport class GithubConnectorModule {}\n"
  },
  {
    "path": "packages/back-nest/src/connectors/github/schemas/github-blob.dto.ts",
    "content": "import { IsEnum, IsNumber, IsString } from 'class-validator';\n\nexport enum GithubBlobEncoding {\n  base64 = 'base64',\n}\n\nexport class GithubBlob {\n  @IsString()\n  sha: string;\n  @IsString()\n  node_id: string;\n  @IsNumber()\n  size: number;\n  @IsString()\n  url: string;\n  @IsString()\n  content: string;\n  @IsEnum(GithubBlobEncoding)\n  encoding: 'base64';\n}\n"
  },
  {
    "path": "packages/back-nest/src/connectors/github/schemas/github-repository.dto.ts",
    "content": "import { IsNumber, IsString, ValidateIf } from 'class-validator';\n\nexport class GithubLicense {\n  @IsString()\n  name: string;\n}\n\nexport class GithubOwner {\n  @IsString()\n  login: string;\n  @IsNumber()\n  id: number;\n  @IsString()\n  avatar_url: string;\n  @IsString()\n  html_url: string;\n}\n\nexport class GithubRepository {\n  @IsNumber()\n  id: number;\n  @IsString()\n  node_id: string;\n  @IsString()\n  name: string;\n  @IsString()\n  full_name: string;\n  @IsString()\n  html_url: string;\n  @IsString()\n  description: string;\n  @IsString()\n  url: string;\n  @IsString()\n  trees_url: string;\n  @IsString()\n  @ValidateIf((_: any, value: unknown) => value !== null)\n  homepage: string | null;\n  @IsNumber()\n  stargazers_count: number;\n  @IsString()\n  language: string;\n  @IsString()\n  default_branch: string;\n  license: GithubLicense;\n  owner: GithubOwner;\n}\n"
  },
  {
    "path": "packages/back-nest/src/connectors/github/schemas/github-tree.dto.ts",
    "content": "import { IsEnum, IsNumber, IsString } from 'class-validator';\n\nexport enum GithubNodeType {\n  blob = 'blob',\n  tree = 'tree',\n}\n\nexport class GithubNode {\n  @IsString()\n  path: string;\n  @IsString()\n  mode: string;\n  @IsEnum(GithubNodeType)\n  type: GithubNodeType;\n  @IsString()\n  sha: string;\n  @IsNumber()\n  size?: number;\n  @IsString()\n  url: string;\n}\n\nexport class GithubTree {\n  @IsString()\n  sha: string;\n  @IsString()\n  url: string;\n  tree: GithubNode[];\n}\n"
  },
  {
    "path": "packages/back-nest/src/connectors/github/services/github-api.ts",
    "content": "import { AxiosResponse } from 'axios';\nimport { HttpService } from '@nestjs/axios';\nimport { Injectable } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport { firstValueFrom } from 'rxjs';\nimport { validateDTO } from 'src/utils/validateDTO';\nimport { GithubBlob } from '../schemas/github-blob.dto';\nimport { GithubRepository } from '../schemas/github-repository.dto';\nimport { GithubTree } from '../schemas/github-tree.dto';\n\n@Injectable()\nexport class GithubAPI {\n  private static BASE_URL = 'https://api.github.com';\n  private static REPOSITORIES_URL = `${GithubAPI.BASE_URL}/repos`;\n  private static REPOSITORY_URL = `${GithubAPI.REPOSITORIES_URL}/{fullName}`;\n  private static TREE_URL = `${GithubAPI.REPOSITORY_URL}/git/trees/{sha}?recursive=true`;\n  private static BLOB_URL = `${GithubAPI.REPOSITORY_URL}/git/blobs/{sha}`;\n  private static BLOB_HTML_PERMA_LINK = `https://github.com/{fullName}/blob/{treeSha}/{path}/#L{startLine}-L{endLine}`;\n\n  private token: string;\n\n  constructor(private readonly http: HttpService, cfg: ConfigService) {\n    this.token = getGithubAccessToken(cfg);\n  }\n\n  static getBlobPermaLink(\n    fullName: string,\n    treeSha: string,\n    path: string,\n    startLine: number,\n    endLine: number,\n  ) {\n    const url = GithubAPI.BLOB_HTML_PERMA_LINK.replace('{fullName}', fullName)\n      .replace('{treeSha}', treeSha)\n      .replace('{path}', path)\n      .replace('{startLine}', startLine.toString())\n      .replace('{endLine}', endLine.toString());\n    return url;\n  }\n\n  private async get(url: string) {\n    const resp = await firstValueFrom(\n      this.http.get(url, {\n        headers: {\n          Authorization: `token ${this.token}`,\n        },\n      }),\n    );\n    this.logRateLimit(resp);\n    return resp.data;\n  }\n\n  private logRateLimit(resp: AxiosResponse) {\n    const rateLimitResetSeconds = resp.headers['x-ratelimit-reset'];\n    const resetDate = new Date(parseInt(rateLimitResetSeconds) * 1000);\n    const rateLimitRemaining = resp.headers['x-ratelimit-remaining'];\n    console.log(\n      `GH Rate Limiting. Remaining: ${rateLimitRemaining} Reset: ${resetDate}`,\n    );\n  }\n\n  async fetchRepository(fullName: string): Promise<GithubRepository> {\n    const url = GithubAPI.REPOSITORY_URL.replace('{fullName}', fullName);\n    const rawData = await this.get(url);\n    const repository = await validateDTO(GithubRepository, rawData);\n    return repository;\n  }\n\n  async fetchTree(fullName: string, sha: string): Promise<GithubTree> {\n    const treeUrl = GithubAPI.TREE_URL.replace('{fullName}', fullName).replace(\n      '{sha}',\n      sha,\n    );\n    const rawData = await this.get(treeUrl);\n    const rootNode = await validateDTO(GithubTree, rawData);\n    return rootNode;\n  }\n\n  async fetchBlob(fullName: string, sha: string): Promise<GithubBlob> {\n    const url = GithubAPI.BLOB_URL.replace('{fullName}', fullName).replace(\n      '{sha}',\n      sha,\n    );\n    const rawData = await this.get(url);\n    const blob = await validateDTO(GithubBlob, rawData);\n    return blob;\n  }\n}\n\nfunction getGithubAccessToken(cfg: ConfigService) {\n  const token = cfg.get<string>('GITHUB_ACCESS_TOKEN');\n  if (!token) {\n    throw new Error(\n      `GITHUB_ACCESS_TOKEN is missing from environment variables`,\n    );\n  }\n  if (!token.startsWith('ghp_')) {\n    throw new Error(\n      `GITHUB_ACCESS_TOKEN is not a valid value. It should start with 'ghp_'`,\n    );\n  }\n  return token;\n}\n"
  },
  {
    "path": "packages/back-nest/src/database.module.ts",
    "content": "import { TypeOrmModule } from '@nestjs/typeorm';\nimport { DataSource } from 'typeorm';\nimport { pgOptions } from './config/postgres';\n\nconst entities = [__dirname + '/**/*.entity.{ts,js}'];\n\nexport const PostgresDataSource = new DataSource({\n  type: 'postgres',\n  synchronize: true,\n  entities,\n  ...pgOptions,\n});\n\nexport const PostgresModule = TypeOrmModule.forRootAsync({\n  useFactory: () => {\n    return {\n      type: 'postgres',\n      synchronize: true,\n      entities,\n      ...pgOptions,\n    };\n  },\n  dataSourceFactory: async () => {\n    return PostgresDataSource.initialize();\n  },\n});\n"
  },
  {
    "path": "packages/back-nest/src/filters/exception.filter.ts",
    "content": "import {\n  ExceptionFilter,\n  Catch,\n  ArgumentsHost,\n  HttpException,\n  HttpStatus,\n} from '@nestjs/common';\nimport { QueryFailedError } from 'typeorm';\n\ninterface ErrorResponse {\n  message: string;\n  status: number;\n}\n\n@Catch()\nexport class AllExceptionsFilter implements ExceptionFilter {\n  catch(exception: any, host: ArgumentsHost) {\n    const ctx = host.switchToHttp();\n    const response = ctx.getResponse();\n\n    let errorResponse: ErrorResponse;\n    const message = exception.message;\n\n    if (exception instanceof HttpException) {\n      errorResponse = { status: exception.getStatus(), message };\n    } else if (exception instanceof QueryFailedError) {\n      errorResponse = {\n        message: 'Internal server error',\n        status: HttpStatus.INTERNAL_SERVER_ERROR,\n      };\n    } else {\n      errorResponse = {\n        message: 'Internal server error',\n        status: HttpStatus.INTERNAL_SERVER_ERROR,\n      };\n    }\n\n    if (errorResponse.status === HttpStatus.INTERNAL_SERVER_ERROR) {\n      console.log(exception);\n    }\n\n    response.status(errorResponse.status).json({\n      statusCode: errorResponse.status,\n      message: errorResponse.message,\n    });\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/main.ts",
    "content": "import * as Sentry from '@sentry/node';\nimport { ValidationPipe } from '@nestjs/common';\nimport { NestFactory } from '@nestjs/core';\nimport { NestExpressApplication } from '@nestjs/platform-express';\nimport { AppModule } from './app.module';\nimport { getAllowedOrigins } from './config/cors';\nimport { guestUserMiddleware } from './middlewares/guest-user';\nimport { SessionAdapter } from './sessions/session.adapter';\nimport { getSessionMiddleware } from './sessions/session.middleware';\nimport { json } from 'express';\nimport { AllExceptionsFilter } from './filters/exception.filter';\n\nconst GLOBAl_API_PREFIX = 'api';\n\nasync function runServer() {\n  Sentry.init({\n    dsn: process.env.SENTRY_DSN,\n    tracesSampleRate: 0,\n  });\n  const port = process.env.PORT || 1337;\n  const app = await NestFactory.create<NestExpressApplication>(AppModule);\n  app.set('trust proxy', 1);\n  const sessionMiddleware = getSessionMiddleware();\n  app.enableCors({\n    origin: getAllowedOrigins(),\n    credentials: true,\n  });\n  app.use(json({ limit: '50mb' }));\n  app.use(sessionMiddleware);\n  app.use(guestUserMiddleware);\n  app.useWebSocketAdapter(new SessionAdapter(app, sessionMiddleware));\n  app.setGlobalPrefix(GLOBAl_API_PREFIX);\n  app.useGlobalFilters(new AllExceptionsFilter());\n  app.useGlobalPipes(new ValidationPipe());\n  await app.listen(port);\n}\n\nrunServer();\n"
  },
  {
    "path": "packages/back-nest/src/middlewares/guest-user.ts",
    "content": "import { NextFunction, Request, Response } from 'express';\nimport { User } from 'src/users/entities/user.entity';\n\nexport function guestUserMiddleware(\n  req: Request,\n  _: Response,\n  next: NextFunction,\n) {\n  if (req.session && !req.session?.user) {\n    req.session.user = User.generateAnonymousUser();\n  }\n  next();\n}\n"
  },
  {
    "path": "packages/back-nest/src/projects/commands/import-untracked-projects-runner.ts",
    "content": "import { Command, CommandRunner } from 'nest-commander';\nimport { ProjectService } from '../services/project.service';\nimport { ProjectsFromFileReader } from '../services/projects-from-file-reader';\nimport { UntrackedProjectService } from '../services/untracked-projects.service';\n\n@Command({\n  name: 'import-projects',\n  arguments: '',\n  options: {},\n})\nexport class ImportUntrackedProjectsRunner extends CommandRunner {\n  constructor(\n    private reader: ProjectsFromFileReader,\n    private untracked: UntrackedProjectService,\n    private synced: ProjectService,\n  ) {\n    super();\n  }\n  async run(): Promise<void> {\n    for await (const project of this.reader.readProjects()) {\n      const syncedProject = await this.synced.findByFullName(project);\n      if (!syncedProject) {\n        await this.untracked.bulkUpsert([project]);\n        console.info(`[ProjectImport]: Imported ${project}`);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/projects/commands/sync-untracked-projects-runner.ts",
    "content": "import { Command, CommandRunner } from 'nest-commander';\nimport { GithubAPI } from 'src/connectors/github/services/github-api';\nimport { Project } from '../entities/project.entity';\nimport { ProjectService } from '../services/project.service';\nimport { UntrackedProjectService } from '../services/untracked-projects.service';\n\n@Command({\n  name: 'sync-projects',\n  arguments: '',\n  options: {},\n})\nexport class SyncUntrackedProjectsRunner extends CommandRunner {\n  constructor(\n    private untracked: UntrackedProjectService,\n    private api: GithubAPI,\n    private synced: ProjectService,\n  ) {\n    super();\n  }\n  async run(): Promise<void> {\n    const untracked = await this.untracked.findAll();\n    for (const untrackedProject of untracked) {\n      const repository = await this.api.fetchRepository(\n        untrackedProject.fullName,\n      );\n      const project = Project.fromGithubRepository(\n        untrackedProject,\n        repository,\n      );\n      await this.synced.bulkUpsert([project]);\n      await this.untracked.remove([untrackedProject]);\n      console.info(`[ProjectSync]: Synced ${project.fullName}`);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/projects/entities/project.entity.ts",
    "content": "import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';\nimport { GithubRepository } from 'src/connectors/github/schemas/github-repository.dto';\nimport { UntrackedProject } from './untracked-project.entity';\nimport { UnsyncedFile } from 'src/challenges/entities/unsynced-file.entity';\n\n@Entity()\nexport class Project {\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n  @Column({ unique: true })\n  fullName: string;\n  @Column({ unique: true })\n  htmlUrl: string;\n  @Column()\n  language: string;\n  @Column()\n  stars: number;\n  @Column()\n  licenseName: string;\n  @Column()\n  ownerAvatar: string;\n  @Column()\n  defaultBranch: string;\n\n  @OneToMany(() => UnsyncedFile, (file) => file.project)\n  files: File[];\n\n  @Column({ nullable: true })\n  syncedSha?: string;\n\n  static fromGithubRepository(\n    tracked: UntrackedProject,\n    repo: GithubRepository,\n  ) {\n    const project = new Project();\n    project.fullName = tracked.fullName;\n    project.htmlUrl = repo.html_url;\n    project.stars = repo.stargazers_count;\n    project.language = repo.language;\n    project.licenseName = repo.license?.name ?? 'Other';\n    project.ownerAvatar = repo.owner.avatar_url;\n    project.defaultBranch = repo.default_branch;\n    return project;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/projects/entities/untracked-project.entity.ts",
    "content": "import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class UntrackedProject {\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n\n  @Column({ unique: true })\n  fullName: string;\n}\n"
  },
  {
    "path": "packages/back-nest/src/projects/project.controller.ts",
    "content": "import { Controller, Get } from '@nestjs/common';\nimport { ProjectService } from './services/project.service';\n\n@Controller('projects')\nexport class ProjectController {\n  constructor(private projectService: ProjectService) {}\n  @Get('languages')\n  getLeaderboard(): Promise<string[]> {\n    return this.projectService.getLanguages();\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/projects/projects.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { GithubConnectorModule } from 'src/connectors/github/github.module';\nimport { ImportUntrackedProjectsRunner } from './commands/import-untracked-projects-runner';\nimport { SyncUntrackedProjectsRunner } from './commands/sync-untracked-projects-runner';\nimport { Project } from './entities/project.entity';\nimport { UntrackedProject } from './entities/untracked-project.entity';\nimport { ProjectController } from './project.controller';\nimport { ProjectService } from './services/project.service';\nimport { ProjectsFromFileReader } from './services/projects-from-file-reader';\nimport { UntrackedProjectService } from './services/untracked-projects.service';\n\n@Module({\n  imports: [\n    TypeOrmModule.forFeature([Project, UntrackedProject]),\n    GithubConnectorModule,\n  ],\n  providers: [\n    UntrackedProjectService,\n    ProjectService,\n    ProjectsFromFileReader,\n    ImportUntrackedProjectsRunner,\n    SyncUntrackedProjectsRunner,\n  ],\n  controllers: [ProjectController],\n  exports: [ProjectService],\n})\nexport class ProjectsModule {}\n"
  },
  {
    "path": "packages/back-nest/src/projects/services/project.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { Project } from '../entities/project.entity';\n\n@Injectable()\nexport class ProjectService {\n  constructor(\n    @InjectRepository(Project)\n    private projectRepository: Repository<Project>,\n  ) {}\n\n  async bulkUpsert(projects: Project[]): Promise<void> {\n    await this.projectRepository.upsert(projects, ['fullName']);\n  }\n\n  async findByFullName(fullName: string) {\n    const project = await this.projectRepository.findOneBy({\n      fullName,\n    });\n    return project;\n  }\n\n  async updateSyncedSha(id: string, syncedSha: string) {\n    await this.projectRepository.update(\n      {\n        id,\n      },\n      { syncedSha },\n    );\n  }\n\n  async findAll(): Promise<Project[]> {\n    const projects = await this.projectRepository.find();\n    return projects;\n  }\n\n  async getLanguages(): Promise<string[]> {\n    const selectedLanguages = await this.projectRepository\n      .createQueryBuilder()\n      .select('language')\n      .distinct()\n      .execute();\n    return selectedLanguages.map((l: any) => l.language);\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/projects/services/projects-from-file-reader.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { createReadStream } from 'fs';\nimport { createInterface } from 'readline';\n\n@Injectable()\nexport class ProjectsFromFileReader {\n  private static FILE_PATH = './tracked-projects.txt';\n  async *readProjects() {\n    const stream = createReadStream(ProjectsFromFileReader.FILE_PATH);\n    const rl = createInterface({\n      input: stream,\n      crlfDelay: Infinity,\n    });\n    for await (const line of rl) {\n      const slug = line.trim();\n      yield validateProjectName(slug);\n    }\n  }\n}\n\nexport function validateProjectName(slug: string) {\n  let [owner, repo] = slug.split('/');\n  owner = owner.trim();\n  repo = repo.trim();\n  if (!owner || !repo) {\n    throw new Error(slug);\n  }\n  return [owner, repo].join('/');\n}\n"
  },
  {
    "path": "packages/back-nest/src/projects/services/untracked-projects.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { UntrackedProject } from '../entities/untracked-project.entity';\n\n@Injectable()\nexport class UntrackedProjectService {\n  constructor(\n    @InjectRepository(UntrackedProject)\n    private untrackedProjects: Repository<UntrackedProject>,\n  ) {}\n\n  async bulkUpsert(names: string[]): Promise<void> {\n    const partialProjects = names.map((fullName) => ({ fullName }));\n    await this.untrackedProjects.upsert(partialProjects, ['fullName']);\n  }\n\n  async remove(untrackedProjects: UntrackedProject[]): Promise<void> {\n    await this.untrackedProjects.remove(untrackedProjects);\n  }\n\n  async findAll(): Promise<UntrackedProject[]> {\n    return await this.untrackedProjects.find();\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/races/entities/race-settings.dto.ts",
    "content": "import { IsBoolean, IsOptional, IsString } from 'class-validator';\n\nexport class RaceSettingsDTO {\n  @IsString()\n  @IsOptional()\n  language: string;\n\n  @IsBoolean()\n  @IsOptional()\n  isPublic: boolean;\n}\n"
  },
  {
    "path": "packages/back-nest/src/races/race.controllers.ts",
    "content": "import {\n  BadRequestException,\n  Controller,\n  Get,\n  Param,\n  Post,\n  Req,\n} from '@nestjs/common';\nimport { PublicRace, RaceManager } from './services/race-manager.service';\nimport { Request } from 'express';\n\n@Controller('races')\nexport class RacesController {\n  constructor(private raceManager: RaceManager) {}\n  @Get()\n  getRaces(): PublicRace[] {\n    return this.raceManager.getPublicRaces();\n  }\n\n  @Get('online')\n  getOnlineCount(): { online: number } {\n    const online = this.raceManager.getOnlineCount();\n    return {\n      online,\n    };\n  }\n\n  @Post('online')\n  toggleOnlineState(@Req() request: Request): { isPublic: boolean } {\n    const userId = request.session.user.id;\n    const raceId = request.session.raceId;\n    const race = this.raceManager.getRace(raceId);\n    if (race.owner !== userId) {\n      throw new BadRequestException();\n    }\n    const isPublic = race.togglePublic();\n    return {\n      isPublic,\n    };\n  }\n\n  @Get(':raceId/status')\n  getRaceStatus(\n    @Req() request: Request,\n    @Param('raceId') raceId: string,\n  ): { ok: boolean } {\n    try {\n      const userId = request.session.user.id;\n      const player = this.raceManager.getPlayer(raceId, userId);\n      return { ok: !!player };\n    } catch (err) {\n      return { ok: false };\n    }\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/races/race.exceptions.ts",
    "content": "import * as Sentry from '@sentry/node';\nimport { ArgumentsHost, Catch } from '@nestjs/common';\nimport { BaseWsExceptionFilter } from '@nestjs/websockets';\nimport { Socket } from 'socket.io';\nimport { InvalidKeystrokeException } from './services/keystroke-validator.service';\nimport { RaceEvents } from './services/race-events.service';\nimport { RaceDoesNotExist } from './services/race-manager.service';\nimport { SessionState } from './services/session-state.service';\n\nexport function getSocketFromArgs(host: ArgumentsHost): Socket {\n  const args = host.getArgs();\n  for (const arg of args) {\n    if (arg instanceof Socket) {\n      return arg;\n    }\n  }\n}\n\n@Catch(RaceDoesNotExist)\nexport class RaceDoesNotExistFilter extends BaseWsExceptionFilter {\n  raceEvents: RaceEvents;\n  sessionState: SessionState;\n  constructor() {\n    super();\n    this.raceEvents = new RaceEvents();\n    this.sessionState = new SessionState();\n  }\n\n  async catch(error: RaceDoesNotExist, host: ArgumentsHost) {\n    const socket = getSocketFromArgs(host);\n    this.sessionState.removeRaceID(socket);\n    this.raceEvents.raceDoesNotExist(socket, error.id);\n  }\n}\n\n@Catch(InvalidKeystrokeException)\nexport class InvalidKeystrokeFilter extends BaseWsExceptionFilter {\n  async catch(error: InvalidKeystrokeException) {\n    Sentry.withScope((scope) => {\n      const player = error.race.members[error.userId];\n      const data = {\n        challengeId: error.race.challenge.id,\n        expected: error.expected,\n        input: error.input,\n        keystroke: error.keystroke,\n        userId: error.userId,\n      };\n      const typedKeystrokes = player.typedKeyStrokes;\n      const validKeyStrokes = player.validKeyStrokes();\n      scope.setUser({\n        id:\n          process.env.NODE_ENV === 'production'\n            ? `${error.race.id}-${error.userId}`\n            : `[local-testing] ${error.race.id}`,\n      });\n      scope.setExtras({ error: data, typedKeystrokes, validKeyStrokes });\n      Sentry.captureException(error);\n    });\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/races/race.gateway.ts",
    "content": "import { UseFilters, UsePipes, ValidationPipe } from '@nestjs/common';\nimport {\n  SubscribeMessage,\n  WebSocketGateway,\n  WebSocketServer,\n} from '@nestjs/websockets';\nimport { Server, Socket } from 'socket.io';\nimport { gatewayMetadata } from 'src/config/cors';\nimport { RaceSettingsDTO } from './entities/race-settings.dto';\nimport {\n  InvalidKeystrokeFilter,\n  RaceDoesNotExistFilter,\n} from './race.exceptions';\nimport { AddKeyStrokeService } from './services/add-keystroke.service';\nimport { CountdownService } from './services/countdown.service';\nimport { Locker } from './services/locker.service';\nimport { RaceEvents } from './services/race-events.service';\nimport { RaceManager } from './services/race-manager.service';\nimport { KeystrokeDTO } from './services/race-player.service';\nimport { SessionState } from './services/session-state.service';\n\n@WebSocketGateway(gatewayMetadata)\nexport class RaceGateway {\n  @WebSocketServer()\n  server: Server;\n\n  constructor(\n    private raceManager: RaceManager,\n    private session: SessionState,\n    private raceEvents: RaceEvents,\n    private addKeyStrokeService: AddKeyStrokeService,\n    private manageRaceLock: Locker,\n    private countdownService: CountdownService,\n  ) {}\n\n  afterInit(server: Server) {\n    console.info('[SpeedTyper.dev] Websocket Server Started.');\n    this.raceEvents.server = server;\n  }\n\n  handleDisconnect(socket: Socket) {\n    console.info(\n      `Client disconnected: ${socket.request.session.user.username}`,\n    );\n    const raceId = this.session.getRaceID(socket);\n    const user = this.session.getUser(socket);\n    this.raceManager.leaveRace(user, raceId);\n    this.session.removeRaceID(socket);\n    this.manageRaceLock.release(socket.id);\n  }\n\n  async handleConnection(socket: Socket) {\n    const userId = this.session.getUser(socket).id;\n    const userIsAlreadyPlaying = this.raceManager.userIsAlreadyPlaying(userId);\n    for (const [sid, s] of this.server.sockets.sockets) {\n      // We need to cleanup other sockets for the same user\n      // Because we can not have several instances of the same user in the same race twice\n      // Consider adding this possibility, but for different races\n      if (sid === socket.id) {\n        console.log('Same socket id, keeping.');\n        continue;\n      }\n      if (s.request.session.user.id === userId) {\n        console.log(\n          'Different socket id, same user. Disconnecting previous socket',\n        );\n        if (userIsAlreadyPlaying) {\n          this.raceManager.leaveRace(\n            s.request.session.user,\n            s.request.session.raceId,\n          );\n        }\n        s.disconnect();\n      }\n\n      if (!this.raceManager.userIsAlreadyPlaying(s.request.session.user.id)) {\n        console.log(\n          'Disconnecting because socket is not playing: ',\n          s.request.session.user.username,\n          s.request.session.user.id,\n        );\n        s.disconnect();\n        continue;\n      }\n\n      console.log(\n        'Keeping: ',\n        s.request.session.user.username,\n        s.request.session.user.id,\n      );\n    }\n\n    console.info(\n      `Client connected: ${socket.request.session.user.username} - ${socket.id}`,\n    );\n  }\n\n  @UseFilters(new RaceDoesNotExistFilter())\n  @SubscribeMessage('refresh_challenge')\n  async onRefreshChallenge(socket: Socket, settings: RaceSettingsDTO) {\n    this.raceEvents.logConnectedSockets();\n    const socketID = socket.id;\n    await this.manageRaceLock.runIfOpen(socketID, async () => {\n      const raceId = this.session.getRaceID(socket);\n      if (!raceId) {\n        this.manageRaceLock.release(socket.id);\n        this.onPlay(socket, settings);\n        return;\n      }\n      const user = this.session.getUser(socket);\n      if (this.raceManager.isOwner(user.id, raceId)) {\n        const race = await this.raceManager.refresh(raceId, settings.language);\n        this.raceEvents.updatedRace(socket, race);\n      }\n    });\n  }\n\n  @UsePipes(new ValidationPipe())\n  @SubscribeMessage('play')\n  async onPlay(socket: Socket, settings: RaceSettingsDTO) {\n    const socketID = socket.id;\n    await this.manageRaceLock.runIfOpen(socketID, async () => {\n      const user = this.session.getUser(socket);\n      const raceId = this.session.getRaceID(socket);\n      this.raceManager.leaveRace(user, raceId);\n      const race = await this.raceManager.create(user, settings);\n      this.raceEvents.createdRace(socket, race);\n      this.session.saveRaceID(socket, race.id);\n    });\n  }\n\n  @UseFilters(new RaceDoesNotExistFilter(), new InvalidKeystrokeFilter())\n  @UsePipes(new ValidationPipe())\n  @SubscribeMessage('key_stroke')\n  async onKeyStroke(socket: Socket, keystroke: KeystrokeDTO) {\n    keystroke.timestamp = new Date().getTime();\n    this.addKeyStrokeService.validate(socket, keystroke);\n    this.addKeyStrokeService.addKeyStroke(socket, keystroke);\n  }\n\n  @SubscribeMessage('join')\n  async onJoin(socket: Socket, id: string) {\n    this.manageRaceLock.runIfOpen(socket.id, async () => {\n      const user = this.session.getUser(socket);\n      const raceID = this.session.getRaceID(socket);\n      this.raceManager.leaveRace(user, raceID);\n      const race = this.raceManager.join(user, id);\n      if (!race) {\n        // if there is no race with the ID in the state\n        // we recreate a race for the user\n        // this makes sure that the game does not crash for the user\n        // TODO: we should create a race with the same ID, and even same challenge selected\n        // So that the other people in the race can then join the same room\n        // instead of creating their own through this same functionality\n        // we do however have to reset the progress for all participants as it is only kept in state\n        this.manageRaceLock.release(socket.id);\n        return this.onPlay(socket, { language: undefined, isPublic: false });\n      }\n      this.raceEvents.joinedRace(socket, race, user);\n      this.session.saveRaceID(socket, id);\n    });\n  }\n\n  @SubscribeMessage('start_race')\n  async onStart(socket: Socket) {\n    const user = this.session.getUser(socket);\n    const raceID = this.session.getRaceID(socket);\n    const race = this.raceManager.getRace(raceID);\n    if (race.canStartRace(user.id)) {\n      this.countdownService.countdown(race);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/races/races.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { ChallengesModule } from 'src/challenges/challenges.module';\nimport { ResultsModule } from 'src/results/results.module';\nimport { TrackingModule } from 'src/tracking/tracking.module';\nimport { RacesController } from './race.controllers';\nimport { RaceGateway } from './race.gateway';\nimport { AddKeyStrokeService } from './services/add-keystroke.service';\nimport { CountdownService } from './services/countdown.service';\nimport { KeyStrokeValidationService } from './services/keystroke-validator.service';\nimport { Locker } from './services/locker.service';\nimport { ProgressService } from './services/progress.service';\nimport { RaceEvents } from './services/race-events.service';\nimport { RaceManager } from './services/race-manager.service';\nimport { ResultsHandlerService } from './services/results-handler.service';\nimport { SessionState } from './services/session-state.service';\n\n@Module({\n  imports: [ChallengesModule, ResultsModule, TrackingModule],\n  controllers: [RacesController],\n  providers: [\n    AddKeyStrokeService,\n    KeyStrokeValidationService,\n    ProgressService,\n    RaceEvents,\n    RaceGateway,\n    RaceManager,\n    ResultsHandlerService,\n    SessionState,\n    Locker,\n    CountdownService,\n  ],\n  exports: [RaceManager, RaceEvents, KeyStrokeValidationService],\n})\nexport class RacesModule {}\n"
  },
  {
    "path": "packages/back-nest/src/races/services/add-keystroke.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { Socket } from 'socket.io';\nimport { TrackingService } from 'src/tracking/tracking.service';\nimport { KeyStrokeValidationService } from './keystroke-validator.service';\nimport { ProgressService } from './progress.service';\nimport { RaceEvents } from './race-events.service';\nimport { RaceManager } from './race-manager.service';\nimport { KeystrokeDTO } from './race-player.service';\nimport { ResultsHandlerService } from './results-handler.service';\nimport { SessionState } from './session-state.service';\n\n@Injectable()\nexport class AddKeyStrokeService {\n  constructor(\n    private manager: RaceManager,\n    private session: SessionState,\n    private validator: KeyStrokeValidationService,\n    private progressService: ProgressService,\n    private trackingService: TrackingService,\n    private events: RaceEvents,\n    private resultHandler: ResultsHandlerService,\n  ) {}\n\n  validate(socket: Socket, keyStroke: KeystrokeDTO) {\n    const user = this.session.getUser(socket);\n    const raceId = this.session.getRaceID(socket);\n    const player = this.manager.getPlayer(raceId, user.id);\n    this.validator.validateKeyStroke(player, keyStroke);\n  }\n\n  async addKeyStroke(socket: Socket, keyStroke: KeystrokeDTO) {\n    const user = this.session.getUser(socket);\n    const raceId = this.session.getRaceID(socket);\n    const player = this.manager.getPlayer(raceId, user.id);\n    if (player.hasNotStartedTyping()) {\n      this.trackingService.trackRaceStarted();\n    }\n    player.addKeyStroke(keyStroke);\n    if (keyStroke.correct) {\n      player.progress = this.progressService.calculateProgress(player);\n      const code = this.manager.getCode(raceId);\n      player.updateLiteral(code, keyStroke);\n      this.events.progressUpdated(socket, raceId, player);\n    }\n    this.syncStartTime(raceId, new Date(keyStroke.timestamp));\n    const race = this.manager.getRace(raceId);\n    this.resultHandler.handleResult(race, user);\n  }\n\n  async syncStartTime(raceId: string, timestamp: Date) {\n    const race = this.manager.getRace(raceId);\n    if (!race.isMultiplayer()) {\n      race.startTime = race.startTime ?? timestamp;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/races/services/countdown.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { RaceEvents } from './race-events.service';\nimport { Race } from './race.service';\n\n@Injectable()\nexport class CountdownService {\n  constructor(private raceEvents: RaceEvents) {}\n  async countdown(race: Race) {\n    race.countdown = true;\n    const seconds = 5;\n    for (let i = seconds; i > 0; i--) {\n      const delay = seconds - i;\n      const timeout = setTimeout(() => {\n        this.raceEvents.countdown(race.id, i);\n      }, delay * 1000);\n      race.timeouts.push(timeout);\n    }\n    const timeout = setTimeout(() => {\n      race.start();\n      this.raceEvents.raceStarted(race);\n      race.timeouts = [];\n      race.countdown = false;\n    }, seconds * 1000);\n    race.timeouts.push(timeout);\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/races/services/keystroke-validator.service.ts",
    "content": "import { BadRequestException, Injectable } from '@nestjs/common';\nimport { Challenge } from 'src/challenges/entities/challenge.entity';\nimport { RaceManager } from './race-manager.service';\nimport { KeystrokeDTO, RacePlayer } from './race-player.service';\nimport { Race } from './race.service';\n\nexport class InvalidKeystrokeException extends Error {\n  userId: string;\n  keystroke: KeystrokeDTO;\n  input: string;\n  expected: string;\n  race: Race;\n  constructor(\n    userId: string,\n    keystroke: KeystrokeDTO,\n    userInput: string,\n    expectedUserInput: string,\n    race: Race,\n  ) {\n    super('Unexpected keystroke received2');\n    this.userId = userId;\n    this.keystroke = keystroke;\n    this.input = userInput;\n    this.expected = expectedUserInput;\n    this.race = race;\n  }\n}\n\nexport class RaceNotStartedException extends BadRequestException {\n  constructor() {\n    super('Race not started');\n  }\n}\n\nexport function getCurrentInputBeforeKeystroke(\n  player: RacePlayer,\n  keystroke: KeystrokeDTO,\n) {\n  const currentInputBeforeKey = player\n    .validKeyStrokes()\n    .filter((stroke) => stroke.index < keystroke.index)\n    .map((stroke) => stroke.key)\n    .join('');\n  return currentInputBeforeKey;\n}\n\n@Injectable()\nexport class KeyStrokeValidationService {\n  constructor(private raceManager: RaceManager) {}\n\n  validateKeyStroke(player: RacePlayer, recentKeyStroke: KeystrokeDTO) {\n    this.validateRaceStarted(player.raceId);\n    const currentInputBeforeKey = getCurrentInputBeforeKeystroke(\n      player,\n      recentKeyStroke,\n    );\n    const userInput = currentInputBeforeKey + recentKeyStroke.key;\n    const expectedInput = this.getStrippedCode(player.raceId, recentKeyStroke);\n    const correct = userInput === expectedInput;\n    if (recentKeyStroke.correct && recentKeyStroke.correct !== correct) {\n      throw new InvalidKeystrokeException(\n        player.id,\n        recentKeyStroke,\n        userInput,\n        expectedInput,\n        this.raceManager.getRace(player.raceId),\n      );\n    }\n  }\n\n  validateRaceStarted(raceID: string) {\n    const race = this.raceManager.getRace(raceID);\n    if (!race.startTime && race.isMultiplayer()) {\n      throw new RaceNotStartedException();\n    }\n  }\n\n  private getStrippedCode(raceId: string, keystroke: KeystrokeDTO) {\n    const code = this.raceManager.getCode(raceId);\n    const strippedCode = Challenge.getStrippedCode(\n      code.substring(0, keystroke.index),\n    );\n    return strippedCode;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/races/services/locker.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\n\n@Injectable()\nexport class Locker {\n  lockedIDs: Set<string>;\n  constructor() {\n    this.lockedIDs = new Set<string>();\n  }\n\n  // this is a global lock function\n  // it locks all run methods called with this lockid\n  // even if they are coming from different classes\n  async runIfOpen<T>(lockID: string, callback: () => Promise<T>): Promise<T> {\n    if (this.lockedIDs.has(lockID)) {\n      return;\n    }\n    this.lockedIDs.add(lockID);\n    try {\n      return await callback();\n    } finally {\n      this.lockedIDs.delete(lockID);\n    }\n  }\n\n  release(id: string) {\n    this.lockedIDs.delete(id);\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/races/services/progress.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { Challenge } from 'src/challenges/entities/challenge.entity';\nimport { RaceManager } from './race-manager.service';\nimport { RacePlayer } from './race-player.service';\n\n@Injectable()\nexport class ProgressService {\n  constructor(private raceManager: RaceManager) {}\n  calculateProgress(player: RacePlayer) {\n    const currentInput = player.getValidInput();\n    const code = this.raceManager.getCode(player.raceId);\n    const strippedFullCode = Challenge.getStrippedCode(code);\n    const progress = Math.floor(\n      (currentInput.length / strippedFullCode.length) * 100,\n    );\n    return progress;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/races/services/race-events.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { Server, Socket } from 'socket.io';\nimport { Result } from 'src/results/entities/result.entity';\nimport { User } from 'src/users/entities/user.entity';\nimport { RacePlayer } from './race-player.service';\nimport { Race } from './race.service';\n\n@Injectable()\nexport class RaceEvents {\n  server: Server;\n\n  getPlayerCount() {\n    return this.server.sockets.sockets.size;\n  }\n\n  createdRace(socket: Socket, race: Race) {\n    socket.join(race.id);\n    socket.emit('race_joined', race);\n    socket.emit('challenge_selected', race.challenge);\n  }\n\n  countdown(raceID: string, i: number) {\n    const event = 'countdown';\n    this.server.to(raceID).emit(event, i);\n  }\n\n  raceStarted(race: Race) {\n    this.server.to(race.id).emit('race_started', race.startTime);\n  }\n\n  updatedRace(_: Socket, race: Race) {\n    this.server.to(race.id).emit('race_joined', race);\n    this.server.to(race.id).emit('challenge_selected', race.challenge);\n  }\n\n  joinedRace(socket: Socket, race: Race, user: User) {\n    socket.join(race.id);\n    socket.emit('race_joined', race);\n    socket.to(race.id).emit('member_joined', race.members[user.id]);\n  }\n\n  leftRace(race: Race, user: User) {\n    this.server.to(race.id).emit('member_left', {\n      member: user.id,\n      owner: race.owner,\n    });\n  }\n\n  progressUpdated(socket: Socket, raceId: string, player: RacePlayer) {\n    socket.to(raceId).emit('progress_updated', player);\n    socket.emit('progress_updated', player);\n  }\n\n  raceCompleted(raceId: string, result: Result) {\n    this.server.to(raceId).emit('race_completed', result);\n  }\n\n  raceDoesNotExist(socket: Socket, id: string) {\n    socket.emit('race_does_not_exist', id);\n  }\n  async logConnectedSockets() {\n    const sockets = await this.server.fetchSockets();\n    console.log('Connected sockets: ', sockets.length);\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/races/services/race-manager.service.ts",
    "content": "import { BadRequestException, Injectable } from '@nestjs/common';\nimport { Challenge } from 'src/challenges/entities/challenge.entity';\nimport { ChallengeService } from 'src/challenges/services/challenge.service';\nimport { LiteralService } from 'src/challenges/services/literal.service';\nimport { User } from 'src/users/entities/user.entity';\nimport { RaceSettingsDTO } from '../entities/race-settings.dto';\nimport { RaceEvents } from './race-events.service';\nimport { RacePlayer } from './race-player.service';\nimport { Race } from './race.service';\n\nexport interface PublicRace {\n  id: string;\n  ownerName: string;\n  memberCount: number;\n}\n\n@Injectable()\nexport class RaceManager {\n  private races: Record<string, Race> = {};\n\n  constructor(\n    private challengeService: ChallengeService,\n    private literalsService: LiteralService,\n    private raceEvents: RaceEvents,\n  ) {}\n\n  getOnlineCount(): number {\n    const memberIds = Object.values(this.races)\n      .flatMap((race) => Object.values(race.members))\n      .map((member) => member.id);\n    const uniqueMemberIds = new Set(memberIds);\n    return uniqueMemberIds.size;\n  }\n\n  getPublicRaces(): PublicRace[] {\n    const races = Object.values(this.races);\n    const publicRaces = races\n      .filter((race) => race.isPublic)\n      .map((race) => {\n        return race.toPublic();\n      });\n    return publicRaces;\n  }\n\n  syncUser(raceId: string, prevUserId: string, user: User) {\n    const race = this.getRace(raceId);\n    if (race.owner === prevUserId) {\n      race.owner = user.id;\n    }\n    const player = race.members[prevUserId];\n    player.id = user.id;\n    player.username = user.username;\n    delete race.members[prevUserId];\n    race.members[user.id] = player;\n  }\n\n  debugSize(msg: string) {\n    const racesSize = JSON.stringify(this.races).length;\n    console.log(msg, {\n      racesSize,\n      races: Object.keys(this.races).length,\n      players: this.getOnlineCount(),\n    });\n  }\n  async create(user: User, settings: RaceSettingsDTO): Promise<Race> {\n    this.debugSize('create');\n    const challenge = await this.challengeService.getRandom(settings.language);\n    const literals = this.literalsService.calculateLiterals(challenge.content);\n    const race = new Race(user, challenge, literals);\n    race.isPublic = settings.isPublic;\n    this.races[race.id] = race;\n    return race;\n  }\n\n  async refresh(id: string, language?: string): Promise<Race> {\n    this.debugSize('refresh');\n    const race = this.getRace(id);\n    const challenge = await this.challengeService.getRandom(language);\n    const literals = this.literalsService.calculateLiterals(challenge.content);\n    race.challenge = challenge;\n    race.literals = literals;\n    race.resetProgress();\n    return race;\n  }\n\n  getRace(id: string): Race {\n    const race = this.races[id];\n    if (!race) throw new RaceDoesNotExist(id);\n    return race;\n  }\n\n  getPlayer(raceId: string, userId: string): RacePlayer {\n    const race = this.getRace(raceId);\n    return race.getPlayer(userId);\n  }\n\n  getChallenge(raceId: string): Challenge {\n    const race = this.getRace(raceId);\n    return race.challenge;\n  }\n\n  // Get the full code string of the currently active challenge for the provided race id\n  getCode(raceId: string): string {\n    return this.getChallenge(raceId).content;\n  }\n\n  join(user: User, raceId: string): Race | null {\n    const race = this.races[raceId];\n    // it's important to return null instead of throwing\n    // a RaceDoesNotExist error because the exception filter\n    // sends a race_does_not_exist event back to the client\n    // and the client tries to join the race\n    // in the controller we create a game if no game exists\n    // preventing an infinite loop\n    // TODO: this should be handled better in the future\n    if (!race) return null;\n    race.addMember(user);\n    return race;\n  }\n\n  leaveRace(user: User, raceId: string) {\n    const race = this.races[raceId];\n    if (!race) return;\n    race.removeMember(user);\n    if (Object.values(race.members).length === 0) {\n      delete this.races[raceId];\n    } else if (race.owner === user.id) {\n      race.owner = Object.values(race.members)[0].id;\n    }\n    this.raceEvents.leftRace(race, user);\n  }\n\n  isOwner(userId: string, raceId: string): boolean {\n    const race = this.races[raceId];\n    if (!race) throw new RaceDoesNotExist(raceId);\n    return race.owner === userId;\n  }\n\n  userIsAlreadyPlaying(userId: string): boolean {\n    return Object.values(this.races)\n      .flatMap((race) => Object.keys(race.members))\n      .includes(userId);\n  }\n}\n\nexport class RaceDoesNotExist extends BadRequestException {\n  id: string;\n  constructor(id: string) {\n    super(`Race with id=${id} does not exist`);\n    this.id = id;\n    Object.setPrototypeOf(this, RaceDoesNotExist.prototype);\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/races/services/race-player.service.ts",
    "content": "import { Exclude, instanceToPlain } from 'class-transformer';\nimport {\n  IsBoolean,\n  IsNotEmpty,\n  IsNumber,\n  IsString,\n  MaxLength,\n} from 'class-validator';\nimport { LiteralService } from 'src/challenges/services/literal.service';\nimport { User } from 'src/users/entities/user.entity';\n\nexport class KeystrokeDTO {\n  @IsString()\n  @IsNotEmpty()\n  @MaxLength(1)\n  key: string;\n  @IsNotEmpty()\n  @IsNumber()\n  timestamp: number;\n  @IsNotEmpty()\n  @IsBoolean()\n  correct: boolean;\n  @IsNotEmpty()\n  @IsNumber()\n  index: number;\n}\n\nexport class RacePlayer {\n  id: string;\n  username: string;\n\n  recentlyTypedLiteral: string;\n\n  @Exclude()\n  literalOffset: number;\n\n  @Exclude()\n  literals: string[];\n\n  @Exclude()\n  saved: boolean;\n\n  progress: number;\n\n  @Exclude()\n  raceId: string;\n\n  @Exclude()\n  typedKeyStrokes: KeystrokeDTO[];\n\n  @Exclude()\n  literalService: LiteralService;\n\n  toJSON() {\n    return instanceToPlain(this);\n  }\n\n  reset(literals: string[]) {\n    this.literals = literals;\n    this.literalOffset = 0;\n    this.recentlyTypedLiteral = this.literals[this.literalOffset];\n    this.progress = 0;\n    this.saved = false;\n    this.typedKeyStrokes = [];\n  }\n\n  validKeyStrokes() {\n    const keyStrokes = this.typedKeyStrokes;\n    const latestKeyStrokePerIndex = Object.fromEntries(\n      keyStrokes.map((keyStroke) => {\n        return [keyStroke.index, keyStroke];\n      }),\n    );\n    const firstIncorrectKeystroke = Object.values(latestKeyStrokePerIndex).find(\n      (keystroke) => !keystroke.correct,\n    );\n    const validKeyStrokes = Object.values(latestKeyStrokePerIndex)\n      .filter((keyStroke) => keyStroke.correct)\n      .filter((keystroke) =>\n        firstIncorrectKeystroke\n          ? keystroke.index < firstIncorrectKeystroke.index\n          : true,\n      );\n    return validKeyStrokes;\n  }\n\n  incorrectKeyStrokes() {\n    const incorrectKeyStrokes = this.typedKeyStrokes.filter(\n      (keyStroke) => !keyStroke.correct,\n    );\n    return incorrectKeyStrokes;\n  }\n\n  getValidInput() {\n    const validInput = this.validKeyStrokes()\n      .map((keyStroke) => keyStroke.key)\n      .join('');\n    return validInput;\n  }\n\n  addKeyStroke(keyStroke: KeystrokeDTO) {\n    keyStroke.timestamp = new Date().getTime();\n    this.typedKeyStrokes.push(keyStroke);\n  }\n\n  updateLiteral(code: string, keyStroke: KeystrokeDTO) {\n    const untypedCode = code.substring(keyStroke.index);\n    const nextLiteral = this.literals[this.literalOffset + 1];\n    const startsWithNextLiteral = this.literalService\n      .calculateLiterals(untypedCode.trimStart())\n      .join('')\n      .startsWith(nextLiteral);\n    if (startsWithNextLiteral && this.literals.length > 1) {\n      this.literalOffset++;\n    }\n    this.recentlyTypedLiteral = this.literals[this.literalOffset];\n  }\n\n  hasNotStartedTyping(): boolean {\n    return this.typedKeyStrokes.length === 0;\n  }\n\n  hasCompletedRace(): boolean {\n    return this.progress === 100;\n  }\n\n  static fromUser(raceId: string, user: User, literals: string[]) {\n    const player = new RacePlayer();\n    player.id = user.id;\n    player.raceId = raceId;\n    player.username = user.username;\n    player.progress = 0;\n    player.literals = literals;\n    player.recentlyTypedLiteral = player.literals[0];\n    player.literalOffset = 0;\n    player.typedKeyStrokes = [];\n    player.literalService = new LiteralService();\n    player.saved = false;\n    return player;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/races/services/race.service.ts",
    "content": "import { Exclude, instanceToPlain } from 'class-transformer';\nimport { randomUUID } from 'crypto';\nimport { Challenge } from 'src/challenges/entities/challenge.entity';\nimport { User } from 'src/users/entities/user.entity';\nimport { RacePlayer } from './race-player.service';\n\nexport interface PublicRace {\n  id: string;\n  ownerName: string;\n  memberCount: number;\n}\n\nexport class Race {\n  id: string;\n  challenge: Challenge;\n  owner: string;\n  members: Record<string, RacePlayer>;\n  @Exclude()\n  literals: string[];\n\n  @Exclude()\n  timeouts: NodeJS.Timeout[];\n\n  startTime?: Date;\n\n  @Exclude()\n  countdown: boolean;\n\n  isPublic: boolean;\n\n  togglePublic(): boolean {\n    this.isPublic = !this.isPublic;\n    return this.isPublic;\n  }\n  toPublic(): PublicRace {\n    const ownerName = this.members[this.owner].username;\n    const memberCount = Object.keys(this.members).length;\n    return {\n      id: this.id,\n      ownerName,\n      memberCount,\n    };\n  }\n\n  isMultiplayer(): boolean {\n    return Object.keys(this.members).length > 1;\n  }\n\n  toJSON() {\n    return instanceToPlain(this);\n  }\n\n  constructor(owner: User, challenge: Challenge, literals: string[]) {\n    this.id = randomUUID().replaceAll('-', '');\n    this.members = {};\n    this.owner = owner.id;\n    this.challenge = challenge;\n    this.literals = literals;\n    this.timeouts = [];\n    this.countdown = false;\n    this.addMember(owner);\n    this.isPublic = false;\n  }\n\n  start() {\n    this.startTime = new Date();\n  }\n\n  canStartRace(userID: string): boolean {\n    return !this.countdown && !this.startTime && this.owner === userID;\n  }\n\n  getPlayer(id: string) {\n    return this.members[id];\n  }\n\n  resetProgress() {\n    Object.values(this.members).forEach((player) => {\n      player.reset(this.literals);\n    });\n    this.startTime = undefined;\n    for (const timeout of this.timeouts) {\n      clearTimeout(timeout);\n    }\n    this.timeouts = [];\n    this.countdown = false;\n  }\n\n  addMember(user: User) {\n    this.members[user.id] = RacePlayer.fromUser(this.id, user, this.literals);\n  }\n\n  removeMember(user: User) {\n    delete this.members[user.id];\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/races/services/results-handler.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { ResultFactoryService } from 'src/results/services/result-factory.service';\nimport { ResultService } from 'src/results/services/results.service';\nimport { TrackingService } from 'src/tracking/tracking.service';\nimport { User } from 'src/users/entities/user.entity';\nimport { RaceEvents } from './race-events.service';\nimport { Race } from './race.service';\n\n@Injectable()\nexport class ResultsHandlerService {\n  constructor(\n    private factory: ResultFactoryService,\n    private events: RaceEvents,\n    private results: ResultService,\n    private tracker: TrackingService,\n  ) {}\n  async handleResult(race: Race, user: User) {\n    const player = race.getPlayer(user.id);\n    if (player.hasCompletedRace()) {\n      if (player.saved) {\n        return;\n      }\n      player.saved = true;\n      let result = this.factory.factory(race, player, user);\n      if (!user.isAnonymous) {\n        result = await this.results.create(result);\n      }\n      result.percentile = await this.results.getResultPercentile(result.cpm);\n      this.tracker.trackRaceCompleted();\n      this.events.raceCompleted(race.id, result);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/races/services/session-state.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { Socket } from 'socket.io';\nimport { User } from 'src/users/entities/user.entity';\n\n@Injectable()\nexport class SessionState {\n  getUser(socket: Socket): User {\n    return socket.request.session.user;\n  }\n\n  getRaceID(socket: Socket): string {\n    return socket.request.session.raceId;\n  }\n\n  saveRaceID(socket: Socket, id: string) {\n    const prevRaceID = socket.request.session.raceId;\n    socket.request.session.raceId = id;\n    socket.request.session.save(() => {\n      socket.leave(prevRaceID);\n    });\n  }\n\n  removeRaceID(socket: Socket) {\n    const prevRaceID = socket.request.session.raceId;\n    socket.request.session.raceId = null;\n    socket.request.session.save(() => {\n      socket.leave(prevRaceID);\n    });\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/races/services/tests/race-player.service.spec.ts",
    "content": "import { RacePlayer } from '../race-player.service';\n\ndescribe('[unit] validKeyStrokes()', () => {\n  const player = new RacePlayer();\n\n  beforeEach(() => {\n    const typedKeystrokes = [\n      { correct: true, index: 1, key: 'f', timestamp: 1676334891980 },\n      { correct: true, index: 2, key: 'u', timestamp: 1676334892242 },\n      { correct: true, index: 3, key: 'n', timestamp: 1676334892503 },\n      { correct: true, index: 4, key: 'c', timestamp: 1676334892503 },\n    ];\n    player.typedKeyStrokes = typedKeystrokes;\n  });\n\n  it('should include all valid keystrokes', () => {\n    const validKeyStrokes = player.validKeyStrokes();\n    expect(validKeyStrokes).toEqual(player.typedKeyStrokes);\n  });\n\n  it('should filter out invalid keystrokes all valid keystrokes', () => {\n    const expectedValidKeystrokes = [...player.typedKeyStrokes];\n    player.typedKeyStrokes.push({\n      correct: false,\n      index: 5,\n      key: 'd',\n      timestamp: 1676334892503,\n    });\n    const validKeyStrokes = player.validKeyStrokes();\n    expect(validKeyStrokes).toEqual(expectedValidKeystrokes);\n  });\n\n  it('should use the latest timestamp for each index', () => {\n    const expectedValidKeystrokes = [player.typedKeyStrokes[0]];\n    player.typedKeyStrokes.push({\n      correct: false,\n      index: 2,\n      key: 'd',\n      timestamp: 1676334892503,\n    });\n    const validKeyStrokes = player.validKeyStrokes();\n    expect(validKeyStrokes).toEqual(expectedValidKeystrokes);\n  });\n});\n\ndescribe('[functional] validKeyStrokes()', () => {\n  const player = new RacePlayer();\n  player.typedKeyStrokes = [\n    { correct: true, index: 1, key: 'f', timestamp: 1676392785787 },\n    { correct: true, index: 2, key: 'u', timestamp: 1676392785931 },\n    { correct: true, index: 3, key: 'n', timestamp: 1676392786176 },\n    { correct: true, index: 4, key: 'c', timestamp: 1676392786253 },\n    { correct: true, index: 5, key: ' ', timestamp: 1676392786343 },\n    { correct: true, index: 6, key: 'n', timestamp: 1676392786485 },\n    { correct: true, index: 7, key: 'e', timestamp: 1676392786572 },\n    { correct: true, index: 8, key: 'w', timestamp: 1676392786630 },\n    { correct: true, index: 9, key: 'W', timestamp: 1676392786851 },\n    { correct: true, index: 10, key: 'a', timestamp: 1676392787083 },\n    { correct: true, index: 11, key: 't', timestamp: 1676392787162 },\n    { correct: true, index: 12, key: 'c', timestamp: 1676392787392 },\n    { correct: true, index: 13, key: 'h', timestamp: 1676392787460 },\n    { correct: true, index: 14, key: 'e', timestamp: 1676392787566 },\n    { correct: true, index: 15, key: 'r', timestamp: 1676392787696 },\n    { correct: true, index: 16, key: 'G', timestamp: 1676392787997 },\n    { correct: false, index: 17, key: 'B', timestamp: 1676392788000 },\n    { correct: false, index: 18, key: 'r', timestamp: 1676392788196 },\n    { correct: true, index: 17, key: 'r', timestamp: 1676392788777 },\n    { correct: false, index: 18, key: 'u', timestamp: 1676392788952 },\n    { correct: false, index: 19, key: 'o', timestamp: 1676392788963 },\n    { correct: false, index: 20, key: 'o', timestamp: 1676392789165 },\n    { correct: true, index: 18, key: 'o', timestamp: 1676392790150 },\n    { correct: true, index: 19, key: 'u', timestamp: 1676392790279 },\n    { correct: true, index: 20, key: 'p', timestamp: 1676392790365 },\n    { correct: true, index: 21, key: '(', timestamp: 1676392790776 },\n    { correct: true, index: 22, key: ')', timestamp: 1676392790847 },\n    { correct: true, index: 23, key: ' ', timestamp: 1676392791108 },\n    { correct: true, index: 24, key: 'w', timestamp: 1676392791308 },\n    { correct: true, index: 25, key: 'a', timestamp: 1676392791492 },\n    { correct: true, index: 26, key: 't', timestamp: 1676392791555 },\n    { correct: true, index: 27, key: 'c', timestamp: 1676392791771 },\n    { correct: true, index: 28, key: 'h', timestamp: 1676392791852 },\n    { correct: true, index: 29, key: 'e', timestamp: 1676392791944 },\n    { correct: true, index: 30, key: 'r', timestamp: 1676392792046 },\n    { correct: true, index: 31, key: 'G', timestamp: 1676392792307 },\n    { correct: true, index: 32, key: 'r', timestamp: 1676392792494 },\n    { correct: true, index: 33, key: 'o', timestamp: 1676392792554 },\n    { correct: true, index: 34, key: 'u', timestamp: 1676392792659 },\n    { correct: true, index: 35, key: 'p', timestamp: 1676392792729 },\n    { correct: true, index: 36, key: ' ', timestamp: 1676392792882 },\n    { correct: true, index: 37, key: '{', timestamp: 1676392793137 },\n    { correct: true, index: 40, key: '\\n', timestamp: 1676392793228 },\n    { correct: true, index: 41, key: 'r', timestamp: 1676392793933 },\n    { correct: true, index: 42, key: 'e', timestamp: 1676392794021 },\n    { correct: true, index: 43, key: 't', timestamp: 1676392794141 },\n    { correct: true, index: 44, key: 'u', timestamp: 1676392794197 },\n    { correct: true, index: 45, key: 'r', timestamp: 1676392794324 },\n    { correct: true, index: 46, key: 'n', timestamp: 1676392794454 },\n    { correct: true, index: 41, key: 'r', timestamp: 1676392795190 },\n    { correct: true, index: 42, key: 'e', timestamp: 1676392795299 },\n    { correct: true, index: 43, key: 't', timestamp: 1676392795410 },\n    { correct: true, index: 44, key: 'u', timestamp: 1676392795468 },\n    { correct: true, index: 45, key: 'r', timestamp: 1676392795601 },\n    { correct: true, index: 46, key: 'n', timestamp: 1676392795683 },\n    { correct: true, index: 47, key: ' ', timestamp: 1676392796151 },\n    { correct: true, index: 48, key: 'w', timestamp: 1676392796361 },\n    { correct: true, index: 49, key: 'a', timestamp: 1676392796514 },\n    { correct: true, index: 50, key: 't', timestamp: 1676392796588 },\n    { correct: true, index: 51, key: 'c', timestamp: 1676392796827 },\n    { correct: true, index: 52, key: 'h', timestamp: 1676392796909 },\n    { correct: true, index: 53, key: 'e', timestamp: 1676392797008 },\n    { correct: true, index: 54, key: 'r', timestamp: 1676392797116 },\n    { correct: true, index: 55, key: 'G', timestamp: 1676392797496 },\n    { correct: true, index: 56, key: 'r', timestamp: 1676392797730 },\n    { correct: true, index: 57, key: 'o', timestamp: 1676392797808 },\n    { correct: true, index: 58, key: 'u', timestamp: 1676392797910 },\n    { correct: true, index: 59, key: 'p', timestamp: 1676392797968 },\n    { correct: true, index: 60, key: '{', timestamp: 1676392798346 },\n    { correct: true, index: 65, key: '\\n', timestamp: 1676392798497 },\n    { correct: true, index: 66, key: 'k', timestamp: 1676392800709 },\n    { correct: true, index: 67, key: 'e', timestamp: 1676392800788 },\n    { correct: true, index: 68, key: 'y', timestamp: 1676392800894 },\n    { correct: true, index: 69, key: 'W', timestamp: 1676392801060 },\n    { correct: true, index: 70, key: 'a', timestamp: 1676392801287 },\n    { correct: true, index: 71, key: 't', timestamp: 1676392801399 },\n    { correct: true, index: 72, key: 'c', timestamp: 1676392801635 },\n    { correct: true, index: 73, key: 'h', timestamp: 1676392801724 },\n    { correct: true, index: 74, key: 'e', timestamp: 1676392801810 },\n    { correct: true, index: 75, key: 'r', timestamp: 1676392801933 },\n    { correct: true, index: 76, key: 's', timestamp: 1676392802024 },\n    { correct: false, index: 66, key: 'd', timestamp: 1676392804327 },\n    { correct: false, index: 67, key: 'e', timestamp: 1676392804543 },\n    { correct: false, index: 68, key: 'y', timestamp: 1676392804700 },\n    { correct: false, index: 69, key: 'W', timestamp: 1676392804958 },\n    { correct: false, index: 70, key: 'a', timestamp: 1676392805036 },\n    { correct: false, index: 71, key: 't', timestamp: 1676392806184 },\n    { correct: false, index: 72, key: 'c', timestamp: 1676392806433 },\n    { correct: false, index: 73, key: 'h', timestamp: 1676392806553 },\n    { correct: false, index: 74, key: 'e', timestamp: 1676392806625 },\n    { correct: false, index: 75, key: 'r', timestamp: 1676392806753 },\n    { correct: false, index: 76, key: 's', timestamp: 1676392806850 },\n    { correct: false, index: 77, key: ':', timestamp: 1676392807263 },\n    { correct: true, index: 66, key: 'k', timestamp: 1676392808209 },\n    { correct: true, index: 67, key: 'e', timestamp: 1676392808311 },\n    { correct: true, index: 68, key: 'y', timestamp: 1676392808401 },\n    { correct: true, index: 69, key: 'W', timestamp: 1676392808559 },\n    { correct: true, index: 70, key: 'a', timestamp: 1676392808736 },\n    { correct: true, index: 71, key: 't', timestamp: 1676392808809 },\n    { correct: true, index: 72, key: 'c', timestamp: 1676392809050 },\n    { correct: true, index: 73, key: 'h', timestamp: 1676392809118 },\n    { correct: true, index: 74, key: 'e', timestamp: 1676392809227 },\n    { correct: true, index: 75, key: 'r', timestamp: 1676392809328 },\n    { correct: true, index: 76, key: 's', timestamp: 1676392809396 },\n    { correct: true, index: 77, key: ':', timestamp: 1676392809639 },\n    { correct: true, index: 78, key: ' ', timestamp: 1676392809832 },\n    { correct: true, index: 79, key: 'm', timestamp: 1676392810011 },\n    { correct: true, index: 80, key: 'a', timestamp: 1676392810089 },\n    { correct: true, index: 81, key: 'k', timestamp: 1676392810198 },\n    { correct: true, index: 82, key: 'e', timestamp: 1676392810274 },\n    { correct: true, index: 83, key: '(', timestamp: 1676392810520 },\n    { correct: true, index: 84, key: 'w', timestamp: 1676392810915 },\n    { correct: true, index: 85, key: 'a', timestamp: 1676392811118 },\n    { correct: true, index: 86, key: 't', timestamp: 1676392811218 },\n    { correct: true, index: 87, key: 'c', timestamp: 1676392811442 },\n    { correct: true, index: 88, key: 'h', timestamp: 1676392811538 },\n    { correct: true, index: 89, key: 'e', timestamp: 1676392811649 },\n    { correct: true, index: 90, key: 'r', timestamp: 1676392811760 },\n    { correct: true, index: 91, key: 'S', timestamp: 1676392811955 },\n    { correct: true, index: 92, key: 'e', timestamp: 1676392812097 },\n    { correct: true, index: 93, key: 't', timestamp: 1676392812197 },\n    { correct: true, index: 94, key: 'B', timestamp: 1676392812554 },\n    { correct: true, index: 95, key: 'y', timestamp: 1676392812704 },\n    { correct: true, index: 96, key: 'K', timestamp: 1676392812921 },\n    { correct: true, index: 97, key: 'e', timestamp: 1676392813049 },\n    { correct: true, index: 98, key: 'y', timestamp: 1676392813137 },\n    { correct: true, index: 99, key: ')', timestamp: 1676392813359 },\n    { correct: true, index: 100, key: ',', timestamp: 1676392813650 },\n    { correct: true, index: 105, key: '\\n', timestamp: 1676392813778 },\n    { correct: true, index: 106, key: 'r', timestamp: 1676392814158 },\n    { correct: true, index: 107, key: 'a', timestamp: 1676392814246 },\n    { correct: true, index: 108, key: 'n', timestamp: 1676392814333 },\n    { correct: true, index: 109, key: 'g', timestamp: 1676392814444 },\n    { correct: true, index: 110, key: 'e', timestamp: 1676392814518 },\n    { correct: true, index: 111, key: 's', timestamp: 1676392814609 },\n    { correct: true, index: 112, key: ':', timestamp: 1676392814749 },\n    { correct: true, index: 113, key: ' ', timestamp: 1676392815613 },\n    { correct: true, index: 114, key: ' ', timestamp: 1676392815800 },\n    { correct: true, index: 115, key: ' ', timestamp: 1676392815972 },\n    { correct: true, index: 116, key: ' ', timestamp: 1676392816163 },\n    { correct: true, index: 117, key: ' ', timestamp: 1676392816369 },\n    { correct: true, index: 118, key: ' ', timestamp: 1676392816606 },\n    { correct: true, index: 119, key: 'a', timestamp: 1676392817044 },\n    { correct: true, index: 120, key: 'd', timestamp: 1676392817074 },\n    { correct: true, index: 121, key: 't', timestamp: 1676392817262 },\n    { correct: true, index: 122, key: '.', timestamp: 1676392817743 },\n    { correct: true, index: 123, key: 'N', timestamp: 1676392818028 },\n    { correct: true, index: 124, key: 'e', timestamp: 1676392818220 },\n    { correct: true, index: 125, key: 'w', timestamp: 1676392818308 },\n    { correct: true, index: 126, key: 'I', timestamp: 1676392818497 },\n    { correct: false, index: 127, key: 'N', timestamp: 1676392818602 },\n    { correct: false, index: 128, key: 't', timestamp: 1676392818801 },\n    { correct: false, index: 129, key: 'e', timestamp: 1676392818895 },\n    { correct: true, index: 127, key: 'n', timestamp: 1676392819553 },\n    { correct: true, index: 128, key: 't', timestamp: 1676392819712 },\n    { correct: true, index: 129, key: 'e', timestamp: 1676392819808 },\n    { correct: true, index: 130, key: 'r', timestamp: 1676392819998 },\n    { correct: true, index: 131, key: 'v', timestamp: 1676392820243 },\n    { correct: true, index: 132, key: 'a', timestamp: 1676392820349 },\n    { correct: true, index: 133, key: 'l', timestamp: 1676392820416 },\n    { correct: true, index: 134, key: 'T', timestamp: 1676392820902 },\n    { correct: true, index: 135, key: 'r', timestamp: 1676392821122 },\n    { correct: true, index: 136, key: 'e', timestamp: 1676392821196 },\n    { correct: true, index: 137, key: 'e', timestamp: 1676392821343 },\n    { correct: true, index: 138, key: '(', timestamp: 1676392821550 },\n    { correct: true, index: 139, key: ')', timestamp: 1676392821785 },\n    { correct: true, index: 140, key: ',', timestamp: 1676392821968 },\n    { correct: true, index: 145, key: '\\n', timestamp: 1676392822798 },\n    { correct: true, index: 146, key: 'w', timestamp: 1676392823218 },\n    { correct: true, index: 147, key: 'a', timestamp: 1676392823403 },\n    { correct: true, index: 148, key: 't', timestamp: 1676392823537 },\n    { correct: true, index: 149, key: 'c', timestamp: 1676392823771 },\n    { correct: true, index: 150, key: 'h', timestamp: 1676392823848 },\n    { correct: true, index: 151, key: 'e', timestamp: 1676392823954 },\n    { correct: true, index: 152, key: 'r', timestamp: 1676392824086 },\n    { correct: true, index: 153, key: 's', timestamp: 1676392824116 },\n    { correct: true, index: 154, key: ':', timestamp: 1676392824376 },\n    { correct: true, index: 155, key: ' ', timestamp: 1676392824700 },\n    { correct: true, index: 156, key: ' ', timestamp: 1676392824882 },\n    { correct: true, index: 157, key: ' ', timestamp: 1676392825040 },\n    { correct: true, index: 158, key: ' ', timestamp: 1676392825547 },\n    { correct: true, index: 159, key: 'm', timestamp: 1676392825845 },\n    { correct: true, index: 160, key: 'a', timestamp: 1676392825913 },\n    { correct: true, index: 161, key: 'k', timestamp: 1676392826036 },\n    { correct: true, index: 162, key: 'e', timestamp: 1676392826111 },\n    { correct: true, index: 163, key: '(', timestamp: 1676392826422 },\n    { correct: true, index: 164, key: 'w', timestamp: 1676392826766 },\n  ];\n\n  it('should include the last correct keystroke', () => {\n    const expectedInput =\n      'func newWatcherGroup() watcherGroup {\\nreturn watcherGroup{\\nkeyWatchers: make(watcherSetByKey),\\nranges:      adt.NewIntervalTree(),\\nwatchers:    make(w';\n\n    const validKeyStrokes = player.validKeyStrokes();\n\n    const actualInput = validKeyStrokes.map((stroke) => stroke.key).join('');\n\n    expect(actualInput).toBe(expectedInput);\n  });\n});\n"
  },
  {
    "path": "packages/back-nest/src/results/entities/leaderboard-result.dto.ts",
    "content": "export class LeaderBoardResult {\n  username: string;\n  avatarUrl: string;\n  cpm: number;\n  accuracy: number;\n  createdAt: Date;\n}\n"
  },
  {
    "path": "packages/back-nest/src/results/entities/result.entity.ts",
    "content": "import { Challenge } from 'src/challenges/entities/challenge.entity';\nimport { User } from 'src/users/entities/user.entity';\nimport {\n  Column,\n  CreateDateColumn,\n  Entity,\n  ManyToOne,\n  PrimaryGeneratedColumn,\n} from 'typeorm';\n\n@Entity('results')\nexport class Result {\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n  @Column({ nullable: true })\n  raceId: string;\n  @Column()\n  timeMS: number;\n  @Column()\n  cpm: number;\n  @Column()\n  mistakes: number;\n  @Column()\n  accuracy: number;\n\n  @Column({ unique: true, nullable: true, default: null })\n  legacyId: string;\n\n  @CreateDateColumn({\n    type: 'timestamp',\n    default: () => 'CURRENT_TIMESTAMP(6)',\n  })\n  public createdAt: Date;\n  @ManyToOne(() => Challenge, (challenge) => challenge.results, {\n    onDelete: 'SET NULL',\n  })\n  challenge: Challenge;\n  @ManyToOne(() => User, (user) => user.results, {\n    onDelete: 'SET NULL',\n  })\n  user: User;\n  userId: string;\n\n  percentile?: number;\n}\n"
  },
  {
    "path": "packages/back-nest/src/results/errors.ts",
    "content": "import { BadRequestException, ForbiddenException } from '@nestjs/common';\n\nexport class SaveResultAnonymousNotAllowed extends ForbiddenException {\n  constructor() {\n    super('Anonymous users cannot save results');\n  }\n}\n\nexport class SaveResultInvalidUserID extends ForbiddenException {\n  constructor() {\n    super('Users can only save their own results');\n  }\n}\n\nexport class SaveResultRaceNotCompleted extends BadRequestException {\n  constructor() {\n    super('User did not complete the race');\n  }\n}\n\nexport class SaveResultUserNotInRace extends BadRequestException {\n  constructor() {\n    super('User is not playing in the race');\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/results/results.controller.ts",
    "content": "import {\n  BadRequestException,\n  Controller,\n  Get,\n  InternalServerErrorException,\n  Param,\n  Req,\n} from '@nestjs/common';\nimport { isUUID } from 'class-validator';\nimport { Request } from 'express';\nimport { LeaderBoardResult } from './entities/leaderboard-result.dto';\nimport { ResultService } from './services/results.service';\n\n@Controller('results')\nexport class ResultsController {\n  leaderboardP?: Promise<LeaderBoardResult[]>;\n  constructor(private resultsService: ResultService) {}\n  @Get('leaderboard')\n  async getLeaderboard(): Promise<LeaderBoardResult[]> {\n    if (this.leaderboardP) {\n      // there is an ongoing promise\n      return this.leaderboardP;\n    }\n    // cache the leaderboard promise so we only hit the DB once per concurrent request\n    this.leaderboardP = this.resultsService.getLeaderboard();\n    return this.leaderboardP.finally(() => {\n      // reset the promise so new clients don't get a stale leaderboard\n      this.leaderboardP = undefined;\n    });\n  }\n  @Get('/stats')\n  async getStatsByUser(@Req() request: Request) {\n    if (!request.session?.user) {\n      throw new InternalServerErrorException();\n    }\n    const startOfToday = new Date();\n    startOfToday.setUTCHours(0, 0, 0, 0);\n\n    const startOfTime = new Date('January 1, 1979');\n    const oneWeekAgo = new Date();\n    oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);\n    oneWeekAgo.setUTCHours(0, 0, 0, 0);\n\n    const user = request.session.user;\n    const [cpmAllTime, cpmToday, cpmLastWeek, cpmLast10] = await Promise.all([\n      this.resultsService.getAverageCPMSince(user.id, startOfTime),\n      this.resultsService.getAverageCPMSince(user.id, startOfToday),\n      this.resultsService.getAverageCPMSince(user.id, oneWeekAgo),\n      this.resultsService.getAverageCPM(user.id, 10),\n    ]);\n    return {\n      cpmLast10,\n      cpmToday,\n      cpmLastWeek,\n      cpmAllTime,\n    };\n  }\n\n  @Get(':resultId')\n  getResultByID(@Param('resultId') resultId: string) {\n    if (!isUUID(resultId)) throw new BadRequestException('Invalid resultId');\n    const result = this.resultsService.getByID(resultId);\n    return result;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/results/results.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { Result } from './entities/result.entity';\nimport { ResultsController } from './results.controller';\nimport { ResultCalculationService } from './services/result-calculation.service';\nimport { ResultFactoryService } from './services/result-factory.service';\nimport { ResultService } from './services/results.service';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([Result])],\n  providers: [ResultService, ResultFactoryService, ResultCalculationService],\n  controllers: [ResultsController],\n  exports: [ResultService, ResultFactoryService],\n})\nexport class ResultsModule {}\n"
  },
  {
    "path": "packages/back-nest/src/results/services/result-calculation.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { Challenge } from 'src/challenges/entities/challenge.entity';\nimport { RacePlayer } from 'src/races/services/race-player.service';\nimport { Race } from 'src/races/services/race.service';\n\n@Injectable()\nexport class ResultCalculationService {\n  getTimeMS(race: Race, player: RacePlayer): number {\n    const firstTimeStampMS = race.startTime.getTime();\n    const keyStrokes = player.validKeyStrokes();\n    const lastTimeStampMS = keyStrokes[keyStrokes.length - 1].timestamp;\n    return lastTimeStampMS - firstTimeStampMS;\n  }\n\n  getCPM(code: string, timeMS: number): number {\n    const timeSeconds = timeMS / 1000;\n    const strippedCode = Challenge.getStrippedCode(code);\n    const cps = strippedCode.length / timeSeconds;\n    const cpm = cps * 60;\n    return Math.floor(cpm);\n  }\n\n  getMistakesCount(player: RacePlayer): number {\n    return player.incorrectKeyStrokes().length;\n  }\n\n  getAccuracy(player: RacePlayer): number {\n    const incorrectKeyStrokes = player.incorrectKeyStrokes().length;\n    const validKeyStrokes = player.validKeyStrokes().length;\n    const totalKeySrokes = validKeyStrokes + incorrectKeyStrokes;\n    return Math.floor((validKeyStrokes / totalKeySrokes) * 100);\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/results/services/result-factory.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { RacePlayer } from 'src/races/services/race-player.service';\nimport { Race } from 'src/races/services/race.service';\nimport { User } from 'src/users/entities/user.entity';\nimport { Result } from '../entities/result.entity';\nimport { ResultCalculationService } from './result-calculation.service';\n\n@Injectable()\nexport class ResultFactoryService {\n  constructor(private resultCalculation: ResultCalculationService) {}\n  factory(race: Race, player: RacePlayer, user: User): Result {\n    const challenge = race.challenge;\n    const result = new Result();\n    const timeMS = this.resultCalculation.getTimeMS(race, player);\n    const cpm = this.resultCalculation.getCPM(challenge.content, timeMS);\n    const mistakes = this.resultCalculation.getMistakesCount(player);\n    const accuracy = this.resultCalculation.getAccuracy(player);\n    result.raceId = player.raceId;\n    result.user = user;\n    result.challenge = challenge;\n    result.timeMS = timeMS;\n    result.cpm = cpm;\n    result.mistakes = mistakes;\n    result.accuracy = accuracy;\n    return result;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/results/services/results.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { LeaderBoardResult } from '../entities/leaderboard-result.dto';\nimport { Result } from '../entities/result.entity';\n\n@Injectable()\nexport class ResultService {\n  constructor(\n    @InjectRepository(Result)\n    private resultsRepository: Repository<Result>,\n  ) {}\n\n  async create(result: Result): Promise<Result> {\n    return await this.resultsRepository.save(result);\n  }\n\n  async upsertByLegacyId(results: Result[]): Promise<void> {\n    await this.resultsRepository.upsert(results, ['legacyId']);\n  }\n\n  async getByID(id: string) {\n    const result = await this.resultsRepository.findOneOrFail({\n      where: {\n        id,\n        // filter out legacy results\n        legacyId: null,\n      },\n      relations: ['user', 'challenge', 'challenge.project'],\n    });\n    result.percentile = await this.getResultPercentile(result.cpm);\n    return result;\n  }\n\n  async getLeaderboard(): Promise<LeaderBoardResult[]> {\n    const oneDayAgo = new Date();\n    oneDayAgo.setDate(oneDayAgo.getDate() - 1);\n    const resultsTodayStream = await this.resultsRepository\n      .createQueryBuilder('r')\n      .leftJoinAndSelect('r.user', 'u')\n      .where(\n        `u.banned=false AND\n        r.createdAt BETWEEN '${oneDayAgo.toISOString()}' AND '${new Date().toISOString()}'`,\n      )\n      .orderBy('r.cpm')\n      .orderBy('r.createdAt', 'DESC')\n      .stream();\n\n    const resultsToday: Record<string, any> = {};\n\n    for await (const r of resultsTodayStream) {\n      if (!resultsToday[r.u_id]) {\n        r.racesPlayed = 1;\n        resultsToday[r.u_id] = r;\n        continue;\n      }\n      const prevResult = resultsToday[r.u_id];\n      if (r.r_cpm > prevResult.r_cpm) {\n        r.racesPlayed = prevResult.racesPlayed;\n        resultsToday[r.u_id] = r;\n      }\n      resultsToday[r.u_id].racesPlayed++;\n    }\n\n    const results = Object.values(resultsToday)\n      .map((r) => {\n        return {\n          username: r.u_username,\n          avatarUrl: r.u_avatarUrl,\n          cpm: r.r_cpm,\n          accuracy: r.r_accuracy,\n          createdAt: r.r_createdAt,\n          racesPlayed: r.racesPlayed,\n          resultId: r.r_id,\n        };\n      })\n      .sort((a, b) => b.cpm - a.cpm);\n    return results;\n  }\n  async getAverageCPM(userId: string, take: number): Promise<number> {\n    const results = await this.resultsRepository.find({\n      where: {\n        user: { id: userId },\n      },\n      order: {\n        createdAt: 'DESC',\n      },\n      take,\n    });\n    const total = results.reduce((prev, curr) => {\n      return prev + curr.cpm;\n    }, 0);\n    const average = total / results.length;\n    return parseInt(average.toString(), 10);\n  }\n  async getAverageCPMSince(userId: string, since: Date): Promise<number> {\n    const { avg } = await this.resultsRepository\n      .createQueryBuilder('r')\n      .where('r.userId=:userId AND r.createdAt > :startOfToday', {\n        userId,\n        startOfToday: since.toISOString(),\n      })\n      .select('AVG(r.cpm)', 'avg')\n      .getRawOne();\n    return parseInt(avg, 10);\n  }\n\n  async getResultPercentile(cpm: number): Promise<number> {\n    const { countBetterThan } = await this.resultsRepository\n      .createQueryBuilder('r')\n      .where('r.cpm < :cpm', {\n        cpm,\n      })\n      .select('COUNT(r.cpm)', 'countBetterThan')\n      .getRawOne();\n\n    const totalCount = await this.resultsRepository.count();\n\n    const percentile = (\n      (parseInt(countBetterThan, 10) / totalCount) *\n      100\n    ).toFixed(0);\n    return parseInt(percentile, 10);\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/seeder/commands/challenge.seeder.ts",
    "content": "import { Command, CommandRunner } from 'nest-commander';\nimport { Challenge } from 'src/challenges/entities/challenge.entity';\nimport { ChallengeService } from 'src/challenges/services/challenge.service';\nimport { Project } from 'src/projects/entities/project.entity';\nimport { ProjectService } from 'src/projects/services/project.service';\n\n@Command({\n  name: 'seed-challenges',\n  arguments: '',\n  options: {},\n})\nexport class ProjectSeedRunner extends CommandRunner {\n  constructor(\n    private projectService: ProjectService,\n    private challengeService: ChallengeService,\n  ) {\n    super();\n  }\n  async run(): Promise<void> {\n    const project = this.project_factory();\n    await this.projectService.bulkUpsert([project]);\n    const challenges = this.challenges_factory(project);\n    await this.challengeService.upsert(challenges);\n  }\n\n  project_factory() {\n    const project = new Project();\n    project.id = '98dac57c-516e-485f-872a-4b9f6e1ad566';\n    project.fullName = 'etcd-io/etcd';\n    project.htmlUrl = 'https://github.com/etcd-io/etcd';\n    project.language = 'Go';\n    project.stars = 41403;\n    project.licenseName = 'Apache License 2.0';\n    project.ownerAvatar =\n      'https://avatars.githubusercontent.com/u/41972792?v=4';\n    project.defaultBranch = 'main';\n    project.syncedSha = '2d638e0fd2c1d91c9c4323591bb1041b594e28fa';\n    return project;\n  }\n\n  challenges_factory(project: Project) {\n    const challenges = [];\n    const firstChallenge = new Challenge();\n    challenges.push(firstChallenge);\n    firstChallenge.id = 'b4b6eec5-333c-4c77-a648-1b0884ae5ad0';\n    firstChallenge.sha = 'fa3011cb39ac784a88da3667b729f3a79f5f22c3';\n    firstChallenge.treeSha = '2d638e0fd2c1d91c9c4323591bb1041b594e28fa';\n    firstChallenge.path = 'server/etcdserver/api/rafthttp/transport.go';\n    firstChallenge.language = 'go';\n    firstChallenge.url =\n      'https://github.com/etcd-io/etcd/blob/2d638e0fd2c1d91c9c4323591bb1041b594e28fa/server/etcdserver/api/rafthttp/transport.go/#L425';\n    firstChallenge.content =\n      'func (t *Transport) Pause() {\\n' +\n      '\\tt.mu.RLock()\\n' +\n      '\\tdefer t.mu.RUnlock()\\n' +\n      '\\tfor _, p := range t.peers {\\n' +\n      '\\t\\tp.(Pausable).Pause()\\n' +\n      '\\t}\\n' +\n      '}';\n    firstChallenge.project = project;\n\n    const secondChallenge = new Challenge();\n    challenges.push(secondChallenge);\n    secondChallenge.id = '8ebf6be1-7f7c-4edf-a622-97b0024636e8';\n    secondChallenge.sha = '69ecc631471975fcb4d207f85a57baf2b5a79460';\n    secondChallenge.treeSha = '2d638e0fd2c1d91c9c4323591bb1041b594e28fa';\n    secondChallenge.language = 'go';\n    secondChallenge.path = 'client/v3/retry.go';\n    secondChallenge.url =\n      'https://github.com/etcd-io/etcd/blob/2d638e0fd2c1d91c9c4323591bb1041b594e28fa/client/v3/retry.go/#L160';\n    secondChallenge.content =\n      'func RetryClusterClient(c *Client) pb.ClusterClient {\\n' +\n      '\\treturn &retryClusterClient{\\n' +\n      '\\t\\tcc: pb.NewClusterClient(c.conn),\\n' +\n      '\\t}\\n' +\n      '}';\n    secondChallenge.project = project;\n\n    const thirdChallenge = new Challenge();\n    challenges.push(thirdChallenge);\n    thirdChallenge.id = '19174a2e-9220-40c8-832a-7effd351a68b';\n    thirdChallenge.sha = 'ea19cf0181bbedbfc65bce9cfce26eb3558cb9ee';\n    thirdChallenge.treeSha = '2d638e0fd2c1d91c9c4323591bb1041b594e28fa';\n    thirdChallenge.path = 'pkg/schedule/schedule.go';\n    thirdChallenge.language = 'go';\n    thirdChallenge.url =\n      'https://github.com/etcd-io/etcd/blob/2d638e0fd2c1d91c9c4323591bb1041b594e28fa/pkg/schedule/schedule.go/#L135';\n    thirdChallenge.content =\n      'func (f *fifo) Finished() int {\\n' +\n      '\\tf.finishCond.L.Lock()\\n' +\n      '\\tdefer f.finishCond.L.Unlock()\\n' +\n      '\\treturn f.finished\\n' +\n      '}';\n    thirdChallenge.project = project;\n\n    return challenges;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/seeder/seeder.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { ChallengesModule } from 'src/challenges/challenges.module';\nimport { ProjectsModule } from 'src/projects/projects.module';\nimport { ProjectSeedRunner } from './commands/challenge.seeder';\n\n@Module({\n  imports: [ProjectsModule, ChallengesModule],\n  providers: [ProjectSeedRunner],\n})\nexport class SeederModule {}\n"
  },
  {
    "path": "packages/back-nest/src/sessions/session.adapter.ts",
    "content": "import { IncomingMessage } from 'http';\nimport { INestApplication } from '@nestjs/common';\nimport { IoAdapter } from '@nestjs/platform-socket.io';\nimport { NextFunction } from 'express';\nimport { Server, Socket } from 'socket.io';\n\ntype SocketIOCompatibleMiddleware = (\n  r: IncomingMessage,\n  object: object,\n  next: NextFunction,\n) => void;\n\nexport function makeSocketIOReadMiddleware(\n  middleware: SocketIOCompatibleMiddleware,\n) {\n  return (socket: Socket, next: NextFunction) => {\n    return middleware(socket.request, {}, next);\n  };\n}\n\nexport const denyWithoutUserInSession = (\n  socket: Socket,\n  next: NextFunction,\n) => {\n  if (!socket.request.session?.user) {\n    console.log(\n      'disconnect because there is no user in the session',\n      socket.id,\n    );\n    socket.request.session?.destroy(() => {\n      /* **/\n    });\n    return socket.disconnect(true);\n  }\n  next();\n};\n\nexport class SessionAdapter extends IoAdapter {\n  constructor(\n    app: INestApplication,\n    private sessionMiddleware: SocketIOCompatibleMiddleware,\n  ) {\n    super(app);\n  }\n\n  createIOServer(port: number, opt?: any): any {\n    const server: Server = super.createIOServer(port, opt);\n    server.use(makeSocketIOReadMiddleware(this.sessionMiddleware));\n    server.use(denyWithoutUserInSession);\n    return server;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/sessions/session.entity.ts",
    "content": "import { ISession } from 'connect-typeorm';\nimport {\n  Column,\n  DeleteDateColumn,\n  Entity,\n  Index,\n  PrimaryColumn,\n} from 'typeorm';\n\n@Entity({ name: 'sessions' })\nexport class Session implements ISession {\n  @Index()\n  @Column('bigint')\n  expiredAt: number = Date.now();\n\n  @PrimaryColumn('varchar', { length: 255 })\n  id: string;\n\n  @Column('text')\n  json: string;\n\n  @DeleteDateColumn()\n  destroyedAt: Date;\n}\n"
  },
  {
    "path": "packages/back-nest/src/sessions/session.middleware.ts",
    "content": "import { TypeormStore } from 'connect-typeorm/out';\nimport * as session from 'express-session';\nimport { PostgresDataSource } from 'src/database.module';\nimport { Session } from './session.entity';\n\nconst SESSION_SECRET_MIN_LENGTH = 12;\n\nconst ONE_DAY = 1000 * 60 * 60 * 24;\n\nexport const cookieName = 'speedtyper-v2-sid';\n\nexport const getSessionMiddleware = () => {\n  const sessionRepository = PostgresDataSource.getRepository(Session);\n  return session({\n    name: cookieName,\n    store: new TypeormStore({\n      cleanupLimit: 2,\n    }).connect(sessionRepository),\n    secret: getSessionSecret(),\n    resave: false,\n    saveUninitialized: false,\n    cookie: {\n      httpOnly: true,\n      sameSite: 'lax',\n      secure: process.env.NODE_ENV === 'production',\n      maxAge: ONE_DAY * 7,\n      ...(process.env.NODE_ENV === 'production'\n        ? {\n            domain: '.speedtyper.dev',\n          }\n        : {}),\n    },\n  });\n};\n\nfunction getSessionSecret() {\n  const secret = process.env.SESSION_SECRET;\n  if (!secret)\n    throw new Error('SESSION_SECRET is missing from environment variables');\n  if (secret.length < SESSION_SECRET_MIN_LENGTH)\n    throw new Error(\n      `SESSION_SECRET is not long enough, must be at least ${SESSION_SECRET_MIN_LENGTH} characters long`,\n    );\n  return secret;\n}\n"
  },
  {
    "path": "packages/back-nest/src/sessions/types.d.ts",
    "content": "import { Session, SessionData } from 'express-session';\nimport { User } from 'src/users/entities/user.entity';\n\ndeclare module 'express-session' {\n  export interface SessionData {\n    user: User;\n    raceId: string;\n  }\n}\n\ndeclare module 'http' {\n  interface IncomingMessage {\n    cookieHolder?: string;\n    session: Session & SessionData;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/tracking/entities/event.entity.ts",
    "content": "import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\nexport enum TrackingEventType {\n  LegacyRaceStarted = 'legacy_race_started',\n  RaceStarted = 'race_started',\n  RaceCompleted = 'race_completed',\n}\n\n@Entity()\nexport class TrackingEvent {\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n  @Column({\n    unique: true,\n    type: 'enum',\n    enum: TrackingEventType,\n  })\n  event: TrackingEventType;\n  @Column({\n    default: 0,\n  })\n  count: number;\n}\n"
  },
  {
    "path": "packages/back-nest/src/tracking/tracking.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { TrackingEvent } from './entities/event.entity';\nimport { TrackingService } from './tracking.service';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([TrackingEvent])],\n  controllers: [],\n  providers: [TrackingService],\n  exports: [TrackingService],\n})\nexport class TrackingModule {}\n"
  },
  {
    "path": "packages/back-nest/src/tracking/tracking.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { TrackingEvent, TrackingEventType } from './entities/event.entity';\n\n@Injectable()\nexport class TrackingService {\n  constructor(\n    @InjectRepository(TrackingEvent)\n    private repository: Repository<TrackingEvent>,\n  ) {}\n\n  async trackRaceStarted(): Promise<TrackingEvent> {\n    return this.trackRaceEvent(TrackingEventType.RaceStarted);\n  }\n\n  async trackRaceCompleted(): Promise<TrackingEvent> {\n    return this.trackRaceEvent(TrackingEventType.RaceCompleted);\n  }\n\n  private async trackRaceEvent(\n    event: TrackingEventType,\n  ): Promise<TrackingEvent> {\n    return await this.repository.manager.transaction(async (transaction) => {\n      let trackingEvent = new TrackingEvent();\n      trackingEvent.event = event;\n      await transaction.upsert(TrackingEvent, trackingEvent, {\n        conflictPaths: ['event'],\n        skipUpdateIfNoValuesChanged: true,\n      });\n      trackingEvent = await transaction.findOneBy(TrackingEvent, {\n        event: trackingEvent.event,\n      });\n      trackingEvent.count++;\n      trackingEvent = await transaction.save(TrackingEvent, trackingEvent);\n      return trackingEvent;\n    });\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/users/controllers/user.controller.ts",
    "content": "import { Controller, Get, HttpException, Req } from '@nestjs/common';\nimport { Request } from 'express';\nimport { User } from '../entities/user.entity';\n\n@Controller('user')\nexport class UserController {\n  @Get()\n  getCurrentUser(@Req() request: Request): User {\n    if (!request.session?.user) {\n      throw new HttpException('Internal server error', 500);\n    }\n    return request.session.user;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/users/entities/upsertGithubUserDTO.ts",
    "content": "import { IsString } from 'class-validator';\nimport { Profile } from 'passport-github';\nimport { User } from './user.entity';\n\nexport class UpsertGithubUserDTO {\n  @IsString()\n  username: string;\n  @IsString()\n  githubId: string;\n  @IsString()\n  githubUrl: string;\n  @IsString()\n  avatarUrl: string;\n\n  static fromGithubProfile(profile: Profile) {\n    const user = new UpsertGithubUserDTO();\n    user.githubId = profile.id;\n    user.username = profile.username;\n    user.githubUrl = profile.profileUrl;\n    user.avatarUrl = profile.photos[0].value;\n    return user;\n  }\n  toUser() {\n    const user = new User();\n    user.githubId = parseInt(this.githubId);\n    user.username = this.username;\n    user.githubUrl = this.githubUrl;\n    user.avatarUrl = this.avatarUrl;\n    return user;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/users/entities/user.entity.ts",
    "content": "import { randomUUID } from 'crypto';\nimport { Result } from 'src/results/entities/result.entity';\nimport {\n  Column,\n  CreateDateColumn,\n  Entity,\n  OneToMany,\n  PrimaryGeneratedColumn,\n} from 'typeorm';\nimport { generateRandomUsername } from '../utils/generateRandomUsername';\n\n@Entity('users')\nexport class User {\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n  @Column({ unique: true })\n  username: string;\n  @Column({ unique: true })\n  githubId: number;\n  @Column({ unique: true })\n  githubUrl: string;\n  @Column()\n  avatarUrl: string;\n  @Column({ unique: true, nullable: true })\n  legacyId: string;\n  @Column({ default: false, select: false })\n  banned: boolean;\n  @CreateDateColumn({\n    type: 'timestamp',\n    default: () => 'CURRENT_TIMESTAMP(6)',\n  })\n  public createdAt: Date;\n\n  @OneToMany(() => Result, (result) => result.user)\n  results: Result[];\n  isAnonymous: boolean;\n  static generateAnonymousUser() {\n    const user = new User();\n    user.id = randomUUID();\n    user.username = generateRandomUsername();\n    user.isAnonymous = true;\n    return user;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/users/services/user.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { User } from '../entities/user.entity';\n\n@Injectable()\nexport class UserService {\n  constructor(\n    @InjectRepository(User)\n    private userRepository: Repository<User>,\n  ) {}\n\n  async upsertGithubUser(userData: User): Promise<User> {\n    const currentUser = await this.userRepository.findOneBy({\n      githubId: userData.githubId,\n    });\n    userData.id = currentUser?.id;\n    userData.banned = currentUser?.banned || userData.banned || false;\n    const user = await this.userRepository.save(userData);\n    user.isAnonymous = false;\n    return user;\n  }\n\n  async findByLegacyID(legacyId: string) {\n    const user = await this.userRepository.findOneBy({\n      legacyId,\n    });\n    return user;\n  }\n}\n"
  },
  {
    "path": "packages/back-nest/src/users/users.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { UserController } from './controllers/user.controller';\nimport { User } from './entities/user.entity';\nimport { UserService } from './services/user.service';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([User])],\n  controllers: [UserController],\n  providers: [UserService],\n  exports: [UserService],\n})\nexport class UsersModule {}\n"
  },
  {
    "path": "packages/back-nest/src/users/utils/generateRandomUsername.ts",
    "content": "import { uniqueNamesGenerator } from 'unique-names-generator';\n\nconst adjectives2 = [\n  'abrupt',\n  'acidic',\n  'adorable',\n  'adventurous',\n  'aggressive',\n  'agitated',\n  'aloof',\n  'amused',\n  'annoyed',\n  'antsy',\n  'anxious',\n  'appalling',\n  'apprehensive',\n  'arrogant',\n  'astonishing',\n  'bitter',\n  'bland',\n  'bored',\n  'brave',\n  'bright',\n  'broad',\n  'bulky',\n  'burly',\n  'charming',\n  'cheeky',\n  'cheerful',\n  'clean',\n  'clear',\n  'cloudy',\n  'clueless',\n  'clumsy',\n  'colorful',\n  'colossal',\n  'confused',\n  'convincing',\n  'convoluted',\n  'cooperative',\n  'courageous',\n  'crooked',\n  'cruel',\n  'cynical',\n  'dangerous',\n  'dashing',\n  'deceitful',\n  'defeated',\n  'defiant',\n  'delicious',\n  'delightful',\n  'depraved',\n  'depressed',\n  'despicable',\n  'determined',\n  'dilapidated',\n  'diminutive',\n  'disgusted',\n  'distinct',\n  'distraught',\n  'distressed',\n  'disturbed',\n  'dizzy',\n  'drab',\n  'drained',\n  'dull',\n  'eager',\n  'ecstatic',\n  'elated',\n  'elegant',\n  'emaciated',\n  'embarrassed',\n  'enchanting',\n  'encouraging',\n  'energetic',\n  'enormous',\n  'enthusiastic',\n  'envious',\n  'exasperated',\n  'excited',\n  'exhilarated',\n  'extensive',\n  'exuberant',\n  'fancy',\n  'fantastic',\n  'fierce',\n  'fluttering',\n  'foolish',\n  'frantic',\n  'fresh',\n  'friendly',\n  'frightened',\n  'frothy',\n  'frustrating',\n  'funny',\n  'fuzzy',\n  'gaudy',\n  'gentle',\n  'giddy',\n  'gigantic',\n  'glamorous',\n  'gleaming',\n  'glorious',\n  'gorgeous',\n  'graceful',\n  'greasy',\n  'grieving',\n  'gritty',\n  'grotesque',\n  'grubby',\n  'grumpy',\n  'handsome',\n  'happy',\n  'harebrained',\n  'healthy',\n  'helpful',\n  'helpless',\n  'high',\n  'hollow',\n  'homely',\n  'horrific',\n  'huge',\n  'hungry',\n  'hurt',\n  'icy',\n  'ideal',\n  'immense',\n  'impressionable',\n  'intrigued',\n  'irate',\n  'irritable',\n  'itchy',\n  'jealous',\n  'jittery',\n  'jolly',\n  'joyous',\n  'juicy',\n  'jumpy',\n  'kind',\n  'large',\n  'lazy',\n  'lethal',\n  'little',\n  'lively',\n  'livid',\n  'lonely',\n  'loose',\n  'lovely',\n  'lucky',\n  'ludicrous',\n  'magnificent',\n  'mammoth',\n  'maniacal',\n  'massive',\n  'melancholy',\n  'melted',\n  'miniature',\n  'minute',\n  'mistaken',\n  'misty',\n  'moody',\n  'mortified',\n  'motionless',\n  'mysterious',\n  'narrow',\n  'nasty',\n  'naughty',\n  'nervous',\n  'nonchalant',\n  'nonsensical',\n  'nutritious',\n  'nutty',\n  'obedient',\n  'oblivious',\n  'obnoxious',\n  'odd',\n  'outrageous',\n  'panicky',\n  'perfect',\n  'perplexed',\n  'petite',\n  'petty',\n  'plain',\n  'pleasant',\n  'poised',\n  'pompous',\n  'precious',\n  'prickly',\n  'proud',\n  'pungent',\n  'puny',\n  'quaint',\n  'quizzical',\n  'ratty',\n  'reassured',\n  'relieved',\n  'repulsive',\n  'responsive',\n  'ripe',\n  'robust',\n  'rotten',\n  'rough',\n  'round',\n  'salty',\n  'sarcastic',\n  'scant',\n  'scary',\n  'scattered',\n  'scrawny',\n  'selfish',\n  'shaky',\n  'shallow',\n  'sharp',\n  'shiny',\n  'short',\n  'silky',\n  'silly',\n  'skinny',\n  'slimy',\n  'slippery',\n  'small',\n  'smarmy',\n  'smiling',\n  'smoggy',\n  'smooth',\n  'smug',\n  'soggy',\n  'solid',\n  'sore',\n  'sour',\n  'sparkling',\n  'spicy',\n  'splendid',\n  'spotless',\n  'square',\n  'stale',\n  'steady',\n  'steep',\n  'responsive',\n  'sticky',\n  'stormy',\n  'stout',\n  'strange',\n  'strong',\n  'stunning',\n  'substantial',\n  'successful',\n  'succulent',\n  'superficial',\n  'superior',\n  'sweet',\n  'tart',\n  'tasty',\n  'tender',\n  'tense',\n  'terrible',\n  'thankful',\n  'thick',\n  'thoughtful',\n  'thoughtless',\n  'timely',\n  'tricky',\n  'troubled',\n  'uneven',\n  'unsightly',\n  'upset',\n  'uptight',\n  'vast',\n  'vexed',\n  'victorious',\n  'virtuous',\n  'vivacious',\n  'vivid',\n  'wacky',\n  'weary',\n  'whimsical',\n  'whopping',\n  'wicked',\n  'witty',\n  'wobbly',\n  'wonderful',\n  'worried',\n  'yummy',\n  'zany',\n  'zealous',\n  'zippy',\n];\nconst technologies = [\n  'Bash',\n  'C',\n  'C#',\n  'C++',\n  'CSS',\n  'Elm',\n  'Eno',\n  'ERB',\n  'Fennel',\n  'Golang',\n  'HTML',\n  'Java',\n  'JavaScript',\n  'Lua',\n  'Make',\n  'Markdown',\n  'OCaml',\n  'PHP',\n  'Python',\n  'Ruby',\n  'Rust',\n  'R',\n  'S-expressions',\n  'SPARQL',\n  'SystemRDL',\n  'Svelte',\n  'TOML',\n  'Turtle',\n  'TypeScript',\n  'Verilog',\n  'VHDL',\n  'Vue',\n  'YAML',\n  'WASM',\n  'Agda',\n  'Erlang',\n  'Dockerfile',\n  'Go',\n  'Haskell',\n  'Kotlin',\n  'Nix',\n  'Perl',\n  'Scala',\n  'Swift',\n  'Arch',\n  'Ubuntu',\n  'Mac',\n  'Windows',\n  'GNU',\n  'Linux',\n  'BSD',\n  'Arduino',\n  'Clojure',\n  'Blockchain',\n  'Elixir',\n  'Angular',\n  'Vue',\n  'Svelte',\n  'React',\n  'Re-frame',\n  'Stateless',\n  'Kernel',\n  'Context',\n  'OpenGL',\n  'MicroServices',\n  'Monolith',\n  'Monorepo',\n  'SQL',\n  'Firebase',\n  'MongoDB',\n  'Postgres',\n  'MySQL',\n  'Ionic',\n  'Phoenix',\n  'Cordova',\n  'ReactNative',\n  'PowerShell',\n  'Vim',\n  'VSCode',\n  'Emacs',\n  'Cobol',\n  'Zsh',\n  'Assembly',\n  'OpenCV',\n  'HTTP',\n  'SSH',\n  'FTP',\n  'Tensorflow',\n  'PyTorch',\n  'Pandas',\n  'Unity',\n  'Unreal',\n  'Docker',\n  'Kubernetes',\n  'Godot',\n];\n\nexport const generateRandomUsername = () => {\n  const randomName: string = uniqueNamesGenerator({\n    dictionaries: [adjectives2, technologies],\n    separator: '',\n    length: 2,\n    style: 'capital',\n  });\n\n  return randomName;\n};\n"
  },
  {
    "path": "packages/back-nest/src/utils/validateDTO.ts",
    "content": "import { ValidationError } from '@nestjs/common';\nimport { ClassConstructor, plainToInstance } from 'class-transformer';\nimport { validate } from 'class-validator';\n\nexport class ValidationErrorContainer extends TypeError {\n  errors: ValidationError[];\n  constructor(name: string, errors: ValidationError[]) {\n    const fields = errors.map((err) => err.property).join(', ');\n    super(`Error validating ${name} for fields: ${fields}`);\n    this.errors = errors;\n    Object.setPrototypeOf(this, ValidationErrorContainer.prototype);\n  }\n}\n\nexport const validateDTO = async <T>(\n  dto: ClassConstructor<T>,\n  obj: unknown,\n) => {\n  const instance = plainToInstance(dto, obj);\n  const errors = await validate(instance as object);\n  if (errors.length > 0) {\n    throw new ValidationErrorContainer(dto.name, errors);\n  }\n  return instance;\n};\n"
  },
  {
    "path": "packages/back-nest/tracked-projects.txt",
    "content": "etcd-io/etcd\nrust-lang/cargo\nrust-lang/rust\ntiangolo/fastapi\npallets/flask\nencode/starlette\napache/zookeeper\nClickHouse/ClickHouse\nrails/rails\nlodash/lodash\nTheAlgorithms/Java\nggerganov/llama.cpp\nggerganov/whisper.cpp\nvitejs/vite\nlampepfl/dotty\ntinygrad/tinygrad\n"
  },
  {
    "path": "packages/back-nest/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"exclude\": [\"node_modules\", \"test\", \"dist\", \"**/*spec.ts\"]\n}\n"
  },
  {
    "path": "packages/back-nest/tsconfig.json",
    "content": "{\n  \"ts-node\": {\n    \"files\": true\n  },\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"declaration\": true,\n    \"removeComments\": true,\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"target\": \"es2021\",\n    \"sourceMap\": true,\n    \"outDir\": \"./dist\",\n    \"baseUrl\": \"./\",\n    \"incremental\": true,\n    \"skipLibCheck\": true,\n    \"strictNullChecks\": false,\n    \"noImplicitAny\": false,\n    \"strictBindCallApply\": false,\n    \"forceConsistentCasingInFileNames\": false,\n    \"noFallthroughCasesInSwitch\": false\n  }\n}\n"
  },
  {
    "path": "packages/webapp-next/.eslintrc.json",
    "content": "{\n  \"extends\": [\"next/core-web-vitals\", \"prettier\"],\n  \"plugins\": [\"prettier\"],\n  \"rules\": {\n    \"prettier/prettier\": [\n      \"error\",\n      {\n        \"endOfLine\": \"auto\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/webapp-next/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n.next/\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "packages/webapp-next/.prettierrc",
    "content": "{\n  \"singleQuote\": false,\n  \"jsxSingleQuote\": false\n}\n\n"
  },
  {
    "path": "packages/webapp-next/README.md",
    "content": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).\n\n## Getting Started\n\nFirst, run the development server:\n\n```bash\nnpm run dev\n# or\nyarn dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n\nYou can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.\n\n[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.\n\nThe `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.\n\n## Learn More\n\nTo learn more about Next.js, take a look at the following resources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.\n- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.\n\nYou can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!\n\n## Deploy on Vercel\n\nThe easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.\n\nCheck out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.\n"
  },
  {
    "path": "packages/webapp-next/Socket.ts",
    "content": "import io from \"socket.io-client\";\n\nexport default class Socket {\n  socket: SocketIOClient.Socket;\n\n  constructor(serverUrl: string) {\n    this.socket = io(serverUrl);\n  }\n\n  disconnect = () => {\n    this.socket.emit(\"disconnect\");\n    if (this.socket) this.socket.disconnect();\n  };\n\n  subscribe = (event: string, cb: (error: string | null, msg: any) => void) => {\n    if (!this.socket) return true;\n    this.socket.on(event, (msg: any) => {\n      return cb(null, msg);\n    });\n  };\n\n  emit = (event: string, data?: any) => {\n    if (this.socket) this.socket.emit(event, data);\n  };\n}\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/BattleIcon.tsx",
    "content": "export const BattleIcon = () => {\n  // <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->\n  return (\n    <svg className=\"h-6 fill-current\" viewBox=\"0 0 512 512\">\n      <path d=\"M448.61 225.62c26.87.18 35.57-7.43 38.92-12.37 12.47-16.32-7.06-47.6-52.85-71.33 17.76-33.58 30.11-63.68 36.34-85.3 3.38-11.83 1.09-19 .45-20.25-1.72 10.52-15.85 48.46-48.2 100.05-25-11.22-56.52-20.1-93.77-23.8-8.94-16.94-34.88-63.86-60.48-88.93C252.18 7.14 238.7 1.07 228.18.22h-.05c-13.83-1.55-22.67 5.85-27.4 11-17.2 18.53-24.33 48.87-25 84.07-7.24-12.35-17.17-24.63-28.5-25.93h-.18c-20.66-3.48-38.39 29.22-36 81.29-38.36 1.38-71 5.75-93 11.23-9.9 2.45-16.22 7.27-17.76 9.72 1-.38 22.4-9.22 111.56-9.22 5.22 53 29.75 101.82 26 93.19-9.73 15.4-38.24 62.36-47.31 97.7-5.87 22.88-4.37 37.61.15 47.14 5.57 12.75 16.41 16.72 23.2 18.26 25 5.71 55.38-3.63 86.7-21.14-7.53 12.84-13.9 28.51-9.06 39.34 7.31 19.65 44.49 18.66 88.44-9.45 20.18 32.18 40.07 57.94 55.7 74.12a39.79 39.79 0 0 0 8.75 7.09c5.14 3.21 8.58 3.37 8.58 3.37-8.24-6.75-34-38-62.54-91.78 22.22-16 45.65-38.87 67.47-69.27 122.82 4.6 143.29-24.76 148-31.64 14.67-19.88 3.43-57.44-57.32-93.69zm-77.85 106.22c23.81-37.71 30.34-67.77 29.45-92.33 27.86 17.57 47.18 37.58 49.06 58.83 1.14 12.93-8.1 29.12-78.51 33.5zM216.9 387.69c9.76-6.23 19.53-13.12 29.2-20.49 6.68 13.33 13.6 26.1 20.6 38.19-40.6 21.86-68.84 12.76-49.8-17.7zm215-171.35c-10.29-5.34-21.16-10.34-32.38-15.05a722.459 722.459 0 0 0 22.74-36.9c39.06 24.1 45.9 53.18 9.64 51.95zM279.18 398c-5.51-11.35-11-23.5-16.5-36.44 43.25 1.27 62.42-18.73 63.28-20.41 0 .07-25 15.64-62.53 12.25a718.78 718.78 0 0 0 85.06-84q13.06-15.31 24.93-31.11c-.36-.29-1.54-3-16.51-12-51.7 60.27-102.34 98-132.75 115.92-20.59-11.18-40.84-31.78-55.71-61.49-20-39.92-30-82.39-31.57-116.07 12.3.91 25.27 2.17 38.85 3.88-22.29 36.8-14.39 63-13.47 64.23 0-.07-.95-29.17 20.14-59.57a695.23 695.23 0 0 0 44.67 152.84c.93-.38 1.84.88 18.67-8.25-26.33-74.47-33.76-138.17-34-173.43 20-12.42 48.18-19.8 81.63-17.81 44.57 2.67 86.36 15.25 116.32 30.71q-10.69 15.66-23.33 32.47C365.63 152 339.1 145.84 337.5 146c.11 0 25.9 14.07 41.52 47.22a717.63 717.63 0 0 0-115.34-31.71 646.608 646.608 0 0 0-39.39-6.05c-.07.45-1.81 1.85-2.16 20.33C300 190.28 358.78 215.68 389.36 233c.74 23.55-6.95 51.61-25.41 79.57-24.6 37.31-56.39 67.23-84.77 85.43zm27.4-287c-44.56-1.66-73.58 7.43-94.69 20.67 2-52.3 21.31-76.38 38.21-75.28C267 52.15 305 108.55 306.58 111zm-130.65 3.1c.48 12.11 1.59 24.62 3.21 37.28-14.55-.85-28.74-1.25-42.4-1.26-.08 3.24-.12-51 24.67-49.59h.09c5.76 1.09 10.63 6.88 14.43 13.57zm-28.06 162c20.76 39.7 43.3 60.57 65.25 72.31-46.79 24.76-77.53 20-84.92 4.51-.2-.21-11.13-15.3 19.67-76.81zm210.06 74.8\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/CopyIcon.tsx",
    "content": "export const CopyIcon = () => {\n  return (\n    <svg\n      viewBox=\"0 0 128 128\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      className=\"h-5 fill-current\"\n    >\n      <path d=\"m118.786 23.732a1.893 1.893 0 0 0 -.479-.9l-22.32-22.322a1.811 1.811 0 0 0 -1.237-.51h-41.043a7.759 7.759 0 0 0 -7.75 7.75v10.635h-10.638a7.759 7.759 0 0 0 -7.75 7.75v10.638h-10.638a7.759 7.759 0 0 0 -7.75 7.75v75.724a7.758 7.758 0 0 0 7.75 7.75h57.362a7.759 7.759 0 0 0 7.75-7.75v-10.638h10.638a7.759 7.759 0 0 0 7.75-7.75v-10.638h10.638a7.759 7.759 0 0 0 7.75-7.75v-59.4a1.689 1.689 0 0 0 -.033-.339zm-22.286-17.76 8.172 8.172 8.172 8.172h-16.344zm-17.957 114.275a4.255 4.255 0 0 1 -4.25 4.25h-57.362a4.255 4.255 0 0 1 -4.25-4.25v-75.724a4.255 4.255 0 0 1 4.25-4.25h10.638v67.586a1.75 1.75 0 0 0 1.75 1.75h49.224zm18.388-18.388a4.255 4.255 0 0 1 -4.25 4.25h-61.612v-79.974a4.255 4.255 0 0 1 4.25-4.25h10.638v67.586a1.75 1.75 0 0 0 1.75 1.75h49.224zm14.138-14.138h-61.612v-79.974a4.255 4.255 0 0 1 4.25-4.25h39.293v20.569a1.749 1.749 0 0 0 1.75 1.75h20.569v57.655a4.255 4.255 0 0 1 -4.25 4.25z\"></path>\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/CrossIcon.tsx",
    "content": "export const CrossIcon = () => {\n  // Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc.\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 512\">\n      <path d=\"M310.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L160 210.7 54.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L114.7 256 9.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 301.3 265.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L205.3 256 310.6 150.6z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/CrownIcon.tsx",
    "content": "export const CrownIcon = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 576 512\"\n      className=\"fill-current\"\n      style={{\n        fontWeight: \"bold\",\n        height: \"15px\",\n      }}\n    >\n      <path d=\"M309 106c11.4-7 19-19.7 19-34c0-22.1-17.9-40-40-40s-40 17.9-40 40c0 14.4 7.6 27 19 34L209.7 220.6c-9.1 18.2-32.7 23.4-48.6 10.7L72 160c5-6.7 8-15 8-24c0-22.1-17.9-40-40-40S0 113.9 0 136s17.9 40 40 40c.2 0 .5 0 .7 0L86.4 427.4c5.5 30.4 32 52.6 63 52.6H426.6c30.9 0 57.4-22.1 63-52.6L535.3 176c.2 0 .5 0 .7 0c22.1 0 40-17.9 40-40s-17.9-40-40-40s-40 17.9-40 40c0 9 3 17.3 8 24l-89.1 71.3c-15.9 12.7-39.5 7.5-48.6-10.7L309 106z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/DiscordLogo.tsx",
    "content": "export const DiscordLogo = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"20\"\n      height=\"20\"\n      viewBox=\"0 0 245 240\"\n      className=\"fill-current\"\n    >\n      <path d=\"M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z\" />\n      <path d=\"M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/DownArrowIcon.tsx",
    "content": "export const DownArrowIcon = () => {\n  return (\n    <svg\n      className=\"-mr-1 ml-2 h-5 w-5\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 20 20\"\n      fill=\"currentColor\"\n      aria-hidden=\"true\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        d=\"M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z\"\n        clipRule=\"evenodd\"\n      />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/GithubLogo.tsx",
    "content": "export const GithubLogo = () => (\n  <svg className=\"fill-current\" height=\"15\" width=\"15\" viewBox=\"0 0 16 16\">\n    <path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z\"></path>\n  </svg>\n);\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/InfoIcon.tsx",
    "content": "export const InfoIcon = () => {\n  return (\n    // Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->\n    <svg\n      className=\"h-full fill-current\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 512 512\"\n    >\n      <path d=\"M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/KogWheel.tsx",
    "content": "export const KogWheel = () => {\n  // Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.\n  return (\n    <svg\n      className=\"h-full fill-current\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 512 512\"\n    >\n      <path d=\"M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336c44.2 0 80-35.8 80-80s-35.8-80-80-80s-80 35.8-80 80s35.8 80 80 80z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/LinkIcon.tsx",
    "content": "export const LinkIcon = () => {\n  return (\n    // Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->\n    <svg viewBox=\"0 0 640 512\" className=\"fill-current\">\n      <path d=\"M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/OnlineIcon.tsx",
    "content": "export const OnlineIcon = () => {\n  return (\n    // Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc.\n    <svg\n      className=\"h-full fill-current\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 512 512\"\n    >\n      <path d=\"M352 256c0 22.2-1.2 43.6-3.3 64H163.3c-2.2-20.4-3.3-41.8-3.3-64s1.2-43.6 3.3-64H348.7c2.2 20.4 3.3 41.8 3.3 64zm28.8-64H503.9c5.3 20.5 8.1 41.9 8.1 64s-2.8 43.5-8.1 64H380.8c2.1-20.6 3.2-42 3.2-64s-1.1-43.4-3.2-64zm112.6-32H376.7c-10-63.9-29.8-117.4-55.3-151.6c78.3 20.7 142 77.5 171.9 151.6zm-149.1 0H167.7c6.1-36.4 15.5-68.6 27-94.7c10.5-23.6 22.2-40.7 33.5-51.5C239.4 3.2 248.7 0 256 0s16.6 3.2 27.8 13.8c11.3 10.8 23 27.9 33.5 51.5c11.6 26 21 58.2 27 94.7zm-209 0H18.6C48.6 85.9 112.2 29.1 190.6 8.4C165.1 42.6 145.3 96.1 135.3 160zM8.1 192H131.2c-2.1 20.6-3.2 42-3.2 64s1.1 43.4 3.2 64H8.1C2.8 299.5 0 278.1 0 256s2.8-43.5 8.1-64zM194.7 446.6c-11.6-26-20.9-58.2-27-94.6H344.3c-6.1 36.4-15.5 68.6-27 94.6c-10.5 23.6-22.2 40.7-33.5 51.5C272.6 508.8 263.3 512 256 512s-16.6-3.2-27.8-13.8c-11.3-10.8-23-27.9-33.5-51.5zM135.3 352c10 63.9 29.8 117.4 55.3 151.6C112.2 482.9 48.6 426.1 18.6 352H135.3zm358.1 0c-30 74.1-93.6 130.9-171.9 151.6c25.5-34.2 45.2-87.7 55.3-151.6H493.4z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/PlayIcon.tsx",
    "content": "export const PlayIcon = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 384 512\"\n      className=\"h-4 fill-current\"\n    >\n      <path d=\"M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/ProfileIcon.tsx",
    "content": "export const ProfileIcon = () => {\n  return (\n    <svg\n      className=\"h-6 fill-current\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 448 512\"\n    >\n      <path d=\"M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0S96 57.3 96 128s57.3 128 128 128zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/ReloadIcon.tsx",
    "content": "export const ReloadIcon = () => {\n  return (\n    // Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.\n    <svg className=\"fill-current\" viewBox=\"0 0 512 512\">\n      <path d=\"M463.5 224H472c13.3 0 24-10.7 24-24V72c0-9.7-5.8-18.5-14.8-22.2s-19.3-1.7-26.2 5.2L413.4 96.6c-87.6-86.5-228.7-86.2-315.8 1c-87.5 87.5-87.5 229.3 0 316.8s229.3 87.5 316.8 0c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0c-62.5 62.5-163.8 62.5-226.3 0s-62.5-163.8 0-226.3c62.2-62.2 162.7-62.5 225.3-1L327 183c-6.9 6.9-8.9 17.2-5.2 26.2s12.5 14.8 22.2 14.8H463.5z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/RightArrowIcon.tsx",
    "content": "export const RightArrowIcon = () => {\n  return (\n    <svg viewBox=\"0 0 24 24\" className=\"h-5 fill-current ml-2\">\n      <path d=\"M7.96 21.15l-.65-.76 9.555-8.16L7.31 4.07l.65-.76 10.445 8.92\"></path>\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/TerminalIcon.tsx",
    "content": "export const TerminalIcon = () => {\n  // <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 640 512\"\n      className=\"fill-current\"\n      style={{\n        fontWeight: \"bold\",\n        height: \"15px\",\n      }}\n    >\n      <path d=\"M392.8 1.2c-17-4.9-34.7 5-39.6 22l-128 448c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l128-448c4.9-17-5-34.7-22-39.6zm80.6 120.1c-12.5 12.5-12.5 32.8 0 45.3L562.7 256l-89.4 89.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l112-112c12.5-12.5 12.5-32.8 0-45.3l-112-112c-12.5-12.5-32.8-12.5-45.3 0zm-306.7 0c-12.5-12.5-32.8-12.5-45.3 0l-112 112c-12.5 12.5-12.5 32.8 0 45.3l112 112c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256l89.4-89.4c12.5-12.5 12.5-32.8 0-45.3z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/TwitchLogo.tsx",
    "content": "export const TwitchLogo = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"17\"\n      height=\"17\"\n      fill=\"currentColor\"\n      viewBox=\"0 0 18 18\"\n      className=\"fill-current\"\n    >\n      <path d=\"M3.857 0L1 2.857v10.286h3.429V16l2.857-2.857H9.57L14.714 8V0H3.857zm9.714 7.429l-2.285 2.285H9l-2 2v-2H4.429V1.143h9.142v6.286z\" />\n      <path d=\"M11.857 3.143h-1.143V6.57h1.143V3.143zm-3.143 0H7.571V6.57h1.143V3.143z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/UserGroupIcon.tsx",
    "content": "export const UserGroupIcon = () => {\n  // <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->\n  return (\n    <svg\n      className=\"fill-current h-full\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 640 512\"\n    >\n      <path d=\"M352 128c0 70.7-57.3 128-128 128s-128-57.3-128-128S153.3 0 224 0s128 57.3 128 128zM0 482.3C0 383.8 79.8 304 178.3 304h91.4C368.2 304 448 383.8 448 482.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3zM609.3 512H471.4c5.4-9.4 8.6-20.3 8.6-32v-8c0-60.7-27.1-115.2-69.8-151.8c2.4-.1 4.7-.2 7.1-.2h61.4C567.8 320 640 392.2 640 481.3c0 17-13.8 30.7-30.7 30.7zM432 256c-31 0-59-12.6-79.3-32.9C372.4 196.5 384 163.6 384 128c0-26.8-6.6-52.1-18.3-74.3C384.3 40.1 407.2 32 432 32c61.9 0 112 50.1 112 112s-50.1 112-112 112z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/WarningIcon.tsx",
    "content": "export const WarningIcon = () => {\n  // <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 512 512\"\n      className=\"h-5 fill-current\"\n    >\n      <path d=\"M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224c0-17.7-14.3-32-32-32s-32 14.3-32 32s14.3 32 32 32s32-14.3 32-32z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/YoutubeLogo.tsx",
    "content": "export const YoutubeLogo = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"-35.20005 -41.33325 305.0671 247.9995\"\n      className=\"fill-current\"\n    >\n      <path d=\"M229.763 25.817c-2.699-10.162-10.65-18.165-20.748-20.881C190.716 0 117.333 0 117.333 0S43.951 0 25.651 4.936C15.553 7.652 7.6 15.655 4.903 25.817 0 44.236 0 82.667 0 82.667s0 38.429 4.903 56.85C7.6 149.68 15.553 157.681 25.65 160.4c18.3 4.934 91.682 4.934 91.682 4.934s73.383 0 91.682-4.934c10.098-2.718 18.049-10.72 20.748-20.882 4.904-18.421 4.904-56.85 4.904-56.85s0-38.431-4.904-56.85\" />\n      <path d=\"M93.333 117.559l61.333-34.89-61.333-34.894z\" fill=\"#fff\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/assets/icons/index.tsx",
    "content": "export * from \"./CopyIcon\";\nexport * from \"./DiscordLogo\";\nexport * from \"./DownArrowIcon\";\nexport * from \"./GithubLogo\";\nexport * from \"./PlayIcon\";\nexport * from \"./ProfileIcon\";\nexport * from \"./RightArrowIcon\";\nexport * from \"./TwitchLogo\";\n"
  },
  {
    "path": "packages/webapp-next/common/api/auth.ts",
    "content": "import { NextRouter } from \"next/router\";\nimport { useCallback } from \"react\";\nimport { useGameStore } from \"../../modules/play2/state/game-store\";\nimport { useUserStore } from \"../state/user-store\";\nimport { getExperimentalServerUrl, getSiteRoot } from \"../utils/getServerUrl\";\nimport { fetchUser } from \"./user\";\n\nexport const useGithubAuthFactory = (router: NextRouter, serverUrl: string) => {\n  return useCallback(() => {\n    let nextUrl = getSiteRoot();\n    if (document !== undefined) {\n      nextUrl = window.location.href;\n    }\n    const authUrl = `${serverUrl}/auth/github?next=${nextUrl}`;\n    router.push(authUrl);\n  }, [router, serverUrl]);\n};\n\nexport const logout = async () => {\n  const serverUrl = getExperimentalServerUrl();\n  const authUrl = `${serverUrl}/api/auth`;\n  return fetch(authUrl, {\n    method: \"DELETE\",\n    credentials: \"include\",\n  }).then(async () => {\n    const prevUserId = useUserStore.getState().id;\n    const user = await fetchUser();\n    useUserStore.setState((state) => ({\n      ...state,\n      ...user,\n      avatarUrl: undefined,\n    }));\n    useGameStore.setState((state) => {\n      return {\n        ...state,\n        owner: state.owner === prevUserId ? user.id : state.owner,\n      };\n    });\n    useGameStore.getState().game?.reconnect();\n  });\n};\n"
  },
  {
    "path": "packages/webapp-next/common/api/races.ts",
    "content": "import { getExperimentalServerUrl } from \"../utils/getServerUrl\";\n\nconst serverUrl = getExperimentalServerUrl();\n\nconst RACE_STATUS_API = \"/api/races/:id/status\";\n\nexport const fetchRaceStatus = async (raceId: string) => {\n  const url = serverUrl + RACE_STATUS_API.replace(\":id\", raceId);\n  return fetch(url, {\n    credentials: \"include\",\n  }).then((resp) => resp.json());\n};\n\nexport const ONLINE_COUNT_API = serverUrl + \"/api/races/online\";\n\nexport const fetchOnlineCount = async () => {\n  return fetch(ONLINE_COUNT_API, {\n    credentials: \"include\",\n  }).then((resp) => resp.json());\n};\n"
  },
  {
    "path": "packages/webapp-next/common/api/types.ts",
    "content": "import { GetServerSidePropsContext, PreviewData } from \"next\";\nimport { ParsedUrlQuery } from \"querystring\";\n\nexport type ServerSideContext = GetServerSidePropsContext<\n  ParsedUrlQuery,\n  PreviewData\n>;\n"
  },
  {
    "path": "packages/webapp-next/common/api/user.ts",
    "content": "import { useEffect, useState } from \"react\";\nimport { User } from \"../state/user-store\";\nimport { getExperimentalServerUrl } from \"../utils/getServerUrl\";\nimport { ServerSideContext } from \"./types\";\n\nconst USER_API = \"/api/user\";\n\nconst withCookie = (ctx?: ServerSideContext) => {\n  const cookie = ctx?.req?.headers?.cookie;\n  return cookie ? { cookie } : undefined;\n};\n\nconst withSetHeaders = (resp: Response, ctx?: ServerSideContext) => {\n  const setCookie = resp.headers.get(\"set-cookie\");\n  if (ctx && setCookie) {\n    ctx.res.setHeader(\"set-cookie\", setCookie);\n  }\n  return resp;\n};\n\nexport const fetchUser = async (context?: ServerSideContext) => {\n  const serverUrl = getExperimentalServerUrl();\n  const url = serverUrl + USER_API;\n  return fetch(url, {\n    credentials: \"include\",\n    headers: withCookie(context),\n  }).then((resp) => withSetHeaders(resp, context).json());\n};\n\nexport const fetchUser2 = async () => {\n  const serverUrl = getExperimentalServerUrl();\n  const url = serverUrl + USER_API;\n  return fetch(url, {\n    credentials: \"include\",\n  }).then((resp) => resp.json());\n};\n\nexport const useUser = () => {\n  const [user, setUser] = useState<User | null>(null);\n  useEffect(() => {\n    fetchUser2().then((u) => setUser(u));\n  }, []);\n  return user;\n};\n"
  },
  {
    "path": "packages/webapp-next/common/components/Avatar.tsx",
    "content": "import Image from \"next/image\";\nimport { ProfileIcon } from \"../../assets/icons\";\n\ninterface AvatarProps {\n  avatarUrl?: string;\n  username: string;\n}\n\nexport const Avatar: React.FC<AvatarProps> = ({ avatarUrl, username }) => {\n  return (\n    <div className=\"flex items-center cursor-pointer gap-2\">\n      <span className=\"hidden sm:block font-extrabold tracking-wider text-sm\">{username}</span>\n      {avatarUrl ? (\n        <Image\n          className=\"cursor-pointer rounded-full\"\n          width=\"30\"\n          height=\"30\"\n          quality={100}\n          src={avatarUrl}\n          alt={username}\n        />\n      ) : (\n        <ProfileIcon />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/common/components/BattleMatcher.tsx",
    "content": "import { useState } from \"react\";\nimport useSWR from \"swr\";\nimport { InfoIcon } from \"../../assets/icons/InfoIcon\";\nimport Modal from \"./modals/Modal\";\nimport { OnlineIcon } from \"../../assets/icons/OnlineIcon\";\nimport { UserGroupIcon } from \"../../assets/icons/UserGroupIcon\";\nimport { ToggleSelector } from \"../../modules/play2/components/RaceSettings\";\nimport { useGameStore, useIsOwner } from \"../../modules/play2/state/game-store\";\nimport {\n  closeModals,\n  openPublicRacesModal,\n  toggleRaceIsPublic,\n  useSettingsStore,\n} from \"../../modules/play2/state/settings-store\";\nimport { ONLINE_COUNT_API } from \"../api/races\";\nimport { getExperimentalServerUrl } from \"../utils/getServerUrl\";\nimport { Overlay } from \"./Overlay\";\nimport ModalCloseButton from \"./buttons/ModalCloseButton\";\n\nexport const BattleMatcher: React.FC = () => {\n  const [isOpen, setIsOpen] = useState(false);\n  const openModal = () => setIsOpen(true);\n  const closeModal = () => setIsOpen(false);\n  return (\n    <div className=\"flex items-center font-semibold text-sm ml-2\">\n      <button\n        className=\"ml-2 flex items-center text-off-white h-5 px-1\"\n        onClick={openModal}\n      >\n        <OnlineIcon />\n        <div className=\"flex h-full items-end\">\n          <div\n            className=\"bg-green-300 rounded-full\"\n            style={{ width: \"5px\", height: \"5px\" }}\n          />\n        </div>\n      </button>\n      {isOpen && <BattleMatcherModal closeModal={closeModal} />}\n    </div>\n  );\n};\n\ninterface BatteListItemProps {\n  race: any;\n  closeModal: () => void;\n}\n\nconst BatteListItem: React.FC<BatteListItemProps> = ({ race, closeModal }) => {\n  const { ownerName, memberCount } = race;\n  const game = useGameStore((state) => state.game);\n  const myRaceID = useGameStore((state) => state.id);\n  const isMyRace = myRaceID === race.id;\n  const joinRace = () => {\n    // TODO: if no game -> we should forward to page with ?id=race.id\n    closeModal();\n    game?.join(race.id);\n  };\n  return (\n    <button\n      onClick={joinRace}\n      disabled={isMyRace}\n      className={`flex w-full items-center justify-between gap-2 text-base rounded-lg bg-gray-300 \n      ${\n        isMyRace\n          ? \"bg-gray-400 hover:cursor-not-allowed\"\n          : \"hover:cursor-pointer hover:bg-gray-400\"\n      } p-2 px-4 my-1`}\n    >\n      <div className=\"flex\">\n        <div className=\"flex items-center bg-purple-300 px-2 rounded mr-2\">\n          <div className=\"h-3\">\n            <UserGroupIcon />\n          </div>\n          <span className=\"mx-2\">{memberCount}</span>\n        </div>\n        <span>{ownerName}</span>\n      </div>\n      {!isMyRace ? (\n        <svg className=\"fill-current h-4\" viewBox=\"0 0 512 512\">\n          <path d=\"M498.1 5.6c10.1 7 15.4 19.1 13.5 31.2l-64 416c-1.5 9.7-7.4 18.2-16 23s-18.9 5.4-28 1.6L284 427.7l-68.5 74.1c-8.9 9.7-22.9 12.9-35.2 8.1S160 493.2 160 480V396.4c0-4 1.5-7.8 4.2-10.7L331.8 202.8c5.8-6.3 5.6-16-.4-22s-15.7-6.4-22-.7L106 360.8 17.7 316.6C7.1 311.3 .3 300.7 0 288.9s5.9-22.8 16.1-28.7l448-256c10.7-6.1 23.9-5.5 34 1.4z\" />\n        </svg>\n      ) : null}\n    </button>\n  );\n};\n\ninterface BattleMatcherModalProps {\n  closeModal: () => void;\n}\n\nexport const PlayingNow = () => {\n  const { data } = useSWR(ONLINE_COUNT_API, (...args) =>\n    fetch(...args).then((res) => res.json())\n  );\n  const isPublic = useSettingsStore((s) => s.raceIsPublic);\n  const isOpen = useSettingsStore((s) => s.publicRacesModalIsOpen);\n  const isOwner = useIsOwner();\n  return (\n    <>\n      <button\n        className=\"font-semibold text-xs tracking-wide\"\n        onClick={openPublicRacesModal}\n      >\n        <div className=\"flex items-center px-1 rounded uppercase\">\n          <div className=\"flex text-dark-ocean items-center bg-gray-300 px-2 rounded\">\n            <div\n              className={`mr-1 h-2 w-2 rounded-full \n      ${isPublic ? \"bg-green-400\" : \"bg-gray-400\"}`}\n            />\n            <div className=\"h-2 mr-1\">\n              <UserGroupIcon />\n            </div>\n            {data?.online ?? 0}\n          </div>\n        </div>\n      </button>\n      {isOpen && (\n        <Overlay onOverlayClick={closeModals}>\n          <Modal>\n            <div className=\"flex items-center\">\n              <h2 className=\"text-xl mr-2\">Public races</h2>\n              <button\n                className=\"cursor-default w-4 mr-1\"\n                title=\"You can configure your races to be public by default in your settings\"\n              >\n                <InfoIcon />\n              </button>\n              <ModalCloseButton onButtonClickHandler={closeModals} />\n            </div>\n            <ToggleSelector\n              title=\"public race\"\n              description=\"Enable to let other players find and join your race\"\n              checked={isPublic}\n              disabled={!isOwner}\n              toggleEnabled={toggleRaceIsPublic}\n            />\n            <BattleMatcherContainer closeModal={closeModals} />\n          </Modal>\n        </Overlay>\n      )}\n    </>\n  );\n};\n\nconst BattleMatcherModal = ({ closeModal }: BattleMatcherModalProps) => {\n  return (\n    <Overlay onOverlayClick={closeModal}>\n      <BattleMatcherContainer closeModal={closeModal} />\n    </Overlay>\n  );\n};\n\nexport const BattleMatcherContainer = ({\n  closeModal,\n}: {\n  closeModal: () => void;\n}) => {\n  const baseUrl = getExperimentalServerUrl();\n  const { data } = useSWR(\n    `${baseUrl}/api/races`,\n    (...args) => fetch(...args).then((res) => res.json()),\n    { refreshInterval: 10000 }\n  );\n\n  const availableRaces = data;\n  return (\n    <div className=\"\">\n      {availableRaces && availableRaces.length > 0 && (\n        <div className=\"\">\n          {availableRaces.map((race: any, i: number) => (\n            <BatteListItem key={i} race={race} closeModal={closeModal} />\n          ))}\n        </div>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/common/components/Button.tsx",
    "content": "import React, { ButtonHTMLAttributes } from \"react\";\n\ntype ButtonColor = \"primary\" | \"secondary\" | \"invisible\";\n\ninterface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  color: ButtonColor;\n  leftIcon?: React.ReactElement;\n  rightIcon?: React.ReactElement;\n  text?: string;\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n\nconst Button = ({\n  color,\n  disabled,\n  onClick,\n  leftIcon,\n  rightIcon,\n  text,\n  title,\n  size = \"md\",\n}: ButtonProps) => {\n  const colorStyles = getColorStyles(color);\n  const disabledStyle = disabled\n    ? \"cursor-not-allowed opacity-80\"\n    : \"cursor-pointer\";\n\n  const buttonSize = () => {\n    switch (size) {\n      case \"lg\":\n        return \"text-xl px-12 py-2\";\n      case \"md\":\n        return \"text-base py-2 px-4\";\n      case \"sm\":\n        return \"text-base py-1 px-2\";\n    }\n  };\n\n  return (\n    <button\n      type=\"button\"\n      title={title}\n      style={{ transition: \"all .15s ease\" }}\n      onClick={onClick}\n      className={`flex items-center ${colorStyles} ${disabledStyle} ${buttonSize()}`}\n      disabled={disabled}\n      aria-expanded=\"true\"\n      aria-haspopup=\"true\"\n    >\n      <>\n        {leftIcon && leftIcon}\n        {text && <p className=\"pl-1\">{text}</p>}\n        {rightIcon && rightIcon}\n      </>\n    </button>\n  );\n};\n\nfunction getColorStyles(color: ButtonColor) {\n  if (color === \"invisible\") {\n    return \"off-white border-none\";\n  }\n\n  const sharedStyle =\n    \"flex items-center text-gray-900 border-gray-200 border rounded\";\n\n  const style =\n    color === \"primary\" ? `bg-off-white` : `bg-purple-400 hover:bg-purple-300`;\n\n  return `${sharedStyle} ${style}`;\n}\n\nexport default Button;\n"
  },
  {
    "path": "packages/webapp-next/common/components/Footer/YoutubeLink.tsx",
    "content": "import getConfig from \"next/config\";\nimport { useEffect, useState } from \"react\";\nimport { YoutubeLogo } from \"../../../assets/icons/YoutubeLogo\";\n\nexport const useHasClicked = (key: string, value: string): boolean => {\n  const [hasClicked, setHasClicked] = useState(true);\n  useEffect(() => {\n    if (typeof window !== \"undefined\") {\n      const storedValue = localStorage.getItem(key);\n      setHasClicked(storedValue === value);\n    }\n  }, [key, value]);\n  return hasClicked;\n};\n\nconst YOUTUBE_LINK_STORAGE_KEY = \"youtube-link\";\nexport const YoutubeLink = () => {\n  const [hasClickedNow, setHasClickedNow] = useState(false);\n  const youtubeLink = \"https://www.youtube.com/watch?v=pNsJS5F-2yg\";\n  const hasClickedPreviously = useHasClicked(\n    YOUTUBE_LINK_STORAGE_KEY,\n    youtubeLink\n  );\n\n  const color =\n    hasClickedPreviously || hasClickedNow ? \"text-faded-gray\" : \"text-red-500\";\n\n  return (\n    <a\n      href={youtubeLink}\n      className={`flex items-center ${color} hover:text-red-500`}\n      target=\"blank\"\n      onClick={() => {\n        localStorage.setItem(YOUTUBE_LINK_STORAGE_KEY, youtubeLink);\n        setHasClickedNow(true);\n      }}\n      onMouseDown={(event: any) => {\n        if (event.button === 1) {\n          localStorage.setItem(YOUTUBE_LINK_STORAGE_KEY, youtubeLink);\n          setHasClickedNow(true);\n        }\n      }}\n    >\n      <div className=\"h-6 w-6 flex items-center\">\n        <YoutubeLogo />\n      </div>\n      <span className=\"ml-1\">watch</span>\n    </a>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/common/components/Footer.tsx",
    "content": "import { faCode } from \"@fortawesome/free-solid-svg-icons\";\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { useEffect, useState } from \"react\";\nimport { DiscordLogo, GithubLogo, TwitchLogo } from \"../../assets/icons\";\nimport { getStargazersCount } from \"../github/stargazers\";\nimport { useIsPlaying } from \"../hooks/useIsPlaying\";\nimport { PlayingNow } from \"./BattleMatcher\";\nimport { YoutubeLink } from \"./Footer/YoutubeLink\";\n\nfunction useStargazersCount() {\n  const [count, setCount] = useState(0);\n  useEffect(() => {\n    getStargazersCount().then((stargazersCount) => {\n      setCount(stargazersCount);\n    });\n  }, []);\n  return count;\n}\n\nexport function KeybindInfo() {\n  return (\n    <div className=\"flex flex-grow items-center\">\n      <PlayingNow />\n    </div>\n  );\n}\n\nexport function Footer() {\n  const isPlaying = useIsPlaying();\n  const stargazersCount = useStargazersCount();\n  return (\n    <footer\n      className=\"h-10 tracking-tighter\"\n      style={{\n        fontFamily: \"Fira Code\",\n      }}\n    >\n      {!isPlaying && (\n        <div className=\"w-full bg-dark-ocean\">\n          <AnimatePresence>\n            <motion.div\n              className=\"flex items-center justify-center text-faded-gray\"\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: 0.5 }}\n            >\n              <div className=\"hidden sm:flex flex-grow items-center mb-2 text-xs\">\n                <h1 className=\"bg-dark-lake py-1 px-2 rounded font-bold text-faded-gray\">\n                  Tab\n                </h1>\n                <span className=\"mx-1 text-faded-gray\">Refresh challenge</span>\n                <h1 className=\"bg-dark-lake py-1 px-2 rounded font-bold ml-2 text-faded-gray\">\n                  Enter\n                </h1>\n\n                <span className=\"mx-1 text-faded-gray\">Start race</span>\n              </div>\n              <div className=\"flex gap-2 sm:gap-4\">\n                <a\n                  href=\"https://github.com/codicocodes/speedtyper.dev\"\n                  className=\"flex items-center text-white hover:text-off-white\"\n                  target=\"blank\"\n                >\n                  <GithubLogo />\n                  <span className=\"hidden sm:block ml-1\">\n                    {stargazersCount} stars\n                  </span>\n                  <span className=\"block sm:hidden ml-1\">star</span>\n                </a>\n                <a\n                  href=\"https://discord.gg/AMbnnN5eep\"\n                  className=\"flex items-center hover:text-blue-300\"\n                  target=\"blank\"\n                >\n                  <DiscordLogo />\n                  <span className=\"ml-1\">join</span>\n                </a>\n                <a\n                  href=\"https://twitch.tv/codico\"\n                  className=\"flex items-center hover:text-purple-400\"\n                  target=\"blank\"\n                >\n                  <TwitchLogo />\n                  <span className=\"ml-1\">live</span>\n                </a>\n                <YoutubeLink />\n                <a\n                  href=\"https://dotfyle.com\"\n                  className=\"flex items-center hover:text-emerald-500 gap-1\"\n                  target=\"blank\"\n                >\n                  <div className=\"h-5 w-5 flex items-center\">\n                    <FontAwesomeIcon icon={faCode} size=\"2x\" color=\"fill\" />\n                  </div>\n                  <span className=\"ml-1\">nvim</span>\n                </a>\n              </div>\n            </motion.div>\n          </AnimatePresence>\n        </div>\n      )}\n    </footer>\n  );\n}\n"
  },
  {
    "path": "packages/webapp-next/common/components/Layout.tsx",
    "content": "import { Footer } from \"./Footer\";\nimport { navbarFactory } from \"./NewNavbar\";\n\ninterface LayoutProps {\n  children: JSX.Element;\n}\n\ninterface ContainerProps {\n  children: JSX.Element;\n  centered: boolean;\n}\n\nexport function Container({ children, centered }: ContainerProps) {\n  return (\n    <div\n      className={`flex justify-center h-full w-full \n     ${centered ? \"items-center justify-content\" : \"\"} `}\n    >\n      <div className=\"flex flex-col max-w-5xl w-full h-full justify-center relative\">\n        {children}\n      </div>\n    </div>\n  );\n}\n\nexport function Layout({ children }: LayoutProps) {\n  const Navbar = navbarFactory();\n  return (\n    <>\n      <Container centered={false}>\n        <Navbar />\n      </Container>\n      <Container centered={true}>{children}</Container>\n      <Container centered={false}>\n        <>\n          <Footer />\n        </>\n      </Container>\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/webapp-next/common/components/NewNavbar.tsx",
    "content": "import { AnimatePresence, motion } from \"framer-motion\";\nimport Link from \"next/link\";\nimport { TerminalIcon } from \"../../assets/icons/TerminalIcon\";\nimport { Logo, WebsiteName } from \"../../components/Navbar\";\nimport { LeaderboardButton } from \"../../modules/play2/components/leaderboard/LeaderboardButton\";\nimport { useGameStore } from \"../../modules/play2/state/game-store\";\nimport { useIsPlaying } from \"../hooks/useIsPlaying\";\nimport { PlayingNow } from \"./BattleMatcher\";\nimport Button from \"./Button\";\nimport { NewGithubLoginModal } from \"./modals/GithubLoginModal\";\nimport { SettingsModal } from \"./modals/SettingsModal\";\n\nexport const navbarFactory = () => {\n  return NewNavbar;\n};\n\nconst HomeLink = () => {\n  return (\n    <Link href=\"/\">\n      <span className=\"flex items-center cursor-pointer trailing-widest leading-normal text-xl  pl-2 text-off-white hover:text-white mr-2 lg:mr-6\">\n        <div className=\"flex items-center mr-4 mb-1\">\n          <Logo />\n        </div>\n        <WebsiteName />\n      </span>\n    </Link>\n  );\n};\n\nconst ProfileSection = () => {\n  const isPlaying = useIsPlaying();\n  return (\n    <>\n      {!isPlaying && (\n        <AnimatePresence>\n          <motion.div\n            className=\"flex-grow flex items-center\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            transition={{ duration: 0.5 }}\n          >\n            <div className=\"text-sm flex-grow\"></div>\n            <NewGithubLoginModal />\n          </motion.div>\n        </AnimatePresence>\n      )}\n    </>\n  );\n};\n\nexport const NewNavbar = () => {\n  const isPlaying = useIsPlaying();\n  return (\n    <header\n      className=\"mt-2 h-10 tracking-tighter\"\n      style={{\n        fontFamily: \"Fira Code\",\n      }}\n    >\n      <div className=\"w-full\">\n        <div className=\"flex items-center items-start py-2\">\n          <HomeLink />\n          {!isPlaying && (\n            <div className=\"flex gap-2\">\n              <Link href=\"/\">\n                <Button\n                  size=\"sm\"\n                  color=\"invisible\"\n                  onClick={() => useGameStore.getState().game?.play()}\n                  leftIcon={<TerminalIcon />}\n                />\n              </Link>\n              <LeaderboardButton />\n              <SettingsModal />\n              <PlayingNow />\n            </div>\n          )}\n          <ProfileSection />\n        </div>\n      </div>\n    </header>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/common/components/Overlay.tsx",
    "content": "import { Keys, useKeyMap } from \"../../hooks/useKeyMap\";\n\ninterface OverlayProps {\n  onOverlayClick: () => void;\n  children: React.ReactNode;\n}\n\nexport const Overlay: React.FC<OverlayProps> = (props) => {\n  useKeyMap(true, Keys.Escape, props.onOverlayClick);\n  return (\n    <>\n      <div\n        className=\"justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none\"\n        onClick={props.onOverlayClick}\n      >\n        <div\n          className=\"relative w-auto my-6 mx-auto flex items-center justify-center\"\n          onClick={(e) => e.stopPropagation()}\n        >\n          {props.children}\n        </div>\n      </div>\n      <div className=\"opacity-25 fixed inset-0 z-40 bg-black\"></div>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/common/components/buttons/GithubLoginButton.tsx",
    "content": "import { GithubLogo } from \"../../../assets/icons\";\nimport Button from \"../Button\";\n\ninterface GithubLoginButtonProps {\n  showModal: () => void;\n}\n\nexport const GithubLoginButton = ({ showModal }: GithubLoginButtonProps) => {\n  return (\n    <div className=\"hidden sm:block\">\n      <Button\n        color=\"primary\"\n        size=\"sm\"\n        leftIcon={<GithubLogo />}\n        onClick={showModal}\n        title=\"Login\"\n        text=\"Sign up | Login\"\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/common/components/buttons/ModalCloseButton.tsx",
    "content": "interface ModalButtonCloseProps {\n  onButtonClickHandler: () => void;\n}\n\nexport default function ModalCloseButton({\n  onButtonClickHandler,\n}: ModalButtonCloseProps) {\n  return (\n    <button\n      className=\"ml-auto bg-transparent border-0 text-black float-right text-3xl leading-none font-semibold outline-none focus:outline-none\"\n      onClick={onButtonClickHandler}\n    >\n      <span className=\"bg-gray-300 rounded transition ease-in-out delay-100 hover:bg-gray-400 hover:text-gray-700 text-dark-ocean h-8 w-8 text-2xl block outline-none focus:outline-none\">\n        ×\n      </span>\n    </button>\n  );\n}\n"
  },
  {
    "path": "packages/webapp-next/common/components/modals/GithubLoginModal.tsx",
    "content": "import { useRouter } from \"next/router\";\nimport React from \"react\";\nimport {\n  closeModals,\n  openProfileModal,\n  useSettingsStore,\n} from \"../../../modules/play2/state/settings-store\";\nimport { useGithubAuthFactory } from \"../../api/auth\";\nimport { useUserStore } from \"../../state/user-store\";\nimport { getExperimentalServerUrl } from \"../../utils/getServerUrl\";\nimport { Avatar } from \"../Avatar\";\nimport { GithubLoginButton } from \"../buttons/GithubLoginButton\";\nimport { GithubLoginOverlay } from \"../overlays/GithubLoginOverlay\";\nimport { ProfileModal } from \"./ProfileModal\";\n\nexport const GithubLoginModal = () => {\n  const [modalIsVisible, setShowModal] = React.useState(false);\n  const closeModal = () => setShowModal(false);\n  const showModal = () => setShowModal(true);\n  return (\n    <>\n      <GithubLoginButton showModal={showModal} />\n      {modalIsVisible ? (\n        <>\n          <GithubLoginOverlay closeModal={closeModal} />\n        </>\n      ) : null}\n    </>\n  );\n};\n\nexport const NewGithubLoginModal = () => {\n  const user = useUserStore();\n  const profileModalIsOpen = useSettingsStore((s) => s.profileModalIsOpen);\n  return (\n    <>\n      <button onClick={openProfileModal}>\n        <Avatar avatarUrl={user.avatarUrl} username={user.username} />\n      </button>\n      {profileModalIsOpen ? (\n        <>\n          {user.isAnonymous ? (\n            <GithubLoginOverlay closeModal={closeModals} />\n          ) : (\n            <ProfileModal closeModal={closeModals} />\n          )}\n        </>\n      ) : null}\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/common/components/modals/GithubModal.tsx",
    "content": "import { ReactNode } from \"react\";\n\ninterface GithubModalProps {\n  children: ReactNode;\n}\n\nexport default function GithubModal({ children }: GithubModalProps) {\n  return (\n    <div className=\"max-w-[800px] border-0 lg:rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none\">\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/webapp-next/common/components/modals/Modal.tsx",
    "content": "import { ReactNode } from \"react\";\n\ninterface ModalProps {\n  children: ReactNode;\n}\n\nexport default function Modal({ children }: ModalProps) {\n  return (\n    <div className=\"max-h-screen\">\n      <div className=\"flex flex-col gap-4 bg-off-white rounded-lg text-dark-ocean p-4\">\n        {children}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/webapp-next/common/components/modals/ProfileModal.tsx",
    "content": "import Image from \"next/image\";\nimport { ReactNode } from \"react\";\nimport { closeModals } from \"../../../modules/play2/state/settings-store\";\nimport { logout } from \"../../api/auth\";\nimport { useUserStore } from \"../../state/user-store\";\nimport { Overlay } from \"../Overlay\";\n\nexport interface ProfileModalProps {\n  closeModal: () => void;\n}\n\nexport function ProfileModal({ closeModal }: ProfileModalProps) {\n  const user = useUserStore();\n  return (\n    <Overlay onOverlayClick={closeModal}>\n      <div className=\"text-dark-ocean bg-off-white p-5 rounded\">\n        <div className=\"flex items-center\">\n          <Image\n            className=\"rounded-full\"\n            width=\"50\"\n            height=\"50\"\n            src={user.avatarUrl}\n            alt=\"\"\n          />\n          <span className=\"ml-4 text-lg tracking-wider\">{user.username}</span>\n        </div>\n        <button\n          onClick={() => logout().then(() => closeModals())}\n          className=\"mt-4 p-2 rounded-full bg-dark-lake text-off-white text-lg font-bold tracking-wider w-full\"\n        >\n          logout\n        </button>\n      </div>\n    </Overlay>\n  );\n}\n\ninterface ProfileItemProps {\n  onClick?: () => void;\n  children: ReactNode;\n}\n\nexport function ProfileItem({ children, onClick }: ProfileItemProps) {\n  return (\n    <div\n      onClick={onClick}\n      className=\"hover:text-purple-700 w-full ml-4 flex items-center text-lg justify-start cursor-pointer border-0 rounded bg-100 outline-none focus:outline-none\"\n    >\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/webapp-next/common/components/modals/SettingsModal.tsx",
    "content": "import React from \"react\";\nimport { KogWheel } from \"../../../assets/icons/KogWheel\";\nimport {\n  closeModals,\n  openSettingsModal,\n  useSettingsStore,\n} from \"../../../modules/play2/state/settings-store\";\nimport Button from \"../Button\";\nimport { SettingsOverlay } from \"../overlays/SettingsOverlay\";\n\nexport const SettingsModal = () => {\n  const isOpen = useSettingsStore((s) => s.settingsModalIsOpen);\n  return (\n    <div>\n      <Button\n        size=\"sm\"\n        onClick={openSettingsModal}\n        color=\"invisible\"\n        leftIcon={\n          <div className=\"h-4 w-auto\">\n            <KogWheel />\n          </div>\n        }\n      />\n      {isOpen ? (\n        <>\n          <SettingsOverlay closeModal={closeModals} />\n        </>\n      ) : null}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/common/components/overlays/GithubLoginOverlay.tsx",
    "content": "import { Overlay } from \"../Overlay\";\nimport ModalCloseButton from \"../buttons/ModalCloseButton\";\nimport GithubModal from \"../modals/GithubModal\";\nimport { GithubLoginButton } from \"../buttons/GithubLoginButton\";\nimport { useRouter } from \"next/router\";\nimport { getExperimentalServerUrl } from \"../../utils/getServerUrl\";\nimport { useGithubAuthFactory } from \"../../api/auth\";\n\ninterface GithubLoginOverlayProps {\n  closeModal: () => void;\n}\n\nexport const GithubLoginOverlay: React.FC<GithubLoginOverlayProps> = ({\n  closeModal,\n}: GithubLoginOverlayProps) => {\n  const router = useRouter();\n  const serverUrl = getExperimentalServerUrl();\n  const initGithubAuth = useGithubAuthFactory(router, serverUrl + \"/api\");\n  return (\n    <>\n      <Overlay onOverlayClick={closeModal}>\n        <GithubModal>\n          <div className=\"flex justify-center items-center p-5 rounded-t\">\n            <ModalCloseButton onButtonClickHandler={closeModal} />\n          </div>\n          <h3 className=\"text-dark-ocean flex-grow text-5xl tracking-wider font-thin flex justify-center\">\n            Welcome\n          </h3>\n          <p className=\"text-dark-ocean text-lg flex-grow tracking-widest font-thin flex justify-center p-10\">\n            Signup to SpeedTyper with your Github account to save your results\n            and get on the toplist.\n          </p>\n          <div className=\"flex justify-center mb-6 flex-auto\">\n            <GithubLoginButton showModal={initGithubAuth} />\n          </div>\n        </GithubModal>\n      </Overlay>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/common/components/overlays/SettingsOverlay.tsx",
    "content": "import { InfoIcon } from \"../../../assets/icons/InfoIcon\";\nimport Modal from \"../modals/Modal\";\nimport { closeModals } from \"../../../modules/play2/state/settings-store\";\nimport ModalCloseButton from \"../buttons/ModalCloseButton\";\nimport {\n  CaretSelector,\n  ToggleSelector,\n} from \"../../../modules/play2/components/RaceSettings\";\nimport {\n  toggleDefaultRaceIsPublic,\n  toggleSyntaxHighlightning,\n  useSettingsStore,\n} from \"../../../modules/play2/state/settings-store\";\nimport { Overlay } from \"../Overlay\";\n\ninterface SettingsOverlayProps {\n  closeModal: () => void;\n}\n\nexport const SettingsOverlay: React.FC<SettingsOverlayProps> = ({\n  closeModal,\n}: SettingsOverlayProps) => {\n  const isSyntaxHighlightingEnabled = useSettingsStore(\n    (s) => s.syntaxHighlighting\n  );\n  const isPublicRaceByDefault = useSettingsStore((s) => s.defaultIsPublic);\n  return (\n    <>\n      <Overlay onOverlayClick={closeModal}>\n        <Modal>\n          <div className=\"flex items-center\">\n            <button\n              className=\"cursor-default w-4 h-auto mr-1\"\n              title=\"Personal settings are stored in your browser\"\n            >\n              <InfoIcon />\n            </button>\n            <h2 className=\"text-xl tracking-wider\">Personal Settings</h2>\n            <ModalCloseButton onButtonClickHandler={closeModals} />\n          </div>\n          <ToggleSelector\n            title=\"syntax highlighting\"\n            description=\"Enable to use syntax highlighting\"\n            checked={isSyntaxHighlightingEnabled}\n            toggleEnabled={toggleSyntaxHighlightning}\n          />\n          <ToggleSelector\n            title=\"Public races\"\n            description=\"Races you start will be public by default\"\n            checked={isPublicRaceByDefault}\n            toggleEnabled={toggleDefaultRaceIsPublic}\n          />\n          <CaretSelector />\n        </Modal>\n      </Overlay>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/common/github/stargazers.ts",
    "content": "const stargazersCountKey = \"stargazersCount\";\n\nconst rateLimitResetKey = \"stargazersCountRateLimitReset\";\n\nexport async function getStargazersCount() {\n  return shouldRefreshCache()\n    ? fetchStargazersCount()\n    : localStorage.getItem(stargazersCountKey);\n}\n\nasync function fetchStargazersCount() {\n  return fetch(\"https://api.github.com/repos/codicocodes/speedtyper.dev\").then(\n    async (res) => {\n      const rateLimitResetSeconds = res.headers.get(\n        \"x-ratelimit-reset\"\n      ) as string;\n      const resetDate = new Date(parseInt(rateLimitResetSeconds) * 1000);\n      const repoData = await res.json();\n      const stargazersCount = repoData.stargazers_count;\n      localStorage.setItem(stargazersCountKey, stargazersCount);\n      localStorage.setItem(rateLimitResetKey, resetDate.toISOString());\n      return stargazersCount;\n    }\n  );\n}\n\nfunction shouldRefreshCache() {\n  const rateLimitReset = localStorage.getItem(rateLimitResetKey);\n  if (!rateLimitReset) {\n    return true;\n  }\n  return new Date() > new Date(rateLimitReset);\n}\n"
  },
  {
    "path": "packages/webapp-next/common/hooks/useIsPlaying.ts",
    "content": "import { useCodeStore } from \"../../modules/play2/state/code-store\";\n\nexport const useIsPlaying = () => {\n  useCodeStore((state) => state.startTime);\n  useCodeStore((state) => state.endTime);\n  const isPlaying = useCodeStore((state) => state.isPlaying)();\n  return isPlaying;\n};\n"
  },
  {
    "path": "packages/webapp-next/common/hooks/useSocket.ts",
    "content": "import { useEffect } from \"react\";\nimport { useConnectionStore } from \"../../modules/play2/state/connection-store\";\nimport SocketLatest from \"../services/Socket\";\nimport { getExperimentalServerUrl } from \"../utils/getServerUrl\";\n\nexport function useSocket() {\n  useEffect(() => {\n    const serverUrl = getExperimentalServerUrl();\n    const socket = new SocketLatest(serverUrl);\n    useConnectionStore.setState((s) => ({ ...s, socket }));\n    return () => {\n      socket.disconnect();\n    };\n  }, []);\n}\n"
  },
  {
    "path": "packages/webapp-next/common/services/Socket.ts",
    "content": "import { connect, Socket as SocketIOSocket } from \"socketio-latest\";\n\nexport default class SocketLatest {\n  socket: SocketIOSocket;\n\n  constructor(serverUrl: string) {\n    this.socket = connect(serverUrl, {\n      withCredentials: true,\n      reconnection: false,\n    });\n    this.socket.on(\"disconnect\", (err) => {\n      console.log(\"disconnected from server\", err);\n    });\n    this.socket.on(\"connect\", () => {\n      console.log(\"connected!!!!\");\n    });\n  }\n\n  disconnect() {\n    if (this.socket) this.socket.disconnect();\n  }\n\n  subscribe(event: string, cb: (error: string | null, msg: any) => void) {\n    if (!this.socket) return true;\n    this.socket.on(event, (msg: any) => {\n      return cb(null, msg);\n    });\n  }\n\n  emit(event: string, data?: any) {\n    if (this.socket) this.socket.emit(event, data);\n  }\n}\n"
  },
  {
    "path": "packages/webapp-next/common/state/user-store.ts",
    "content": "import { useEffect } from \"react\";\nimport create from \"zustand\";\nimport { fetchUser } from \"../api/user\";\n\nexport interface User {\n  id: string;\n  username: string;\n  avatarUrl: string;\n  isAnonymous: boolean;\n}\n\nexport const useUserStore = create<User>((_set, _get) => ({\n  id: \"\",\n  username: \"\",\n  avatarUrl: \"\",\n  isAnonymous: true,\n}));\n\nexport const useInitializeUserStore = (user: User | null) => {\n  useEffect(() => {\n    if (user) {\n      useUserStore.setState((userStore) => ({\n        ...userStore,\n        ...user,\n      }));\n    }\n  }, [user]);\n};\n\nexport const updateUserInStore = async () => {\n  const user = await fetchUser();\n  useUserStore.setState((userStore) => ({\n    ...userStore,\n    ...user,\n  }));\n};\n"
  },
  {
    "path": "packages/webapp-next/common/utils/clipboard.ts",
    "content": "import { toast } from \"react-toastify\";\n\nexport function copyToClipboard(url: string, message: string) {\n  navigator.clipboard.writeText(url);\n  toast.dark(message);\n}\n"
  },
  {
    "path": "packages/webapp-next/common/utils/cpmToWPM.ts",
    "content": "export function cpmToWPM(cpm: number) {\n  return Math.floor(cpm / 5);\n}\n"
  },
  {
    "path": "packages/webapp-next/common/utils/getServerUrl.ts",
    "content": "import { publicRuntimeConfig } from \"../../next.config\";\n\nexport function getSiteRoot() {\n  return publicRuntimeConfig?.siteRoot;\n}\n\nexport function getServerUrl() {\n  return publicRuntimeConfig?.serverUrl;\n}\n\nexport function getExperimentalServerUrl() {\n  return publicRuntimeConfig?.experimentalServerUrl;\n}\n"
  },
  {
    "path": "packages/webapp-next/common/utils/router.ts",
    "content": "import Router from \"next/router\";\n\nexport const addIDtoQueryParams = (id: string) => {\n  Router.push(\n    {\n      pathname: \"/play2\",\n      query: { id },\n    },\n    undefined,\n    { shallow: true }\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/common/utils/toHumanReadableTime.ts",
    "content": "const MINUTES_IN_SECONDS = 60;\n\nconst HOURS_IN_SECONDS = MINUTES_IN_SECONDS * 60;\n\nconst DAYS_IN_SECONDS = HOURS_IN_SECONDS * 24;\n\nexport const toHumanReadableTime = (secs: number) => {\n  const days = Math.floor(secs / DAYS_IN_SECONDS);\n  const hours = Math.floor((secs - days * DAYS_IN_SECONDS) / HOURS_IN_SECONDS);\n\n  const minutes =\n    Math.floor(\n      (secs - days * DAYS_IN_SECONDS - hours * HOURS_IN_SECONDS) / 60\n    ) % MINUTES_IN_SECONDS;\n\n  const seconds =\n    secs -\n    days * DAYS_IN_SECONDS -\n    hours * HOURS_IN_SECONDS -\n    minutes * MINUTES_IN_SECONDS;\n\n  let result = \"\";\n  result = days ? result.concat(`${days} days `) : result;\n  result = hours ? result.concat(`${hours}h `) : result;\n  if (!days) result = minutes ? result.concat(`${minutes}m `) : result;\n  if (!hours) result = result.concat(`${seconds}s`);\n  return result.trim();\n};\n"
  },
  {
    "path": "packages/webapp-next/components/Countdown.tsx",
    "content": "import React from \"react\";\n\nconst Countdown = ({ countdown }: { countdown: number }) => {\n  const renderedString = countdown === 0 ? \"START TYPING\" : countdown;\n  return countdown === 0 ? null : (\n    <div className=\"absolute py-4 px-8 z-10 left-1/2 transform -translate-x-1/2 bg-dark-ocean shadow-xl rounded-lg\">\n      <div className=\"flex flex-grow items-center justify-center\">\n        <div className=\"text-5xl text-off-white tracking-wider\">\n          {renderedString}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default Countdown;\n"
  },
  {
    "path": "packages/webapp-next/components/Navbar.tsx",
    "content": "import Image from \"next/image\";\nimport { useIsPlaying } from \"../common/hooks/useIsPlaying\";\n\nexport const WebsiteName = () => {\n  const isPlaying = useIsPlaying();\n  const websiteName = \"speedtyper.dev\";\n  const colorClass = isPlaying ? \"text-faded-gray\" : \"text-inherit\";\n  return (\n    <h2\n      className={`hidden sm:block whitespace-no-wrap font-bold ${colorClass}`}\n    >\n      {websiteName}\n    </h2>\n  );\n};\n\nexport const Logo = () => {\n  const isPlaying = useIsPlaying();\n  return isPlaying ? (\n    <Image\n      width=\"45px\"\n      height=\"25px\"\n      src=\"/faded-logo.png\"\n      quality={100}\n      alt=\"speedtyper logo\"\n    />\n  ) : (\n    <Image\n      width=\"45px\"\n      height=\"25px\"\n      src=\"/logo.png\"\n      quality={100}\n      alt=\"speedtyper logo\"\n    />\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/components/Stream.tsx",
    "content": "import { faChevronUp, faCircle, faX } from \"@fortawesome/free-solid-svg-icons\";\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\nimport { useMemo, useState } from \"react\";\nimport { useIsPlaying } from \"../common/hooks/useIsPlaying\";\n\nexport const Stream = () => {\n  const isPlaying = useIsPlaying();\n  const isLive = false;\n  const [isHidden, setIsHidden] = useState(false);\n  useMemo(() => {\n    if (isLive && typeof document !== \"undefined\") {\n      try {\n        const script = document.createElement(\"script\");\n        script.setAttribute(\"src\", \"https://embed.twitch.tv/embed/v1.js\");\n        script.addEventListener(\"load\", () => {\n          // @ts-ignore\n          new window.Twitch.Embed(\"twitch-embed\", {\n            width: \"340\",\n            height: \"200\",\n            layout: \"video\",\n            muted: true,\n            channel: \"codico\",\n          });\n        });\n        document.body.appendChild(script);\n      } catch (error) {\n        console.log(error);\n      }\n    }\n  }, [isLive]);\n  return (\n    <>\n      <div\n        className={`${\n          isPlaying ? \"hidden\" : \"\"\n        } bottom-0 right-0 mr-8 mb-8 absolute`}\n      >\n        <div className={`${!isLive || isPlaying || isHidden ? \"hidden\" : \"\"}`}>\n          <div className=\"flex text-red-400 items-center gap-2 font-semibold justify-between\">\n            <div className=\"flex items-center gap-2 m-2\">\n              <div className=\"h-3 w-3 flex items-center\">\n                <FontAwesomeIcon icon={faCircle} />\n              </div>\n              live now\n            </div>\n            <button\n              onClick={() => setIsHidden(true)}\n              className=\"flex gap-2 items-center text-gray-400 hover:text-gray-200 hover:cursor-pointer font-normal\"\n            >\n              hide\n              <div className=\"h-2 w-2 flex items-center\">\n                <FontAwesomeIcon icon={faX} />\n              </div>\n            </button>\n          </div>\n          <div id=\"twitch-embed\" />\n        </div>\n        {isHidden && (\n          <button\n            className=\"flex gap-2 items-center text-gray-400 hover:text-gray-200 hover:cursor-pointer font-normal\"\n            onClick={() => setIsHidden(false)}\n          >\n            <div className=\"h-2 w-2 flex items-center\">\n              <FontAwesomeIcon icon={faChevronUp} />\n            </div>\n            show stream\n          </button>\n        )}\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/hooks/useKeyMap.ts",
    "content": "import { useEffect, useState } from \"react\";\n\nexport enum Keys {\n  Tab = \"Tab\",\n  Enter = \"Enter\",\n  Escape = \"Escape\",\n}\n\nexport const triggerKeys = \"abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*() \";\n\nexport const useKeyMap = (\n  isActive: boolean,\n  selectedKeys: string,\n  callback: () => void\n) => {\n  const [capsLockActive, setCapsLockActive] = useState(false);\n\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      const { key: pressedKey } = e;\n      if (pressedKey === \"CapsLock\") {\n        setCapsLockActive(!capsLockActive);\n      }\n      if (\n        Object.values(Keys)\n          .map((en) => en.toString())\n          .includes(selectedKeys)\n      ) {\n        if (pressedKey !== selectedKeys) return;\n        e.preventDefault();\n        callback();\n        return;\n      }\n      if (!selectedKeys.includes(pressedKey)) return;\n      e.preventDefault();\n      callback();\n    };\n\n    const handleCapsLock = (e: KeyboardEvent) => {\n      setCapsLockActive(e.getModifierState(\"CapsLock\"));\n    };\n\n    if (window && document) {\n      if (isActive) {\n        document.addEventListener(\"keydown\", handleKeyDown);\n        document.addEventListener(\"keydown\", handleCapsLock);\n        return () => {\n          document.removeEventListener(\"keydown\", handleKeyDown);\n          document.removeEventListener(\"keydown\", handleCapsLock);\n        };\n      } else {\n        document.removeEventListener(\"keydown\", handleKeyDown);\n        document.removeEventListener(\"keydown\", handleCapsLock);\n      }\n    }\n  }, [isActive, callback, selectedKeys, capsLockActive]);\n\n  return { capsLockActive };\n};\n"
  },
  {
    "path": "packages/webapp-next/hooks/useTotalSeconds.ts",
    "content": "import { useEffect, useState } from \"react\";\n\nexport default (startTime?: number, endTime?: number): number => {\n  const [totalSeconds, setTotalSeconds] = useState(0);\n\n  let currTimeout: NodeJS.Timeout;\n\n  useEffect(() => {\n    if (startTime && !endTime) {\n      currTimeout = setTimeout(() => {\n        setTotalSeconds((totalSeconds) => {\n          return totalSeconds + 1;\n        });\n      }, 1000);\n    } else if (!endTime) {\n      setTotalSeconds(0);\n    }\n\n    return () => clearTimeout(currTimeout);\n  }, [totalSeconds, startTime, endTime]);\n\n  return totalSeconds;\n};\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/CodeArea.tsx",
    "content": "import { ReactNode } from \"react\";\nimport Countdown from \"../../../components/Countdown\";\nimport { useGameStore } from \"../state/game-store\";\n\ninterface CodeAreaProps {\n  filePath: string;\n  focused: boolean;\n  children: ReactNode;\n  staticHeigh: boolean;\n}\n\nexport function CodeArea({\n  filePath,\n  focused,\n  children,\n  staticHeigh = true,\n}: CodeAreaProps) {\n  const countDown = useGameStore((state) => state.countdown);\n  return (\n    <div\n      className={`${\n        staticHeigh ? \"h-[250px] sm:h-[420px]\" : \"\"\n      } bg-dark-lake text-faded-gray flex-shrink tracking-tight sm:tracking-wider rounded-xl p-4 text-xs sm:text-2xl w-full`}\n      style={{\n        fontFamily: \"Fira Code\",\n        fontWeight: \"normal\",\n      }}\n    >\n      {!focused && (\n        <div className=\"absolute flex justify-center items-center w-full h-full\">\n          Click or press any key to focus\n        </div>\n      )}\n      {countDown && (\n        <div className=\"absolute flex justify-center items-center w-full h-full\">\n          <Countdown countdown={countDown} />\n        </div>\n      )}\n\n      <CodeAreaHeader filePath={filePath} />\n      <pre className={focused ? \"blur-none opacity-100\" : \"blur-sm opacity-40\"}>\n        <code>{children}</code>\n      </pre>\n    </div>\n  );\n}\n\nfunction CodeAreaHeader({ filePath }: { filePath: string }) {\n  return (\n    <div className=\"flex items-center flex-row mb-4 w-full\">\n      <div className=\"flex flex-row gap-2 mr-2 relative\">\n        <div className=\"w-2.5 h-2.5 bg-slate-600 rounded-full\" />\n        <div className=\"w-2.5 h-2.5 bg-slate-600 rounded-full\" />\n        <div className=\"w-2.5 h-2.5 bg-slate-600 rounded-full\" />\n      </div>\n      <div className=\"flex items-start justify-center flex-row w-full h-6 pr-12\">\n        <span className=\"hidden sm:block italic text-base opacity-80 truncate\">\n          {filePath}\n        </span>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/HiddenCodeInput.tsx",
    "content": "import {\n  ChangeEvent,\n  ClipboardEvent,\n  KeyboardEvent,\n  MouseEvent,\n  useEffect,\n  useState,\n} from \"react\";\n\nimport { isSkippable, useCodeStore } from \"../state/code-store\";\nimport { useCanType, useGameStore } from \"../state/game-store\";\n\ninterface HiddenCodeInputProps {\n  hide: boolean; // Used for debugging the input\n  disabled: boolean;\n  inputRef: (node: HTMLTextAreaElement) => void;\n}\n\nconst useAutoTyper = (\n  handleOnChange: (e: ChangeEvent<HTMLTextAreaElement>) => void\n) => {\n  const isAutoTyperEnabled = false;\n  const code = useCodeStore.getState().code;\n  useEffect(() => {\n    if (code && isAutoTyperEnabled) {\n      const current = useCodeStore.getState().currentChar();\n      const untyped = useCodeStore\n        .getState()\n        .untypedChars()\n        .split(\"\\n\")\n        .map((st) => st.trimStart())\n        .join(\"\\n\");\n      const value = current + untyped;\n      handleOnChange({\n        target: {\n          value,\n        },\n      } as unknown as ChangeEvent<HTMLTextAreaElement>);\n    }\n  }, [isAutoTyperEnabled, code, handleOnChange]);\n};\n\nexport const HiddenCodeInput = ({\n  disabled,\n  hide,\n  inputRef,\n}: HiddenCodeInputProps) => {\n  const game = useGameStore((s) => s.game);\n  const handleBackspace = useCodeStore((state) => state.handleBackspace);\n  const handleKeyPress = useCodeStore((state) => state.handleKeyPress);\n  const keyPressFactory = useCodeStore((state) => state.keyPressFactory);\n  useAutoTyper(handleOnChange);\n  const canType = useCanType();\n\n  // TODO: remove input and setInput\n  // instead introduc getTypedInput method in the store\n  // which gets code.substr(0, correctIndex)\n  const [input, setInput] = useState(\"\");\n\n  function handleOnChange(e: ChangeEvent<HTMLTextAreaElement>) {\n    // TODO: use e.isTrusted\n    if (!canType) return;\n    if (!game) return;\n    const backspaces = input.length - e.target.value.length;\n    // send backspaces\n    if (backspaces > 0) {\n      for (let i = 1; i <= backspaces; i++) {\n        handleBackspace();\n      }\n    } else {\n      // send regular characters\n      const typed = e.target.value.substring(input.length);\n      for (const char of typed) {\n        if (isSkippable(char)) continue;\n        const keyPress = keyPressFactory(char);\n        handleKeyPress(keyPress);\n        game.sendKeyStroke(keyPress);\n      }\n    }\n    setInput(e.target.value);\n  }\n\n  return (\n    <textarea\n      className=\"text-black\"\n      ref={inputRef}\n      value={input}\n      autoFocus\n      disabled={disabled}\n      onChange={handleOnChange}\n      onKeyDown={preventArrowKeys}\n      onClick={preventClick}\n      onPaste={preventPaste}\n      style={{\n        ...(hide\n          ? {\n              position: \"absolute\",\n              left: \"-10000000px\",\n            }\n          : {}),\n      }}\n    />\n  );\n};\n\nfunction preventClick(e: MouseEvent<HTMLTextAreaElement>) {\n  e.preventDefault();\n}\n\nfunction preventPaste(e: ClipboardEvent<HTMLTextAreaElement>) {\n  e.preventDefault();\n}\n\nfunction preventArrowKeys(e: KeyboardEvent<HTMLTextAreaElement>) {\n  switch (e.key) {\n    case ArrowKey.Up:\n    case ArrowKey.Down:\n    case ArrowKey.Left:\n    case ArrowKey.Right:\n      e.preventDefault();\n  }\n}\n\nenum ArrowKey {\n  Up = \"ArrowUp\",\n  Down = \"ArrowDown\",\n  Left = \"ArrowLeft\",\n  Right = \"ArrowRight\",\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/IncorrectChars.tsx",
    "content": "import { useCodeStore } from \"../state/code-store\";\n\nfunction isOnlySpace(str: string) {\n  return str.trim().length === 0;\n}\n\nexport function IncorrectChars() {\n  const incorrectChars = useCodeStore((state) => state.incorrectChars);\n  const charGroups = parseIncorrectCharGroups(incorrectChars());\n  return (\n    <>\n      {charGroups.map((chars, index) => {\n        const bgColor =\n          isOnlySpace(chars) && chars.length > 1 ? \"\" : \"bg-red-500\";\n        return (\n          <span key={index} className={`${bgColor} pb-1 pt-2`}>\n            {chars}\n          </span>\n        );\n      })}\n    </>\n  );\n}\n\n// TODO: figure out where these constants should go and if we can reuse them in other places\nconst LineBreak = \"\\n\";\nconst LineBreakChar = \"↵\";\nconst LineBreakWithChar = `${LineBreakChar}${LineBreak}`;\n\nfunction parseIncorrectCharGroups(incorrectChars: string) {\n  const incorrectLines = incorrectChars\n    .replaceAll(LineBreak, LineBreakWithChar)\n    .split(\"\\n\")\n    .filter(Boolean);\n\n  const charGroups = incorrectLines\n    .map((line) => {\n      const subline = line.split(/(\\s+)/);\n      return subline.map((chars) => {\n        return chars.replaceAll(LineBreakChar, LineBreakWithChar);\n      });\n    })\n    .flat();\n  return charGroups;\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/NextChar.tsx",
    "content": "import { AnimatePresence, motion } from \"framer-motion\";\nimport { useNodeRect } from \"../hooks/useNodeRect\";\nimport { useCodeStore } from \"../state/code-store\";\nimport { useSettingsStore } from \"../state/settings-store\";\nimport {\n  useBlinkingCursorAnimation,\n  OFF_WHITE_COLOR as GRAY_COLOR,\n  SmoothCaret,\n} from \"./SmoothCaret\";\n\ninterface NextCharProps {\n  focused: boolean;\n}\n\nexport function NextChar({ focused }: NextCharProps) {\n  const useSmoothCaret = useSettingsStore((state) => state.smoothCaret);\n  const index = useCodeStore((state) => state.index);\n  const [{ top, left }, nextCharRef] = useNodeRect<HTMLSpanElement>(\n    index.toString()\n  );\n  const getNextChar = useCodeStore((state) => state.currentChar);\n  const nextChar = getNextChar().replace(/\\n/g, \"↵\\n\");\n  const runBlinkingCursorAnimation = !useSmoothCaret;\n  const controls = useBlinkingCursorAnimation(\n    GRAY_COLOR,\n    runBlinkingCursorAnimation\n  );\n\n  return (\n    <>\n      {focused && useSmoothCaret && typeof window !== \"undefined\" && (\n        <SmoothCaret top={top} left={left} />\n      )}\n      <AnimatePresence>\n        <motion.span\n          ref={nextCharRef}\n          animate={controls}\n          className=\"rounded-sm pb-1 pt-2\"\n          transition={{\n            duration: 1,\n            repeat: Infinity,\n          }}\n        >\n          {nextChar}\n        </motion.span>\n      </AnimatePresence>\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/RaceSettings.tsx",
    "content": "import { RadioGroup, Switch } from \"@headlessui/react\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport React, { Fragment } from \"react\";\nimport { OnlineIcon } from \"../../../assets/icons/OnlineIcon\";\nimport { Overlay } from \"../../../common/components/Overlay\";\nimport { useIsOwner } from \"../state/game-store\";\nimport {\n  closeModals,\n  openLanguageModal,\n  setCaretType,\n  useSettingsStore,\n} from \"../state/settings-store\";\nimport { ActionButton } from \"./play-footer/PlayFooter\";\nimport { LanguageSelector } from \"./race-settings/LanguageSelector\";\n\nexport const RaceSettings: React.FC = () => {\n  const isOpen = useSettingsStore((s) => s.languageModalIsOpen);\n  const languageSelected = useSettingsStore((s) => s.languageSelected);\n  const language = languageSelected ? languageSelected.name : \"language\";\n\n  return (\n    <>\n      <ActionButton\n        className=\"flex items-center w-auto min-w-8\"\n        onClick={openLanguageModal}\n        color=\"invisible\"\n        icon={\n          <div className=\"h-3\">\n            <OnlineIcon />\n          </div>\n        }\n        text={language}\n      />\n      {isOpen && <RaceSettingsModal />}\n    </>\n  );\n};\n\nexport const RaceSettingsModal: React.FC = () => {\n  const isOwner = useIsOwner();\n  return (\n    <Overlay onOverlayClick={closeModals}>\n      <AnimatePresence>\n        <motion.div\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          exit={{ opacity: 0 }}\n          transition={{ duration: 0.2 }}\n          className=\"flex flex-col w-full text-dark-ocean p-5 rounded gap-4 w-full\"\n          style={{ fontFamily: \"Fira Code\" }}\n        >\n          <div className=\"flex flex-col gap-4 rounded-lg p-4 min-w-8\">\n            {isOwner && <LanguageSelector />}\n          </div>\n        </motion.div>\n      </AnimatePresence>\n    </Overlay>\n  );\n};\n\nexport const CaretSelector = () => {\n  const isSmoothCaret = useSettingsStore((s) => s.smoothCaret);\n  const selectedCaretType = isSmoothCaret ? \"smooth\" : \"block\";\n  return (\n    <RadioGroup value={selectedCaretType} onChange={setCaretType}>\n      <RadioGroup.Label className=\"text-xs font-semibold uppercase tracking-widest\">\n        Caret style\n      </RadioGroup.Label>\n      <div className=\"flex w-full font-bold tracking-widest gap-1\">\n        <RadioGroup.Option className=\"w-full cursor-pointer\" value=\"smooth\">\n          {({ checked }) => (\n            <div\n              className={`flex items-center h-full w-full p-3 rounded-lg ${\n                checked\n                  ? \"bg-purple-200 hover:bg-purple-300\"\n                  : \"bg-gray-200 hover:bg-gray-300\"\n              }`}\n            >\n              <span\n                className=\"flex flex-col h-full rounded-lg bg-gray-600 mr-4\"\n                style={{\n                  width: \"2px\",\n                }}\n              />\n              <span>Line (smooth)</span>\n            </div>\n          )}\n        </RadioGroup.Option>\n        <RadioGroup.Option className=\"w-full cursor-pointer\" value=\"block\">\n          {({ checked }) => (\n            <div\n              className={`flex items-center h-full w-full p-3 rounded-lg ${\n                checked\n                  ? \"bg-purple-200 hover:bg-purple-300\"\n                  : \"bg-gray-200 hover:bg-gray-300\"\n              }`}\n            >\n              <span className=\"flex flex-col h-full rounded-sm bg-gray-600 mr-4 w-3\" />\n              <span>Block</span>\n            </div>\n          )}\n        </RadioGroup.Option>\n      </div>\n    </RadioGroup>\n  );\n};\n\ninterface ToggleSelectorProps {\n  title: string;\n  description: string;\n  checked: boolean;\n  toggleEnabled: () => void;\n  disabled?: boolean;\n}\n\nexport const ToggleSelector: React.FC<ToggleSelectorProps> = ({\n  checked,\n  disabled = false,\n  toggleEnabled,\n  title,\n  description,\n}) => {\n  return (\n    <>\n      <div className=\"rounded-lg border-gray-200\">\n        <div className=\"flex items-center\">\n          <Switch checked={checked} onChange={toggleEnabled} as={Fragment}>\n            {({ checked }) => (\n              /* Use the `checked` state to conditionally style the button. */\n              <button\n                disabled={disabled}\n                className={`${\n                  disabled\n                    ? \"bg-gray-500\"\n                    : checked\n                    ? \"bg-purple-300\"\n                    : \"bg-faded-gray\"\n                } relative inline-flex h-6 w-11 items-center rounded-full`}\n              >\n                <span className=\"sr-only\">Enable notifications</span>\n                <span\n                  className={`${\n                    checked ? \"translate-x-6\" : \"translate-x-1\"\n                  } inline-block h-4 w-4 transform rounded-full bg-white transition`}\n                />\n              </button>\n            )}\n          </Switch>\n          <div className=\"flex justify-between items-center w-full ml-4\">\n            <span className=\"text-sm font-semibold uppercase tracking-widest\">\n              {title}\n            </span>\n            <span className=\"w-auto tracking-wider font-thin text-sm ml-8\">\n              {description}\n            </span>\n          </div>\n        </div>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/ResultsChart.tsx",
    "content": "import React, { RefObject, useEffect, useMemo, useRef } from \"react\";\nimport {\n  Chart,\n  LineController,\n  CategoryScale,\n  LinearScale,\n  PointElement,\n  LineElement,\n  Tooltip,\n} from \"chart.js\";\nimport { useCodeStore } from \"../state/code-store\";\n\nChart.register(\n  LineController,\n  CategoryScale,\n  LinearScale,\n  PointElement,\n  LineElement,\n  Tooltip\n);\n\nconst renderChart = (\n  ref: RefObject<HTMLCanvasElement>,\n  wpmChartData: number[]\n) => {\n  const ctx = ref.current?.getContext(\"2d\");\n  if (!ctx) return;\n  const labels = [];\n  const data = [];\n  let seconds = 1;\n  for (const wpm of wpmChartData) {\n    labels.push(seconds);\n    data.push(wpm);\n    seconds++;\n  }\n  return new Chart(ctx, {\n    type: \"line\",\n    data: {\n      labels,\n      datasets: [\n        {\n          label: \"WPM each second\",\n          data,\n          backgroundColor: \"#7e22ce\",\n          borderColor: \"#7e22ce\",\n          tension: 0.4,\n          fill: true,\n        },\n      ],\n    },\n    options: {\n      responsive: true,\n      maintainAspectRatio: false,\n      scales: {\n        y: {\n          beginAtZero: true,\n        },\n      },\n      plugins: {\n        tooltip: {\n          enabled: true,\n          callbacks: {\n            title: (items) => {\n              const item = items[0];\n              const label = \"Second \" + item.label;\n              return label;\n            },\n          },\n        },\n      },\n    },\n  });\n};\n\nexport default function ResultsChart() {\n  const chartRef = useRef<HTMLCanvasElement>(null);\n  const getChartWPM = useCodeStore((state) => state.getChartWPM);\n  const chartWPMData = useMemo(() => getChartWPM(), [getChartWPM]);\n  useEffect(() => {\n    const chart = renderChart(chartRef, chartWPMData);\n    return () => {\n      chart?.destroy();\n    };\n  }, [chartWPMData]);\n\n  return (\n    <div className=\"flex rounded-xl flex-col bg-dark-lake grow m-2 max-w-screen\">\n      <div className=\"flex flex-row\">\n        <h1 className=\"text-sm p-4 font-semibold\">Words Per Minute</h1>\n      </div>\n      <div className=\"bg-dark-lake p-2 rounded-xl max-w-full h-full\">\n        <canvas ref={chartRef} />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/SmoothCaret.tsx",
    "content": "import { useEffect, useMemo } from \"react\";\nimport { AnimatePresence, motion, useAnimationControls } from \"framer-motion\";\nimport { useCodeStore } from \"../state/code-store\";\n\nconst useHasLoadedCode = () => {\n  const code = useCodeStore((state) => state.code);\n  return code.length > 0;\n};\n\nconst SMOOTH_CARET_ELEMENT_ID = \"smooth-caret-element\";\n\nexport const PRIMARY_PINK_COLOR = \"#d6bcfa\";\n\nexport const OFF_WHITE_COLOR = \"#374151\";\n\nexport const useBlinkingCursorAnimation = (\n  color: string,\n  runAnimation: boolean = true\n) => {\n  const controls = useAnimationControls();\n  const isPlaying = useCodeStore((state) => state.isPlaying)();\n  useEffect(() => {\n    if (!runAnimation) {\n      controls.set({\n        backgroundColor: [\"rgba(0,0,0,0)\"],\n      });\n      controls.stop();\n      return;\n    }\n    if (!isPlaying) {\n      controls.start({\n        backgroundColor: [\"rgba(0,0,0,0)\", color],\n      });\n    } else {\n      controls.set({\n        backgroundColor: [\"rgba(0,0,0,0)\", color],\n      });\n      controls.stop();\n    }\n  }, [runAnimation, color, controls, isPlaying]);\n  return controls;\n};\n\nexport const SmoothCaret = ({ top, left }: { top: number; left: number }) => {\n  const hasLoaded = useHasLoadedCode();\n  const animator = useAnimator();\n\n  useEffect(() => {\n    animator.animate({ left, top });\n  }, [animator, left, top]);\n\n  const controls = useBlinkingCursorAnimation(PRIMARY_PINK_COLOR);\n\n  return (\n    <AnimatePresence>\n      <motion.span\n        animate={controls}\n        transition={{\n          duration: 1,\n          repeat: Infinity,\n        }}\n        hidden={!hasLoaded}\n        id={`${SMOOTH_CARET_ELEMENT_ID}`}\n        className={`absolute rounded-lg`}\n        style={{\n          height: \"34px\",\n          width: \"3px\",\n        }}\n      />\n    </AnimatePresence>\n  );\n};\n\nfunction useAnimator() {\n  return useMemo(() => {\n    return new Animator(SMOOTH_CARET_ELEMENT_ID);\n  }, []);\n}\n\nclass Animator {\n  private element: HTMLElement | null;\n  constructor(private elementId: string) {\n    this.element = null;\n  }\n\n  private getElement() {\n    if (this.element) {\n      return this.element;\n    }\n    this.element = document.getElementById(this.elementId);\n    return this.element;\n  }\n\n  private elementInStarterPosition(element: HTMLElement) {\n    const left = element.style.left;\n    const top = element.style.top;\n    return left === \"\" || top === \"\";\n  }\n\n  animate(rect: { left: number; top: number }) {\n    const element = this.getElement();\n    if (!element) return;\n    const left = rect.left - 4 + \"px\";\n    const top = rect.top + 3 + \"px\";\n    const duration = this.elementInStarterPosition(element) ? 0 : 75;\n    const caretAnimation = element.animate([{ left, top }], {\n      easing: \"linear\",\n      duration,\n      iterations: 1,\n    });\n    caretAnimation.onfinish = () => {\n      element.style.left = left;\n      element.style.top = top;\n    };\n  }\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/TweetResult.tsx",
    "content": "import { faTwitter } from \"@fortawesome/free-brands-svg-icons\";\nimport { IconDefinition } from \"@fortawesome/free-solid-svg-icons\";\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\nimport React from \"react\";\n\nexport interface TweetResultProps {\n  wpm: number;\n  url: string;\n}\n\nexport const TweetResult = ({ wpm, url }: TweetResultProps) => {\n  let tweetText = `Checkout this typing game for programmers. Can you beat ${wpm} wpm?`;\n  let tweetUrl = `https://twitter.com/intent/tweet?url=${url}&text=${tweetText}&via=codicocodes`;\n  return (\n    <a\n      href={tweetUrl}\n      target=\"blank\"\n      className=\"w-full sm:w-auto flex shadow-lg hover:shadow-violet-900 hover:cursor-pointer text-faded-gray hover:text-off-white bg-dark-lake flex-col items-center justify-center px-1 rounded hover:bg-white/10\"\n    >\n      <div className=\"h-4 w-4\">\n        <FontAwesomeIcon icon={faTwitter as IconDefinition} />\n      </div>\n    </a>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/TypedChars.tsx",
    "content": "import highlightjs from \"highlight.js\";\nimport \"highlight.js/styles/github-dark.css\";\nimport { useEffect, useRef } from \"react\";\nimport { useCodeStore } from \"../state/code-store\";\nimport { useSettingsStore } from \"../state/settings-store\";\n\ninterface TypedCharsProps {\n  language: string;\n}\n\nexport function TypedChars({ language }: TypedCharsProps) {\n  const isSyntaxHighlightingEnabled = useSettingsStore(\n    (s) => s.syntaxHighlighting\n  );\n  useCodeStore((state) => state.code);\n  const index = useCodeStore((state) => state.index);\n  const typedChars = useCodeStore((state) => state.correctChars);\n  const typedRef = useRef<HTMLSpanElement>(null);\n  useEffect(() => {\n    if (!isSyntaxHighlightingEnabled) return;\n    if (typedRef.current) {\n      highlightjs.highlightElement(typedRef.current);\n    }\n  }, [index, isSyntaxHighlightingEnabled]);\n  return (\n    <span\n      className={`text-violet-400 ${language}`}\n      ref={typedRef}\n      style={{ background: \"none\" }}\n    >\n      {typedChars()}\n    </span>\n  );\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/UntypedChars.tsx",
    "content": "import { useCodeStore } from \"../state/code-store\";\nexport function UntypedChars() {\n  const untypedChars = useCodeStore((state) => state.untypedChars);\n  return <>{untypedChars()}</>;\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/leaderboard/Leaderboard.tsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { RadioGroup } from \"@headlessui/react\";\nimport Image from \"next/image\";\nimport Link from \"next/link\";\nimport useSWR from \"swr\";\nimport { GithubLogo } from \"../../../../assets/icons\";\nimport ModalCloseButton from \"../../../../common/components/buttons/ModalCloseButton\";\nimport Modal from \"../../../../common/components/modals/Modal\";\nimport { CrownIcon } from \"../../../../assets/icons/CrownIcon\";\nimport { cpmToWPM } from \"../../../../common/utils/cpmToWPM\";\nimport { getExperimentalServerUrl } from \"../../../../common/utils/getServerUrl\";\nimport { humanizeAbsolute } from \"../../../../utils/humanize\";\nimport { closeModals } from \"../../state/settings-store\";\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\nimport { faExternalLink } from \"@fortawesome/free-solid-svg-icons\";\n\nexport const Leaderboard: React.FC = () => {\n  const baseUrl = getExperimentalServerUrl();\n  const [selectedLeaderboard, setSelectedLeaderboard] =\n    useState<Leaderboards>(\"wpm\");\n\n  const [activityData, setActivityData] = useState<any[]>([]);\n  const { data, isLoading } = useSWR(\n    baseUrl + \"/api/results/leaderboard\",\n    (...args) => fetch(...args).then((res) => res.json()),\n    { refreshInterval: 15000 }\n  );\n\n  useEffect(() => {\n    if (data) {\n      const newData = [...data];\n      newData.sort((a, b) => b.racesPlayed - a.racesPlayed);\n      setActivityData(newData);\n    }\n  }, [data]);\n  return (\n    <Modal>\n      <div className=\"flex flex-col sm:flex-row justify-between items-center gap-4\">\n        <h2 className=\"text-2xl tracking-widest font-thin\">\n          Daily Leaderboard\n        </h2>\n        <div className=\"flex justify-end gap-2\">\n          <LeaderboardSelector\n            selectedLeaderboard={selectedLeaderboard}\n            setSelectedLeaderboard={setSelectedLeaderboard}\n          />\n          <ModalCloseButton onButtonClickHandler={closeModals} />\n        </div>\n      </div>\n      <div className=\"flex\">\n        {selectedLeaderboard === \"wpm\" && (\n          <WPMLeaderboard results={data} isLoading={isLoading} />\n        )}\n        {selectedLeaderboard === \"activity\" && (\n          <ActivityLeaderboard results={activityData} isLoading={isLoading} />\n        )}\n      </div>\n    </Modal>\n  );\n};\n\nexport const ActivityLeaderboard = ({\n  isLoading,\n  results,\n}: WPMLeaderboardProps) => {\n  return (\n    <>\n      {!isLoading && results && (\n        <div className=\"tracking-wider font-thin\">\n          <LeaderboardRowActivity\n            placement=\"#\"\n            user=\"user\"\n            racesPlayed=\"Races played\"\n          />\n          {results.map((r: any, i: number) => {\n            const placement = i === 0 ? <CrownIcon /> : i + 1;\n            const userNode = (\n              <div className=\"flex items-center\">\n                <div className=\"hidden sm:block\">\n                  <Image\n                    className=\"hidden sm:block rounded-full\"\n                    width=\"30\"\n                    height=\"30\"\n                    quality={100}\n                    src={r.avatarUrl}\n                    alt=\"\"\n                  />\n                </div>\n                <span className=\"ml-2\" title={r.username}>\n                  {r.username}\n                </span>\n                <Link href={`https://github.com/${r.username}`}>\n                  <a target=\"_blank\" className=\"ml-1\">\n                    <GithubLogo />\n                  </a>\n                </Link>\n              </div>\n            );\n\n            return (\n              <LeaderboardRowActivity\n                key={i}\n                placement={placement}\n                user={userNode}\n                racesPlayed={r.racesPlayed}\n              />\n            );\n          })}\n        </div>\n      )}\n    </>\n  );\n};\n\ninterface WPMLeaderboardProps {\n  isLoading: boolean;\n  results: any[];\n}\n\nexport const WPMLeaderboard = ({ isLoading, results }: WPMLeaderboardProps) => {\n  return (\n    <>\n      {!isLoading && results && (\n        <div className=\"tracking-wider font-thin\">\n          <LeaderboardRowWPM\n            placement=\"#\"\n            user=\"user\"\n            speed=\"speed (wpm)\"\n            accuracy=\"accuracy\"\n            timeAgo=\"time ago\"\n            resultLink=\"\"\n          />\n          {results.map((r: any, i: number) => {\n            const placement = i === 0 ? <CrownIcon /> : i + 1;\n            const userNode = (\n              <div className=\"flex items-center\">\n                <div className=\"hidden sm:block\">\n                  <Image\n                    className=\"rounded-full\"\n                    width=\"30\"\n                    height=\"30\"\n                    quality={100}\n                    src={r.avatarUrl}\n                    alt=\"\"\n                  />\n                </div>\n                <span className=\"ml-2\" title={r.username}>\n                  {r.username}\n                </span>\n                <Link href={`https://github.com/${r.username}`}>\n                  <a target=\"_blank\" className=\"ml-1\">\n                    <GithubLogo />\n                  </a>\n                </Link>\n              </div>\n            );\n\n            const resultLink = (\n              <Link href={`/results/${r.resultId}`}>\n                <div\n                  className=\"h-4 w-4 hover:cursor-pointer\"\n                  onClick={closeModals}\n                >\n                  <FontAwesomeIcon icon={faExternalLink} />\n                </div>\n              </Link>\n            );\n\n            return (\n              <LeaderboardRowWPM\n                key={i}\n                placement={placement}\n                user={userNode}\n                speed={cpmToWPM(r.cpm).toString()}\n                accuracy={r.accuracy + \"%\"}\n                timeAgo={humanizeAbsolute(new Date(r.createdAt))}\n                resultLink={resultLink}\n              />\n            );\n          })}\n        </div>\n      )}\n    </>\n  );\n};\n\ninterface LeaderboardRowWPMProps {\n  placement: React.ReactNode;\n  user: React.ReactNode;\n  speed: string;\n  accuracy: string;\n  timeAgo: string;\n  resultLink: React.ReactNode;\n}\n\nexport const LeaderboardRowWPM: React.FC<LeaderboardRowWPMProps> = (props) => {\n  return (\n    <div\n      className={`flex items-center justify-start gap-8 p-1 px-2 my-1 first:font-bold even:bg-gray-200 rounded`}\n    >\n      <span className=\"ml-2 w-[25px]\">{props.placement}</span>\n      <div className=\"w-[125px] sm:w-[300px] truncate\">{props.user}</div>\n      <span className=\"w-[120px]\">{props.speed}</span>\n      <span className=\"hidden sm:block w-[125px]\">{props.accuracy}</span>\n      <span className=\"hidden sm:block mr-2 w-[125px]\">{props.timeAgo}</span>\n      <span className=\"hidden sm:block mr-2\">{props.resultLink}</span>\n    </div>\n  );\n};\n\ninterface LeaderboardRowActivityProps {\n  placement: React.ReactNode;\n  user: React.ReactNode;\n  racesPlayed: string;\n}\n\nexport const LeaderboardRowActivity: React.FC<LeaderboardRowActivityProps> = (\n  props\n) => {\n  return (\n    <div\n      className={`flex items-center justify-start gap-8 p-1 px-2 my-1 first:font-bold even:bg-gray-200 rounded`}\n    >\n      <span className=\"ml-2 w-[25px]\">{props.placement}</span>\n      <div className=\"w-[125px] sm:w-[300px] truncate\">{props.user}</div>\n      <span className=\"hidden sm:block sm:w-[120px]\"></span>\n      <span className=\"hidden sm:block sm:w-[100px]\"></span>\n      <span className=\"mr-2 w-100px sm:w-[150px]\">{props.racesPlayed}</span>\n    </div>\n  );\n};\n\ntype Leaderboards = \"wpm\" | \"activity\";\n\ninterface LeaderboardSelectorProps {\n  selectedLeaderboard: Leaderboards;\n  setSelectedLeaderboard: (board: Leaderboards) => void;\n}\n\nexport const LeaderboardSelector = ({\n  selectedLeaderboard,\n  setSelectedLeaderboard,\n}: LeaderboardSelectorProps) => {\n  return (\n    <RadioGroup value={selectedLeaderboard} onChange={setSelectedLeaderboard}>\n      <div className=\"flex w-full font-bold tracking-widest gap-1\">\n        <RadioGroup.Option\n          className=\"w-full cursor-pointer min-w-[150px]\"\n          value=\"wpm\"\n        >\n          {({ checked }) => (\n            <div\n              className={`flex items-center h-8 w-full p-3 rounded-lg transition ease-in-out ${\n                checked\n                  ? \"bg-purple-200 hover:bg-purple-300\"\n                  : \"bg-gray-300 hover:bg-gray-400\"\n              }`}\n            >\n              <span>wpm</span>\n            </div>\n          )}\n        </RadioGroup.Option>\n        <RadioGroup.Option\n          className=\"w-full cursor-pointer min-w-[150px]\"\n          value=\"activity\"\n        >\n          {({ checked }) => (\n            <div\n              className={`flex items-center h-8 w-full p-3 rounded-lg transition ease-in-out ${\n                checked\n                  ? \"bg-purple-200 hover:bg-purple-300\"\n                  : \"bg-gray-300 hover:bg-gray-400\"\n              }`}\n            >\n              activity\n            </div>\n          )}\n        </RadioGroup.Option>\n      </div>\n    </RadioGroup>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/leaderboard/LeaderboardButton.tsx",
    "content": "import { AnimatePresence, motion } from \"framer-motion\";\nimport React from \"react\";\nimport { CrownIcon } from \"../../../../assets/icons/CrownIcon\";\nimport Button from \"../../../../common/components/Button\";\nimport { Overlay } from \"../../../../common/components/Overlay\";\nimport {\n  closeModals,\n  openLeaderboardModal,\n  useSettingsStore,\n} from \"../../state/settings-store\";\nimport { Leaderboard } from \"./Leaderboard\";\n\nexport const LeaderboardButton: React.FC = () => {\n  const isOpen = useSettingsStore((s) => s.leaderboardModalIsOpen);\n  return (\n    <>\n      <Button\n        size=\"sm\"\n        onClick={openLeaderboardModal}\n        color=\"invisible\"\n        leftIcon={<CrownIcon />}\n      />\n      {isOpen && (\n        <Overlay onOverlayClick={closeModals}>\n          <AnimatePresence>\n            <motion.div\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: 0.2 }}\n            >\n              <Leaderboard />\n            </motion.div>\n          </AnimatePresence>\n        </Overlay>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/play-footer/ChallengeSource/ChallengeSource.tsx",
    "content": "import React from \"react\";\nimport Link from \"next/link\";\nimport { GithubLogo } from \"../../../../../assets/icons\";\nimport {\n  closeModals,\n  openProjectModal,\n  useSettingsStore,\n} from \"../../../state/settings-store\";\nimport { Overlay } from \"../../../../../common/components/Overlay\";\nimport ModalCloseButton from \"../../../../../common/components/buttons/ModalCloseButton\";\n\nexport interface ChallengeSourceProps {\n  url: string;\n  name: string;\n  license: string;\n}\n\nexport const ChallengeSource: React.FC<ChallengeSourceProps> = (props) => {\n  const { name } = props;\n  const isOpen = useSettingsStore((s) => s.projectModalIsOpen);\n  return (\n    <>\n      <button onClick={openProjectModal}>\n        <a target=\"_blank\" className=\"flex items-center font-semibold text-sm hover:text-off-white\">\n          <div className=\"mr-1\">\n            <GithubLogo />\n          </div>\n          {name}\n        </a>\n      </button>\n      {isOpen && (\n        <Overlay onOverlayClick={closeModals}>\n          <div\n            className=\"flex flex-col bg-off-white text-dark-ocean p-5 rounded gap-2 w-full max-h-screen overflow-y-scroll gap-y-4 sm:min-w-[400px]\"\n            style={{ fontFamily: \"Fira Code\" }}\n          >\n            <div className=\"flex items-center justify-between\">\n              <div className=\"mr-2\">\n                <GithubLogo />\n              </div>\n              <h2 className=\"font-semibold tracking-wider text-xl\">{name}</h2>\n              <Link href={`https://github.com/${name}`}>\n                <a target=\"_blank\" className=\"mr-2\">\n                  <OutwardLinkIcon />\n                </a>\n              </Link>\n              <ModalCloseButton onButtonClickHandler={closeModals} />\n            </div>\n            <div className=\"flex items-center\">\n              <h2 className=\"text-sm font-semibold mr-2\">License:</h2>\n              <h2 className=\"text-sm\">{props.license}</h2>\n            </div>\n\n            <div className=\"flex items-center\">\n              <h2 className=\"text-sm tracking-wider font-semibold\">\n                Challenge source\n              </h2>\n              <Link href={props.url}>\n                <a target=\"_blank\" className=\"mr-2\">\n                  <OutwardLinkIcon />\n                </a>\n              </Link>\n            </div>\n            <div className=\"flex items-center\">\n              <div className=\"text-green-500\">\n                <svg\n                  className=\"fill-current mr-2\"\n                  viewBox=\"0 0 16 16\"\n                  version=\"1.1\"\n                  width=\"16\"\n                  height=\"16\"\n                  aria-hidden=\"true\"\n                >\n                  <path d=\"M8 9.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z\"></path>\n                  <path d=\"M8 0a8 8 0 100 16A8 8 0 008 0zM1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0z\"></path>\n                </svg>\n              </div>\n              <h2 className=\"text-sm tracking-wider font-semibold\">\n                Good first issues\n              </h2>\n              <Link\n                href={`https://github.com/${name}/issues?q=is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22+`}\n              >\n                <a target=\"_blank\" className=\"mr-2\">\n                  <OutwardLinkIcon />\n                </a>\n              </Link>\n            </div>\n          </div>\n        </Overlay>\n      )}\n    </>\n  );\n};\n\nexport const OutwardLinkIcon = () => {\n  // SVG Below: Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 512 512\"\n      className=\"ml-2 h-3\"\n    >\n      <path d=\"M320 0c-17.7 0-32 14.3-32 32s14.3 32 32 32h82.7L201.4 265.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L448 109.3V192c0 17.7 14.3 32 32 32s32-14.3 32-32V32c0-17.7-14.3-32-32-32H320zM80 32C35.8 32 0 67.8 0 112V432c0 44.2 35.8 80 80 80H400c44.2 0 80-35.8 80-80V320c0-17.7-14.3-32-32-32s-32 14.3-32 32V432c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V112c0-8.8 7.2-16 16-16H192c17.7 0 32-14.3 32-32s-14.3-32-32-32H80z\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/play-footer/ChallengeSource/index.ts",
    "content": "export * from \"./ChallengeSource\";\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/play-footer/PlayFooter/PlayFooter.tsx",
    "content": "import { faPerson, faUserGroup } from \"@fortawesome/free-solid-svg-icons\";\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { ButtonHTMLAttributes, useState } from \"react\";\nimport { PlayIcon } from \"../../../../../assets/icons\";\nimport { InfoIcon } from \"../../../../../assets/icons/InfoIcon\";\nimport { LinkIcon } from \"../../../../../assets/icons/LinkIcon\";\nimport { ReloadIcon } from \"../../../../../assets/icons/ReloadIcon\";\nimport { WarningIcon } from \"../../../../../assets/icons/WarningIcon\";\nimport { GithubLoginOverlay } from \"../../../../../common/components/overlays/GithubLoginOverlay\";\nimport { useIsPlaying } from \"../../../../../common/hooks/useIsPlaying\";\nimport { useUserStore } from \"../../../../../common/state/user-store\";\nimport { copyToClipboard } from \"../../../../../common/utils/clipboard\";\nimport { toHumanReadableTime } from \"../../../../../common/utils/toHumanReadableTime\";\nimport { Keys, useKeyMap } from \"../../../../../hooks/useKeyMap\";\nimport useTotalSeconds from \"../../../../../hooks/useTotalSeconds\";\nimport { ChallengeInfo } from \"../../../hooks/useChallenge\";\nimport { useCodeStore } from \"../../../state/code-store\";\nimport { useConnectionStore } from \"../../../state/connection-store\";\nimport {\n  useGameStore,\n  useIsMultiplayer,\n  useIsOwner,\n} from \"../../../state/game-store\";\nimport {\n  closeModals,\n  openProfileModal,\n  useHasOpenModal,\n  useSettingsStore,\n} from \"../../../state/settings-store\";\nimport { RaceSettings } from \"../../RaceSettings\";\nimport { ChallengeSource } from \"../ChallengeSource\";\n\ninterface PlayFooterProps {\n  challenge: ChallengeInfo;\n}\n\nfunction useCodeStoreTotalSeconds() {\n  // TODO: move useTotalSeconds to modules folder\n  const startTime = useCodeStore((state) => state.startTime);\n  const endTime = useCodeStore((state) => state.endTime);\n  const totalSeconds = useTotalSeconds(\n    startTime?.getTime(),\n    endTime?.getTime()\n  );\n  return totalSeconds;\n}\n\nfunction useMistakeWarningMessage() {\n  const currentMistakeCount = useCodeStore((state) => state.incorrectChars)()\n    .length;\n  return currentMistakeCount > 10;\n}\n\nexport function WarningContainer() {\n  const mistakesWarning = useMistakeWarningMessage();\n  const isConnected = useConnectionStore((state) => state.isConnected);\n  const raceExistsInServer = useConnectionStore(\n    (state) => state.raceExistsInServer\n  );\n  const alreadyPlaying = useConnectionStore((state) => state.alreadyPlaying);\n  const showRaceDoesNotExistWarning = !raceExistsInServer && !alreadyPlaying;\n  const showDisconnectedWarning = !alreadyPlaying && !isConnected;\n  return (\n    <>\n      {mistakesWarning && (\n        <span className=\"flex ml-2 text-red-400 font-medium gap-1\">\n          <WarningIcon />\n          Undo mistakes to continue\n        </span>\n      )}\n      {showDisconnectedWarning && (\n        <span className=\"flex ml-2 text-red-400 font-medium gap-1\">\n          <WarningIcon />\n          You are not connected to the server.\n        </span>\n      )}\n      {alreadyPlaying && (\n        <span className=\"flex ml-2 text-red-400 font-medium gap-1\">\n          <WarningIcon />\n          You can not play in two browsers simultaneously\n        </span>\n      )}\n      {showRaceDoesNotExistWarning && (\n        <h2 className=\"text-red-400 flex justify-center items-center ml-2 text-lg font-medium gap-1 my-2\">\n          <WarningIcon />\n          This race does not exist. Refresh to continue.\n          <i\n            className=\"text-off-white h-4\"\n            title=\"When the server restarts current race state is resets\"\n          >\n            <InfoIcon />\n          </i>\n        </h2>\n      )}\n    </>\n  );\n}\n\nexport function PlayFooter({ challenge }: PlayFooterProps) {\n  const isPlaying = useIsPlaying();\n  const totalSeconds = useCodeStoreTotalSeconds();\n  const isAnonymous = useUserStore((u) => u.isAnonymous);\n  const profileModalIsOpen = useSettingsStore((s) => s.profileModalIsOpen);\n  return (\n    <div className=\"w-full h-10 px-2\">\n      <AnimatePresence>\n        <motion.div\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          exit={{ opacity: 0 }}\n          transition={{ duration: 0.5 }}\n          className=\"w-full\"\n        >\n          {!isPlaying && (\n            <div className=\"w-full\">\n              <div className=\"flex row justify-between items-top\">\n                <ActionButtons />\n                <div className=\"text-faded-gray flex gap-4\">\n                  {isAnonymous && (\n                    <>\n                      <button\n                        onClick={openProfileModal}\n                        className=\"flex text-xs items-center font-semibold tracking-wide hover:cursor-pointer gap-2 hover:text-off-white\"\n                      >\n                        <div className=\"h-4 w-4 flex items-center\">\n                          <FontAwesomeIcon icon={faUserGroup} size=\"xs\" />\n                        </div>\n                        Login | Signup\n                      </button>\n                      {profileModalIsOpen && (\n                        <GithubLoginOverlay closeModal={closeModals} />\n                      )}\n                    </>\n                  )}\n                  {challenge.projectName && (\n                    <ChallengeSource\n                      name={challenge.projectName}\n                      url={challenge.url}\n                      license={challenge.license}\n                    />\n                  )}\n                </div>\n              </div>\n            </div>\n          )}\n        </motion.div>\n      </AnimatePresence>\n      <AnimatePresence>\n        <motion.div\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          exit={{ opacity: 0 }}\n          transition={{ duration: 0.5 }}\n          className=\"flex items-center w-full\"\n        >\n          {isPlaying && <Timer seconds={totalSeconds} />}\n          <WarningContainer />\n        </motion.div>\n      </AnimatePresence>\n    </div>\n  );\n}\n\ninterface ActionButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  icon: React.ReactElement;\n  text: String;\n}\n\nexport function ActionButton({\n  text,\n  icon,\n  ...buttonProps\n}: ActionButtonProps) {\n  return (\n    <button\n      {...buttonProps}\n      style={{\n        fontFamily: \"Fira Code\",\n      }}\n      className=\"flex text-sm font-light text-dark-ocean items-center justify-between gap-2 rounded-3xl bg-gray-300 hover:bg-gray-400 hover:cursor-pointer px-3 py-0.5 my-1\"\n    >\n      <div className=\"hidden sm:flex\">{text}</div>\n      {icon}\n    </button>\n  );\n}\n\nfunction ActionButtons() {\n  const game = useGameStore((s) => s.game);\n  const isMultiplayer = useIsMultiplayer();\n  const isOwner = useIsOwner();\n  const hasEndTime = useCodeStore((state) => state.endTime);\n  const countdown = useGameStore((state) => state.countdown);\n  const hasOpenModal = useHasOpenModal();\n  const canManuallyStartGame =\n    !hasOpenModal && isOwner && isMultiplayer && !hasEndTime && !countdown;\n  const waitingForOwnerToStart =\n    !isOwner && isMultiplayer && !hasEndTime && !countdown;\n  const startGame = () => game?.start();\n  useKeyMap(canManuallyStartGame, Keys.Enter, startGame);\n  const [isThrottled, setIsThrottled] = useState(false);\n  return (\n    <div className=\"flex text-faded-gray gap-1\">\n      {isOwner && (\n        <ActionButton\n          text=\"refresh\"\n          title=\"Refresh the challenge\"\n          disabled={isThrottled}\n          onClick={() => {\n            if (isThrottled) return;\n            game?.next();\n            setIsThrottled(true);\n            setTimeout(() => {\n              setIsThrottled(false);\n            }, 2000);\n          }}\n          icon={\n            <div className=\"h-3 w-3\">\n              <ReloadIcon />\n            </div>\n          }\n        />\n      )}\n      <ActionButton\n        text=\"invite\"\n        title=\"Invite your friends to play\"\n        icon={\n          <div className=\"h-3 w-4\">\n            <LinkIcon />\n          </div>\n        }\n        onClick={() => {\n          const url = new URL(window.location.href);\n          if (game?.id) {\n            url.searchParams.set(\"id\", game.id);\n          }\n          copyToClipboard(url.toString(), `${url} copied to clipboard`);\n        }}\n      />\n      {isOwner && <RaceSettings />}\n      {canManuallyStartGame && (\n        <ActionButton\n          title=\"Start the game\"\n          text=\"start\"\n          onClick={startGame}\n          icon={<PlayIcon />}\n        />\n      )}\n      {waitingForOwnerToStart && (\n        <span className=\"flex text-sm font-light text-off-white items-center justify-between\">\n          Waiting for race to start\n        </span>\n      )}\n    </div>\n  );\n}\n\nfunction Timer({ seconds }: { seconds: number }) {\n  return (\n    <div className=\"text-3xl ml-2 font-bold text-purple-300\">\n      {toHumanReadableTime(seconds)}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/play-footer/PlayFooter/index.ts",
    "content": "export * from \"./PlayFooter\";\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/play-header/PlayHeader/PlayHeader.tsx",
    "content": "import { AnimatePresence, motion } from \"framer-motion\";\nimport { CrownIcon } from \"../../../../../assets/icons/CrownIcon\";\nimport { toHumanReadableTime } from \"../../../../../common/utils/toHumanReadableTime\";\nimport cpmToWpm from \"../../../../../utils/cpmToWpm\";\nimport {\n  RacePlayer,\n  RaceResult,\n  useGameStore,\n  useIsMultiplayer,\n} from \"../../../state/game-store\";\n\nexport function ResultsContainer() {\n  const isMultiplayer = useIsMultiplayer();\n  const results = useGameStore((state) => state.results);\n  return isMultiplayer ? (\n    <div className=\"my-1\">\n      {Object.values(results).map((result, i) => {\n        const place = i + 1;\n        return <Result key={i} result={result} place={place} />;\n      })}\n    </div>\n  ) : null;\n}\n\nexport function ProgressContainer() {\n  const isMultiplayer = useIsMultiplayer();\n  const members = useGameStore((state) => state.members);\n  return isMultiplayer ? (\n    <div className=\"my-2\">\n      {Object.values(members).map((player) => {\n        return <ProgressBar key={player.id} player={player} />;\n      })}\n    </div>\n  ) : null;\n}\n\ninterface ResultProps {\n  result: RaceResult;\n  place: number;\n}\n\nexport function Result({ result, place }: ResultProps) {\n  return (\n    <div className=\"flex row w-full items-center bg-dark-lake rounded-lg px-3 py-2 my-2\">\n      <span className=\"flex w-48 ml-1 mr-4 text-sm font-semibold truncate\">\n        {result.user.username}\n      </span>\n      <div className=\"flex w-full gap-2\">\n        <span className=\"flex font-semibold text-xs rounded-lg px-2 py-1 bg-purple-300 text-dark-ocean\">\n          {place} place\n        </span>\n        <div className=\"flex flex-grow justify-end gap-2\">\n          <span className=\"font-semibold text-xs rounded-lg px-2 py-1 bg-gray-700\">\n            {cpmToWpm(result.cpm)} wpm\n          </span>\n          <span className=\"font-semibold text-xs rounded-lg px-2 py-1 bg-gray-700\">\n            {result.accuracy}% accuracy\n          </span>\n          <span className=\"font-semibold text-xs rounded-lg px-2 py-1 bg-gray-700\">\n            {toHumanReadableTime(Math.floor(result.timeMS / 1000))}\n          </span>\n          <span className=\"font-semibold text-xs rounded-lg px-2 py-1 bg-gray-700\">\n            {result.mistakes} mistakes\n          </span>\n        </div>\n      </div>\n    </div>\n  );\n}\n\ninterface ProgressBarProps {\n  player: RacePlayer;\n}\n\ninterface ProgressProps {\n  progress: Number;\n  word: string;\n}\n\nexport function Progress({ progress, word }: ProgressProps) {\n  return (\n    <div\n      className=\"w-full bg-white rounded-lg flex items-center\"\n      style={{\n        height: \"4px\",\n      }}\n    >\n      <div\n        className=\"bg-purple-300 h-full rounded-lg\"\n        style={{ width: `${progress}%`, transition: \"width 200ms ease-in-out\" }}\n      ></div>\n      {word && (\n        <span className=\"font-semibold text-xs rounded-lg px-2 py-1 bg-gray-700\">\n          {word}\n        </span>\n      )}\n    </div>\n  );\n}\n\nexport function ProgressBar({ player }: ProgressBarProps) {\n  const ownerId = useGameStore.getState().owner;\n  const isOwner = ownerId === player.id;\n  const isCompleted = player.progress === 100;\n  return !isCompleted ? (\n    <div className=\"flex row w-full items-center bg-dark-lake rounded-lg px-3 py-2 my-2\">\n      <span className=\"flex w-48 ml-1 mr-4 text-sm font-semibold truncate\">\n        {player.username}\n        {isOwner ? (\n          <div className=\"ml-1\">\n            <CrownIcon />\n          </div>\n        ) : null}\n      </span>\n      <Progress progress={player.progress} word={player.recentlyTypedLiteral} />\n    </div>\n  ) : null;\n}\n\nexport function PlayHeader() {\n  return (\n    <div className=\"w-full relative\">\n      <AnimatePresence>\n        <motion.div\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          exit={{ opacity: 0 }}\n          transition={{ duration: 0.5 }}\n          className=\"w-full\"\n        >\n          <ResultsContainer />\n          <ProgressContainer />\n        </motion.div>\n      </AnimatePresence>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/play-header/PlayHeader/index.tsx",
    "content": "export * from \"./PlayHeader\";\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/race-settings/LanguageSelector.tsx",
    "content": "import useSWR from \"swr\";\nimport { Listbox } from \"@headlessui/react\";\nimport { CrossIcon } from \"../../../../assets/icons/CrossIcon\";\nimport { getExperimentalServerUrl } from \"../../../../common/utils/getServerUrl\";\nimport { useSettingsStore, LanguageDTO, setLanguage } from \"../../state/settings-store\";\nimport { useGameStore } from \"../../state/game-store\";\n\nconst selectProgrammingLanguage = (languageSelected: LanguageDTO | null) => {\n  setLanguage(languageSelected);\n  if (languageSelected) {\n    useGameStore.getState().game?.next();\n  }\n};\n\nconst baseUrl = getExperimentalServerUrl();\n\nexport function LanguageSelector() {\n  const { data, isLoading } = useSWR(baseUrl + \"/api/languages\", (...args) =>\n    fetch(...args).then((res) => res.json())\n  );\n  const languages = data as undefined | { language: string; name: string }[];\n  const selectedLanguage = useSettingsStore((s) => s.languageSelected);\n  return (\n    <div className=\"w-full text-dark-ocean font-thin w-[250px]\">\n      <h2 className=\"text-xs mb-1 font-semibold uppercase tracking-widest\">\n        select language\n      </h2>\n      <Listbox value={selectedLanguage} onChange={selectProgrammingLanguage}>\n        <div className=\"flex items-center\">\n          <Listbox.Button className=\"flex items-center justify-between px-2 bg-gray-200 p-1 w-full rounded\">\n            {selectedLanguage?.name || \"nothing selected\"}\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox=\"0 0 20 20\"\n              fill=\"currentColor\"\n              aria-hidden=\"true\"\n              className=\"h-5 w-5 text-gray-400\"\n            >\n              <path\n                fillRule=\"evenodd\"\n                d=\"M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z\"\n                clipRule=\"evenodd\"\n              ></path>\n            </svg>\n          </Listbox.Button>\n          {selectedLanguage && (\n            <button\n              onClick={() => selectProgrammingLanguage(null)}\n              className=\"flex items-center p-1 h-full  bg-gray-200 ml-2 rounded\"\n            >\n              <div className=\"w-2 text-red-500 fill-current mx-1\">\n                <CrossIcon />\n              </div>\n              <span>clear</span>\n            </button>\n          )}\n        </div>\n        {!isLoading && (\n          <Listbox.Options static>\n            {languages?.map((language) => (\n              <Listbox.Option key={language.name} value={language}>\n                {({ active, selected }) => {\n                  return (\n                    <li\n                      className={`pl-2 flex items-center gap-2 my-1 p-1 rounded cursor-pointer ${\n                        active ? \"bg-gray-300\" : \"bg-gray-200\"\n                      }`}\n                    >\n                      {language.name}\n                      {selected && (\n                        <svg\n                          xmlns=\"http://www.w3.org/2000/svg\"\n                          viewBox=\"0 0 512 512\"\n                          className=\"h-5 w-auto text-green-500 fill-current\"\n                        >\n                          <path d=\"M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7 425.4 105.4c12.5-12.5 32.8-12.5 45.3 0z\" />\n                        </svg>\n                      )}\n                    </li>\n                  );\n                }}\n              </Listbox.Option>\n            ))}\n          </Listbox.Options>\n        )}\n      </Listbox>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/containers/CodeTypingContainer.tsx",
    "content": "import { useFocusRef } from \"../hooks/useFocusRef\";\nimport { useCodeStore } from \"../state/code-store\";\nimport { CodeArea } from \"../components/CodeArea\";\nimport { HiddenCodeInput } from \"../components/HiddenCodeInput\";\nimport { TypedChars } from \"../components/TypedChars\";\nimport { NextChar } from \"../components/NextChar\";\nimport { IncorrectChars } from \"../components/IncorrectChars\";\nimport { UntypedChars } from \"../components/UntypedChars\";\nimport { useEffect, useState, useCallback, MouseEvent } from \"react\";\nimport { useIsPlaying } from \"../../../common/hooks/useIsPlaying\";\nimport { useKeyMap, triggerKeys } from \"../../../hooks/useKeyMap\";\nimport { useHasOpenModal } from \"../state/settings-store\";\n\ninterface CodeTypingContainerProps {\n  filePath: string;\n  language: string;\n}\n\nconst CODE_INPUT_BLUR_DEBOUNCE_MS = 1000;\n\nlet trulyFocusedCodeInput = true;\n\nexport function CodeTypingContainer({\n  filePath,\n  language,\n}: CodeTypingContainerProps) {\n  useCodeStore((state) => state.code);\n  const isPlaying = useIsPlaying();\n  const code = useCodeStore((state) => state.code);\n  const start = useCodeStore((state) => state.start);\n  const index = useCodeStore((state) => state.index);\n  const hasOpenModal = useHasOpenModal();\n  const [inputRef, triggerFocus] = useFocusRef<HTMLTextAreaElement>();\n  const [focused, setFocused] = useState(true);\n\n  useKeyMap(!hasOpenModal && !focused, triggerKeys, () => {\n    triggerFocus();\n    setFocused(true);\n  });\n\n  useEffect(() => {\n    triggerFocus();\n  }, [code, triggerFocus]);\n\n  useEffect(() => {\n    if (!isPlaying && index > 0) {\n      start();\n    }\n  }, [index, isPlaying, start]);\n\n  const onFocus = useCallback(() => {\n    trulyFocusedCodeInput = true;\n    setFocused(true);\n  }, [setFocused]);\n\n  const onBlur = useCallback(() => {\n    trulyFocusedCodeInput = false;\n    setTimeout(() => {\n      if (!trulyFocusedCodeInput) {\n        setFocused(false);\n      }\n    }, CODE_INPUT_BLUR_DEBOUNCE_MS);\n  }, [setFocused]);\n\n  // onBlur gets triggered when onFocus is also called more than once\n  // which caused a flicker when you repeatedly click the code area\n  // this will prevent onBlur from getting called repeatedly\n  // Ref: https://github.com/react-toolbox/react-toolbox/issues/1323#issuecomment-656778859\n  const onMouseDownPreventBlur = useCallback(\n    (e: MouseEvent<HTMLDivElement>) => {\n      e.preventDefault();\n    },\n    []\n  );\n\n  return (\n    <div className=\"w-full relative\" onClick={triggerFocus}>\n      <div\n        className=\"flex flex-col w-full\"\n        onFocus={onFocus}\n        onBlur={onBlur}\n        onMouseDown={onMouseDownPreventBlur}\n      >\n        <HiddenCodeInput hide={true} disabled={false} inputRef={inputRef} />\n        <CodeArea staticHeigh={true} filePath={filePath} focused={focused}>\n          <TypedChars language={language} />\n          <IncorrectChars />\n          <NextChar focused={focused} />\n          <UntypedChars />\n        </CodeArea>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/containers/ResultsContainer.tsx",
    "content": "import {\n  faArrowDown,\n  faArrowTrendUp,\n  faArrowUp,\n  faCheckCircle,\n  faCircleXmark,\n  faExternalLink,\n  faShare,\n  faSquarePollVertical,\n} from \"@fortawesome/free-solid-svg-icons\";\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\nimport Link from \"next/link\";\nimport { copyToClipboard } from \"../../../common/utils/clipboard\";\nimport { cpmToWPM } from \"../../../common/utils/cpmToWPM\";\nimport { toHumanReadableTime } from \"../../../common/utils/toHumanReadableTime\";\nimport ResultsChart from \"../components/ResultsChart\";\nimport { TweetResult } from \"../components/TweetResult\";\nimport { useGameStore } from \"../state/game-store\";\nimport { useTrendStore } from \"../state/trends-store\";\n\nexport function ResultsText({\n  info,\n  title,\n  value,\n}: {\n  info: string;\n  title: string;\n  value: React.ReactNode;\n}) {\n  return (\n    <div\n      title={info}\n      className=\"h-full flex flex-col justify-end px-2 w-full sm:min-w-[150px] bg-dark-lake rounded p-2 py-4\"\n    >\n      <p className=\"flex justify-start color-inherit font-bold text-off-white text-xs\">\n        {title}\n      </p>\n      <div className=\"flex items-center gap-2 text-faded-gray justify-between\">\n        <p className=\"font-bold text-2xl\">{value}</p>\n      </div>\n    </div>\n  );\n}\nexport function ShareResultButton({ url }: { url: string }) {\n  return (\n    <button\n      onClick={() => {\n        const message = `Result URL copied to clipboard: ${url}`;\n        copyToClipboard(url, message);\n      }}\n      className=\"w-full sm:w-auto flex shadow-lg hover:shadow-violet-900 hover:cursor-pointer text-faded-gray hover:text-off-white bg-dark-lake flex-col items-center justify-center px-1 rounded hover:bg-white/10\"\n    >\n      <div className=\"h-4 w-4\">\n        <FontAwesomeIcon icon={faShare} />\n      </div>\n    </button>\n  );\n}\n\nexport function DailyStreak() {\n  return (\n    <ResultsText\n      info=\"How many days in a row have you played speedtyper.dev\"\n      title=\"daily streak\"\n      value={\n        <div className=\"flex items-center gap-2\">\n          2\n          <div className=\"hidden sm:flex flex-wrap items-center text-xs gap-1\">\n            {Array(3)\n              .fill(undefined)\n              .map((_, i) => {\n                const done = i > 0;\n                return (\n                  <div key={i} className=\"h-3 w-3\">\n                    {done ? (\n                      <div className=\"text-violet-400\">\n                        <FontAwesomeIcon icon={faCheckCircle} />{\" \"}\n                      </div>\n                    ) : (\n                      <div className=\"text-faded-gray\">\n                        <FontAwesomeIcon icon={faCircleXmark} />{\" \"}\n                      </div>\n                    )}\n                  </div>\n                );\n              })}\n          </div>\n        </div>\n      }\n    />\n  );\n}\n\nexport function ResultsContainer() {\n  const result = useGameStore((state) => state.myResult);\n  // TODO: Show loading indicator here\n  if (!result) return null;\n  const cpm = result.cpm;\n  const wpm = cpmToWPM(cpm);\n  const ms = result.timeMS;\n  const time = toHumanReadableTime(Math.floor(ms / 1000));\n  const mistakesCount = result.mistakes;\n  const accuracy = result.accuracy;\n  const base = window.location.origin;\n  const url = `${base}/results/${result.id}`;\n  return (\n    <div className=\"w-full flex flex-col\">\n      <div className=\"w-full flex flex-row gap-4 justify-between mb-2\">\n        <div className=\"flex flex-col gap-1 mx-2 w-full\">\n          <h3 className=\"px-2 flex color-inherit font-bold text-faded-gray text-sm items-center gap-2\">\n            <FontAwesomeIcon\n              className=\"h-5 w-5 flex items-center justify-center\"\n              icon={faSquarePollVertical}\n            />\n            result\n          </h3>\n          <div className=\"w-full grid grid-cols-2 sm:flex sm:flex-row gap-2\">\n            <ResultsText\n              info=\"words per minute typed in race\"\n              title=\"words per minute\"\n              value={wpm.toString()}\n            />\n            <ResultsText\n              info=\"Percentage of results on speedtyper.dev this race was faster than\"\n              title=\"global rank\"\n              value={`${result.percentile}%`}\n            />\n            <ResultsText\n              info=\"% correctly typed characters in race\"\n              title=\"accuracy\"\n              value={`${accuracy}%`}\n            />\n            <ResultsText\n              info=\"time it took to complete race\"\n              title=\"time\"\n              value={time}\n            />\n            <ResultsText\n              info=\"number of mistakes made during race\"\n              title=\"mistakes\"\n              value={mistakesCount.toString()}\n            />\n            {result.id && (\n              <div className=\"flex gap-2\">\n                <div className=\"flex sm:flex-col gap-2\">\n                  <div className=\"flex grow w-full\">\n                    <ShareResultButton url={url} />\n                  </div>\n                  <div className=\"flex grow w-full\">\n                    <TweetResult url={url} wpm={cpmToWPM(cpm)} />\n                  </div>\n                </div>\n                <Link href={url}>\n                  <a className=\"flex w-full grow hover:cursor-pointer text-faded-gray hover:text-off-white bg-dark-lake flex-col items-center justify-center rounded hover:bg-white/10 px-2\">\n                    <div className=\"h-3 w-3 \">\n                      <FontAwesomeIcon icon={faExternalLink} />\n                    </div>\n                  </a>\n                </Link>\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n      <div className=\"w-full flex flex-col sm:flex-row\">\n        {!result.user.isAnonymous ? <TrendsWPM currWPM={wpm} /> : null}\n        <ResultsChart />\n      </div>\n    </div>\n  );\n}\n\nfunction TrendsWPM({ currWPM }: { currWPM: number }) {\n  const { tenGameWPM, todayWPM, weekWPM, allTimeWPM } = useTrendStore();\n  const noResults = !tenGameWPM && !weekWPM && !todayWPM && !allTimeWPM;\n  if (noResults) {\n    return null;\n  }\n  return (\n    <div className=\"mx-2 sm:ml-2 sm:mr-0 flex flex-col sm:justify-start gap-1\">\n      <h3 className=\"px-2 flex color-inherit font-bold text-faded-gray text-sm items-center gap-2\">\n        <FontAwesomeIcon\n          className=\"h-5 w-5 flex items-center justify-center\"\n          icon={faArrowTrendUp}\n        />\n        average wpm\n      </h3>\n      <div className=\"flex flex-col gap-2 h-full sm:mb-2\">\n        {tenGameWPM ? (\n          <HistoryicalResult\n            title={\"last 10 games\"}\n            currWPM={currWPM}\n            wpm={tenGameWPM}\n          />\n        ) : null}\n        {todayWPM ? (\n          <HistoryicalResult title={\"today\"} currWPM={currWPM} wpm={todayWPM} />\n        ) : null}\n        {weekWPM ? (\n          <HistoryicalResult\n            title={\"last week\"}\n            currWPM={currWPM}\n            wpm={weekWPM}\n          />\n        ) : null}\n        {allTimeWPM ? (\n          <HistoryicalResult\n            title={\"all time\"}\n            currWPM={currWPM}\n            wpm={allTimeWPM}\n          />\n        ) : null}\n      </div>\n    </div>\n  );\n}\n\nfunction HistoryicalResult({\n  title,\n  currWPM,\n  wpm,\n}: {\n  title: string;\n  currWPM: number;\n  wpm: number;\n}) {\n  const percentageChange = (currWPM / wpm) * 100 - 100;\n  const popover =\n    percentageChange > 0\n      ? `This race was ${Math.abs(percentageChange).toFixed(\n          0\n        )}% faster than the average ${title} (wpm)`\n      : `This race was ${Math.abs(percentageChange).toFixed(\n          0\n        )}% slower than the average ${title} (wpm)`;\n  return (\n    <div\n      title={popover}\n      className=\"h-full flex flex-col justify-center px-2 sm:w-[150px] bg-dark-lake rounded p-2 gap-1\"\n    >\n      <p className=\"flex justify-start color-inherit tracking-wide font-semibold text-off-white text-sm\">\n        {title}\n      </p>\n      <div className=\"flex items-center gap-2 text-faded-gray justify-between\">\n        <p className=\"font-bold text-2xl flex\">\n          {wpm}{\" \"}\n          <span className=\"flex text-xs flex-col justify-end pl-1\">/wpm</span>\n        </p>\n        <div className=\"font-bold text-xs flex items-center gap-1\">\n          {percentageChange > 0 ? (\n            <div className=\"h-2 w-2 text-green-500\">\n              <FontAwesomeIcon icon={faArrowUp} />\n            </div>\n          ) : (\n            <div className=\"h-2 w-2 text-red-500\">\n              <FontAwesomeIcon icon={faArrowDown} />\n            </div>\n          )}\n          {Math.abs(percentageChange).toFixed(0)}%\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/hooks/useChallenge.ts",
    "content": "import { useEffect, useState } from \"react\";\nimport SocketLatest from \"../../../common/services/Socket\";\nimport { useCodeStore } from \"../state/code-store\";\nimport { useConnectionStore } from \"../state/connection-store\";\n\nexport interface ChallengeInfo {\n  code: string;\n  filePath: string;\n  language: string;\n  url: string;\n  projectName: string;\n  license: string;\n}\n\nexport function useChallenge(): ChallengeInfo {\n  const initialize = useCodeStore((state) => state.initialize);\n  const [challenge, setChallenge] = useState({\n    loaded: false,\n    code: \"\",\n    filePath: \"\",\n    language: \"\",\n    url: \"\",\n    projectName: \"\",\n    license: \"\",\n  });\n\n  const socket = useConnectionStore((s) => s.socket);\n\n  useEffect(() => {\n    socket?.subscribe(\"challenge_selected\", (_, challenge) => {\n      setChallenge({\n        loaded: true,\n        projectName: challenge.project.fullName,\n        url: challenge.url,\n        code: challenge.content,\n        language: challenge.project.language,\n        filePath: challenge.path,\n        license: challenge.project.licenseName,\n      });\n      initialize(challenge.content.replaceAll(\"\\t\", \"  \"));\n    });\n    socket?.subscribe(\"race_joined\", (_, raceData) => {\n      const { challenge } = raceData;\n      setChallenge({\n        loaded: true,\n        projectName: challenge.project.fullName,\n        url: challenge.url,\n        code: challenge.content,\n        language: challenge.project.language,\n        filePath: challenge.path,\n        license: challenge.project.licenseName,\n      });\n      initialize(challenge.content.replaceAll(\"\\t\", \"  \"));\n    });\n  }, [socket, initialize]);\n\n  return challenge;\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/hooks/useEndGame.ts",
    "content": "import { useEffect } from \"react\";\nimport { useIsPlaying } from \"../../../common/hooks/useIsPlaying\";\nimport { useCodeStore } from \"../state/code-store\";\nimport { useIsCompleted } from \"./useIsCompleted\";\n\nexport function useEndGame() {\n  const endGame = useCodeStore((state) => state.end);\n  const isCompleted = useIsCompleted();\n  const isPlaying = useIsPlaying();\n  useEffect(() => {\n    if (isCompleted && isPlaying) {\n      endGame();\n    }\n  }, [endGame, isPlaying, isCompleted]);\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/hooks/useFocusRef.ts",
    "content": "import { useCallback, useEffect, useState } from \"react\";\n\nexport function useFocusRef<T extends HTMLElement>(): [\n  (node: T) => void,\n  () => void\n] {\n  const [node, setNode] = useState<T>();\n  const triggerFocus = useCallback(() => {\n    if (!node) return;\n    node?.focus();\n  }, [node]);\n\n  useEffect(() => {\n    triggerFocus();\n  }, [triggerFocus]);\n\n  return [setNode, triggerFocus];\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/hooks/useGame.ts",
    "content": "import { useMemo } from \"react\";\nimport { Game } from \"../services/Game\";\nimport { useConnectionStore } from \"../state/connection-store\";\nimport { useInitialRaceIdQueryParam } from \"./useGameIdQueryParam\";\n\nexport const useGame = () => {\n  const raceIdQueryParam = useInitialRaceIdQueryParam();\n  const socket = useConnectionStore((s) => s.socket);\n  return useMemo(\n    () => socket && new Game(socket, raceIdQueryParam),\n    [socket, raceIdQueryParam]\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/hooks/useGameIdQueryParam.ts",
    "content": "import { useRouter } from \"next/router\";\n\nexport function useInitialRaceIdQueryParam(): string | undefined {\n  var router = useRouter();\n  var id = router.query[\"id\"];\n  return typeof id === \"string\" ? id : undefined;\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/hooks/useIsCompleted.ts",
    "content": "import { useCodeStore } from \"../state/code-store\";\n\nexport const useIsCompleted = () => {\n  useCodeStore((state) => state.correctIndex);\n  return useCodeStore((state) => state.isCompleted)();\n};\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/hooks/useNodeRect.ts",
    "content": "import { useEffect, useState } from \"react\";\n\ninterface IRect {\n  top: number;\n  left: number;\n}\n\nexport function useNodeRect<T extends HTMLElement>(\n  refreshValue: string\n): [IRect, (node: T) => void] {\n  const [node, setNode] = useState<T>();\n  const [rect, setRect] = useState({\n    top: 0,\n    left: 0,\n  });\n  const top = node?.offsetTop ?? 0;\n  const left = node?.offsetLeft ?? 0;\n  useEffect(() => {\n    if (!node) return;\n    setRect({\n      top,\n      left,\n    });\n  }, [node, top, left, refreshValue]);\n  return [rect, setNode];\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/hooks/useResetStateOnUnmount.ts",
    "content": "import { useEffect } from \"react\";\nimport { useCodeStore } from \"../state/code-store\";\n\nexport function useResetStateOnUnmount() {\n  const initialize = useCodeStore((state) => state.initialize);\n  useEffect(() => {\n    return () => {\n      initialize(\"\");\n    };\n  }, [initialize]);\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/services/Game.ts",
    "content": "import SocketLatest from \"../../../common/services/Socket\";\nimport { useUserStore } from \"../../../common/state/user-store\";\nimport { KeyStroke, useCodeStore } from \"../state/code-store\";\nimport { useConnectionStore } from \"../state/connection-store\";\nimport { RacePlayer, RaceResult, useGameStore } from \"../state/game-store\";\nimport { useSettingsStore } from \"../state/settings-store\";\n\nexport class Game {\n  onConnectHasRun: boolean;\n  onConnect(raceId?: string) {\n    this.socket.socket.on(\"connect\", () => {\n      if (this.onConnectHasRun) return;\n      console.log(\"registering all the things\");\n      this.listenForRaceJoined();\n      this.listenForRaceStarted();\n      this.listenForMemberJoined();\n      this.listenForCountdown();\n      this.listenForMemberLeft();\n      this.listenForProgressUpdated();\n      this.listenForRaceCompleted();\n      this.listenForRaceDoesNotExist();\n      this.listenForDisconnect();\n      this.socket.subscribe(\"challenge_selected\", (_) => {\n        useGameStore.setState((game) => {\n          return {\n            ...game,\n            results: {},\n            myResult: undefined,\n          };\n        });\n      });\n      if (!raceId) {\n        this.play();\n      } else if (this.id !== raceId) {\n        this.join(raceId);\n      }\n      this.onConnectHasRun = true;\n    });\n  }\n  constructor(private socket: SocketLatest, raceId?: string) {\n    this.initializeConnectedState(socket);\n    this.onConnectHasRun = false;\n    this.onConnect(raceId);\n  }\n\n  reconnect() {\n    this.socket.socket.disconnect();\n    this.socket.socket.connect();\n    this.join(this.id ?? \"\");\n  }\n\n  get id() {\n    return useGameStore.getState().id;\n  }\n\n  start() {\n    this.socket.emit(\"start_race\");\n  }\n\n  sendKeyStroke(keyStroke: KeyStroke) {\n    this.socket.emit(\"key_stroke\", keyStroke);\n  }\n\n  next() {\n    const connectedToValidRace =\n      useConnectionStore.getState().raceExistsInServer;\n    if (connectedToValidRace) {\n      const language = useSettingsStore.getState().languageSelected?.language;\n      const dto = { language };\n      this.socket.emit(\"refresh_challenge\", dto);\n    } else {\n      this.play();\n    }\n  }\n\n  join(id: string) {\n    this.socket.emit(\"join\", id);\n  }\n\n  play() {\n    const isPublic = useSettingsStore.getState().defaultIsPublic;\n    const language = useSettingsStore.getState().languageSelected?.language;\n    const dto = { language, isPublic };\n    this.socket.emit(\"play\", dto);\n  }\n\n  private listenForRaceStarted() {\n    this.socket.subscribe(\"race_started\", (_, time: string) => {\n      useCodeStore.setState((codeState) => ({\n        ...codeState,\n        startTime: new Date(time),\n      }));\n      useGameStore.setState((state) => ({\n        ...state,\n        countdown: undefined,\n      }));\n    });\n  }\n\n  private listenForRaceJoined() {\n    this.socket.subscribe(\"race_joined\", (_, race) => {\n      useConnectionStore.setState((s) => ({ ...s, isConnected: true }));\n      useGameStore.setState((game) => ({\n        ...game,\n        id: race.id,\n        owner: race.owner,\n        members: race.members,\n        countdown: undefined,\n      }));\n      useConnectionStore.setState((state) => ({\n        ...state,\n        raceExistsInServer: true,\n      }));\n      useSettingsStore.setState((s) => ({ ...s, raceIsPublic: race.isPublic }));\n    });\n  }\n\n  private listenForCountdown() {\n    this.socket.subscribe(\"countdown\", (_, countdown: number) => {\n      useGameStore.setState((state) => ({\n        ...state,\n        countdown,\n      }));\n    });\n  }\n\n  private listenForMemberJoined() {\n    this.socket.subscribe(\"member_joined\", (_, member: RacePlayer) => {\n      console.log(\"member_joined\", member);\n      this.updateMemberInState(member);\n    });\n  }\n\n  private listenForMemberLeft() {\n    this.socket.subscribe(\"member_left\", (_, { member, owner }) => {\n      useGameStore.setState((game) => {\n        const members = { ...game.members };\n        delete members[member];\n        return {\n          ...game,\n          owner,\n          members,\n        };\n      });\n    });\n  }\n\n  private listenForProgressUpdated() {\n    this.socket.subscribe(\"progress_updated\", (_, member: RacePlayer) => {\n      this.updateMemberInState(member);\n    });\n  }\n\n  private listenForRaceCompleted() {\n    this.socket.subscribe(\"race_completed\", (_, result: RaceResult) => {\n      console.log(result)\n      const userId = useUserStore.getState().id;\n      const isMyResult = userId === result.user.id;\n      useGameStore.setState((game) => {\n        const results = {\n          ...game.results,\n          [result.user.id]: result,\n        };\n        return {\n          ...game,\n          results,\n          myResult: isMyResult ? result : game.myResult,\n        };\n      });\n    });\n  }\n\n  private updateMemberInState(member: RacePlayer) {\n    useGameStore.setState((game) => {\n      const members = { ...game.members };\n      members[member.id] = member;\n      return {\n        ...game,\n        members,\n      };\n    });\n  }\n\n  private listenForRaceDoesNotExist() {\n    this.socket.subscribe(\"race_does_not_exist\", (_, id) => {\n      console.log(\"race_does_not_exist\", id);\n      useConnectionStore.setState((state) => ({\n        ...state,\n        raceExistsInServer: false,\n      }));\n    });\n  }\n\n  private listenForDisconnect() {\n    this.socket.subscribe(\"disconnect\", (_, _data) => {\n      useGameStore.setState((game) => {\n        return {\n          ...game,\n          connected: false,\n        };\n      });\n    });\n  }\n  private initializeConnectedState(socket: SocketLatest) {\n    const connected = socket.socket.connected;\n    useGameStore.setState((game) => {\n      return {\n        ...game,\n        connected,\n        game: this,\n      };\n    });\n  }\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/state/code-store.ts",
    "content": "import create from \"zustand\";\nimport { cpmToWPM } from \"../../../common/utils/cpmToWPM\";\n\nexport interface KeyStroke {\n  key: string;\n  timestamp: number;\n  index: number;\n  correct: boolean;\n}\n\ninterface CodeState {\n  // Match state\n  startTime?: Date;\n  endTime?: Date;\n  keyStrokes: KeyStroke[];\n  // TODO: Can we move match state to GameState\n  // perhaps start and end time can be set from backend events\n  start: () => void;\n  end: () => void;\n  isPlaying: () => boolean;\n  getChartWPM: () => number[];\n  _getValidKeyStrokes: () => KeyStroke[];\n  _getIncorrectKeyStrokes: () => KeyStroke[];\n\n  // Code rendering state\n  code: string;\n  index: number;\n  correctIndex: number;\n  correctChars: () => string;\n  incorrectChars: () => string;\n  currentChar: () => string;\n  untypedChars: () => string;\n  initialize: (code: string) => void;\n  handleBackspace: () => void;\n  handleKeyPress: (keyStroke: KeyStroke) => void;\n  keyPressFactory: (key: string) => KeyStroke;\n  isCompleted: () => boolean;\n  correctInput: () => string;\n  // private helper methods\n  _getBackspaceOffset: () => number;\n  _getForwardOffset: () => number;\n  _allCharsTyped: () => boolean;\n}\n\n// There are 3 separate parts of logic in this store\n// 1. Code rendering logic which is necessary to render the code strings\n// 2. Match logic which concerns itself with maintaining the match\n// 3. Results logic which shows data after the race\n// Match logic depends on code rendering logic.\n// Results logic depends on Match logic.\n// Perhaps Match and results logic could be split into a separate store\n// But this was a bit simpler as we can just push to keyStrokes from the handleKeyPress method\n// The other option would be to pass in the saveKeyStroke method into the handleKeyPress method\n\nexport const useCodeStore = create<CodeState>((set, get) => ({\n  // RESULTS logic\n  getChartWPM: () => {\n    const startTime = get().startTime?.getTime();\n    if (!startTime) {\n      return [];\n    }\n    const wpm = [];\n    let count = 0;\n    let seconds = 1;\n    const validKeyStrokes = get()._getValidKeyStrokes();\n\n    for (let i = 0; i < validKeyStrokes.length; i++) {\n      const keyStroke = validKeyStrokes[i];\n      const breaktime = startTime + seconds * 1000;\n      const isLastKeyStroke = i === validKeyStrokes.length - 1;\n      const diffMS = keyStroke.timestamp - breaktime;\n      const diffSeconds = diffMS / 1000;\n      if (keyStroke.timestamp > breaktime) {\n        // If more than a second has passed since the last WPM calculation\n        // we push another WPM calculation to the array\n        const cpm = Math.floor((60 * count) / (seconds + diffSeconds));\n        wpm.push(cpmToWPM(cpm));\n        seconds++;\n      } else if (isLastKeyStroke) {\n        // if this is the last keystroke in the valid keystrokes array\n        // we push the last uncounted characters as CPM to the array\n        // even if we have not passed a full second\n        const cpm = Math.floor((60 * count) / (seconds + diffSeconds));\n        wpm.push(cpmToWPM(cpm));\n        seconds++;\n      }\n      count++;\n    }\n    return wpm;\n  },\n  // BUG: this suffers from the same bug as backend used to suffer from.\n  // It's not as bad because it's used only to render the chart\n  // See: RacePlayer.validKeyStrokes()\n  // we should completely remove this method and rely on the backend to get the valid keystrokes\n  // this way we only need the calculation in one place\n  _getValidKeyStrokes: () => {\n    const keyStrokes = get().keyStrokes;\n    const validKeyStrokes = Object.values(\n      Object.fromEntries(\n        keyStrokes\n          .filter((stroke) => stroke.correct)\n          .map((keyStroke) => [keyStroke.index, keyStroke])\n      )\n    );\n    return validKeyStrokes;\n  },\n  _getIncorrectKeyStrokes: () => {\n    const keyStrokes = get().keyStrokes;\n    return keyStrokes.filter((stroke) => !stroke.correct);\n  },\n  // MATCH logic\n  keyStrokes: [],\n  incorrectKeyStrokes: [],\n  start: () => {\n    set((state) => {\n      return { ...state, startTime: new Date() };\n    });\n  },\n  end: () => {\n    set((state) => {\n      return { ...state, endTime: new Date() };\n    });\n  },\n  isPlaying: () => {\n    return !!get().startTime && !get().endTime;\n  },\n\n  // CODE rendering logic\n  code: \"\",\n  index: 0,\n  correctIndex: 0,\n  initialize: (code: string) => {\n    set((state) => ({\n      ...state,\n      code,\n      index: 0,\n      correctIndex: 0,\n      startTime: undefined,\n      endTime: undefined,\n      chars: [],\n      keyStrokes: [],\n    }));\n  },\n  handleBackspace: () => {\n    set((state) => {\n      const offset = state._getBackspaceOffset();\n      const index = Math.max(state.index - offset, 0);\n      const correctIndex = Math.min(index, state.correctIndex);\n      return { ...state, index, correctIndex };\n    });\n  },\n  keyPressFactory: (unparsedKey: string) => {\n    const key = parseKey(unparsedKey);\n    const offset = get()._getForwardOffset();\n    const index = Math.min(offset + get().index, get().code.length);\n    // BUG: \"correct\" below is a bug\n    // if index i-1 is not correct this calculation can evaluate index i to be correct\n    // this is addressed in the backend but should also be fixed here...\n    // or perhaps removed compleptely and we can rely on the backend calculation with appropriate text coverage\n    const correct =\n      get().index === get().correctIndex && key === get().code[get().index];\n    const keyStroke = {\n      key,\n      index,\n      timestamp: new Date().getTime(),\n      correct,\n    };\n    return keyStroke;\n  },\n  handleKeyPress: (keyStroke: KeyStroke) => {\n    set((state) => {\n      if (isSkippable(keyStroke.key)) return state;\n      if (state._allCharsTyped()) return state;\n      const index = keyStroke.index;\n      const correctIndex = !keyStroke.correct ? state.correctIndex : index;\n      state.keyStrokes.push(keyStroke);\n      return { ...state, index, correctIndex };\n    });\n  },\n  correctChars: () => {\n    return get().code.slice(0, get().correctIndex);\n  },\n  currentChar: () => {\n    if (get().code.length <= get().index) {\n      return \"\";\n    }\n    return get().code[get().index];\n  },\n  incorrectChars: () => {\n    if (get().code.length <= get().index) {\n      return get().code.slice(get().correctIndex);\n    }\n    return get().code.slice(get().correctIndex, get().index);\n  },\n  untypedChars: () => {\n    if (get().code.length <= get().index) {\n      return \"\";\n    }\n    return get().code.slice(get().index + 1);\n  },\n  correctInput: () => {\n    return get().code.substring(0, get().correctIndex);\n  },\n  isCompleted: () => {\n    return get().correctIndex > 0 && get().correctIndex === get().code.length;\n  },\n  _allCharsTyped: () => {\n    return get().index === get().code.length;\n  },\n  _getForwardOffset: () => {\n    let offset = 1;\n\n    // if current char is a line break \\n:\n    if (isLineBreak(get().currentChar())) {\n      // skip repeated spaces\n      while (get().code[get().index + offset] === \" \") {\n        offset++;\n      }\n    }\n\n    // TODO: move this logic to parsing in order to remove too many spaces\n    // if next char and next next char are going to be a space:\n    // else if (\n    //   isSpace(get().code[get().index + 1]) &&\n    //   isSpace(get().code[get().index + 2])\n    // ) {\n    //   // skip repeated spaces\n    //   while (get().code[get().index + offset] === \" \") {\n    //     offset++;\n    //   }\n    // }\n\n    return offset;\n  },\n  _getBackspaceOffset: () => {\n    let offset = 1;\n    // if previous char and previous previous char is a space:\n    if (\n      get().code[get().index - 1] === \" \" &&\n      get().code[get().index - 2] === \" \"\n    ) {\n      while (get().code[get().index - offset] === \" \") {\n        offset++;\n      }\n    }\n    return offset;\n  },\n}));\n\nexport enum TrackedKeys {\n  Backspace = \"Backspace\",\n}\n\nfunction isLineBreak(key: string) {\n  return key === \"\\n\";\n}\n\nfunction parseKey(key: string) {\n  switch (key) {\n    case \"Enter\":\n      return \"\\n\";\n    default:\n      return key;\n  }\n}\n\nexport function isSkippable(key: string) {\n  switch (key) {\n    case \"Shift\":\n    case \"OS\":\n    case \"Control\":\n      return true;\n    default:\n      return false;\n  }\n}\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/state/connection-store.ts",
    "content": "import { useCallback, useEffect } from \"react\";\nimport create from \"zustand\";\nimport { fetchRaceStatus } from \"../../../common/api/races\";\nimport SocketLatest from \"../../../common/services/Socket\";\nimport { updateUserInStore } from \"../../../common/state/user-store\";\nimport { useGameStore } from \"./game-store\";\n\nexport interface ConnectionState {\n  isConnected: boolean;\n  raceExistsInServer: boolean;\n  alreadyPlaying: boolean;\n  socket?: SocketLatest;\n}\n\nexport const useConnectionStore = create<ConnectionState>((_set, _get) => ({\n  isConnected: true,\n  raceExistsInServer: true,\n  alreadyPlaying: false,\n}));\n\nexport const refreshRaceState = async (raceId: string) => {\n  fetchRaceStatus(raceId).then(({ ok }) => {\n    useConnectionStore.setState((state) => ({\n      ...state,\n      raceExistsInServer: ok,\n    }));\n  });\n};\n\nexport const useRefreshRaceStatus = () => {\n  const raceId = useGameStore((state) => state.id);\n  useEffect(() => {\n    if (raceId) {\n      fetchRaceStatus(raceId);\n    }\n  }, [raceId]);\n};\n\nexport const setRaceExists = () => {\n  useConnectionStore.setState((state) => ({\n    ...state,\n    raceExistsInServer: true,\n  }));\n};\n\nexport const useConnectionManager = () => {\n  const socket = useConnectionStore((s) => s.socket);\n  const isConnected = useConnectionStore((state) => state.isConnected);\n  const raceId = useGameStore((state) => state.id);\n  const onConnect = useCallback(\n    (_err: string | null, _msg: string) => {\n      useConnectionStore.setState((state) => ({ ...state, isConnected: true }));\n      if (raceId && !isConnected) {\n        fetchRaceStatus(raceId).then(({ ok }) => {\n          const currentRaceID = useGameStore.getState().id;\n          const raceExistsInServer = raceId !== currentRaceID || ok;\n          useConnectionStore.setState((state) => ({\n            ...state,\n            raceExistsInServer,\n          }));\n        });\n        updateUserInStore();\n      }\n    },\n    [isConnected, raceId]\n  );\n  useEffect(() => {\n    const onAlreadyPlaying = () => {\n      console.log(\"alreadyPlaying received\");\n      useConnectionStore.setState((state) => ({\n        ...state,\n        alreadyPlaying: true,\n      }));\n    };\n    const onDisconnect = (_err: string | null, _msg: string) => {\n      useConnectionStore.setState((state) => ({\n        ...state,\n        isConnected: false,\n      }));\n    };\n    socket?.subscribe(\"already_playing\", onAlreadyPlaying);\n    socket?.subscribe(\"connect_error\", onDisconnect);\n    socket?.subscribe(\"disconnect\", onDisconnect);\n    socket?.subscribe(\"connect\", onConnect);\n  }, [socket, onConnect]);\n};\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/state/game-store.ts",
    "content": "import create from \"zustand\";\nimport { User, useUserStore } from \"../../../common/state/user-store\";\nimport { Game } from \"../services/Game\";\nimport { useCodeStore } from \"./code-store\";\nimport { useHasOpenModal } from \"./settings-store\";\n\nexport interface GameState {\n  id?: string;\n  owner?: string;\n  members: Record<string, RacePlayer>;\n  results: Record<string, RaceResult>;\n  myResult?: RaceResult;\n  countdown?: number;\n  game?: Game;\n}\n\nexport interface RacePlayer {\n  id: string;\n  username: string;\n  progress: number;\n  recentlyTypedLiteral: string;\n}\n\nexport interface RaceResult {\n  id: string;\n  raceId: string;\n  timeMS: number;\n  cpm: number;\n  mistakes: number;\n  accuracy: number;\n  createdAt: Date;\n  user: User;\n  userId: string;\n  percentile?: number;\n}\n\nexport const useGameStore = create<GameState>((_set, _get) => ({\n  members: {},\n  results: {},\n}));\n\nexport const useCanType = () => {\n  const hasOpenModal = useHasOpenModal();\n  const game = useGameStore((s) => s.game);\n  const isMultiplayer = useIsMultiplayer();\n  const hasStartTime = useCodeStore((state) => state.startTime);\n  return (\n    (!hasOpenModal && !!game && !isMultiplayer) ||\n    (!hasOpenModal && hasStartTime)\n  );\n};\n\nexport const useIsMultiplayer = () => {\n  const members = useGameStore((state) => state.members);\n  return Object.values(members).length > 1;\n};\n\nexport const useIsOwner = () => {\n  const userId = useUserStore((state) => state.id);\n  const owner = useGameStore((state) => state.owner);\n  return userId === owner;\n};\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/state/settings-store.ts",
    "content": "import create from \"zustand\";\nimport { getExperimentalServerUrl } from \"../../../common/utils/getServerUrl\";\n\nexport interface LanguageDTO {\n  language: string;\n  name: string;\n}\n\nexport interface SettingsState {\n  settingsModalIsOpen: boolean;\n  languageModalIsOpen: boolean;\n  leaderboardModalIsOpen: boolean;\n  profileModalIsOpen: boolean;\n  projectModalIsOpen: boolean;\n  publicRacesModalIsOpen: boolean;\n  languageSelected: LanguageDTO | null;\n  smoothCaret: boolean;\n  syntaxHighlighting: boolean;\n  raceIsPublic: boolean;\n  defaultIsPublic: boolean;\n}\n\nconst SYNTAX_HIGHLIGHTING_KEY = \"syntaxHighlighting\";\n\nconst SMOOTH_CARET_KEY = \"smoothCaret\";\n\nconst DEFAULT_RACE_IS_PUBLIC_KEY = \"defaultRaceIsPublic2\";\n\nconst LANGUAGE_KEY = \"language\";\n\nfunction getInitialToggleStateFromLocalStorage(\n  key: string,\n  defaultToggleValue: boolean\n): boolean {\n  if (typeof document !== \"undefined\" && window) {\n    let toggleStateStr = localStorage.getItem(key);\n    if (!toggleStateStr) {\n      localStorage.setItem(key, defaultToggleValue.toString());\n      toggleStateStr = defaultToggleValue.toString();\n    }\n    return toggleStateStr === \"true\" ?? false;\n  }\n  return defaultToggleValue;\n}\n\nfunction getInitialLanguageFromLocalStorage(key: string): LanguageDTO | null {\n  if (typeof document !== \"undefined\" && window) {\n    let languageStr = localStorage.getItem(key) ?? \"\";\n    let lang;\n    try {\n      lang = JSON.parse(languageStr);\n    } catch (e) {}\n    if (!lang?.language || !lang?.name) {\n      return null;\n    }\n    return lang;\n  }\n  return null;\n}\n\nexport const useSettingsStore = create<SettingsState>((_set, _get) => ({\n  settingsModalIsOpen: false,\n  languageModalIsOpen: false,\n  leaderboardModalIsOpen: false,\n  profileModalIsOpen: false,\n  publicRacesModalIsOpen: false,\n  projectModalIsOpen: false,\n  smoothCaret: getInitialToggleStateFromLocalStorage(SMOOTH_CARET_KEY, false),\n  syntaxHighlighting: getInitialToggleStateFromLocalStorage(\n    SYNTAX_HIGHLIGHTING_KEY,\n    false\n  ),\n  raceIsPublic: false,\n  defaultIsPublic: getInitialToggleStateFromLocalStorage(\n    DEFAULT_RACE_IS_PUBLIC_KEY,\n    false\n  ),\n  languageSelected: getInitialLanguageFromLocalStorage(LANGUAGE_KEY),\n}));\n\nexport const setCaretType = (caretType: \"smooth\" | \"block\") => {\n  const smoothCaret = caretType === \"smooth\";\n  localStorage.setItem(SMOOTH_CARET_KEY, smoothCaret.toString());\n  useSettingsStore.setState((state) => ({ ...state, smoothCaret }));\n};\n\nexport const setLanguage = (language: LanguageDTO | null) => {\n  let stored = \"\";\n  if (language) {\n    stored = JSON.stringify(language);\n  }\n  localStorage.setItem(LANGUAGE_KEY, stored);\n  useSettingsStore.setState((state) => ({\n    ...state,\n    languageSelected: language,\n  }));\n};\n\nexport const toggleDefaultRaceIsPublic = () => {\n  const booleanStrValue = localStorage.getItem(DEFAULT_RACE_IS_PUBLIC_KEY);\n  let defaultIsPublic = booleanStrValue === \"true\";\n  defaultIsPublic = !defaultIsPublic;\n  localStorage.setItem(DEFAULT_RACE_IS_PUBLIC_KEY, defaultIsPublic.toString());\n  useSettingsStore.setState((state) => ({ ...state, defaultIsPublic }));\n};\n\nexport const toggleSyntaxHighlightning = () => {\n  const syntaxHighlightingStr = localStorage.getItem(SYNTAX_HIGHLIGHTING_KEY);\n  let syntaxHighlighting = syntaxHighlightingStr === \"true\";\n  syntaxHighlighting = !syntaxHighlighting;\n  localStorage.setItem(SYNTAX_HIGHLIGHTING_KEY, syntaxHighlighting.toString());\n  useSettingsStore.setState((state) => ({ ...state, syntaxHighlighting }));\n};\n\nexport const openSettingsModal = () => {\n  if (useSettingsStore.getState().profileModalIsOpen) return;\n  if (useSettingsStore.getState().leaderboardModalIsOpen) return;\n  useSettingsStore.setState((s) => ({\n    ...s,\n    settingsModalIsOpen: true,\n  }));\n};\n\nexport const openLanguageModal = () => {\n  if (useSettingsStore.getState().profileModalIsOpen) return;\n  if (useSettingsStore.getState().leaderboardModalIsOpen) return;\n  useSettingsStore.setState((s) => ({\n    ...s,\n    languageModalIsOpen: true,\n  }));\n};\n\nexport const openProfileModal = () => {\n  useSettingsStore.setState((s) => ({\n    ...s,\n    profileModalIsOpen: true,\n  }));\n};\n\nexport const openLeaderboardModal = () => {\n  if (useSettingsStore.getState().settingsModalIsOpen) return;\n  useSettingsStore.setState((s) => ({\n    ...s,\n    leaderboardModalIsOpen: true,\n  }));\n};\n\nexport const openPublicRacesModal = () => {\n  if (useSettingsStore.getState().settingsModalIsOpen) return;\n  useSettingsStore.setState((s) => ({\n    ...s,\n    publicRacesModalIsOpen: true,\n  }));\n};\n\nexport const openProjectModal = () => {\n  useSettingsStore.setState((s) => ({\n    ...s,\n    projectModalIsOpen: true,\n  }));\n};\n\nexport const useHasOpenModal = () => {\n  const leaderboardModalIsOpen = useSettingsStore(\n    (s) => s.leaderboardModalIsOpen\n  );\n  const settingsModalIsOpen = useSettingsStore((s) => s.settingsModalIsOpen);\n  return leaderboardModalIsOpen || settingsModalIsOpen;\n};\n\nexport const closeModals = () => {\n  useSettingsStore.setState((s) => ({\n    ...s,\n    settingsModalIsOpen: false,\n    leaderboardModalIsOpen: false,\n    profileModalIsOpen: false,\n    publicRacesModalIsOpen: false,\n    languageModalIsOpen: false,\n    projectModalIsOpen: false,\n  }));\n};\n\nexport const toggleRaceIsPublic = () => {\n  const baseUrl = getExperimentalServerUrl();\n  const url = baseUrl + \"/api/races/online\";\n  fetch(url, {\n    method: \"POST\",\n    credentials: \"include\",\n  }).then((res) =>\n    res.json().then(({ isPublic: raceIsPublic }) => {\n      useSettingsStore.setState((s) => ({ ...s, raceIsPublic }));\n    })\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/state/trends-store.ts",
    "content": "import create from \"zustand\";\nimport { useUserStore } from \"../../../common/state/user-store\";\nimport { cpmToWPM } from \"../../../common/utils/cpmToWPM\";\nimport { getExperimentalServerUrl } from \"../../../common/utils/getServerUrl\";\n\nexport interface TrendsState {\n  tenGameWPM: number | null;\n  todayWPM: number | null;\n  weekWPM: number | null;\n  allTimeWPM: number | null;\n}\n\nexport const useTrendStore = create<TrendsState>((_set, _get) => ({\n  tenGameWPM: null,\n  todayWPM: null,\n  weekWPM: null,\n  allTimeWPM: null,\n}));\n\nexport const refreshTrends = () => {\n  const isAnonymous = useUserStore.getState().isAnonymous;\n  if (isAnonymous) {\n    return;\n  }\n  const baseUrl = getExperimentalServerUrl();\n  const url = baseUrl + \"/api/results/stats\";\n  fetch(url, {\n    credentials: \"include\",\n  }).then((res) =>\n    res.json().then(({ cpmLast10, cpmToday, cpmLastWeek, cpmAllTime }) => {\n      useTrendStore.setState(() => ({\n        tenGameWPM: cpmToWPM(cpmLast10),\n        todayWPM: cpmToWPM(cpmToday),\n        weekWPM: cpmToWPM(cpmLastWeek),\n        allTimeWPM: cpmToWPM(cpmAllTime),\n      }));\n    })\n  );\n};\n"
  },
  {
    "path": "packages/webapp-next/next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: false,\n  swcMinify: true,\n  images: {\n    domains: [\"avatars.githubusercontent.com\"],\n  },\n  publicRuntimeConfig: {\n    isProduction: process.env.NODE_ENV === \"production\",\n    title: \"speedtyper.dev | Typing Practice For Programmers | Typing races\",\n    description:\n      \"speedtyper.dev is a typing application for programmers. Battle against other developers by typing challenges from real open source projects as fast as possible. Practice your typing to become a faster and more accurate programmer by practicing typing actual code sequences and symbols that are hard to find on the keyboard.\",\n    lastBuilt: Date.now(),\n    siteRoot:\n      process.env.NODE_ENV === \"production\"\n        ? \"https://speedtyper.dev\"\n        : \"http://localhost:3001\",\n    serverUrl:\n      process.env.NODE_ENV === \"production\"\n        ? \"https://v2.speedtyper.dev\"\n        : \"http://localhost:5001\",\n    experimentalServerUrl:\n      process.env.NODE_ENV === \"production\"\n        ? \"https://v3.speedtyper.dev\"\n        : \"http://localhost:1337\",\n  },\n};\n\nmodule.exports = nextConfig;\n"
  },
  {
    "path": "packages/webapp-next/package.json",
    "content": "{\n  \"name\": \"webapp2\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"packageManager\": \"yarn@1.22.21\",\n  \"engines\": {\n    \"yarn\": \"^1.22.21\"\n  },\n  \"scripts\": {\n    \"dev\": \"next dev -p 3001\",\n    \"build:css\": \"postcss tailwind.css -o tailwind.min.css\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"export\": \"next export\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"@fortawesome/fontawesome-svg-core\": \"^6.3.0\",\n    \"@fortawesome/free-brands-svg-icons\": \"^6.4.0\",\n    \"@fortawesome/free-solid-svg-icons\": \"^6.3.0\",\n    \"@fortawesome/react-fontawesome\": \"^0.2.0\",\n    \"@headlessui/react\": \"^1.7.11\",\n    \"chart.js\": \"^3.9.1\",\n    \"date-fns\": \"^2.29.2\",\n    \"framer-motion\": \"^7.5.1\",\n    \"highlight.js\": \"^11.6.0\",\n    \"ky\": \"^0.31.3\",\n    \"ky-universal\": \"^0.10.1\",\n    \"next\": \"12.2.5\",\n    \"nextjs-progressbar\": \"^0.0.15\",\n    \"react\": \"18.2.0\",\n    \"react-dom\": \"18.2.0\",\n    \"react-switch\": \"^7.0.0\",\n    \"react-toastify\": \"^9.0.8\",\n    \"sharp\": \"^0.31.1\",\n    \"socket.io-client\": \"^2\",\n    \"socketio-latest\": \"npm:socket.io-client\",\n    \"swr\": \"^2.0.3\",\n    \"zustand\": \"^4.1.1\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"18.7.14\",\n    \"@types/react\": \"18.0.18\",\n    \"@types/react-dom\": \"18.0.6\",\n    \"@types/socket.io-client\": \"^1.4.36\",\n    \"autoprefixer\": \"^10.4.8\",\n    \"eslint\": \"8.23.0\",\n    \"eslint-config-next\": \"12.2.5\",\n    \"eslint-config-prettier\": \"^8.5.0\",\n    \"eslint-plugin-prettier\": \"^4.2.1\",\n    \"postcss\": \"^8.4.16\",\n    \"postcss-cli\": \"^10.0.0\",\n    \"prettier\": \"2.7.1\",\n    \"tailwindcss\": \"^3.1.8\",\n    \"typescript\": \"4.8.2\"\n  }\n}\n"
  },
  {
    "path": "packages/webapp-next/pages/404.tsx",
    "content": "import React from \"react\";\n\nconst NotFoundPage = () => (\n  <div>\n    <h1 className=\"text-off-white\">\n      404 - Oh no! We could not find that page! :(\n    </h1>\n  </div>\n);\n\nexport default NotFoundPage;\n"
  },
  {
    "path": "packages/webapp-next/pages/_app.tsx",
    "content": "import \"../styles/globals.css\";\n\nimport type { AppProps } from \"next/app\";\nimport Head from \"next/head\";\nimport NextNProgress from \"nextjs-progressbar\";\nimport { Layout } from \"../common/components/Layout\";\nimport { Stream } from \"../components/Stream\";\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\nimport { faTiktok } from \"@fortawesome/free-brands-svg-icons\";\nimport { useIsPlaying } from \"../common/hooks/useIsPlaying\";\nimport { IconProp } from \"@fortawesome/fontawesome-svg-core\";\n\nfunction MyApp({ Component, pageProps }: AppProps) {\n  const title = \"SpeedTyper.dev | Typing practice for programmers\";\n  const isPlaying = useIsPlaying();\n  const isPlayingCss = isPlaying ? \"hidden\" : \"\";\n  return (\n    <>\n      <div\n        style={{\n          height: \"100vh\",\n          display: \"grid\",\n          gridTemplateRows: \"auto 1fr auto\",\n        }}\n      >\n        <Head>\n          <title>{title}</title>\n          <meta property=\"og:title\" content={title} />\n          <meta charSet=\"UTF-8\" />\n          <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n          <meta\n            name=\"keywords\"\n            content=\"typing practice, coding speed, programming speed, code faster, speed coding, coding practice, coding tutor, programming tutor, coding games, learn programming, learn coding, typing tutor, typing competition for programmers, typing challenges for programmers, programming competitions, coding competitions, programming challenges, open source coding competitions\"\n          />\n          <meta\n            name=\"description\"\n            content=\"speedtyper.dev is a typing application for programmers. Battle against other developers by typing challenges from real open source projects as fast as possible. Practice your typing to become a faster and more accurate programmer by practicing typing actual code sequences and symbols that are hard to find on the keyboard.\"\n          />\n          <meta\n            property=\"og:description\"\n            content=\"speedtyper.dev is a typing application for programmers. Battle against other developers by typing challenges from real open source projects as fast as possible. Practice your typing to become a faster and more accurate programmer by practicing typing actual code sequences and symbols that are hard to find on the keyboard.\"\n          />\n          <meta property=\"og:image:type\" content=\"image/png\" />\n          <meta property=\"og:image:width\" content=\"1024\" />\n          <meta property=\"og:image:height\" content=\"1024\" />\n          <meta property=\"og:type\" content=\"website\" />\n          <meta property=\"og:url\" content=\"https://speedtyper.dev\" />\n          <link rel=\"icon\" href=\"/favicon.ico\" />\n          <script\n            async\n            src=\"https://umami-production-7f33.up.railway.app/script.js\"\n            data-website-id=\"ed902c85-74a2-427f-a554-520fdf0925e5\"\n          ></script>\n        </Head>\n        <NextNProgress\n          options={{ showSpinner: false }}\n          color=\"#d6bbfa\"\n          height={2}\n        />\n        <Layout>\n          <Component {...pageProps} />\n        </Layout>\n        <Stream />\n      </div>\n\n      <div className=\"absolute bottom-0 mb-12 sm:mb-24 flex w-full justify-center\">\n        <a\n          data-umami-event=\"TikTok Banner - Click\"\n          href=\"https://www.tiktok.com/tag/speedtyperdev\"\n          target=\"_blank\"\n          className={`${isPlayingCss} border border-gray-400 rounded-lg p-4 flex gap-2 items-center hover:border-purple-400 transition-all hover:text-transparent hover:bg-clip-text hover:bg-gradient-to-r hover:from-purple-400 hover:to-fuchsia-700`}\n          rel=\"noreferrer\"\n        >\n          <FontAwesomeIcon\n            className=\"w-5 text-lg text-white\"\n            icon={faTiktok as IconProp}\n          />\n          <span className=\"text-white\">create & watch</span>\n          <span className=\"md:font-semibold hidden sm:inline\">\n            #speedtyperdev\n          </span>\n          <span className=\"hidden sm:inline text-white\">TikToks</span>\n        </a>\n      </div>\n    </>\n  );\n}\n\nexport default MyApp;\n"
  },
  {
    "path": "packages/webapp-next/pages/_document.tsx",
    "content": "import Document, { Html, Head, Main, NextScript } from \"next/document\";\n\nclass MyDocument extends Document {\n  static async getInitialProps(ctx: any) {\n    const initialProps = await Document.getInitialProps(ctx);\n    return { ...initialProps };\n  }\n\n  render() {\n    return (\n      <Html>\n        <Head>\n          <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n          <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" />\n          <link\n            href=\"https://fonts.googleapis.com/css2?family=Fira+Code&display=swap\"\n            rel=\"stylesheet\"\n          />\n        </Head>\n        <body>\n          <script>0</script>\n          <Main />\n          <NextScript />\n        </body>\n      </Html>\n    );\n  }\n}\n\nexport default MyDocument;\n"
  },
  {
    "path": "packages/webapp-next/pages/index.tsx",
    "content": "import { AnimatePresence, motion } from \"framer-motion\";\nimport \"react-toastify/dist/ReactToastify.css\";\nimport { ToastContainer } from \"react-toastify\";\nimport { useSocket } from \"../common/hooks/useSocket\";\nimport { Keys, useKeyMap } from \"../hooks/useKeyMap\";\nimport { CodeTypingContainer } from \"../modules/play2/containers/CodeTypingContainer\";\nimport { useGame } from \"../modules/play2/hooks/useGame\";\nimport { useIsCompleted } from \"../modules/play2/hooks/useIsCompleted\";\nimport { ResultsContainer } from \"../modules/play2/containers/ResultsContainer\";\nimport { useUser } from \"../common/api/user\";\nimport { useChallenge } from \"../modules/play2/hooks/useChallenge\";\nimport { useEndGame } from \"../modules/play2/hooks/useEndGame\";\nimport { useResetStateOnUnmount } from \"../modules/play2/hooks/useResetStateOnUnmount\";\nimport { PlayFooter } from \"../modules/play2/components/play-footer/PlayFooter\";\nimport { PlayHeader } from \"../modules/play2/components/play-header/PlayHeader\";\nimport { useInitializeUserStore } from \"../common/state/user-store\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { useConnectionManager } from \"../modules/play2/state/connection-store\";\nimport {\n  closeModals,\n  useHasOpenModal,\n  useSettingsStore,\n} from \"../modules/play2/state/settings-store\";\nimport { useIsPlaying } from \"../common/hooks/useIsPlaying\";\nimport { refreshTrends } from \"../modules/play2/state/trends-store\";\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\nimport { faLock } from \"@fortawesome/free-solid-svg-icons\";\n\nexport const config = { runtime: \"experimental-edge\" };\n\nfunction Play2Page() {\n  const user = useUser();\n  useInitializeUserStore(user);\n  const isCompleted = useIsCompleted();\n  const isPlaying = useIsPlaying();\n  useSocket();\n  useConnectionManager();\n  const hasOpenModal = useHasOpenModal();\n  const game = useGame();\n  const challenge = useChallenge();\n  const [isThrottled, setIsThrottled] = useState(false);\n  const { capsLockActive } = useKeyMap(\n    true,\n    Keys.Tab,\n    useCallback(() => {\n      if (isThrottled) return;\n      if (hasOpenModal) return;\n      game?.next();\n      setIsThrottled(true);\n      setTimeout(() => {\n        setIsThrottled(false);\n      }, 2000);\n    }, [isThrottled, hasOpenModal, game])\n  );\n  useSettingsStore((s) => s.settingsModalIsOpen);\n  useResetStateOnUnmount();\n  useEndGame();\n  useEffect(() => {\n    if (isPlaying) {\n      refreshTrends();\n      closeModals();\n    }\n  }, [isPlaying]);\n\n  return (\n    <div className=\"flex flex-col relative\">\n      <>\n        <PlayHeader />\n        {capsLockActive && (\n          <div className=\"absolute top-[-30px] z-10 flex w-full items-center justify-center gap-2 font-medium text-red-400\">\n            <div className=\"w-4 text-dark-ocean\">\n              <FontAwesomeIcon icon={faLock} className=\"text-red-400\" />\n            </div>\n            Caps Lock is active\n          </div>\n        )}\n        <AnimatePresence>\n          <motion.div\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            transition={{ duration: 0.5 }}\n            className=\"w-full\"\n          >\n            {isCompleted && <ResultsContainer />}\n          </motion.div>\n        </AnimatePresence>\n        <AnimatePresence>\n          <motion.div\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            transition={{ duration: 0.5 }}\n            className=\"w-full\"\n          >\n            {!isCompleted && (\n              <CodeTypingContainer\n                filePath={challenge.filePath}\n                language={challenge.language}\n              />\n            )}\n          </motion.div>\n        </AnimatePresence>\n        <PlayFooter challenge={challenge} />\n      </>\n      <ToastContainer />\n    </div>\n  );\n}\n\nexport default Play2Page;\n"
  },
  {
    "path": "packages/webapp-next/pages/results/[id].tsx",
    "content": "import { AnimatePresence, motion } from \"framer-motion\";\nimport \"react-toastify/dist/ReactToastify.css\";\nimport { ToastContainer } from \"react-toastify\";\nimport {\n  ResultsText,\n  ShareResultButton,\n} from \"../../modules/play2/containers/ResultsContainer\";\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\nimport { faCode } from \"@fortawesome/free-solid-svg-icons\";\nimport { CodeArea } from \"../../modules/play2/components/CodeArea\";\nimport useSWR from \"swr\";\nimport { getExperimentalServerUrl } from \"../../common/utils/getServerUrl\";\nimport { useRouter } from \"next/router\";\nimport { cpmToWPM } from \"../../common/utils/cpmToWPM\";\nimport { toHumanReadableTime } from \"../../common/utils/toHumanReadableTime\";\nimport Image from \"next/image\";\nimport { format } from \"date-fns\";\nimport { ActionButton } from \"../../modules/play2/components/play-footer/PlayFooter\";\nimport { ChallengeSource } from \"../../modules/play2/components/play-footer/ChallengeSource\";\nimport { useInitializeUserStore } from \"../../common/state/user-store\";\nimport { useUser } from \"../../common/api/user\";\nimport Head from \"next/head\";\nimport { TweetResult } from \"../../modules/play2/components/TweetResult\";\n\nconst baseURL = getExperimentalServerUrl();\n\nexport const config = { runtime: \"experimental-edge\" };\n\nfunction ResultPage() {\n  const user = useUser();\n  useInitializeUserStore(user);\n  const router = useRouter();\n  const { id } = router.query;\n  const { data, isLoading } = useSWR(\n    `${baseURL}/api/results/${id}`,\n    (...args) => id && fetch(...args).then((res) => res.json())\n  );\n  const base = typeof window !== \"undefined\" ? window.location.origin : \"\";\n  const url = `${base}/results/${id}`;\n\n  const resultTitle = data?.user\n    ? `${data.user.username} ${cpmToWPM(data.cpm)}wpm`\n    : \"Result\";\n\n  return (\n    <>\n      <Head>\n        <title>{resultTitle}</title>\n      </Head>\n      <div className=\"flex flex-col items-center\">\n        {data?.user && (\n          <AnimatePresence>\n            <motion.div\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: 0.5 }}\n              className=\"flex flex-col items-center justify-center gap-2\"\n            >\n              <div className=\"w-full flex items-center justify-between\">\n                <div className=\"flex items-center gap-2\">\n                  <Image\n                    alt={`${data.user.username} profile picture`}\n                    className=\"rounded-full\"\n                    width={50}\n                    height={50}\n                    src={data.user.avatarUrl}\n                  />\n                  <h1 className=\"text-lg font-semibold\">\n                    {data.user.username}\n                  </h1>\n                </div>\n                <span className=\"text-sm font-light\">\n                  {format(new Date(data.createdAt), \"yyyy-MM-dd HH:mm\")}\n                </span>\n              </div>\n              <div className=\"w-full grid grid-cols-2 sm:flex sm:flex-row sm:justify-center gap-2\">\n                <ResultsText\n                  info=\"words per minute typed in race\"\n                  title=\"words per minute\"\n                  value={`${cpmToWPM(data.cpm)}`}\n                />\n                <ResultsText\n                  info=\"Percentage of results on speedtyper.dev this race was faster than\"\n                  title=\"global rank\"\n                  value={`${data.percentile}%`}\n                />\n                <ResultsText\n                  info=\"% correctly typed characters in race\"\n                  title=\"accuracy\"\n                  value={`${data.accuracy}%`}\n                />\n                <ResultsText\n                  info=\"time it took to complete race\"\n                  title=\"time\"\n                  value={toHumanReadableTime(Math.floor(data.timeMS / 1000))}\n                />\n                <ResultsText\n                  info=\"number of mistakes made during race\"\n                  title=\"mistakes\"\n                  value={data.mistakes.toString()}\n                />\n                <ShareResultButton url={url} />\n                <TweetResult url={url} wpm={cpmToWPM(data.cpm)} />\n              </div>\n              <CodeArea\n                staticHeigh={false}\n                filePath={truncateFile(data.challenge.path)}\n                focused={true}\n              >\n                <span className=\"text-xs sm:text-sm tracking-tight leading-1\">\n                  {data.challenge.content}\n                </span>\n              </CodeArea>\n              <div className=\"w-full flex justify-between items-center\">\n                <ActionButton\n                  text=\"play\"\n                  title=\"Start playing\"\n                  onClick={() => router.push(\"/\")}\n                  icon={\n                    <div className=\"h-3 w-3 flex items-center\">\n                      <FontAwesomeIcon icon={faCode} />\n                    </div>\n                  }\n                />\n                <ChallengeSource\n                  name={data.challenge.project.fullName}\n                  url={data.challenge.url}\n                  license={data.challenge.project.licenseName}\n                />\n              </div>\n            </motion.div>\n          </AnimatePresence>\n        )}\n\n        {user && !isLoading && !data?.user && (\n          <div className=\"text-4xl font-semibold\">404 result not found</div>\n        )}\n        <ToastContainer />\n      </div>\n    </>\n  );\n}\n\nfunction truncateFile(file: string) {\n  const cutoff = 40;\n  return file.length > cutoff + 3\n    ? file.substring(0, cutoff).trim().concat(\"...\")\n    : file;\n}\n\nexport default ResultPage;\n"
  },
  {
    "path": "packages/webapp-next/postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "packages/webapp-next/public/robots.txt",
    "content": "User-agent: *\n"
  },
  {
    "path": "packages/webapp-next/styles/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nhtml,\nbody {\n  padding: 0;\n  margin: 0;\n  color: #eeeeee;\n  background: #0e0e11;\n  /* font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, */\n  /*   Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; */\n}\n\na {\n  color: inherit;\n  text-decoration: none;\n}\n\n* {\n  box-sizing: border-box;\n}\n\n::-webkit-scrollbar {\n  width: 7px;\n  background: #0e0e11;\n}\n::-webkit-scrollbar-thumb {\n  background: #dbdbdb;\n  border-radius: 100px;\n}"
  },
  {
    "path": "packages/webapp-next/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\n    \"./pages/**/*.{js,ts,jsx,tsx}\",\n    \"./modules/**/*.{js,ts,jsx,tsx}\",\n    \"./common/**/*.{js,ts,jsx,tsx}\",\n    \"./components/**/*.{js,ts,jsx,tsx}\",\n  ],\n  darkMode: false, // or 'media' or 'class'\n  variants: {},\n  plugins: [],\n  theme: {\n    top: {\n      \"1/2\": \"50%\",\n    },\n    left: {\n      \"1/2\": \"50%\",\n    },\n    inset: {\n      0: 0,\n      auto: \"auto\",\n      \"1/2\": \"50%\",\n    },\n    extend: {\n      colors: {\n        citrus: \"#F5CBA7\",\n        \"faded-gray\": \"rgb(184, 184, 184, 0.8)\",\n        \"off-white\": \"#eeeeee\",\n        \"dark-ocean\": \"#0E0E11\",\n        \"dark-lake\": \"#18181b\",\n        code: \"#f5f2f0;\",\n      },\n      width: {\n        \"1/2\": \"50%\",\n        680: \"680px\",\n        340: \"340px\",\n        632: \"632px\",\n        450: \"450px\",\n        300: \"300px\",\n      },\n      width: {\n        680: \"680px\",\n        340: \"340px\",\n        632: \"632px\",\n        450: \"450px\",\n        300: \"300px\",\n      },\n      screens: {\n        sm: \"640px\",\n        // => @media (min-width: 640px) { ... }\n\n        md: \"768px\",\n        // => @media (min-width: 768px) { ... }\n\n        lg: \"1024px\",\n        // => @media (min-width: 1024px) { ... }\n\n        xl: \"1280px\",\n        // => @media (min-width: 1280px) { ... }\n\n        \"2xl\": \"1900px\",\n      },\n      boxShadow: {\n        \"highlight-menu\": \"3px 0 0 0 #0ba90b inset\",\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "packages/webapp-next/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "packages/webapp-next/utils/calculateAccuracy.ts",
    "content": "export default (typedCharsCount: number, mistakeCount: number) =>\n  Math.floor(((typedCharsCount - mistakeCount) / typedCharsCount) * 100);\n"
  },
  {
    "path": "packages/webapp-next/utils/cpmToWpm.ts",
    "content": "export default (cpm: number): number => Math.floor(cpm / 5);\n"
  },
  {
    "path": "packages/webapp-next/utils/getTimeDifference.ts",
    "content": "import {\n  differenceInDays,\n  differenceInHours,\n  differenceInMinutes,\n  differenceInSeconds,\n} from \"date-fns\";\n\nexport default (time: string) => {\n  if (differenceInDays(new Date(), new Date(time))) {\n    return `${differenceInDays(new Date(), new Date(time))} hours`;\n  }\n  if (differenceInHours(new Date(), new Date(time))) {\n    return `${differenceInHours(new Date(), new Date(time))} hours`;\n  }\n\n  if (differenceInMinutes(new Date(), new Date(time))) {\n    return `${differenceInMinutes(new Date(), new Date(time))} minutes`;\n  }\n\n  if (differenceInSeconds(new Date(), new Date(time))) {\n    return `${differenceInSeconds(new Date(), new Date(time))} seconds`;\n  }\n};\n"
  },
  {
    "path": "packages/webapp-next/utils/humanize.ts",
    "content": "// source https://gist.github.com/jweyrich/f39c496b83f73d2c5b0587f4d841651b\ninterface TimeUnit {\n  [key: string]: number;\n}\n\nconst TIME_UNITS: TimeUnit = {\n  year: 3.154e10,\n  month: 2.628e9,\n  week: 6.048e8,\n  day: 8.64e7,\n  hour: 3.6e6,\n  minute: 60000,\n  second: 1000,\n};\n\nexport function humanizeAbsolute(when: Date | number) {\n  const diff =\n    new Date().getTime() - (typeof when === \"number\" ? when : when.getTime());\n  for (const unit in TIME_UNITS) {\n    const quotient = Math.floor(diff / TIME_UNITS[unit]);\n    if (quotient > 0) {\n      return `${quotient} ${unit}${quotient > 1 ? \"s\" : \"\"}`;\n    }\n  }\n  return \"just now\";\n}\n\nexport function humanizeRelative(pastMilliseconds: number) {\n  return humanizeAbsolute(new Date().getTime() - pastMilliseconds);\n}\n"
  },
  {
    "path": "packages/webapp-next/utils/stripIndentation.ts",
    "content": "export default (text: string): string => {\n  return text\n    .split(\"\\n\")\n    .map((subText) => subText.trimStart())\n    .join(\"\\n\");\n};\n"
  }
]