├── .browserslistrc ├── .editorconfig ├── .github └── workflows │ └── commitlint.yml ├── .gitignore ├── README.md ├── angular.json ├── firebase.json ├── firestore.indexes.json ├── firestore.rules ├── karma.conf.js ├── ngsw-config.json ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── scripts ├── create.js └── firebase.js ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── components │ │ └── form │ │ │ ├── counter │ │ │ ├── counter.component.html │ │ │ ├── counter.component.scss │ │ │ ├── counter.component.spec.ts │ │ │ └── counter.component.ts │ │ │ ├── form.component.html │ │ │ ├── form.component.scss │ │ │ ├── form.component.spec.ts │ │ │ ├── form.component.ts │ │ │ ├── text │ │ │ ├── text.component.html │ │ │ ├── text.component.scss │ │ │ ├── text.component.spec.ts │ │ │ └── text.component.ts │ │ │ ├── timer │ │ │ ├── timer.component.html │ │ │ ├── timer.component.scss │ │ │ ├── timer.component.spec.ts │ │ │ └── timer.component.ts │ │ │ ├── toggle │ │ │ ├── toggle.component.html │ │ │ ├── toggle.component.scss │ │ │ ├── toggle.component.spec.ts │ │ │ └── toggle.component.ts │ │ │ └── widget-row │ │ │ ├── widget-row.component.html │ │ │ ├── widget-row.component.scss │ │ │ ├── widget-row.component.spec.ts │ │ │ └── widget-row.component.ts │ ├── directives │ │ ├── long-press.directive.spec.ts │ │ └── long-press.directive.ts │ ├── models │ │ ├── schema.model.ts │ │ ├── scout.model.ts │ │ ├── stage.model.ts │ │ └── user.model.ts │ ├── pages │ │ ├── admin-panel │ │ │ ├── admin-panel.component.html │ │ │ ├── admin-panel.component.scss │ │ │ ├── admin-panel.component.spec.ts │ │ │ ├── admin-panel.component.ts │ │ │ ├── schema-editor │ │ │ │ ├── schema-editor.component.html │ │ │ │ ├── schema-editor.component.scss │ │ │ │ ├── schema-editor.component.spec.ts │ │ │ │ └── schema-editor.component.ts │ │ │ └── scout-overview │ │ │ │ ├── scout-overview.component.html │ │ │ │ ├── scout-overview.component.scss │ │ │ │ ├── scout-overview.component.spec.ts │ │ │ │ ├── scout-overview.component.ts │ │ │ │ └── team-scouts │ │ │ │ ├── team-scouts.component.html │ │ │ │ ├── team-scouts.component.scss │ │ │ │ ├── team-scouts.component.spec.ts │ │ │ │ └── team-scouts.component.ts │ │ └── fun-zone │ │ │ ├── fun-zone.component.html │ │ │ ├── fun-zone.component.scss │ │ │ ├── fun-zone.component.spec.ts │ │ │ ├── fun-zone.component.ts │ │ │ ├── leaderboard │ │ │ ├── leaderboard.component.html │ │ │ ├── leaderboard.component.scss │ │ │ ├── leaderboard.component.spec.ts │ │ │ └── leaderboard.component.ts │ │ │ ├── matches │ │ │ ├── matches.component.html │ │ │ ├── matches.component.scss │ │ │ ├── matches.component.spec.ts │ │ │ └── matches.component.ts │ │ │ └── simon │ │ │ ├── simon.component.html │ │ │ ├── simon.component.scss │ │ │ ├── simon.component.spec.ts │ │ │ └── simon.component.ts │ ├── services │ │ ├── admin.guard.spec.ts │ │ ├── admin.guard.ts │ │ ├── authentication.service.spec.ts │ │ ├── authentication.service.ts │ │ ├── backend.service.spec.ts │ │ ├── backend.service.ts │ │ ├── the-blue-alliance.service.spec.ts │ │ └── the-blue-alliance.service.ts │ └── utilities │ │ └── widget.ts ├── assets │ ├── .gitkeep │ ├── icons │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ ├── icon-72x72.png │ │ ├── icon-96x96.png │ │ ├── icon-ios.png │ │ └── icon.png │ └── sounds │ │ ├── error.mp3 │ │ ├── simonSound1.mp3 │ │ ├── simonSound2.mp3 │ │ ├── simonSound3.mp3 │ │ └── simonSound4.mp3 ├── environments │ ├── Secrets.json │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── manifest.webmanifest ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Commit Messages 2 | on: [push] 3 | 4 | jobs: 5 | commitlint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | with: 10 | fetch-depth: 0 11 | - uses: wagoid/commitlint-github-action@v4 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | .history/* 32 | 33 | # misc 34 | /.sass-cache 35 | /connect.lock 36 | /coverage 37 | /libpeerconnection.log 38 | npm-debug.log 39 | yarn-error.log 40 | testem.log 41 | /typings 42 | /.firebaserc 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | 48 | /src/environments/firebase.json 49 | /.firebase 50 | /build 51 | /src/environments/secrets.json 52 | .angular -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | Scouting 5 |
6 | 7 | 8 | 9 |

10 | 11 |

12 | A website and PWA for scouting teams at FRC competitions built with Angular, Material and Firebase 13 |

14 | 15 |

16 | 💡 Features 17 |  ·  18 | 🏃 Getting Started 19 |  ·  20 | 🚗 Roadmap 21 |  ·  22 | ⚠️ Common Issues 23 |  ·  24 | ❓ FAQ 25 |

26 | 27 | ## Features 28 | 29 | - Customizable Schema 30 | 31 | - Sections and widgets can be customized to fit your team's analysis criteria. 32 |

33 | schema 34 |

35 | 36 | - Offline Work 37 | - Simple Cross-Platform Installation 38 | - Completions and Verification using The Blue Alliance 39 | - Quick Overview of Missing and Invalid Scouts 40 | 41 |

42 | schema 43 |

44 | 45 | - In-App Schema Editor 46 | - Minigames 47 | 48 |

49 | schema 50 |

51 | 52 | - Security 53 | 54 | ## Getting Started 55 | 56 | - Download the repository, either with `git clone https://github.com/miscar/Scouting` or by downloading the zip archive from GitHub. 57 | - Install [NodeJS](https://nodejs.org/en/download/) and the Node Package Manager. 58 | - Install Scouting's dependencies by running `npm install` in the command-line inside the repository. You may need to run `npm install --force`. 59 | - Create a new project by running `npm run create`. 60 | - Open the [Firebase Console](https://console.firebase.google.com). 61 | - Enable Cloud Firestore by clicking `Firestore Database` in the side panel and then `Create database`. You may start in production/test mode (this will get overridden upon deploy), and pick your own Cloud Firestore location. 62 | 63 | ![image](https://user-images.githubusercontent.com/88707580/144745623-90d8f405-90aa-49e6-a73a-00f31a96d18a.png) 64 | 65 | - Enable Firebase Authentication by clicking `Authentication` in the side panel and then `Get started`. 66 | 67 | ![image](https://user-images.githubusercontent.com/88707580/144745739-613a95e3-787f-4bdd-8219-c400ddd268dd.png) 68 | 69 | - Enable Google Authentication by clicking `Google` under `Additional providers`, and then `Enable`. Select a project support email and then hit `Save`. Make sure you're in the `Sign-in method` tab. 70 | 71 | ![image](https://user-images.githubusercontent.com/88707580/144745826-0e5c4ea2-5a8b-4339-a9cf-988d661dfc21.png) 72 | 73 | - Deploy the app to Firebase Hosting and the firestore rules by running `npm run deploy`. 74 | - Sign in with your Google Account to the live instance, find the UUID in Firebase Authentication and enter it into an "admins" document in the "admin" collection inside an array called `users`. 75 | - That's it! You now have a fully usable Scouting instance. 76 | 77 | ### Dashboard 78 | 79 | You may create a copy of [this](https://docs.google.com/spreadsheets/d/1NOD7aGRPPc0cSSQ7eraKIjULRJrQ1xT-7rlkZ3AyF1I/edit?usp=sharing) Google Sheets document which contains a basic Google Apps Script to fetch all of the data from Firestore. 80 | To set it up, you'll need to get your Firebase service worker and plug it into the script. 81 | 82 | - Open the Firebase project settings: 83 | 84 | ![image](https://user-images.githubusercontent.com/88707580/144745084-18f6bb99-bb59-4778-80f6-0e972680d5a6.png) 85 | 86 | - Open the service accounts tab: 87 | 88 | ![image](https://user-images.githubusercontent.com/88707580/144745233-40fea63c-aba8-44e3-a3d0-bba1a0754b2b.png) 89 | 90 | - Open the script like so: 91 | 92 | ![image](https://user-images.githubusercontent.com/88707580/144744971-d00b5a52-1dca-43f2-9f19-abcebd542393.png) 93 | 94 | - Paste the service account mail into the Google Apps Script placeholder. 95 | - Click `Generate new private key`, then `Generate key` and open the JSON file. 96 | - Copy the `project_id` and `private_key` (from `----BEGIN` to `-----END`) exactly as they are into the Google Apps Script placeholders. 97 | 98 | ## Common Issues 99 | 100 | **Problem** 101 | 102 | `Could not log in because of a FirebaseError: Firebase: Error (auth/configuration-not-found)` shows as an error in the console when trying to sign in with Google. 103 | 104 | **Solution** 105 | 106 | Enable Google Authentication inside Firebase, as instructed [above](#getting-started). 107 | 108 | **Problem** 109 | 110 | `Warning: initial exceeded maximum budget.` shows up when building/deploying. 111 | 112 | **Solution** 113 | 114 | Scouting is currently a big application. You can safely ignore this warning. 115 | 116 | **Problem** 117 | 118 | `Error: src/app/services/the-blue-alliance.service.ts:4:21 - error TS2307: Cannot find module 'environments/secrets.json' or its corresponding type declarations.` shows up when building/deploying. 119 | 120 | **Solution** 121 | 122 | Create a `secrets.json` file with the contents: 123 | 124 | ```json 125 | { 126 | "TBAKey": " here>" 127 | } 128 | ``` 129 | 130 | **Problem** 131 | 132 | ``` 133 | Error: Failed to create project. See firebase-debug.log for more info. 134 | node:child_process:826 135 | err = new Error(msg); 136 | ``` 137 | 138 | when running `npm run create` to set up a new Scouting instance. 139 | 140 | **Solution** 141 | 142 | Your Google Organization probably blocks creating Firebase projects. Run `firebase logout` and try signing in with a different Google account. 143 | 144 | ## Frequently Asked Questions 145 | 146 | **How do you view submitted scout data?** 147 | 148 | A quick overview of invalid scouts (where the team number doesn't match the match teams from The Blue Alliance) and missing scouts (where the match has some scouts but not of all teams) appear in the Admin Panel. 149 | 150 | The data is stored in Cloud Firestore, where each event creates its own collection (You may need to refresh after the first event scout). 151 | 152 | You may also use our Google Sheets template linked [above](#dashboard). 153 | 154 | ## Roadmap 155 | 156 | The following things can greatly improve the quality of the application: 157 | 158 | - [ ] Add unit tests. 159 | - [ ] Reduce bundle size. 160 | - [ ] Simplify the Getting Started process. 161 | - [ ] Add more games. 162 | 163 | ## License 164 | 165 | MIT 166 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "scouting": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist/scouting", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "tsconfig.app.json", 28 | "inlineStyleLanguage": "scss", 29 | "assets": [ 30 | "src/favicon.ico", 31 | "src/assets", 32 | "src/manifest.webmanifest" 33 | ], 34 | "styles": ["src/styles.scss"], 35 | "scripts": [], 36 | "serviceWorker": true, 37 | "ngswConfigPath": "ngsw-config.json" 38 | }, 39 | "configurations": { 40 | "production": { 41 | "budgets": [ 42 | { 43 | "type": "initial", 44 | "maximumWarning": "500kb", 45 | "maximumError": "5mb" 46 | }, 47 | { 48 | "type": "anyComponentStyle", 49 | "maximumWarning": "2kb", 50 | "maximumError": "4kb" 51 | } 52 | ], 53 | "fileReplacements": [ 54 | { 55 | "replace": "src/environments/environment.ts", 56 | "with": "src/environments/environment.prod.ts" 57 | } 58 | ], 59 | "outputHashing": "all" 60 | }, 61 | "development": { 62 | "buildOptimizer": false, 63 | "optimization": false, 64 | "vendorChunk": true, 65 | "extractLicenses": false, 66 | "sourceMap": true, 67 | "namedChunks": true 68 | } 69 | }, 70 | "defaultConfiguration": "production" 71 | }, 72 | "serve": { 73 | "builder": "@angular-devkit/build-angular:dev-server", 74 | "configurations": { 75 | "production": { 76 | "browserTarget": "scouting:build:production" 77 | }, 78 | "development": { 79 | "browserTarget": "scouting:build:development" 80 | } 81 | }, 82 | "defaultConfiguration": "development" 83 | }, 84 | "extract-i18n": { 85 | "builder": "@angular-devkit/build-angular:extract-i18n", 86 | "options": { 87 | "browserTarget": "scouting:build" 88 | } 89 | }, 90 | "test": { 91 | "builder": "@angular-devkit/build-angular:karma", 92 | "options": { 93 | "main": "src/test.ts", 94 | "polyfills": "src/polyfills.ts", 95 | "tsConfig": "tsconfig.spec.json", 96 | "karmaConfig": "karma.conf.js", 97 | "inlineStyleLanguage": "scss", 98 | "assets": [ 99 | "src/favicon.ico", 100 | "src/assets", 101 | "src/manifest.webmanifest" 102 | ], 103 | "styles": ["src/styles.scss"], 104 | "scripts": [] 105 | } 106 | }, 107 | "deploy": { 108 | "builder": "@angular/fire:deploy", 109 | "options": {} 110 | } 111 | } 112 | } 113 | }, 114 | "defaultProject": "scouting" 115 | } 116 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist/scouting", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ] 9 | }, 10 | "firestore": { 11 | "rules": "firestore.rules", 12 | "indexes": "firestore.indexes.json" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [], 3 | "fieldOverrides": [] 4 | } 5 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | function isAdmin() { 5 | return request.auth != null && request.auth.uid != null && request.auth.uid in get(/databases/$(database)/documents/admin/admins).data.users; 6 | } 7 | 8 | function isTeamUser() { 9 | return request.auth != null && request.auth.uid != null; 10 | } 11 | 12 | match /admin/{document} { 13 | allow read: if isTeamUser(); 14 | allow write: if isAdmin(); 15 | } 16 | 17 | match /users/{document} { 18 | allow read: if true; 19 | allow write: if document == request.auth.uid; 20 | } 21 | 22 | match /games/{document} { 23 | allow read, write: if true; 24 | } 25 | 26 | match /{collection}/{document=**} { 27 | allow read, write: if (collection != "admin" && collection != "users" && collection != "games") && (isTeamUser() || isAdmin()); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: "", 7 | frameworks: ["jasmine", "@angular-devkit/build-angular"], 8 | plugins: [ 9 | require("karma-jasmine"), 10 | require("karma-chrome-launcher"), 11 | require("karma-jasmine-html-reporter"), 12 | require("karma-coverage"), 13 | require("@angular-devkit/build-angular/plugins/karma"), 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true, // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require("path").join(__dirname, "./coverage/Scouting"), 29 | subdir: ".", 30 | reporters: [{ type: "html" }, { type: "text-summary" }], 31 | }, 32 | reporters: ["progress", "kjhtml"], 33 | port: 9876, 34 | colors: true, 35 | logLevel: config.LOG_INFO, 36 | autoWatch: true, 37 | browsers: ["Chrome"], 38 | singleRun: false, 39 | restartOnFileChange: true, 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/manifest.webmanifest", 13 | "/*.css", 14 | "/*.js" 15 | ] 16 | } 17 | }, 18 | { 19 | "name": "assets", 20 | "installMode": "lazy", 21 | "updateMode": "prefetch", 22 | "resources": { 23 | "files": [ 24 | "/assets/**", 25 | "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)" 26 | ] 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scouting", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "deploy": "ng build --configuration production && firebase deploy --only hosting,firestore:rules", 10 | "test": "ng test", 11 | "firebase:environment": "node scripts/firebase.js", 12 | "create": "node scripts/create.js && npm run firebase:environment && npm run deploy" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "^13.1.1", 17 | "@angular/cdk": "^13.1.1", 18 | "@angular/common": "^13.1.1", 19 | "@angular/compiler": "^13.1.1", 20 | "@angular/core": "^13.1.1", 21 | "@angular/fire": "^7.2.0", 22 | "@angular/forms": "^13.1.1", 23 | "@angular/material": "^13.1.1", 24 | "@angular/platform-browser": "^13.1.1", 25 | "@angular/platform-browser-dynamic": "^13.1.1", 26 | "@angular/router": "^13.1.1", 27 | "@angular/service-worker": "^13.1.1", 28 | "firebase": "^9.6.1", 29 | "rxfire": "^6.0.3", 30 | "rxjs": "^7.4.0", 31 | "tslib": "^2.3.1", 32 | "zone.js": "^0.11.4" 33 | }, 34 | "devDependencies": { 35 | "@angular-devkit/build-angular": "^13.1.1", 36 | "@angular/cli": "^13.1.1", 37 | "@angular/compiler-cli": "^13.1.1", 38 | "@types/jasmine": "^3.10.2", 39 | "@types/node": "^17.0.2", 40 | "firebase-tools": "^10.0.1", 41 | "jasmine-core": "^3.10.1", 42 | "karma": "^6.3.9", 43 | "karma-chrome-launcher": "^3.1.0", 44 | "karma-coverage": "^2.1.0", 45 | "karma-jasmine": "^4.0.1", 46 | "karma-jasmine-html-reporter": "^1.7.0", 47 | "typescript": "^4.5.4" 48 | }, 49 | "prettier": { 50 | "semi": false, 51 | "singleQuote": false 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /scripts/create.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require("child_process") 2 | const { readFileSync, writeFileSync } = require("fs") 3 | const { join } = require("path") 4 | const { exit } = require("process") 5 | const { createInterface } = require("readline") 6 | 7 | const rl = createInterface({ 8 | input: process.stdin, 9 | output: process.stdout, 10 | }) 11 | const prompt = (query) => new Promise((resolve) => rl.question(query, resolve)) 12 | 13 | const main = async () => { 14 | const teamNumber = await prompt("Enter your team's number (e.g. 1574): ") 15 | 16 | const environment = join("src", "environments", "environment.ts") 17 | const productionEnvironment = join( 18 | "src", 19 | "environments", 20 | "environment.prod.ts" 21 | ) 22 | 23 | writeFileSync( 24 | environment, 25 | readFileSync(environment) 26 | .toString() 27 | .replace("team: 1574", `team: ${teamNumber}`) 28 | ) 29 | writeFileSync( 30 | productionEnvironment, 31 | readFileSync(productionEnvironment) 32 | .toString() 33 | .replace("team: 1574", `team: ${teamNumber}`) 34 | ) 35 | 36 | execSync("npm exec firebase login", { stdio: "inherit" }) 37 | 38 | const id = "scouting-" + Math.floor(Math.random() * 157415741574 + 1574) 39 | execSync(`npm exec -- firebase projects:create --display-name ${id} ${id}`, { 40 | stdio: "inherit", 41 | }) 42 | 43 | execSync(`npm exec firebase use ${id}`, { 44 | stdio: "inherit", 45 | }) 46 | 47 | execSync("npm exec firebase apps:create WEB Scouting", { 48 | stdio: "inherit", 49 | }) 50 | 51 | writeFileSync( 52 | ".firebaserc", 53 | `{ 54 | "projects": { 55 | "scouting": "${id}", 56 | "default": "${id}" 57 | }, 58 | "targets": { 59 | "${id}": { 60 | "hosting": { 61 | "scouting": [ 62 | "${id}" 63 | ] 64 | } 65 | } 66 | } 67 | }` 68 | ) 69 | 70 | console.log( 71 | `Almost done! Please enable Google Authentication in the Firebase Console: 72 | https://console.firebase.google.com/u/0/project/${id}/authentication` 73 | ) 74 | exit(0) 75 | } 76 | 77 | main() 78 | -------------------------------------------------------------------------------- /scripts/firebase.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require("child_process") 2 | const { writeFileSync } = require("fs") 3 | const { join } = require("path") 4 | 5 | const output = execSync("npm exec firebase apps:sdkconfig WEB", { 6 | stdio: "pipe", 7 | }).toString() 8 | const configuration = output.substring( 9 | output.indexOf("{"), 10 | output.indexOf("}") + 1 11 | ) 12 | 13 | writeFileSync(join("src", "environments", "firebase.json"), configuration) 14 | 15 | // https://console.firebase.google.com/u/0/project/scouting-76930636711/authentication 16 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { AdminPanelComponent } from "./pages/admin-panel/admin-panel.component" 2 | import { NgModule } from "@angular/core" 3 | import { RouterModule, Routes } from "@angular/router" 4 | import { LeaderboardComponent } from "./pages/fun-zone/leaderboard/leaderboard.component" 5 | import { FunZoneComponent } from "./pages/fun-zone/fun-zone.component" 6 | import { SimonComponent } from "./pages/fun-zone/simon/simon.component" 7 | import { FormComponent } from "./components/form/form.component" 8 | import { ScoutOverviewComponent } from "./pages/admin-panel/scout-overview/scout-overview.component" 9 | import { SchemaEditorComponent } from "./pages/admin-panel/schema-editor/schema-editor.component" 10 | import { AdminGuard } from "./services/admin.guard" 11 | import { TeamScoutsComponent } from "./pages/admin-panel/scout-overview/team-scouts/team-scouts.component" 12 | import { MatchesComponent } from "./pages/fun-zone/matches/matches.component" 13 | 14 | const routes: Routes = [ 15 | { path: "", redirectTo: "/form", pathMatch: "full" }, 16 | { path: "fun-zone", component: FunZoneComponent }, 17 | { path: "fun-zone/leaderboard", component: LeaderboardComponent }, 18 | { path: "fun-zone/simon", component: SimonComponent }, 19 | { 20 | path: "fun-zone/matches", 21 | component: MatchesComponent, 22 | }, 23 | { path: "form", component: FormComponent }, 24 | { 25 | path: "admin-panel", 26 | component: AdminPanelComponent, 27 | canActivate: [AdminGuard], 28 | }, 29 | { 30 | path: "admin-panel/scout-overview", 31 | component: ScoutOverviewComponent, 32 | canActivate: [AdminGuard], 33 | }, 34 | { 35 | path: "admin-panel/scout-overview/team", 36 | component: TeamScoutsComponent, 37 | canActivate: [AdminGuard], 38 | }, 39 | { 40 | path: "admin-panel/schema-editor", 41 | component: SchemaEditorComponent, 42 | canActivate: [AdminGuard], 43 | }, 44 | { 45 | path: "fun-zone/team-games", 46 | component: MatchesComponent, 47 | }, 48 | ] 49 | 50 | @NgModule({ 51 | imports: [RouterModule.forRoot(routes)], 52 | exports: [RouterModule], 53 | }) 54 | export class AppRoutingModule {} 55 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 14 | 15 | 18 | 19 | Scouting 20 |
21 | 24 | 31 |
32 |
33 | 34 |
35 |
36 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | flex-direction: column; 4 | max-height: 100%; 5 | background-color: white; 6 | 7 | @media (prefers-color-scheme: dark) { 8 | background-color: #333333; 9 | } 10 | 11 | .content { 12 | height: 100vh; 13 | padding: 20px; 14 | flex-grow: 1; 15 | overflow-y: auto; 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | } 20 | } 21 | 22 | .app-title { 23 | text-decoration: none; 24 | color: inherit; 25 | padding: 0 10px; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from "@angular/core/testing" 2 | import { RouterTestingModule } from "@angular/router/testing" 3 | import { AppComponent } from "./app.component" 4 | 5 | describe("AppComponent", () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [RouterTestingModule], 9 | declarations: [AppComponent], 10 | }).compileComponents() 11 | }) 12 | 13 | it("should create the app", () => { 14 | const fixture = TestBed.createComponent(AppComponent) 15 | const app = fixture.componentInstance 16 | expect(app).toBeTruthy() 17 | }) 18 | 19 | it(`should have as title 'Scouting'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent) 21 | const app = fixture.componentInstance 22 | expect(app.title).toEqual("Scouting") 23 | }) 24 | 25 | it("should render title", () => { 26 | const fixture = TestBed.createComponent(AppComponent) 27 | fixture.detectChanges() 28 | const compiled = fixture.nativeElement as HTMLElement 29 | expect(compiled.querySelector(".content span")?.textContent).toContain( 30 | "Scouting app is running!" 31 | ) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core" 2 | import { AuthenticationService } from "./services/authentication.service" 3 | 4 | @Component({ 5 | selector: "app-root", 6 | templateUrl: "./app.component.html", 7 | styleUrls: ["./app.component.scss"], 8 | }) 9 | export class AppComponent { 10 | title = "Scouting" 11 | 12 | constructor(public authentication: AuthenticationService) {} 13 | } 14 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core" 2 | import { initializeApp, provideFirebaseApp } from "@angular/fire/app" 3 | import { getAuth, provideAuth } from "@angular/fire/auth" 4 | import { 5 | enableIndexedDbPersistence, 6 | getFirestore, 7 | provideFirestore, 8 | } from "@angular/fire/firestore" 9 | import { FormsModule, ReactiveFormsModule } from "@angular/forms" 10 | import { MatButtonModule } from "@angular/material/button" 11 | import { MatCardModule } from "@angular/material/card" 12 | import { MatFormFieldModule } from "@angular/material/form-field" 13 | import { MatIconModule } from "@angular/material/icon" 14 | import { MatInputModule } from "@angular/material/input" 15 | import { MatCheckboxModule } from "@angular/material/checkbox" 16 | import { MatSelectModule } from "@angular/material/select" 17 | import { MatSlideToggleModule } from "@angular/material/slide-toggle" 18 | import { MatToolbarModule } from "@angular/material/toolbar" 19 | import { MatSnackBarModule } from "@angular/material/snack-bar" 20 | import { MatBadgeModule } from "@angular/material/badge" 21 | import { MatTableModule } from "@angular/material/table" 22 | import { BrowserModule } from "@angular/platform-browser" 23 | import { BrowserAnimationsModule } from "@angular/platform-browser/animations" 24 | import { ServiceWorkerModule } from "@angular/service-worker" 25 | import { environment } from "../environments/environment" 26 | import config from "../environments/firebase.json" 27 | import { AppRoutingModule } from "./app-routing.module" 28 | import { AppComponent } from "./app.component" 29 | import { CounterComponent } from "./components/form/counter/counter.component" 30 | import { FormComponent } from "./components/form/form.component" 31 | import { TextComponent } from "./components/form/text/text.component" 32 | import { TimerComponent } from "./components/form/timer/timer.component" 33 | import { ToggleComponent } from "./components/form/toggle/toggle.component" 34 | import { WidgetRowComponent } from "./components/form/widget-row/widget-row.component" 35 | import { SimonComponent } from "./pages/fun-zone/simon/simon.component" 36 | import { LongPressDirective } from "./directives/long-press.directive" 37 | import { LeaderboardComponent } from "./pages/fun-zone/leaderboard/leaderboard.component" 38 | import { FunZoneComponent } from "./pages/fun-zone/fun-zone.component" 39 | import { HttpClientModule } from "@angular/common/http" 40 | import { AdminPanelComponent } from "./pages/admin-panel/admin-panel.component" 41 | import { MatListModule } from "@angular/material/list" 42 | import { ScoutOverviewComponent } from "./pages/admin-panel/scout-overview/scout-overview.component" 43 | import { SchemaEditorComponent } from "./pages/admin-panel/schema-editor/schema-editor.component" 44 | import { TeamScoutsComponent } from "./pages/admin-panel/scout-overview/team-scouts/team-scouts.component" 45 | import { MatchesComponent } from "./pages/fun-zone/matches/matches.component" 46 | 47 | @NgModule({ 48 | declarations: [ 49 | AppComponent, 50 | FormComponent, 51 | CounterComponent, 52 | LongPressDirective, 53 | TextComponent, 54 | ToggleComponent, 55 | TimerComponent, 56 | WidgetRowComponent, 57 | SimonComponent, 58 | LeaderboardComponent, 59 | FunZoneComponent, 60 | AdminPanelComponent, 61 | ScoutOverviewComponent, 62 | SchemaEditorComponent, 63 | TeamScoutsComponent, 64 | MatchesComponent, 65 | ], 66 | imports: [ 67 | BrowserModule, 68 | ReactiveFormsModule, 69 | FormsModule, 70 | AppRoutingModule, 71 | HttpClientModule, 72 | provideFirebaseApp(() => initializeApp(config)), 73 | provideFirestore(() => { 74 | const firestore = getFirestore() 75 | enableIndexedDbPersistence(firestore) 76 | return firestore 77 | }), 78 | provideAuth(() => getAuth()), 79 | //provideAuth(getAuth), 80 | BrowserAnimationsModule, 81 | MatButtonModule, 82 | MatCardModule, 83 | MatIconModule, 84 | MatFormFieldModule, 85 | MatInputModule, 86 | MatSelectModule, 87 | MatSnackBarModule, 88 | MatSlideToggleModule, 89 | MatToolbarModule, 90 | MatCheckboxModule, 91 | MatTableModule, 92 | MatListModule, 93 | MatBadgeModule, 94 | ServiceWorkerModule.register("ngsw-worker.js", { 95 | enabled: environment.production, 96 | // Register the ServiceWorker as soon as the app is stable 97 | // or after 30 seconds (whichever comes first). 98 | registrationStrategy: "registerWhenStable:30000", 99 | }), 100 | ], 101 | providers: [], 102 | bootstrap: [AppComponent], 103 | }) 104 | export class AppModule {} 105 | -------------------------------------------------------------------------------- /src/app/components/form/counter/counter.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 6 | 9 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /src/app/components/form/counter/counter.component.scss: -------------------------------------------------------------------------------- 1 | .value { 2 | border-radius: 0; 3 | } 4 | 5 | .increment { 6 | border-radius: 0 50px 50px 0; 7 | } 8 | 9 | .decrement { 10 | border-radius: 50px 0 0 50px; 11 | } 12 | 13 | .increment, 14 | .decrement { 15 | min-width: 35px; 16 | max-width: 35px; 17 | padding: 0 8px; 18 | } 19 | 20 | .value { 21 | min-width: 50px; 22 | max-width: 50px; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/components/form/counter/counter.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from "@angular/core/testing" 2 | 3 | import { CounterComponent } from "./counter.component" 4 | 5 | describe("CounterComponent", () => { 6 | let component: CounterComponent 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [CounterComponent], 12 | }).compileComponents() 13 | }) 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(CounterComponent) 17 | component = fixture.componentInstance 18 | fixture.detectChanges() 19 | }) 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/app/components/form/counter/counter.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from "@angular/core" 2 | import { WidgetInfo } from "app/models/schema.model" 3 | import Widget from "app/utilities/widget" 4 | 5 | @Component({ 6 | selector: "app-counter", 7 | templateUrl: "./counter.component.html", 8 | styleUrls: ["./counter.component.scss"], 9 | }) 10 | export class CounterComponent extends Widget implements OnInit { 11 | @Input() 12 | widget?: WidgetInfo 13 | 14 | @Input() 15 | prefix?: string 16 | 17 | constructor() { 18 | super() 19 | } 20 | 21 | reset() { 22 | this.value = this.widget?.min ?? 0 23 | } 24 | 25 | increment() { 26 | const max = this.widget?.max 27 | if (this.value !== undefined && (max === undefined || this.value < max)) { 28 | this.value++ 29 | } 30 | } 31 | 32 | decrement() { 33 | const min = this.widget?.min 34 | if (this.value !== undefined && (min === undefined || this.value > min)) { 35 | this.value-- 36 | } 37 | } 38 | 39 | ngOnInit(): void { 40 | if (this.widget !== undefined && this.prefix !== undefined) { 41 | this.initialize(this.widget, this.prefix) 42 | this.value = this.widget.min ?? 0 43 | this.initial = this.value 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/components/form/form.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Form

3 | 4 | 5 | Scouter Name 6 | 7 | 8 | 9 | 10 | Game 11 |
12 | 13 | Stage 14 | 15 | Practice 16 | Qualifications 17 | Quarterfinals 18 | Semifinals 19 | Finals 20 | 21 | 22 | 23 | Game Number 24 | 25 | 26 | 27 | Team Number 28 | 29 | 30 |
31 |
32 |
33 | 41 |
42 |
43 | 51 |
52 |
53 |
54 | 57 |
58 |
59 |
60 | 61 | {{ section.title }} 62 |
63 |
64 | 65 | 66 | 67 | 68 |
69 |
70 |
71 | 72 | Submission 73 |
74 |

75 | Note: 76 | You cannot send your response until you are logged in! 77 |

78 |
79 | 80 | 81 | 82 | 85 | 86 |
87 |
88 |
89 |
90 | -------------------------------------------------------------------------------- /src/app/components/form/form.component.scss: -------------------------------------------------------------------------------- 1 | mat-card { 2 | margin-bottom: 20px; 3 | user-select: none; 4 | } 5 | 6 | .content { 7 | display: flex; 8 | flex-direction: column; 9 | padding: 10px 0; 10 | } 11 | 12 | @media (max-width: 400px) { 13 | .button-bar button { 14 | padding: 0 10px; 15 | min-width: 55px; 16 | } 17 | } 18 | 19 | .teamBottom { 20 | margin: 10px; 21 | margin-left: 30px; 22 | margin-right: 30px; 23 | border-radius: 1000px; 24 | } 25 | 26 | .blue { 27 | background-color: blue; 28 | } 29 | 30 | .red { 31 | background-color: red; 32 | } 33 | -------------------------------------------------------------------------------- /src/app/components/form/form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from "@angular/core/testing" 2 | 3 | import { FormComponent } from "./form.component" 4 | 5 | describe("FormComponent", () => { 6 | let component: FormComponent 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [FormComponent], 12 | }).compileComponents() 13 | }) 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(FormComponent) 17 | component = fixture.componentInstance 18 | fixture.detectChanges() 19 | }) 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/app/components/form/form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core" 2 | import { 3 | Firestore, 4 | doc, 5 | onSnapshot, 6 | setDoc, 7 | getDoc, 8 | } from "@angular/fire/firestore" 9 | import Schema from "app/models/schema.model" 10 | import { storagePrefix } from "app/utilities/widget" 11 | import { MatSnackBar } from "@angular/material/snack-bar" 12 | import { AuthenticationService } from "app/services/authentication.service" 13 | import { 14 | TBAEvents, 15 | TheBlueAllianceService, 16 | } from "app/services/the-blue-alliance.service" 17 | import { MatSelectChange } from "@angular/material/select" 18 | import { BackendService } from "app/services/backend.service" 19 | 20 | interface Scout { 21 | [keyof: string]: { 22 | [keyof: string]: any 23 | } 24 | } 25 | 26 | @Component({ 27 | selector: "app-form", 28 | templateUrl: "./form.component.html", 29 | styleUrls: ["./form.component.scss"], 30 | }) 31 | export class FormComponent implements OnInit { 32 | /** The schema of the scouting form */ 33 | schema: Schema 34 | /** The competition stage (qualifications, quarterfinals, etc.) the scout is about */ 35 | stage?: string 36 | /** The game number the scout is about */ 37 | game?: number 38 | /** The team number the scout is about */ 39 | team?: number 40 | 41 | blueTeams: number[] = [] 42 | redTeams: number[] = [] 43 | 44 | scouterName: string; 45 | 46 | /** The events your team is competing at */ 47 | events: TBAEvents = [] 48 | 49 | aouth: any 50 | 51 | constructor( 52 | private firestore: Firestore, 53 | private snack: MatSnackBar, 54 | public authentication: AuthenticationService, 55 | private tba: TheBlueAllianceService, 56 | public backend: BackendService 57 | ) { 58 | this.schema = { 59 | sections: [], 60 | } 61 | 62 | onSnapshot(doc(firestore, "admin/schema"), (snapshot) => { 63 | const schema = snapshot.data() as Schema 64 | this.schema = schema 65 | window.localStorage.setItem("Schema", JSON.stringify(schema)) 66 | this.clearWidgets(); 67 | }) 68 | 69 | this.events = this.tba 70 | .getEvents() 71 | .filter((event) => new Date(event.end_date).getFullYear() > 2019) 72 | 73 | console.log(authentication) 74 | 75 | this.scouterName = (localStorage.getItem("[Form] zScouter Name") ?? "") 76 | } 77 | 78 | /** Bindings */ 79 | 80 | onStageChange(event: MatSelectChange) { 81 | this.stage = event.value 82 | this.showTeams() 83 | } 84 | 85 | onGameChanged(event: KeyboardEvent): void { 86 | const value = (event.target as HTMLInputElement).value 87 | if (value !== "") { 88 | this.game = Number(value) 89 | this.showTeams() 90 | } 91 | } 92 | 93 | onTeamChanged(event: Event): void { 94 | this.team = Number((event.target as HTMLInputElement).value) 95 | } 96 | 97 | onScouterNameChange(event: Event): void { 98 | localStorage.setItem('[Form] zScouter Name', String((event.target as HTMLInputElement).value)) 99 | } 100 | 101 | /** Actions */ 102 | 103 | clear(): void { 104 | this.team = undefined 105 | this.game = undefined 106 | this.redTeams = [] 107 | this.blueTeams = [] 108 | window.dispatchEvent(new Event("formclear")) 109 | } 110 | 111 | get scout(): Scout { 112 | localStorage.setItem("[Form] zScouter Name", "\"" + localStorage.getItem("[Form] zScouter Name") + "\"") 113 | const result: Scout = {} 114 | const schema = JSON.parse(localStorage.getItem("Schema") ?? "") 115 | let keys = [] 116 | let sections = [] 117 | for (let section of schema["sections"]) { 118 | console.log(section) 119 | sections.push(section["prefix"]) 120 | for (let widget of section["widgets"]) { 121 | console.log(widget) 122 | keys.push(widget["key"]) 123 | } 124 | } 125 | 126 | for (let i = 0; i < localStorage.length; i++) { 127 | let fullKey = localStorage.key(i) 128 | if (!fullKey?.startsWith(storagePrefix)) { 129 | continue 130 | } 131 | 132 | console.log(fullKey) 133 | 134 | const value = JSON.parse(localStorage.getItem(fullKey)!) 135 | fullKey = fullKey.substring(storagePrefix.length) 136 | 137 | const prefix = fullKey.substring(0, fullKey.indexOf(" ")) 138 | 139 | console.log(prefix) 140 | 141 | if (!sections.includes(prefix) && prefix != "zScouter") { 142 | continue 143 | } 144 | 145 | if (!Object.keys(result).includes(prefix)) { 146 | result[prefix] = {} 147 | } 148 | 149 | const key = fullKey.substring(prefix.length + 1) 150 | if (keys.includes(key) || key == "Name") { 151 | result[prefix][key] = value 152 | } 153 | } 154 | 155 | localStorage.setItem("[Form] zScouter Name", (localStorage.getItem("[Form] zScouter Name") ?? "").replace("\"", "").replace("\"", "")) 156 | 157 | return result 158 | } 159 | 160 | copy(): void { 161 | navigator.clipboard.writeText(JSON.stringify(this.scout, null, 2)) 162 | } 163 | 164 | save(): void { 165 | const link = document.createElement("a") 166 | link.href = URL.createObjectURL( 167 | new Blob([JSON.stringify(this.scout, null, 4)], { 168 | type: "application/json", 169 | }) 170 | ) 171 | link.download = "submission.json" 172 | link.style.display = "none" 173 | document.body.appendChild(link) 174 | link.click() 175 | document.body.removeChild(link) 176 | } 177 | 178 | get filled() { 179 | return ( 180 | this.stage !== undefined && 181 | this.game !== undefined && 182 | this.team !== undefined 183 | ) 184 | } 185 | 186 | send(): void { 187 | if (!this.filled) { 188 | this.snack.open( 189 | "You must enter the stage, game and team number before submitting your scout", 190 | "Dismiss", 191 | { duration: 3000 } 192 | ) 193 | return 194 | } 195 | 196 | setDoc( 197 | doc( 198 | this.firestore, 199 | `${this.backend.event}/${this.stage} ${this.game} ${this.team}` 200 | ), 201 | this.scout, 202 | { 203 | merge: true, 204 | } 205 | ) 206 | .then(() => { 207 | this.snack.open("Submission Received", "Dismiss", { 208 | duration: 3000, 209 | }) 210 | }) 211 | .catch(() => { 212 | this.snack.open("Couldn't Send Submission", "Dismiss", { 213 | duration: 3000, 214 | }) 215 | }) 216 | } 217 | 218 | open(): void { 219 | const input = document.createElement("input") 220 | input.type = "file" 221 | input.style.display = "none" 222 | document.body.appendChild(input) 223 | input.click() 224 | input.onchange = () => { 225 | const file = input.files![0] 226 | const reader = new FileReader() 227 | reader.onload = () => { 228 | const data = JSON.parse(reader.result as string) 229 | this.set(data) 230 | } 231 | reader.readAsText(file) 232 | document.body.removeChild(input) 233 | } 234 | } 235 | 236 | showTeams() { 237 | this.redTeams = [] 238 | this.blueTeams = [] 239 | if ( 240 | this.backend.event === undefined || 241 | this.stage === undefined || 242 | this.game === undefined 243 | ) { 244 | return 245 | } 246 | 247 | const [redTeams, blueTeams] = this.tba.getTeams( 248 | this.backend.event, 249 | this.stage, 250 | this.game 251 | ) 252 | 253 | this.redTeams = redTeams 254 | this.blueTeams = blueTeams 255 | } 256 | 257 | async fetchScout(): Promise { 258 | const document = await getDoc( 259 | doc( 260 | this.firestore, 261 | `${this.backend.event}/${this.stage} ${this.game} ${this.team}` 262 | ) 263 | ) 264 | 265 | if (!document.exists()) { 266 | this.snack.open("Scout Not Available", "Dismiss", { duration: 3000 }) 267 | return 268 | } 269 | 270 | const data = document.data() 271 | this.set(data) 272 | } 273 | 274 | set(data: Scout): void { 275 | for (const prefix in data) { 276 | for (const key in data[prefix]) { 277 | const actualKey = storagePrefix + prefix + " " + key 278 | const newValue = JSON.stringify(data[prefix][key]) 279 | localStorage.setItem(actualKey, newValue) 280 | window.dispatchEvent( 281 | new StorageEvent("storage", { key: actualKey, newValue }) 282 | ) 283 | } 284 | } 285 | } 286 | 287 | ngOnInit(): void { 288 | this.schema = JSON.parse(localStorage.getItem("Schema") ?? "{}") 289 | } 290 | 291 | clearWidgets(): void { 292 | for (let i = 0; i < localStorage.length; i++) { 293 | let fullKey = localStorage.key(i) 294 | if (!fullKey?.startsWith(storagePrefix)) { 295 | continue 296 | } 297 | localStorage.removeItem(fullKey) 298 | } 299 | } 300 | } 301 | 302 | -------------------------------------------------------------------------------- /src/app/components/form/text/text.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{ 4 | widget.label 5 | }} 6 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /src/app/components/form/text/text.component.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 10px 0; 5 | width: 40.5em; 6 | height: auto; 7 | 8 | .widget-row:not(:last-child) { 9 | padding-bottom: 10px; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/components/form/text/text.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from "@angular/core/testing" 2 | 3 | import { TextComponent } from "./text.component" 4 | 5 | describe("TextComponent", () => { 6 | let component: TextComponent 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [TextComponent], 12 | }).compileComponents() 13 | }) 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(TextComponent) 17 | component = fixture.componentInstance 18 | fixture.detectChanges() 19 | }) 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/app/components/form/text/text.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from "@angular/core" 2 | import { WidgetInfo } from "app/models/schema.model" 3 | import Widget from "app/utilities/widget" 4 | 5 | @Component({ 6 | selector: "app-text", 7 | templateUrl: "./text.component.html", 8 | styleUrls: ["./text.component.scss"], 9 | }) 10 | export class TextComponent extends Widget implements OnInit { 11 | @Input() 12 | widget?: WidgetInfo 13 | 14 | @Input() 15 | prefix?: string 16 | 17 | @Input() 18 | numOfRows?: number 19 | 20 | constructor() { 21 | super() 22 | this.value = "" 23 | this.initial = this.value 24 | } 25 | 26 | changed(event: Event): void { 27 | this.value = (event.target as HTMLInputElement).value 28 | } 29 | 30 | ngOnInit(): void { 31 | if (this.widget !== undefined && this.prefix !== undefined) { 32 | this.initialize(this.widget, this.prefix) 33 | } 34 | } 35 | 36 | isRTL(x: string): boolean { 37 | return /[\u0590-\u05FF]/.test(x) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/components/form/timer/timer.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 6 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /src/app/components/form/timer/timer.component.scss: -------------------------------------------------------------------------------- 1 | button { 2 | border-radius: 50px; 3 | display: inline-flex; 4 | align-items: center; 5 | justify-content: center; 6 | } 7 | 8 | .timer { 9 | min-width: 85px; 10 | max-width: 85px; 11 | } 12 | 13 | .reset { 14 | min-width: 35px; 15 | max-width: 35px; 16 | padding: 0 8px; 17 | height: 36px; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/components/form/timer/timer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from "@angular/core/testing" 2 | 3 | import { TimerComponent } from "./timer.component" 4 | 5 | describe("TimerComponent", () => { 6 | let component: TimerComponent 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [TimerComponent], 12 | }).compileComponents() 13 | }) 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(TimerComponent) 17 | component = fixture.componentInstance 18 | fixture.detectChanges() 19 | }) 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/app/components/form/timer/timer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from "@angular/core" 2 | import { WidgetInfo } from "app/models/schema.model" 3 | import Widget from "app/utilities/widget" 4 | 5 | @Component({ 6 | selector: "app-timer", 7 | templateUrl: "./timer.component.html", 8 | styleUrls: ["./timer.component.scss"], 9 | }) 10 | export class TimerComponent extends Widget implements OnInit { 11 | @Input() 12 | widget?: WidgetInfo 13 | 14 | @Input() 15 | prefix?: string 16 | 17 | started?: number 18 | interval?: number 19 | offset = 0 20 | 21 | get text() { 22 | const value = this.value 23 | 24 | if (value === undefined) { 25 | return "00.00" 26 | } 27 | 28 | return `${Math.floor(value / 10)}${Math.floor(value % 10)}.${Math.floor( 29 | (value * 10) % 10 30 | )}${Math.floor((value * 100) % 10)}` 31 | } 32 | 33 | constructor() { 34 | super() 35 | this.value = 0 36 | } 37 | 38 | updateTimer() { 39 | if (this.started === undefined) { 40 | return 41 | } 42 | 43 | this.value = (Date.now() - this.started) / 1000 + this.offset 44 | } 45 | 46 | click() { 47 | if (this.started === undefined) { 48 | if (this.value === undefined) { 49 | this.value = 0 50 | } 51 | this.offset = this.value 52 | 53 | this.started = Date.now() 54 | this.interval = window.setInterval(() => this.updateTimer(), 50) 55 | } else { 56 | this.offset += (Date.now() - this.started) / 1000 57 | window.clearInterval(this.interval) 58 | this.started = undefined 59 | this.interval = undefined 60 | } 61 | } 62 | 63 | reset() { 64 | this.value = 0 65 | this.offset = 0 66 | } 67 | 68 | ngOnInit(): void { 69 | if (this.widget !== undefined && this.prefix !== undefined) { 70 | this.initialize(this.widget, this.prefix) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/app/components/form/toggle/toggle.component.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /src/app/components/form/toggle/toggle.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/app/components/form/toggle/toggle.component.scss -------------------------------------------------------------------------------- /src/app/components/form/toggle/toggle.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from "@angular/core/testing" 2 | 3 | import { ToggleComponent } from "./toggle.component" 4 | 5 | describe("ToggleComponent", () => { 6 | let component: ToggleComponent 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ToggleComponent], 12 | }).compileComponents() 13 | }) 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ToggleComponent) 17 | component = fixture.componentInstance 18 | fixture.detectChanges() 19 | }) 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/app/components/form/toggle/toggle.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from "@angular/core" 2 | import { MatSlideToggleChange } from "@angular/material/slide-toggle" 3 | import { WidgetInfo } from "app/models/schema.model" 4 | import Widget from "app/utilities/widget" 5 | 6 | @Component({ 7 | selector: "app-toggle", 8 | templateUrl: "./toggle.component.html", 9 | styleUrls: ["./toggle.component.scss"], 10 | }) 11 | export class ToggleComponent extends Widget implements OnInit { 12 | @Input() 13 | widget?: WidgetInfo 14 | 15 | @Input() 16 | prefix?: string 17 | 18 | constructor() { 19 | super() 20 | this.value = false 21 | this.initial = this.value 22 | } 23 | 24 | changed(event: MatSlideToggleChange) { 25 | this.value = event.checked 26 | } 27 | 28 | ngOnInit(): void { 29 | if (this.widget !== undefined && this.prefix !== undefined) { 30 | this.initialize(this.widget, this.prefix) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/components/form/widget-row/widget-row.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ label }} 4 |
5 |
6 | 7 |
8 | -------------------------------------------------------------------------------- /src/app/components/form/widget-row/widget-row.component.scss: -------------------------------------------------------------------------------- 1 | .widget-row { 2 | display: flex; 3 | align-items: center; 4 | height: 40px; 5 | margin-top: 10px; 6 | margin-bottom: 10px; 7 | } 8 | 9 | .label { 10 | margin-top: auto; 11 | margin-bottom: auto; 12 | } 13 | 14 | .spacer { 15 | flex-grow: 1; 16 | } 17 | 18 | .widget-row:not(:last-child) { 19 | padding-bottom: 10px; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/components/form/widget-row/widget-row.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from "@angular/core/testing" 2 | 3 | import { WidgetRowComponent } from "./widget-row.component" 4 | 5 | describe("WidgetRowComponent", () => { 6 | let component: WidgetRowComponent 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [WidgetRowComponent], 12 | }).compileComponents() 13 | }) 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(WidgetRowComponent) 17 | component = fixture.componentInstance 18 | fixture.detectChanges() 19 | }) 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/app/components/form/widget-row/widget-row.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from "@angular/core" 2 | 3 | @Component({ 4 | selector: "app-widget-row", 5 | templateUrl: "./widget-row.component.html", 6 | styleUrls: ["./widget-row.component.scss"], 7 | }) 8 | export class WidgetRowComponent implements OnInit { 9 | @Input() 10 | label?: string 11 | 12 | constructor() {} 13 | 14 | ngOnInit(): void {} 15 | } 16 | -------------------------------------------------------------------------------- /src/app/directives/long-press.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { LongPressDirective } from "./long-press.directive" 2 | 3 | describe("LongPressDirective", () => { 4 | it("should create an instance", () => { 5 | const directive = new LongPressDirective() 6 | expect(directive).toBeTruthy() 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /src/app/directives/long-press.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Output, EventEmitter, HostListener } from "@angular/core" 2 | 3 | @Directive({ 4 | selector: "button", 5 | }) 6 | export class LongPressDirective { 7 | timeout: any 8 | 9 | @Output() 10 | longclick = new EventEmitter() 11 | 12 | @HostListener("touchstart") 13 | @HostListener("mousedown") 14 | onMouseDown() { 15 | this.timeout = setTimeout(() => { 16 | this.longclick.emit() 17 | }, 1500) 18 | } 19 | 20 | @HostListener("touchend") 21 | @HostListener("mouseup") 22 | @HostListener("mouseleave") 23 | endPress() { 24 | clearTimeout(this.timeout) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/models/schema.model.ts: -------------------------------------------------------------------------------- 1 | export interface WidgetInfo { 2 | key: string 3 | label: string 4 | type: "Counter" | "Toggle" | "Timer" | "Text" 5 | min?: number 6 | max?: number 7 | rows?: number 8 | } 9 | 10 | export interface Section { 11 | prefix: string 12 | title: string 13 | subtitle?: string 14 | widgets: WidgetInfo[] 15 | } 16 | 17 | export default interface Schema { 18 | sections: Section[] 19 | } 20 | -------------------------------------------------------------------------------- /src/app/models/scout.model.ts: -------------------------------------------------------------------------------- 1 | import { stage } from "./stage.model" 2 | 3 | export interface Scout { 4 | level: stage 5 | match: number 6 | team: number 7 | [key: string]: any 8 | } 9 | -------------------------------------------------------------------------------- /src/app/models/stage.model.ts: -------------------------------------------------------------------------------- 1 | export type stage = "pr" | "qm" | "qf" | "sf" | "f" 2 | 3 | export const display = (stage: stage) => { 4 | if (stage == "pr") { 5 | return "Practice" 6 | } 7 | if (stage == "qm") { 8 | return "Qualifications" 9 | } 10 | if (stage == "qf") { 11 | return "Quarterfinals" 12 | } 13 | if (stage == "sf") { 14 | return "Semifinals" 15 | } 16 | if (stage == "f") { 17 | return "Finals" 18 | } 19 | return "" 20 | } 21 | 22 | export const order = ["pr", "qm", "qf", "sf", "f"] 23 | -------------------------------------------------------------------------------- /src/app/models/user.model.ts: -------------------------------------------------------------------------------- 1 | export default interface User { 2 | uid: string 3 | name: string 4 | photo: string 5 | } 6 | -------------------------------------------------------------------------------- /src/app/pages/admin-panel/admin-panel.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Admin Panel

3 | 6 |
7 | 10 |
11 | 12 | Event 13 | 14 | {{ 15 | event.year + " " + event.name 16 | }} 17 | 18 | 19 |
20 | 21 | 22 | 23 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
Admin 24 | 26 | 27 | Name{{ element.name }}
38 |
-------------------------------------------------------------------------------- /src/app/pages/admin-panel/admin-panel.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/app/pages/admin-panel/admin-panel.component.scss -------------------------------------------------------------------------------- /src/app/pages/admin-panel/admin-panel.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from "@angular/core/testing" 2 | 3 | import { AdminPanelComponent } from "./admin-panel.component" 4 | 5 | describe("AdminPanelComponent", () => { 6 | let component: AdminPanelComponent 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [AdminPanelComponent], 12 | }).compileComponents() 13 | }) 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(AdminPanelComponent) 17 | component = fixture.componentInstance 18 | fixture.detectChanges() 19 | }) 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/app/pages/admin-panel/admin-panel.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core" 2 | import { 3 | collection, 4 | Firestore, 5 | onSnapshot, 6 | doc, 7 | setDoc, 8 | } from "@angular/fire/firestore" 9 | import User from "app/models/user.model" 10 | import { BackendService } from "app/services/backend.service" 11 | import { TheBlueAllianceService } from "app/services/the-blue-alliance.service" 12 | 13 | @Component({ 14 | selector: "app-admin-panel", 15 | templateUrl: "./admin-panel.component.html", 16 | styleUrls: ["./admin-panel.component.scss"], 17 | }) 18 | export class AdminPanelComponent implements OnInit { 19 | users: User[] = [] 20 | admins: string[] = [] 21 | columns = ["selection", "name"] 22 | 23 | constructor( 24 | private firestore: Firestore, 25 | public backend: BackendService, 26 | public tba: TheBlueAllianceService 27 | ) { } 28 | 29 | ngOnInit(): void { 30 | onSnapshot(doc(this.firestore, "admin/admins"), (snapshot) => { 31 | this.admins = snapshot.data()?.users ?? [] 32 | }) 33 | 34 | //console.log(this.backend.event) 35 | 36 | onSnapshot(collection(this.firestore, "users"), (snapshot) => { 37 | this.users = snapshot.docs.map((document) => { 38 | const data = document.data() 39 | return { uid: document.ref.id, name: data.name, photo: data.photo } 40 | }) 41 | }) 42 | } 43 | 44 | updateAdmin(uid: string, admin: boolean) { 45 | if (admin) { 46 | this.admins.push(uid) 47 | } else { 48 | this.admins.splice(this.admins.indexOf(uid), 1) 49 | } 50 | setDoc(doc(this.firestore, "admin/admins"), { users: this.admins }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/pages/admin-panel/schema-editor/schema-editor.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Schema Editor

3 |
4 | 5 |
6 | 9 | 16 | 17 | Prefix 18 | 24 | 25 | 26 | Title 27 | 33 | 34 | 41 |
42 | 43 |
44 | 45 |
49 | 55 | 61 | 62 | 63 | Key 64 | 70 | 71 | 72 | Label 73 | 79 | 80 | 81 | 82 | Type 83 | 87 | Timer 88 | Counter 89 | Toggle 90 | Text 91 | 92 | 93 | 94 | 101 |
102 | 103 | 111 |
112 |
113 | 114 | 115 | 118 | 119 | 120 | 128 |
129 | -------------------------------------------------------------------------------- /src/app/pages/admin-panel/schema-editor/schema-editor.component.scss: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | margin: 10px; 6 | margin-bottom: 20px; 7 | overflow-x: auto; 8 | 9 | * { 10 | flex: none; 11 | } 12 | 13 | :first-child { 14 | margin-left: auto; 15 | } 16 | 17 | :last-child { 18 | margin-right: auto; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/pages/admin-panel/schema-editor/schema-editor.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from "@angular/core/testing" 2 | 3 | import { SchemaEditorComponent } from "./schema-editor.component" 4 | 5 | describe("SchemaEditorComponent", () => { 6 | let component: SchemaEditorComponent 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [SchemaEditorComponent], 12 | }).compileComponents() 13 | }) 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(SchemaEditorComponent) 17 | component = fixture.componentInstance 18 | fixture.detectChanges() 19 | }) 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/app/pages/admin-panel/schema-editor/schema-editor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core" 2 | import { doc, Firestore, getDoc, setDoc } from "@angular/fire/firestore" 3 | import Schema, { Section } from "app/models/schema.model" 4 | 5 | @Component({ 6 | selector: "app-schema-editor", 7 | templateUrl: "./schema-editor.component.html", 8 | styleUrls: ["./schema-editor.component.scss"], 9 | }) 10 | export class SchemaEditorComponent implements OnInit { 11 | sections: Section[] = [] 12 | schema: Schema = { sections: [] } 13 | 14 | showSchemaEdit: boolean = false 15 | showMangeScouts: boolean = false 16 | 17 | constructor(private firestore: Firestore) {} 18 | 19 | ngOnInit(): void { 20 | this.getCurrentData() 21 | } 22 | 23 | async getCurrentData() { 24 | let schema = await getDoc(doc(this.firestore, "admin/schema")) 25 | let data = schema.data() 26 | for (let section of data?.sections) { 27 | this.sections.push(section) 28 | } 29 | } 30 | 31 | addSection() { 32 | this.sections.push({ title: "", prefix: "", widgets: [] }) 33 | } 34 | 35 | addWidget(section: Section) { 36 | section.widgets.push({ key: "", label: "", type: "Counter" }) 37 | } 38 | 39 | removeWidget(widgetIndex: number, sectionIndex: number) { 40 | this.sections[sectionIndex].widgets.splice(widgetIndex, 1) 41 | } 42 | 43 | removeSection(sectionIndex: number) { 44 | this.sections.splice(sectionIndex, 1) 45 | } 46 | 47 | eventToString(event: Event): string { 48 | return (event.target as HTMLInputElement).value 49 | } 50 | 51 | eventToNumber(event: Event): number { 52 | return Number((event.target as HTMLInputElement).value) 53 | } 54 | 55 | async update() { 56 | this.schema = { sections: this.sections } as Schema 57 | await setDoc(doc(this.firestore, "admin/schema"), this.schema) 58 | } 59 | 60 | moveWidgetUp(widgetIndex: number, sectionIndex: number) { 61 | if (widgetIndex == 0) return 62 | 63 | const widget = this.sections[sectionIndex].widgets.splice(widgetIndex, 1) 64 | this.sections[sectionIndex].widgets.splice(widgetIndex - 1, 0, widget[0]) 65 | } 66 | 67 | moveWidgetDown(widgetIndex: number, sectionIndex: number) { 68 | if (this.sections[sectionIndex].widgets.length == widgetIndex + 1) return 69 | const widget = this.sections[sectionIndex].widgets.splice(widgetIndex, 1) 70 | this.sections[sectionIndex].widgets.splice(widgetIndex + 1, 0, widget[0]) 71 | } 72 | 73 | moveSectionUp(sectionIndex: number) { 74 | if (sectionIndex == 0) return 75 | const section = this.sections.splice(sectionIndex, 1) 76 | this.sections.splice(sectionIndex - 1, 0, section[0]) 77 | } 78 | 79 | moveSectionDown(sectionIndex: number) { 80 | if (sectionIndex == this.sections.length) return 81 | const section = this.sections.splice(sectionIndex, 1) 82 | this.sections.splice(sectionIndex + 1, 0, section[0]) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app/pages/admin-panel/scout-overview/scout-overview.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Scout Overview

