Add Pit Scouting Tab
Signed-off-by: Emily Markova <emily.markova@gmail.com>
Change-Id: Iede446546e20f2915bb53e134050b5025976da36
diff --git a/scouting/www/BUILD b/scouting/www/BUILD
index 6ecad54..55e9a8f 100644
--- a/scouting/www/BUILD
+++ b/scouting/www/BUILD
@@ -19,6 +19,7 @@
"//scouting/www/entry",
"//scouting/www/match_list",
"//scouting/www/notes",
+ "//scouting/www/pit_scouting",
"//scouting/www/shift_schedule",
"//scouting/www/view",
],
diff --git a/scouting/www/app/app.module.ts b/scouting/www/app/app.module.ts
index 7b200d0..bf393e4 100644
--- a/scouting/www/app/app.module.ts
+++ b/scouting/www/app/app.module.ts
@@ -9,6 +9,7 @@
import {ShiftScheduleModule} from '../shift_schedule';
import {ViewModule} from '../view';
import {DriverRankingModule} from '../driver_ranking';
+import {PitScoutingModule} from '../pit_scouting';
@NgModule({
declarations: [App],
@@ -21,6 +22,7 @@
ShiftScheduleModule,
DriverRankingModule,
ViewModule,
+ PitScoutingModule,
],
exports: [App],
bootstrap: [App],
diff --git a/scouting/www/app/app.ng.html b/scouting/www/app/app.ng.html
index b7f1873..5f7ea9f 100644
--- a/scouting/www/app/app.ng.html
+++ b/scouting/www/app/app.ng.html
@@ -64,6 +64,15 @@
View
</a>
</li>
+ <li class="nav-item">
+ <a
+ class="nav-link"
+ [class.active]="tabIs('Pit')"
+ (click)="switchTabToGuarded('Pit')"
+ >
+ Pit
+ </a>
+ </li>
</ul>
<ng-container [ngSwitch]="tab">
@@ -83,4 +92,5 @@
<app-driver-ranking *ngSwitchCase="'DriverRanking'"></app-driver-ranking>
<shift-schedule *ngSwitchCase="'ShiftSchedule'"></shift-schedule>
<app-view *ngSwitchCase="'View'"></app-view>
+ <app-pit-scouting *ngSwitchCase="'Pit'"></app-pit-scouting>
</ng-container>
diff --git a/scouting/www/app/app.ts b/scouting/www/app/app.ts
index c19895a..16e3197 100644
--- a/scouting/www/app/app.ts
+++ b/scouting/www/app/app.ts
@@ -6,7 +6,8 @@
| 'Entry'
| 'DriverRanking'
| 'ShiftSchedule'
- | 'View';
+ | 'View'
+ | 'Pit';
// Ignore the guard for tabs that don't require the user to enter any data.
const unguardedTabs: Tab[] = ['MatchList'];
diff --git a/scouting/www/pit_scouting/BUILD b/scouting/www/pit_scouting/BUILD
new file mode 100644
index 0000000..740dee1
--- /dev/null
+++ b/scouting/www/pit_scouting/BUILD
@@ -0,0 +1,18 @@
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("//tools/build_rules:js.bzl", "ng_pkg")
+
+npm_link_all_packages(name = "node_modules")
+
+ng_pkg(
+ name = "pit_scouting",
+ extra_srcs = [
+ "//scouting/www:app_common_css",
+ ],
+ deps = [
+ ":node_modules/@angular/forms",
+ "//scouting/webserver/requests/messages:error_response_ts_fbs",
+ "//scouting/webserver/requests/messages:submit_pit_image_response_ts_fbs",
+ "//scouting/webserver/requests/messages:submit_pit_image_ts_fbs",
+ "@com_github_google_flatbuffers//ts:flatbuffers_ts",
+ ],
+)
diff --git a/scouting/www/pit_scouting/package.json b/scouting/www/pit_scouting/package.json
new file mode 100644
index 0000000..e58fc51
--- /dev/null
+++ b/scouting/www/pit_scouting/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@org_frc971/scouting/www/pit_scouting",
+ "private": true,
+ "dependencies": {
+ "@angular/forms": "15.1.5"
+ }
+}
diff --git a/scouting/www/pit_scouting/pit_scouting.component.css b/scouting/www/pit_scouting/pit_scouting.component.css
new file mode 100644
index 0000000..b224d73
--- /dev/null
+++ b/scouting/www/pit_scouting/pit_scouting.component.css
@@ -0,0 +1,8 @@
+* {
+ padding: 10px;
+}
+
+button {
+ touch-action: manipulation;
+ margin-top: 1vh;
+}
diff --git a/scouting/www/pit_scouting/pit_scouting.component.ts b/scouting/www/pit_scouting/pit_scouting.component.ts
new file mode 100644
index 0000000..b74c119
--- /dev/null
+++ b/scouting/www/pit_scouting/pit_scouting.component.ts
@@ -0,0 +1,75 @@
+import {Component} from '@angular/core';
+import {Builder, ByteBuffer} from 'flatbuffers';
+import {ErrorResponse} from '../../webserver/requests/messages/error_response_generated';
+import {SubmitPitImage} from '../../webserver/requests/messages/submit_pit_image_generated';
+
+type Section = 'TeamSelection' | 'Data';
+
+interface Input {
+ teamNumber: number;
+ pitImage: HTMLImageElement;
+}
+
+@Component({
+ selector: 'app-pit-scouting',
+ templateUrl: './pit_scouting.ng.html',
+ styleUrls: ['../app/common.css', './pit_scouting.component.css'],
+})
+export class PitScoutingComponent {
+ section: Section = 'Data';
+
+ errorMessage = '';
+ teamNumber: number = 971;
+ pitImage: string = '';
+
+ async readFile(file): Promise<ArrayBuffer> {
+ return new Promise((resolve, reject) => {
+ let reader = new FileReader();
+ reader.addEventListener('loadend', (e) =>
+ resolve(e.target.result as ArrayBuffer)
+ );
+ reader.addEventListener('error', reject);
+ reader.readAsArrayBuffer(file);
+ });
+ }
+
+ async getImageData() {
+ const selectedFile = (<HTMLInputElement>document.getElementById('pitImage'))
+ .files[0];
+ return new Uint8Array(await this.readFile(selectedFile));
+ }
+
+ async submitData() {
+ const builder = new Builder();
+ const teamNumber = builder.createString(this.teamNumber.toString());
+ const pitImage = builder.createString(this.pitImage.toString());
+ const imageData = SubmitPitImage.createImageDataVector(
+ builder,
+ await this.getImageData()
+ );
+ builder.finish(
+ SubmitPitImage.createSubmitPitImage(
+ builder,
+ teamNumber,
+ pitImage,
+ imageData
+ )
+ );
+
+ const buffer = builder.asUint8Array();
+ const res = await fetch('/requests/submit/submit_pit_image', {
+ method: 'POST',
+ body: buffer,
+ });
+ if (!res.ok) {
+ const resBuffer = await res.arrayBuffer();
+ const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+ const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
+
+ const errorMessage = parsedResponse.errorMessage();
+ this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+ }
+ this.section = 'TeamSelection';
+ this.pitImage = '';
+ }
+}
diff --git a/scouting/www/pit_scouting/pit_scouting.module.ts b/scouting/www/pit_scouting/pit_scouting.module.ts
new file mode 100644
index 0000000..7d92f6c
--- /dev/null
+++ b/scouting/www/pit_scouting/pit_scouting.module.ts
@@ -0,0 +1,11 @@
+import {NgModule} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {FormsModule} from '@angular/forms';
+import {PitScoutingComponent} from './pit_scouting.component';
+
+@NgModule({
+ declarations: [PitScoutingComponent],
+ exports: [PitScoutingComponent],
+ imports: [CommonModule, FormsModule],
+})
+export class PitScoutingModule {}
diff --git a/scouting/www/pit_scouting/pit_scouting.ng.html b/scouting/www/pit_scouting/pit_scouting.ng.html
new file mode 100644
index 0000000..98aa313
--- /dev/null
+++ b/scouting/www/pit_scouting/pit_scouting.ng.html
@@ -0,0 +1,30 @@
+<div class="header">
+ <h2>Pit Scouting</h2>
+</div>
+<ng-container [ngSwitch]="section">
+ <div *ngSwitchCase="'Data'" id="pitImageSelection" class="container-fluid">
+ <label for="teamNumber">Team Number</label>
+ <input
+ [(ngModel)]="teamNumber"
+ type="string"
+ id="teamNumber"
+ min="1"
+ max="9999"
+ />
+ <form action="setPitImage()">
+ <label for="pitImage">Select pit image:</label>
+ <br />
+ <input
+ id="pitImage"
+ [(ngModel)]="pitImage"
+ type="file"
+ accept="image/*"
+ />
+ </form>
+ <button id="submit-button" class="btn btn-success" (click)="submitData()">
+ Submit
+ </button>
+ </div>
+
+ <span class="error_message" role="alert">{{ errorMessage }}</span>
+</ng-container>