Full Code of codicocodes/speedtyper.dev for AI

main 7778555e37a4 cached
217 files
304.8 KB
93.0k tokens
489 symbols
1 requests
Download .txt
Showing preview only (360K chars total). Download the full file or copy to clipboard to get everything.
Repository: codicocodes/speedtyper.dev
Branch: main
Commit: 7778555e37a4
Files: 217
Total size: 304.8 KB

Directory structure:
gitextract_8g1dy2zt/

├── .github/
│   └── workflows/
│       └── webapp-linting-and-unit-tests.yaml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
└── packages/
    ├── back-nest/
    │   ├── .eslintrc.js
    │   ├── .gitignore
    │   ├── .prettierrc
    │   ├── Dockerfile
    │   ├── README.md
    │   ├── docker-compose.yml
    │   ├── nest-cli.json
    │   ├── package.json
    │   ├── scripts/
    │   │   ├── seed-local.sh
    │   │   └── seed-production.sh
    │   ├── src/
    │   │   ├── app.module.ts
    │   │   ├── auth/
    │   │   │   ├── auth.module.ts
    │   │   │   └── github/
    │   │   │       ├── github.controller.ts
    │   │   │       ├── github.guard.ts
    │   │   │       └── github.strategy.ts
    │   │   ├── challenges/
    │   │   │   ├── challenges.module.ts
    │   │   │   ├── commands/
    │   │   │   │   ├── calculate-language-runner.ts
    │   │   │   │   ├── challenge-import-runner.ts
    │   │   │   │   ├── reformat-challenges-runner.ts
    │   │   │   │   └── unsynced-file-import-runner.ts
    │   │   │   ├── entities/
    │   │   │   │   ├── challenge.entity.ts
    │   │   │   │   ├── language.dto.ts
    │   │   │   │   └── unsynced-file.entity.ts
    │   │   │   ├── languages.controller.ts
    │   │   │   └── services/
    │   │   │       ├── challenge.service.ts
    │   │   │       ├── literal.service.ts
    │   │   │       ├── parser.service.ts
    │   │   │       ├── tests/
    │   │   │       │   └── parser.service.spec.ts
    │   │   │       ├── ts-parser.factory.ts
    │   │   │       ├── unsynced-file-filterer.ts
    │   │   │       ├── unsynced-file-importer.ts
    │   │   │       └── unsynced-file.service.ts
    │   │   ├── commands.ts
    │   │   ├── config/
    │   │   │   ├── cors.ts
    │   │   │   └── postgres.ts
    │   │   ├── connectors/
    │   │   │   └── github/
    │   │   │       ├── github.module.ts
    │   │   │       ├── schemas/
    │   │   │       │   ├── github-blob.dto.ts
    │   │   │       │   ├── github-repository.dto.ts
    │   │   │       │   └── github-tree.dto.ts
    │   │   │       └── services/
    │   │   │           └── github-api.ts
    │   │   ├── database.module.ts
    │   │   ├── filters/
    │   │   │   └── exception.filter.ts
    │   │   ├── main.ts
    │   │   ├── middlewares/
    │   │   │   └── guest-user.ts
    │   │   ├── projects/
    │   │   │   ├── commands/
    │   │   │   │   ├── import-untracked-projects-runner.ts
    │   │   │   │   └── sync-untracked-projects-runner.ts
    │   │   │   ├── entities/
    │   │   │   │   ├── project.entity.ts
    │   │   │   │   └── untracked-project.entity.ts
    │   │   │   ├── project.controller.ts
    │   │   │   ├── projects.module.ts
    │   │   │   └── services/
    │   │   │       ├── project.service.ts
    │   │   │       ├── projects-from-file-reader.ts
    │   │   │       └── untracked-projects.service.ts
    │   │   ├── races/
    │   │   │   ├── entities/
    │   │   │   │   └── race-settings.dto.ts
    │   │   │   ├── race.controllers.ts
    │   │   │   ├── race.exceptions.ts
    │   │   │   ├── race.gateway.ts
    │   │   │   ├── races.module.ts
    │   │   │   └── services/
    │   │   │       ├── add-keystroke.service.ts
    │   │   │       ├── countdown.service.ts
    │   │   │       ├── keystroke-validator.service.ts
    │   │   │       ├── locker.service.ts
    │   │   │       ├── progress.service.ts
    │   │   │       ├── race-events.service.ts
    │   │   │       ├── race-manager.service.ts
    │   │   │       ├── race-player.service.ts
    │   │   │       ├── race.service.ts
    │   │   │       ├── results-handler.service.ts
    │   │   │       ├── session-state.service.ts
    │   │   │       └── tests/
    │   │   │           └── race-player.service.spec.ts
    │   │   ├── results/
    │   │   │   ├── entities/
    │   │   │   │   ├── leaderboard-result.dto.ts
    │   │   │   │   └── result.entity.ts
    │   │   │   ├── errors.ts
    │   │   │   ├── results.controller.ts
    │   │   │   ├── results.module.ts
    │   │   │   └── services/
    │   │   │       ├── result-calculation.service.ts
    │   │   │       ├── result-factory.service.ts
    │   │   │       └── results.service.ts
    │   │   ├── seeder/
    │   │   │   ├── commands/
    │   │   │   │   └── challenge.seeder.ts
    │   │   │   └── seeder.module.ts
    │   │   ├── sessions/
    │   │   │   ├── session.adapter.ts
    │   │   │   ├── session.entity.ts
    │   │   │   ├── session.middleware.ts
    │   │   │   └── types.d.ts
    │   │   ├── tracking/
    │   │   │   ├── entities/
    │   │   │   │   └── event.entity.ts
    │   │   │   ├── tracking.module.ts
    │   │   │   └── tracking.service.ts
    │   │   ├── users/
    │   │   │   ├── controllers/
    │   │   │   │   └── user.controller.ts
    │   │   │   ├── entities/
    │   │   │   │   ├── upsertGithubUserDTO.ts
    │   │   │   │   └── user.entity.ts
    │   │   │   ├── services/
    │   │   │   │   └── user.service.ts
    │   │   │   ├── users.module.ts
    │   │   │   └── utils/
    │   │   │       └── generateRandomUsername.ts
    │   │   └── utils/
    │   │       └── validateDTO.ts
    │   ├── tracked-projects.txt
    │   ├── tsconfig.build.json
    │   └── tsconfig.json
    └── webapp-next/
        ├── .eslintrc.json
        ├── .gitignore
        ├── .prettierrc
        ├── README.md
        ├── Socket.ts
        ├── assets/
        │   └── icons/
        │       ├── BattleIcon.tsx
        │       ├── CopyIcon.tsx
        │       ├── CrossIcon.tsx
        │       ├── CrownIcon.tsx
        │       ├── DiscordLogo.tsx
        │       ├── DownArrowIcon.tsx
        │       ├── GithubLogo.tsx
        │       ├── InfoIcon.tsx
        │       ├── KogWheel.tsx
        │       ├── LinkIcon.tsx
        │       ├── OnlineIcon.tsx
        │       ├── PlayIcon.tsx
        │       ├── ProfileIcon.tsx
        │       ├── ReloadIcon.tsx
        │       ├── RightArrowIcon.tsx
        │       ├── TerminalIcon.tsx
        │       ├── TwitchLogo.tsx
        │       ├── UserGroupIcon.tsx
        │       ├── WarningIcon.tsx
        │       ├── YoutubeLogo.tsx
        │       └── index.tsx
        ├── common/
        │   ├── api/
        │   │   ├── auth.ts
        │   │   ├── races.ts
        │   │   ├── types.ts
        │   │   └── user.ts
        │   ├── components/
        │   │   ├── Avatar.tsx
        │   │   ├── BattleMatcher.tsx
        │   │   ├── Button.tsx
        │   │   ├── Footer/
        │   │   │   └── YoutubeLink.tsx
        │   │   ├── Footer.tsx
        │   │   ├── Layout.tsx
        │   │   ├── NewNavbar.tsx
        │   │   ├── Overlay.tsx
        │   │   ├── buttons/
        │   │   │   ├── GithubLoginButton.tsx
        │   │   │   └── ModalCloseButton.tsx
        │   │   ├── modals/
        │   │   │   ├── GithubLoginModal.tsx
        │   │   │   ├── GithubModal.tsx
        │   │   │   ├── Modal.tsx
        │   │   │   ├── ProfileModal.tsx
        │   │   │   └── SettingsModal.tsx
        │   │   └── overlays/
        │   │       ├── GithubLoginOverlay.tsx
        │   │       └── SettingsOverlay.tsx
        │   ├── github/
        │   │   └── stargazers.ts
        │   ├── hooks/
        │   │   ├── useIsPlaying.ts
        │   │   └── useSocket.ts
        │   ├── services/
        │   │   └── Socket.ts
        │   ├── state/
        │   │   └── user-store.ts
        │   └── utils/
        │       ├── clipboard.ts
        │       ├── cpmToWPM.ts
        │       ├── getServerUrl.ts
        │       ├── router.ts
        │       └── toHumanReadableTime.ts
        ├── components/
        │   ├── Countdown.tsx
        │   ├── Navbar.tsx
        │   └── Stream.tsx
        ├── hooks/
        │   ├── useKeyMap.ts
        │   └── useTotalSeconds.ts
        ├── modules/
        │   └── play2/
        │       ├── components/
        │       │   ├── CodeArea.tsx
        │       │   ├── HiddenCodeInput.tsx
        │       │   ├── IncorrectChars.tsx
        │       │   ├── NextChar.tsx
        │       │   ├── RaceSettings.tsx
        │       │   ├── ResultsChart.tsx
        │       │   ├── SmoothCaret.tsx
        │       │   ├── TweetResult.tsx
        │       │   ├── TypedChars.tsx
        │       │   ├── UntypedChars.tsx
        │       │   ├── leaderboard/
        │       │   │   ├── Leaderboard.tsx
        │       │   │   └── LeaderboardButton.tsx
        │       │   ├── play-footer/
        │       │   │   ├── ChallengeSource/
        │       │   │   │   ├── ChallengeSource.tsx
        │       │   │   │   └── index.ts
        │       │   │   └── PlayFooter/
        │       │   │       ├── PlayFooter.tsx
        │       │   │       └── index.ts
        │       │   ├── play-header/
        │       │   │   └── PlayHeader/
        │       │   │       ├── PlayHeader.tsx
        │       │   │       └── index.tsx
        │       │   └── race-settings/
        │       │       └── LanguageSelector.tsx
        │       ├── containers/
        │       │   ├── CodeTypingContainer.tsx
        │       │   └── ResultsContainer.tsx
        │       ├── hooks/
        │       │   ├── useChallenge.ts
        │       │   ├── useEndGame.ts
        │       │   ├── useFocusRef.ts
        │       │   ├── useGame.ts
        │       │   ├── useGameIdQueryParam.ts
        │       │   ├── useIsCompleted.ts
        │       │   ├── useNodeRect.ts
        │       │   └── useResetStateOnUnmount.ts
        │       ├── services/
        │       │   └── Game.ts
        │       └── state/
        │           ├── code-store.ts
        │           ├── connection-store.ts
        │           ├── game-store.ts
        │           ├── settings-store.ts
        │           └── trends-store.ts
        ├── next.config.js
        ├── package.json
        ├── pages/
        │   ├── 404.tsx
        │   ├── _app.tsx
        │   ├── _document.tsx
        │   ├── index.tsx
        │   └── results/
        │       └── [id].tsx
        ├── postcss.config.js
        ├── public/
        │   └── robots.txt
        ├── styles/
        │   └── globals.css
        ├── tailwind.config.js
        ├── tsconfig.json
        └── utils/
            ├── calculateAccuracy.ts
            ├── cpmToWpm.ts
            ├── getTimeDifference.ts
            ├── humanize.ts
            └── stripIndentation.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/webapp-linting-and-unit-tests.yaml
================================================
name: Frontent linting and unit tests
 
on:
  push:
    branches:
      - main
  pull_request:
 
env:
  NODE_VERSION: 16
 
jobs:
  linting-and-tests:
    name: Webapp linting and unit tests
    runs-on: ubuntu-latest
    steps:
      - name: Install NodeJS
        uses: actions/setup-node@v2
        with:
          node-version: ${{ env.NODE_VERSION }}
 
      - name: Code Checkout
        uses: actions/checkout@v2
 
      - name: Install webapp dependencies
        run: yarn --cwd ./packages/webapp-next install --frozen-lockfile

      - name: Build webapp
        run: yarn --cwd ./packages/webapp-next build
 
      - name: Webapp code linting
        run: yarn --cwd ./packages/webapp-next lint --quiet

      - name: Webapp unit test
        run: echo "no tests to run"


================================================
FILE: .gitignore
================================================
# Notes
.mind

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Deploys
.netlify

# Compiled binary addons (https://nodejs.org/api/addons.html)
dist/
artifacts/
tmp/

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# Local Netlify folder
.netlify

# development
.idea


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

*This is a work in progress.*