3 | 6 | 10 | 11 | {{ 12 | team 13 | }} 14 | 15 | 16 | 23 | 24 |
25 |

Invalid / Missing Scouts

26 | 27 | Stage 28 | 29 | Practice 30 | Qualifications 31 | Quarterfinals 32 | Semifinals 33 | Finals 34 | 35 | 36 |
37 | 38 | 46 | 47 | 50 | 53 | 54 | 55 | 56 | 59 | 62 | 63 | 64 | 65 | 66 |
48 | Missing Scouts 49 | 51 | {{ element.missing }} 52 | 57 | Incorrect Scouts 58 | 60 | {{ element.incorrect }} 61 |
67 | -------------------------------------------------------------------------------- /src/app/pages/admin-panel/scout-overview/scout-overview.component.scss: -------------------------------------------------------------------------------- 1 | .missing { 2 | text-align: center; 3 | margin: 5px; 4 | margin-right: 10rem; 5 | } 6 | 7 | .topPart { 8 | margin-bottom: 20px; 9 | display: flex; 10 | flex-direction: column; 11 | text-align: center; 12 | } 13 | 14 | .teams { 15 | display: flex; 16 | flex-direction: row; 17 | margin-bottom: auto; 18 | } 19 | .incorrect { 20 | text-align: center; 21 | margin: 5px; 22 | margin-left: 10rem; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/pages/admin-panel/scout-overview/scout-overview.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from "@angular/core/testing" 2 | 3 | import { ScoutOverviewComponent } from "./scout-overview.component" 4 | 5 | describe("ScoutOverviewComponent", () => { 6 | let component: ScoutOverviewComponent 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ScoutOverviewComponent], 12 | }).compileComponents() 13 | }) 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ScoutOverviewComponent) 17 | component = fixture.componentInstance 18 | fixture.detectChanges() 19 | }) 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/app/pages/admin-panel/scout-overview/scout-overview.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core" 2 | import { collection, Firestore, getDocs } from "@angular/fire/firestore" 3 | import { MatSnackBar } from "@angular/material/snack-bar" 4 | import { display, stage } from "app/models/stage.model" 5 | import { BackendService } from "app/services/backend.service" 6 | import { 7 | TBAEvents, 8 | TheBlueAllianceService, 9 | } from "app/services/the-blue-alliance.service" 10 | 11 | interface TableRow { 12 | missing: string 13 | incorrect: string 14 | } 15 | 16 | @Component({ 17 | selector: "app-scout-overview", 18 | templateUrl: "./scout-overview.component.html", 19 | styleUrls: ["./scout-overview.component.scss"], 20 | }) 21 | export class ScoutOverviewComponent implements OnInit { 22 | events: TBAEvents = [] 23 | stage?: stage 24 | missingTeams: string[] = [] 25 | incorrectTeams: string[] = [] 26 | showMissingTeams: boolean = true 27 | displayedColumns: string[] = ["missingScouts", "incorrectScouts"] 28 | daraSource: TableRow[] = [] 29 | showTable = false 30 | allTeamsWithScouts: string[] = [] 31 | team: string = "" 32 | 33 | constructor( 34 | private tba: TheBlueAllianceService, 35 | private firestore: Firestore, 36 | private backend: BackendService, 37 | private snack: MatSnackBar 38 | ) { 39 | this.updateAllTeams() 40 | } 41 | 42 | ngOnInit(): void {} 43 | 44 | getTeams(game: number) { 45 | if (this.stage === undefined) { 46 | return [] 47 | } 48 | return this.tba.getTeams(this.backend.event, this.stage, game) 49 | } 50 | 51 | async getScoutsNames(): Promise { 52 | if (this.stage === undefined) { 53 | return [] 54 | } 55 | 56 | const document = await getDocs( 57 | collection(this.firestore, this.backend.event) 58 | ) 59 | const data = document.docs 60 | let names: any[] = [] 61 | for (let name of data) { 62 | if (this.stage == "f") { 63 | if (name.id.split("")[0] == this.stage) names.push(name.id) 64 | } else { 65 | if (name.id.includes(this.stage)) names.push(name.id) 66 | } 67 | } 68 | return names 69 | } 70 | 71 | async findMissingScouts() { 72 | if (this.stage === undefined) { 73 | return 74 | } 75 | 76 | this.missingTeams = [] 77 | this.incorrectTeams = [] 78 | const scouts = await this.getScoutsNames() 79 | let splitedScouts = [] 80 | for (let scout of scouts) { 81 | splitedScouts.push(scout.split(" ")) 82 | } 83 | let gameNumbers = [] 84 | for (let number of splitedScouts) { 85 | gameNumbers.push(Number(number[1])) 86 | } 87 | 88 | const lastGame = this.findMaxGameNum(gameNumbers) 89 | for (let i = 1; i < lastGame + 1; i++) { 90 | let specificTeams: number[] = [] 91 | for (let scout of splitedScouts) { 92 | if (Number(scout[1]) === i) { 93 | specificTeams.push(Number(scout[2])) 94 | } 95 | } 96 | 97 | const [redTeams, blueTeams] = await this.getTeams(i) 98 | const needTeams = [...redTeams, ...blueTeams] 99 | 100 | for (let team of needTeams) { 101 | if (!specificTeams.includes(team)) { 102 | this.missingTeams.push( 103 | display(this.stage) + " " + i + ": Team #" + team 104 | ) 105 | } 106 | } 107 | 108 | for (let team of specificTeams) { 109 | if (!needTeams.includes(team)) { 110 | this.incorrectTeams.push( 111 | display(this.stage) + " " + i + ": Team #" + team 112 | ) 113 | } 114 | } 115 | } 116 | this.showMissingTeams = true 117 | 118 | this.updateDataSource() 119 | } 120 | 121 | findMaxGameNum(gameNumbers: number[]): number { 122 | return Math.max(...gameNumbers) 123 | } 124 | 125 | updateDataSource() { 126 | this.showTable = false 127 | this.daraSource = [] 128 | for ( 129 | let i = 0; 130 | i < Math.max(this.incorrectTeams.length, this.missingTeams.length); 131 | i++ 132 | ) { 133 | let missing = "" 134 | let incorrect = "" 135 | if (this.missingTeams[i] !== undefined) { 136 | missing = this.missingTeams[i] 137 | } 138 | 139 | if (this.incorrectTeams[i] !== undefined) { 140 | incorrect = this.incorrectTeams[i] 141 | } 142 | this.daraSource.push({ missing: missing, incorrect: incorrect }) 143 | } 144 | this.showTable = true 145 | } 146 | 147 | fetchAllScouts() { 148 | getDocs(collection(this.firestore, this.backend.event)) 149 | .then((result) => { 150 | const scouts = result.docs.map((doc) => { 151 | const [level, match, team] = doc.id.split(" ") 152 | return { 153 | level, 154 | match: Number(match), 155 | team: Number(team), 156 | ...doc.data(), 157 | } 158 | }) 159 | window.localStorage.setItem("Scouts", JSON.stringify(scouts)) 160 | this.snack.open("Scouts Fetched Succesfully", "Dismiss", { 161 | duration: 3000, 162 | }) 163 | }) 164 | .catch(() => { 165 | this.snack.open("Couldn't Fetch Scouts", "Dismiss", { 166 | duration: 3000, 167 | }) 168 | }) 169 | } 170 | 171 | updateAllTeams() { 172 | const scouts = JSON.parse(window.localStorage.getItem("Scouts") ?? "[]") 173 | let teams: string[] = [] 174 | for (const scout of scouts) { 175 | if (!teams.includes(scout.team)) { 176 | teams.push(scout.team) 177 | } 178 | } 179 | this.allTeamsWithScouts = teams.sort() 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/app/pages/admin-panel/scout-overview/team-scouts/team-scouts.component.html: -------------------------------------------------------------------------------- 1 |