### **Table of Contents**
- [Required](#required) 
- [Running Speedtyper.dev](#running-speedtyperdev)
    - [Backend](#backend)
    - [Frontend](#frontend)

## Required

|Prerequisite                               |Link                                                                   |
|-------------------------------------------|-----------------------------------------------------------------------|
|Git                                        |[🔗](https://git-scm.com/downloads)                                   |
|Node 20                                    |[🔗](https://nodejs.org/en/)                                          |
| Yarn                                      |[🔗](https://classic.yarnpkg.com/lang/en/docs/install/#windows-stable)|
|PostgreSQL                                 |            |
|build-essential (or equivalent for your OS)|                                                                       |
| Docker (Optional)                         |[🔗](https://www.docker.com/)                                         |

## Running Speedtyper.dev

### Backend

1. Install dependencies:

    ```
    make install-backend-dependencies
    ```
1. Copy over path of env file:

    ```
    cp ./packages/back-nest/.env.development ./packages/back-nest/.env
    ```

1. 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.

1. Start Docker Compose in the background:

    ```
    make run-dev-db
    ```

1. Seed the db with example challenges:

    ```
    make run-seed-codesources
    ```

1. Run the backend:

    ```
    make run-backend-dev
    ```

### Frontend

1. Install dependencies:

    ```
    make install-webapp-dependencies
    ```

1. Run the frontend:

    ```
    make run-webapp-dev
    ```


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2022 codico <codicocodes@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
# backend

install-backend-dependencies:
	yarn --cwd ./packages/back-nest

run-backend-dev:
	yarn --cwd ./packages/back-nest start:dev

run-dev-db:
	docker compose -f ./packages/back-nest/docker-compose.yml up -d

run-seed-codesources:
	yarn --cwd ./packages/back-nest command seed-challenges

# webapp

install-webapp-dependencies:
	yarn --cwd ./packages/webapp-next

run-webapp-dev:
	yarn --cwd ./packages/webapp-next dev


================================================
FILE: README.md
================================================

<br>
<div align="center">
  <a href="https://speedtyper.dev" target="_blank">
    <img src="https://www.speedtyper.dev/logo.png" alt="Speedtyper" height="100" width="auto"/>
  </a>
  <h1><i>speedtyper.dev</i></h1>
</div>

<p align="center">
  <b>
      Typing competitions for programmers 🧑‍💻👩‍💻👨‍💻
  </b>
</p>
<p align="center">
  <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>
</p>

### **Table of Contents**
- [Features](#features-🎉)
- [Contribute](#contribute-👷)
- [Community](#community-☕)
- [License](#license-📜)
- [Project Contributors](#project-contributors⭐)

## Features 🎉

- ✍️ [**Practice**](https://speedtyper.dev/play?mode=private) - type code snippets from real open source projects
- 🏎️ [**Battle**](https://speedtyper.dev/play?mode=private) - play with your friends in real time with the private race mode
- 🏅 [**Compete**](https://speedtyper.dev) - get on the global leaderboard

## Contribute 👷
- 🦄 **Pull requests are very appreciated!**
- 📚 Read the [contributor introduction (wip)](https://github.com/codicocodes/speedtyper.dev/blob/main/CONTRIBUTING.md)
- 🐛 If you encounter a bug, please [open an issue](https://github.com/codicocodes/speedtyper.dev/issues/new)
- 🗨️ 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!

## Community ☕
<a href="https://discord.gg/AMbnnN5eep" target="__blank">
  <img src="https://discordapp.com/api/guilds/774781405506568202/widget.png?style=banner2" alt="SpeedTyper Discord" width="auto" height="50px"/>
</a>
<a href="https://twitch.tv/codico" target="__blank">
  <img src="https://user-images.githubusercontent.com/76068197/187993983-6133fe16-46ed-45f7-a459-fa798bda4a92.png" alt="Twitch Stream" width="auto" height="50px"/>
</a>

## License 📜

speedtyper.dev is open source software licensed as [MIT](https://github.com/codicocodes/speedtyper.dev/blob/main/LICENSE).

The [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.


## Project Contributors⭐ 

<a href="https://github.com/codicocodes/speedtyper.dev/graphs/contributors" align="center">
  <img src="https://contrib.rocks/image?repo=codicocodes/speedtyper.dev" /> 
</a>


================================================
FILE: packages/back-nest/.eslintrc.js
================================================
module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    project: 'tsconfig.json',
    tsconfigRootDir : __dirname, 
    sourceType: 'module',
  },
  plugins: ['@typescript-eslint/eslint-plugin'],
  extends: [
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
  ],
  root: true,
  env: {
    node: true,
    jest: true,
  },
  ignorePatterns: ['.eslintrc.js', 'node_modules', 'dist'],
  rules: {
    '@typescript-eslint/interface-name-prefix': 'off',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-explicit-any': 'off',
  },
};


================================================
FILE: packages/back-nest/.gitignore
================================================
# compiled output
/dist
/node_modules

# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# OS
.DS_Store

# Tests
/coverage
/.nyc_output

# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

================================================
FILE: packages/back-nest/.prettierrc
================================================
{
  "singleQuote": true,
  "trailingComma": "all"
}

================================================
FILE: packages/back-nest/Dockerfile
================================================
FROM node:20

# Create app directory
RUN mkdir -p /app
WORKDIR /app

# Install app dependencies
COPY package.json /app
COPY yarn.lock /app
RUN yarn install --frozen-lockfile

# Bundle app source
COPY . /app
RUN yarn build

EXPOSE 80

CMD [ "node", "dist/main.js" ]


================================================
FILE: packages/back-nest/README.md
================================================
## Seed challenge data

### Seed test challenges

`yarn command seed-challenges`

### Seed production challenges

Requires configuring a personal `GITHUB_ACCESS_TOKEN` in your .env file

`yarn command import-projects`
`yarn command sync-projects`
`yarn command import-files`
`yarn command import-challenges`


================================================
FILE: packages/back-nest/docker-compose.yml
================================================
# Use postgres/example user/password credentials
version: "3.1"

services:
  db:
    image: postgres
    restart: always

    ports:
      - 5432:5432

    environment:
      POSTGRES_USERNAME: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: speedtyper

  adminer:
    image: adminer
    restart: always
    ports:
      - 8080:8080


================================================
FILE: packages/back-nest/nest-cli.json
================================================
{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src"
}


================================================
FILE: packages/back-nest/package.json
================================================
{
  "name": "back-nest",
  "version": "0.0.1",
  "description": "",
  "author": "",
  "private": true,
  "license": "UNLICENSED",
  "scripts": {
    "command": "TS_NODE_PROJECT=./tsconfig.json ts-node -r tsconfig-paths/register ./src/commands.ts",
    "prebuild": "rimraf dist",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "yarn start:prod",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },
  "dependencies": {
    "@nestjs/axios": "^0.1.0",
    "@nestjs/common": "^9.0.0",
    "@nestjs/config": "^2.2.0",
    "@nestjs/core": "^9.0.0",
    "@nestjs/passport": "^9.0.0",
    "@nestjs/platform-express": "^9.0.0",
    "@nestjs/platform-socket.io": "^9.1.4",
    "@nestjs/typeorm": "^10.0.1",
    "@nestjs/websockets": "^9.1.4",
    "@sentry/node": "^7.37.2",
    "@types/passport-github": "^1.1.7",
    "class-transformer": "^0.5.1",
    "class-validator": "^0.13.2",
    "connect-typeorm": "^2.0.0",
    "express-session": "^1.17.3",
    "express-socket.io-session": "^1.3.5",
    "nest-commander": "^3.1.0",
    "passport": "^0.6.0",
    "passport-github": "^1.1.0",
    "pg": "^8.8.0",
    "pg-query-stream": "^4.3.0",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^7.2.0",
    "socket.io": "^4.5.2",
    "tree-sitter": "^0.20.0",
    "tree-sitter-c": "^0.19.0",
    "tree-sitter-c-sharp": "^0.19.0",
    "tree-sitter-cpp": "^0.19.0",
    "tree-sitter-css": "^0.19.0",
    "tree-sitter-go": "^0.19.1",
    "tree-sitter-java": "^0.19.1",
    "tree-sitter-javascript": "^0.19.0",
    "tree-sitter-lua": "^1.6.2",
    "tree-sitter-ocaml": "^0.19.0",
    "tree-sitter-php": "^0.19.0",
    "tree-sitter-python": "^0.19.0",
    "tree-sitter-ruby": "^0.19.0",
    "tree-sitter-rust": "^0.19.1",
    "tree-sitter-scala": "^0.19.0",
    "tree-sitter-typescript": "^0.19.0",
    "typeorm": "^0.3.10",
    "unique-names-generator": "^4.7.1"
  },
  "devDependencies": {
    "@nestjs/cli": "^9.0.0",
    "@nestjs/schematics": "^9.0.0",
    "@nestjs/testing": "^9.0.0",
    "@types/express": "^4.17.13",
    "@types/express-session": "^1.17.5",
    "@types/express-socket.io-session": "^1.3.6",
    "@types/jest": "28.1.8",
    "@types/node": "^20.0.0",
    "@types/supertest": "^2.0.11",
    "@typescript-eslint/eslint-plugin": "^5.0.0",
    "@typescript-eslint/parser": "^5.0.0",
    "eslint": "^8.0.1",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "jest": "28.1.3",
    "prettier": "^2.3.2",
    "source-map-support": "^0.5.20",
    "supertest": "^6.1.3",
    "ts-jest": "28.0.8",
    "ts-loader": "^9.2.3",
    "ts-node": "^10.0.0",
    "tsconfig-paths": "4.1.0",
    "typescript": "^4.7.4"
  },
  "jest": {
    "moduleFileExtensions": [
      "node",
      "js",
      "json",
      "ts"
    ],
    "rootDir": "src",
    "testRegex": ".*\\.spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": [
      "**/*.(t|j)s"
    ],
    "coverageDirectory": "../coverage",
    "testEnvironment": "node",
    "moduleNameMapper": {
      "^src/(.*)$": "<rootDir>/$1"
    }
  }
}


================================================
FILE: packages/back-nest/scripts/seed-local.sh
================================================
#!/bin/bash

yarn command import-projects &&
yarn command sync-projects &&
yarn command import-files &&
yarn command import-challenges


================================================
FILE: packages/back-nest/scripts/seed-production.sh
================================================
#!/bin/bash
railway run ./seed-local.sh


================================================
FILE: packages/back-nest/src/app.module.ts
================================================
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { GithubConnectorModule } from './connectors/github/github.module';
import { ProjectsModule } from './projects/projects.module';
import { ChallengesModule } from './challenges/challenges.module';
import { UsersModule } from './users/users.module';
import { PostgresModule } from './database.module';
import { RacesModule } from './races/races.module';
import { SeederModule } from './seeder/seeder.module';
import { ResultsModule } from './results/results.module';
import { AuthModule } from './auth/auth.module';

@Module({
  imports: [
    ChallengesModule,
    ConfigModule.forRoot(),
    GithubConnectorModule,
    PostgresModule,
    ProjectsModule,
    RacesModule,
    ResultsModule,
    SeederModule,
    UsersModule,
    AuthModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}


================================================
FILE: packages/back-nest/src/auth/auth.module.ts
================================================
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule } from '@nestjs/config';
import {
  AuthController,
  GithubAuthController,
} from './github/github.controller';
import { GithubStrategy } from './github/github.strategy';
import { UsersModule } from 'src/users/users.module';
import { RacesModule } from 'src/races/races.module';

@Module({
  imports: [
    PassportModule.register({
      // session: true,
    }),
    ConfigModule,
    UsersModule,
    RacesModule,
  ],
  controllers: [GithubAuthController, AuthController],
  providers: [GithubStrategy],
})
export class AuthModule {}


================================================
FILE: packages/back-nest/src/auth/github/github.controller.ts
================================================
import {
  Controller,
  Delete,
  Get,
  HttpException,
  Req,
  Res,
  UseGuards,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { cookieName } from 'src/sessions/session.middleware';
import { User } from 'src/users/entities/user.entity';
import { GithubOauthGuard } from './github.guard';

@Controller('auth')
export class AuthController {
  @Delete()
  async logout(@Req() request: Request, @Res() response: Response) {
    await new Promise<void>((resolve, reject) =>
      request.session?.destroy((err) => {
        console.log('session destroyed', { err });
        if (err) {
          return reject(err);
        }
        return resolve();
      }),
    );
    response.clearCookie(cookieName);
    return response.send({
      ok: true,
    });
  }
}

@Controller('auth/github')
export class GithubAuthController {
  @Get()
  @UseGuards(GithubOauthGuard)
  async githubLogin() {
    // NOTE: the GithubOauthGuard initiates the authentication flow
  }

  @Get('callback')
  @UseGuards(GithubOauthGuard)
  async githubCallback(
    @Req() request: Request,
    @Res({ passthrough: true }) response: Response,
  ) {
    if (!request.session) {
      throw new HttpException('Internal server error', 500);
    }
    request.session.user = request.user as User;
    const next =
      process.env.NODE_ENV === 'production'
        ? 'https://www.speedtyper.dev'
        : 'http://localhost:3001';
    response.redirect(next);
  }
}


================================================
FILE: packages/back-nest/src/auth/github/github.guard.ts
================================================
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard, IAuthModuleOptions } from '@nestjs/passport';
import { Request } from 'express';

@Injectable()
export class GithubOauthGuard extends AuthGuard('github') {
  getAuthenticateOptions(context: ExecutionContext): IAuthModuleOptions {
    const request = context.switchToHttp().getRequest<Request>();
    return {
      state: this.getState(request),
      session: true,
    };
  }

  getState(request: Request) {
    const { next } = request.query as Record<string, string>;
    const queryParams = next
      ? {
          next,
        }
      : {};
    const state = new URLSearchParams(queryParams).toString();
    return state;
  }
}


================================================
FILE: packages/back-nest/src/auth/github/github.strategy.ts
================================================
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Profile, Strategy } from 'passport-github';
import { UserService } from 'src/users/services/user.service';
import { UpsertGithubUserDTO } from 'src/users/entities/upsertGithubUserDTO';

@Injectable()
export class GithubStrategy extends PassportStrategy(Strategy, 'github') {
  constructor(cfg: ConfigService, private userService: UserService) {
    const BASE_URL =
      process.env.NODE_ENV === 'production'
        ? 'https://v3.speedtyper.dev'
        : 'http://localhost:1337';
    super({
      clientID: cfg.get<string>('GITHUB_CLIENT_ID'),
      clientSecret: cfg.get<string>('GITHUB_CLIENT_SECRET'),
      callbackURL: `${BASE_URL}/api/auth/github/callback`,
      scope: ['public_profile'],
    });
  }

  async validate(
    _accessToken: string,
    _refreshToken: string,
    profile: Profile,
  ) {
    const upsertUserDTO = UpsertGithubUserDTO.fromGithubProfile(profile);
    const user = await this.userService.upsertGithubUser(
      upsertUserDTO.toUser(),
    );
    return user;
  }
}


================================================
FILE: packages/back-nest/src/challenges/challenges.module.ts
================================================
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { GithubConnectorModule } from 'src/connectors/github/github.module';
import { ProjectsModule } from 'src/projects/projects.module';
import { CalculateLanguageRunner } from './commands/calculate-language-runner';
import { ChallengeImportRunner } from './commands/challenge-import-runner';
import { ReformatChallengesRunner } from './commands/reformat-challenges-runner';
import { UnsyncedFileImportRunner } from './commands/unsynced-file-import-runner';
import { Challenge } from './entities/challenge.entity';
import { UnsyncedFile } from './entities/unsynced-file.entity';
import { LanguageController } from './languages.controller';
import { ChallengeService } from './services/challenge.service';
import { LiteralService } from './services/literal.service';
import { ParserService } from './services/parser.service';
import { UnsyncedFileFilterer } from './services/unsynced-file-filterer';
import { UnsyncedFileImporter } from './services/unsynced-file-importer';
import { UnsyncedFileService } from './services/unsynced-file.service';

@Module({
  imports: [
    TypeOrmModule.forFeature([UnsyncedFile, Challenge]),
    GithubConnectorModule,
    ProjectsModule,
  ],
  controllers: [LanguageController],
  providers: [
    ParserService,
    ChallengeService,
    LiteralService,
    ChallengeImportRunner,
    UnsyncedFileFilterer,
    UnsyncedFileImporter,
    UnsyncedFileImportRunner,
    UnsyncedFileService,
    CalculateLanguageRunner,
    ReformatChallengesRunner,
  ],
  exports: [ChallengeService, LiteralService],
})
export class ChallengesModule {}


================================================
FILE: packages/back-nest/src/challenges/commands/calculate-language-runner.ts
================================================
import { InjectRepository } from '@nestjs/typeorm';
import { Command, CommandRunner } from 'nest-commander';
import { Repository } from 'typeorm';
import { Challenge } from '../entities/challenge.entity';

@Command({
  name: 'calculate-language',
  arguments: '',
  options: {},
})
export class CalculateLanguageRunner extends CommandRunner {
  constructor(
    @InjectRepository(Challenge)
    private repository: Repository<Challenge>,
  ) {
    super();
  }

  async run(): Promise<void> {
    const stream = await this.repository
      .createQueryBuilder('ch')
      .select('id, path')
      .where('ch.language IS NULL')
      .stream();

    const updatesByLanguage: Record<string, string[]> = {};

    for await (const { id, path } of stream) {
      const dotSplitted = path.split('.');
      const language = dotSplitted[dotSplitted.length - 1];
      if (!updatesByLanguage[language]) {
        updatesByLanguage[language] = [];
      }
      updatesByLanguage[language].push(id);
    }

    await Promise.all(
      Object.entries(updatesByLanguage).map(async ([language, ids]) => {
        await this.repository.update(ids, { language });
      }),
    );
  }
}


================================================
FILE: packages/back-nest/src/challenges/commands/challenge-import-runner.ts
================================================
import { Command, CommandRunner } from 'nest-commander';
import { GithubAPI } from 'src/connectors/github/services/github-api';
import { Challenge } from '../entities/challenge.entity';
import { UnsyncedFile } from '../entities/unsynced-file.entity';
import { ChallengeService } from '../services/challenge.service';
import { ParserService } from '../services/parser.service';
import { UnsyncedFileService } from '../services/unsynced-file.service';

@Command({
  name: 'import-challenges',
  arguments: '',
  options: {},
})
export class ChallengeImportRunner extends CommandRunner {
  constructor(
    private api: GithubAPI,
    private unsynced: UnsyncedFileService,
    private parserService: ParserService,
    private challengeService: ChallengeService,
  ) {
    super();
  }
  async run(): Promise<void> {
    let filesSynced = 0;
    const files = await this.unsynced.findAllWithProject();
    for (const file of files) {
      const challenges = await this.syncChallengesFromFile(file);
      filesSynced++;
      console.info(
        `[challenge-import]: ${filesSynced}/${files.length} synced. Challenges added=${challenges.length}`,
      );
    }
  }

  private async syncChallengesFromFile(file: UnsyncedFile) {
    const blob = await this.api.fetchBlob(
      file.project.fullName,
      file.currentSha,
    );
    const nodes = this.parseNodesFromContent(file.path, blob.content);
    const challenges = nodes.map((node) =>
      Challenge.fromTSNode(file.project, file, node),
    );
    await this.challengeService.upsert(challenges);
    await this.unsynced.remove([file]);
    return challenges;
  }

  private parseNodesFromContent(path: string, base64Content: string) {
    const fileExtension = path.split('.').pop();
    const parser = this.parserService.getParser(fileExtension);
    const content = Buffer.from(base64Content, 'base64').toString();
    const nodes = parser.parseTrackedNodes(content);
    return nodes;
  }
}


================================================
FILE: packages/back-nest/src/challenges/commands/reformat-challenges-runner.ts
================================================
import { InjectRepository } from '@nestjs/typeorm';
import { Command, CommandRunner } from 'nest-commander';
import { Repository } from 'typeorm';
import { Challenge } from '../entities/challenge.entity';
import { getFormattedText } from '../services/parser.service';

@Command({
  name: 'reformat-challenges',
  arguments: '',
  options: {},
})
export class ReformatChallengesRunner extends CommandRunner {
  constructor(
    @InjectRepository(Challenge)
    private repository: Repository<Challenge>,
  ) {
    super();
  }

  async run(): Promise<void> {
    const stream = await this.repository
      .createQueryBuilder('ch')
      .select('id, content')
      .stream();

    const pendingUpdates = [];
    for await (const { id, content } of stream) {
      const formattedContent = getFormattedText(content);
      if (formattedContent !== content) {
        pendingUpdates.push(
          this.repository.update({ id }, { content: formattedContent }),
        );
      }
    }

    await Promise.all(pendingUpdates);

    console.log(`Reformatted ${pendingUpdates.length} challenges`);
  }
}


================================================
FILE: packages/back-nest/src/challenges/commands/unsynced-file-import-runner.ts
================================================
import { Command, CommandRunner } from 'nest-commander';
import { ProjectService } from 'src/projects/services/project.service';
import { UnsyncedFileImporter } from '../services/unsynced-file-importer';

@Command({
  name: 'import-files',
  arguments: '',
  options: {},
})
export class UnsyncedFileImportRunner extends CommandRunner {
  constructor(
    private projectService: ProjectService,
    private importer: UnsyncedFileImporter,
  ) {
    super();
  }
  async run(): Promise<void> {
    const projects = await this.projectService.findAll();
    for (const project of projects) {
      // Only sync unsynced projects for now
      if (!project.syncedSha) {
        const sha = await this.importer.import(project);
        await this.projectService.updateSyncedSha(project.id, sha);
        console.info(`[FileImport]: Imported files for ${project.fullName}`);
      }
    }
  }
}


================================================
FILE: packages/back-nest/src/challenges/entities/challenge.entity.ts
================================================
import TSParser from 'tree-sitter';
import { Project } from 'src/projects/entities/project.entity';
import {
  Entity,
  PrimaryGeneratedColumn,
  ManyToOne,
  Column,
  OneToMany,
} from 'typeorm';
import { UnsyncedFile } from './unsynced-file.entity';
import { GithubAPI } from 'src/connectors/github/services/github-api';
import { Result } from 'src/results/entities/result.entity';
import { getFormattedText } from '../services/parser.service';

@Entity()
export class Challenge {
  @PrimaryGeneratedColumn('uuid')
  id: string;
  @Column({ select: false })
  sha: string;
  @Column({ select: false })
  treeSha: string;
  @Column({ nullable: false })
  language: string;
  @Column()
  path: string;
  @Column({ unique: true })
  url: string;
  @Column({ unique: true })
  content: string;
  @ManyToOne(() => Project, (project) => project.files)
  project: Project;
  @OneToMany(() => Result, (result) => result.user)
  results: Result[];

  static fromTSNode(
    project: Project,
    file: UnsyncedFile,
    node: TSParser.SyntaxNode,
  ) {
    const challenge = new Challenge();
    challenge.path = file.path;
    challenge.sha = file.currentSha;
    challenge.treeSha = file.currentTreeSha;
    challenge.project = project;
    challenge.content = getFormattedText(node.text);
    challenge.url = GithubAPI.getBlobPermaLink(
      project.fullName,
      file.currentTreeSha,
      file.path,
      // NOTE: row is 0 indexed, while #L is 1 indexed
      node.startPosition.row + 1,
      node.endPosition.row + 1,
    );
    const dotSplitPath = file.path.split('.');
    challenge.language = dotSplitPath[dotSplitPath.length - 1];
    return challenge;
  }
  static getStrippedCode(code: string) {
    const strippedCode = code
      .split('\n')
      .map((subText) => subText.trimStart())
      .join('\n');
    return strippedCode;
  }
}


================================================
FILE: packages/back-nest/src/challenges/entities/language.dto.ts
================================================
import { IsString } from 'class-validator';

export class LanguageDTO {
  @IsString()
  language: string;
  @IsString()
  name: string;
}


================================================
FILE: packages/back-nest/src/challenges/entities/unsynced-file.entity.ts
================================================
import { GithubNode } from 'src/connectors/github/schemas/github-tree.dto';
import { Project } from 'src/projects/entities/project.entity';
import {
  Entity,
  PrimaryGeneratedColumn,
  ManyToOne,
  Column,
  Index,
} from 'typeorm';

@Entity()
@Index(['path', 'project'], { unique: true })
export class UnsyncedFile {
  @PrimaryGeneratedColumn('uuid')
  id: string;
  @Column()
  path: string;
  @Column()
  currentSha: string;
  @Column()
  currentTreeSha: string;
  @Column({ nullable: true })
  syncedSha?: string;

  @ManyToOne(() => Project, (project) => project.files)
  project: Project;

  static fromGithubNode(project: Project, treeSha: string, node: GithubNode) {
    const file = new UnsyncedFile();
    file.path = node.path;
    file.currentSha = node.sha;
    file.currentTreeSha = treeSha;
    file.project = project;
    return file;
  }
}


================================================
FILE: packages/back-nest/src/challenges/languages.controller.ts
================================================
import { Controller, Get } from '@nestjs/common';
import { LanguageDTO } from './entities/language.dto';
import { ChallengeService } from './services/challenge.service';

@Controller('languages')
export class LanguageController {
  constructor(private service: ChallengeService) {}
  @Get()
  getLeaderboard(): Promise<LanguageDTO[]> {
    return this.service.getLanguages();
  }
}


================================================
FILE: packages/back-nest/src/challenges/services/challenge.service.ts
================================================
import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Challenge } from '../entities/challenge.entity';
import { LanguageDTO } from '../entities/language.dto';

@Injectable()
export class ChallengeService {
  private static UpsertOptions = {
    conflictPaths: ['content'],
    skipUpdateIfNoValuesChanged: true,
  };
  constructor(
    @InjectRepository(Challenge)
    private challengeRepository: Repository<Challenge>,
  ) {}

  async upsert(challenges: Challenge[]): Promise<void> {
    await this.challengeRepository.upsert(
      challenges,
      ChallengeService.UpsertOptions,
    );
  }

  async getRandom(language?: string): Promise<Challenge> {
    let query = this.challengeRepository
      .createQueryBuilder('challenge')
      .leftJoinAndSelect('challenge.project', 'project');

    if (language) {
      query = query.where('challenge.language = :language', {
        language,
      });
    }

    const randomChallenge = await query.orderBy('RANDOM()').getOne();

    if (!randomChallenge)
      throw new BadRequestException(`No challenges for language: ${language}`);

    return randomChallenge;
  }

  async getLanguages(): Promise<LanguageDTO[]> {
    const selectedLanguages = await this.challengeRepository
      .createQueryBuilder()
      .select('language')
      .distinct()
      .execute();

    const languages = selectedLanguages.map(
      ({ language }: { language: string }) => ({
        language,
        name: this.getLanguageName(language),
      }),
    );

    languages.sort((a, b) => a.name.localeCompare(b.name));

    return languages;
  }

  private getLanguageName(language: string): string {
    const allLanguages = {
      js: 'JavaScript',
      ts: 'TypeScript',
      rs: 'Rust',
      c: 'C',
      java: 'Java',
      cpp: 'C++',
      go: 'Go',
      lua: 'Lua',
      php: 'PHP',
      py: 'Python',
      rb: 'Ruby',
      cs: 'C-Sharp',
      scala: 'Scala',
    };
    return allLanguages[language];
  }
}


================================================
FILE: packages/back-nest/src/challenges/services/literal.service.ts
================================================
import { Injectable } from '@nestjs/common';

@Injectable()
export class LiteralService {
  calculateLiterals(code: string) {
    const literals = code
      .substring(0)
      .split(/[.\-=/_\:\;\,\}\{\)\(\"\'\]\[\/\#\?\>\<\&\*]/)
      .flatMap((r) => {
        return r.split(/[\n\r\s\t]+/);
      })
      .filter(Boolean);
    return literals;
  }
}


================================================
FILE: packages/back-nest/src/challenges/services/parser.service.ts
================================================
import * as TSParser from 'tree-sitter';
import { Injectable } from '@nestjs/common';
import { getTSLanguageParser } from './ts-parser.factory';

// TODO: Chars like ♡ should be filtered out
@Injectable()
export class ParserService {
  getParser(language: string) {
    const tsParser = getTSLanguageParser(language);
    return new Parser(tsParser);
  }
}

export enum NodeTypes {
  ClassDeclaration = 'class_declaration',
  ClassDefinition = 'class_definition',
  FunctionDeclaration = 'function_declaration',
  FunctionDefinition = 'function_definition',
  FunctionItem = 'function_item',
  MethodDeclaration = 'method_declaration',
  Module = 'module',
  Call = 'call',
  UsingDirective = 'using_directive',
  NamespaceDeclaration = 'namespace_declaration',
}

export class Parser {
  private MAX_NODE_LENGTH = 300;
  private MIN_NODE_LENGTH = 100;
  private MAX_NUM_LINES = 11;
  private MAX_LINE_LENGTH = 55;

  constructor(private ts: TSParser) {}

  parseTrackedNodes(content: string) {
    const root = this.ts.parse(content).rootNode;
    return this.filterNodes(root);
  }

  private filterNodes(root: TSParser.SyntaxNode) {
    const nodes = root.children
      .filter((n) => this.filterValidNodeTypes(n))
      .filter((n) => this.filterLongNodes(n))
      .filter((n) => this.filterShortNodes(n))
      .filter((n) => this.filterTooLongLines(n))
      .filter((n) => this.filterTooManyLines(n));
    return nodes;
  }

  private filterValidNodeTypes(node: TSParser.SyntaxNode) {
    switch (node.type) {
      case NodeTypes.ClassDeclaration:
      case NodeTypes.ClassDefinition:
      case NodeTypes.FunctionDeclaration:
      case NodeTypes.FunctionDefinition:
      case NodeTypes.FunctionItem:
      case NodeTypes.MethodDeclaration:
      case NodeTypes.Module:
      case NodeTypes.Call:
      case NodeTypes.UsingDirective:
      case NodeTypes.NamespaceDeclaration:
        // We want method declarations if they are on the root node (i.e. golang)
        return true;
      default:
        console.log(node.type);
        return false;
    }
  }

  private filterLongNodes(node: TSParser.SyntaxNode) {
    return this.MAX_NODE_LENGTH > node.text.length;
  }

  private filterShortNodes(node: TSParser.SyntaxNode) {
    return node.text.length > this.MIN_NODE_LENGTH;
  }

  private filterTooManyLines(node: TSParser.SyntaxNode) {
    const lines = node.text.split('\n');
    return lines.length <= this.MAX_NUM_LINES;
  }

  private filterTooLongLines(node: TSParser.SyntaxNode) {
    for (const line of node.text.split('\n')) {
      if (line.length > this.MAX_LINE_LENGTH) {
        return false;
      }
    }
    return true;
  }
}

export function removeDuplicateNewLines(rawText: string) {
  const newLine = '\n';
  const duplicateNewLine = '\n\n';
  let newRawText = rawText;
  let prevRawText = rawText;
  do {
    prevRawText = newRawText;
    newRawText = newRawText.replaceAll(duplicateNewLine, newLine);
  } while (newRawText !== prevRawText);
  return newRawText;
}

export function replaceTabsWithSpaces(rawText: string) {
  const tab = '\t';
  const spaces = '  ';
  return rawText.replaceAll(tab, spaces);
}

export function removeTrailingSpaces(rawText: string) {
  return rawText
    .split('\n')
    .map((line) => line.trimEnd())
    .join('\n');
}

export function dedupeInnerSpaces(rawText: string) {
  const innerSpaces = /(?<=\S+)\s+(?=\S+)/g;
  const space = ' ';
  return rawText
    .split('\n')
    .map((line) => line.replaceAll(innerSpaces, space))
    .join('\n');
}

export function getFormattedText(rawText: string) {
  rawText = replaceTabsWithSpaces(rawText);
  rawText = removeTrailingSpaces(rawText);
  rawText = removeDuplicateNewLines(rawText);
  rawText = dedupeInnerSpaces(rawText);
  return rawText;
}


================================================
FILE: packages/back-nest/src/challenges/services/tests/parser.service.spec.ts
================================================
import { getFormattedText } from '../parser.service';

const dubbleNewLineInput = `func newGRPCProxyCommand() *cobra.Command {
  lpc := &cobra.Command{
    Use:   "grpc-proxy <subcommand>",
    Short: "grpc-proxy related command",
  }
  lpc.AddCommand(newGRPCProxyStartCommand())

  return lpc
}`;

const trippleNewLineInput = `func newGRPCProxyCommand() *cobra.Command {
  lpc := &cobra.Command{
    Use:   "grpc-proxy <subcommand>",
    Short: "grpc-proxy related command",
  }
  lpc.AddCommand(newGRPCProxyStartCommand())


  return lpc
}`;

const inputWithTabs = `func newGRPCProxyCommand() *cobra.Command {
\tlpc := &cobra.Command{
\t\tUse:   "grpc-proxy <subcommand>",
\t\tShort: "grpc-proxy related command",
\t}
\tlpc.AddCommand(newGRPCProxyStartCommand())
\treturn lpc
}`;

const inputWithEmptyLineWithSpaces = `func newGRPCProxyCommand() *cobra.Command {
  lpc := &cobra.Command{   
    Use:   "grpc-proxy <subcommand>",
    Short: "grpc-proxy related command",
  }     
   
  lpc.AddCommand(newGRPCProxyStartCommand())
  return lpc
}`;

const inputWithTrailingSpaces = `func newGRPCProxyCommand() *cobra.Command {
  lpc := &cobra.Command{   
    Use:   "grpc-proxy <subcommand>",
    Short: "grpc-proxy related command",
  }     
  lpc.AddCommand(newGRPCProxyStartCommand())
  return lpc
}`;

const inputWithStructAlignment = `func newGRPCProxyCommand() *cobra.Command {
  lpc := &cobra.Command{
    Use:   "grpc-proxy <subcommand>",
    Short: "grpc-proxy related command",
  }
  lpc.AddCommand(newGRPCProxyStartCommand())
  return lpc
}`;

const output = `func newGRPCProxyCommand() *cobra.Command {
  lpc := &cobra.Command{
    Use: "grpc-proxy <subcommand>",
    Short: "grpc-proxy related command",
  }
  lpc.AddCommand(newGRPCProxyStartCommand())
  return lpc
}`;

describe('getFormattedText', () => {
  it('should remove double newlines', () => {
    const parsed = getFormattedText(dubbleNewLineInput);
    expect(parsed).toEqual(output);
  });
  it('should remove tripple newlines', () => {
    const parsed = getFormattedText(trippleNewLineInput);
    expect(parsed).toEqual(output);
  });
  it('should replace tabs with spaces', () => {
    const parsed = getFormattedText(inputWithTabs);
    expect(parsed).toEqual(output);
  });
  it('should return the same if called twice', () => {
    const firstParsed = getFormattedText(inputWithTabs);
    const parsed = getFormattedText(firstParsed);
    expect(parsed).toEqual(output);
  });
  it('should remove trailing spaces', () => {
    const parsed = getFormattedText(inputWithTrailingSpaces);
    expect(parsed).toEqual(output);
  });
  it('should remove empty line with spaces', () => {
    const parsed = getFormattedText(inputWithEmptyLineWithSpaces);
    expect(parsed).toEqual(output);
  });
  it('should dedupe multiple interior spaces', () => {
    const parsed = getFormattedText(inputWithStructAlignment);
    expect(parsed).toEqual(output);
  });
});


================================================
FILE: packages/back-nest/src/challenges/services/ts-parser.factory.ts
================================================
import * as TSParser from 'tree-sitter';

import * as js from 'tree-sitter-javascript';
import * as ts from 'tree-sitter-typescript/typescript';
import * as java from 'tree-sitter-java';
import * as c from 'tree-sitter-c';
import * as cpp from 'tree-sitter-cpp';
import * as lua from 'tree-sitter-lua';
import * as php from 'tree-sitter-php';
import * as py from 'tree-sitter-python';
import * as rb from 'tree-sitter-ruby';
import * as cs from 'tree-sitter-c-sharp';
import * as go from 'tree-sitter-go';
import * as rs from 'tree-sitter-rust';
import * as scala from 'tree-sitter-scala';

const languageParserMap: { [key: string]: any } = {
  js,
  ts,
  rs,
  c,
  java,
  cpp,
  go,
  lua,
  php,
  py,
  rb,
  cs,
  scala,
};

export const getSupportFileExtensions = () => {
  return Object.keys(languageParserMap).map((ext) => `.${ext}`);
};

export class InvalidLanguage extends Error {
  constructor(language: string) {
    super(`Error getting parser for language='${language}'`);
    Object.setPrototypeOf(this, InvalidLanguage.prototype);
  }
}

export const getTSLanguageParser = (language: string) => {
  const langParser = languageParserMap[language];
  if (!langParser) throw new InvalidLanguage(language);
  const parser = new TSParser();
  parser.setLanguage(langParser);
  return parser;
};


================================================
FILE: packages/back-nest/src/challenges/services/unsynced-file-filterer.ts
================================================
import { Injectable } from '@nestjs/common';
import { GithubNode } from 'src/connectors/github/schemas/github-tree.dto';
import { getSupportFileExtensions } from './ts-parser.factory';

@Injectable()
export class UnsyncedFileFilterer {
  filter(nodes: GithubNode[]) {
    return nodes
      .filter(isBlobNode)
      .filter(hasTrackedFileExt)
      .filter(isNotExcludedPath);
  }
}

function isBlobNode(node: GithubNode) {
  return node.type === 'blob';
}

function hasTrackedFileExt(node: GithubNode) {
  const trackedFileExtensions = getSupportFileExtensions();
  for (const includedExt of trackedFileExtensions) {
    if (node.path.endsWith(includedExt)) {
      // ends with tracked file extension
      return true;
    }
  }
  // untracked file extension
  return false;
}

function isNotExcludedPath(node: GithubNode) {
  const excludedSubStrings = [
    '.ci',
    '.jenkins',
    '.build',
    '.idea',
    '.devcontainer',
    'migrations',
    'benchmarks',
    'build-tools',
    'conventions',
    'licenses',
    'requirements',
    '.svg',
    'docs',
    '.github',
    'example',
    'types',
    'test',
    '.pb.',
    '.proto',
    'doc',
  ];
  for (const excludeStr of excludedSubStrings) {
    if (node.path.includes(excludeStr)) {
      // is excluded path
      return false;
    }
  }
  // is not excluded path
  return true;
}


================================================
FILE: packages/back-nest/src/challenges/services/unsynced-file-importer.ts
================================================
import { Injectable } from '@nestjs/common';
import { GithubAPI } from 'src/connectors/github/services/github-api';
import { Project } from 'src/projects/entities/project.entity';
import { UnsyncedFile } from '../entities/unsynced-file.entity';
import { UnsyncedFileFilterer } from './unsynced-file-filterer';
import { UnsyncedFileService } from './unsynced-file.service';

@Injectable()
export class UnsyncedFileImporter {
  constructor(
    private api: GithubAPI,
    private filterer: UnsyncedFileFilterer,
    private svc: UnsyncedFileService,
  ) {}
  async import(project: Project) {
    const root = await this.api.fetchTree(
      project.fullName,
      project.defaultBranch,
    );
    const nodes = this.filterer.filter(root.tree);
    const files = nodes.map((node) =>
      UnsyncedFile.fromGithubNode(project, root.sha, node),
    );
    await this.svc.bulkUpsert(files);
    return root.sha;
  }
}


================================================
FILE: packages/back-nest/src/challenges/services/unsynced-file.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UnsyncedFile } from '../entities/unsynced-file.entity';

@Injectable()
export class UnsyncedFileService {
  private static UpsertOptions = {
    conflictPaths: ['path', 'project'],
    skipUpdateIfNoValuesChanged: true,
  };
  constructor(
    @InjectRepository(UnsyncedFile)
    private filesRepository: Repository<UnsyncedFile>,
  ) {}

  async bulkUpsert(files: UnsyncedFile[]): Promise<void> {
    await this.filesRepository.upsert(files, UnsyncedFileService.UpsertOptions);
  }

  async findAllWithProject(): Promise<UnsyncedFile[]> {
    const files = await this.filesRepository.find({
      relations: {
        project: true,
      },
    });
    return files;
  }

  async remove(files: UnsyncedFile[]): Promise<void> {
    await this.filesRepository.remove(files);
  }
}


================================================
FILE: packages/back-nest/src/commands.ts
================================================
import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module';

async function runCommand() {
  await CommandFactory.run(AppModule, ['warn', 'error']);
}

runCommand();


================================================
FILE: packages/back-nest/src/config/cors.ts
================================================
import { GatewayMetadata } from '@nestjs/websockets';

export const getAllowedOrigins = () => {
  return process.env.NODE_ENV === 'production'
    ? ['https://speedtyper.dev', 'https://www.speedtyper.dev']
    : ['http://localhost:3001'];
};

export const gatewayMetadata: GatewayMetadata = {
  cors: {
    origin: getAllowedOrigins(),
    methods: ['GET', 'POST'],
    credentials: true,
  },
};


================================================
FILE: packages/back-nest/src/config/postgres.ts
================================================
import * as dotenv from 'dotenv';
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';
dotenv.config({ path: __dirname + '/../../.env' });

export const pgOptions: Partial<PostgresConnectionOptions> = {
  url: process.env.DATABASE_PRIVATE_URL,
  extra: {
    // 120 seconds idle timeout
    idleTimeoutMillis: 120000,
    max: 10,
  },
};


================================================
FILE: packages/back-nest/src/connectors/github/github.module.ts
================================================
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { GithubAPI } from './services/github-api';

@Module({
  imports: [HttpModule, ConfigModule],
  providers: [GithubAPI],
  exports: [GithubAPI],
})
export class GithubConnectorModule {}


================================================
FILE: packages/back-nest/src/connectors/github/schemas/github-blob.dto.ts
================================================
import { IsEnum, IsNumber, IsString } from 'class-validator';

export enum GithubBlobEncoding {
  base64 = 'base64',
}

export class GithubBlob {
  @IsString()
  sha: string;
  @IsString()
  node_id: string;
  @IsNumber()
  size: number;
  @IsString()
  url: string;
  @IsString()
  content: string;
  @IsEnum(GithubBlobEncoding)
  encoding: 'base64';
}


================================================
FILE: packages/back-nest/src/connectors/github/schemas/github-repository.dto.ts
================================================
import { IsNumber, IsString, ValidateIf } from 'class-validator';

export class GithubLicense {
  @IsString()
  name: string;
}

export class GithubOwner {
  @IsString()
  login: string;
  @IsNumber()
  id: number;
  @IsString()
  avatar_url: string;
  @IsString()
  html_url: string;
}

export class GithubRepository {
  @IsNumber()
  id: number;
  @IsString()
  node_id: string;
  @IsString()
  name: string;
  @IsString()
  full_name: string;
  @IsString()
  html_url: string;
  @IsString()
  description: string;
  @IsString()
  url: string;
  @IsString()
  trees_url: string;
  @IsString()
  @ValidateIf((_: any, value: unknown) => value !== null)
  homepage: string | null;
  @IsNumber()
  stargazers_count: number;
  @IsString()
  language: string;
  @IsString()
  default_branch: string;
  license: GithubLicense;
  owner: GithubOwner;
}


================================================
FILE: packages/back-nest/src/connectors/github/schemas/github-tree.dto.ts
================================================
import { IsEnum, IsNumber, IsString } from 'class-validator';

export enum GithubNodeType {
  blob = 'blob',
  tree = 'tree',
}

export class GithubNode {
  @IsString()
  path: string;
  @IsString()
  mode: string;
  @IsEnum(GithubNodeType)
  type: GithubNodeType;
  @IsString()
  sha: string;
  @IsNumber()
  size?: number;
  @IsString()
  url: string;
}

export class GithubTree {
  @IsString()
  sha: string;
  @IsString()
  url: string;
  tree: GithubNode[];
}


================================================
FILE: packages/back-nest/src/connectors/github/services/github-api.ts
================================================
import { AxiosResponse } from 'axios';
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { firstValueFrom } from 'rxjs';
import { validateDTO } from 'src/utils/validateDTO';
import { GithubBlob } from '../schemas/github-blob.dto';
import { GithubRepository } from '../schemas/github-repository.dto';
import { GithubTree } from '../schemas/github-tree.dto';

@Injectable()
export class GithubAPI {
  private static BASE_URL = 'https://api.github.com';
  private static REPOSITORIES_URL = `${GithubAPI.BASE_URL}/repos`;
  private static REPOSITORY_URL = `${GithubAPI.REPOSITORIES_URL}/{fullName}`;
  private static TREE_URL = `${GithubAPI.REPOSITORY_URL}/git/trees/{sha}?recursive=true`;
  private static BLOB_URL = `${GithubAPI.REPOSITORY_URL}/git/blobs/{sha}`;
  private static BLOB_HTML_PERMA_LINK = `https://github.com/{fullName}/blob/{treeSha}/{path}/#L{startLine}-L{endLine}`;

  private token: string;

  constructor(private readonly http: HttpService, cfg: ConfigService) {
    this.token = getGithubAccessToken(cfg);
  }

  static getBlobPermaLink(
    fullName: string,
    treeSha: string,
    path: string,
    startLine: number,
    endLine: number,
  ) {
    const url = GithubAPI.BLOB_HTML_PERMA_LINK.replace('{fullName}', fullName)
      .replace('{treeSha}', treeSha)
      .replace('{path}', path)
      .replace('{startLine}', startLine.toString())
      .replace('{endLine}', endLine.toString());
    return url;
  }

  private async get(url: string) {
    const resp = await firstValueFrom(
      this.http.get(url, {
        headers: {
          Authorization: `token ${this.token}`,
        },
      }),
    );
    this.logRateLimit(resp);
    return resp.data;
  }

  private logRateLimit(resp: AxiosResponse) {
    const rateLimitResetSeconds = resp.headers['x-ratelimit-reset'];
    const resetDate = new Date(parseInt(rateLimitResetSeconds) * 1000);
    const rateLimitRemaining = resp.headers['x-ratelimit-remaining'];
    console.log(
      `GH Rate Limiting. Remaining: ${rateLimitRemaining} Reset: ${resetDate}`,
    );
  }

  async fetchRepository(fullName: string): Promise<GithubRepository> {
    const url = GithubAPI.REPOSITORY_URL.replace('{fullName}', fullName);
    const rawData = await this.get(url);
    const repository = await validateDTO(GithubRepository, rawData);
    return repository;
  }

  async fetchTree(fullName: string, sha: string): Promise<GithubTree> {
    const treeUrl = GithubAPI.TREE_URL.replace('{fullName}', fullName).replace(
      '{sha}',
      sha,
    );
    const rawData = await this.get(treeUrl);
    const rootNode = await validateDTO(GithubTree, rawData);
    return rootNode;
  }

  async fetchBlob(fullName: string, sha: string): Promise<GithubBlob> {
    const url = GithubAPI.BLOB_URL.replace('{fullName}', fullName).replace(
      '{sha}',
      sha,
    );
    const rawData = await this.get(url);
    const blob = await validateDTO(GithubBlob, rawData);
    return blob;
  }
}

function getGithubAccessToken(cfg: ConfigService) {
  const token = cfg.get<string>('GITHUB_ACCESS_TOKEN');
  if (!token) {
    throw new Error(
      `GITHUB_ACCESS_TOKEN is missing from environment variables`,
    );
  }
  if (!token.startsWith('ghp_')) {
    throw new Error(
      `GITHUB_ACCESS_TOKEN is not a valid value. It should start with 'ghp_'`,
    );
  }
  return token;
}


================================================
FILE: packages/back-nest/src/database.module.ts
================================================
import { TypeOrmModule } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { pgOptions } from './config/postgres';

const entities = [__dirname + '/**/*.entity.{ts,js}'];

export const PostgresDataSource = new DataSource({
  type: 'postgres',
  synchronize: true,
  entities,
  ...pgOptions,
});

export const PostgresModule = TypeOrmModule.forRootAsync({
  useFactory: () => {
    return {
      type: 'postgres',
      synchronize: true,
      entities,
      ...pgOptions,
    };
  },
  dataSourceFactory: async () => {
    return PostgresDataSource.initialize();
  },
});


================================================
FILE: packages/back-nest/src/filters/exception.filter.ts
================================================
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { QueryFailedError } from 'typeorm';

interface ErrorResponse {
  message: string;
  status: number;
}

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();

    let errorResponse: ErrorResponse;
    const message = exception.message;

    if (exception instanceof HttpException) {
      errorResponse = { status: exception.getStatus(), message };
    } else if (exception instanceof QueryFailedError) {
      errorResponse = {
        message: 'Internal server error',
        status: HttpStatus.INTERNAL_SERVER_ERROR,
      };
    } else {
      errorResponse = {
        message: 'Internal server error',
        status: HttpStatus.INTERNAL_SERVER_ERROR,
      };
    }

    if (errorResponse.status === HttpStatus.INTERNAL_SERVER_ERROR) {
      console.log(exception);
    }

    response.status(errorResponse.status).json({
      statusCode: errorResponse.status,
      message: errorResponse.message,
    });
  }
}


================================================
FILE: packages/back-nest/src/main.ts
================================================
import * as Sentry from '@sentry/node';
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
import { getAllowedOrigins } from './config/cors';
import { guestUserMiddleware } from './middlewares/guest-user';
import { SessionAdapter } from './sessions/session.adapter';
import { getSessionMiddleware } from './sessions/session.middleware';
import { json } from 'express';
import { AllExceptionsFilter } from './filters/exception.filter';

const GLOBAl_API_PREFIX = 'api';

async function runServer() {
  Sentry.init({
    dsn: process.env.SENTRY_DSN,
    tracesSampleRate: 0,
  });
  const port = process.env.PORT || 1337;
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.set('trust proxy', 1);
  const sessionMiddleware = getSessionMiddleware();
  app.enableCors({
    origin: getAllowedOrigins(),
    credentials: true,
  });
  app.use(json({ limit: '50mb' }));
  app.use(sessionMiddleware);
  app.use(guestUserMiddleware);
  app.useWebSocketAdapter(new SessionAdapter(app, sessionMiddleware));
  app.setGlobalPrefix(GLOBAl_API_PREFIX);
  app.useGlobalFilters(new AllExceptionsFilter());
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(port);
}

runServer();


================================================
FILE: packages/back-nest/src/middlewares/guest-user.ts
================================================
import { NextFunction, Request, Response } from 'express';
import { User } from 'src/users/entities/user.entity';

export function guestUserMiddleware(
  req: Request,
  _: Response,
  next: NextFunction,
) {
  if (req.session && !req.session?.user) {
    req.session.user = User.generateAnonymousUser();
  }
  next();
}


================================================
FILE: packages/back-nest/src/projects/commands/import-untracked-projects-runner.ts
================================================
import { Command, CommandRunner } from 'nest-commander';
import { ProjectService } from '../services/project.service';
import { ProjectsFromFileReader } from '../services/projects-from-file-reader';
import { UntrackedProjectService } from '../services/untracked-projects.service';

@Command({
  name: 'import-projects',
  arguments: '',
  options: {},
})
export class ImportUntrackedProjectsRunner extends CommandRunner {
  constructor(
    private reader: ProjectsFromFileReader,
    private untracked: UntrackedProjectService,
    private synced: ProjectService,
  ) {
    super();
  }
  async run(): Promise<void> {
    for await (const project of this.reader.readProjects()) {
      const syncedProject = await this.synced.findByFullName(project);
      if (!syncedProject) {
        await this.untracked.bulkUpsert([project]);
        console.info(`[ProjectImport]: Imported ${project}`);
      }
    }
  }
}


================================================
FILE: packages/back-nest/src/projects/commands/sync-untracked-projects-runner.ts
================================================
import { Command, CommandRunner } from 'nest-commander';
import { GithubAPI } from 'src/connectors/github/services/github-api';
import { Project } from '../entities/project.entity';
import { ProjectService } from '../services/project.service';
import { UntrackedProjectService } from '../services/untracked-projects.service';

@Command({
  name: 'sync-projects',
  arguments: '',
  options: {},
})
export class SyncUntrackedProjectsRunner extends CommandRunner {
  constructor(
    private untracked: UntrackedProjectService,
    private api: GithubAPI,
    private synced: ProjectService,
  ) {
    super();
  }
  async run(): Promise<void> {
    const untracked = await this.untracked.findAll();
    for (const untrackedProject of untracked) {
      const repository = await this.api.fetchRepository(
        untrackedProject.fullName,
      );
      const project = Project.fromGithubRepository(
        untrackedProject,
        repository,
      );
      await this.synced.bulkUpsert([project]);
      await this.untracked.remove([untrackedProject]);
      console.info(`[ProjectSync]: Synced ${project.fullName}`);
    }
  }
}


================================================
FILE: packages/back-nest/src/projects/entities/project.entity.ts
================================================
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { GithubRepository } from 'src/connectors/github/schemas/github-repository.dto';
import { UntrackedProject } from './untracked-project.entity';
import { UnsyncedFile } from 'src/challenges/entities/unsynced-file.entity';

@Entity()
export class Project {
  @PrimaryGeneratedColumn('uuid')
  id: string;
  @Column({ unique: true })
  fullName: string;
  @Column({ unique: true })
  htmlUrl: string;
  @Column()
  language: string;
  @Column()
  stars: number;
  @Column()
  licenseName: string;
  @Column()
  ownerAvatar: string;
  @Column()
  defaultBranch: string;

  @OneToMany(() => UnsyncedFile, (file) => file.project)
  files: File[];

  @Column({ nullable: true })
  syncedSha?: string;

  static fromGithubRepository(
    tracked: UntrackedProject,
    repo: GithubRepository,
  ) {
    const project = new Project();
    project.fullName = tracked.fullName;
    project.htmlUrl = repo.html_url;
    project.stars = repo.stargazers_count;
    project.language = repo.language;
    project.licenseName = repo.license?.name ?? 'Other';
    project.ownerAvatar = repo.owner.avatar_url;
    project.defaultBranch = repo.default_branch;
    return project;
  }
}


================================================
FILE: packages/back-nest/src/projects/entities/untracked-project.entity.ts
================================================
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class UntrackedProject {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ unique: true })
  fullName: string;
}


================================================
FILE: packages/back-nest/src/projects/project.controller.ts
================================================
import { Controller, Get } from '@nestjs/common';
import { ProjectService } from './services/project.service';

@Controller('projects')
export class ProjectController {
  constructor(private projectService: ProjectService) {}
  @Get('languages')
  getLeaderboard(): Promise<string[]> {
    return this.projectService.getLanguages();
  }
}


================================================
FILE: packages/back-nest/src/projects/projects.module.ts
================================================
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { GithubConnectorModule } from 'src/connectors/github/github.module';
import { ImportUntrackedProjectsRunner } from './commands/import-untracked-projects-runner';
import { SyncUntrackedProjectsRunner } from './commands/sync-untracked-projects-runner';
import { Project } from './entities/project.entity';
import { UntrackedProject } from './entities/untracked-project.entity';
import { ProjectController } from './project.controller';
import { ProjectService } from './services/project.service';
import { ProjectsFromFileReader } from './services/projects-from-file-reader';
import { UntrackedProjectService } from './services/untracked-projects.service';

@Module({
  imports: [
    TypeOrmModule.forFeature([Project, UntrackedProject]),
    GithubConnectorModule,
  ],
  providers: [
    UntrackedProjectService,
    ProjectService,
    ProjectsFromFileReader,
    ImportUntrackedProjectsRunner,
    SyncUntrackedProjectsRunner,
  ],
  controllers: [ProjectController],
  exports: [ProjectService],
})
export class ProjectsModule {}


================================================
FILE: packages/back-nest/src/projects/services/project.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Project } from '../entities/project.entity';

@Injectable()
export class ProjectService {
  constructor(
    @InjectRepository(Project)
    private projectRepository: Repository<Project>,
  ) {}

  async bulkUpsert(projects: Project[]): Promise<void> {
    await this.projectRepository.upsert(projects, ['fullName']);
  }

  async findByFullName(fullName: string) {
    const project = await this.projectRepository.findOneBy({
      fullName,
    });
    return project;
  }

  async updateSyncedSha(id: string, syncedSha: string) {
    await this.projectRepository.update(
      {
        id,
      },
      { syncedSha },
    );
  }

  async findAll(): Promise<Project[]> {
    const projects = await this.projectRepository.find();
    return projects;
  }

  async getLanguages(): Promise<string[]> {
    const selectedLanguages = await this.projectRepository
      .createQueryBuilder()
      .select('language')
      .distinct()
      .execute();
    return selectedLanguages.map((l: any) => l.language);
  }
}


================================================
FILE: packages/back-nest/src/projects/services/projects-from-file-reader.ts
================================================
import { Injectable } from '@nestjs/common';
import { createReadStream } from 'fs';
import { createInterface } from 'readline';

@Injectable()
export class ProjectsFromFileReader {
  private static FILE_PATH = './tracked-projects.txt';
  async *readProjects() {
    const stream = createReadStream(ProjectsFromFileReader.FILE_PATH);
    const rl = createInterface({
      input: stream,
      crlfDelay: Infinity,
    });
    for await (const line of rl) {
      const slug = line.trim();
      yield validateProjectName(slug);
    }
  }
}

export function validateProjectName(slug: string) {
  let [owner, repo] = slug.split('/');
  owner = owner.trim();
  repo = repo.trim();
  if (!owner || !repo) {
    throw new Error(slug);
  }
  return [owner, repo].join('/');
}


================================================
FILE: packages/back-nest/src/projects/services/untracked-projects.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UntrackedProject } from '../entities/untracked-project.entity';

@Injectable()
export class UntrackedProjectService {
  constructor(
    @InjectRepository(UntrackedProject)
    private untrackedProjects: Repository<UntrackedProject>,
  ) {}

  async bulkUpsert(names: string[]): Promise<void> {
    const partialProjects = names.map((fullName) => ({ fullName }));
    await this.untrackedProjects.upsert(partialProjects, ['fullName']);
  }

  async remove(untrackedProjects: UntrackedProject[]): Promise<void> {
    await this.untrackedProjects.remove(untrackedProjects);
  }

  async findAll(): Promise<UntrackedProject[]> {
    return await this.untrackedProjects.find();
  }
}


================================================
FILE: packages/back-nest/src/races/entities/race-settings.dto.ts
================================================
import { IsBoolean, IsOptional, IsString } from 'class-validator';

export class RaceSettingsDTO {
  @IsString()
  @IsOptional()
  language: string;

  @IsBoolean()
  @IsOptional()
  isPublic: boolean;
}


================================================
FILE: packages/back-nest/src/races/race.controllers.ts
================================================
import {
  BadRequestException,
  Controller,
  Get,
  Param,
  Post,
  Req,
} from '@nestjs/common';
import { PublicRace, RaceManager } from './services/race-manager.service';
import { Request } from 'express';

@Controller('races')
export class RacesController {
  constructor(private raceManager: RaceManager) {}
  @Get()
  getRaces(): PublicRace[] {
    return this.raceManager.getPublicRaces();
  }

  @Get('online')
  getOnlineCount(): { online: number } {
    const online = this.raceManager.getOnlineCount();
    return {
      online,
    };
  }

  @Post('online')
  toggleOnlineState(@Req() request: Request): { isPublic: boolean } {
    const userId = request.session.user.id;
    const raceId = request.session.raceId;
    const race = this.raceManager.getRace(raceId);
    if (race.owner !== userId) {
      throw new BadRequestException();
    }
    const isPublic = race.togglePublic();
    return {
      isPublic,
    };
  }

  @Get(':raceId/status')
  getRaceStatus(
    @Req() request: Request,
    @Param('raceId') raceId: string,
  ): { ok: boolean } {
    try {
      const userId = request.session.user.id;
      const player = this.raceManager.getPlayer(raceId, userId);
      return { ok: !!player };
    } catch (err) {
      return { ok: false };
    }
  }
}


================================================
FILE: packages/back-nest/src/races/race.exceptions.ts
================================================
import * as Sentry from '@sentry/node';
import { ArgumentsHost, Catch } from '@nestjs/common';
import { BaseWsExceptionFilter } from '@nestjs/websockets';
import { Socket } from 'socket.io';
import { InvalidKeystrokeException } from './services/keystroke-validator.service';
import { RaceEvents } from './services/race-events.service';
import { RaceDoesNotExist } from './services/race-manager.service';
import { SessionState } from './services/session-state.service';

export function getSocketFromArgs(host: ArgumentsHost): Socket {
  const args = host.getArgs();
  for (const arg of args) {
    if (arg instanceof Socket) {
      return arg;
    }
  }
}

@Catch(RaceDoesNotExist)
export class RaceDoesNotExistFilter extends BaseWsExceptionFilter {
  raceEvents: RaceEvents;
  sessionState: SessionState;
  constructor() {
    super();
    this.raceEvents = new RaceEvents();
    this.sessionState = new SessionState();
  }

  async catch(error: RaceDoesNotExist, host: ArgumentsHost) {
    const socket = getSocketFromArgs(host);
    this.sessionState.removeRaceID(socket);
    this.raceEvents.raceDoesNotExist(socket, error.id);
  }
}

@Catch(InvalidKeystrokeException)
export class InvalidKeystrokeFilter extends BaseWsExceptionFilter {
  async catch(error: InvalidKeystrokeException) {
    Sentry.withScope((scope) => {
      const player = error.race.members[error.userId];
      const data = {
        challengeId: error.race.challenge.id,
        expected: error.expected,
        input: error.input,
        keystroke: error.keystroke,
        userId: error.userId,
      };
      const typedKeystrokes = player.typedKeyStrokes;
      const validKeyStrokes = player.validKeyStrokes();
      scope.setUser({
        id:
          process.env.NODE_ENV === 'production'
            ? `${error.race.id}-${error.userId}`
            : `[local-testing] ${error.race.id}`,
      });
      scope.setExtras({ error: data, typedKeystrokes, validKeyStrokes });
      Sentry.captureException(error);
    });
  }
}


================================================
FILE: packages/back-nest/src/races/race.gateway.ts
================================================
import { UseFilters, UsePipes, ValidationPipe } from '@nestjs/common';
import {
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { gatewayMetadata } from 'src/config/cors';
import { RaceSettingsDTO } from './entities/race-settings.dto';
import {
  InvalidKeystrokeFilter,
  RaceDoesNotExistFilter,
} from './race.exceptions';
import { AddKeyStrokeService } from './services/add-keystroke.service';
import { CountdownService } from './services/countdown.service';
import { Locker } from './services/locker.service';
import { RaceEvents } from './services/race-events.service';
import { RaceManager } from './services/race-manager.service';
import { KeystrokeDTO } from './services/race-player.service';
import { SessionState } from './services/session-state.service';

@WebSocketGateway(gatewayMetadata)
export class RaceGateway {
  @WebSocketServer()
  server: Server;

  constructor(
    private raceManager: RaceManager,
    private session: SessionState,
    private raceEvents: RaceEvents,
    private addKeyStrokeService: AddKeyStrokeService,
    private manageRaceLock: Locker,
    private countdownService: CountdownService,
  ) {}

  afterInit(server: Server) {
    console.info('[SpeedTyper.dev] Websocket Server Started.');
    this.raceEvents.server = server;
  }

  handleDisconnect(socket: Socket) {
    console.info(
      `Client disconnected: ${socket.request.session.user.username}`,
    );
    const raceId = this.session.getRaceID(socket);
    const user = this.session.getUser(socket);
    this.raceManager.leaveRace(user, raceId);
    this.session.removeRaceID(socket);
    this.manageRaceLock.release(socket.id);
  }

  async handleConnection(socket: Socket) {
    const userId = this.session.getUser(socket).id;
    const userIsAlreadyPlaying = this.raceManager.userIsAlreadyPlaying(userId);
    for (const [sid, s] of this.server.sockets.sockets) {
      // We need to cleanup other sockets for the same user
      // Because we can not have several instances of the same user in the same race twice
      // Consider adding this possibility, but for different races
      if (sid === socket.id) {
        console.log('Same socket id, keeping.');
        continue;
      }
      if (s.request.session.user.id === userId) {
        console.log(
          'Different socket id, same user. Disconnecting previous socket',
        );
        if (userIsAlreadyPlaying) {
          this.raceManager.leaveRace(
            s.request.session.user,
            s.request.session.raceId,
          );
        }
        s.disconnect();
      }

      if (!this.raceManager.userIsAlreadyPlaying(s.request.session.user.id)) {
        console.log(
          'Disconnecting because socket is not playing: ',
          s.request.session.user.username,
          s.request.session.user.id,
        );
        s.disconnect();
        continue;
      }

      console.log(
        'Keeping: ',
        s.request.session.user.username,
        s.request.session.user.id,
      );
    }

    console.info(
      `Client connected: ${socket.request.session.user.username} - ${socket.id}`,
    );
  }

  @UseFilters(new RaceDoesNotExistFilter())
  @SubscribeMessage('refresh_challenge')
  async onRefreshChallenge(socket: Socket, settings: RaceSettingsDTO) {
    this.raceEvents.logConnectedSockets();
    const socketID = socket.id;
    await this.manageRaceLock.runIfOpen(socketID, async () => {
      const raceId = this.session.getRaceID(socket);
      if (!raceId) {
        this.manageRaceLock.release(socket.id);
        this.onPlay(socket, settings);
        return;
      }
      const user = this.session.getUser(socket);
      if (this.raceManager.isOwner(user.id, raceId)) {
        const race = await this.raceManager.refresh(raceId, settings.language);
        this.raceEvents.updatedRace(socket, race);
      }
    });
  }

  @UsePipes(new ValidationPipe())
  @SubscribeMessage('play')
  async onPlay(socket: Socket, settings: RaceSettingsDTO) {
    const socketID = socket.id;
    await this.manageRaceLock.runIfOpen(socketID, async () => {
      const user = this.session.getUser(socket);
      const raceId = this.session.getRaceID(socket);
      this.raceManager.leaveRace(user, raceId);
      const race = await this.raceManager.create(user, settings);
      this.raceEvents.createdRace(socket, race);
      this.session.saveRaceID(socket, race.id);
    });
  }

  @UseFilters(new RaceDoesNotExistFilter(), new InvalidKeystrokeFilter())
  @UsePipes(new ValidationPipe())
  @SubscribeMessage('key_stroke')
  async onKeyStroke(socket: Socket, keystroke: KeystrokeDTO) {
    keystroke.timestamp = new Date().getTime();
    this.addKeyStrokeService.validate(socket, keystroke);
    this.addKeyStrokeService.addKeyStroke(socket, keystroke);
  }

  @SubscribeMessage('join')
  async onJoin(socket: Socket, id: string) {
    this.manageRaceLock.runIfOpen(socket.id, async () => {
      const user = this.session.getUser(socket);
      const raceID = this.session.getRaceID(socket);
      this.raceManager.leaveRace(user, raceID);
      const race = this.raceManager.join(user, id);
      if (!race) {
        // if there is no race with the ID in the state
        // we recreate a race for the user
        // this makes sure that the game does not crash for the user
        // TODO: we should create a race with the same ID, and even same challenge selected
        // So that the other people in the race can then join the same room
        // instead of creating their own through this same functionality
        // we do however have to reset the progress for all participants as it is only kept in state
        this.manageRaceLock.release(socket.id);
        return this.onPlay(socket, { language: undefined, isPublic: false });
      }
      this.raceEvents.joinedRace(socket, race, user);
      this.session.saveRaceID(socket, id);
    });
  }

  @SubscribeMessage('start_race')
  async onStart(socket: Socket) {
    const user = this.session.getUser(socket);
    const raceID = this.session.getRaceID(socket);
    const race = this.raceManager.getRace(raceID);
    if (race.canStartRace(user.id)) {
      this.countdownService.countdown(race);
    }
  }
}


================================================
FILE: packages/back-nest/src/races/races.module.ts
================================================
import { Module } from '@nestjs/common';
import { ChallengesModule } from 'src/challenges/challenges.module';
import { ResultsModule } from 'src/results/results.module';
import { TrackingModule } from 'src/tracking/tracking.module';
import { RacesController } from './race.controllers';
import { RaceGateway } from './race.gateway';
import { AddKeyStrokeService } from './services/add-keystroke.service';
import { CountdownService } from './services/countdown.service';
import { KeyStrokeValidationService } from './services/keystroke-validator.service';
import { Locker } from './services/locker.service';
import { ProgressService } from './services/progress.service';
import { RaceEvents } from './services/race-events.service';
import { RaceManager } from './services/race-manager.service';
import { ResultsHandlerService } from './services/results-handler.service';
import { SessionState } from './services/session-state.service';

@Module({
  imports: [ChallengesModule, ResultsModule, TrackingModule],
  controllers: [RacesController],
  providers: [
    AddKeyStrokeService,
    KeyStrokeValidationService,
    ProgressService,
    RaceEvents,
    RaceGateway,
    RaceManager,
    ResultsHandlerService,
    SessionState,
    Locker,
    CountdownService,
  ],
  exports: [RaceManager, RaceEvents, KeyStrokeValidationService],
})
export class RacesModule {}


================================================
FILE: packages/back-nest/src/races/services/add-keystroke.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { Socket } from 'socket.io';
import { TrackingService } from 'src/tracking/tracking.service';
import { KeyStrokeValidationService } from './keystroke-validator.service';
import { ProgressService } from './progress.service';
import { RaceEvents } from './race-events.service';
import { RaceManager } from './race-manager.service';
import { KeystrokeDTO } from './race-player.service';
import { ResultsHandlerService } from './results-handler.service';
import { SessionState } from './session-state.service';

@Injectable()
export class AddKeyStrokeService {
  constructor(
    private manager: RaceManager,
    private session: SessionState,
    private validator: KeyStrokeValidationService,
    private progressService: ProgressService,
    private trackingService: TrackingService,
    private events: RaceEvents,
    private resultHandler: ResultsHandlerService,
  ) {}

  validate(socket: Socket, keyStroke: KeystrokeDTO) {
    const user = this.session.getUser(socket);
    const raceId = this.session.getRaceID(socket);
    const player = this.manager.getPlayer(raceId, user.id);
    this.validator.validateKeyStroke(player, keyStroke);
  }

  async addKeyStroke(socket: Socket, keyStroke: KeystrokeDTO) {
    const user = this.session.getUser(socket);
    const raceId = this.session.getRaceID(socket);
    const player = this.manager.getPlayer(raceId, user.id);
    if (player.hasNotStartedTyping()) {
      this.trackingService.trackRaceStarted();
    }
    player.addKeyStroke(keyStroke);
    if (keyStroke.correct) {
      player.progress = this.progressService.calculateProgress(player);
      const code = this.manager.getCode(raceId);
      player.updateLiteral(code, keyStroke);
      this.events.progressUpdated(socket, raceId, player);
    }
    this.syncStartTime(raceId, new Date(keyStroke.timestamp));
    const race = this.manager.getRace(raceId);
    this.resultHandler.handleResult(race, user);
  }

  async syncStartTime(raceId: string, timestamp: Date) {
    const race = this.manager.getRace(raceId);
    if (!race.isMultiplayer()) {
      race.startTime = race.startTime ?? timestamp;
    }
  }
}


================================================
FILE: packages/back-nest/src/races/services/countdown.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { RaceEvents } from './race-events.service';
import { Race } from './race.service';

@Injectable()
export class CountdownService {
  constructor(private raceEvents: RaceEvents) {}
  async countdown(race: Race) {
    race.countdown = true;
    const seconds = 5;
    for (let i = seconds; i > 0; i--) {
      const delay = seconds - i;
      const timeout = setTimeout(() => {
        this.raceEvents.countdown(race.id, i);
      }, delay * 1000);
      race.timeouts.push(timeout);
    }
    const timeout = setTimeout(() => {
      race.start();
      this.raceEvents.raceStarted(race);
      race.timeouts = [];
      race.countdown = false;
    }, seconds * 1000);
    race.timeouts.push(timeout);
  }
}


================================================
FILE: packages/back-nest/src/races/services/keystroke-validator.service.ts
================================================
import { BadRequestException, Injectable } from '@nestjs/common';
import { Challenge } from 'src/challenges/entities/challenge.entity';
import { RaceManager } from './race-manager.service';
import { KeystrokeDTO, RacePlayer } from './race-player.service';
import { Race } from './race.service';

export class InvalidKeystrokeException extends Error {
  userId: string;
  keystroke: KeystrokeDTO;
  input: string;
  expected: string;
  race: Race;
  constructor(
    userId: string,
    keystroke: KeystrokeDTO,
    userInput: string,
    expectedUserInput: string,
    race: Race,
  ) {
    super('Unexpected keystroke received2');
    this.userId = userId;
    this.keystroke = keystroke;
    this.input = userInput;
    this.expected = expectedUserInput;
    this.race = race;
  }
}

export class RaceNotStartedException extends BadRequestException {
  constructor() {
    super('Race not started');
  }
}

export function getCurrentInputBeforeKeystroke(
  player: RacePlayer,
  keystroke: KeystrokeDTO,
) {
  const currentInputBeforeKey = player
    .validKeyStrokes()
    .filter((stroke) => stroke.index < keystroke.index)
    .map((stroke) => stroke.key)
    .join('');
  return currentInputBeforeKey;
}

@Injectable()
export class KeyStrokeValidationService {
  constructor(private raceManager: RaceManager) {}

  validateKeyStroke(player: RacePlayer, recentKeyStroke: KeystrokeDTO) {
    this.validateRaceStarted(player.raceId);
    const currentInputBeforeKey = getCurrentInputBeforeKeystroke(
      player,
      recentKeyStroke,
    );
    const userInput = currentInputBeforeKey + recentKeyStroke.key;
    const expectedInput = this.getStrippedCode(player.raceId, recentKeyStroke);
    const correct = userInput === expectedInput;
    if (recentKeyStroke.correct && recentKeyStroke.correct !== correct) {
      throw new InvalidKeystrokeException(
        player.id,
        recentKeyStroke,
        userInput,
        expectedInput,
        this.raceManager.getRace(player.raceId),
      );
    }
  }

  validateRaceStarted(raceID: string) {
    const race = this.raceManager.getRace(raceID);
    if (!race.startTime && race.isMultiplayer()) {
      throw new RaceNotStartedException();
    }
  }

  private getStrippedCode(raceId: string, keystroke: KeystrokeDTO) {
    const code = this.raceManager.getCode(raceId);
    const strippedCode = Challenge.getStrippedCode(
      code.substring(0, keystroke.index),
    );
    return strippedCode;
  }
}


================================================
FILE: packages/back-nest/src/races/services/locker.service.ts
================================================
import { Injectable } from '@nestjs/common';

@Injectable()
export class Locker {
  lockedIDs: Set<string>;
  constructor() {
    this.lockedIDs = new Set<string>();
  }

  // this is a global lock function
  // it locks all run methods called with this lockid
  // even if they are coming from different classes
  async runIfOpen<T>(lockID: string, callback: () => Promise<T>): Promise<T> {
    if (this.lockedIDs.has(lockID)) {
      return;
    }
    this.lockedIDs.add(lockID);
    try {
      return await callback();
    } finally {
      this.lockedIDs.delete(lockID);
    }
  }

  release(id: string) {
    this.lockedIDs.delete(id);
  }
}


================================================
FILE: packages/back-nest/src/races/services/progress.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { Challenge } from 'src/challenges/entities/challenge.entity';
import { RaceManager } from './race-manager.service';
import { RacePlayer } from './race-player.service';

@Injectable()
export class ProgressService {
  constructor(private raceManager: RaceManager) {}
  calculateProgress(player: RacePlayer) {
    const currentInput = player.getValidInput();
    const code = this.raceManager.getCode(player.raceId);
    const strippedFullCode = Challenge.getStrippedCode(code);
    const progress = Math.floor(
      (currentInput.length / strippedFullCode.length) * 100,
    );
    return progress;
  }
}


================================================
FILE: packages/back-nest/src/races/services/race-events.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { Server, Socket } from 'socket.io';
import { Result } from 'src/results/entities/result.entity';
import { User } from 'src/users/entities/user.entity';
import { RacePlayer } from './race-player.service';
import { Race } from './race.service';

@Injectable()
export class RaceEvents {
  server: Server;

  getPlayerCount() {
    return this.server.sockets.sockets.size;
  }

  createdRace(socket: Socket, race: Race) {
    socket.join(race.id);
    socket.emit('race_joined', race);
    socket.emit('challenge_selected', race.challenge);
  }

  countdown(raceID: string, i: number) {
    const event = 'countdown';
    this.server.to(raceID).emit(event, i);
  }

  raceStarted(race: Race) {
    this.server.to(race.id).emit('race_started', race.startTime);
  }

  updatedRace(_: Socket, race: Race) {
    this.server.to(race.id).emit('race_joined', race);
    this.server.to(race.id).emit('challenge_selected', race.challenge);
  }

  joinedRace(socket: Socket, race: Race, user: User) {
    socket.join(race.id);
    socket.emit('race_joined', race);
    socket.to(race.id).emit('member_joined', race.members[user.id]);
  }

  leftRace(race: Race, user: User) {
    this.server.to(race.id).emit('member_left', {
      member: user.id,
      owner: race.owner,
    });
  }

  progressUpdated(socket: Socket, raceId: string, player: RacePlayer) {
    socket.to(raceId).emit('progress_updated', player);
    socket.emit('progress_updated', player);
  }

  raceCompleted(raceId: string, result: Result) {
    this.server.to(raceId).emit('race_completed', result);
  }

  raceDoesNotExist(socket: Socket, id: string) {
    socket.emit('race_does_not_exist', id);
  }
  async logConnectedSockets() {
    const sockets = await this.server.fetchSockets();
    console.log('Connected sockets: ', sockets.length);
  }
}


================================================
FILE: packages/back-nest/src/races/services/race-manager.service.ts
================================================
import { BadRequestException, Injectable } from '@nestjs/common';
import { Challenge } from 'src/challenges/entities/challenge.entity';
import { ChallengeService } from 'src/challenges/services/challenge.service';
import { LiteralService } from 'src/challenges/services/literal.service';
import { User } from 'src/users/entities/user.entity';
import { RaceSettingsDTO } from '../entities/race-settings.dto';
import { RaceEvents } from './race-events.service';
import { RacePlayer } from './race-player.service';
import { Race } from './race.service';

export interface PublicRace {
  id: string;
  ownerName: string;
  memberCount: number;
}

@Injectable()
export class RaceManager {
  private races: Record<string, Race> = {};

  constructor(
    private challengeService: ChallengeService,
    private literalsService: LiteralService,
    private raceEvents: RaceEvents,
  ) {}

  getOnlineCount(): number {
    const memberIds = Object.values(this.races)
      .flatMap((race) => Object.values(race.members))
      .map((member) => member.id);
    const uniqueMemberIds = new Set(memberIds);
    return uniqueMemberIds.size;
  }

  getPublicRaces(): PublicRace[] {
    const races = Object.values(this.races);
    const publicRaces = races
      .filter((race) => race.isPublic)
      .map((race) => {
        return race.toPublic();
      });
    return publicRaces;
  }

  syncUser(raceId: string, prevUserId: string, user: User) {
    const race = this.getRace(raceId);
    if (race.owner === prevUserId) {
      race.owner = user.id;
    }
    const player = race.members[prevUserId];
    player.id = user.id;
    player.username = user.username;
    delete race.members[prevUserId];
    race.members[user.id] = player;
  }

  debugSize(msg: string) {
    const racesSize = JSON.stringify(this.races).length;
    console.log(msg, {
      racesSize,
      races: Object.keys(this.races).length,
      players: this.getOnlineCount(),
    });
  }
  async create(user: User, settings: RaceSettingsDTO): Promise<Race> {
    this.debugSize('create');
    const challenge = await this.challengeService.getRandom(settings.language);
    const literals = this.literalsService.calculateLiterals(challenge.content);
    const race = new Race(user, challenge, literals);
    race.isPublic = settings.isPublic;
    this.races[race.id] = race;
    return race;
  }

  async refresh(id: string, language?: string): Promise<Race> {
    this.debugSize('refresh');
    const race = this.getRace(id);
    const challenge = await this.challengeService.getRandom(language);
    const literals = this.literalsService.calculateLiterals(challenge.content);
    race.challenge = challenge;
    race.literals = literals;
    race.resetProgress();
    return race;
  }

  getRace(id: string): Race {
    const race = this.races[id];
    if (!race) throw new RaceDoesNotExist(id);
    return race;
  }

  getPlayer(raceId: string, userId: string): RacePlayer {
    const race = this.getRace(raceId);
    return race.getPlayer(userId);
  }

  getChallenge(raceId: string): Challenge {
    const race = this.getRace(raceId);
    return race.challenge;
  }

  // Get the full code string of the currently active challenge for the provided race id
  getCode(raceId: string): string {
    return this.getChallenge(raceId).content;
  }

  join(user: User, raceId: string): Race | null {
    const race = this.races[raceId];
    // it's important to return null instead of throwing
    // a RaceDoesNotExist error because the exception filter
    // sends a race_does_not_exist event back to the client
    // and the client tries to join the race
    // in the controller we create a game if no game exists
    // preventing an infinite loop
    // TODO: this should be handled better in the future
    if (!race) return null;
    race.addMember(user);
    return race;
  }

  leaveRace(user: User, raceId: string) {
    const race = this.races[raceId];
    if (!race) return;
    race.removeMember(user);
    if (Object.values(race.members).length === 0) {
      delete this.races[raceId];
    } else if (race.owner === user.id) {
      race.owner = Object.values(race.members)[0].id;
    }
    this.raceEvents.leftRace(race, user);
  }

  isOwner(userId: string, raceId: string): boolean {
    const race = this.races[raceId];
    if (!race) throw new RaceDoesNotExist(raceId);
    return race.owner === userId;
  }

  userIsAlreadyPlaying(userId: string): boolean {
    return Object.values(this.races)
      .flatMap((race) => Object.keys(race.members))
      .includes(userId);
  }
}

export class RaceDoesNotExist extends BadRequestException {
  id: string;
  constructor(id: string) {
    super(`Race with id=${id} does not exist`);
    this.id = id;
    Object.setPrototypeOf(this, RaceDoesNotExist.prototype);
  }
}


================================================
FILE: packages/back-nest/src/races/services/race-player.service.ts
================================================
import { Exclude, instanceToPlain } from 'class-transformer';
import {
  IsBoolean,
  IsNotEmpty,
  IsNumber,
  IsString,
  MaxLength,
} from 'class-validator';
import { LiteralService } from 'src/challenges/services/literal.service';
import { User } from 'src/users/entities/user.entity';

export class KeystrokeDTO {
  @IsString()
  @IsNotEmpty()
  @MaxLength(1)
  key: string;
  @IsNotEmpty()
  @IsNumber()
  timestamp: number;
  @IsNotEmpty()
  @IsBoolean()
  correct: boolean;
  @IsNotEmpty()
  @IsNumber()
  index: number;
}

export class RacePlayer {
  id: string;
  username: string;

  recentlyTypedLiteral: string;

  @Exclude()
  literalOffset: number;

  @Exclude()
  literals: string[];

  @Exclude()
  saved: boolean;

  progress: number;

  @Exclude()
  raceId: string;

  @Exclude()
  typedKeyStrokes: KeystrokeDTO[];

  @Exclude()
  literalService: LiteralService;

  toJSON() {
    return instanceToPlain(this);
  }

  reset(literals: string[]) {
    this.literals = literals;
    this.literalOffset = 0;
    this.recentlyTypedLiteral = this.literals[this.literalOffset];
    this.progress = 0;
    this.saved = false;
    this.typedKeyStrokes = [];
  }

  validKeyStrokes() {
    const keyStrokes = this.typedKeyStrokes;
    const latestKeyStrokePerIndex = Object.fromEntries(
      keyStrokes.map((keyStroke) => {
        return [keyStroke.index, keyStroke];
      }),
    );
    const firstIncorrectKeystroke = Object.values(latestKeyStrokePerIndex).find(
      (keystroke) => !keystroke.correct,
    );
    const validKeyStrokes = Object.values(latestKeyStrokePerIndex)
      .filter((keyStroke) => keyStroke.correct)
      .filter((keystroke) =>
        firstIncorrectKeystroke
          ? keystroke.index < firstIncorrectKeystroke.index
          : true,
      );
    return validKeyStrokes;
  }

  incorrectKeyStrokes() {
    const incorrectKeyStrokes = this.typedKeyStrokes.filter(
      (keyStroke) => !keyStroke.correct,
    );
    return incorrectKeyStrokes;
  }

  getValidInput() {
    const validInput = this.validKeyStrokes()
      .map((keyStroke) => keyStroke.key)
      .join('');
    return validInput;
  }

  addKeyStroke(keyStroke: KeystrokeDTO) {
    keyStroke.timestamp = new Date().getTime();
    this.typedKeyStrokes.push(keyStroke);
  }

  updateLiteral(code: string, keyStroke: KeystrokeDTO) {
    const untypedCode = code.substring(keyStroke.index);
    const nextLiteral = this.literals[this.literalOffset + 1];
    const startsWithNextLiteral = this.literalService
      .calculateLiterals(untypedCode.trimStart())
      .join('')
      .startsWith(nextLiteral);
    if (startsWithNextLiteral && this.literals.length > 1) {
      this.literalOffset++;
    }
    this.recentlyTypedLiteral = this.literals[this.literalOffset];
  }

  hasNotStartedTyping(): boolean {
    return this.typedKeyStrokes.length === 0;
  }

  hasCompletedRace(): boolean {
    return this.progress === 100;
  }

  static fromUser(raceId: string, user: User, literals: string[]) {
    const player = new RacePlayer();
    player.id = user.id;
    player.raceId = raceId;
    player.username = user.username;
    player.progress = 0;
    player.literals = literals;
    player.recentlyTypedLiteral = player.literals[0];
    player.literalOffset = 0;
    player.typedKeyStrokes = [];
    player.literalService = new LiteralService();
    player.saved = false;
    return player;
  }
}


================================================
FILE: packages/back-nest/src/races/services/race.service.ts
================================================
import { Exclude, instanceToPlain } from 'class-transformer';
import { randomUUID } from 'crypto';
import { Challenge } from 'src/challenges/entities/challenge.entity';
import { User } from 'src/users/entities/user.entity';
import { RacePlayer } from './race-player.service';

export interface PublicRace {
  id: string;
  ownerName: string;
  memberCount: number;
}

export class Race {
  id: string;
  challenge: Challenge;
  owner: string;
  members: Record<string, RacePlayer>;
  @Exclude()
  literals: string[];

  @Exclude()
  timeouts: NodeJS.Timeout[];

  startTime?: Date;

  @Exclude()
  countdown: boolean;

  isPublic: boolean;

  togglePublic(): boolean {
    this.isPublic = !this.isPublic;
    return this.isPublic;
  }
  toPublic(): PublicRace {
    const ownerName = this.members[this.owner].username;
    const memberCount = Object.keys(this.members).length;
    return {
      id: this.id,
      ownerName,
      memberCount,
    };
  }

  isMultiplayer(): boolean {
    return Object.keys(this.members).length > 1;
  }

  toJSON() {
    return instanceToPlain(this);
  }

  constructor(owner: User, challenge: Challenge, literals: string[]) {
    this.id = randomUUID().replaceAll('-', '');
    this.members = {};
    this.owner = owner.id;
    this.challenge = challenge;
    this.literals = literals;
    this.timeouts = [];
    this.countdown = false;
    this.addMember(owner);
    this.isPublic = false;
  }

  start() {
    this.startTime = new Date();
  }

  canStartRace(userID: string): boolean {
    return !this.countdown && !this.startTime && this.owner === userID;
  }

  getPlayer(id: string) {
    return this.members[id];
  }

  resetProgress() {
    Object.values(this.members).forEach((player) => {
      player.reset(this.literals);
    });
    this.startTime = undefined;
    for (const timeout of this.timeouts) {
      clearTimeout(timeout);
    }
    this.timeouts = [];
    this.countdown = false;
  }

  addMember(user: User) {
    this.members[user.id] = RacePlayer.fromUser(this.id, user, this.literals);
  }

  removeMember(user: User) {
    delete this.members[user.id];
  }
}


================================================
FILE: packages/back-nest/src/races/services/results-handler.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { ResultFactoryService } from 'src/results/services/result-factory.service';
import { ResultService } from 'src/results/services/results.service';
import { TrackingService } from 'src/tracking/tracking.service';
import { User } from 'src/users/entities/user.entity';
import { RaceEvents } from './race-events.service';
import { Race } from './race.service';

@Injectable()
export class ResultsHandlerService {
  constructor(
    private factory: ResultFactoryService,
    private events: RaceEvents,
    private results: ResultService,
    private tracker: TrackingService,
  ) {}
  async handleResult(race: Race, user: User) {
    const player = race.getPlayer(user.id);
    if (player.hasCompletedRace()) {
      if (player.saved) {
        return;
      }
      player.saved = true;
      let result = this.factory.factory(race, player, user);
      if (!user.isAnonymous) {
        result = await this.results.create(result);
      }
      result.percentile = await this.results.getResultPercentile(result.cpm);
      this.tracker.trackRaceCompleted();
      this.events.raceCompleted(race.id, result);
    }
  }
}


================================================
FILE: packages/back-nest/src/races/services/session-state.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { Socket } from 'socket.io';
import { User } from 'src/users/entities/user.entity';

@Injectable()
export class SessionState {
  getUser(socket: Socket): User {
    return socket.request.session.user;
  }

  getRaceID(socket: Socket): string {
    return socket.request.session.raceId;
  }

  saveRaceID(socket: Socket, id: string) {
    const prevRaceID = socket.request.session.raceId;
    socket.request.session.raceId = id;
    socket.request.session.save(() => {
      socket.leave(prevRaceID);
    });
  }

  removeRaceID(socket: Socket) {
    const prevRaceID = socket.request.session.raceId;
    socket.request.session.raceId = null;
    socket.request.session.save(() => {
      socket.leave(prevRaceID);
    });
  }
}


================================================
FILE: packages/back-nest/src/races/services/tests/race-player.service.spec.ts
================================================
import { RacePlayer } from '../race-player.service';

describe('[unit] validKeyStrokes()', () => {
  const player = new RacePlayer();

  beforeEach(() => {
    const typedKeystrokes = [
      { correct: true, index: 1, key: 'f', timestamp: 1676334891980 },
      { correct: true, index: 2, key: 'u', timestamp: 1676334892242 },
      { correct: true, index: 3, key: 'n', timestamp: 1676334892503 },
      { correct: true, index: 4, key: 'c', timestamp: 1676334892503 },
    ];
    player.typedKeyStrokes = typedKeystrokes;
  });

  it('should include all valid keystrokes', () => {
    const validKeyStrokes = player.validKeyStrokes();
    expect(validKeyStrokes).toEqual(player.typedKeyStrokes);
  });

  it('should filter out invalid keystrokes all valid keystrokes', () => {
    const expectedValidKeystrokes = [...player.typedKeyStrokes];
    player.typedKeyStrokes.push({
      correct: false,
      index: 5,
      key: 'd',
      timestamp: 1676334892503,
    });
    const validKeyStrokes = player.validKeyStrokes();
    expect(validKeyStrokes).toEqual(expectedValidKeystrokes);
  });

  it('should use the latest timestamp for each index', () => {
    const expectedValidKeystrokes = [player.typedKeyStrokes[0]];
    player.typedKeyStrokes.push({
      correct: false,
      index: 2,
      key: 'd',
      timestamp: 1676334892503,
    });
    const validKeyStrokes = player.validKeyStrokes();
    expect(validKeyStrokes).toEqual(expectedValidKeystrokes);
  });
});

describe('[functional] validKeyStrokes()', () => {
  const player = new RacePlayer();
  player.typedKeyStrokes = [
    { correct: true, index: 1, key: 'f', timestamp: 1676392785787 },
    { correct: true, index: 2, key: 'u', timestamp: 1676392785931 },
    { correct: true, index: 3, key: 'n', timestamp: 1676392786176 },
    { correct: true, index: 4, key: 'c', timestamp: 1676392786253 },
    { correct: true, index: 5, key: ' ', timestamp: 1676392786343 },
    { correct: true, index: 6, key: 'n', timestamp: 1676392786485 },
    { correct: true, index: 7, key: 'e', timestamp: 1676392786572 },
    { correct: true, index: 8, key: 'w', timestamp: 1676392786630 },
    { correct: true, index: 9, key: 'W', timestamp: 1676392786851 },
    { correct: true, index: 10, key: 'a', timestamp: 1676392787083 },
    { correct: true, index: 11, key: 't', timestamp: 1676392787162 },
    { correct: true, index: 12, key: 'c', timestamp: 1676392787392 },
    { correct: true, index: 13, key: 'h', timestamp: 1676392787460 },
    { correct: true, index: 14, key: 'e', timestamp: 1676392787566 },
    { correct: true, index: 15, key: 'r', timestamp: 1676392787696 },
    { correct: true, index: 16, key: 'G', timestamp: 1676392787997 },
    { correct: false, index: 17, key: 'B', timestamp: 1676392788000 },
    { correct: false, index: 18, key: 'r', timestamp: 1676392788196 },
    { correct: true, index: 17, key: 'r', timestamp: 1676392788777 },
    { correct: false, index: 18, key: 'u', timestamp: 1676392788952 },
    { correct: false, index: 19, key: 'o', timestamp: 1676392788963 },
    { correct: false, index: 20, key: 'o', timestamp: 1676392789165 },
    { correct: true, index: 18, key: 'o', timestamp: 1676392790150 },
    { correct: true, index: 19, key: 'u', timestamp: 1676392790279 },
    { correct: true, index: 20, key: 'p', timestamp: 1676392790365 },
    { correct: true, index: 21, key: '(', timestamp: 1676392790776 },
    { correct: true, index: 22, key: ')', timestamp: 1676392790847 },
    { correct: true, index: 23, key: ' ', timestamp: 1676392791108 },
    { correct: true, index: 24, key: 'w', timestamp: 1676392791308 },
    { correct: true, index: 25, key: 'a', timestamp: 1676392791492 },
    { correct: true, index: 26, key: 't', timestamp: 1676392791555 },
    { correct: true, index: 27, key: 'c', timestamp: 1676392791771 },
    { correct: true, index: 28, key: 'h', timestamp: 1676392791852 },
    { correct: true, index: 29, key: 'e', timestamp: 1676392791944 },
    { correct: true, index: 30, key: 'r', timestamp: 1676392792046 },
    { correct: true, index: 31, key: 'G', timestamp: 1676392792307 },
    { correct: true, index: 32, key: 'r', timestamp: 1676392792494 },
    { correct: true, index: 33, key: 'o', timestamp: 1676392792554 },
    { correct: true, index: 34, key: 'u', timestamp: 1676392792659 },
    { correct: true, index: 35, key: 'p', timestamp: 1676392792729 },
    { correct: true, index: 36, key: ' ', timestamp: 1676392792882 },
    { correct: true, index: 37, key: '{', timestamp: 1676392793137 },
    { correct: true, index: 40, key: '\n', timestamp: 1676392793228 },
    { correct: true, index: 41, key: 'r', timestamp: 1676392793933 },
    { correct: true, index: 42, key: 'e', timestamp: 1676392794021 },
    { correct: true, index: 43, key: 't', timestamp: 1676392794141 },
    { correct: true, index: 44, key: 'u', timestamp: 1676392794197 },
    { correct: true, index: 45, key: 'r', timestamp: 1676392794324 },
    { correct: true, index: 46, key: 'n', timestamp: 1676392794454 },
    { correct: true, index: 41, key: 'r', timestamp: 1676392795190 },
    { correct: true, index: 42, key: 'e', timestamp: 1676392795299 },
    { correct: true, index: 43, key: 't', timestamp: 1676392795410 },
    { correct: true, index: 44, key: 'u', timestamp: 1676392795468 },
    { correct: true, index: 45, key: 'r', timestamp: 1676392795601 },
    { correct: true, index: 46, key: 'n', timestamp: 1676392795683 },
    { correct: true, index: 47, key: ' ', timestamp: 1676392796151 },
    { correct: true, index: 48, key: 'w', timestamp: 1676392796361 },
    { correct: true, index: 49, key: 'a', timestamp: 1676392796514 },
    { correct: true, index: 50, key: 't', timestamp: 1676392796588 },
    { correct: true, index: 51, key: 'c', timestamp: 1676392796827 },
    { correct: true, index: 52, key: 'h', timestamp: 1676392796909 },
    { correct: true, index: 53, key: 'e', timestamp: 1676392797008 },
    { correct: true, index: 54, key: 'r', timestamp: 1676392797116 },
    { correct: true, index: 55, key: 'G', timestamp: 1676392797496 },
    { correct: true, index: 56, key: 'r', timestamp: 1676392797730 },
    { correct: true, index: 57, key: 'o', timestamp: 1676392797808 },
    { correct: true, index: 58, key: 'u', timestamp: 1676392797910 },
    { correct: true, index: 59, key: 'p', timestamp: 1676392797968 },
    { correct: true, index: 60, key: '{', timestamp: 1676392798346 },
    { correct: true, index: 65, key: '\n', timestamp: 1676392798497 },
    { correct: true, index: 66, key: 'k', timestamp: 1676392800709 },
    { correct: true, index: 67, key: 'e', timestamp: 1676392800788 },
    { correct: true, index: 68, key: 'y', timestamp: 1676392800894 },
    { correct: true, index: 69, key: 'W', timestamp: 1676392801060 },
    { correct: true, index: 70, key: 'a', timestamp: 1676392801287 },
    { correct: true, index: 71, key: 't', timestamp: 1676392801399 },
    { correct: true, index: 72, key: 'c', timestamp: 1676392801635 },
    { correct: true, index: 73, key: 'h', timestamp: 1676392801724 },
    { correct: true, index: 74, key: 'e', timestamp: 1676392801810 },
    { correct: true, index: 75, key: 'r', timestamp: 1676392801933 },
    { correct: true, index: 76, key: 's', timestamp: 1676392802024 },
    { correct: false, index: 66, key: 'd', timestamp: 1676392804327 },
    { correct: false, index: 67, key: 'e', timestamp: 1676392804543 },
    { correct: false, index: 68, key: 'y', timestamp: 1676392804700 },
    { correct: false, index: 69, key: 'W', timestamp: 1676392804958 },
    { correct: false, index: 70, key: 'a', timestamp: 1676392805036 },
    { correct: false, index: 71, key: 't', timestamp: 1676392806184 },
    { correct: false, index: 72, key: 'c', timestamp: 1676392806433 },
    { correct: false, index: 73, key: 'h', timestamp: 1676392806553 },
    { correct: false, index: 74, key: 'e', timestamp: 1676392806625 },
    { correct: false, index: 75, key: 'r', timestamp: 1676392806753 },
    { correct: false, index: 76, key: 's', timestamp: 1676392806850 },
    { correct: false, index: 77, key: ':', timestamp: 1676392807263 },
    { correct: true, index: 66, key: 'k', timestamp: 1676392808209 },
    { correct: true, index: 67, key: 'e', timestamp: 1676392808311 },
    { correct: true, index: 68, key: 'y', timestamp: 1676392808401 },
    { correct: true, index: 69, key: 'W', timestamp: 1676392808559 },
    { correct: true, index: 70, key: 'a', timestamp: 1676392808736 },
    { correct: true, index: 71, key: 't', timestamp: 1676392808809 },
    { correct: true, index: 72, key: 'c', timestamp: 1676392809050 },
    { correct: true, index: 73, key: 'h', timestamp: 1676392809118 },
    { correct: true, index: 74, key: 'e', timestamp: 1676392809227 },
    { correct: true, index: 75, key: 'r', timestamp: 1676392809328 },
    { correct: true, index: 76, key: 's', timestamp: 1676392809396 },
    { correct: true, index: 77, key: ':', timestamp: 1676392809639 },
    { correct: true, index: 78, key: ' ', timestamp: 1676392809832 },
    { correct: true, index: 79, key: 'm', timestamp: 1676392810011 },
    { correct: true, index: 80, key: 'a', timestamp: 1676392810089 },
    { correct: true, index: 81, key: 'k', timestamp: 1676392810198 },
    { correct: true, index: 82, key: 'e', timestamp: 1676392810274 },
    { correct: true, index: 83, key: '(', timestamp: 1676392810520 },
    { correct: true, index: 84, key: 'w', timestamp: 1676392810915 },
    { correct: true, index: 85, key: 'a', timestamp: 1676392811118 },
    { correct: true, index: 86, key: 't', timestamp: 1676392811218 },
    { correct: true, index: 87, key: 'c', timestamp: 1676392811442 },
    { correct: true, index: 88, key: 'h', timestamp: 1676392811538 },
    { correct: true, index: 89, key: 'e', timestamp: 1676392811649 },
    { correct: true, index: 90, key: 'r', timestamp: 1676392811760 },
    { correct: true, index: 91, key: 'S', timestamp: 1676392811955 },
    { correct: true, index: 92, key: 'e', timestamp: 1676392812097 },
    { correct: true, index: 93, key: 't', timestamp: 1676392812197 },
    { correct: true, index: 94, key: 'B', timestamp: 1676392812554 },
    { correct: true, index: 95, key: 'y', timestamp: 1676392812704 },
    { correct: true, index: 96, key: 'K', timestamp: 1676392812921 },
    { correct: true, index: 97, key: 'e', timestamp: 1676392813049 },
    { correct: true, index: 98, key: 'y', timestamp: 1676392813137 },
    { correct: true, index: 99, key: ')', timestamp: 1676392813359 },
    { correct: true, index: 100, key: ',', timestamp: 1676392813650 },
    { correct: true, index: 105, key: '\n', timestamp: 1676392813778 },
    { correct: true, index: 106, key: 'r', timestamp: 1676392814158 },
    { correct: true, index: 107, key: 'a', timestamp: 1676392814246 },
    { correct: true, index: 108, key: 'n', timestamp: 1676392814333 },
    { correct: true, index: 109, key: 'g', timestamp: 1676392814444 },
    { correct: true, index: 110, key: 'e', timestamp: 1676392814518 },
    { correct: true, index: 111, key: 's', timestamp: 1676392814609 },
    { correct: true, index: 112, key: ':', timestamp: 1676392814749 },
    { correct: true, index: 113, key: ' ', timestamp: 1676392815613 },
    { correct: true, index: 114, key: ' ', timestamp: 1676392815800 },
    { correct: true, index: 115, key: ' ', timestamp: 1676392815972 },
    { correct: true, index: 116, key: ' ', timestamp: 1676392816163 },
    { correct: true, index: 117, key: ' ', timestamp: 1676392816369 },
    { correct: true, index: 118, key: ' ', timestamp: 1676392816606 },
    { correct: true, index: 119, key: 'a', timestamp: 1676392817044 },
    { correct: true, index: 120, key: 'd', timestamp: 1676392817074 },
    { correct: true, index: 121, key: 't', timestamp: 1676392817262 },
    { correct: true, index: 122, key: '.', timestamp: 1676392817743 },
    { correct: true, index: 123, key: 'N', timestamp: 1676392818028 },
    { correct: true, index: 124, key: 'e', timestamp: 1676392818220 },
    { correct: true, index: 125, key: 'w', timestamp: 1676392818308 },
    { correct: true, index: 126, key: 'I', timestamp: 1676392818497 },
    { correct: false, index: 127, key: 'N', timestamp: 1676392818602 },
    { correct: false, index: 128, key: 't', timestamp: 1676392818801 },
    { correct: false, index: 129, key: 'e', timestamp: 1676392818895 },
    { correct: true, index: 127, key: 'n', timestamp: 1676392819553 },
    { correct: true, index: 128, key: 't', timestamp: 1676392819712 },
    { correct: true, index: 129, key: 'e', timestamp: 1676392819808 },
    { correct: true, index: 130, key: 'r', timestamp: 1676392819998 },
    { correct: true, index: 131, key: 'v', timestamp: 1676392820243 },
    { correct: true, index: 132, key: 'a', timestamp: 1676392820349 },
    { correct: true, index: 133, key: 'l', timestamp: 1676392820416 },
    { correct: true, index: 134, key: 'T', timestamp: 1676392820902 },
    { correct: true, index: 135, key: 'r', timestamp: 1676392821122 },
    { correct: true, index: 136, key: 'e', timestamp: 1676392821196 },
    { correct: true, index: 137, key: 'e', timestamp: 1676392821343 },
    { correct: true, index: 138, key: '(', timestamp: 1676392821550 },
    { correct: true, index: 139, key: ')', timestamp: 1676392821785 },
    { correct: true, index: 140, key: ',', timestamp: 1676392821968 },
    { correct: true, index: 145, key: '\n', timestamp: 1676392822798 },
    { correct: true, index: 146, key: 'w', timestamp: 1676392823218 },
    { correct: true, index: 147, key: 'a', timestamp: 1676392823403 },
    { correct: true, index: 148, key: 't', timestamp: 1676392823537 },
    { correct: true, index: 149, key: 'c', timestamp: 1676392823771 },
    { correct: true, index: 150, key: 'h', timestamp: 1676392823848 },
    { correct: true, index: 151, key: 'e', timestamp: 1676392823954 },
    { correct: true, index: 152, key: 'r', timestamp: 1676392824086 },
    { correct: true, index: 153, key: 's', timestamp: 1676392824116 },
    { correct: true, index: 154, key: ':', timestamp: 1676392824376 },
    { correct: true, index: 155, key: ' ', timestamp: 1676392824700 },
    { correct: true, index: 156, key: ' ', timestamp: 1676392824882 },
    { correct: true, index: 157, key: ' ', timestamp: 1676392825040 },
    { correct: true, index: 158, key: ' ', timestamp: 1676392825547 },
    { correct: true, index: 159, key: 'm', timestamp: 1676392825845 },
    { correct: true, index: 160, key: 'a', timestamp: 1676392825913 },
    { correct: true, index: 161, key: 'k', timestamp: 1676392826036 },
    { correct: true, index: 162, key: 'e', timestamp: 1676392826111 },
    { correct: true, index: 163, key: '(', timestamp: 1676392826422 },
    { correct: true, index: 164, key: 'w', timestamp: 1676392826766 },
  ];

  it('should include the last correct keystroke', () => {
    const expectedInput =
      'func newWatcherGroup() watcherGroup {\nreturn watcherGroup{\nkeyWatchers: make(watcherSetByKey),\nranges:      adt.NewIntervalTree(),\nwatchers:    make(w';

    const validKeyStrokes = player.validKeyStrokes();

    const actualInput = validKeyStrokes.map((stroke) => stroke.key).join('');

    expect(actualInput).toBe(expectedInput);
  });
});


================================================
FILE: packages/back-nest/src/results/entities/leaderboard-result.dto.ts
================================================
export class LeaderBoardResult {
  username: string;
  avatarUrl: string;
  cpm: number;
  accuracy: number;
  createdAt: Date;
}


================================================
FILE: packages/back-nest/src/results/entities/result.entity.ts
================================================
import { Challenge } from 'src/challenges/entities/challenge.entity';
import { User } from 'src/users/entities/user.entity';
import {
  Column,
  CreateDateColumn,
  Entity,
  ManyToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';

@Entity('results')
export class Result {
  @PrimaryGeneratedColumn('uuid')
  id: string;
  @Column({ nullable: true })
  raceId: string;
  @Column()
  timeMS: number;
  @Column()
  cpm: number;
  @Column()
  mistakes: number;
  @Column()
  accuracy: number;

  @Column({ unique: true, nullable: true, default: null })
  legacyId: string;

  @CreateDateColumn({
    type: 'timestamp',
    default: () => 'CURRENT_TIMESTAMP(6)',
  })
  public createdAt: Date;
  @ManyToOne(() => Challenge, (challenge) => challenge.results, {
    onDelete: 'SET NULL',
  })
  challenge: Challenge;
  @ManyToOne(() => User, (user) => user.results, {
    onDelete: 'SET NULL',
  })
  user: User;
  userId: string;

  percentile?: number;
}


================================================
FILE: packages/back-nest/src/results/errors.ts
================================================
import { BadRequestException, ForbiddenException } from '@nestjs/common';

export class SaveResultAnonymousNotAllowed extends ForbiddenException {
  constructor() {
    super('Anonymous users cannot save results');
  }
}

export class SaveResultInvalidUserID extends ForbiddenException {
  constructor() {
    super('Users can only save their own results');
  }
}

export class SaveResultRaceNotCompleted extends BadRequestException {
  constructor() {
    super('User did not complete the race');
  }
}

export class SaveResultUserNotInRace extends BadRequestException {
  constructor() {
    super('User is not playing in the race');
  }
}


================================================
FILE: packages/back-nest/src/results/results.controller.ts
================================================
import {
  BadRequestException,
  Controller,
  Get,
  InternalServerErrorException,
  Param,
  Req,
} from '@nestjs/common';
import { isUUID } from 'class-validator';
import { Request } from 'express';
import { LeaderBoardResult } from './entities/leaderboard-result.dto';
import { ResultService } from './services/results.service';

@Controller('results')
export class ResultsController {
  leaderboardP?: Promise<LeaderBoardResult[]>;
  constructor(private resultsService: ResultService) {}
  @Get('leaderboard')
  async getLeaderboard(): Promise<LeaderBoardResult[]> {
    if (this.leaderboardP) {
      // there is an ongoing promise
      return this.leaderboardP;
    }
    // cache the leaderboard promise so we only hit the DB once per concurrent request
    this.leaderboardP = this.resultsService.getLeaderboard();
    return this.leaderboardP.finally(() => {
      // reset the promise so new clients don't get a stale leaderboard
      this.leaderboardP = undefined;
    });
  }
  @Get('/stats')
  async getStatsByUser(@Req() request: Request) {
    if (!request.session?.user) {
      throw new InternalServerErrorException();
    }
    const startOfToday = new Date();
    startOfToday.setUTCHours(0, 0, 0, 0);

    const startOfTime = new Date('January 1, 1979');
    const oneWeekAgo = new Date();
    oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
    oneWeekAgo.setUTCHours(0, 0, 0, 0);

    const user = request.session.user;
    const [cpmAllTime, cpmToday, cpmLastWeek, cpmLast10] = await Promise.all([
      this.resultsService.getAverageCPMSince(user.id, startOfTime),
      this.resultsService.getAverageCPMSince(user.id, startOfToday),
      this.resultsService.getAverageCPMSince(user.id, oneWeekAgo),
      this.resultsService.getAverageCPM(user.id, 10),
    ]);
    return {
      cpmLast10,
      cpmToday,
      cpmLastWeek,
      cpmAllTime,
    };
  }

  @Get(':resultId')
  getResultByID(@Param('resultId') resultId: string) {
    if (!isUUID(resultId)) throw new BadRequestException('Invalid resultId');
    const result = this.resultsService.getByID(resultId);
    return result;
  }
}


================================================
FILE: packages/back-nest/src/results/results.module.ts
================================================
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Result } from './entities/result.entity';
import { ResultsController } from './results.controller';
import { ResultCalculationService } from './services/result-calculation.service';
import { ResultFactoryService } from './services/result-factory.service';
import { ResultService } from './services/results.service';

@Module({
  imports: [TypeOrmModule.forFeature([Result])],
  providers: [ResultService, ResultFactoryService, ResultCalculationService],
  controllers: [ResultsController],
  exports: [ResultService, ResultFactoryService],
})
export class ResultsModule {}


================================================
FILE: packages/back-nest/src/results/services/result-calculation.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { Challenge } from 'src/challenges/entities/challenge.entity';
import { RacePlayer } from 'src/races/services/race-player.service';
import { Race } from 'src/races/services/race.service';

@Injectable()
export class ResultCalculationService {
  getTimeMS(race: Race, player: RacePlayer): number {
    const firstTimeStampMS = race.startTime.getTime();
    const keyStrokes = player.validKeyStrokes();
    const lastTimeStampMS = keyStrokes[keyStrokes.length - 1].timestamp;
    return lastTimeStampMS - firstTimeStampMS;
  }

  getCPM(code: string, timeMS: number): number {
    const timeSeconds = timeMS / 1000;
    const strippedCode = Challenge.getStrippedCode(code);
    const cps = strippedCode.length / timeSeconds;
    const cpm = cps * 60;
    return Math.floor(cpm);
  }

  getMistakesCount(player: RacePlayer): number {
    return player.incorrectKeyStrokes().length;
  }

  getAccuracy(player: RacePlayer): number {
    const incorrectKeyStrokes = player.incorrectKeyStrokes().length;
    const validKeyStrokes = player.validKeyStrokes().length;
    const totalKeySrokes = validKeyStrokes + incorrectKeyStrokes;
    return Math.floor((validKeyStrokes / totalKeySrokes) * 100);
  }
}


================================================
FILE: packages/back-nest/src/results/services/result-factory.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { RacePlayer } from 'src/races/services/race-player.service';
import { Race } from 'src/races/services/race.service';
import { User } from 'src/users/entities/user.entity';
import { Result } from '../entities/result.entity';
import { ResultCalculationService } from './result-calculation.service';

@Injectable()
export class ResultFactoryService {
  constructor(private resultCalculation: ResultCalculationService) {}
  factory(race: Race, player: RacePlayer, user: User): Result {
    const challenge = race.challenge;
    const result = new Result();
    const timeMS = this.resultCalculation.getTimeMS(race, player);
    const cpm = this.resultCalculation.getCPM(challenge.content, timeMS);
    const mistakes = this.resultCalculation.getMistakesCount(player);
    const accuracy = this.resultCalculation.getAccuracy(player);
    result.raceId = player.raceId;
    result.user = user;
    result.challenge = challenge;
    result.timeMS = timeMS;
    result.cpm = cpm;
    result.mistakes = mistakes;
    result.accuracy = accuracy;
    return result;
  }
}


================================================
FILE: packages/back-nest/src/results/services/results.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { LeaderBoardResult } from '../entities/leaderboard-result.dto';
import { Result } from '../entities/result.entity';

@Injectable()
export class ResultService {
  constructor(
    @InjectRepository(Result)
    private resultsRepository: Repository<Result>,
  ) {}

  async create(result: Result): Promise<Result> {
    return await this.resultsRepository.save(result);
  }

  async upsertByLegacyId(results: Result[]): Promise<void> {
    await this.resultsRepository.upsert(results, ['legacyId']);
  }

  async getByID(id: string) {
    const result = await this.resultsRepository.findOneOrFail({
      where: {
        id,
        // filter out legacy results
        legacyId: null,
      },
      relations: ['user', 'challenge', 'challenge.project'],
    });
    result.percentile = await this.getResultPercentile(result.cpm);
    return result;
  }

  async getLeaderboard(): Promise<LeaderBoardResult[]> {
    const oneDayAgo = new Date();
    oneDayAgo.setDate(oneDayAgo.getDate() - 1);
    const resultsTodayStream = await this.resultsRepository
      .createQueryBuilder('r')
      .leftJoinAndSelect('r.user', 'u')
      .where(
        `u.banned=false AND
        r.createdAt BETWEEN '${oneDayAgo.toISOString()}' AND '${new Date().toISOString()}'`,
      )
      .orderBy('r.cpm')
      .orderBy('r.createdAt', 'DESC')
      .stream();

    const resultsToday: Record<string, any> = {};

    for await (const r of resultsTodayStream) {
      if (!resultsToday[r.u_id]) {
        r.racesPlayed = 1;
        resultsToday[r.u_id] = r;
        continue;
      }
      const prevResult = resultsToday[r.u_id];
      if (r.r_cpm > prevResult.r_cpm) {
        r.racesPlayed = prevResult.racesPlayed;
        resultsToday[r.u_id] = r;
      }
      resultsToday[r.u_id].racesPlayed++;
    }

    const results = Object.values(resultsToday)
      .map((r) => {
        return {
          username: r.u_username,
          avatarUrl: r.u_avatarUrl,
          cpm: r.r_cpm,
          accuracy: r.r_accuracy,
          createdAt: r.r_createdAt,
          racesPlayed: r.racesPlayed,
          resultId: r.r_id,
        };
      })
      .sort((a, b) => b.cpm - a.cpm);
    return results;
  }
  async getAverageCPM(userId: string, take: number): Promise<number> {
    const results = await this.resultsRepository.find({
      where: {
        user: { id: userId },
      },
      order: {
        createdAt: 'DESC',
      },
      take,
    });
    const total = results.reduce((prev, curr) => {
      return prev + curr.cpm;
    }, 0);
    const average = total / results.length;
    return parseInt(average.toString(), 10);
  }
  async getAverageCPMSince(userId: string, since: Date): Promise<number> {
    const { avg } = await this.resultsRepository
      .createQueryBuilder('r')
      .where('r.userId=:userId AND r.createdAt > :startOfToday', {
        userId,
        startOfToday: since.toISOString(),
      })
      .select('AVG(r.cpm)', 'avg')
      .getRawOne();
    return parseInt(avg, 10);
  }

  async getResultPercentile(cpm: number): Promise<number> {
    const { countBetterThan } = await this.resultsRepository
      .createQueryBuilder('r')
      .where('r.cpm < :cpm', {
        cpm,
      })
      .select('COUNT(r.cpm)', 'countBetterThan')
      .getRawOne();

    const totalCount = await this.resultsRepository.count();

    const percentile = (
      (parseInt(countBetterThan, 10) / totalCount) *
      100
    ).toFixed(0);
    return parseInt(percentile, 10);
  }
}


================================================
FILE: packages/back-nest/src/seeder/commands/challenge.seeder.ts
================================================
import { Command, CommandRunner } from 'nest-commander';
import { Challenge } from 'src/challenges/entities/challenge.entity';
import { ChallengeService } from 'src/challenges/services/challenge.service';
import { Project } from 'src/projects/entities/project.entity';
import { ProjectService } from 'src/projects/services/project.service';

@Command({
  name: 'seed-challenges',
  arguments: '',
  options: {},
})
export class ProjectSeedRunner extends CommandRunner {
  constructor(
    private projectService: ProjectService,
    private challengeService: ChallengeService,
  ) {
    super();
  }
  async run(): Promise<void> {
    const project = this.project_factory();
    await this.projectService.bulkUpsert([project]);
    const challenges = this.challenges_factory(project);
    await this.challengeService.upsert(challenges);
  }

  project_factory() {
    const project = new Project();
    project.id = '98dac57c-516e-485f-872a-4b9f6e1ad566';
    project.fullName = 'etcd-io/etcd';
    project.htmlUrl = 'https://github.com/etcd-io/etcd';
    project.language = 'Go';
    project.stars = 41403;
    project.licenseName = 'Apache License 2.0';
    project.ownerAvatar =
      'https://avatars.githubusercontent.com/u/41972792?v=4';
    project.defaultBranch = 'main';
    project.syncedSha = '2d638e0fd2c1d91c9c4323591bb1041b594e28fa';
    return project;
  }

  challenges_factory(project: Project) {
    const challenges = [];
    const firstChallenge = new Challenge();
    challenges.push(firstChallenge);
    firstChallenge.id = 'b4b6eec5-333c-4c77-a648-1b0884ae5ad0';
    firstChallenge.sha = 'fa3011cb39ac784a88da3667b729f3a79f5f22c3';
    firstChallenge.treeSha = '2d638e0fd2c1d91c9c4323591bb1041b594e28fa';
    firstChallenge.path = 'server/etcdserver/api/rafthttp/transport.go';
    firstChallenge.language = 'go';
    firstChallenge.url =
      'https://github.com/etcd-io/etcd/blob/2d638e0fd2c1d91c9c4323591bb1041b594e28fa/server/etcdserver/api/rafthttp/transport.go/#L425';
    firstChallenge.content =
      'func (t *Transport) Pause() {\n' +
      '\tt.mu.RLock()\n' +
      '\tdefer t.mu.RUnlock()\n' +
      '\tfor _, p := range t.peers {\n' +
      '\t\tp.(Pausable).Pause()\n' +
      '\t}\n' +
      '}';
    firstChallenge.project = project;

    const secondChallenge = new Challenge();
    challenges.push(secondChallenge);
    secondChallenge.id = '8ebf6be1-7f7c-4edf-a622-97b0024636e8';
    secondChallenge.sha = '69ecc631471975fcb4d207f85a57baf2b5a79460';
    secondChallenge.treeSha = '2d638e0fd2c1d91c9c4323591bb1041b594e28fa';
    secondChallenge.language = 'go';
    secondChallenge.path = 'client/v3/retry.go';
    secondChallenge.url =
      'https://github.com/etcd-io/etcd/blob/2d638e0fd2c1d91c9c4323591bb1041b594e28fa/client/v3/retry.go/#L160';
    secondChallenge.content =
      'func RetryClusterClient(c *Client) pb.ClusterClient {\n' +
      '\treturn &retryClusterClient{\n' +
      '\t\tcc: pb.NewClusterClient(c.conn),\n' +
      '\t}\n' +
      '}';
    secondChallenge.project = project;

    const thirdChallenge = new Challenge();
    challenges.push(thirdChallenge);
    thirdChallenge.id = '19174a2e-9220-40c8-832a-7effd351a68b';
    thirdChallenge.sha = 'ea19cf0181bbedbfc65bce9cfce26eb3558cb9ee';
    thirdChallenge.treeSha = '2d638e0fd2c1d91c9c4323591bb1041b594e28fa';
    thirdChallenge.path = 'pkg/schedule/schedule.go';
    thirdChallenge.language = 'go';
    thirdChallenge.url =
      'https://github.com/etcd-io/etcd/blob/2d638e0fd2c1d91c9c4323591bb1041b594e28fa/pkg/schedule/schedule.go/#L135';
    thirdChallenge.content =
      'func (f *fifo) Finished() int {\n' +
      '\tf.finishCond.L.Lock()\n' +
      '\tdefer f.finishCond.L.Unlock()\n' +
      '\treturn f.finished\n' +
      '}';
    thirdChallenge.project = project;

    return challenges;
  }
}


================================================
FILE: packages/back-nest/src/seeder/seeder.module.ts
================================================
import { Module } from '@nestjs/common';
import { ChallengesModule } from 'src/challenges/challenges.module';
import { ProjectsModule } from 'src/projects/projects.module';
import { ProjectSeedRunner } from './commands/challenge.seeder';

@Module({
  imports: [ProjectsModule, ChallengesModule],
  providers: [ProjectSeedRunner],
})
export class SeederModule {}


================================================
FILE: packages/back-nest/src/sessions/session.adapter.ts
================================================
import { IncomingMessage } from 'http';
import { INestApplication } from '@nestjs/common';
import { IoAdapter } from '@nestjs/platform-socket.io';
import { NextFunction } from 'express';
import { Server, Socket } from 'socket.io';

type SocketIOCompatibleMiddleware = (
  r: IncomingMessage,
  object: object,
  next: NextFunction,
) => void;

export function makeSocketIOReadMiddleware(
  middleware: SocketIOCompatibleMiddleware,
) {
  return (socket: Socket, next: NextFunction) => {
    return middleware(socket.request, {}, next);
  };
}

export const denyWithoutUserInSession = (
  socket: Socket,
  next: NextFunction,
) => {
  if (!socket.request.session?.user) {
    console.log(
      'disconnect because there is no user in the session',
      socket.id,
    );
    socket.request.session?.destroy(() => {
      /* **/
    });
    return socket.disconnect(true);
  }
  next();
};

export class SessionAdapter extends IoAdapter {
  constructor(
    app: INestApplication,
    private sessionMiddleware: SocketIOCompatibleMiddleware,
  ) {
    super(app);
  }

  createIOServer(port: number, opt?: any): any {
    const server: Server = super.createIOServer(port, opt);
    server.use(makeSocketIOReadMiddleware(this.sessionMiddleware));
    server.use(denyWithoutUserInSession);
    return server;
  }
}


================================================
FILE: packages/back-nest/src/sessions/session.entity.ts
================================================
import { ISession } from 'connect-typeorm';
import {
  Column,
  DeleteDateColumn,
  Entity,
  Index,
  PrimaryColumn,
} from 'typeorm';

@Entity({ name: 'sessions' })
export class Session implements ISession {
  @Index()
  @Column('bigint')
  expiredAt: number = Date.now();

  @PrimaryColumn('varchar', { length: 255 })
  id: string;

  @Column('text')
  json: string;

  @DeleteDateColumn()
  destroyedAt: Date;
}


================================================
FILE: packages/back-nest/src/sessions/session.middleware.ts
================================================
import { TypeormStore } from 'connect-typeorm/out';
import * as session from 'express-session';
import { PostgresDataSource } from 'src/database.module';
import { Session } from './session.entity';

const SESSION_SECRET_MIN_LENGTH = 12;

const ONE_DAY = 1000 * 60 * 60 * 24;

export const cookieName = 'speedtyper-v2-sid';

export const getSessionMiddleware = () => {
  const sessionRepository = PostgresDataSource.getRepository(Session);
  return session({
    name: cookieName,
    store: new TypeormStore({
      cleanupLimit: 2,
    }).connect(sessionRepository),
    secret: getSessionSecret(),
    resave: false,
    saveUninitialized: false,
    cookie: {
      httpOnly: true,
      sameSite: 'lax',
      secure: process.env.NODE_ENV === 'production',
      maxAge: ONE_DAY * 7,
      ...(process.env.NODE_ENV === 'production'
        ? {
            domain: '.speedtyper.dev',
          }
        : {}),
    },
  });
};

function getSessionSecret() {
  const secret = process.env.SESSION_SECRET;
  if (!secret)
    throw new Error('SESSION_SECRET is missing from environment variables');
  if (secret.length < SESSION_SECRET_MIN_LENGTH)
    throw new Error(
      `SESSION_SECRET is not long enough, must be at least ${SESSION_SECRET_MIN_LENGTH} characters long`,
    );
  return secret;
}


================================================
FILE: packages/back-nest/src/sessions/types.d.ts
================================================
import { Session, SessionData } from 'express-session';
import { User } from 'src/users/entities/user.entity';

declare module 'express-session' {
  export interface SessionData {
    user: User;
    raceId: string;
  }
}

declare module 'http' {
  interface IncomingMessage {
    cookieHolder?: string;
    session: Session & SessionData;
  }
}


================================================
FILE: packages/back-nest/src/tracking/entities/event.entity.ts
================================================
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

export enum TrackingEventType {
  LegacyRaceStarted = 'legacy_race_started',
  RaceStarted = 'race_started',
  RaceCompleted = 'race_completed',
}

@Entity()
export class TrackingEvent {
  @PrimaryGeneratedColumn('uuid')
  id: string;
  @Column({
    unique: true,
    type: 'enum',
    enum: TrackingEventType,
  })
  event: TrackingEventType;
  @Column({
    default: 0,
  })
  count: number;
}


================================================
FILE: packages/back-nest/src/tracking/tracking.module.ts
================================================
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TrackingEvent } from './entities/event.entity';
import { TrackingService } from './tracking.service';

@Module({
  imports: [TypeOrmModule.forFeature([TrackingEvent])],
  controllers: [],
  providers: [TrackingService],
  exports: [TrackingService],
})
export class TrackingModule {}


================================================
FILE: packages/back-nest/src/tracking/tracking.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TrackingEvent, TrackingEventType } from './entities/event.entity';

@Injectable()
export class TrackingService {
  constructor(
    @InjectRepository(TrackingEvent)
    private repository: Repository<TrackingEvent>,
  ) {}

  async trackRaceStarted(): Promise<TrackingEvent> {
    return this.trackRaceEvent(TrackingEventType.RaceStarted);
  }

  async trackRaceCompleted(): Promise<TrackingEvent> {
    return this.trackRaceEvent(TrackingEventType.RaceCompleted);
  }

  private async trackRaceEvent(
    event: TrackingEventType,
  ): Promise<TrackingEvent> {
    return await this.repository.manager.transaction(async (transaction) => {
      let trackingEvent = new TrackingEvent();
      trackingEvent.event = event;
      await transaction.upsert(TrackingEvent, trackingEvent, {
        conflictPaths: ['event'],
        skipUpdateIfNoValuesChanged: true,
      });
      trackingEvent = await transaction.findOneBy(TrackingEvent, {
        event: trackingEvent.event,
      });
      trackingEvent.count++;
      trackingEvent = await transaction.save(TrackingEvent, trackingEvent);
      return trackingEvent;
    });
  }
}


================================================
FILE: packages/back-nest/src/users/controllers/user.controller.ts
================================================
import { Controller, Get, HttpException, Req } from '@nestjs/common';
import { Request } from 'express';
import { User } from '../entities/user.entity';

@Controller('user')
export class UserController {
  @Get()
  getCurrentUser(@Req() request: Request): User {
    if (!request.session?.user) {
      throw new HttpException('Internal server error', 500);
    }
    return request.session.user;
  }
}


================================================
FILE: packages/back-nest/src/users/entities/upsertGithubUserDTO.ts
================================================
import { IsString } from 'class-validator';
import { Profile } from 'passport-github';
import { User } from './user.entity';

export class UpsertGithubUserDTO {
  @IsString()
  username: string;
  @IsString()
  githubId: string;
  @IsString()
  githubUrl: string;
  @IsString()
  avatarUrl: string;

  static fromGithubProfile(profile: Profile) {
    const user = new UpsertGithubUserDTO();
    user.githubId = profile.id;
    user.username = profile.username;
    user.githubUrl = profile.profileUrl;
    user.avatarUrl = profile.photos[0].value;
    return user;
  }
  toUser() {
    const user = new User();
    user.githubId = parseInt(this.githubId);
    user.username = this.username;
    user.githubUrl = this.githubUrl;
    user.avatarUrl = this.avatarUrl;
    return user;
  }
}


================================================
FILE: packages/back-nest/src/users/entities/user.entity.ts
================================================
import { randomUUID } from 'crypto';
import { Result } from 'src/results/entities/result.entity';
import {
  Column,
  CreateDateColumn,
  Entity,
  OneToMany,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { generateRandomUsername } from '../utils/generateRandomUsername';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;
  @Column({ unique: true })
  username: string;
  @Column({ unique: true })
  githubId: number;
  @Column({ unique: true })
  githubUrl: string;
  @Column()
  avatarUrl: string;
  @Column({ unique: true, nullable: true })
  legacyId: string;
  @Column({ default: false, select: false })
  banned: boolean;
  @CreateDateColumn({
    type: 'timestamp',
    default: () => 'CURRENT_TIMESTAMP(6)',
  })
  public createdAt: Date;

  @OneToMany(() => Result, (result) => result.user)
  results: Result[];
  isAnonymous: boolean;
  static generateAnonymousUser() {
    const user = new User();
    user.id = randomUUID();
    user.username = generateRandomUsername();
    user.isAnonymous = true;
    return user;
  }
}


================================================
FILE: packages/back-nest/src/users/services/user.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../entities/user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  async upsertGithubUser(userData: User): Promise<User> {
    const currentUser = await this.userRepository.findOneBy({
      githubId: userData.githubId,
    });
    userData.id = currentUser?.id;
    userData.banned = currentUser?.banned || userData.banned || false;
    const user = await this.userRepository.save(userData);
    user.isAnonymous = false;
    return user;
  }

  async findByLegacyID(legacyId: string) {
    const user = await this.userRepository.findOneBy({
      legacyId,
    });
    return user;
  }
}


================================================
FILE: packages/back-nest/src/users/users.module.ts
================================================
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserController } from './controllers/user.controller';
import { User } from './entities/user.entity';
import { UserService } from './services/user.service';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UsersModule {}


================================================
FILE: packages/back-nest/src/users/utils/generateRandomUsername.ts
================================================
import { uniqueNamesGenerator } from 'unique-names-generator';

const adjectives2 = [
  'abrupt',
  'acidic',
  'adorable',
  'adventurous',
  'aggressive',
  'agitated',
  'aloof',
  'amused',
  'annoyed',
  'antsy',
  'anxious',
  'appalling',
  'apprehensive',
  'arrogant',
  'astonishing',
  'bitter',
  'bland',
  'bored',
  'brave',
  'bright',
  'broad',
  'bulky',
  'burly',
  'charming',
  'cheeky',
  'cheerful',
  'clean',
  'clear',
  'cloudy',
  'clueless',
  'clumsy',
  'colorful',
  'colossal',
  'confused',
  'convincing',
  'convoluted',
  'cooperative',
  'courageous',
  'crooked',
  'cruel',
  'cynical',
  'dangerous',
  'dashing',
  'deceitful',
  'defeated',
  'defiant',
  'delicious',
  'delightful',
  'depraved',
  'depressed',
  'despicable',
  'determined',
  'dilapidated',
  'diminutive',
  'disgusted',
  'distinct',
  'distraught',
  'distressed',
  'disturbed',
  'dizzy',
  'drab',
  'drained',
  'dull',
  'eager',
  'ecstatic',
  'elated',
  'elegant',
  'emaciated',
  'embarrassed',
  'enchanting',
  'encouraging',
  'energetic',
  'enormous',
  'enthusiastic',
  'envious',
  'exasperated',
  'excited',
  'exhilarated',
  'extensive',
  'exuberant',
  'fancy',
  'fantastic',
  'fierce',
  'fluttering',
  'foolish',
  'frantic',
  'fresh',
  'friendly',
  'frightened',
  'frothy',
  'frustrating',
  'funny',
  'fuzzy',
  'gaudy',
  'gentle',
  'giddy',
  'gigantic',
  'glamorous',
  'gleaming',
  'glorious',
  'gorgeous',
  'graceful',
  'greasy',
  'grieving',
  'gritty',
  'grotesque',
  'grubby',
  'grumpy',
  'handsome',
  'happy',
  'harebrained',
  'healthy',
  'helpful',
  'helpless',
  'high',
  'hollow',
  'homely',
  'horrific',
  'huge',
  'hungry',
  'hurt',
  'icy',
  'ideal',
  'immense',
  'impressionable',
  'intrigued',
  'irate',
  'irritable',
  'itchy',
  'jealous',
  'jittery',
  'jolly',
  'joyous',
  'juicy',
  'jumpy',
  'kind',
  'large',
  'lazy',
  'lethal',
  'little',
  'lively',
  'livid',
  'lonely',
  'loose',
  'lovely',
  'lucky',
  'ludicrous',
  'magnificent',
  'mammoth',
  'maniacal',
  'massive',
  'melancholy',
  'melted',
  'miniature',
  'minute',
  'mistaken',
  'misty',
  'moody',
  'mortified',
  'motionless',
  'mysterious',
  'narrow',
  'nasty',
  'naughty',
  'nervous',
  'nonchalant',
  'nonsensical',
  'nutritious',
  'nutty',
  'obedient',
  'oblivious',
  'obnoxious',
  'odd',
  'outrageous',
  'panicky',
  'perfect',
  'perplexed',
  'petite',
  'petty',
  'plain',
  'pleasant',
  'poised',
  'pompous',
  'precious',
  'prickly',
  'proud',
  'pungent',
  'puny',
  'quaint',
  'quizzical',
  'ratty',
  'reassured',
  'relieved',
  'repulsive',
  'responsive',
  'ripe',
  'robust',
  'rotten',
  'rough',
  'round',
  'salty',
  'sarcastic',
  'scant',
  'scary',
  'scattered',
  'scrawny',
  'selfish',
  'shaky',
  'shallow',
  'sharp',
  'shiny',
  'short',
  'silky',
  'silly',
  'skinny',
  'slimy',
  'slippery',
  'small',
  'smarmy',
  'smiling',
  'smoggy',
  'smooth',
  'smug',
  'soggy',
  'solid',
  'sore',
  'sour',
  'sparkling',
  'spicy',
  'splendid',
  'spotless',
  'square',
  'stale',
  'steady',
  'steep',
  'responsive',
  'sticky',
  'stormy',
  'stout',
  'strange',
  'strong',
  'stunning',
  'substantial',
  'successful',
  'succulent',
  'superficial',
  'superior',
  'sweet',
  'tart',
  'tasty',
  'tender',
  'tense',
  'terrible',
  'thankful',
  'thick',
  'thoughtful',
  'thoughtless',
  'timely',
  'tricky',
  'troubled',
  'uneven',
  'unsightly',
  'upset',
  'uptight',
  'vast',
  'vexed',
  'victorious',
  'virtuous',
  'vivacious',
  'vivid',
  'wacky',
  'weary',
  'whimsical',
  'whopping',
  'wicked',
  'witty',
  'wobbly',
  'wonderful',
  'worried',
  'yummy',
  'zany',
  'zealous',
  'zippy',
];
const technologies = [
  'Bash',
  'C',
  'C#',
  'C++',
  'CSS',
  'Elm',
  'Eno',
  'ERB',
  'Fennel',
  'Golang',
  'HTML',
  'Java',
  'JavaScript',
  'Lua',
  'Make',
  'Markdown',
  'OCaml',
  'PHP',
  'Python',
  'Ruby',
  'Rust',
  'R',
  'S-expressions',
  'SPARQL',
  'SystemRDL',
  'Svelte',
  'TOML',
  'Turtle',
  'TypeScript',
  'Verilog',
  'VHDL',
  'Vue',
  'YAML',
  'WASM',
  'Agda',
  'Erlang',
  'Dockerfile',
  'Go',
  'Haskell',
  'Kotlin',
  'Nix',
  'Perl',
  'Scala',
  'Swift',
  'Arch',
  'Ubuntu',
  'Mac',
  'Windows',
  'GNU',
  'Linux',
  'BSD',
  'Arduino',
  'Clojure',
  'Blockchain',
  'Elixir',
  'Angular',
  'Vue',
  'Svelte',
  'React',
  'Re-frame',
  'Stateless',
  'Kernel',
  'Context',
  'OpenGL',
  'MicroServices',
  'Monolith',
  'Monorepo',
  'SQL',
  'Firebase',
  'MongoDB',
  'Postgres',
  'MySQL',
  'Ionic',
  'Phoenix',
  'Cordova',
  'ReactNative',
  'PowerShell',
  'Vim',
  'VSCode',
  'Emacs',
  'Cobol',
  'Zsh',
  'Assembly',
  'OpenCV',
  'HTTP',
  'SSH',
  'FTP',
  'Tensorflow',
  'PyTorch',
  'Pandas',
  'Unity',
  'Unreal',
  'Docker',
  'Kubernetes',
  'Godot',
];

export const generateRandomUsername = () => {
  const randomName: string = uniqueNamesGenerator({
    dictionaries: [adjectives2, technologies],
    separator: '',
    length: 2,
    style: 'capital',
  });

  return randomName;
};


================================================
FILE: packages/back-nest/src/utils/validateDTO.ts
================================================
import { ValidationError } from '@nestjs/common';
import { ClassConstructor, plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';

export class ValidationErrorContainer extends TypeError {
  errors: ValidationError[];
  constructor(name: string, errors: ValidationError[]) {
    const fields = errors.map((err) => err.property).join(', ');
    super(`Error validating ${name} for fields: ${fields}`);
    this.errors = errors;
    Object.setPrototypeOf(this, ValidationErrorContainer.prototype);
  }
}

export const validateDTO = async <T>(
  dto: ClassConstructor<T>,
  obj: unknown,
) => {
  const instance = plainToInstance(dto, obj);
  const errors = await validate(instance as object);
  if (errors.length > 0) {
    throw new ValidationErrorContainer(dto.name, errors);
  }
  return instance;
};


================================================
FILE: packages/back-nest/tracked-projects.txt
================================================
etcd-io/etcd
rust-lang/cargo
rust-lang/rust
tiangolo/fastapi
pallets/flask
encode/starlette
apache/zookeeper
ClickHouse/ClickHouse
rails/rails
lodash/lodash
TheAlgorithms/Java
ggerganov/llama.cpp
ggerganov/whisper.cpp
vitejs/vite
lampepfl/dotty
tinygrad/tinygrad


================================================
FILE: packages/back-nest/tsconfig.build.json
================================================
{
  "extends": "./tsconfig.json",
  "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}


================================================
FILE: packages/back-nest/tsconfig.json
================================================
{
  "ts-node": {
    "files": true
  },
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2021",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true,
    "skipLibCheck": true,
    "strictNullChecks": false,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false
  }
}


================================================
FILE: packages/webapp-next/.eslintrc.json
================================================
{
  "extends": ["next/core-web-vitals", "prettier"],
  "plugins": ["prettier"],
  "rules": {
    "prettier/prettier": [
      "error",
      {
        "endOfLine": "auto"
      }
    ]
  }
}


================================================
FILE: packages/webapp-next/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
.next/
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts


================================================
FILE: packages/webapp-next/.prettierrc
================================================
{
  "singleQuote": false,
  "jsxSingleQuote": false
}



================================================
FILE: packages/webapp-next/README.md
================================================
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).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.

[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`.

The `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.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

## Deploy on Vercel

The 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.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.


================================================
FILE: packages/webapp-next/Socket.ts
================================================
import io from "socket.io-client";

export default class Socket {
  socket: SocketIOClient.Socket;

  constructor(serverUrl: string) {
    this.socket = io(serverUrl);
  }

  disconnect = () => {
    this.socket.emit("disconnect");
    if (this.socket) this.socket.disconnect();
  };

  subscribe = (event: string, cb: (error: string | null, msg: any) => void) => {
    if (!this.socket) return true;
    this.socket.on(event, (msg: any) => {
      return cb(null, msg);
    });
  };

  emit = (event: string, data?: any) => {
    if (this.socket) this.socket.emit(event, data);
  };
}


================================================
FILE: packages/webapp-next/assets/icons/BattleIcon.tsx
================================================
export const BattleIcon = () => {
  // <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
  return (
    <svg className="h-6 fill-current" viewBox="0 0 512 512">
      <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" />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/CopyIcon.tsx
================================================
export const CopyIcon = () => {
  return (
    <svg
      viewBox="0 0 128 128"
      xmlns="http://www.w3.org/2000/svg"
      className="h-5 fill-current"
    >
      <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>
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/CrossIcon.tsx
================================================
export const CrossIcon = () => {
  // Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc.
  return (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
      <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" />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/CrownIcon.tsx
================================================
export const CrownIcon = () => {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 576 512"
      className="fill-current"
      style={{
        fontWeight: "bold",
        height: "15px",
      }}
    >
      <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" />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/DiscordLogo.tsx
================================================
export const DiscordLogo = () => {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width="20"
      height="20"
      viewBox="0 0 245 240"
      className="fill-current"
    >
      <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" />
      <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" />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/DownArrowIcon.tsx
================================================
export const DownArrowIcon = () => {
  return (
    <svg
      className="-mr-1 ml-2 h-5 w-5"
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 20 20"
      fill="currentColor"
      aria-hidden="true"
    >
      <path
        fillRule="evenodd"
        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"
        clipRule="evenodd"
      />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/GithubLogo.tsx
================================================
export const GithubLogo = () => (
  <svg className="fill-current" height="15" width="15" viewBox="0 0 16 16">
    <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>
  </svg>
);


================================================
FILE: packages/webapp-next/assets/icons/InfoIcon.tsx
================================================
export const InfoIcon = () => {
  return (
    // Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
    <svg
      className="h-full fill-current"
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 512 512"
    >
      <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" />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/KogWheel.tsx
================================================
export const KogWheel = () => {
  // Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.
  return (
    <svg
      className="h-full fill-current"
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 512 512"
    >
      <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" />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/LinkIcon.tsx
================================================
export const LinkIcon = () => {
  return (
    // Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
    <svg viewBox="0 0 640 512" className="fill-current">
      <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" />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/OnlineIcon.tsx
================================================
export const OnlineIcon = () => {
  return (
    // Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc.
    <svg
      className="h-full fill-current"
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 512 512"
    >
      <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" />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/PlayIcon.tsx
================================================
export const PlayIcon = () => {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 384 512"
      className="h-4 fill-current"
    >
      <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" />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/ProfileIcon.tsx
================================================
export const ProfileIcon = () => {
  return (
    <svg
      className="h-6 fill-current"
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 448 512"
    >
      <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" />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/ReloadIcon.tsx
================================================
export const ReloadIcon = () => {
  return (
    // Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.
    <svg className="fill-current" viewBox="0 0 512 512">
      <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" />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/RightArrowIcon.tsx
================================================
export const RightArrowIcon = () => {
  return (
    <svg viewBox="0 0 24 24" className="h-5 fill-current ml-2">
      <path d="M7.96 21.15l-.65-.76 9.555-8.16L7.31 4.07l.65-.76 10.445 8.92"></path>
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/TerminalIcon.tsx
================================================
export const TerminalIcon = () => {
  // <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 640 512"
      className="fill-current"
      style={{
        fontWeight: "bold",
        height: "15px",
      }}
    >
      <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" />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/TwitchLogo.tsx
================================================
export const TwitchLogo = () => {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width="17"
      height="17"
      fill="currentColor"
      viewBox="0 0 18 18"
      className="fill-current"
    >
      <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" />
      <path d="M11.857 3.143h-1.143V6.57h1.143V3.143zm-3.143 0H7.571V6.57h1.143V3.143z" />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/UserGroupIcon.tsx
================================================
export const UserGroupIcon = () => {
  // <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
  return (
    <svg
      className="fill-current h-full"
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 640 512"
    >
      <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" />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/WarningIcon.tsx
================================================
export const WarningIcon = () => {
  // <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 512 512"
      className="h-5 fill-current"
    >
      <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" />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/YoutubeLogo.tsx
================================================
export const YoutubeLogo = () => {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="-35.20005 -41.33325 305.0671 247.9995"
      className="fill-current"
    >
      <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" />
      <path d="M93.333 117.559l61.333-34.89-61.333-34.894z" fill="#fff" />
    </svg>
  );
};


================================================
FILE: packages/webapp-next/assets/icons/index.tsx
================================================
export * from "./CopyIcon";
export * from "./DiscordLogo";
export * from "./DownArrowIcon";
export * from "./GithubLogo";
export * from "./PlayIcon";
export * from "./ProfileIcon";
export * from "./RightArrowIcon";
export * from "./TwitchLogo";


================================================
FILE: packages/webapp-next/common/api/auth.ts
================================================
import { NextRouter } from "next/router";
import { useCallback } from "react";
import { useGameStore } from "../../modules/play2/state/game-store";
import { useUserStore } from "../state/user-store";
import { getExperimentalServerUrl, getSiteRoot } from "../utils/getServerUrl";
import { fetchUser } from "./user";

export const useGithubAuthFactory = (router: NextRouter, serverUrl: string) => {
  return useCallback(() => {
    let nextUrl = getSiteRoot();
    if (document !== undefined) {
      nextUrl = window.location.href;
    }
    const authUrl = `${serverUrl}/auth/github?next=${nextUrl}`;
    router.push(authUrl);
  }, [router, serverUrl]);
};

export const logout = async () => {
  const serverUrl = getExperimentalServerUrl();
  const authUrl = `${serverUrl}/api/auth`;
  return fetch(authUrl, {
    method: "DELETE",
    credentials: "include",
  }).then(async () => {
    const prevUserId = useUserStore.getState().id;
    const user = await fetchUser();
    useUserStore.setState((state) => ({
      ...state,
      ...user,
      avatarUrl: undefined,
    }));
    useGameStore.setState((state) => {
      return {
        ...state,
        owner: state.owner === prevUserId ? user.id : state.owner,
      };
    });
    useGameStore.getState().game?.reconnect();
  });
};


================================================
FILE: packages/webapp-next/common/api/races.ts
================================================
import { getExperimentalServerUrl } from "../utils/getServerUrl";

const serverUrl = getExperimentalServerUrl();

const RACE_STATUS_API = "/api/races/:id/status";

export const fetchRaceStatus = async (raceId: string) => {
  const url = serverUrl + RACE_STATUS_API.replace(":id", raceId);
  return fetch(url, {
    credentials: "include",
  }).then((resp) => resp.json());
};

export const ONLINE_COUNT_API = serverUrl + "/api/races/online";

export const fetchOnlineCount = async () => {
  return fetch(ONLINE_COUNT_API, {
    credentials: "include",
  }).then((resp) => resp.json());
};


================================================
FILE: packages/webapp-next/common/api/types.ts
================================================
import { GetServerSidePropsContext, PreviewData } from "next";
import { ParsedUrlQuery } from "querystring";

export type ServerSideContext = GetServerSidePropsContext<
  ParsedUrlQuery,
  PreviewData
>;


================================================
FILE: packages/webapp-next/common/api/user.ts
================================================
import { useEffect, useState } from "react";
import { User } from "../state/user-store";
import { getExperimentalServerUrl } from "../utils/getServerUrl";
import { ServerSideContext } from "./types";

const USER_API = "/api/user";

const withCookie = (ctx?: ServerSideContext) => {
  const cookie = ctx?.req?.headers?.cookie;
  return cookie ? { cookie } : undefined;
};

const withSetHeaders = (resp: Response, ctx?: ServerSideContext) => {
  const setCookie = resp.headers.get("set-cookie");
  if (ctx && setCookie) {
    ctx.res.setHeader("set-cookie", setCookie);
  }
  return resp;
};

export const fetchUser = async (context?: ServerSideContext) => {
  const serverUrl = getExperimentalServerUrl();
  const url = serverUrl + USER_API;
  return fetch(url, {
    credentials: "include",
    headers: withCookie(context),
  }).then((resp) => withSetHeaders(resp, context).json());
};

export const fetchUser2 = async () => {
  const serverUrl = getExperimentalServerUrl();
  const url = serverUrl + USER_API;
  return fetch(url, {
    credentials: "include",
  }).then((resp) => resp.json());
};

export const useUser = () => {
  const [user, setUser] = useState<User | null>(null);
  useEffect(() => {
    fetchUser2().then((u) => setUser(u));
  }, []);
  return user;
};


================================================
FILE: packages/webapp-next/common/components/Avatar.tsx
================================================
import Image from "next/image";
import { ProfileIcon } from "../../assets/icons";

interface AvatarProps {
  avatarUrl?: string;
  username: string;
}

export const Avatar: React.FC<AvatarProps> = ({ avatarUrl, username }) => {
  return (
    <div className="flex items-center cursor-pointer gap-2">
      <span className="hidden sm:block font-extrabold tracking-wider text-sm">{username}</span>
      {avatarUrl ? (
        <Image
          className="cursor-pointer rounded-full"
          width="30"
          height="30"
          quality={100}
          src={avatarUrl}
          alt={username}
        />
      ) : (
        <ProfileIcon />
      )}
    </div>
  );
};


================================================
FILE: packages/webapp-next/common/components/BattleMatcher.tsx
================================================
import { useState } from "react";
import useSWR from "swr";
import { InfoIcon } from "../../assets/icons/InfoIcon";
import Modal from "./modals/Modal";
import { OnlineIcon } from "../../assets/icons/OnlineIcon";
import { UserGroupIcon } from "../../assets/icons/UserGroupIcon";
import { ToggleSelector } from "../../modules/play2/components/RaceSettings";
import { useGameStore, useIsOwner } from "../../modules/play2/state/game-store";
import {
  closeModals,
  openPublicRacesModal,
  toggleRaceIsPublic,
  useSettingsStore,
} from "../../modules/play2/state/settings-store";
import { ONLINE_COUNT_API } from "../api/races";
import { getExperimentalServerUrl } from "../utils/getServerUrl";
import { Overlay } from "./Overlay";
import ModalCloseButton from "./buttons/ModalCloseButton";

export const BattleMatcher: React.FC = () => {
  const [isOpen, setIsOpen] = useState(false);
  const openModal = () => setIsOpen(true);
  const closeModal = () => setIsOpen(false);
  return (
    <div className="flex items-center font-semibold text-sm ml-2">
      <button
        className="ml-2 flex items-center text-off-white h-5 px-1"
        onClick={openModal}
      >
        <OnlineIcon />
        <div className="flex h-full items-end">
          <div
            className="bg-green-300 rounded-full"
            style={{ width: "5px", height: "5px" }}
          />
        </div>
      </button>
      {isOpen && <BattleMatcherModal closeModal={closeModal} />}
    </div>
  );
};

interface BatteListItemProps {
  race: any;
  closeModal: () => void;
}

const BatteListItem: React.FC<BatteListItemProps> = ({ race, closeModal }) => {
  const { ownerName, memberCount } = race;
  const game = useGameStore((state) => state.game);
  const myRaceID = useGameStore((state) => state.id);
  const isMyRace = myRaceID === race.id;
  const joinRace = () => {
    // TODO: if no game -> we should forward to page with ?id=race.id
    closeModal();
    game?.join(race.id);
  };
  return (
    <button
      onClick={joinRace}
      disabled={isMyRace}
      className={`flex w-full items-center justify-between gap-2 text-base rounded-lg bg-gray-300 
      ${
        isMyRace
          ? "bg-gray-400 hover:cursor-not-allowed"
          : "hover:cursor-pointer hover:bg-gray-400"
      } p-2 px-4 my-1`}
    >
      <div className="flex">
        <div className="flex items-center bg-purple-300 px-2 rounded mr-2">
          <div className="h-3">
            <UserGroupIcon />
          </div>
          <span className="mx-2">{memberCount}</span>
        </div>
        <span>{ownerName}</span>
      </div>
      {!isMyRace ? (
        <svg className="fill-current h-4" viewBox="0 0 512 512">
          <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" />
        </svg>
      ) : null}
    </button>
  );
};

interface BattleMatcherModalProps {
  closeModal: () => void;
}

export const PlayingNow = () => {
  const { data } = useSWR(ONLINE_COUNT_API, (...args) =>
    fetch(...args).then((res) => res.json())
  );
  const isPublic = useSettingsStore((s) => s.raceIsPublic);
  const isOpen = useSettingsStore((s) => s.publicRacesModalIsOpen);
  const isOwner = useIsOwner();
  return (
    <>
      <button
        className="font-semibold text-xs tracking-wide"
        onClick={openPublicRacesModal}
      >
        <div className="flex items-center px-1 rounded uppercase">
          <div className="flex text-dark-ocean items-center bg-gray-300 px-2 rounded">
            <div
              className={`mr-1 h-2 w-2 rounded-full 
      ${isPublic ? "bg-green-400" : "bg-gray-400"}`}
            />
            <div className="h-2 mr-1">
              <UserGroupIcon />
            </div>
            {data?.online ?? 0}
          </div>
        </div>
      </button>
      {isOpen && (
        <Overlay onOverlayClick={closeModals}>
          <Modal>
            <div className="flex items-center">
              <h2 className="text-xl mr-2">Public races</h2>
              <button
                className="cursor-default w-4 mr-1"
                title="You can configure your races to be public by default in your settings"
              >
                <InfoIcon />
              </button>
              <ModalCloseButton onButtonClickHandler={closeModals} />
            </div>
            <ToggleSelector
              title="public race"
              description="Enable to let other players find and join your race"
              checked={isPublic}
              disabled={!isOwner}
              toggleEnabled={toggleRaceIsPublic}
            />
            <BattleMatcherContainer closeModal={closeModals} />
          </Modal>
        </Overlay>
      )}
    </>
  );
};

const BattleMatcherModal = ({ closeModal }: BattleMatcherModalProps) => {
  return (
    <Overlay onOverlayClick={closeModal}>
      <BattleMatcherContainer closeModal={closeModal} />
    </Overlay>
  );
};

export const BattleMatcherContainer = ({
  closeModal,
}: {
  closeModal: () => void;
}) => {
  const baseUrl = getExperimentalServerUrl();
  const { data } = useSWR(
    `${baseUrl}/api/races`,
    (...args) => fetch(...args).then((res) => res.json()),
    { refreshInterval: 10000 }
  );

  const availableRaces = data;
  return (
    <div className="">
      {availableRaces && availableRaces.length > 0 && (
        <div className="">
          {availableRaces.map((race: any, i: number) => (
            <BatteListItem key={i} race={race} closeModal={closeModal} />
          ))}
        </div>
      )}
    </div>
  );
};


================================================
FILE: packages/webapp-next/common/components/Button.tsx
================================================
import React, { ButtonHTMLAttributes } from "react";

type ButtonColor = "primary" | "secondary" | "invisible";

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  color: ButtonColor;
  leftIcon?: React.ReactElement;
  rightIcon?: React.ReactElement;
  text?: string;
  size?: "sm" | "md" | "lg";
}

const Button = ({
  color,
  disabled,
  onClick,
  leftIcon,
  rightIcon,
  text,
  title,
  size = "md",
}: ButtonProps) => {
  const colorStyles = getColorStyles(color);
  const disabledStyle = disabled
    ? "cursor-not-allowed opacity-80"
    : "cursor-pointer";

  const buttonSize = () => {
    switch (size) {
      case "lg":
        return "text-xl px-12 py-2";
      case "md":
        return "text-base py-2 px-4";
      case "sm":
        return "text-base py-1 px-2";
    }
  };

  return (
    <button
      type="button"
      title={title}
      style={{ transition: "all .15s ease" }}
      onClick={onClick}
      className={`flex items-center ${colorStyles} ${disabledStyle} ${buttonSize()}`}
      disabled={disabled}
      aria-expanded="true"
      aria-haspopup="true"
    >
      <>
        {leftIcon && leftIcon}
        {text && <p className="pl-1">{text}</p>}
        {rightIcon && rightIcon}
      </>
    </button>
  );
};

function getColorStyles(color: ButtonColor) {
  if (color === "invisible") {
    return "off-white border-none";
  }

  const sharedStyle =
    "flex items-center text-gray-900 border-gray-200 border rounded";

  const style =
    color === "primary" ? `bg-off-white` : `bg-purple-400 hover:bg-purple-300`;

  return `${sharedStyle} ${style}`;
}

export default Button;


================================================
FILE: packages/webapp-next/common/components/Footer/YoutubeLink.tsx
================================================
import getConfig from "next/config";
import { useEffect, useState } from "react";
import { YoutubeLogo } from "../../../assets/icons/YoutubeLogo";

export const useHasClicked = (key: string, value: string): boolean => {
  const [hasClicked, setHasClicked] = useState(true);
  useEffect(() => {
    if (typeof window !== "undefined") {
      const storedValue = localStorage.getItem(key);
      setHasClicked(storedValue === value);
    }
  }, [key, value]);
  return hasClicked;
};

const YOUTUBE_LINK_STORAGE_KEY = "youtube-link";
export const YoutubeLink = () => {
  const [hasClickedNow, setHasClickedNow] = useState(false);
  const youtubeLink = "https://www.youtube.com/watch?v=pNsJS5F-2yg";
  const hasClickedPreviously = useHasClicked(
    YOUTUBE_LINK_STORAGE_KEY,
    youtubeLink
  );

  const color =
    hasClickedPreviously || hasClickedNow ? "text-faded-gray" : "text-red-500";

  return (
    <a
      href={youtubeLink}
      className={`flex items-center ${color} hover:text-red-500`}
      target="blank"
      onClick={() => {
        localStorage.setItem(YOUTUBE_LINK_STORAGE_KEY, youtubeLink);
        setHasClickedNow(true);
      }}
      onMouseDown={(event: any) => {
        if (event.button === 1) {
          localStorage.setItem(YOUTUBE_LINK_STORAGE_KEY, youtubeLink);
          setHasClickedNow(true);
        }
      }}
    >
      <div className="h-6 w-6 flex items-center">
        <YoutubeLogo />
      </div>
      <span className="ml-1">watch</span>
    </a>
  );
};


================================================
FILE: packages/webapp-next/common/components/Footer.tsx
================================================
import { faCode } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useState } from "react";
import { DiscordLogo, GithubLogo, TwitchLogo } from "../../assets/icons";
import { getStargazersCount } from "../github/stargazers";
import { useIsPlaying } from "../hooks/useIsPlaying";
import { PlayingNow } from "./BattleMatcher";
import { YoutubeLink } from "./Footer/YoutubeLink";

function useStargazersCount() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    getStargazersCount().then((stargazersCount) => {
      setCount(stargazersCount);
    });
  }, []);
  return count;
}

export function KeybindInfo() {
  return (
    <div className="flex flex-grow items-center">
      <PlayingNow />
    </div>
  );
}

export function Footer() {
  const isPlaying = useIsPlaying();
  const stargazersCount = useStargazersCount();
  return (
    <footer
      className="h-10 tracking-tighter"
      style={{
        fontFamily: "Fira Code",
      }}
    >
      {!isPlaying && (
        <div className="w-full bg-dark-ocean">
          <AnimatePresence>
            <motion.div
              className="flex items-center justify-center text-faded-gray"
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              transition={{ duration: 0.5 }}
            >
              <div className="hidden sm:flex flex-grow items-center mb-2 text-xs">
                <h1 className="bg-dark-lake py-1 px-2 rounded font-bold text-faded-gray">
                  Tab
                </h1>
                <span className="mx-1 text-faded-gray">Refresh challenge</span>
                <h1 className="bg-dark-lake py-1 px-2 rounded font-bold ml-2 text-faded-gray">
                  Enter
                </h1>

                <span className="mx-1 text-faded-gray">Start race</span>
              </div>
              <div className="flex gap-2 sm:gap-4">
                <a
                  href="https://github.com/codicocodes/speedtyper.dev"
                  className="flex items-center text-white hover:text-off-white"
                  target="blank"
                >
                  <GithubLogo />
                  <span className="hidden sm:block ml-1">
                    {stargazersCount} stars
                  </span>
                  <span className="block sm:hidden ml-1">star</span>
                </a>
                <a
                  href="https://discord.gg/AMbnnN5eep"
                  className="flex items-center hover:text-blue-300"
                  target="blank"
                >
                  <DiscordLogo />
                  <span className="ml-1">join</span>
                </a>
                <a
                  href="https://twitch.tv/codico"
                  className="flex items-center hover:text-purple-400"
                  target="blank"
                >
                  <TwitchLogo />
                  <span className="ml-1">live</span>
                </a>
                <YoutubeLink />
                <a
                  href="https://dotfyle.com"
                  className="flex items-center hover:text-emerald-500 gap-1"
                  target="blank"
                >
                  <div className="h-5 w-5 flex items-center">
                    <FontAwesomeIcon icon={faCode} size="2x" color="fill" />
                  </div>
                  <span className="ml-1">nvim</span>
                </a>
              </div>
            </motion.div>
          </AnimatePresence>
        </div>
      )}
    </footer>
  );
}


================================================
FILE: packages/webapp-next/common/components/Layout.tsx
================================================
import { Footer } from "./Footer";
import { navbarFactory } from "./NewNavbar";

interface LayoutProps {
  children: JSX.Element;
}

interface ContainerProps {
  children: JSX.Element;
  centered: boolean;
}

export function Container({ children, centered }: ContainerProps) {
  return (
    <div
      className={`flex justify-center h-full w-full 
     ${centered ? "items-center justify-content" : ""} `}
    >
      <div className="flex flex-col max-w-5xl w-full h-full justify-center relative">
        {children}
      </div>
    </div>
  );
}

export function Layout({ children }: LayoutProps) {
  const Navbar = navbarFactory();
  return (
    <>
      <Container centered={false}>
        <Navbar />
      </Container>
      <Container centered={true}>{children}</Container>
      <Container centered={false}>
        <>
          <Footer />
        </>
      </Container>
    </>
  );
}


================================================
FILE: packages/webapp-next/common/components/NewNavbar.tsx
================================================
import { AnimatePresence, motion } from "framer-motion";
import Link from "next/link";
import { TerminalIcon } from "../../assets/icons/TerminalIcon";
import { Logo, WebsiteName } from "../../components/Navbar";
import { LeaderboardButton } from "../../modules/play2/components/leaderboard/LeaderboardButton";
import { useGameStore } from "../../modules/play2/state/game-store";
import { useIsPlaying } from "../hooks/useIsPlaying";
import { PlayingNow } from "./BattleMatcher";
import Button from "./Button";
import { NewGithubLoginModal } from "./modals/GithubLoginModal";
import { SettingsModal } from "./modals/SettingsModal";

export const navbarFactory = () => {
  return NewNavbar;
};

const HomeLink = () => {
  return (
    <Link href="/">
      <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">
        <div className="flex items-center mr-4 mb-1">
          <Logo />
        </div>
        <WebsiteName />
      </span>
    </Link>
  );
};

const ProfileSection = () => {
  const isPlaying = useIsPlaying();
  return (
    <>
      {!isPlaying && (
        <AnimatePresence>
          <motion.div
            className="flex-grow flex items-center"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={{ duration: 0.5 }}
          >
            <div className="text-sm flex-grow"></div>
            <NewGithubLoginModal />
          </motion.div>
        </AnimatePresence>
      )}
    </>
  );
};

export const NewNavbar = () => {
  const isPlaying = useIsPlaying();
  return (
    <header
      className="mt-2 h-10 tracking-tighter"
      style={{
        fontFamily: "Fira Code",
      }}
    >
      <div className="w-full">
        <div className="flex items-center items-start py-2">
          <HomeLink />
          {!isPlaying && (
            <div className="flex gap-2">
              <Link href="/">
                <Button
                  size="sm"
                  color="invisible"
                  onClick={() => useGameStore.getState().game?.play()}
                  leftIcon={<TerminalIcon />}
                />
              </Link>
              <LeaderboardButton />
              <SettingsModal />
              <PlayingNow />
            </div>
          )}
          <ProfileSection />
        </div>
      </div>
    </header>
  );
};


================================================
FILE: packages/webapp-next/common/components/Overlay.tsx
================================================
import { Keys, useKeyMap } from "../../hooks/useKeyMap";

interface OverlayProps {
  onOverlayClick: () => void;
  children: React.ReactNode;
}

export const Overlay: React.FC<OverlayProps> = (props) => 
Download .txt
gitextract_8g1dy2zt/

├── .github/
│   └── workflows/
│       └── webapp-linting-and-unit-tests.yaml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
└── packages/
    ├── back-nest/
    │   ├── .eslintrc.js
    │   ├── .gitignore
    │   ├── .prettierrc
    │   ├── Dockerfile
    │   ├── README.md
    │   ├── docker-compose.yml
    │   ├── nest-cli.json
    │   ├── package.json
    │   ├── scripts/
    │   │   ├── seed-local.sh
    │   │   └── seed-production.sh
    │   ├── src/
    │   │   ├── app.module.ts
    │   │   ├── auth/
    │   │   │   ├── auth.module.ts
    │   │   │   └── github/
    │   │   │       ├── github.controller.ts
    │   │   │       ├── github.guard.ts
    │   │   │       └── github.strategy.ts
    │   │   ├── challenges/
    │   │   │   ├── challenges.module.ts
    │   │   │   ├── commands/
    │   │   │   │   ├── calculate-language-runner.ts
    │   │   │   │   ├── challenge-import-runner.ts
    │   │   │   │   ├── reformat-challenges-runner.ts
    │   │   │   │   └── unsynced-file-import-runner.ts
    │   │   │   ├── entities/
    │   │   │   │   ├── challenge.entity.ts
    │   │   │   │   ├── language.dto.ts
    │   │   │   │   └── unsynced-file.entity.ts
    │   │   │   ├── languages.controller.ts
    │   │   │   └── services/
    │   │   │       ├── challenge.service.ts
    │   │   │       ├── literal.service.ts
    │   │   │       ├── parser.service.ts
    │   │   │       ├── tests/
    │   │   │       │   └── parser.service.spec.ts
    │   │   │       ├── ts-parser.factory.ts
    │   │   │       ├── unsynced-file-filterer.ts
    │   │   │       ├── unsynced-file-importer.ts
    │   │   │       └── unsynced-file.service.ts
    │   │   ├── commands.ts
    │   │   ├── config/
    │   │   │   ├── cors.ts
    │   │   │   └── postgres.ts
    │   │   ├── connectors/
    │   │   │   └── github/
    │   │   │       ├── github.module.ts
    │   │   │       ├── schemas/
    │   │   │       │   ├── github-blob.dto.ts
    │   │   │       │   ├── github-repository.dto.ts
    │   │   │       │   └── github-tree.dto.ts
    │   │   │       └── services/
    │   │   │           └── github-api.ts
    │   │   ├── database.module.ts
    │   │   ├── filters/
    │   │   │   └── exception.filter.ts
    │   │   ├── main.ts
    │   │   ├── middlewares/
    │   │   │   └── guest-user.ts
    │   │   ├── projects/
    │   │   │   ├── commands/
    │   │   │   │   ├── import-untracked-projects-runner.ts
    │   │   │   │   └── sync-untracked-projects-runner.ts
    │   │   │   ├── entities/
    │   │   │   │   ├── project.entity.ts
    │   │   │   │   └── untracked-project.entity.ts
    │   │   │   ├── project.controller.ts
    │   │   │   ├── projects.module.ts
    │   │   │   └── services/
    │   │   │       ├── project.service.ts
    │   │   │       ├── projects-from-file-reader.ts
    │   │   │       └── untracked-projects.service.ts
    │   │   ├── races/
    │   │   │   ├── entities/
    │   │   │   │   └── race-settings.dto.ts
    │   │   │   ├── race.controllers.ts
    │   │   │   ├── race.exceptions.ts
    │   │   │   ├── race.gateway.ts
    │   │   │   ├── races.module.ts
    │   │   │   └── services/
    │   │   │       ├── add-keystroke.service.ts
    │   │   │       ├── countdown.service.ts
    │   │   │       ├── keystroke-validator.service.ts
    │   │   │       ├── locker.service.ts
    │   │   │       ├── progress.service.ts
    │   │   │       ├── race-events.service.ts
    │   │   │       ├── race-manager.service.ts
    │   │   │       ├── race-player.service.ts
    │   │   │       ├── race.service.ts
    │   │   │       ├── results-handler.service.ts
    │   │   │       ├── session-state.service.ts
    │   │   │       └── tests/
    │   │   │           └── race-player.service.spec.ts
    │   │   ├── results/
    │   │   │   ├── entities/
    │   │   │   │   ├── leaderboard-result.dto.ts
    │   │   │   │   └── result.entity.ts
    │   │   │   ├── errors.ts
    │   │   │   ├── results.controller.ts
    │   │   │   ├── results.module.ts
    │   │   │   └── services/
    │   │   │       ├── result-calculation.service.ts
    │   │   │       ├── result-factory.service.ts
    │   │   │       └── results.service.ts
    │   │   ├── seeder/
    │   │   │   ├── commands/
    │   │   │   │   └── challenge.seeder.ts
    │   │   │   └── seeder.module.ts
    │   │   ├── sessions/
    │   │   │   ├── session.adapter.ts
    │   │   │   ├── session.entity.ts
    │   │   │   ├── session.middleware.ts
    │   │   │   └── types.d.ts
    │   │   ├── tracking/
    │   │   │   ├── entities/
    │   │   │   │   └── event.entity.ts
    │   │   │   ├── tracking.module.ts
    │   │   │   └── tracking.service.ts
    │   │   ├── users/
    │   │   │   ├── controllers/
    │   │   │   │   └── user.controller.ts
    │   │   │   ├── entities/
    │   │   │   │   ├── upsertGithubUserDTO.ts
    │   │   │   │   └── user.entity.ts
    │   │   │   ├── services/
    │   │   │   │   └── user.service.ts
    │   │   │   ├── users.module.ts
    │   │   │   └── utils/
    │   │   │       └── generateRandomUsername.ts
    │   │   └── utils/
    │   │       └── validateDTO.ts
    │   ├── tracked-projects.txt
    │   ├── tsconfig.build.json
    │   └── tsconfig.json
    └── webapp-next/
        ├── .eslintrc.json
        ├── .gitignore
        ├── .prettierrc
        ├── README.md
        ├── Socket.ts
        ├── assets/
        │   └── icons/
        │       ├── BattleIcon.tsx
        │       ├── CopyIcon.tsx
        │       ├── CrossIcon.tsx
        │       ├── CrownIcon.tsx
        │       ├── DiscordLogo.tsx
        │       ├── DownArrowIcon.tsx
        │       ├── GithubLogo.tsx
        │       ├── InfoIcon.tsx
        │       ├── KogWheel.tsx
        │       ├── LinkIcon.tsx
        │       ├── OnlineIcon.tsx
        │       ├── PlayIcon.tsx
        │       ├── ProfileIcon.tsx
        │       ├── ReloadIcon.tsx
        │       ├── RightArrowIcon.tsx
        │       ├── TerminalIcon.tsx
        │       ├── TwitchLogo.tsx
        │       ├── UserGroupIcon.tsx
        │       ├── WarningIcon.tsx
        │       ├── YoutubeLogo.tsx
        │       └── index.tsx
        ├── common/
        │   ├── api/
        │   │   ├── auth.ts
        │   │   ├── races.ts
        │   │   ├── types.ts
        │   │   └── user.ts
        │   ├── components/
        │   │   ├── Avatar.tsx
        │   │   ├── BattleMatcher.tsx
        │   │   ├── Button.tsx
        │   │   ├── Footer/
        │   │   │   └── YoutubeLink.tsx
        │   │   ├── Footer.tsx
        │   │   ├── Layout.tsx
        │   │   ├── NewNavbar.tsx
        │   │   ├── Overlay.tsx
        │   │   ├── buttons/
        │   │   │   ├── GithubLoginButton.tsx
        │   │   │   └── ModalCloseButton.tsx
        │   │   ├── modals/
        │   │   │   ├── GithubLoginModal.tsx
        │   │   │   ├── GithubModal.tsx
        │   │   │   ├── Modal.tsx
        │   │   │   ├── ProfileModal.tsx
        │   │   │   └── SettingsModal.tsx
        │   │   └── overlays/
        │   │       ├── GithubLoginOverlay.tsx
        │   │       └── SettingsOverlay.tsx
        │   ├── github/
        │   │   └── stargazers.ts
        │   ├── hooks/
        │   │   ├── useIsPlaying.ts
        │   │   └── useSocket.ts
        │   ├── services/
        │   │   └── Socket.ts
        │   ├── state/
        │   │   └── user-store.ts
        │   └── utils/
        │       ├── clipboard.ts
        │       ├── cpmToWPM.ts
        │       ├── getServerUrl.ts
        │       ├── router.ts
        │       └── toHumanReadableTime.ts
        ├── components/
        │   ├── Countdown.tsx
        │   ├── Navbar.tsx
        │   └── Stream.tsx
        ├── hooks/
        │   ├── useKeyMap.ts
        │   └── useTotalSeconds.ts
        ├── modules/
        │   └── play2/
        │       ├── components/
        │       │   ├── CodeArea.tsx
        │       │   ├── HiddenCodeInput.tsx
        │       │   ├── IncorrectChars.tsx
        │       │   ├── NextChar.tsx
        │       │   ├── RaceSettings.tsx
        │       │   ├── ResultsChart.tsx
        │       │   ├── SmoothCaret.tsx
        │       │   ├── TweetResult.tsx
        │       │   ├── TypedChars.tsx
        │       │   ├── UntypedChars.tsx
        │       │   ├── leaderboard/
        │       │   │   ├── Leaderboard.tsx
        │       │   │   └── LeaderboardButton.tsx
        │       │   ├── play-footer/
        │       │   │   ├── ChallengeSource/
        │       │   │   │   ├── ChallengeSource.tsx
        │       │   │   │   └── index.ts
        │       │   │   └── PlayFooter/
        │       │   │       ├── PlayFooter.tsx
        │       │   │       └── index.ts
        │       │   ├── play-header/
        │       │   │   └── PlayHeader/
        │       │   │       ├── PlayHeader.tsx
        │       │   │       └── index.tsx
        │       │   └── race-settings/
        │       │       └── LanguageSelector.tsx
        │       ├── containers/
        │       │   ├── CodeTypingContainer.tsx
        │       │   └── ResultsContainer.tsx
        │       ├── hooks/
        │       │   ├── useChallenge.ts
        │       │   ├── useEndGame.ts
        │       │   ├── useFocusRef.ts
        │       │   ├── useGame.ts
        │       │   ├── useGameIdQueryParam.ts
        │       │   ├── useIsCompleted.ts
        │       │   ├── useNodeRect.ts
        │       │   └── useResetStateOnUnmount.ts
        │       ├── services/
        │       │   └── Game.ts
        │       └── state/
        │           ├── code-store.ts
        │           ├── connection-store.ts
        │           ├── game-store.ts
        │           ├── settings-store.ts
        │           └── trends-store.ts
        ├── next.config.js
        ├── package.json
        ├── pages/
        │   ├── 404.tsx
        │   ├── _app.tsx
        │   ├── _document.tsx
        │   ├── index.tsx
        │   └── results/
        │       └── [id].tsx
        ├── postcss.config.js
        ├── public/
        │   └── robots.txt
        ├── styles/
        │   └── globals.css
        ├── tailwind.config.js
        ├── tsconfig.json
        └── utils/
            ├── calculateAccuracy.ts
            ├── cpmToWpm.ts
            ├── getTimeDifference.ts
            ├── humanize.ts
            └── stripIndentation.ts
Download .txt
SYMBOL INDEX (489 symbols across 139 files)

FILE: packages/back-nest/src/app.module.ts
  class AppModule (line 29) | class AppModule {}

FILE: packages/back-nest/src/auth/auth.module.ts
  class AuthModule (line 24) | class AuthModule {}

FILE: packages/back-nest/src/auth/github/github.controller.ts
  class AuthController (line 16) | class AuthController {
    method logout (line 18) | async logout(@Req() request: Request, @Res() response: Response) {
  class GithubAuthController (line 36) | class GithubAuthController {
    method githubLogin (line 39) | async githubLogin() {
    method githubCallback (line 45) | async githubCallback(

FILE: packages/back-nest/src/auth/github/github.guard.ts
  class GithubOauthGuard (line 6) | class GithubOauthGuard extends AuthGuard('github') {
    method getAuthenticateOptions (line 7) | getAuthenticateOptions(context: ExecutionContext): IAuthModuleOptions {
    method getState (line 15) | getState(request: Request) {

FILE: packages/back-nest/src/auth/github/github.strategy.ts
  class GithubStrategy (line 9) | class GithubStrategy extends PassportStrategy(Strategy, 'github') {
    method constructor (line 10) | constructor(cfg: ConfigService, private userService: UserService) {
    method validate (line 23) | async validate(

FILE: packages/back-nest/src/challenges/challenges.module.ts
  class ChallengesModule (line 40) | class ChallengesModule {}

FILE: packages/back-nest/src/challenges/commands/calculate-language-runner.ts
  class CalculateLanguageRunner (line 11) | class CalculateLanguageRunner extends CommandRunner {
    method constructor (line 12) | constructor(
    method run (line 19) | async run(): Promise<void> {

FILE: packages/back-nest/src/challenges/commands/challenge-import-runner.ts
  class ChallengeImportRunner (line 14) | class ChallengeImportRunner extends CommandRunner {
    method constructor (line 15) | constructor(
    method run (line 23) | async run(): Promise<void> {
    method syncChallengesFromFile (line 35) | private async syncChallengesFromFile(file: UnsyncedFile) {
    method parseNodesFromContent (line 49) | private parseNodesFromContent(path: string, base64Content: string) {

FILE: packages/back-nest/src/challenges/commands/reformat-challenges-runner.ts
  class ReformatChallengesRunner (line 12) | class ReformatChallengesRunner extends CommandRunner {
    method constructor (line 13) | constructor(
    method run (line 20) | async run(): Promise<void> {

FILE: packages/back-nest/src/challenges/commands/unsynced-file-import-runner.ts
  class UnsyncedFileImportRunner (line 10) | class UnsyncedFileImportRunner extends CommandRunner {
    method constructor (line 11) | constructor(
    method run (line 17) | async run(): Promise<void> {

FILE: packages/back-nest/src/challenges/entities/challenge.entity.ts
  class Challenge (line 16) | class Challenge {
    method fromTSNode (line 36) | static fromTSNode(
    method getStrippedCode (line 59) | static getStrippedCode(code: string) {

FILE: packages/back-nest/src/challenges/entities/language.dto.ts
  class LanguageDTO (line 3) | class LanguageDTO {

FILE: packages/back-nest/src/challenges/entities/unsynced-file.entity.ts
  class UnsyncedFile (line 13) | class UnsyncedFile {
    method fromGithubNode (line 28) | static fromGithubNode(project: Project, treeSha: string, node: GithubN...

FILE: packages/back-nest/src/challenges/languages.controller.ts
  class LanguageController (line 6) | class LanguageController {
    method constructor (line 7) | constructor(private service: ChallengeService) {}
    method getLeaderboard (line 9) | getLeaderboard(): Promise<LanguageDTO[]> {

FILE: packages/back-nest/src/challenges/services/challenge.service.ts
  class ChallengeService (line 8) | class ChallengeService {
    method constructor (line 13) | constructor(
    method upsert (line 18) | async upsert(challenges: Challenge[]): Promise<void> {
    method getRandom (line 25) | async getRandom(language?: string): Promise<Challenge> {
    method getLanguages (line 44) | async getLanguages(): Promise<LanguageDTO[]> {
    method getLanguageName (line 63) | private getLanguageName(language: string): string {

FILE: packages/back-nest/src/challenges/services/literal.service.ts
  class LiteralService (line 4) | class LiteralService {
    method calculateLiterals (line 5) | calculateLiterals(code: string) {

FILE: packages/back-nest/src/challenges/services/parser.service.ts
  class ParserService (line 7) | class ParserService {
    method getParser (line 8) | getParser(language: string) {
  type NodeTypes (line 14) | enum NodeTypes {
  class Parser (line 27) | class Parser {
    method constructor (line 33) | constructor(private ts: TSParser) {}
    method parseTrackedNodes (line 35) | parseTrackedNodes(content: string) {
    method filterNodes (line 40) | private filterNodes(root: TSParser.SyntaxNode) {
    method filterValidNodeTypes (line 50) | private filterValidNodeTypes(node: TSParser.SyntaxNode) {
    method filterLongNodes (line 70) | private filterLongNodes(node: TSParser.SyntaxNode) {
    method filterShortNodes (line 74) | private filterShortNodes(node: TSParser.SyntaxNode) {
    method filterTooManyLines (line 78) | private filterTooManyLines(node: TSParser.SyntaxNode) {
    method filterTooLongLines (line 83) | private filterTooLongLines(node: TSParser.SyntaxNode) {
  function removeDuplicateNewLines (line 93) | function removeDuplicateNewLines(rawText: string) {
  function replaceTabsWithSpaces (line 105) | function replaceTabsWithSpaces(rawText: string) {
  function removeTrailingSpaces (line 111) | function removeTrailingSpaces(rawText: string) {
  function dedupeInnerSpaces (line 118) | function dedupeInnerSpaces(rawText: string) {
  function getFormattedText (line 127) | function getFormattedText(rawText: string) {

FILE: packages/back-nest/src/challenges/services/ts-parser.factory.ts
  class InvalidLanguage (line 37) | class InvalidLanguage extends Error {
    method constructor (line 38) | constructor(language: string) {

FILE: packages/back-nest/src/challenges/services/unsynced-file-filterer.ts
  class UnsyncedFileFilterer (line 6) | class UnsyncedFileFilterer {
    method filter (line 7) | filter(nodes: GithubNode[]) {
  function isBlobNode (line 15) | function isBlobNode(node: GithubNode) {
  function hasTrackedFileExt (line 19) | function hasTrackedFileExt(node: GithubNode) {
  function isNotExcludedPath (line 31) | function isNotExcludedPath(node: GithubNode) {

FILE: packages/back-nest/src/challenges/services/unsynced-file-importer.ts
  class UnsyncedFileImporter (line 9) | class UnsyncedFileImporter {
    method constructor (line 10) | constructor(
    method import (line 15) | async import(project: Project) {

FILE: packages/back-nest/src/challenges/services/unsynced-file.service.ts
  class UnsyncedFileService (line 7) | class UnsyncedFileService {
    method constructor (line 12) | constructor(
    method bulkUpsert (line 17) | async bulkUpsert(files: UnsyncedFile[]): Promise<void> {
    method findAllWithProject (line 21) | async findAllWithProject(): Promise<UnsyncedFile[]> {
    method remove (line 30) | async remove(files: UnsyncedFile[]): Promise<void> {

FILE: packages/back-nest/src/commands.ts
  function runCommand (line 4) | async function runCommand() {

FILE: packages/back-nest/src/connectors/github/github.module.ts
  class GithubConnectorModule (line 11) | class GithubConnectorModule {}

FILE: packages/back-nest/src/connectors/github/schemas/github-blob.dto.ts
  type GithubBlobEncoding (line 3) | enum GithubBlobEncoding {
  class GithubBlob (line 7) | class GithubBlob {

FILE: packages/back-nest/src/connectors/github/schemas/github-repository.dto.ts
  class GithubLicense (line 3) | class GithubLicense {
  class GithubOwner (line 8) | class GithubOwner {
  class GithubRepository (line 19) | class GithubRepository {

FILE: packages/back-nest/src/connectors/github/schemas/github-tree.dto.ts
  type GithubNodeType (line 3) | enum GithubNodeType {
  class GithubNode (line 8) | class GithubNode {
  class GithubTree (line 23) | class GithubTree {

FILE: packages/back-nest/src/connectors/github/services/github-api.ts
  class GithubAPI (line 12) | class GithubAPI {
    method constructor (line 22) | constructor(private readonly http: HttpService, cfg: ConfigService) {
    method getBlobPermaLink (line 26) | static getBlobPermaLink(
    method get (line 41) | private async get(url: string) {
    method logRateLimit (line 53) | private logRateLimit(resp: AxiosResponse) {
    method fetchRepository (line 62) | async fetchRepository(fullName: string): Promise<GithubRepository> {
    method fetchTree (line 69) | async fetchTree(fullName: string, sha: string): Promise<GithubTree> {
    method fetchBlob (line 79) | async fetchBlob(fullName: string, sha: string): Promise<GithubBlob> {
  function getGithubAccessToken (line 90) | function getGithubAccessToken(cfg: ConfigService) {

FILE: packages/back-nest/src/filters/exception.filter.ts
  type ErrorResponse (line 10) | interface ErrorResponse {
  class AllExceptionsFilter (line 16) | class AllExceptionsFilter implements ExceptionFilter {
    method catch (line 17) | catch(exception: any, host: ArgumentsHost) {

FILE: packages/back-nest/src/main.ts
  function runServer (line 15) | async function runServer() {

FILE: packages/back-nest/src/middlewares/guest-user.ts
  function guestUserMiddleware (line 4) | function guestUserMiddleware(

FILE: packages/back-nest/src/projects/commands/import-untracked-projects-runner.ts
  class ImportUntrackedProjectsRunner (line 11) | class ImportUntrackedProjectsRunner extends CommandRunner {
    method constructor (line 12) | constructor(
    method run (line 19) | async run(): Promise<void> {

FILE: packages/back-nest/src/projects/commands/sync-untracked-projects-runner.ts
  class SyncUntrackedProjectsRunner (line 12) | class SyncUntrackedProjectsRunner extends CommandRunner {
    method constructor (line 13) | constructor(
    method run (line 20) | async run(): Promise<void> {

FILE: packages/back-nest/src/projects/entities/project.entity.ts
  class Project (line 7) | class Project {
    method fromGithubRepository (line 31) | static fromGithubRepository(

FILE: packages/back-nest/src/projects/entities/untracked-project.entity.ts
  class UntrackedProject (line 4) | class UntrackedProject {

FILE: packages/back-nest/src/projects/project.controller.ts
  class ProjectController (line 5) | class ProjectController {
    method constructor (line 6) | constructor(private projectService: ProjectService) {}
    method getLeaderboard (line 8) | getLeaderboard(): Promise<string[]> {

FILE: packages/back-nest/src/projects/projects.module.ts
  class ProjectsModule (line 28) | class ProjectsModule {}

FILE: packages/back-nest/src/projects/services/project.service.ts
  class ProjectService (line 7) | class ProjectService {
    method constructor (line 8) | constructor(
    method bulkUpsert (line 13) | async bulkUpsert(projects: Project[]): Promise<void> {
    method findByFullName (line 17) | async findByFullName(fullName: string) {
    method updateSyncedSha (line 24) | async updateSyncedSha(id: string, syncedSha: string) {
    method findAll (line 33) | async findAll(): Promise<Project[]> {
    method getLanguages (line 38) | async getLanguages(): Promise<string[]> {

FILE: packages/back-nest/src/projects/services/projects-from-file-reader.ts
  class ProjectsFromFileReader (line 6) | class ProjectsFromFileReader {
    method readProjects (line 8) | async *readProjects() {
  function validateProjectName (line 21) | function validateProjectName(slug: string) {

FILE: packages/back-nest/src/projects/services/untracked-projects.service.ts
  class UntrackedProjectService (line 7) | class UntrackedProjectService {
    method constructor (line 8) | constructor(
    method bulkUpsert (line 13) | async bulkUpsert(names: string[]): Promise<void> {
    method remove (line 18) | async remove(untrackedProjects: UntrackedProject[]): Promise<void> {
    method findAll (line 22) | async findAll(): Promise<UntrackedProject[]> {

FILE: packages/back-nest/src/races/entities/race-settings.dto.ts
  class RaceSettingsDTO (line 3) | class RaceSettingsDTO {

FILE: packages/back-nest/src/races/race.controllers.ts
  class RacesController (line 13) | class RacesController {
    method constructor (line 14) | constructor(private raceManager: RaceManager) {}
    method getRaces (line 16) | getRaces(): PublicRace[] {
    method getOnlineCount (line 21) | getOnlineCount(): { online: number } {
    method toggleOnlineState (line 29) | toggleOnlineState(@Req() request: Request): { isPublic: boolean } {
    method getRaceStatus (line 43) | getRaceStatus(

FILE: packages/back-nest/src/races/race.exceptions.ts
  function getSocketFromArgs (line 10) | function getSocketFromArgs(host: ArgumentsHost): Socket {
  class RaceDoesNotExistFilter (line 20) | class RaceDoesNotExistFilter extends BaseWsExceptionFilter {
    method constructor (line 23) | constructor() {
    method catch (line 29) | async catch(error: RaceDoesNotExist, host: ArgumentsHost) {
  class InvalidKeystrokeFilter (line 37) | class InvalidKeystrokeFilter extends BaseWsExceptionFilter {
    method catch (line 38) | async catch(error: InvalidKeystrokeException) {

FILE: packages/back-nest/src/races/race.gateway.ts
  class RaceGateway (line 23) | class RaceGateway {
    method constructor (line 27) | constructor(
    method afterInit (line 36) | afterInit(server: Server) {
    method handleDisconnect (line 41) | handleDisconnect(socket: Socket) {
    method handleConnection (line 52) | async handleConnection(socket: Socket) {
    method onRefreshChallenge (line 100) | async onRefreshChallenge(socket: Socket, settings: RaceSettingsDTO) {
    method onPlay (line 120) | async onPlay(socket: Socket, settings: RaceSettingsDTO) {
    method onKeyStroke (line 135) | async onKeyStroke(socket: Socket, keystroke: KeystrokeDTO) {
    method onJoin (line 142) | async onJoin(socket: Socket, id: string) {
    method onStart (line 165) | async onStart(socket: Socket) {

FILE: packages/back-nest/src/races/races.module.ts
  class RacesModule (line 34) | class RacesModule {}

FILE: packages/back-nest/src/races/services/add-keystroke.service.ts
  class AddKeyStrokeService (line 13) | class AddKeyStrokeService {
    method constructor (line 14) | constructor(
    method validate (line 24) | validate(socket: Socket, keyStroke: KeystrokeDTO) {
    method addKeyStroke (line 31) | async addKeyStroke(socket: Socket, keyStroke: KeystrokeDTO) {
    method syncStartTime (line 50) | async syncStartTime(raceId: string, timestamp: Date) {

FILE: packages/back-nest/src/races/services/countdown.service.ts
  class CountdownService (line 6) | class CountdownService {
    method constructor (line 7) | constructor(private raceEvents: RaceEvents) {}
    method countdown (line 8) | async countdown(race: Race) {

FILE: packages/back-nest/src/races/services/keystroke-validator.service.ts
  class InvalidKeystrokeException (line 7) | class InvalidKeystrokeException extends Error {
    method constructor (line 13) | constructor(
  class RaceNotStartedException (line 29) | class RaceNotStartedException extends BadRequestException {
    method constructor (line 30) | constructor() {
  function getCurrentInputBeforeKeystroke (line 35) | function getCurrentInputBeforeKeystroke(
  class KeyStrokeValidationService (line 48) | class KeyStrokeValidationService {
    method constructor (line 49) | constructor(private raceManager: RaceManager) {}
    method validateKeyStroke (line 51) | validateKeyStroke(player: RacePlayer, recentKeyStroke: KeystrokeDTO) {
    method validateRaceStarted (line 71) | validateRaceStarted(raceID: string) {
    method getStrippedCode (line 78) | private getStrippedCode(raceId: string, keystroke: KeystrokeDTO) {

FILE: packages/back-nest/src/races/services/locker.service.ts
  class Locker (line 4) | class Locker {
    method constructor (line 6) | constructor() {
    method runIfOpen (line 13) | async runIfOpen<T>(lockID: string, callback: () => Promise<T>): Promis...
    method release (line 25) | release(id: string) {

FILE: packages/back-nest/src/races/services/progress.service.ts
  class ProgressService (line 7) | class ProgressService {
    method constructor (line 8) | constructor(private raceManager: RaceManager) {}
    method calculateProgress (line 9) | calculateProgress(player: RacePlayer) {

FILE: packages/back-nest/src/races/services/race-events.service.ts
  class RaceEvents (line 9) | class RaceEvents {
    method getPlayerCount (line 12) | getPlayerCount() {
    method createdRace (line 16) | createdRace(socket: Socket, race: Race) {
    method countdown (line 22) | countdown(raceID: string, i: number) {
    method raceStarted (line 27) | raceStarted(race: Race) {
    method updatedRace (line 31) | updatedRace(_: Socket, race: Race) {
    method joinedRace (line 36) | joinedRace(socket: Socket, race: Race, user: User) {
    method leftRace (line 42) | leftRace(race: Race, user: User) {
    method progressUpdated (line 49) | progressUpdated(socket: Socket, raceId: string, player: RacePlayer) {
    method raceCompleted (line 54) | raceCompleted(raceId: string, result: Result) {
    method raceDoesNotExist (line 58) | raceDoesNotExist(socket: Socket, id: string) {
    method logConnectedSockets (line 61) | async logConnectedSockets() {

FILE: packages/back-nest/src/races/services/race-manager.service.ts
  type PublicRace (line 11) | interface PublicRace {
  class RaceManager (line 18) | class RaceManager {
    method constructor (line 21) | constructor(
    method getOnlineCount (line 27) | getOnlineCount(): number {
    method getPublicRaces (line 35) | getPublicRaces(): PublicRace[] {
    method syncUser (line 45) | syncUser(raceId: string, prevUserId: string, user: User) {
    method debugSize (line 57) | debugSize(msg: string) {
    method create (line 65) | async create(user: User, settings: RaceSettingsDTO): Promise<Race> {
    method refresh (line 75) | async refresh(id: string, language?: string): Promise<Race> {
    method getRace (line 86) | getRace(id: string): Race {
    method getPlayer (line 92) | getPlayer(raceId: string, userId: string): RacePlayer {
    method getChallenge (line 97) | getChallenge(raceId: string): Challenge {
    method getCode (line 103) | getCode(raceId: string): string {
    method join (line 107) | join(user: User, raceId: string): Race | null {
    method leaveRace (line 121) | leaveRace(user: User, raceId: string) {
    method isOwner (line 133) | isOwner(userId: string, raceId: string): boolean {
    method userIsAlreadyPlaying (line 139) | userIsAlreadyPlaying(userId: string): boolean {
  class RaceDoesNotExist (line 146) | class RaceDoesNotExist extends BadRequestException {
    method constructor (line 148) | constructor(id: string) {

FILE: packages/back-nest/src/races/services/race-player.service.ts
  class KeystrokeDTO (line 12) | class KeystrokeDTO {
  class RacePlayer (line 28) | class RacePlayer {
    method toJSON (line 54) | toJSON() {
    method reset (line 58) | reset(literals: string[]) {
    method validKeyStrokes (line 67) | validKeyStrokes() {
    method incorrectKeyStrokes (line 87) | incorrectKeyStrokes() {
    method getValidInput (line 94) | getValidInput() {
    method addKeyStroke (line 101) | addKeyStroke(keyStroke: KeystrokeDTO) {
    method updateLiteral (line 106) | updateLiteral(code: string, keyStroke: KeystrokeDTO) {
    method hasNotStartedTyping (line 119) | hasNotStartedTyping(): boolean {
    method hasCompletedRace (line 123) | hasCompletedRace(): boolean {
    method fromUser (line 127) | static fromUser(raceId: string, user: User, literals: string[]) {

FILE: packages/back-nest/src/races/services/race.service.ts
  type PublicRace (line 7) | interface PublicRace {
  class Race (line 13) | class Race {
    method togglePublic (line 31) | togglePublic(): boolean {
    method toPublic (line 35) | toPublic(): PublicRace {
    method isMultiplayer (line 45) | isMultiplayer(): boolean {
    method toJSON (line 49) | toJSON() {
    method constructor (line 53) | constructor(owner: User, challenge: Challenge, literals: string[]) {
    method start (line 65) | start() {
    method canStartRace (line 69) | canStartRace(userID: string): boolean {
    method getPlayer (line 73) | getPlayer(id: string) {
    method resetProgress (line 77) | resetProgress() {
    method addMember (line 89) | addMember(user: User) {
    method removeMember (line 93) | removeMember(user: User) {

FILE: packages/back-nest/src/races/services/results-handler.service.ts
  class ResultsHandlerService (line 10) | class ResultsHandlerService {
    method constructor (line 11) | constructor(
    method handleResult (line 17) | async handleResult(race: Race, user: User) {

FILE: packages/back-nest/src/races/services/session-state.service.ts
  class SessionState (line 6) | class SessionState {
    method getUser (line 7) | getUser(socket: Socket): User {
    method getRaceID (line 11) | getRaceID(socket: Socket): string {
    method saveRaceID (line 15) | saveRaceID(socket: Socket, id: string) {
    method removeRaceID (line 23) | removeRaceID(socket: Socket) {

FILE: packages/back-nest/src/results/entities/leaderboard-result.dto.ts
  class LeaderBoardResult (line 1) | class LeaderBoardResult {

FILE: packages/back-nest/src/results/entities/result.entity.ts
  class Result (line 12) | class Result {

FILE: packages/back-nest/src/results/errors.ts
  class SaveResultAnonymousNotAllowed (line 3) | class SaveResultAnonymousNotAllowed extends ForbiddenException {
    method constructor (line 4) | constructor() {
  class SaveResultInvalidUserID (line 9) | class SaveResultInvalidUserID extends ForbiddenException {
    method constructor (line 10) | constructor() {
  class SaveResultRaceNotCompleted (line 15) | class SaveResultRaceNotCompleted extends BadRequestException {
    method constructor (line 16) | constructor() {
  class SaveResultUserNotInRace (line 21) | class SaveResultUserNotInRace extends BadRequestException {
    method constructor (line 22) | constructor() {

FILE: packages/back-nest/src/results/results.controller.ts
  class ResultsController (line 15) | class ResultsController {
    method constructor (line 17) | constructor(private resultsService: ResultService) {}
    method getLeaderboard (line 19) | async getLeaderboard(): Promise<LeaderBoardResult[]> {
    method getStatsByUser (line 32) | async getStatsByUser(@Req() request: Request) {
    method getResultByID (line 60) | getResultByID(@Param('resultId') resultId: string) {

FILE: packages/back-nest/src/results/results.module.ts
  class ResultsModule (line 15) | class ResultsModule {}

FILE: packages/back-nest/src/results/services/result-calculation.service.ts
  class ResultCalculationService (line 7) | class ResultCalculationService {
    method getTimeMS (line 8) | getTimeMS(race: Race, player: RacePlayer): number {
    method getCPM (line 15) | getCPM(code: string, timeMS: number): number {
    method getMistakesCount (line 23) | getMistakesCount(player: RacePlayer): number {
    method getAccuracy (line 27) | getAccuracy(player: RacePlayer): number {

FILE: packages/back-nest/src/results/services/result-factory.service.ts
  class ResultFactoryService (line 9) | class ResultFactoryService {
    method constructor (line 10) | constructor(private resultCalculation: ResultCalculationService) {}
    method factory (line 11) | factory(race: Race, player: RacePlayer, user: User): Result {

FILE: packages/back-nest/src/results/services/results.service.ts
  class ResultService (line 8) | class ResultService {
    method constructor (line 9) | constructor(
    method create (line 14) | async create(result: Result): Promise<Result> {
    method upsertByLegacyId (line 18) | async upsertByLegacyId(results: Result[]): Promise<void> {
    method getByID (line 22) | async getByID(id: string) {
    method getLeaderboard (line 35) | async getLeaderboard(): Promise<LeaderBoardResult[]> {
    method getAverageCPM (line 80) | async getAverageCPM(userId: string, take: number): Promise<number> {
    method getAverageCPMSince (line 96) | async getAverageCPMSince(userId: string, since: Date): Promise<number> {
    method getResultPercentile (line 108) | async getResultPercentile(cpm: number): Promise<number> {

FILE: packages/back-nest/src/seeder/commands/challenge.seeder.ts
  class ProjectSeedRunner (line 12) | class ProjectSeedRunner extends CommandRunner {
    method constructor (line 13) | constructor(
    method run (line 19) | async run(): Promise<void> {
    method project_factory (line 26) | project_factory() {
    method challenges_factory (line 41) | challenges_factory(project: Project) {

FILE: packages/back-nest/src/seeder/seeder.module.ts
  class SeederModule (line 10) | class SeederModule {}

FILE: packages/back-nest/src/sessions/session.adapter.ts
  type SocketIOCompatibleMiddleware (line 7) | type SocketIOCompatibleMiddleware = (
  function makeSocketIOReadMiddleware (line 13) | function makeSocketIOReadMiddleware(
  class SessionAdapter (line 38) | class SessionAdapter extends IoAdapter {
    method constructor (line 39) | constructor(
    method createIOServer (line 46) | createIOServer(port: number, opt?: any): any {

FILE: packages/back-nest/src/sessions/session.entity.ts
  class Session (line 11) | class Session implements ISession {

FILE: packages/back-nest/src/sessions/session.middleware.ts
  constant SESSION_SECRET_MIN_LENGTH (line 6) | const SESSION_SECRET_MIN_LENGTH = 12;
  constant ONE_DAY (line 8) | const ONE_DAY = 1000 * 60 * 60 * 24;
  function getSessionSecret (line 36) | function getSessionSecret() {

FILE: packages/back-nest/src/sessions/types.d.ts
  type SessionData (line 5) | interface SessionData {
  type IncomingMessage (line 12) | interface IncomingMessage {

FILE: packages/back-nest/src/tracking/entities/event.entity.ts
  type TrackingEventType (line 3) | enum TrackingEventType {
  class TrackingEvent (line 10) | class TrackingEvent {

FILE: packages/back-nest/src/tracking/tracking.module.ts
  class TrackingModule (line 12) | class TrackingModule {}

FILE: packages/back-nest/src/tracking/tracking.service.ts
  class TrackingService (line 7) | class TrackingService {
    method constructor (line 8) | constructor(
    method trackRaceStarted (line 13) | async trackRaceStarted(): Promise<TrackingEvent> {
    method trackRaceCompleted (line 17) | async trackRaceCompleted(): Promise<TrackingEvent> {
    method trackRaceEvent (line 21) | private async trackRaceEvent(

FILE: packages/back-nest/src/users/controllers/user.controller.ts
  class UserController (line 6) | class UserController {
    method getCurrentUser (line 8) | getCurrentUser(@Req() request: Request): User {

FILE: packages/back-nest/src/users/entities/upsertGithubUserDTO.ts
  class UpsertGithubUserDTO (line 5) | class UpsertGithubUserDTO {
    method fromGithubProfile (line 15) | static fromGithubProfile(profile: Profile) {
    method toUser (line 23) | toUser() {

FILE: packages/back-nest/src/users/entities/user.entity.ts
  class User (line 13) | class User {
    method generateAnonymousUser (line 37) | static generateAnonymousUser() {

FILE: packages/back-nest/src/users/services/user.service.ts
  class UserService (line 7) | class UserService {
    method constructor (line 8) | constructor(
    method upsertGithubUser (line 13) | async upsertGithubUser(userData: User): Promise<User> {
    method findByLegacyID (line 24) | async findByLegacyID(legacyId: string) {

FILE: packages/back-nest/src/users/users.module.ts
  class UsersModule (line 13) | class UsersModule {}

FILE: packages/back-nest/src/utils/validateDTO.ts
  class ValidationErrorContainer (line 5) | class ValidationErrorContainer extends TypeError {
    method constructor (line 7) | constructor(name: string, errors: ValidationError[]) {

FILE: packages/webapp-next/Socket.ts
  class Socket (line 3) | class Socket {
    method constructor (line 6) | constructor(serverUrl: string) {

FILE: packages/webapp-next/common/api/races.ts
  constant RACE_STATUS_API (line 5) | const RACE_STATUS_API = "/api/races/:id/status";
  constant ONLINE_COUNT_API (line 14) | const ONLINE_COUNT_API = serverUrl + "/api/races/online";

FILE: packages/webapp-next/common/api/types.ts
  type ServerSideContext (line 4) | type ServerSideContext = GetServerSidePropsContext<

FILE: packages/webapp-next/common/api/user.ts
  constant USER_API (line 6) | const USER_API = "/api/user";

FILE: packages/webapp-next/common/components/Avatar.tsx
  type AvatarProps (line 4) | interface AvatarProps {

FILE: packages/webapp-next/common/components/BattleMatcher.tsx
  type BatteListItemProps (line 43) | interface BatteListItemProps {
  type BattleMatcherModalProps (line 87) | interface BattleMatcherModalProps {

FILE: packages/webapp-next/common/components/Button.tsx
  type ButtonColor (line 3) | type ButtonColor = "primary" | "secondary" | "invisible";
  type ButtonProps (line 5) | interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  function getColorStyles (line 59) | function getColorStyles(color: ButtonColor) {

FILE: packages/webapp-next/common/components/Footer.tsx
  function useStargazersCount (line 11) | function useStargazersCount() {
  function KeybindInfo (line 21) | function KeybindInfo() {
  function Footer (line 29) | function Footer() {

FILE: packages/webapp-next/common/components/Footer/YoutubeLink.tsx
  constant YOUTUBE_LINK_STORAGE_KEY (line 16) | const YOUTUBE_LINK_STORAGE_KEY = "youtube-link";

FILE: packages/webapp-next/common/components/Layout.tsx
  type LayoutProps (line 4) | interface LayoutProps {
  type ContainerProps (line 8) | interface ContainerProps {
  function Container (line 13) | function Container({ children, centered }: ContainerProps) {
  function Layout (line 26) | function Layout({ children }: LayoutProps) {

FILE: packages/webapp-next/common/components/Overlay.tsx
  type OverlayProps (line 3) | interface OverlayProps {

FILE: packages/webapp-next/common/components/buttons/GithubLoginButton.tsx
  type GithubLoginButtonProps (line 4) | interface GithubLoginButtonProps {

FILE: packages/webapp-next/common/components/buttons/ModalCloseButton.tsx
  type ModalButtonCloseProps (line 1) | interface ModalButtonCloseProps {
  function ModalCloseButton (line 5) | function ModalCloseButton({

FILE: packages/webapp-next/common/components/modals/GithubModal.tsx
  type GithubModalProps (line 3) | interface GithubModalProps {
  function GithubModal (line 7) | function GithubModal({ children }: GithubModalProps) {

FILE: packages/webapp-next/common/components/modals/Modal.tsx
  type ModalProps (line 3) | interface ModalProps {
  function Modal (line 7) | function Modal({ children }: ModalProps) {

FILE: packages/webapp-next/common/components/modals/ProfileModal.tsx
  type ProfileModalProps (line 8) | interface ProfileModalProps {
  function ProfileModal (line 12) | function ProfileModal({ closeModal }: ProfileModalProps) {
  type ProfileItemProps (line 38) | interface ProfileItemProps {
  function ProfileItem (line 43) | function ProfileItem({ children, onClick }: ProfileItemProps) {

FILE: packages/webapp-next/common/components/overlays/GithubLoginOverlay.tsx
  type GithubLoginOverlayProps (line 9) | interface GithubLoginOverlayProps {

FILE: packages/webapp-next/common/components/overlays/SettingsOverlay.tsx
  type SettingsOverlayProps (line 16) | interface SettingsOverlayProps {

FILE: packages/webapp-next/common/github/stargazers.ts
  function getStargazersCount (line 5) | async function getStargazersCount() {
  function fetchStargazersCount (line 11) | async function fetchStargazersCount() {
  function shouldRefreshCache (line 27) | function shouldRefreshCache() {

FILE: packages/webapp-next/common/hooks/useSocket.ts
  function useSocket (line 6) | function useSocket() {

FILE: packages/webapp-next/common/services/Socket.ts
  class SocketLatest (line 3) | class SocketLatest {
    method constructor (line 6) | constructor(serverUrl: string) {
    method disconnect (line 19) | disconnect() {
    method subscribe (line 23) | subscribe(event: string, cb: (error: string | null, msg: any) => void) {
    method emit (line 30) | emit(event: string, data?: any) {

FILE: packages/webapp-next/common/state/user-store.ts
  type User (line 5) | interface User {

FILE: packages/webapp-next/common/utils/clipboard.ts
  function copyToClipboard (line 3) | function copyToClipboard(url: string, message: string) {

FILE: packages/webapp-next/common/utils/cpmToWPM.ts
  function cpmToWPM (line 1) | function cpmToWPM(cpm: number) {

FILE: packages/webapp-next/common/utils/getServerUrl.ts
  function getSiteRoot (line 3) | function getSiteRoot() {
  function getServerUrl (line 7) | function getServerUrl() {
  function getExperimentalServerUrl (line 11) | function getExperimentalServerUrl() {

FILE: packages/webapp-next/common/utils/toHumanReadableTime.ts
  constant MINUTES_IN_SECONDS (line 1) | const MINUTES_IN_SECONDS = 60;
  constant HOURS_IN_SECONDS (line 3) | const HOURS_IN_SECONDS = MINUTES_IN_SECONDS * 60;
  constant DAYS_IN_SECONDS (line 5) | const DAYS_IN_SECONDS = HOURS_IN_SECONDS * 24;

FILE: packages/webapp-next/hooks/useKeyMap.ts
  type Keys (line 3) | enum Keys {

FILE: packages/webapp-next/modules/play2/components/CodeArea.tsx
  type CodeAreaProps (line 5) | interface CodeAreaProps {
  function CodeArea (line 12) | function CodeArea({
  function CodeAreaHeader (line 48) | function CodeAreaHeader({ filePath }: { filePath: string }) {

FILE: packages/webapp-next/modules/play2/components/HiddenCodeInput.tsx
  type HiddenCodeInputProps (line 13) | interface HiddenCodeInputProps {
  function handleOnChange (line 60) | function handleOnChange(e: ChangeEvent<HTMLTextAreaElement>) {
  function preventClick (line 106) | function preventClick(e: MouseEvent<HTMLTextAreaElement>) {
  function preventPaste (line 110) | function preventPaste(e: ClipboardEvent<HTMLTextAreaElement>) {
  function preventArrowKeys (line 114) | function preventArrowKeys(e: KeyboardEvent<HTMLTextAreaElement>) {
  type ArrowKey (line 124) | enum ArrowKey {

FILE: packages/webapp-next/modules/play2/components/IncorrectChars.tsx
  function isOnlySpace (line 3) | function isOnlySpace(str: string) {
  function IncorrectChars (line 7) | function IncorrectChars() {
  function parseIncorrectCharGroups (line 30) | function parseIncorrectCharGroups(incorrectChars: string) {

FILE: packages/webapp-next/modules/play2/components/NextChar.tsx
  type NextCharProps (line 11) | interface NextCharProps {
  function NextChar (line 15) | function NextChar({ focused }: NextCharProps) {

FILE: packages/webapp-next/modules/play2/components/RaceSettings.tsx
  type ToggleSelectorProps (line 108) | interface ToggleSelectorProps {

FILE: packages/webapp-next/modules/play2/components/ResultsChart.tsx
  function ResultsChart (line 75) | function ResultsChart() {

FILE: packages/webapp-next/modules/play2/components/SmoothCaret.tsx
  constant SMOOTH_CARET_ELEMENT_ID (line 10) | const SMOOTH_CARET_ELEMENT_ID = "smooth-caret-element";
  constant PRIMARY_PINK_COLOR (line 12) | const PRIMARY_PINK_COLOR = "#d6bcfa";
  constant OFF_WHITE_COLOR (line 14) | const OFF_WHITE_COLOR = "#374151";
  function useAnimator (line 74) | function useAnimator() {
  class Animator (line 80) | class Animator {
    method constructor (line 82) | constructor(private elementId: string) {
    method getElement (line 86) | private getElement() {
    method elementInStarterPosition (line 94) | private elementInStarterPosition(element: HTMLElement) {
    method animate (line 100) | animate(rect: { left: number; top: number }) {

FILE: packages/webapp-next/modules/play2/components/TweetResult.tsx
  type TweetResultProps (line 6) | interface TweetResultProps {

FILE: packages/webapp-next/modules/play2/components/TypedChars.tsx
  type TypedCharsProps (line 7) | interface TypedCharsProps {
  function TypedChars (line 11) | function TypedChars({ language }: TypedCharsProps) {

FILE: packages/webapp-next/modules/play2/components/UntypedChars.tsx
  function UntypedChars (line 2) | function UntypedChars() {

FILE: packages/webapp-next/modules/play2/components/leaderboard/Leaderboard.tsx
  type WPMLeaderboardProps (line 115) | interface WPMLeaderboardProps {
  type LeaderboardRowWPMProps (line 187) | interface LeaderboardRowWPMProps {
  type LeaderboardRowActivityProps (line 211) | interface LeaderboardRowActivityProps {
  type Leaderboards (line 233) | type Leaderboards = "wpm" | "activity";
  type LeaderboardSelectorProps (line 235) | interface LeaderboardSelectorProps {

FILE: packages/webapp-next/modules/play2/components/play-footer/ChallengeSource/ChallengeSource.tsx
  type ChallengeSourceProps (line 12) | interface ChallengeSourceProps {

FILE: packages/webapp-next/modules/play2/components/play-footer/PlayFooter/PlayFooter.tsx
  type PlayFooterProps (line 34) | interface PlayFooterProps {
  function useCodeStoreTotalSeconds (line 38) | function useCodeStoreTotalSeconds() {
  function useMistakeWarningMessage (line 49) | function useMistakeWarningMessage() {
  function WarningContainer (line 55) | function WarningContainer() {
  function PlayFooter (line 100) | function PlayFooter({ challenge }: PlayFooterProps) {
  type ActionButtonProps (line 165) | interface ActionButtonProps extends ButtonHTMLAttributes<HTMLButtonEleme...
  function ActionButton (line 170) | function ActionButton({
  function ActionButtons (line 189) | function ActionButtons() {
  function Timer (line 259) | function Timer({ seconds }: { seconds: number }) {

FILE: packages/webapp-next/modules/play2/components/play-header/PlayHeader/PlayHeader.tsx
  function ResultsContainer (line 12) | function ResultsContainer() {
  function ProgressContainer (line 25) | function ProgressContainer() {
  type ResultProps (line 37) | interface ResultProps {
  function Result (line 42) | function Result({ result, place }: ResultProps) {
  type ProgressBarProps (line 71) | interface ProgressBarProps {
  type ProgressProps (line 75) | interface ProgressProps {
  function Progress (line 80) | function Progress({ progress, word }: ProgressProps) {
  function ProgressBar (line 101) | function ProgressBar({ player }: ProgressBarProps) {
  function PlayHeader (line 120) | function PlayHeader() {

FILE: packages/webapp-next/modules/play2/components/race-settings/LanguageSelector.tsx
  function LanguageSelector (line 17) | function LanguageSelector() {

FILE: packages/webapp-next/modules/play2/containers/CodeTypingContainer.tsx
  type CodeTypingContainerProps (line 14) | interface CodeTypingContainerProps {
  constant CODE_INPUT_BLUR_DEBOUNCE_MS (line 19) | const CODE_INPUT_BLUR_DEBOUNCE_MS = 1000;
  function CodeTypingContainer (line 23) | function CodeTypingContainer({

FILE: packages/webapp-next/modules/play2/containers/ResultsContainer.tsx
  function ResultsText (line 21) | function ResultsText({
  function ShareResultButton (line 44) | function ShareResultButton({ url }: { url: string }) {
  function DailyStreak (line 60) | function DailyStreak() {
  function ResultsContainer (line 94) | function ResultsContainer() {
  function TrendsWPM (line 173) | function TrendsWPM({ currWPM }: { currWPM: number }) {
  function HistoryicalResult (line 218) | function HistoryicalResult({

FILE: packages/webapp-next/modules/play2/hooks/useChallenge.ts
  type ChallengeInfo (line 6) | interface ChallengeInfo {
  function useChallenge (line 15) | function useChallenge(): ChallengeInfo {

FILE: packages/webapp-next/modules/play2/hooks/useEndGame.ts
  function useEndGame (line 6) | function useEndGame() {

FILE: packages/webapp-next/modules/play2/hooks/useFocusRef.ts
  function useFocusRef (line 3) | function useFocusRef<T extends HTMLElement>(): [

FILE: packages/webapp-next/modules/play2/hooks/useGameIdQueryParam.ts
  function useInitialRaceIdQueryParam (line 3) | function useInitialRaceIdQueryParam(): string | undefined {

FILE: packages/webapp-next/modules/play2/hooks/useNodeRect.ts
  type IRect (line 3) | interface IRect {
  function useNodeRect (line 8) | function useNodeRect<T extends HTMLElement>(

FILE: packages/webapp-next/modules/play2/hooks/useResetStateOnUnmount.ts
  function useResetStateOnUnmount (line 4) | function useResetStateOnUnmount() {

FILE: packages/webapp-next/modules/play2/services/Game.ts
  class Game (line 8) | class Game {
    method onConnect (line 10) | onConnect(raceId?: string) {
    method constructor (line 40) | constructor(private socket: SocketLatest, raceId?: string) {
    method reconnect (line 46) | reconnect() {
    method id (line 52) | get id() {
    method start (line 56) | start() {
    method sendKeyStroke (line 60) | sendKeyStroke(keyStroke: KeyStroke) {
    method next (line 64) | next() {
    method join (line 76) | join(id: string) {
    method play (line 80) | play() {
    method listenForRaceStarted (line 87) | private listenForRaceStarted() {
    method listenForRaceJoined (line 100) | private listenForRaceJoined() {
    method listenForCountdown (line 118) | private listenForCountdown() {
    method listenForMemberJoined (line 127) | private listenForMemberJoined() {
    method listenForMemberLeft (line 134) | private listenForMemberLeft() {
    method listenForProgressUpdated (line 148) | private listenForProgressUpdated() {
    method listenForRaceCompleted (line 154) | private listenForRaceCompleted() {
    method updateMemberInState (line 173) | private updateMemberInState(member: RacePlayer) {
    method listenForRaceDoesNotExist (line 184) | private listenForRaceDoesNotExist() {
    method listenForDisconnect (line 194) | private listenForDisconnect() {
    method initializeConnectedState (line 204) | private initializeConnectedState(socket: SocketLatest) {

FILE: packages/webapp-next/modules/play2/state/code-store.ts
  type KeyStroke (line 4) | interface KeyStroke {
  type CodeState (line 11) | interface CodeState {
  type TrackedKeys (line 250) | enum TrackedKeys {
  function isLineBreak (line 254) | function isLineBreak(key: string) {
  function parseKey (line 258) | function parseKey(key: string) {
  function isSkippable (line 267) | function isSkippable(key: string) {

FILE: packages/webapp-next/modules/play2/state/connection-store.ts
  type ConnectionState (line 8) | interface ConnectionState {

FILE: packages/webapp-next/modules/play2/state/game-store.ts
  type GameState (line 7) | interface GameState {
  type RacePlayer (line 17) | interface RacePlayer {
  type RaceResult (line 24) | interface RaceResult {

FILE: packages/webapp-next/modules/play2/state/settings-store.ts
  type LanguageDTO (line 4) | interface LanguageDTO {
  type SettingsState (line 9) | interface SettingsState {
  constant SYNTAX_HIGHLIGHTING_KEY (line 23) | const SYNTAX_HIGHLIGHTING_KEY = "syntaxHighlighting";
  constant SMOOTH_CARET_KEY (line 25) | const SMOOTH_CARET_KEY = "smoothCaret";
  constant DEFAULT_RACE_IS_PUBLIC_KEY (line 27) | const DEFAULT_RACE_IS_PUBLIC_KEY = "defaultRaceIsPublic2";
  constant LANGUAGE_KEY (line 29) | const LANGUAGE_KEY = "language";
  function getInitialToggleStateFromLocalStorage (line 31) | function getInitialToggleStateFromLocalStorage(
  function getInitialLanguageFromLocalStorage (line 46) | function getInitialLanguageFromLocalStorage(key: string): LanguageDTO | ...

FILE: packages/webapp-next/modules/play2/state/trends-store.ts
  type TrendsState (line 6) | interface TrendsState {

FILE: packages/webapp-next/pages/_app.tsx
  function MyApp (line 13) | function MyApp({ Component, pageProps }: AppProps) {

FILE: packages/webapp-next/pages/_document.tsx
  class MyDocument (line 3) | class MyDocument extends Document {
    method getInitialProps (line 4) | static async getInitialProps(ctx: any) {
    method render (line 9) | render() {

FILE: packages/webapp-next/pages/index.tsx
  function Play2Page (line 31) | function Play2Page() {

FILE: packages/webapp-next/pages/results/[id].tsx
  function ResultPage (line 29) | function ResultPage() {
  function truncateFile (line 145) | function truncateFile(file: string) {

FILE: packages/webapp-next/utils/humanize.ts
  type TimeUnit (line 2) | interface TimeUnit {
  constant TIME_UNITS (line 6) | const TIME_UNITS: TimeUnit = {
  function humanizeAbsolute (line 16) | function humanizeAbsolute(when: Date | number) {
  function humanizeRelative (line 28) | function humanizeRelative(pastMilliseconds: number) {
Condensed preview — 217 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (343K chars).
[
  {
    "path": ".github/workflows/webapp-linting-and-unit-tests.yaml",
    "chars": 781,
    "preview": "name: Frontent linting and unit tests\n \non:\n  push:\n    branches:\n      - main\n  pull_request:\n \nenv:\n  NODE_VERSION: 16"
  },
  {
    "path": ".gitignore",
    "chars": 470,
    "preview": "# 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# "
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1976,
    "preview": "# Contributing\n\n*This is a work in progress.*\n\n### **Table of Contents**\n- [Required](#required) \n- [Running Speedtyper."
  },
  {
    "path": "LICENSE",
    "chars": 1087,
    "preview": "MIT License\n\nCopyright (c) 2022 codico <codicocodes@gmail.com>\n\nPermission is hereby granted, free of charge, to any per"
  },
  {
    "path": "Makefile",
    "chars": 424,
    "preview": "# backend\n\ninstall-backend-dependencies:\n\tyarn --cwd ./packages/back-nest\n\nrun-backend-dev:\n\tyarn --cwd ./packages/back-"
  },
  {
    "path": "README.md",
    "chars": 2523,
    "preview": "\r\n<br>\r\n<div align=\"center\">\r\n  <a href=\"https://speedtyper.dev\" target=\"_blank\">\r\n    <img src=\"https://www.speedtyper."
  },
  {
    "path": "packages/back-nest/.eslintrc.js",
    "chars": 689,
    "preview": "module.exports = {\n  parser: '@typescript-eslint/parser',\n  parserOptions: {\n    project: 'tsconfig.json',\n    tsconfigR"
  },
  {
    "path": "packages/back-nest/.gitignore",
    "chars": 391,
    "preview": "# 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*\n"
  },
  {
    "path": "packages/back-nest/.prettierrc",
    "chars": 51,
    "preview": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}"
  },
  {
    "path": "packages/back-nest/Dockerfile",
    "chars": 265,
    "preview": "FROM node:20\n\n# Create app directory\nRUN mkdir -p /app\nWORKDIR /app\n\n# Install app dependencies\nCOPY package.json /app\nC"
  },
  {
    "path": "packages/back-nest/README.md",
    "chars": 308,
    "preview": "## Seed challenge data\n\n### Seed test challenges\n\n`yarn command seed-challenges`\n\n### Seed production challenges\n\nRequir"
  },
  {
    "path": "packages/back-nest/docker-compose.yml",
    "chars": 347,
    "preview": "# Use postgres/example user/password credentials\nversion: \"3.1\"\n\nservices:\n  db:\n    image: postgres\n    restart: always"
  },
  {
    "path": "packages/back-nest/nest-cli.json",
    "chars": 118,
    "preview": "{\n  \"$schema\": \"https://json.schemastore.org/nest-cli\",\n  \"collection\": \"@nestjs/schematics\",\n  \"sourceRoot\": \"src\"\n}\n"
  },
  {
    "path": "packages/back-nest/package.json",
    "chars": 3537,
    "preview": "{\n  \"name\": \"back-nest\",\n  \"version\": \"0.0.1\",\n  \"description\": \"\",\n  \"author\": \"\",\n  \"private\": true,\n  \"license\": \"UNL"
  },
  {
    "path": "packages/back-nest/scripts/seed-local.sh",
    "chars": 135,
    "preview": "#!/bin/bash\n\nyarn command import-projects &&\nyarn command sync-projects &&\nyarn command import-files &&\nyarn command imp"
  },
  {
    "path": "packages/back-nest/scripts/seed-production.sh",
    "chars": 40,
    "preview": "#!/bin/bash\nrailway run ./seed-local.sh\n"
  },
  {
    "path": "packages/back-nest/src/app.module.ts",
    "chars": 907,
    "preview": "import { Module } from '@nestjs/common';\nimport { ConfigModule } from '@nestjs/config';\nimport { GithubConnectorModule }"
  },
  {
    "path": "packages/back-nest/src/auth/auth.module.ts",
    "chars": 653,
    "preview": "import { Module } from '@nestjs/common';\nimport { PassportModule } from '@nestjs/passport';\nimport { ConfigModule } from"
  },
  {
    "path": "packages/back-nest/src/auth/github/github.controller.ts",
    "chars": 1469,
    "preview": "import {\n  Controller,\n  Delete,\n  Get,\n  HttpException,\n  Req,\n  Res,\n  UseGuards,\n} from '@nestjs/common';\nimport { Re"
  },
  {
    "path": "packages/back-nest/src/auth/github/github.guard.ts",
    "chars": 719,
    "preview": "import { ExecutionContext, Injectable } from '@nestjs/common';\nimport { AuthGuard, IAuthModuleOptions } from '@nestjs/pa"
  },
  {
    "path": "packages/back-nest/src/auth/github/github.strategy.ts",
    "chars": 1160,
    "preview": "import { PassportStrategy } from '@nestjs/passport';\nimport { Injectable } from '@nestjs/common';\nimport { ConfigService"
  },
  {
    "path": "packages/back-nest/src/challenges/challenges.module.ts",
    "chars": 1665,
    "preview": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { GithubConnectorModule"
  },
  {
    "path": "packages/back-nest/src/challenges/commands/calculate-language-runner.ts",
    "chars": 1176,
    "preview": "import { InjectRepository } from '@nestjs/typeorm';\nimport { Command, CommandRunner } from 'nest-commander';\nimport { Re"
  },
  {
    "path": "packages/back-nest/src/challenges/commands/challenge-import-runner.ts",
    "chars": 1955,
    "preview": "import { Command, CommandRunner } from 'nest-commander';\nimport { GithubAPI } from 'src/connectors/github/services/githu"
  },
  {
    "path": "packages/back-nest/src/challenges/commands/reformat-challenges-runner.ts",
    "chars": 1101,
    "preview": "import { InjectRepository } from '@nestjs/typeorm';\nimport { Command, CommandRunner } from 'nest-commander';\nimport { Re"
  },
  {
    "path": "packages/back-nest/src/challenges/commands/unsynced-file-import-runner.ts",
    "chars": 890,
    "preview": "import { Command, CommandRunner } from 'nest-commander';\nimport { ProjectService } from 'src/projects/services/project.s"
  },
  {
    "path": "packages/back-nest/src/challenges/entities/challenge.entity.ts",
    "chars": 1853,
    "preview": "import TSParser from 'tree-sitter';\nimport { Project } from 'src/projects/entities/project.entity';\nimport {\n  Entity,\n "
  },
  {
    "path": "packages/back-nest/src/challenges/entities/language.dto.ts",
    "chars": 138,
    "preview": "import { IsString } from 'class-validator';\n\nexport class LanguageDTO {\n  @IsString()\n  language: string;\n  @IsString()\n"
  },
  {
    "path": "packages/back-nest/src/challenges/entities/unsynced-file.entity.ts",
    "chars": 859,
    "preview": "import { GithubNode } from 'src/connectors/github/schemas/github-tree.dto';\nimport { Project } from 'src/projects/entiti"
  },
  {
    "path": "packages/back-nest/src/challenges/languages.controller.ts",
    "chars": 382,
    "preview": "import { Controller, Get } from '@nestjs/common';\nimport { LanguageDTO } from './entities/language.dto';\nimport { Challe"
  },
  {
    "path": "packages/back-nest/src/challenges/services/challenge.service.ts",
    "chars": 2079,
    "preview": "import { BadRequestException, Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nim"
  },
  {
    "path": "packages/back-nest/src/challenges/services/literal.service.ts",
    "chars": 356,
    "preview": "import { Injectable } from '@nestjs/common';\n\n@Injectable()\nexport class LiteralService {\n  calculateLiterals(code: stri"
  },
  {
    "path": "packages/back-nest/src/challenges/services/parser.service.ts",
    "chars": 3770,
    "preview": "import * as TSParser from 'tree-sitter';\nimport { Injectable } from '@nestjs/common';\nimport { getTSLanguageParser } fro"
  },
  {
    "path": "packages/back-nest/src/challenges/services/tests/parser.service.spec.ts",
    "chars": 2933,
    "preview": "import { getFormattedText } from '../parser.service';\n\nconst dubbleNewLineInput = `func newGRPCProxyCommand() *cobra.Com"
  },
  {
    "path": "packages/back-nest/src/challenges/services/ts-parser.factory.ts",
    "chars": 1309,
    "preview": "import * as TSParser from 'tree-sitter';\n\nimport * as js from 'tree-sitter-javascript';\nimport * as ts from 'tree-sitter"
  },
  {
    "path": "packages/back-nest/src/challenges/services/unsynced-file-filterer.ts",
    "chars": 1356,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { GithubNode } from 'src/connectors/github/schemas/github-tree.dto';"
  },
  {
    "path": "packages/back-nest/src/challenges/services/unsynced-file-importer.ts",
    "chars": 915,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { GithubAPI } from 'src/connectors/github/services/github-api';\nimpo"
  },
  {
    "path": "packages/back-nest/src/challenges/services/unsynced-file.service.ts",
    "chars": 925,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } f"
  },
  {
    "path": "packages/back-nest/src/commands.ts",
    "chars": 197,
    "preview": "import { CommandFactory } from 'nest-commander';\nimport { AppModule } from './app.module';\n\nasync function runCommand() "
  },
  {
    "path": "packages/back-nest/src/config/cors.ts",
    "chars": 397,
    "preview": "import { GatewayMetadata } from '@nestjs/websockets';\n\nexport const getAllowedOrigins = () => {\n  return process.env.NOD"
  },
  {
    "path": "packages/back-nest/src/config/postgres.ts",
    "chars": 381,
    "preview": "import * as dotenv from 'dotenv';\nimport { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionO"
  },
  {
    "path": "packages/back-nest/src/connectors/github/github.module.ts",
    "chars": 324,
    "preview": "import { HttpModule } from '@nestjs/axios';\nimport { Module } from '@nestjs/common';\nimport { ConfigModule } from '@nest"
  },
  {
    "path": "packages/back-nest/src/connectors/github/schemas/github-blob.dto.ts",
    "chars": 354,
    "preview": "import { IsEnum, IsNumber, IsString } from 'class-validator';\n\nexport enum GithubBlobEncoding {\n  base64 = 'base64',\n}\n\n"
  },
  {
    "path": "packages/back-nest/src/connectors/github/schemas/github-repository.dto.ts",
    "chars": 846,
    "preview": "import { IsNumber, IsString, ValidateIf } from 'class-validator';\n\nexport class GithubLicense {\n  @IsString()\n  name: st"
  },
  {
    "path": "packages/back-nest/src/connectors/github/schemas/github-tree.dto.ts",
    "chars": 465,
    "preview": "import { IsEnum, IsNumber, IsString } from 'class-validator';\n\nexport enum GithubNodeType {\n  blob = 'blob',\n  tree = 't"
  },
  {
    "path": "packages/back-nest/src/connectors/github/services/github-api.ts",
    "chars": 3439,
    "preview": "import { AxiosResponse } from 'axios';\nimport { HttpService } from '@nestjs/axios';\nimport { Injectable } from '@nestjs/"
  },
  {
    "path": "packages/back-nest/src/database.module.ts",
    "chars": 595,
    "preview": "import { TypeOrmModule } from '@nestjs/typeorm';\nimport { DataSource } from 'typeorm';\nimport { pgOptions } from './conf"
  },
  {
    "path": "packages/back-nest/src/filters/exception.filter.ts",
    "chars": 1180,
    "preview": "import {\n  ExceptionFilter,\n  Catch,\n  ArgumentsHost,\n  HttpException,\n  HttpStatus,\n} from '@nestjs/common';\nimport { Q"
  },
  {
    "path": "packages/back-nest/src/main.ts",
    "chars": 1356,
    "preview": "import * as Sentry from '@sentry/node';\nimport { ValidationPipe } from '@nestjs/common';\nimport { NestFactory } from '@n"
  },
  {
    "path": "packages/back-nest/src/middlewares/guest-user.ts",
    "chars": 321,
    "preview": "import { NextFunction, Request, Response } from 'express';\nimport { User } from 'src/users/entities/user.entity';\n\nexpor"
  },
  {
    "path": "packages/back-nest/src/projects/commands/import-untracked-projects-runner.ts",
    "chars": 914,
    "preview": "import { Command, CommandRunner } from 'nest-commander';\nimport { ProjectService } from '../services/project.service';\ni"
  },
  {
    "path": "packages/back-nest/src/projects/commands/sync-untracked-projects-runner.ts",
    "chars": 1133,
    "preview": "import { Command, CommandRunner } from 'nest-commander';\nimport { GithubAPI } from 'src/connectors/github/services/githu"
  },
  {
    "path": "packages/back-nest/src/projects/entities/project.entity.ts",
    "chars": 1250,
    "preview": "import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';\nimport { GithubRepository } from 'src/conne"
  },
  {
    "path": "packages/back-nest/src/projects/entities/untracked-project.entity.ts",
    "chars": 208,
    "preview": "import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class UntrackedProject {\n  @PrimaryG"
  },
  {
    "path": "packages/back-nest/src/projects/project.controller.ts",
    "chars": 339,
    "preview": "import { Controller, Get } from '@nestjs/common';\nimport { ProjectService } from './services/project.service';\n\n@Control"
  },
  {
    "path": "packages/back-nest/src/projects/projects.module.ts",
    "chars": 1132,
    "preview": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { GithubConnectorModule"
  },
  {
    "path": "packages/back-nest/src/projects/services/project.service.ts",
    "chars": 1161,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } f"
  },
  {
    "path": "packages/back-nest/src/projects/services/projects-from-file-reader.ts",
    "chars": 770,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { createReadStream } from 'fs';\nimport { createInterface } from 'rea"
  },
  {
    "path": "packages/back-nest/src/projects/services/untracked-projects.service.ts",
    "chars": 824,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } f"
  },
  {
    "path": "packages/back-nest/src/races/entities/race-settings.dto.ts",
    "chars": 204,
    "preview": "import { IsBoolean, IsOptional, IsString } from 'class-validator';\n\nexport class RaceSettingsDTO {\n  @IsString()\n  @IsOp"
  },
  {
    "path": "packages/back-nest/src/races/race.controllers.ts",
    "chars": 1286,
    "preview": "import {\n  BadRequestException,\n  Controller,\n  Get,\n  Param,\n  Post,\n  Req,\n} from '@nestjs/common';\nimport { PublicRac"
  },
  {
    "path": "packages/back-nest/src/races/race.exceptions.ts",
    "chars": 2012,
    "preview": "import * as Sentry from '@sentry/node';\nimport { ArgumentsHost, Catch } from '@nestjs/common';\nimport { BaseWsExceptionF"
  },
  {
    "path": "packages/back-nest/src/races/race.gateway.ts",
    "chars": 6271,
    "preview": "import { UseFilters, UsePipes, ValidationPipe } from '@nestjs/common';\nimport {\n  SubscribeMessage,\n  WebSocketGateway,\n"
  },
  {
    "path": "packages/back-nest/src/races/races.module.ts",
    "chars": 1366,
    "preview": "import { Module } from '@nestjs/common';\nimport { ChallengesModule } from 'src/challenges/challenges.module';\nimport { R"
  },
  {
    "path": "packages/back-nest/src/races/services/add-keystroke.service.ts",
    "chars": 2176,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { Socket } from 'socket.io';\nimport { TrackingService } from 'src/tr"
  },
  {
    "path": "packages/back-nest/src/races/services/countdown.service.ts",
    "chars": 759,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { RaceEvents } from './race-events.service';\nimport { Race } from '."
  },
  {
    "path": "packages/back-nest/src/races/services/keystroke-validator.service.ts",
    "chars": 2463,
    "preview": "import { BadRequestException, Injectable } from '@nestjs/common';\nimport { Challenge } from 'src/challenges/entities/cha"
  },
  {
    "path": "packages/back-nest/src/races/services/locker.service.ts",
    "chars": 648,
    "preview": "import { Injectable } from '@nestjs/common';\n\n@Injectable()\nexport class Locker {\n  lockedIDs: Set<string>;\n  constructo"
  },
  {
    "path": "packages/back-nest/src/races/services/progress.service.ts",
    "chars": 657,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { Challenge } from 'src/challenges/entities/challenge.entity';\nimpor"
  },
  {
    "path": "packages/back-nest/src/races/services/race-events.service.ts",
    "chars": 1863,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { Server, Socket } from 'socket.io';\nimport { Result } from 'src/res"
  },
  {
    "path": "packages/back-nest/src/races/services/race-manager.service.ts",
    "chars": 4798,
    "preview": "import { BadRequestException, Injectable } from '@nestjs/common';\nimport { Challenge } from 'src/challenges/entities/cha"
  },
  {
    "path": "packages/back-nest/src/races/services/race-player.service.ts",
    "chars": 3405,
    "preview": "import { Exclude, instanceToPlain } from 'class-transformer';\nimport {\n  IsBoolean,\n  IsNotEmpty,\n  IsNumber,\n  IsString"
  },
  {
    "path": "packages/back-nest/src/races/services/race.service.ts",
    "chars": 2126,
    "preview": "import { Exclude, instanceToPlain } from 'class-transformer';\nimport { randomUUID } from 'crypto';\nimport { Challenge } "
  },
  {
    "path": "packages/back-nest/src/races/services/results-handler.service.ts",
    "chars": 1171,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { ResultFactoryService } from 'src/results/services/result-factory.s"
  },
  {
    "path": "packages/back-nest/src/races/services/session-state.service.ts",
    "chars": 780,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { Socket } from 'socket.io';\nimport { User } from 'src/users/entitie"
  },
  {
    "path": "packages/back-nest/src/races/services/tests/race-player.service.spec.ts",
    "chars": 15201,
    "preview": "import { RacePlayer } from '../race-player.service';\n\ndescribe('[unit] validKeyStrokes()', () => {\n  const player = new "
  },
  {
    "path": "packages/back-nest/src/results/entities/leaderboard-result.dto.ts",
    "chars": 130,
    "preview": "export class LeaderBoardResult {\n  username: string;\n  avatarUrl: string;\n  cpm: number;\n  accuracy: number;\n  createdAt"
  },
  {
    "path": "packages/back-nest/src/results/entities/result.entity.ts",
    "chars": 951,
    "preview": "import { Challenge } from 'src/challenges/entities/challenge.entity';\nimport { User } from 'src/users/entities/user.enti"
  },
  {
    "path": "packages/back-nest/src/results/errors.ts",
    "chars": 642,
    "preview": "import { BadRequestException, ForbiddenException } from '@nestjs/common';\n\nexport class SaveResultAnonymousNotAllowed ex"
  },
  {
    "path": "packages/back-nest/src/results/results.controller.ts",
    "chars": 2121,
    "preview": "import {\n  BadRequestException,\n  Controller,\n  Get,\n  InternalServerErrorException,\n  Param,\n  Req,\n} from '@nestjs/com"
  },
  {
    "path": "packages/back-nest/src/results/results.module.ts",
    "chars": 672,
    "preview": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { Result } from './enti"
  },
  {
    "path": "packages/back-nest/src/results/services/result-calculation.service.ts",
    "chars": 1247,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { Challenge } from 'src/challenges/entities/challenge.entity';\nimpor"
  },
  {
    "path": "packages/back-nest/src/results/services/result-factory.service.ts",
    "chars": 1114,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { RacePlayer } from 'src/races/services/race-player.service';\nimport"
  },
  {
    "path": "packages/back-nest/src/results/services/results.service.ts",
    "chars": 3638,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } f"
  },
  {
    "path": "packages/back-nest/src/seeder/commands/challenge.seeder.ts",
    "chars": 3831,
    "preview": "import { Command, CommandRunner } from 'nest-commander';\nimport { Challenge } from 'src/challenges/entities/challenge.en"
  },
  {
    "path": "packages/back-nest/src/seeder/seeder.module.ts",
    "chars": 362,
    "preview": "import { Module } from '@nestjs/common';\nimport { ChallengesModule } from 'src/challenges/challenges.module';\nimport { P"
  },
  {
    "path": "packages/back-nest/src/sessions/session.adapter.ts",
    "chars": 1314,
    "preview": "import { IncomingMessage } from 'http';\nimport { INestApplication } from '@nestjs/common';\nimport { IoAdapter } from '@n"
  },
  {
    "path": "packages/back-nest/src/sessions/session.entity.ts",
    "chars": 417,
    "preview": "import { ISession } from 'connect-typeorm';\nimport {\n  Column,\n  DeleteDateColumn,\n  Entity,\n  Index,\n  PrimaryColumn,\n}"
  },
  {
    "path": "packages/back-nest/src/sessions/session.middleware.ts",
    "chars": 1300,
    "preview": "import { TypeormStore } from 'connect-typeorm/out';\nimport * as session from 'express-session';\nimport { PostgresDataSou"
  },
  {
    "path": "packages/back-nest/src/sessions/types.d.ts",
    "chars": 346,
    "preview": "import { Session, SessionData } from 'express-session';\nimport { User } from 'src/users/entities/user.entity';\n\ndeclare "
  },
  {
    "path": "packages/back-nest/src/tracking/entities/event.entity.ts",
    "chars": 464,
    "preview": "import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\nexport enum TrackingEventType {\n  LegacyRaceStarted ="
  },
  {
    "path": "packages/back-nest/src/tracking/tracking.module.ts",
    "chars": 383,
    "preview": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { TrackingEvent } from "
  },
  {
    "path": "packages/back-nest/src/tracking/tracking.service.ts",
    "chars": 1276,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } f"
  },
  {
    "path": "packages/back-nest/src/users/controllers/user.controller.ts",
    "chars": 403,
    "preview": "import { Controller, Get, HttpException, Req } from '@nestjs/common';\nimport { Request } from 'express';\nimport { User }"
  },
  {
    "path": "packages/back-nest/src/users/entities/upsertGithubUserDTO.ts",
    "chars": 788,
    "preview": "import { IsString } from 'class-validator';\nimport { Profile } from 'passport-github';\nimport { User } from './user.enti"
  },
  {
    "path": "packages/back-nest/src/users/entities/user.entity.ts",
    "chars": 1077,
    "preview": "import { randomUUID } from 'crypto';\nimport { Result } from 'src/results/entities/result.entity';\nimport {\n  Column,\n  C"
  },
  {
    "path": "packages/back-nest/src/users/services/user.service.ts",
    "chars": 847,
    "preview": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } f"
  },
  {
    "path": "packages/back-nest/src/users/users.module.ts",
    "chars": 432,
    "preview": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { UserController } from"
  },
  {
    "path": "packages/back-nest/src/users/utils/generateRandomUsername.ts",
    "chars": 5150,
    "preview": "import { uniqueNamesGenerator } from 'unique-names-generator';\n\nconst adjectives2 = [\n  'abrupt',\n  'acidic',\n  'adorabl"
  },
  {
    "path": "packages/back-nest/src/utils/validateDTO.ts",
    "chars": 837,
    "preview": "import { ValidationError } from '@nestjs/common';\nimport { ClassConstructor, plainToInstance } from 'class-transformer';"
  },
  {
    "path": "packages/back-nest/tracked-projects.txt",
    "chars": 263,
    "preview": "etcd-io/etcd\nrust-lang/cargo\nrust-lang/rust\ntiangolo/fastapi\npallets/flask\nencode/starlette\napache/zookeeper\nClickHouse/"
  },
  {
    "path": "packages/back-nest/tsconfig.build.json",
    "chars": 97,
    "preview": "{\n  \"extends\": \"./tsconfig.json\",\n  \"exclude\": [\"node_modules\", \"test\", \"dist\", \"**/*spec.ts\"]\n}\n"
  },
  {
    "path": "packages/back-nest/tsconfig.json",
    "chars": 584,
    "preview": "{\n  \"ts-node\": {\n    \"files\": true\n  },\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"declaration\": true,\n    \"r"
  },
  {
    "path": "packages/webapp-next/.eslintrc.json",
    "chars": 191,
    "preview": "{\n  \"extends\": [\"next/core-web-vitals\", \"prettier\"],\n  \"plugins\": [\"prettier\"],\n  \"rules\": {\n    \"prettier/prettier\": [\n"
  },
  {
    "path": "packages/webapp-next/.gitignore",
    "chars": 392,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "packages/webapp-next/.prettierrc",
    "chars": 55,
    "preview": "{\n  \"singleQuote\": false,\n  \"jsxSingleQuote\": false\n}\n\n"
  },
  {
    "path": "packages/webapp-next/README.md",
    "chars": 1582,
    "preview": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js"
  },
  {
    "path": "packages/webapp-next/Socket.ts",
    "chars": 586,
    "preview": "import io from \"socket.io-client\";\n\nexport default class Socket {\n  socket: SocketIOClient.Socket;\n\n  constructor(server"
  },
  {
    "path": "packages/webapp-next/assets/icons/BattleIcon.tsx",
    "chars": 2812,
    "preview": "export const BattleIcon = () => {\n  // <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - "
  },
  {
    "path": "packages/webapp-next/assets/icons/CopyIcon.tsx",
    "chars": 1100,
    "preview": "export const CopyIcon = () => {\n  return (\n    <svg\n      viewBox=\"0 0 128 128\"\n      xmlns=\"http://www.w3.org/2000/svg\""
  },
  {
    "path": "packages/webapp-next/assets/icons/CrossIcon.tsx",
    "chars": 592,
    "preview": "export const CrossIcon = () => {\n  // Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https:/"
  },
  {
    "path": "packages/webapp-next/assets/icons/CrownIcon.tsx",
    "chars": 697,
    "preview": "export const CrownIcon = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 576 512"
  },
  {
    "path": "packages/webapp-next/assets/icons/DiscordLogo.tsx",
    "chars": 1198,
    "preview": "export const DiscordLogo = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"20\"\n      h"
  },
  {
    "path": "packages/webapp-next/assets/icons/DownArrowIcon.tsx",
    "chars": 439,
    "preview": "export const DownArrowIcon = () => {\n  return (\n    <svg\n      className=\"-mr-1 ml-2 h-5 w-5\"\n      xmlns=\"http://www.w3"
  },
  {
    "path": "packages/webapp-next/assets/icons/GithubLogo.tsx",
    "chars": 713,
    "preview": "export const GithubLogo = () => (\n  <svg className=\"fill-current\" height=\"15\" width=\"15\" viewBox=\"0 0 16 16\">\n    <path "
  },
  {
    "path": "packages/webapp-next/assets/icons/InfoIcon.tsx",
    "chars": 626,
    "preview": "export const InfoIcon = () => {\n  return (\n    // Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com Licen"
  },
  {
    "path": "packages/webapp-next/assets/icons/KogWheel.tsx",
    "chars": 1432,
    "preview": "export const KogWheel = () => {\n  // Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://"
  },
  {
    "path": "packages/webapp-next/assets/icons/LinkIcon.tsx",
    "chars": 1087,
    "preview": "export const LinkIcon = () => {\n  return (\n    // Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com Licen"
  },
  {
    "path": "packages/webapp-next/assets/icons/OnlineIcon.tsx",
    "chars": 1348,
    "preview": "export const OnlineIcon = () => {\n  return (\n    // Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com Lic"
  },
  {
    "path": "packages/webapp-next/assets/icons/PlayIcon.tsx",
    "chars": 347,
    "preview": "export const PlayIcon = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 384 512\""
  },
  {
    "path": "packages/webapp-next/assets/icons/ProfileIcon.tsx",
    "chars": 413,
    "preview": "export const ProfileIcon = () => {\n  return (\n    <svg\n      className=\"h-6 fill-current\"\n      xmlns=\"http://www.w3.org"
  },
  {
    "path": "packages/webapp-next/assets/icons/ReloadIcon.tsx",
    "chars": 665,
    "preview": "export const ReloadIcon = () => {\n  return (\n    // Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com Lic"
  },
  {
    "path": "packages/webapp-next/assets/icons/RightArrowIcon.tsx",
    "chars": 218,
    "preview": "export const RightArrowIcon = () => {\n  return (\n    <svg viewBox=\"0 0 24 24\" className=\"h-5 fill-current ml-2\">\n      <"
  },
  {
    "path": "packages/webapp-next/assets/icons/TerminalIcon.tsx",
    "chars": 913,
    "preview": "export const TerminalIcon = () => {\n  // <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License "
  },
  {
    "path": "packages/webapp-next/assets/icons/TwitchLogo.tsx",
    "chars": 476,
    "preview": "export const TwitchLogo = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"17\"\n      he"
  },
  {
    "path": "packages/webapp-next/assets/icons/UserGroupIcon.tsx",
    "chars": 905,
    "preview": "export const UserGroupIcon = () => {\n  // <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License"
  },
  {
    "path": "packages/webapp-next/assets/icons/WarningIcon.tsx",
    "chars": 710,
    "preview": "export const WarningIcon = () => {\n  // <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License -"
  },
  {
    "path": "packages/webapp-next/assets/icons/YoutubeLogo.tsx",
    "chars": 689,
    "preview": "export const YoutubeLogo = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"-35.20005"
  },
  {
    "path": "packages/webapp-next/assets/icons/index.tsx",
    "chars": 245,
    "preview": "export * from \"./CopyIcon\";\nexport * from \"./DiscordLogo\";\nexport * from \"./DownArrowIcon\";\nexport * from \"./GithubLogo\""
  },
  {
    "path": "packages/webapp-next/common/api/auth.ts",
    "chars": 1292,
    "preview": "import { NextRouter } from \"next/router\";\nimport { useCallback } from \"react\";\nimport { useGameStore } from \"../../modul"
  },
  {
    "path": "packages/webapp-next/common/api/races.ts",
    "chars": 589,
    "preview": "import { getExperimentalServerUrl } from \"../utils/getServerUrl\";\n\nconst serverUrl = getExperimentalServerUrl();\n\nconst "
  },
  {
    "path": "packages/webapp-next/common/api/types.ts",
    "chars": 204,
    "preview": "import { GetServerSidePropsContext, PreviewData } from \"next\";\nimport { ParsedUrlQuery } from \"querystring\";\n\nexport typ"
  },
  {
    "path": "packages/webapp-next/common/api/user.ts",
    "chars": 1276,
    "preview": "import { useEffect, useState } from \"react\";\nimport { User } from \"../state/user-store\";\nimport { getExperimentalServerU"
  },
  {
    "path": "packages/webapp-next/common/components/Avatar.tsx",
    "chars": 675,
    "preview": "import Image from \"next/image\";\nimport { ProfileIcon } from \"../../assets/icons\";\n\ninterface AvatarProps {\n  avatarUrl?:"
  },
  {
    "path": "packages/webapp-next/common/components/BattleMatcher.tsx",
    "chars": 5817,
    "preview": "import { useState } from \"react\";\nimport useSWR from \"swr\";\nimport { InfoIcon } from \"../../assets/icons/InfoIcon\";\nimpo"
  },
  {
    "path": "packages/webapp-next/common/components/Button.tsx",
    "chars": 1647,
    "preview": "import React, { ButtonHTMLAttributes } from \"react\";\n\ntype ButtonColor = \"primary\" | \"secondary\" | \"invisible\";\n\ninterfa"
  },
  {
    "path": "packages/webapp-next/common/components/Footer/YoutubeLink.tsx",
    "chars": 1502,
    "preview": "import getConfig from \"next/config\";\nimport { useEffect, useState } from \"react\";\nimport { YoutubeLogo } from \"../../../"
  },
  {
    "path": "packages/webapp-next/common/components/Footer.tsx",
    "chars": 3708,
    "preview": "import { faCode } from \"@fortawesome/free-solid-svg-icons\";\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawes"
  },
  {
    "path": "packages/webapp-next/common/components/Layout.tsx",
    "chars": 897,
    "preview": "import { Footer } from \"./Footer\";\nimport { navbarFactory } from \"./NewNavbar\";\n\ninterface LayoutProps {\n  children: JSX"
  },
  {
    "path": "packages/webapp-next/common/components/NewNavbar.tsx",
    "chars": 2449,
    "preview": "import { AnimatePresence, motion } from \"framer-motion\";\nimport Link from \"next/link\";\nimport { TerminalIcon } from \"../"
  },
  {
    "path": "packages/webapp-next/common/components/Overlay.tsx",
    "chars": 767,
    "preview": "import { Keys, useKeyMap } from \"../../hooks/useKeyMap\";\n\ninterface OverlayProps {\n  onOverlayClick: () => void;\n  child"
  },
  {
    "path": "packages/webapp-next/common/components/buttons/GithubLoginButton.tsx",
    "chars": 474,
    "preview": "import { GithubLogo } from \"../../../assets/icons\";\nimport Button from \"../Button\";\n\ninterface GithubLoginButtonProps {\n"
  },
  {
    "path": "packages/webapp-next/common/components/buttons/ModalCloseButton.tsx",
    "chars": 607,
    "preview": "interface ModalButtonCloseProps {\n  onButtonClickHandler: () => void;\n}\n\nexport default function ModalCloseButton({\n  on"
  },
  {
    "path": "packages/webapp-next/common/components/modals/GithubLoginModal.tsx",
    "chars": 1546,
    "preview": "import { useRouter } from \"next/router\";\nimport React from \"react\";\nimport {\n  closeModals,\n  openProfileModal,\n  useSet"
  },
  {
    "path": "packages/webapp-next/common/components/modals/GithubModal.tsx",
    "chars": 347,
    "preview": "import { ReactNode } from \"react\";\n\ninterface GithubModalProps {\n  children: ReactNode;\n}\n\nexport default function Githu"
  },
  {
    "path": "packages/webapp-next/common/components/modals/Modal.tsx",
    "chars": 327,
    "preview": "import { ReactNode } from \"react\";\n\ninterface ModalProps {\n  children: ReactNode;\n}\n\nexport default function Modal({ chi"
  },
  {
    "path": "packages/webapp-next/common/components/modals/ProfileModal.tsx",
    "chars": 1530,
    "preview": "import Image from \"next/image\";\nimport { ReactNode } from \"react\";\nimport { closeModals } from \"../../../modules/play2/s"
  },
  {
    "path": "packages/webapp-next/common/components/modals/SettingsModal.tsx",
    "chars": 768,
    "preview": "import React from \"react\";\nimport { KogWheel } from \"../../../assets/icons/KogWheel\";\nimport {\n  closeModals,\n  openSett"
  },
  {
    "path": "packages/webapp-next/common/components/overlays/GithubLoginOverlay.tsx",
    "chars": 1540,
    "preview": "import { Overlay } from \"../Overlay\";\nimport ModalCloseButton from \"../buttons/ModalCloseButton\";\nimport GithubModal fro"
  },
  {
    "path": "packages/webapp-next/common/components/overlays/SettingsOverlay.tsx",
    "chars": 1902,
    "preview": "import { InfoIcon } from \"../../../assets/icons/InfoIcon\";\nimport Modal from \"../modals/Modal\";\nimport { closeModals } f"
  },
  {
    "path": "packages/webapp-next/common/github/stargazers.ts",
    "chars": 1050,
    "preview": "const stargazersCountKey = \"stargazersCount\";\n\nconst rateLimitResetKey = \"stargazersCountRateLimitReset\";\n\nexport async "
  },
  {
    "path": "packages/webapp-next/common/hooks/useIsPlaying.ts",
    "chars": 279,
    "preview": "import { useCodeStore } from \"../../modules/play2/state/code-store\";\n\nexport const useIsPlaying = () => {\n  useCodeStore"
  },
  {
    "path": "packages/webapp-next/common/hooks/useSocket.ts",
    "chars": 503,
    "preview": "import { useEffect } from \"react\";\nimport { useConnectionStore } from \"../../modules/play2/state/connection-store\";\nimpo"
  },
  {
    "path": "packages/webapp-next/common/services/Socket.ts",
    "chars": 818,
    "preview": "import { connect, Socket as SocketIOSocket } from \"socketio-latest\";\n\nexport default class SocketLatest {\n  socket: Sock"
  },
  {
    "path": "packages/webapp-next/common/state/user-store.ts",
    "chars": 726,
    "preview": "import { useEffect } from \"react\";\nimport create from \"zustand\";\nimport { fetchUser } from \"../api/user\";\n\nexport interf"
  },
  {
    "path": "packages/webapp-next/common/utils/clipboard.ts",
    "chars": 168,
    "preview": "import { toast } from \"react-toastify\";\n\nexport function copyToClipboard(url: string, message: string) {\n  navigator.cli"
  },
  {
    "path": "packages/webapp-next/common/utils/cpmToWPM.ts",
    "chars": 72,
    "preview": "export function cpmToWPM(cpm: number) {\n  return Math.floor(cpm / 5);\n}\n"
  },
  {
    "path": "packages/webapp-next/common/utils/getServerUrl.ts",
    "chars": 310,
    "preview": "import { publicRuntimeConfig } from \"../../next.config\";\n\nexport function getSiteRoot() {\n  return publicRuntimeConfig?."
  },
  {
    "path": "packages/webapp-next/common/utils/router.ts",
    "chars": 207,
    "preview": "import Router from \"next/router\";\n\nexport const addIDtoQueryParams = (id: string) => {\n  Router.push(\n    {\n      pathna"
  },
  {
    "path": "packages/webapp-next/common/utils/toHumanReadableTime.ts",
    "chars": 862,
    "preview": "const MINUTES_IN_SECONDS = 60;\n\nconst HOURS_IN_SECONDS = MINUTES_IN_SECONDS * 60;\n\nconst DAYS_IN_SECONDS = HOURS_IN_SECO"
  },
  {
    "path": "packages/webapp-next/components/Countdown.tsx",
    "chars": 547,
    "preview": "import React from \"react\";\n\nconst Countdown = ({ countdown }: { countdown: number }) => {\n  const renderedString = count"
  },
  {
    "path": "packages/webapp-next/components/Navbar.tsx",
    "chars": 780,
    "preview": "import Image from \"next/image\";\nimport { useIsPlaying } from \"../common/hooks/useIsPlaying\";\n\nexport const WebsiteName ="
  },
  {
    "path": "packages/webapp-next/components/Stream.tsx",
    "chars": 2431,
    "preview": "import { faChevronUp, faCircle, faX } from \"@fortawesome/free-solid-svg-icons\";\nimport { FontAwesomeIcon } from \"@fortaw"
  },
  {
    "path": "packages/webapp-next/hooks/useKeyMap.ts",
    "chars": 1621,
    "preview": "import { useEffect, useState } from \"react\";\n\nexport enum Keys {\n  Tab = \"Tab\",\n  Enter = \"Enter\",\n  Escape = \"Escape\",\n"
  },
  {
    "path": "packages/webapp-next/hooks/useTotalSeconds.ts",
    "chars": 577,
    "preview": "import { useEffect, useState } from \"react\";\n\nexport default (startTime?: number, endTime?: number): number => {\n  const"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/CodeArea.tsx",
    "chars": 1904,
    "preview": "import { ReactNode } from \"react\";\nimport Countdown from \"../../../components/Countdown\";\nimport { useGameStore } from \""
  },
  {
    "path": "packages/webapp-next/modules/play2/components/HiddenCodeInput.tsx",
    "chars": 3294,
    "preview": "import {\n  ChangeEvent,\n  ClipboardEvent,\n  KeyboardEvent,\n  MouseEvent,\n  useEffect,\n  useState,\n} from \"react\";\n\nimpor"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/IncorrectChars.tsx",
    "chars": 1255,
    "preview": "import { useCodeStore } from \"../state/code-store\";\n\nfunction isOnlySpace(str: string) {\n  return str.trim().length === "
  },
  {
    "path": "packages/webapp-next/modules/play2/components/NextChar.tsx",
    "chars": 1410,
    "preview": "import { AnimatePresence, motion } from \"framer-motion\";\nimport { useNodeRect } from \"../hooks/useNodeRect\";\nimport { us"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/RaceSettings.tsx",
    "chars": 5198,
    "preview": "import { RadioGroup, Switch } from \"@headlessui/react\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport "
  },
  {
    "path": "packages/webapp-next/modules/play2/components/ResultsChart.tsx",
    "chars": 2149,
    "preview": "import React, { RefObject, useEffect, useMemo, useRef } from \"react\";\nimport {\n  Chart,\n  LineController,\n  CategoryScal"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/SmoothCaret.tsx",
    "chars": 2906,
    "preview": "import { useEffect, useMemo } from \"react\";\nimport { AnimatePresence, motion, useAnimationControls } from \"framer-motion"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/TweetResult.tsx",
    "chars": 952,
    "preview": "import { faTwitter } from \"@fortawesome/free-brands-svg-icons\";\nimport { IconDefinition } from \"@fortawesome/free-solid-"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/TypedChars.tsx",
    "chars": 1026,
    "preview": "import highlightjs from \"highlight.js\";\nimport \"highlight.js/styles/github-dark.css\";\nimport { useEffect, useRef } from "
  },
  {
    "path": "packages/webapp-next/modules/play2/components/UntypedChars.tsx",
    "chars": 187,
    "preview": "import { useCodeStore } from \"../state/code-store\";\nexport function UntypedChars() {\n  const untypedChars = useCodeStore"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/leaderboard/Leaderboard.tsx",
    "chars": 8988,
    "preview": "import React, { useEffect, useState } from \"react\";\nimport { RadioGroup } from \"@headlessui/react\";\nimport Image from \"n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/leaderboard/LeaderboardButton.tsx",
    "chars": 1119,
    "preview": "import { AnimatePresence, motion } from \"framer-motion\";\nimport React from \"react\";\nimport { CrownIcon } from \"../../../"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/play-footer/ChallengeSource/ChallengeSource.tsx",
    "chars": 4144,
    "preview": "import React from \"react\";\nimport Link from \"next/link\";\nimport { GithubLogo } from \"../../../../../assets/icons\";\nimpor"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/play-footer/ChallengeSource/index.ts",
    "chars": 35,
    "preview": "export * from \"./ChallengeSource\";\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/play-footer/PlayFooter/PlayFooter.tsx",
    "chars": 8908,
    "preview": "import { faPerson, faUserGroup } from \"@fortawesome/free-solid-svg-icons\";\nimport { FontAwesomeIcon } from \"@fortawesome"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/play-footer/PlayFooter/index.ts",
    "chars": 30,
    "preview": "export * from \"./PlayFooter\";\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/play-header/PlayHeader/PlayHeader.tsx",
    "chars": 4043,
    "preview": "import { AnimatePresence, motion } from \"framer-motion\";\nimport { CrownIcon } from \"../../../../../assets/icons/CrownIco"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/play-header/PlayHeader/index.tsx",
    "chars": 30,
    "preview": "export * from \"./PlayHeader\";\n"
  },
  {
    "path": "packages/webapp-next/modules/play2/components/race-settings/LanguageSelector.tsx",
    "chars": 3712,
    "preview": "import useSWR from \"swr\";\nimport { Listbox } from \"@headlessui/react\";\nimport { CrossIcon } from \"../../../../assets/ico"
  },
  {
    "path": "packages/webapp-next/modules/play2/containers/CodeTypingContainer.tsx",
    "chars": 2984,
    "preview": "import { useFocusRef } from \"../hooks/useFocusRef\";\nimport { useCodeStore } from \"../state/code-store\";\nimport { CodeAre"
  },
  {
    "path": "packages/webapp-next/modules/play2/containers/ResultsContainer.tsx",
    "chars": 8612,
    "preview": "import {\n  faArrowDown,\n  faArrowTrendUp,\n  faArrowUp,\n  faCheckCircle,\n  faCircleXmark,\n  faExternalLink,\n  faShare,\n  "
  },
  {
    "path": "packages/webapp-next/modules/play2/hooks/useChallenge.ts",
    "chars": 1659,
    "preview": "import { useEffect, useState } from \"react\";\nimport SocketLatest from \"../../../common/services/Socket\";\nimport { useCod"
  },
  {
    "path": "packages/webapp-next/modules/play2/hooks/useEndGame.ts",
    "chars": 489,
    "preview": "import { useEffect } from \"react\";\nimport { useIsPlaying } from \"../../../common/hooks/useIsPlaying\";\nimport { useCodeSt"
  },
  {
    "path": "packages/webapp-next/modules/play2/hooks/useFocusRef.ts",
    "chars": 393,
    "preview": "import { useCallback, useEffect, useState } from \"react\";\n\nexport function useFocusRef<T extends HTMLElement>(): [\n  (no"
  },
  {
    "path": "packages/webapp-next/modules/play2/hooks/useGame.ts",
    "chars": 462,
    "preview": "import { useMemo } from \"react\";\nimport { Game } from \"../services/Game\";\nimport { useConnectionStore } from \"../state/c"
  },
  {
    "path": "packages/webapp-next/modules/play2/hooks/useGameIdQueryParam.ts",
    "chars": 220,
    "preview": "import { useRouter } from \"next/router\";\n\nexport function useInitialRaceIdQueryParam(): string | undefined {\n  var route"
  },
  {
    "path": "packages/webapp-next/modules/play2/hooks/useIsCompleted.ts",
    "chars": 196,
    "preview": "import { useCodeStore } from \"../state/code-store\";\n\nexport const useIsCompleted = () => {\n  useCodeStore((state) => sta"
  },
  {
    "path": "packages/webapp-next/modules/play2/hooks/useNodeRect.ts",
    "chars": 543,
    "preview": "import { useEffect, useState } from \"react\";\n\ninterface IRect {\n  top: number;\n  left: number;\n}\n\nexport function useNod"
  },
  {
    "path": "packages/webapp-next/modules/play2/hooks/useResetStateOnUnmount.ts",
    "chars": 285,
    "preview": "import { useEffect } from \"react\";\nimport { useCodeStore } from \"../state/code-store\";\n\nexport function useResetStateOnU"
  },
  {
    "path": "packages/webapp-next/modules/play2/services/Game.ts",
    "chars": 5723,
    "preview": "import SocketLatest from \"../../../common/services/Socket\";\nimport { useUserStore } from \"../../../common/state/user-sto"
  },
  {
    "path": "packages/webapp-next/modules/play2/state/code-store.ts",
    "chars": 8156,
    "preview": "import create from \"zustand\";\nimport { cpmToWPM } from \"../../../common/utils/cpmToWPM\";\n\nexport interface KeyStroke {\n "
  },
  {
    "path": "packages/webapp-next/modules/play2/state/connection-store.ts",
    "chars": 2600,
    "preview": "import { useCallback, useEffect } from \"react\";\nimport create from \"zustand\";\nimport { fetchRaceStatus } from \"../../../"
  },
  {
    "path": "packages/webapp-next/modules/play2/state/game-store.ts",
    "chars": 1529,
    "preview": "import create from \"zustand\";\nimport { User, useUserStore } from \"../../../common/state/user-store\";\nimport { Game } fro"
  },
  {
    "path": "packages/webapp-next/modules/play2/state/settings-store.ts",
    "chars": 5598,
    "preview": "import create from \"zustand\";\nimport { getExperimentalServerUrl } from \"../../../common/utils/getServerUrl\";\n\nexport int"
  },
  {
    "path": "packages/webapp-next/modules/play2/state/trends-store.ts",
    "chars": 1116,
    "preview": "import create from \"zustand\";\nimport { useUserStore } from \"../../../common/state/user-store\";\nimport { cpmToWPM } from "
  }
]

// ... and 17 more files (download for full content)

About this extraction

This page contains the full source code of the codicocodes/speedtyper.dev GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 217 files (304.8 KB), approximately 93.0k tokens, and a symbol index with 489 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!