Team #{{ team }} Scouts

2 | 3 | 7 | 10 | 13 | 14 | 15 | 16 | 17 |
8 | {{ property }} 9 | 11 | {{ scout[property] }} 12 |
18 | -------------------------------------------------------------------------------- /src/app/pages/admin-panel/scout-overview/team-scouts/team-scouts.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/app/pages/admin-panel/scout-overview/team-scouts/team-scouts.component.scss -------------------------------------------------------------------------------- /src/app/pages/admin-panel/scout-overview/team-scouts/team-scouts.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from "@angular/core/testing" 2 | 3 | import { TeamScoutsComponent } from "./team-scouts.component" 4 | 5 | describe("TeamScoutsComponent", () => { 6 | let component: TeamScoutsComponent 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [TeamScoutsComponent], 12 | }).compileComponents() 13 | }) 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(TeamScoutsComponent) 17 | component = fixture.componentInstance 18 | fixture.detectChanges() 19 | }) 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/app/pages/admin-panel/scout-overview/team-scouts/team-scouts.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core" 2 | import { ActivatedRoute } from "@angular/router" 3 | import { Scout } from "app/models/scout.model" 4 | 5 | @Component({ 6 | selector: "app-team-scouts", 7 | templateUrl: "./team-scouts.component.html", 8 | styleUrls: ["./team-scouts.component.scss"], 9 | }) 10 | export class TeamScoutsComponent implements OnInit { 11 | team?: number 12 | scouts: any[] = [] 13 | 14 | constructor(route: ActivatedRoute) { 15 | route.queryParams.subscribe((params) => this.update(Number(params.number))) 16 | } 17 | 18 | ngOnInit(): void {} 19 | 20 | update(team: number) { 21 | this.team = team 22 | const scouts = JSON.parse( 23 | window.localStorage.getItem("Scouts") ?? "[]" 24 | ) as Scout[] 25 | this.scouts = scouts 26 | .filter((scout) => scout.team === team) 27 | .map((scout) => { 28 | const result: any = { 29 | Level: scout.level, 30 | Match: scout.match, 31 | } 32 | for (const category in scout) { 33 | if ( 34 | category === "level" || 35 | category === "match" || 36 | category === "team" 37 | ) { 38 | continue 39 | } 40 | for (const key in scout[category]) { 41 | result[category + " " + key] = scout[category][key] 42 | } 43 | } 44 | return result 45 | }) 46 | } 47 | 48 | get displayedColumns() { 49 | return Object.keys(this.scouts[0] ?? {}) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/pages/fun-zone/fun-zone.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Fun Zone

3 | 6 |
7 | 10 |
11 | 14 |
15 | -------------------------------------------------------------------------------- /src/app/pages/fun-zone/fun-zone.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/app/pages/fun-zone/fun-zone.component.scss -------------------------------------------------------------------------------- /src/app/pages/fun-zone/fun-zone.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from "@angular/core/testing" 2 | 3 | import { FunZoneComponent } from "./fun-zone.component" 4 | 5 | describe("FunZoneComponent", () => { 6 | let component: FunZoneComponent 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [FunZoneComponent], 12 | }).compileComponents() 13 | }) 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(FunZoneComponent) 17 | component = fixture.componentInstance 18 | fixture.detectChanges() 19 | }) 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/app/pages/fun-zone/fun-zone.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core" 2 | 3 | @Component({ 4 | selector: "app-fun-zone", 5 | templateUrl: "./fun-zone.component.html", 6 | styleUrls: ["./fun-zone.component.scss"], 7 | }) 8 | export class FunZoneComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit(): void {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/pages/fun-zone/leaderboard/leaderboard.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Leaderboard

3 | 4 |

Simon

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
Position{{ element.position }}Name{{ element.name }}Score{{ element.score }}
24 |
25 | -------------------------------------------------------------------------------- /src/app/pages/fun-zone/leaderboard/leaderboard.component.scss: -------------------------------------------------------------------------------- 1 | table td { 2 | padding: 20px; 3 | } 4 | 5 | table th { 6 | padding: 20px; 7 | } 8 | 9 | .update { 10 | flex-direction: row; 11 | margin-bottom: 25px; 12 | } 13 | 14 | th.mat-header-cell { 15 | text-align: center !important; 16 | } 17 | 18 | td, 19 | th { 20 | text-align: left !important; 21 | vertical-align: left !important; 22 | } 23 | 24 | .mat-sort-header-container { 25 | justify-content: left !important; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/pages/fun-zone/leaderboard/leaderboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from "@angular/core/testing" 2 | 3 | import { LeaderboardComponent } from "./leaderboard.component" 4 | 5 | describe("leaderbordComponent", () => { 6 | let component: LeaderboardComponent 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [LeaderboardComponent], 12 | }).compileComponents() 13 | }) 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(LeaderboardComponent) 17 | component = fixture.componentInstance 18 | fixture.detectChanges() 19 | }) 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/app/pages/fun-zone/leaderboard/leaderboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Firestore, doc, getDoc, onSnapshot } from "@angular/fire/firestore" 2 | import { Component, OnInit, ViewChild } from "@angular/core" 3 | import { MatTable } from "@angular/material/table" 4 | 5 | export interface RowElement { 6 | position: number 7 | name: string 8 | score: number 9 | } 10 | 11 | const ELEMENT_DATA: RowElement[] = [] 12 | 13 | @Component({ 14 | selector: "app-leaderboard", 15 | templateUrl: "./leaderboard.component.html", 16 | styleUrls: ["./leaderboard.component.scss"], 17 | }) 18 | export class LeaderboardComponent implements OnInit { 19 | dataSource = [...ELEMENT_DATA] 20 | highScores = new Map() 21 | names: [string] 22 | displayedColumns: string[] = ["position", "name", "score"] 23 | constructor(private firestore: Firestore) { 24 | this.names = [""] 25 | } 26 | 27 | @ViewChild(MatTable) table!: MatTable 28 | 29 | ngOnInit(): void {} 30 | async getHighScoresArray(): Promise> { 31 | const document = await getDoc(doc(this.firestore, "games/simon")) 32 | const data = document.data() 33 | for (let name in data) { 34 | let score = String(data[name].score) 35 | this.highScores.set(name, score) 36 | } 37 | 38 | return new Map([...this.highScores].sort((a, b) => b[1] - a[1])) 39 | } 40 | 41 | updateHighScores = onSnapshot(doc(this.firestore, "games/simon"), () => { 42 | this.updateTable() 43 | }) 44 | 45 | async updateTable() { 46 | this.dataSource = [...ELEMENT_DATA] 47 | let names = Array.from((await this.getHighScoresArray()).keys()) 48 | let scores = Array.from((await this.getHighScoresArray()).values()) 49 | for (let i = 1; i < Math.min(names.length + 1, 11); i++) { 50 | let row: RowElement = { 51 | position: i, 52 | name: names[i - 1], 53 | score: scores[i - 1], 54 | } 55 | this.dataSource.push(row) 56 | } 57 | 58 | this.table.renderRows() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/app/pages/fun-zone/matches/matches.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Matches

3 | 4 | 15 | {{ getMatchName(match) }} 16 |
17 | 25 | 33 | 41 |
42 | 43 |
44 | 51 | 58 | 65 |
66 | 67 |
78 |
79 |
80 | -------------------------------------------------------------------------------- /src/app/pages/fun-zone/matches/matches.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/app/pages/fun-zone/matches/matches.component.scss -------------------------------------------------------------------------------- /src/app/pages/fun-zone/matches/matches.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MatchesComponent } from './matches.component'; 4 | 5 | describe('MatchesComponent', () => { 6 | let component: MatchesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ MatchesComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MatchesComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/fun-zone/matches/matches.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core" 2 | import { display } from "app/models/stage.model" 3 | import { 4 | TBASimpleMatch, 5 | TheBlueAllianceService, 6 | } from "app/services/the-blue-alliance.service" 7 | import { environment } from "environments/environment" 8 | 9 | @Component({ 10 | selector: "app-matches", 11 | templateUrl: "./matches.component.html", 12 | styleUrls: ["./matches.component.scss"], 13 | }) 14 | export class MatchesComponent implements OnInit { 15 | matches: any[] = [] 16 | team = environment.team 17 | 18 | constructor(private tba: TheBlueAllianceService) {} 19 | 20 | async ngOnInit(): Promise { 21 | this.matches = this.tba 22 | .getMatches() 23 | .filter( 24 | (match) => 25 | match.alliances.red.team_keys.includes("frc" + this.team) || 26 | match.alliances.blue.team_keys.includes("frc" + this.team) 27 | ) 28 | } 29 | 30 | getMatchName(match: TBASimpleMatch) { 31 | const name = String(match.key.split("_")[1]) 32 | const [stage, number] = this.tba.splitMatch(name) 33 | return display(stage) + " " + number 34 | } 35 | 36 | getTeamRanking(team_key: string) { 37 | for (let rank of this.tba.getRankings()) { 38 | if (rank.team_key === team_key) { 39 | return rank.rank 40 | } 41 | } 42 | console.error("Should not get here, couldn't find a rank for " + team_key) 43 | return 0 44 | } 45 | 46 | getTeamAlliance(match: TBASimpleMatch) { 47 | return match.alliances.red.team_keys.includes("frc" + this.team) 48 | ? "red" 49 | : "blue" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/pages/fun-zone/simon/simon.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Simon

3 |
4 |

Score: {{ score }}

5 |

High Score: {{ highScore }}

6 | 9 |
10 |
11 |
12 |
17 |
22 |
23 | 24 |
25 |
30 |
35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /src/app/pages/fun-zone/simon/simon.component.scss: -------------------------------------------------------------------------------- 1 | .panel { 2 | width: 150px; 3 | height: 150px; 4 | display: inline-block; 5 | cursor: pointer; 6 | border-color: transparent; 7 | transition: 100ms ease-in-out; 8 | } 9 | 10 | .row { 11 | display: flex; 12 | justify-content: center; 13 | } 14 | 15 | #root { 16 | text-align: center; 17 | } 18 | 19 | .panel:hover { 20 | filter: brightness(60%); 21 | } 22 | 23 | .bottom-right-panel { 24 | border-bottom-right-radius: 90%; 25 | background-color: red; 26 | } 27 | 28 | .bottom-left-panel { 29 | border-bottom-left-radius: 90%; 30 | background-color: blue; 31 | } 32 | 33 | .top-right-panel { 34 | border-top-right-radius: 90%; 35 | background-color: green; 36 | } 37 | 38 | .top-left-panel { 39 | border-top-left-radius: 90%; 40 | background-color: yellow; 41 | } 42 | 43 | .active { 44 | background-color: white; 45 | } 46 | -------------------------------------------------------------------------------- /src/app/pages/fun-zone/simon/simon.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from "@angular/core/testing" 2 | 3 | import { SimonComponent } from "./simon.component" 4 | 5 | describe("SimonComponent", () => { 6 | let component: SimonComponent 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [SimonComponent], 12 | }).compileComponents() 13 | }) 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(SimonComponent) 17 | component = fixture.componentInstance 18 | fixture.detectChanges() 19 | }) 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/app/pages/fun-zone/simon/simon.component.ts: -------------------------------------------------------------------------------- 1 | import { MatSnackBar } from "@angular/material/snack-bar" 2 | import { Component, ElementRef, ViewChild } from "@angular/core" 3 | import { Firestore, doc, setDoc, getDoc } from "@angular/fire/firestore" 4 | import { AuthenticationService } from "app/services/authentication.service" 5 | 6 | @Component({ 7 | selector: "app-simon", 8 | templateUrl: "./simon.component.html", 9 | styleUrls: ["./simon.component.scss"], 10 | }) 11 | export class SimonComponent { 12 | @ViewChild("topLeft") topLeft!: ElementRef 13 | @ViewChild("bottomLeft") bottomLeft!: ElementRef 14 | @ViewChild("bottomRight") bottomRight!: ElementRef 15 | @ViewChild("topRight") topRight!: ElementRef 16 | 17 | canClick: boolean 18 | sequence: ElementRef[] 19 | sequenceToGuess: ElementRef[] 20 | score: number 21 | highScore: number 22 | timeBetweenFlashs: number 23 | lastPanelGuessed?: ElementRef 24 | 25 | constructor( 26 | private snack: MatSnackBar, 27 | private firestore: Firestore, 28 | private authentication: AuthenticationService 29 | ) { 30 | this.canClick = false 31 | this.sequence = [] 32 | this.sequenceToGuess = [] 33 | this.score = 0 34 | this.timeBetweenFlashs = 800 35 | this.highScore = 0 36 | } 37 | 38 | async start() { 39 | this.timeBetweenFlashs = 800 40 | this.score = 0 41 | this.sequence = [this.getRandomPanel()] 42 | this.sequenceToGuess = [...this.sequence] 43 | 44 | await this.runSequence() 45 | } 46 | 47 | async runSequence() { 48 | this.canClick = false 49 | for (const panel of this.sequence) { 50 | await this.flash(panel) 51 | } 52 | this.canClick = true 53 | } 54 | 55 | async panelClicked(panel: string) { 56 | if (!this.canClick) return 57 | 58 | const expectedPanel = this.sequenceToGuess.shift() 59 | if (expectedPanel?.nativeElement.classList.contains(panel)) { 60 | this.playSound(panel) 61 | 62 | if (this.sequenceToGuess.length === 0) { 63 | this.sequence.push(this.getRandomPanel()) 64 | this.sequenceToGuess = [...this.sequence] 65 | this.score++ 66 | if (this.score > this.highScore) { 67 | this.highScore = this.score 68 | } 69 | this.canClick = false 70 | await new Promise((resolve) => setTimeout(resolve, 1000)) 71 | this.runSequence() 72 | } 73 | } else { 74 | this.playSound("wrong") 75 | this.canClick = false 76 | this.updateHighScore( 77 | this.highScore, 78 | this.authentication.user?.displayName 79 | ) 80 | this.score = 0 81 | this.snack.open("You Guessed the Wrong Panel! Game Over", "Dismiss", { 82 | duration: 3000, 83 | }) 84 | } 85 | } 86 | 87 | getRandomPanel() { 88 | const panels = [ 89 | this.topLeft, 90 | this.topRight, 91 | this.bottomLeft, 92 | this.bottomRight, 93 | ] 94 | let panel = panels[Math.floor(Math.random() * panels.length)] 95 | if (panel === this.lastPanelGuessed) { 96 | panel = panels[Math.floor(Math.random() * panels.length)] 97 | } 98 | this.lastPanelGuessed = panel 99 | return panel 100 | } 101 | 102 | async flash(panel: ElementRef) { 103 | if (this.timeBetweenFlashs > 130) { 104 | this.timeBetweenFlashs -= 30 105 | } 106 | 107 | this.playSound(panel.nativeElement.classList[0]) 108 | 109 | panel.nativeElement.classList.add("active") 110 | await new Promise((resolve) => setTimeout(resolve, this.timeBetweenFlashs)) 111 | panel.nativeElement.classList.remove("active") 112 | 113 | await new Promise((resolve) => setTimeout(resolve, 250)) 114 | } 115 | 116 | async updateHighScore(highScore: number, name?: string | null) { 117 | if (name === undefined || name === null) name = "" 118 | if (highScore > (await this.getCurrentHighScore(name))) { 119 | setDoc( 120 | doc(this.firestore, "games/simon"), 121 | { [name]: { score: highScore } }, 122 | { merge: true } 123 | ) 124 | } 125 | } 126 | 127 | async getCurrentHighScore(targetName: string): Promise { 128 | let highScore = 0 129 | const document = await getDoc(doc(this.firestore, "games/simon")) 130 | const data = document.data() 131 | for (let name in data) { 132 | if (name === targetName) { 133 | let score = data[name].score 134 | highScore = score 135 | } 136 | } 137 | return highScore 138 | } 139 | 140 | playSound(color: string) { 141 | let audio = new Audio() 142 | if (color == "top-right-panel") { 143 | audio.src = "assets/sounds/simonSound1.mp3" 144 | } 145 | if (color == "top-left-panel") { 146 | audio.src = "assets/sounds/simonSound2.mp3" 147 | } 148 | if (color == "bottom-right-panel") { 149 | audio.src = "assets/sounds/simonSound3.mp3" 150 | } 151 | if (color == "bottom-left-panel") { 152 | audio.src = "assets/sounds/simonSound4.mp3" 153 | } 154 | if (color == "wrong") { 155 | audio.src = "assets/sounds/error.mp3" 156 | } 157 | audio.load() 158 | audio.play() 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/app/services/admin.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from "@angular/core/testing" 2 | 3 | import { AdminGuard } from "./admin.guard" 4 | 5 | describe("AdminGuard", () => { 6 | let guard: AdminGuard 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}) 10 | guard = TestBed.inject(AdminGuard) 11 | }) 12 | 13 | it("should be created", () => { 14 | expect(guard).toBeTruthy() 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/app/services/admin.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core" 2 | import { MatSnackBar } from "@angular/material/snack-bar" 3 | import { CanActivate, UrlTree } from "@angular/router" 4 | import { Observable } from "rxjs" 5 | import { AuthenticationService } from "./authentication.service" 6 | 7 | @Injectable({ 8 | providedIn: "root", 9 | }) 10 | export class AdminGuard implements CanActivate { 11 | constructor( 12 | private authentication: AuthenticationService, 13 | private snack: MatSnackBar 14 | ) {} 15 | 16 | canActivate(): 17 | | Observable 18 | | Promise 19 | | boolean 20 | | UrlTree { 21 | if (!this.authentication.isAdmin) { 22 | this.snack.open("You aren't an admin!", "Dismiss", { duration: 3000 }) 23 | return false 24 | } 25 | return true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/services/authentication.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from "@angular/core/testing" 2 | 3 | import { AuthenticationService } from "./authentication.service" 4 | 5 | describe("AuthenticationService", () => { 6 | let service: AuthenticationService 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}) 10 | service = TestBed.inject(AuthenticationService) 11 | }) 12 | 13 | it("should be created", () => { 14 | expect(service).toBeTruthy() 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/app/services/authentication.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core" 2 | import { 3 | onAuthStateChanged, 4 | signInWithPopup, 5 | signOut, 6 | GoogleAuthProvider, 7 | Auth, 8 | } from "@angular/fire/auth" 9 | import { 10 | doc, 11 | getDoc, 12 | onSnapshot, 13 | setDoc, 14 | Firestore, 15 | } from "@angular/fire/firestore" 16 | import { User } from "@angular/fire/auth" 17 | import { MatSnackBar } from "@angular/material/snack-bar" 18 | 19 | @Injectable({ 20 | providedIn: "root", 21 | }) 22 | export class AuthenticationService { 23 | public user?: User 24 | public isAdmin = false 25 | 26 | constructor( 27 | private firestore: Firestore, 28 | private authentication: Auth, 29 | private snack: MatSnackBar 30 | ) { 31 | onAuthStateChanged(this.authentication, async (user) => { 32 | this.user = user ?? undefined 33 | 34 | if (this.user !== undefined) { 35 | const document = doc(this.firestore, `users/${this.user.uid}`) 36 | if (!(await (await getDoc(document)).exists())) { 37 | setDoc( 38 | document, 39 | { 40 | name: this.user.displayName, 41 | photo: this.user.photoURL, 42 | }, 43 | { merge: true } 44 | ) 45 | } 46 | } 47 | 48 | const data = (await getDoc(doc(this.firestore, "admin/admins"))).data() 49 | if (data === undefined) { 50 | this.isAdmin = false 51 | return 52 | } 53 | 54 | this.isAdmin = data.users.includes(this.user?.uid) 55 | 56 | localStorage.setItem("[Form] zScouter Name", this.user?.displayName ?? "") 57 | }) 58 | 59 | onSnapshot(doc(this.firestore, "admin/admins"), (snapshot) => { 60 | const data = snapshot.data() 61 | if (data === undefined) { 62 | this.isAdmin = false 63 | return 64 | } 65 | 66 | this.isAdmin = data.users.includes(this.user?.uid) 67 | }) 68 | } 69 | 70 | get signedIn(): boolean { 71 | return this.user !== undefined 72 | } 73 | 74 | signIn(): void { 75 | signInWithPopup(this.authentication, new GoogleAuthProvider()).catch( 76 | (error) => { 77 | this.snack.open("Could not log in because of a " + error, "Dismiss", { 78 | duration: 3000, 79 | }) 80 | } 81 | ) 82 | } 83 | 84 | signOut(): void { 85 | if (window.confirm("Are you sure you want to sign out?")) { 86 | signOut(this.authentication) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/app/services/backend.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from "@angular/core/testing" 2 | 3 | import { BackendService } from "./backend.service" 4 | 5 | describe("BackendService", () => { 6 | let service: BackendService 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}) 10 | service = TestBed.inject(BackendService) 11 | }) 12 | 13 | it("should be created", () => { 14 | expect(service).toBeTruthy() 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/app/services/backend.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core" 2 | import { Firestore, doc, onSnapshot, setDoc } from "@angular/fire/firestore" 3 | 4 | @Injectable({ 5 | providedIn: "root", 6 | }) 7 | export class BackendService { 8 | private _event = "" 9 | 10 | constructor(private firestore: Firestore) { 11 | this._event = localStorage.getItem("Event Key") ?? "" 12 | 13 | onSnapshot(doc(this.firestore, "/admin/event"), (snapshot) => { 14 | this._event = snapshot.data()?.key ?? "" 15 | localStorage.setItem("Event Key", this._event) 16 | }) 17 | } 18 | 19 | get event() { 20 | return this._event 21 | } 22 | 23 | set event(newEvent: string) { 24 | setDoc(doc(this.firestore, "/admin/event"), { key: newEvent }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/services/the-blue-alliance.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from "@angular/core/testing" 2 | 3 | import { TheBlueAllianceService } from "./the-blue-alliance.service" 4 | 5 | describe("TheBlueAllianceService", () => { 6 | let service: TheBlueAllianceService 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}) 10 | service = TestBed.inject(TheBlueAllianceService) 11 | }) 12 | 13 | it("should be created", () => { 14 | expect(service).toBeTruthy() 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/app/services/the-blue-alliance.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from "@angular/common/http" 2 | import { Injectable } from "@angular/core" 3 | import { environment } from "environments/environment" 4 | import Secrets from "environments/Secrets.json" 5 | import { BackendService } from "./backend.service" 6 | import { firstValueFrom } from "rxjs" 7 | import { MatSnackBar } from "@angular/material/snack-bar" 8 | import { order, stage } from "app/models/stage.model" 9 | 10 | export interface TBAEvent { 11 | address: string 12 | city: string 13 | country: string 14 | name: string 15 | end_date: string 16 | short_name: string 17 | key: string 18 | year: number 19 | playoff_type: number | null 20 | } 21 | 22 | export type TBAEvents = TBAEvent[] 23 | 24 | export interface TBASimpleMatch { 25 | key: string 26 | comp_level: stage 27 | match_number: number 28 | set_number: number 29 | event_key: string 30 | predicted_time: number 31 | alliances: { 32 | red: { 33 | team_keys: string[] 34 | surrogate_team_keys: string[] 35 | dq_team_keys: string[] 36 | } 37 | blue: { 38 | team_keys: string[] 39 | surrogate_team_keys: string[] 40 | dq_team_keys: string[] 41 | } 42 | } 43 | winning_alliance?: string 44 | } 45 | 46 | export interface TBARanking { 47 | team_key: string 48 | rank: number 49 | } 50 | 51 | @Injectable({ 52 | providedIn: "root", 53 | }) 54 | export class TheBlueAllianceService { 55 | QUARTERFINALS = [ 56 | "1m1", 57 | "2m1", 58 | "3m1", 59 | "4m1", 60 | "1m2", 61 | "2m2", 62 | "3m2", 63 | "4m2", 64 | "1m3", 65 | "2m3", 66 | "3m3", 67 | "4m3", 68 | ] 69 | QUARTERFINALS_ORDER = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4] 70 | SEMIFINALS = ["1m1", "2m1", "1m2", "2m2", "1m3", "2m3"] 71 | SEMIFINALS_ORDER = [1, 2, 1, 2, 1, 2] 72 | FINALS = ["1m1", "1m2", "1m3"] 73 | FINALS_ORDER = [1, 1, 1] 74 | 75 | private setNumber(match: number, stage: string): number { 76 | if (stage === "qf") { 77 | return this.QUARTERFINALS_ORDER[match - 1] 78 | } else if (stage === "sf") { 79 | return this.SEMIFINALS_ORDER[match - 1] 80 | } else if (stage === "f") { 81 | return this.FINALS_ORDER[match - 1] 82 | } else { 83 | return match 84 | } 85 | } 86 | 87 | private headers = { 88 | "X-TBA-Auth-Key": Secrets.TBAKey, 89 | } 90 | 91 | private url = "https://www.thebluealliance.com/api/v3" 92 | private events: TBAEvents = [] 93 | private matches: TBASimpleMatch[] = [] 94 | private rankings: TBARanking[] = [] 95 | 96 | constructor( 97 | private http: HttpClient, 98 | private backend: BackendService, 99 | private snack: MatSnackBar 100 | ) { 101 | this.events = JSON.parse(localStorage.getItem("Events") ?? "[]") 102 | this.matches = JSON.parse(localStorage.getItem("Matches") ?? "[]") 103 | this.rankings = JSON.parse(localStorage.getItem("Rankings") ?? "[]") 104 | 105 | const checkTBAKey = (reason: any) => { 106 | if (reason.error.Error.includes("X-TBA-Auth-Key")) { 107 | this.snack.open("Your TBA Read Key is Invalid", "Dismiss", { 108 | duration: 3000, 109 | }) 110 | } 111 | } 112 | 113 | firstValueFrom( 114 | this.http.get( 115 | this.url + `/team/frc${environment.team}/events`, 116 | { 117 | headers: this.headers, 118 | } 119 | ) 120 | ) 121 | .then((events) => { 122 | this.events = events 123 | localStorage.setItem("Events", JSON.stringify(events)) 124 | }) 125 | .catch(checkTBAKey) 126 | 127 | firstValueFrom( 128 | this.http.get( 129 | this.url + `/event/${this.backend.event}/matches/simple`, 130 | { 131 | headers: this.headers, 132 | } 133 | ) 134 | ) 135 | .then((matches) => { 136 | this.matches = matches.sort((a, b) => { 137 | if (a.comp_level === b.comp_level) { 138 | return a.match_number - b.match_number 139 | } 140 | return order.indexOf(a.comp_level) - order.indexOf(b.comp_level) 141 | }) 142 | localStorage.setItem("Matches", JSON.stringify(this.matches)) 143 | }) 144 | .catch(checkTBAKey) 145 | 146 | firstValueFrom( 147 | this.http.get<{ rankings: TBARanking[] }>( 148 | this.url + `/event/${this.backend.event}/rankings`, 149 | { 150 | headers: this.headers, 151 | } 152 | ) 153 | ).then((response) => { 154 | this.rankings = response.rankings 155 | localStorage.setItem("Rankings", JSON.stringify(this.rankings)) 156 | }) 157 | } 158 | 159 | getEvents(): TBAEvents { 160 | return this.events 161 | } 162 | 163 | getTeams(event: string, stage: string, game: number): number[][] { 164 | let match: TBASimpleMatch | undefined 165 | if (stage == "qm" || stage == "pr") { 166 | match = this.matches.find( 167 | (match) => 168 | match.event_key === event && 169 | match.comp_level === stage && 170 | match.match_number === game 171 | ) 172 | } else { 173 | const setNumber = this.setNumber(game, stage) 174 | match = this.matches.find( 175 | (match) => 176 | match.event_key === event && 177 | match.comp_level === stage && 178 | match.set_number === setNumber 179 | ) 180 | } 181 | if (match === undefined) { 182 | return [] 183 | } 184 | 185 | return [match.alliances.red.team_keys, match.alliances.blue.team_keys].map( 186 | (teams) => teams.map((team) => Number(team.substring(3))) 187 | ) 188 | } 189 | 190 | getRankings() { 191 | return this.rankings 192 | } 193 | 194 | getMatches() { 195 | return this.matches 196 | } 197 | 198 | removeFRCPrefix(team_key: string) { 199 | return Number(team_key.substring(3)) 200 | } 201 | 202 | splitMatch(match: string): [stage, number] { 203 | console.log(match) 204 | if (match[0] === "p") { 205 | return ["pr", Number(match.substring(2))] 206 | } else if (match[0] === "f") { 207 | return ["f", this.FINALS.indexOf(match.substring(1)) + 1] 208 | } else if (match[0] === "s") { 209 | return ["sf", this.SEMIFINALS.indexOf(match.substring(2)) + 1] 210 | } else if (match[1] === "m") { 211 | return ["qm", Number(match.substring(2))] 212 | } else { 213 | return ["qf", this.QUARTERFINALS.indexOf(match.substring(2)) + 1] 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/app/utilities/widget.ts: -------------------------------------------------------------------------------- 1 | import { WidgetInfo } from "app/models/schema.model" 2 | 3 | export const storagePrefix = "[Form] " 4 | 5 | export default abstract class Widget { 6 | private key?: string 7 | private _initial?: T 8 | private _value?: T 9 | 10 | public get value(): T | undefined { 11 | return this._value 12 | } 13 | 14 | protected set value(value: T | undefined) { 15 | this._value = value 16 | if (this.key !== undefined) { 17 | localStorage.setItem(this.key, JSON.stringify(value)) 18 | } 19 | } 20 | 21 | public get initial(): T | undefined { 22 | return this._initial 23 | } 24 | 25 | public set initial(value: T | undefined) { 26 | this._initial = value 27 | } 28 | 29 | private update(value: string | null) { 30 | if (value !== null) { 31 | this.value = JSON.parse(value) 32 | } 33 | } 34 | 35 | protected initialize(info: WidgetInfo, prefix: string) { 36 | this.key = storagePrefix + prefix + " " + info.key 37 | const current = localStorage.getItem(this.key) 38 | if (current !== null) { 39 | this.update(current) 40 | } else { 41 | localStorage.setItem(this.key, JSON.stringify(this.value)) 42 | } 43 | 44 | window.addEventListener("storage", ({ key, newValue }) => { 45 | if (this.key !== undefined && key === this.key) { 46 | this.update(newValue) 47 | } 48 | }) 49 | 50 | window.addEventListener("formclear", () => { 51 | this.value = this.initial 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /src/assets/icons/icon-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/assets/icons/icon-ios.png -------------------------------------------------------------------------------- /src/assets/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/assets/icons/icon.png -------------------------------------------------------------------------------- /src/assets/sounds/error.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/assets/sounds/error.mp3 -------------------------------------------------------------------------------- /src/assets/sounds/simonSound1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/assets/sounds/simonSound1.mp3 -------------------------------------------------------------------------------- /src/assets/sounds/simonSound2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/assets/sounds/simonSound2.mp3 -------------------------------------------------------------------------------- /src/assets/sounds/simonSound3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/assets/sounds/simonSound3.mp3 -------------------------------------------------------------------------------- /src/assets/sounds/simonSound4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/assets/sounds/simonSound4.mp3 -------------------------------------------------------------------------------- /src/environments/Secrets.json: -------------------------------------------------------------------------------- 1 | { 2 | "TBAKey": "hmUpNjFJiInnrq0r8l4uQ34pUjA4PPPH2dR6IGTulR5iB48twtwrACAXzTr2PAWx" 3 | } -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | team: 1574, 4 | } 5 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | team: 1574, 8 | } 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisCar/Scouting/161bb361fcd69c9743201dc68961b5d79a61dbb0/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scouting 6 | 10 | 11 | 15 | 16 | 17 | 18 | 22 | 26 | 27 | 28 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from "@angular/core" 2 | import { platformBrowserDynamic } from "@angular/platform-browser-dynamic" 3 | 4 | import { AppModule } from "./app/app.module" 5 | import { environment } from "./environments/environment" 6 | 7 | if (environment.production) { 8 | enableProdMode() 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch((err) => console.error(err)) 14 | -------------------------------------------------------------------------------- /src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Scouting", 3 | "short_name": "Scouting", 4 | "theme_color": "#7e0c2b", 5 | "background_color": "#fafafa", 6 | "display": "standalone", 7 | "scope": "./", 8 | "start_url": "./", 9 | "icons": [ 10 | { 11 | "src": "assets/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png", 14 | "purpose": "maskable any" 15 | }, 16 | { 17 | "src": "assets/icons/icon-96x96.png", 18 | "sizes": "96x96", 19 | "type": "image/png", 20 | "purpose": "maskable any" 21 | }, 22 | { 23 | "src": "assets/icons/icon-128x128.png", 24 | "sizes": "128x128", 25 | "type": "image/png", 26 | "purpose": "maskable any" 27 | }, 28 | { 29 | "src": "assets/icons/icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image/png", 32 | "purpose": "maskable any" 33 | }, 34 | { 35 | "src": "assets/icons/icon-152x152.png", 36 | "sizes": "152x152", 37 | "type": "image/png", 38 | "purpose": "maskable any" 39 | }, 40 | { 41 | "src": "assets/icons/icon-192x192.png", 42 | "sizes": "192x192", 43 | "type": "image/png", 44 | "purpose": "maskable any" 45 | }, 46 | { 47 | "src": "assets/icons/icon-384x384.png", 48 | "sizes": "384x384", 49 | "type": "image/png", 50 | "purpose": "maskable any" 51 | }, 52 | { 53 | "src": "assets/icons/icon-512x512.png", 54 | "sizes": "512x512", 55 | "type": "image/png", 56 | "purpose": "maskable any" 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * IE11 requires the following for NgClass support on SVG elements 23 | */ 24 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 25 | 26 | /** 27 | * Web Animations `@angular/platform-browser/animations` 28 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 29 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 30 | */ 31 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 32 | 33 | /** 34 | * By default, zone.js will patch all possible macroTask and DomEvents 35 | * user can disable parts of macroTask/DomEvents patch by setting following flags 36 | * because those flags need to be set before `zone.js` being loaded, and webpack 37 | * will put import in the top of bundle, so user need to create a separate file 38 | * in this directory (for example: zone-flags.ts), and put the following flags 39 | * into that file, and then add the following code before importing zone.js. 40 | * import './zone-flags'; 41 | * 42 | * The flags allowed in zone-flags.ts are listed here. 43 | * 44 | * The following flags will work for all browsers. 45 | * 46 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 47 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 48 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 49 | * 50 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 51 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 52 | * 53 | * (window as any).__Zone_enable_cross_context_check = true; 54 | * 55 | */ 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import "zone.js" // Included with Angular CLI. 61 | 62 | /*************************************************************************************************** 63 | * APPLICATION IMPORTS 64 | */ 65 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | // Custom Theming for Angular materialerial 2 | // For more informaterialion: https://materialerial.angular.io/guide/theming 3 | @use "@angular/material" as material; 4 | // Plus imports for other components in your app. 5 | 6 | // Include the common styles for Angular materialerial. We include this here so that you only 7 | // have to load a single css file for Angular materialerial in your app. 8 | // Be sure that you only ever include this mixin once! 9 | @include material.core(); 10 | 11 | // Define the palettes for your theme using the materialerial Design palettes available in palette.scss 12 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 13 | // hue. Available color palettes: https://materialerial.io/design/color/ 14 | 15 | $scouting-primary-scheme: ( 16 | 100: #fae4bc, 17 | 500: #eda41e, 18 | 700: #e58912, 19 | contrast: ( 20 | 100: rgba(0, 0, 0, 0.87), 21 | 500: rgba(0, 0, 0, 0.87), 22 | 700: #ffffff, 23 | ), 24 | ); 25 | $scouting-accent-scheme: ( 26 | 100: #61061a, 27 | 500: #7e0c2b, 28 | 700: #d8b6bf, 29 | contrast: ( 30 | 100: rgba(0, 0, 0, 0.87), 31 | 500: #ffffff, 32 | 700: #ffffff, 33 | ), 34 | ); 35 | 36 | $scouting-primary: material.define-palette($scouting-primary-scheme); 37 | $scouting-accent: material.define-palette($scouting-accent-scheme); 38 | 39 | // The warn palette is optional (defaults to red). 40 | $scouting-warn: material.define-palette(material.$red-palette); 41 | 42 | // Create the theme object. A theme consists of configurations for individual 43 | // theming systems such as "color" or "typography". 44 | $scouting-light-theme: material.define-light-theme( 45 | ( 46 | color: ( 47 | primary: $scouting-primary, 48 | accent: $scouting-accent, 49 | warn: $scouting-warn, 50 | ), 51 | ) 52 | ); 53 | 54 | $scouting-dark-theme: material.define-dark-theme( 55 | ( 56 | color: ( 57 | primary: $scouting-primary, 58 | accent: $scouting-accent, 59 | warn: $scouting-warn, 60 | ), 61 | ) 62 | ); 63 | 64 | // Include theme styles for core and each component used in your app. 65 | // Alternatively, you can import and @include the theme mixins for each component 66 | // that you are using. 67 | @include material.all-component-themes($scouting-light-theme); 68 | @media (prefers-color-scheme: dark) { 69 | @include material.all-component-colors($scouting-dark-theme); 70 | 71 | body { 72 | background-color: #333; 73 | } 74 | } 75 | 76 | /* You can add global styles to this file, and also import other style files */ 77 | 78 | html, 79 | body { 80 | position: fixed; 81 | height: 100%; 82 | width: 100%; 83 | } 84 | 85 | body { 86 | margin: 0; 87 | font-family: Roboto, "Helvetica Neue", sans-serif; 88 | background-color: #f5f5f5; 89 | 90 | @media (prefers-color-scheme: dark) { 91 | background-color: #212121; 92 | color: white; 93 | } 94 | } 95 | 96 | .button-bar { 97 | height: 36px; 98 | display: flex; 99 | 100 | button:not(:first-child) { 101 | border-top-left-radius: 0; 102 | border-bottom-left-radius: 0; 103 | border-left-width: 0; 104 | } 105 | button:not(:last-child) { 106 | border-top-right-radius: 0; 107 | border-bottom-right-radius: 0; 108 | } 109 | 110 | button:disabled { 111 | border-color: rgba(0, 0, 0, 0.12); 112 | 113 | @media (prefers-color-scheme: dark) { 114 | border-color: rgba(255, 255, 255, 0.12); 115 | } 116 | } 117 | } 118 | 119 | input::-webkit-outer-spin-button, 120 | input::-webkit-inner-spin-button { 121 | -webkit-appearance: none; 122 | margin: 0; 123 | } 124 | 125 | *::-webkit-scrollbar { 126 | display: none; 127 | } 128 | 129 | input[type="number"] { 130 | -moz-appearance: textfield; 131 | } 132 | 133 | mat-form-field { 134 | max-width: 100%; 135 | } 136 | 137 | mat-toolbar { 138 | min-height: 56px; 139 | max-height: 56px; 140 | } 141 | 142 | .mat-form-field-wrapper { 143 | padding: 0; 144 | } 145 | 146 | .mat-form-field-underline { 147 | bottom: 0; 148 | } 149 | 150 | .container { 151 | width: 70vw; 152 | 153 | @media (max-width: 600px) { 154 | width: 90vw; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import "zone.js/testing" 4 | import { getTestBed } from "@angular/core/testing" 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting, 8 | } from "@angular/platform-browser-dynamic/testing" 9 | 10 | declare const require: { 11 | context( 12 | path: string, 13 | deep?: boolean, 14 | filter?: RegExp 15 | ): { 16 | keys(): string[] 17 | (id: string): T 18 | } 19 | } 20 | 21 | // First, initialize the Angular testing environment. 22 | getTestBed().initTestEnvironment( 23 | BrowserDynamicTestingModule, 24 | platformBrowserDynamicTesting(), 25 | { teardown: { destroyAfterEach: true } } 26 | ) 27 | 28 | // Then we find all the tests. 29 | const context = require.context("./", true, /\.spec\.ts$/) 30 | // And load the modules. 31 | context.keys().map(context) 32 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./src", 6 | "resolveJsonModule": true, 7 | "esModuleInterop": true, 8 | "outDir": "./dist/out-tsc", 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "es2017", 20 | "module": "es2020", 21 | "lib": [ 22 | "es2018", 23 | "dom" 24 | ], 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": true 27 | }, 28 | "angularCompilerOptions": { 29 | "enableI18nLegacyMessageIdFormat": false, 30 | "strictInjectionParameters": true, 31 | "strictInputAccessModifiers": true, 32 | "strictTemplates": false 33 | } 34 | } -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": ["jasmine"] 7 | }, 8 | "files": ["src/test.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 10 | } 11 | --------------------------------------------------------------------------------