From 0066d0973fc46e352b1a61f5df5c24d0264296a6 Mon Sep 17 00:00:00 2001 From: greenblitz Date: Sun, 24 Nov 2024 19:05:27 +0200 Subject: [PATCH 01/14] Yoni - sure --- application/src/components/SureButton.tsx | 39 ++++++ application/src/scouter/ScoutingTab.tsx | 28 +--- application/src/strategy/AutoSection.tsx | 123 ------------------ application/src/strategy/charts/AutoChart.tsx | 63 --------- 4 files changed, 43 insertions(+), 210 deletions(-) create mode 100644 application/src/components/SureButton.tsx delete mode 100644 application/src/strategy/AutoSection.tsx delete mode 100644 application/src/strategy/charts/AutoChart.tsx diff --git a/application/src/components/SureButton.tsx b/application/src/components/SureButton.tsx new file mode 100644 index 0000000..71d9cb1 --- /dev/null +++ b/application/src/components/SureButton.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { useState } from "react"; + +interface SureButtonProps { + name: string; + onClick: () => void; +} + +const SureButton: React.FC = ({ onClick, name }) => { + const [areYouSure, setAreYouSure] = useState(false); + + return ( + <> + {areYouSure ? ( + <> +

Are You Sure?

+ + + + ) : ( + <> +
+
+
+ + + )} + + ); +}; + + +export default SureButton; \ No newline at end of file diff --git a/application/src/scouter/ScoutingTab.tsx b/application/src/scouter/ScoutingTab.tsx index b06ec08..d418a30 100644 --- a/application/src/scouter/ScoutingTab.tsx +++ b/application/src/scouter/ScoutingTab.tsx @@ -6,6 +6,7 @@ import Autonomous from "./tabs/Autonomous"; import Teleoperated from "./tabs/Teleoperated"; import PostMatch from "./tabs/PostMatch"; import { renderScouterNavBar } from "../App"; +import SureButton from "../components/SureButton"; const sections: React.FC[] = [PreMatch, Autonomous, Teleoperated, PostMatch]; @@ -13,12 +14,11 @@ function ScouterTab() { const navigate = useNavigate(); const [currentSectionNumber, setSectionNumber] = useState(0); - const [areYouSure, setAreYouSure] = useState(false); - function handleSubmit() { const formValues: Record = {}; Object.keys(localStorage) .filter((item) => item.startsWith(localStorageTabName)) + .forEach((item) => { formValues[item.slice(localStorageTabName.length)] = localStorage.getItem(item) + ""; @@ -26,6 +26,7 @@ function ScouterTab() { localStorage.removeItem(item); } }); + navigate("/", { state: formValues }); } @@ -65,28 +66,7 @@ function ScouterTab() { Next )} -
- - {areYouSure ? ( - <> -

Are You Sure?

- - - - ) : ( - <> -
-
-
- - - )} +
); } diff --git a/application/src/strategy/AutoSection.tsx b/application/src/strategy/AutoSection.tsx deleted file mode 100644 index fb25ea2..0000000 --- a/application/src/strategy/AutoSection.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { - autoBlueNotePositions, - Match, - Note, -} from "../Utils"; -import AutoChart from "./charts/AutoChart"; -import { width as autoMapWidth } from "../scouter/querytypes/AutoMap"; - -interface AutoTabProps { - matches: Match[] -} - -type NotePercenteges = [Note, number][]; -type Accuracy = number; -type Auto = [NotePercenteges, number, Accuracy, Accuracy]; - -const speakerName = "Speaker/Auto"; -function getAutos(matches: Match[]): Auto[] { - const matchesNotes: Note[][] = matches.map((match) => { - const notes: Note[] = JSON.parse(match["Automap/Notes"]); - return notes.some( - (note) => note.x === autoMapWidth - autoBlueNotePositions[0].x - ) - ? notes.map((note) => { - return { - x: autoMapWidth - note.x, - y: note.y, - color: note.color, - } as Note; - }) - : notes; - }); - - const speakerData: [number, number][] = matches.map((match) => { - return [parseInt(match[speakerName + "/Score"]), parseInt(match[speakerName + "/Miss"])]; - }); - - const bitMap = matchesNotes.map((matchNotes) => - matchNotes.map((note) => note.color !== "orange") - ); - function isBoolArraySame(arr1: boolean[], arr2: boolean[]) { - return arr1.every((value, index) => value === arr2[index]); - } - - //bro don't even try to debug from here on out - const samiesIndexes: number[][] = []; - bitMap.forEach((bools, boolsIndex) => { - let isIn = false; - samiesIndexes.forEach((samie) => { - if (isBoolArraySame(bitMap[samie[0]], bools)) { - isIn = true; - samie.push(boolsIndex); - } - }); - if (!isIn) { - samiesIndexes.push([boolsIndex]); - } - }); - - const notePercenteges: NotePercenteges[] = samiesIndexes.map( - (samieIndexes) => { - const sums: [Note, number][] = matchesNotes[samieIndexes[0]].map( - (note) => { - return [note, 0]; - } - ); - samieIndexes.forEach((samieIndex) => { - matchesNotes[samieIndex].forEach((note, index) => { - sums[index] = [ - note, - sums[index][1] + (note.color === "green" ? 1 : 0), - ]; - }); - }); - return sums.map(([note, sum]) => { - return [note, sum / samieIndexes.length]; - }); - } - ); - const speakerPercentages: [Accuracy, Accuracy][] = samiesIndexes.map( - (samieIndexes) => { - const sums: [number, number] = [0, 0]; - samieIndexes.forEach((samieIndex) => { - sums[0] = sums[0] + speakerData[samieIndex][0]; - sums[1] = sums[1] + speakerData[samieIndex][1]; - }); - return [sums[0] / samieIndexes.length, sums[1] / samieIndexes.length]; - } - ); - return notePercenteges.map((percentage, index) => [ - percentage, - samiesIndexes[index].length, - speakerPercentages[index][0], - speakerPercentages[index][1], - ]); -} - -const AutoTab: React.FC = ({matches}) => { - const [autos, setAutos] = useState([]); - - useEffect(() => { - setAutos(getAutos(matches)); - }, [matches]); - - console.log(autos); - - return ( - <> - {autos.map((auto, index) => ( -
- -
-

Times Done: {auto[1]}

-

Speaker Score: {auto[2]}

-

Speaker Miss: {auto[3]}

-
-
- ))} - - ); -}; -export default AutoTab; diff --git a/application/src/strategy/charts/AutoChart.tsx b/application/src/strategy/charts/AutoChart.tsx deleted file mode 100644 index 98a67e4..0000000 --- a/application/src/strategy/charts/AutoChart.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useEffect, useRef } from "react"; -import { Note } from "../../Utils"; -import { width, height } from "../../scouter/querytypes/AutoMap"; - -interface AutoChartProps { - notes: [Note, number][]; -} - -const noteRadius = 10; -const textSize = 1; -function drawNote( - note: Note, - percentage: number, - context: CanvasRenderingContext2D -) { - if (!context) return; - context.strokeStyle = note.color; - context.lineWidth = 4; - context.beginPath(); - context.arc(note.x, note.y, noteRadius, 0, 2 * Math.PI); - context.stroke(); - context.strokeStyle = "white"; - context.lineWidth = textSize; - context.strokeText( - (percentage * 100 + "").slice(0, 4), - note.x - 2, - note.y - 2 - ); -} - -const AutoChart: React.FC = ({ notes }) => { - const imagePath = "./src/assets/Blue Auto Map.png"; - - const canvasRef = useRef(null); - - function drawNotes() { - if (!canvasRef.current) { - return; - } - const context = canvasRef.current.getContext("2d"); - if (!context) { - return; - } - context.clearRect(0, 0, width, height); - notes.forEach((note) => drawNote(note[0], note[1], context)); - } - - useEffect(() => drawNotes(), [drawNotes, notes]); - return ( -
- -
- ); -}; - -export default AutoChart; From 842ba452af70518bb8ce4c29b6eeec309f876c58 Mon Sep 17 00:00:00 2001 From: greenblitz Date: Sun, 24 Nov 2024 20:09:33 +0200 Subject: [PATCH 02/14] Yoni - session lol --- application/src/scouter/ScoutingTab.tsx | 2 +- application/src/utils/Session.tsx | 53 +++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 application/src/utils/Session.tsx diff --git a/application/src/scouter/ScoutingTab.tsx b/application/src/scouter/ScoutingTab.tsx index d418a30..25031e2 100644 --- a/application/src/scouter/ScoutingTab.tsx +++ b/application/src/scouter/ScoutingTab.tsx @@ -26,7 +26,7 @@ function ScouterTab() { localStorage.removeItem(item); } }); - + navigate("/", { state: formValues }); } diff --git a/application/src/utils/Session.tsx b/application/src/utils/Session.tsx new file mode 100644 index 0000000..91396bb --- /dev/null +++ b/application/src/utils/Session.tsx @@ -0,0 +1,53 @@ +export default class Session implements Storage { + [name: string]: any; + length: number; + + private prefix: string; + private parent: Storage | Session; + + constructor(parent: Storage | Session, prefix?: string) { + this.prefix = prefix ? prefix : ""; + this.parent = parent; + } + + clear(): void { + this.keys() + .filter((key) => key.startsWith(this.prefix)) + .forEach((key) => this.removeItem(key)); + } + getItem(key: string): string | null { + return this.parent.getItem(this.prefix + key); + } + key(index: number): string | null { + let count = 0; + for (const key in this.keys()) { + if (count === index) { + return key.slice(this.prefix.length); + } + count++; + } + return null; + } + removeItem(key: string): void { + this.parent.removeItem(this.prefix + key); + } + setItem(key: string, value: string): void { + this.parent.setItem(this.prefix + key, value); + } + + keys() { + return Object.keys(this.parent) + .filter((key) => key.startsWith(this.prefix)) + .map((key) => key.slice(this.prefix.length)); + } + entries() { + return this.keys().map((key) => [key, this.getItem(key)]); + } + values() { + return this.keys().map((key) => this.getItem(key)); + } + + with(prefix: string) { + return new Session(this, prefix); + } +} From 532a749b04fd49e510f6c2dc7e43b7ea432dc0c7 Mon Sep 17 00:00:00 2001 From: greenblitz Date: Sun, 24 Nov 2024 20:10:15 +0200 Subject: [PATCH 03/14] Yoni - autonomus no more --- .../src/scouter/querytypes/AutoMap.tsx | 130 ------------------ application/src/scouter/tabs/Autonomous.tsx | 7 +- application/src/strategy/TeamTab.tsx | 6 +- 3 files changed, 2 insertions(+), 141 deletions(-) delete mode 100644 application/src/scouter/querytypes/AutoMap.tsx diff --git a/application/src/scouter/querytypes/AutoMap.tsx b/application/src/scouter/querytypes/AutoMap.tsx deleted file mode 100644 index 36e58f8..0000000 --- a/application/src/scouter/querytypes/AutoMap.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { useRef } from "react"; -import { Point } from "chart.js"; -import { localStorageTabName } from "../ScouterQuery"; -import { autoBlueNotePositions, autoNotePositions, Note } from "../../Utils"; - -interface AutoMapProps { - side: "blue" | "red"; -} -const noteRadius = 10; - -export const width = 360 * 0.8; -export const height = 240; -const AutoMap: React.FC = ({ side }) => { - const localStorageKey = localStorageTabName + "Automap/Notes"; - const canvasRef = useRef(null); - - const imagePath = - side === "blue" - ? "./src/assets/Blue Auto Map.png" - : "./src/assets/Red Auto Map.png"; - - function getNotes(): Note[] { - const newNotes: Note[] = autoNotePositions - .concat(autoBlueNotePositions) - .map((note) => { - return { - x: side === "blue" ? note.x * 0.8 : width - note.x * 0.8, - y: note.y * 0.8, - color: "orange", - }; - }); - const previousSide = - localStorage.getItem(localStorageTabName + "AutoMap/Side") || side; - console.log(previousSide); - const previousNotesJson = localStorage.getItem(localStorageKey); - const previousNotes: Note[] = previousNotesJson - ? JSON.parse(previousNotesJson) - : newNotes; - const notes = side != previousSide ? newNotes : previousNotes; - return notes; - } - - const [notes, setNotes] = useState(getNotes()); - - function getClosestNote(position: Point) { - let [min, closestNote]: [number, Note] = [ - 100000, - { x: 0, y: 0, color: "orange" }, - ]; - notes.forEach((note) => { - const distanceSquared = - (note.x - position.x) ** 2 + (note.y - position.y) ** 2; - if (min > distanceSquared) { - min = distanceSquared; - closestNote = note; - } - }); - if (min > 30 ** 2) { - return null; - } - return closestNote; - } - - function handleClick(event: React.MouseEvent) { - const clickedPoint: Point = { - x: event.pageX - event.currentTarget.offsetLeft, - y: event.pageY - event.currentTarget.offsetTop, - }; - const closestNote = getClosestNote(clickedPoint); - if (!closestNote) { - return; - } - if (closestNote.color === "orange") closestNote.color = "green"; - else if (closestNote.color === "green") { - closestNote.color = "red"; - } else { - closestNote.color = "orange"; - } - setNotes(notes); - drawNotes(); - localStorage.setItem(localStorageKey, JSON.stringify(notes)); - } - - useEffect(() => { - drawNotes(); - localStorage.setItem(localStorageKey, JSON.stringify(notes)); - localStorage.setItem(localStorageTabName + "AutoMap/Side", side); - }, [notes, canvasRef]); - - function drawNote(note: Note, context: CanvasRenderingContext2D) { - if (!context) return; - context.strokeStyle = note.color; - context.lineWidth = 4; - context.beginPath(); - context.arc(note.x, note.y, noteRadius, 0, 2 * Math.PI); - context.stroke(); - } - - function drawNotes() { - if (!canvasRef.current) { - return; - } - const context = canvasRef.current.getContext("2d"); - if (!context) { - return; - } - context.clearRect(0, 0, width, height); - notes.forEach((note) => drawNote(note, context)); - } - - return ( -
- -
- ); -}; -export default AutoMap; diff --git a/application/src/scouter/tabs/Autonomous.tsx b/application/src/scouter/tabs/Autonomous.tsx index 97d00fa..776e96b 100644 --- a/application/src/scouter/tabs/Autonomous.tsx +++ b/application/src/scouter/tabs/Autonomous.tsx @@ -1,6 +1,5 @@ import React, { useRef } from "react"; import CounterQuery from "../querytypes/CounterQuery"; -import AutoMap from "../querytypes/AutoMap"; interface AutonomousProps {} const Autonomous: React.FC = () => { @@ -21,11 +20,7 @@ const Autonomous: React.FC = () => {
- + ); }; diff --git a/application/src/strategy/TeamTab.tsx b/application/src/strategy/TeamTab.tsx index 5e6489c..09f6bc1 100644 --- a/application/src/strategy/TeamTab.tsx +++ b/application/src/strategy/TeamTab.tsx @@ -11,7 +11,6 @@ import { import { TeamData } from "../TeamData"; import React from "react"; import { renderStrategyNavBar } from "../App"; -import AutoTab from "./AutoSection"; interface TeamTabProps {} @@ -193,10 +192,7 @@ const TeamTab: React.FC = () => {
-
-

Autonomus

- -
+

Comments

From 8356ee05dc57d3ea593baa1392e47fbe05355bd5 Mon Sep 17 00:00:00 2001 From: greenblitz Date: Sun, 24 Nov 2024 20:11:39 +0200 Subject: [PATCH 04/14] Yoni - session --- application/src/utils/Session.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application/src/utils/Session.tsx b/application/src/utils/Session.tsx index 91396bb..fcc6b77 100644 --- a/application/src/utils/Session.tsx +++ b/application/src/utils/Session.tsx @@ -51,3 +51,5 @@ export default class Session implements Storage { return new Session(this, prefix); } } + +export const localSession = new Session(localStorage); From edf9968ccb8d906974d96ca8fdcab50370216859 Mon Sep 17 00:00:00 2001 From: greenblitz Date: Wed, 27 Nov 2024 17:23:13 +0200 Subject: [PATCH 05/14] Yoni - folder shish --- application/src/scouter/MatchList.tsx | 15 ++++++------- application/src/scouter/ScouterQuery.tsx | 12 +++++----- application/src/scouter/ScoutingTab.tsx | 22 +++++++++---------- .../src/scouter/querytypes/CheckboxQuery.tsx | 13 +++++------ .../src/scouter/querytypes/CounterQuery.tsx | 11 +++++----- .../src/scouter/querytypes/ListQuery.tsx | 11 +++++----- .../src/scouter/querytypes/MapQuery.tsx | 8 +++---- .../src/scouter/querytypes/RadioQuery.tsx | 13 +++++------ application/src/scouter/tabs/Teleoperated.tsx | 3 ++- .../utils/{Session.tsx => FolderStorage.tsx} | 15 ++++++++----- 10 files changed, 60 insertions(+), 63 deletions(-) rename application/src/utils/{Session.tsx => FolderStorage.tsx} (71%) diff --git a/application/src/scouter/MatchList.tsx b/application/src/scouter/MatchList.tsx index de8f534..b24a879 100644 --- a/application/src/scouter/MatchList.tsx +++ b/application/src/scouter/MatchList.tsx @@ -4,9 +4,9 @@ import React, { useState } from "react"; import QRCodeGenerator from "../components/QRCode-Generator"; import { getServerHostname } from "../Utils"; import { renderScouterNavBar } from "../App"; +import { matchFolder as matchesFolder } from "../utils/FolderStorage"; export const matchName = "Qual"; -const matchesTab = "Matches/"; const collapsibleSize = 10; @@ -15,24 +15,23 @@ const MatchList: React.FC = () => { const navigate = useNavigate(); const [matches, setMatches] = useState[]>( - Object.keys(localStorage) - .filter((match) => match.startsWith(matchesTab)) - .map((matchName) => JSON.parse(localStorage.getItem(matchName) || "{}")) + matchesFolder.keys() + .map((matchName) => JSON.parse(matchesFolder.getItem(matchName) || "{}")) ); - const latestMatch = location.state; + const latestMatch: Record | undefined = location.state; location.state = {}; if (latestMatch?.[matchName]) { matches.push(latestMatch); - localStorage.setItem( - matchesTab + latestMatch[matchName], + matchesFolder.setItem( + latestMatch[matchName], JSON.stringify(latestMatch) ); } function removeMatch(qualNumber: string, index: number) { - localStorage.removeItem(matchesTab + qualNumber); + matchesFolder.removeItem(qualNumber); const filtered = [...matches]; filtered.splice(index, 1); setMatches(filtered); diff --git a/application/src/scouter/ScouterQuery.tsx b/application/src/scouter/ScouterQuery.tsx index c6fa446..258c334 100644 --- a/application/src/scouter/ScouterQuery.tsx +++ b/application/src/scouter/ScouterQuery.tsx @@ -3,6 +3,7 @@ import CheckboxQuery from "./querytypes/CheckboxQuery"; import CounterQuery from "./querytypes/CounterQuery"; import ListQuery from "./querytypes/ListQuery"; import RadioQuery from "./querytypes/RadioQuery"; +import { queryFolder } from "../utils/FolderStorage"; interface ScouterQueryProps { name: string; queryType: "text" | "counter" | "checkbox" | "number" | "list" | "radio"; @@ -10,8 +11,6 @@ interface ScouterQueryProps { list?: string[]; } -export const localStorageTabName = "Queries/"; - const ScouterQuery: React.FC = ({ name, queryType, @@ -33,10 +32,9 @@ const ScouterQuery: React.FC = ({ ); default: - const storageName = localStorageTabName + name; useEffect(() => { - if (!localStorage.getItem(storageName)) - localStorage.setItem(storageName, ""); + if (!queryFolder.getItem(name)) + queryFolder.setItem(name, ""); }); return ( = ({ id={name} name={name} required={required} - defaultValue={localStorage.getItem(storageName) || ""} + defaultValue={queryFolder.getItem(name) || ""} onChange={(event) => - localStorage.setItem(storageName, event.target.value) + queryFolder.setItem(name, event.target.value) } /> ); diff --git a/application/src/scouter/ScoutingTab.tsx b/application/src/scouter/ScoutingTab.tsx index 25031e2..960323d 100644 --- a/application/src/scouter/ScoutingTab.tsx +++ b/application/src/scouter/ScoutingTab.tsx @@ -1,4 +1,3 @@ -import { localStorageTabName } from "./ScouterQuery"; import { useNavigate } from "react-router-dom"; import React, { useState } from "react"; import PreMatch from "./tabs/PreMatch"; @@ -7,23 +6,23 @@ import Teleoperated from "./tabs/Teleoperated"; import PostMatch from "./tabs/PostMatch"; import { renderScouterNavBar } from "../App"; import SureButton from "../components/SureButton"; +import { queryFolder } from "../utils/FolderStorage"; const sections: React.FC[] = [PreMatch, Autonomous, Teleoperated, PostMatch]; +const constantValues = ["Scouter Name", "Game Side"] + function ScouterTab() { const navigate = useNavigate(); const [currentSectionNumber, setSectionNumber] = useState(0); function handleSubmit() { const formValues: Record = {}; - Object.keys(localStorage) - .filter((item) => item.startsWith(localStorageTabName)) - - .forEach((item) => { - formValues[item.slice(localStorageTabName.length)] = - localStorage.getItem(item) + ""; - if (item !== "Queries/Scouter Name") { - localStorage.removeItem(item); + queryFolder.keys().forEach((item) => { + formValues[item] = + queryFolder.getItem(item) + ""; + if (!constantValues.includes(item)) { + queryFolder.removeItem(item); } }); @@ -31,9 +30,8 @@ function ScouterTab() { } function clearQueryStorage() { - Object.keys(localStorage) - .filter((item) => item.startsWith(localStorageTabName)) - .forEach((item) => localStorage.removeItem(item)); + queryFolder.keys() + .forEach((item) => queryFolder.removeItem(item)); } function handleReset() { diff --git a/application/src/scouter/querytypes/CheckboxQuery.tsx b/application/src/scouter/querytypes/CheckboxQuery.tsx index 4766bc1..8a56e44 100644 --- a/application/src/scouter/querytypes/CheckboxQuery.tsx +++ b/application/src/scouter/querytypes/CheckboxQuery.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from "react"; -import { localStorageTabName } from "../ScouterQuery"; +import { queryFolder } from "../../utils/FolderStorage"; interface CheckboxQueryProps { name: string; @@ -7,16 +7,15 @@ interface CheckboxQueryProps { } const CheckboxQuery: React.FC = ({ name, required }) => { - const localStorageKey = localStorageTabName + name; function updateCheckbox() { const newValue = - localStorage.getItem(localStorageKey) === "true" ? "false" : "true"; - localStorage.setItem(localStorageKey, newValue); + queryFolder.getItem(name) === "true" ? "false" : "true"; + queryFolder.setItem(name, newValue); } useEffect(() => { - if (!localStorage.getItem(localStorageKey)) { - localStorage.setItem(localStorageKey, "false"); + if (!queryFolder.getItem(name)) { + queryFolder.setItem(name, "false"); } }); return ( @@ -26,7 +25,7 @@ const CheckboxQuery: React.FC = ({ name, required }) => { name={name} required={required} onChange={updateCheckbox} - defaultChecked={localStorage.getItem(localStorageKey) === "true"} + defaultChecked={queryFolder.getItem(name) === "true"} /> ); }; diff --git a/application/src/scouter/querytypes/CounterQuery.tsx b/application/src/scouter/querytypes/CounterQuery.tsx index d0a1a30..de35ba6 100644 --- a/application/src/scouter/querytypes/CounterQuery.tsx +++ b/application/src/scouter/querytypes/CounterQuery.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { localStorageTabName } from "../ScouterQuery"; import React from "react"; +import { queryFolder } from "../../utils/FolderStorage"; interface CounterQueryProps { name: string; @@ -8,19 +8,18 @@ interface CounterQueryProps { } const CounterQuery: React.FC = ({ name, color }) => { - const localStorageKey = localStorageTabName + name; - const initialValue = localStorage.getItem(localStorageKey) || ""; + const initialValue = queryFolder.getItem(name) || ""; const startingNumber = initialValue === "" ? 0 : parseInt(initialValue); const [count, setCountState] = useState(startingNumber); function setCount(newCount: number) { - localStorage.setItem(localStorageKey, newCount + ""); + queryFolder.setItem(name, newCount + ""); setCountState(newCount); } useEffect(() => { - if (!localStorage.getItem(localStorageKey)) { - localStorage.setItem(localStorageKey, "0"); + if (!queryFolder.getItem(name)) { + queryFolder.setItem(name, "0"); } }, []); diff --git a/application/src/scouter/querytypes/ListQuery.tsx b/application/src/scouter/querytypes/ListQuery.tsx index 5288042..a7f3134 100644 --- a/application/src/scouter/querytypes/ListQuery.tsx +++ b/application/src/scouter/querytypes/ListQuery.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from "react"; -import { localStorageTabName } from "../ScouterQuery"; +import { queryFolder } from "../../utils/FolderStorage"; interface ListQueryProps { name: string; @@ -8,10 +8,9 @@ interface ListQueryProps { } const ListQuery: React.FC = ({ name, required, list }) => { - const localStorageKey = localStorageTabName + name; useEffect(() => { - if (!localStorage.getItem(localStorageKey)) { - localStorage.setItem(localStorageKey, list[0]); + if (!queryFolder.getItem(name)) { + queryFolder.setItem(name, list[0]); } }); return ( @@ -19,9 +18,9 @@ const ListQuery: React.FC = ({ name, required, list }) => { name={name} id={name} required={required} - defaultValue={localStorage.getItem(localStorageKey) || ""} + defaultValue={queryFolder.getItem(name) || ""} onChange={(event) => - localStorage.setItem(localStorageKey, event.target.value) + queryFolder.setItem(name, event.target.value) } > {list?.map((item, index) => ( diff --git a/application/src/scouter/querytypes/MapQuery.tsx b/application/src/scouter/querytypes/MapQuery.tsx index 34d604e..4b4f57b 100644 --- a/application/src/scouter/querytypes/MapQuery.tsx +++ b/application/src/scouter/querytypes/MapQuery.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from "react"; import { Point } from "chart.js"; import React from "react"; -import { localStorageTabName } from "../ScouterQuery"; import CounterQuery from "./CounterQuery"; +import { queryFolder } from "../../utils/FolderStorage"; interface MapQueryProps { name: string; side: "blue" | "red"; @@ -34,9 +34,9 @@ const MapQuery: React.FC = ({ imagePath, side, }) => { - const localStorageKey = localStorageTabName + name + "/Points"; + const mapFolder = queryFolder.with(name + "/") const [dataPoints, setDataPoints] = useState<(DataPoint | PassingPoint)[]>( - JSON.parse(localStorage.getItem(localStorageKey) || "[]") + JSON.parse(mapFolder.getItem("Points") || "[]") ); const [pressedButton, setPressedButton] = useState(defaultButton); @@ -129,7 +129,7 @@ const MapQuery: React.FC = ({ } useEffect(() => { - localStorage.setItem(localStorageKey, JSON.stringify(dataPoints)); + mapFolder.setItem("Points", JSON.stringify(dataPoints)); drawPoints(); }, [dataPoints, addPoint]); diff --git a/application/src/scouter/querytypes/RadioQuery.tsx b/application/src/scouter/querytypes/RadioQuery.tsx index 36a7b41..59163dd 100644 --- a/application/src/scouter/querytypes/RadioQuery.tsx +++ b/application/src/scouter/querytypes/RadioQuery.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from "react"; -import { localStorageTabName } from "../ScouterQuery"; +import { queryFolder } from "../../utils/FolderStorage"; interface RadioQueryProps { name: string; @@ -8,13 +8,12 @@ interface RadioQueryProps { } const RadioQuery: React.FC = ({ name, required, list }) => { - const localStorageKey = localStorageTabName + name; useEffect(() => { - if (!localStorage.getItem(localStorageKey)) { - localStorage.setItem(localStorageKey, ""); + if (!queryFolder.getItem(name)) { + queryFolder.setItem(name, ""); } }); - return list?.map((item, index) => ( + return list.map((item, index) => ( = ({ name, required, list }) => { name={name} value={item} required={required} - onChange={() => localStorage.setItem(localStorageKey, item)} - defaultChecked={item === localStorage.getItem(localStorageKey)} + onChange={() => queryFolder.setItem(name, item)} + defaultChecked={item === queryFolder.getItem(name)} /> diff --git a/application/src/scouter/tabs/Teleoperated.tsx b/application/src/scouter/tabs/Teleoperated.tsx index d5a3f30..a3885ba 100644 --- a/application/src/scouter/tabs/Teleoperated.tsx +++ b/application/src/scouter/tabs/Teleoperated.tsx @@ -1,5 +1,6 @@ import React from "react"; import MapQuery from "../querytypes/MapQuery"; +import { queryFolder } from "../../utils/FolderStorage"; interface TeleoperatedProps {} @@ -9,7 +10,7 @@ const Teleoperated: React.FC = () => { Date: Wed, 27 Nov 2024 17:36:22 +0200 Subject: [PATCH 06/14] Yoni - no duality of man" --- application/src/utils/FolderStorage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/utils/FolderStorage.tsx b/application/src/utils/FolderStorage.tsx index 2d21973..8ee4676 100644 --- a/application/src/utils/FolderStorage.tsx +++ b/application/src/utils/FolderStorage.tsx @@ -3,9 +3,9 @@ export default class FolderStorage implements Storage { length: number; private prefix: string; - public readonly parent: Storage | FolderStorage; + public readonly parent: Storage; - constructor(parent: Storage | FolderStorage, prefix?: string) { + constructor(parent: Storage, prefix?: string) { this.prefix = prefix ? prefix : ""; this.parent = parent; } From c98e7c213f39ec45400900d1c2176126263a464a Mon Sep 17 00:00:00 2001 From: greenblitz Date: Wed, 27 Nov 2024 19:37:21 +0200 Subject: [PATCH 07/14] Yoni - cleanup strategy --- application/src/TeamData.ts | 18 +++++-- application/src/Utils.ts | 44 ++++++++--------- application/src/scouter/MatchList.tsx | 8 +--- application/src/strategy/GeneralTab.tsx | 1 + application/src/strategy/TeamTab.tsx | 48 +++++++++++++------ application/src/strategy/charts/BarChart.tsx | 15 ++++-- application/src/strategy/charts/LineChart.tsx | 15 ++++-- application/src/strategy/charts/PieChart.tsx | 14 ++++-- 8 files changed, 101 insertions(+), 62 deletions(-) diff --git a/application/src/TeamData.ts b/application/src/TeamData.ts index 9ff54b7..9e88faf 100644 --- a/application/src/TeamData.ts +++ b/application/src/TeamData.ts @@ -1,3 +1,5 @@ +import { SectionData } from "./strategy/charts/PieChart"; + export class TeamData { public readonly matches: Record>; [key: string]: any; @@ -14,7 +16,10 @@ export class TeamData { return ( points.filter((point) => { if (data === "Pass" && point[0]) { - return point[0]["data"] === "Pass" && point[0]["successfulness"] === succesfulness; + return ( + point[0]["data"] === "Pass" && + point[0]["successfulness"] === succesfulness + ); } return ( point["data"] === data && @@ -78,14 +83,17 @@ export class TeamData { return dataSet; } - getAsPie(data: string, colorMap: Record) { - const dataSet: Record = {}; + getAsPie( + data: string, + colorMap: Record + ): Record { + const dataSet: Record = {}; Object.entries(this.matches).forEach(([_, match]) => { const dataValue = match[data]; if (!dataSet[dataValue]) { - dataSet[dataValue] = [0, colorMap[dataValue]]; + dataSet[dataValue] = { color: colorMap[dataValue], label: 0 }; } - dataSet[dataValue][0]++; + dataSet[dataValue].label++; }); return dataSet; } diff --git a/application/src/Utils.ts b/application/src/Utils.ts index 2611feb..a33632e 100644 --- a/application/src/Utils.ts +++ b/application/src/Utils.ts @@ -14,37 +14,37 @@ export function matchToSheet(match: Record) { return `${keys}\n${values}`; } -export async function getMatchesByCriteria(field?: string, value?: string) { - const searchedField = field && value ? `/${field}/${value}` : ``; - const data: Record[] = await fetch( - `https://${getServerHostname()}/Matches${searchedField}`, - { - method: "GET", - mode: "cors", - headers: { - "Content-Type": "application/json", - }, - } - ) +export async function fetchData(field: string); +export async function fetchData( + field: string, + method: string = "GET", + body?: string +) { + return await fetch(`https://${getServerHostname()}/${field}`, { + method: method, + mode: "cors", + headers: { + "Content-Type": "application/json", + }, + body: body, + }) .then((response) => { if (!response.ok) { throw new Error("Network response was not ok"); } return response.json(); }) - .catch((error) => { - console.log(error); - return []; - }); - return data; +} + +export async function getMatchesByCriteria(field?: string, value?: string) { + const searchedField = field && value ? `${field}/${value}` : ``; + return await fetchData(searchedField); } export function sortMatches(matches: Match[]) { - return matches.sort( - (match1, match2) => { - return parseInt(match1["Qual"]) - parseInt(match2["Qual"]); - } - ); + return matches.sort((match1, match2) => { + return parseInt(match1["Qual"]) - parseInt(match2["Qual"]); + }); } export type Match = Record; diff --git a/application/src/scouter/MatchList.tsx b/application/src/scouter/MatchList.tsx index b24a879..334bd4a 100644 --- a/application/src/scouter/MatchList.tsx +++ b/application/src/scouter/MatchList.tsx @@ -2,7 +2,7 @@ import { useLocation, useNavigate } from "react-router-dom"; import Collapsible from "react-collapsible"; import React, { useState } from "react"; import QRCodeGenerator from "../components/QRCode-Generator"; -import { getServerHostname } from "../Utils"; +import { fetchData, getServerHostname } from "../Utils"; import { renderScouterNavBar } from "../App"; import { matchFolder as matchesFolder } from "../utils/FolderStorage"; @@ -39,11 +39,7 @@ const MatchList: React.FC = () => { } function sendMatch(match: Record, index: number) { - fetch(`https://${getServerHostname()}/Match`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(match), - }) + fetchData("Match") .then(() => { alert("Succesfully Sent Match✅"); removeMatch(match?.[matchName], index); diff --git a/application/src/strategy/GeneralTab.tsx b/application/src/strategy/GeneralTab.tsx index 3a5930d..58912ad 100644 --- a/application/src/strategy/GeneralTab.tsx +++ b/application/src/strategy/GeneralTab.tsx @@ -44,6 +44,7 @@ const GeneralTab: React.FC = () => { } updateTeamTable(); }, []); + return ( <> {renderStrategyNavBar()} diff --git a/application/src/strategy/TeamTab.tsx b/application/src/strategy/TeamTab.tsx index 09f6bc1..bc5a6ce 100644 --- a/application/src/strategy/TeamTab.tsx +++ b/application/src/strategy/TeamTab.tsx @@ -105,9 +105,15 @@ const TeamTab: React.FC = () => { height={300} width={400} dataSets={{ - Speaker: ["pink", teamData.getAsLine("Speaker Score")], - Amp: ["yellow", teamData.getAsLine("Amp Score")], - Pass: ["purple", teamData.getAsLine("Pass Successful")], + Speaker: { + color: "pink", + data: teamData.getAsLine("Speaker Score"), + }, + Amp: { color: "yellow", data: teamData.getAsLine("Amp Score") }, + Pass: { + color: "purple", + data: teamData.getAsLine("Pass Successful"), + }, }} />
@@ -118,9 +124,15 @@ const TeamTab: React.FC = () => { height={300} width={400} dataSets={{ - Speaker: ["pink", teamData.getAsLine("Speaker Miss")], - Amp: ["yellow", teamData.getAsLine("Amp Miss")], - Pass: ["purple", teamData.getAsLine("Pass Unsuccessful")], + Speaker: { + color: "pink", + data: teamData.getAsLine("Speaker Miss"), + }, + Amp: { color: "yellow", data: teamData.getAsLine("Amp Miss") }, + Pass: { + color: "purple", + data: teamData.getAsLine("Pass Unsuccessful"), + }, }} /> @@ -131,8 +143,14 @@ const TeamTab: React.FC = () => { height={300} width={400} dataSets={{ - Score: ["green", teamData.getAsLine("Speaker/Auto/Score")], - Miss: ["red", teamData.getAsLine("Speaker/Auto/Miss")] + Score: { + color: "green", + data: teamData.getAsLine("Speaker/Auto/Score"), + }, + Miss: { + color: "red", + data: teamData.getAsLine("Speaker/Auto/Miss"), + }, }} /> @@ -166,8 +184,8 @@ const TeamTab: React.FC = () => {

Amp Accuracy

@@ -176,8 +194,8 @@ const TeamTab: React.FC = () => {

Speaker Accuracy

@@ -185,14 +203,14 @@ const TeamTab: React.FC = () => {

Pass Accuracy


- +

Comments

diff --git a/application/src/strategy/charts/BarChart.tsx b/application/src/strategy/charts/BarChart.tsx index d1f72a8..95aed51 100644 --- a/application/src/strategy/charts/BarChart.tsx +++ b/application/src/strategy/charts/BarChart.tsx @@ -4,22 +4,27 @@ import { Bar } from "react-chartjs-2"; Chart.register(CategoryScale, LinearScale, BarElement); -type DataSet = [Color, Record]; + type Color = string; + +interface DataSet { + color: Color; + data: Record; +} interface BarChartProps { dataSets: Record; } const BarChart: React.FC = ({ dataSets }) => { const data = { - labels: Object.keys(Object.values(dataSets)[0][1]), + labels: Object.keys(Object.values(dataSets)[0].data), datasets: Object.entries(dataSets).map(([dataSetName, dataSetValue]) => { return { label: dataSetName, - backgroundColor: [...Array(Object.values(dataSetValue[1]).length)].map( - () => dataSetValue[0] + backgroundColor: [...Array(Object.values(dataSetValue.data).length)].map( + () => dataSetValue.color ), - data: Object.values(dataSetValue[1]), + data: Object.values(dataSetValue.data), }; }), }; diff --git a/application/src/strategy/charts/LineChart.tsx b/application/src/strategy/charts/LineChart.tsx index 5a1320b..eaa9791 100644 --- a/application/src/strategy/charts/LineChart.tsx +++ b/application/src/strategy/charts/LineChart.tsx @@ -11,8 +11,15 @@ import { Line } from "react-chartjs-2"; Chart.register(LineElement, PointElement, CategoryScale, LinearScale, Legend); -type DataSet = [Color, Record]; + + type Color = string; + +interface DataSet { + color: Color; + data: Record; +} + interface LineChartProps { dataSets: Record; height: number; @@ -20,14 +27,14 @@ interface LineChartProps { } const LineChart: React.FC = ({ dataSets, height, width }) => { const data = { - labels: Object.keys(Object.values(dataSets)[0][1]), + labels: Object.keys(Object.values(dataSets)[0].data), datasets: Object.entries(dataSets).map(([dataSetName, dataSetValue]) => { return { label: dataSetName, - borderColor: dataSetValue[0], + borderColor: dataSetValue.color, tension: 0.2, - data: Object.values(dataSetValue[1]), + data: Object.values(dataSetValue.data), }; }), }; diff --git a/application/src/strategy/charts/PieChart.tsx b/application/src/strategy/charts/PieChart.tsx index 5713947..b557813 100644 --- a/application/src/strategy/charts/PieChart.tsx +++ b/application/src/strategy/charts/PieChart.tsx @@ -4,17 +4,21 @@ import { Chart, ArcElement, Tooltip, Legend } from "chart.js"; Chart.register(ArcElement, Tooltip, Legend); +export interface SectionData { + label: number; + color: string; +} interface PieChartProps { - pieData: Record; + pieData: Record; } const PieChart: React.FC = ({ pieData }) => { const names = Object.keys(pieData); - const data = Object.values(pieData).map(([num, _]) => { - return num; + const data = Object.values(pieData).map((section) => { + return section.label; }); - const backgroundColor = Object.values(pieData).map(([_, color]) => { - return color; + const backgroundColor = Object.values(pieData).map((section) => { + return section.color; }); const chartData = { From 5d15b1bb6ad932ed30bdb06c0eb4918d9301d1ea Mon Sep 17 00:00:00 2001 From: greenblitz Date: Sun, 1 Dec 2024 19:47:13 +0200 Subject: [PATCH 08/14] Yoni - better Queries --- application/src/App.css | 62 +++++-------- application/src/components/SureButton.tsx | 5 +- application/src/scouter/ScouterQuery.tsx | 79 ++++++++--------- application/src/scouter/ScoutingTab.tsx | 1 + .../src/scouter/querytypes/CheckboxQuery.tsx | 52 ++++++----- .../src/scouter/querytypes/CounterQuery.tsx | 87 ++++++++++--------- .../src/scouter/querytypes/ListQuery.tsx | 57 ++++++------ .../src/scouter/querytypes/RadioQuery.tsx | 51 ++++++----- application/src/scouter/tabs/PostMatch.tsx | 10 +-- application/src/scouter/tabs/PreMatch.tsx | 15 ++-- application/src/strategy/TeamTab.tsx | 1 - application/src/utils/FolderStorage.tsx | 2 +- 12 files changed, 200 insertions(+), 222 deletions(-) diff --git a/application/src/App.css b/application/src/App.css index 49a843c..e073810 100644 --- a/application/src/App.css +++ b/application/src/App.css @@ -5,18 +5,6 @@ text-align: center; } -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} .strategy-app { .section { @@ -72,12 +60,35 @@ height: 20%; padding: 20px; } + .map-buttons { grid-gap: 0px; display: flex; justify-content: space-between; } + +.collapsible-trigger { + border-style: solid; +} + +.cool-radio-input { + height: 20px; + width: 20px; +} + +.auto-chart { + display: flex; +} + +.team-picker { + select, + input, + label { + font-size: 20px; + } +} + .nav-bar { background: #333; padding: 1rem; @@ -108,30 +119,3 @@ .nav-bar a:hover { text-decoration: underline; } - -.auto-chart { - display: flex; -} -.team-picker { - select, - input, - label { - font-size: 20px; - } -} -.collapsible-trigger { - border-style: solid; -} - -.cool-radio-input { - height: 20px; - width: 20px; -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/application/src/components/SureButton.tsx b/application/src/components/SureButton.tsx index 71d9cb1..84644ab 100644 --- a/application/src/components/SureButton.tsx +++ b/application/src/components/SureButton.tsx @@ -2,7 +2,7 @@ import React from "react"; import { useState } from "react"; interface SureButtonProps { - name: string; + name: string; onClick: () => void; } @@ -35,5 +35,4 @@ const SureButton: React.FC = ({ onClick, name }) => { ); }; - -export default SureButton; \ No newline at end of file +export default SureButton; diff --git a/application/src/scouter/ScouterQuery.tsx b/application/src/scouter/ScouterQuery.tsx index 258c334..7b8683a 100644 --- a/application/src/scouter/ScouterQuery.tsx +++ b/application/src/scouter/ScouterQuery.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from "react"; -import CheckboxQuery from "./querytypes/CheckboxQuery"; +import c from "./querytypes/CheckboxQuery"; import CounterQuery from "./querytypes/CounterQuery"; import ListQuery from "./querytypes/ListQuery"; import RadioQuery from "./querytypes/RadioQuery"; @@ -11,52 +11,41 @@ interface ScouterQueryProps { list?: string[]; } -const ScouterQuery: React.FC = ({ - name, - queryType, - required, - list, -}) => { - function renderInput() { - switch (queryType) { - case "counter": - return ; - case "checkbox": - return ; - case "list": - return ( - - ); - case "radio": - return ( - - ); - default: - useEffect(() => { - if (!queryFolder.getItem(name)) - queryFolder.setItem(name, ""); - }); - return ( - - queryFolder.setItem(name, event.target.value) - } - /> - ); +export interface QueryProps { + name: string; + required?: boolean | undefined; + defaultValue?: T; +} + +abstract class ScouterQuery< + T, + State extends {} = {}, + Props = {} +> extends React.Component & Props, State> { + constructor(props: QueryProps & Props) { + super(props); + + if (!queryFolder.getItem(props.name)) { + queryFolder.setItem( + props.name, + (this.props.defaultValue || this.getInitialValue(props)) + "" + ); } + this.state = this.getStartingState(props); + } + + render(): React.ReactNode { + return ( +
+

{this.props.name}

+ {this.renderInput()} +
+ ); } - return ( -
-

{name}

- {renderInput()} -
- ); -}; + abstract getStartingState(props: QueryProps & Props): State; + abstract renderInput(): React.ReactNode; + abstract getInitialValue(props: QueryProps & Props): T; +} export default ScouterQuery; diff --git a/application/src/scouter/ScoutingTab.tsx b/application/src/scouter/ScoutingTab.tsx index 960323d..4f1a0a4 100644 --- a/application/src/scouter/ScoutingTab.tsx +++ b/application/src/scouter/ScoutingTab.tsx @@ -10,6 +10,7 @@ import { queryFolder } from "../utils/FolderStorage"; const sections: React.FC[] = [PreMatch, Autonomous, Teleoperated, PostMatch]; + const constantValues = ["Scouter Name", "Game Side"] function ScouterTab() { diff --git a/application/src/scouter/querytypes/CheckboxQuery.tsx b/application/src/scouter/querytypes/CheckboxQuery.tsx index 8a56e44..9a0233b 100644 --- a/application/src/scouter/querytypes/CheckboxQuery.tsx +++ b/application/src/scouter/querytypes/CheckboxQuery.tsx @@ -1,33 +1,31 @@ import React, { useEffect } from "react"; import { queryFolder } from "../../utils/FolderStorage"; +import ScouterQuery from "../ScouterQuery"; -interface CheckboxQueryProps { - name: string; - required: boolean | undefined; -} - -const CheckboxQuery: React.FC = ({ name, required }) => { - function updateCheckbox() { - const newValue = - queryFolder.getItem(name) === "true" ? "false" : "true"; - queryFolder.setItem(name, newValue); +class CheckboxQuery extends ScouterQuery { + getStartingState(): {} { + return {}; } - - useEffect(() => { - if (!queryFolder.getItem(name)) { - queryFolder.setItem(name, "false"); - } - }); - return ( - - ); -}; + renderInput(): React.ReactNode { + const updateCheckbox = () => { + const newValue = + queryFolder.getItem(this.props.name) === "true" ? "false" : "true"; + queryFolder.setItem(this.props.name, newValue); + }; + return ( + + ); + } + getInitialValue(): boolean { + return this.props.defaultValue || false; + } +} export default CheckboxQuery; diff --git a/application/src/scouter/querytypes/CounterQuery.tsx b/application/src/scouter/querytypes/CounterQuery.tsx index de35ba6..b5db1cb 100644 --- a/application/src/scouter/querytypes/CounterQuery.tsx +++ b/application/src/scouter/querytypes/CounterQuery.tsx @@ -1,48 +1,57 @@ -import { useEffect, useState } from "react"; import React from "react"; import { queryFolder } from "../../utils/FolderStorage"; +import ScouterQuery, { QueryProps } from "../ScouterQuery"; -interface CounterQueryProps { - name: string; - color?: string; -} - -const CounterQuery: React.FC = ({ name, color }) => { - const initialValue = queryFolder.getItem(name) || ""; - const startingNumber = initialValue === "" ? 0 : parseInt(initialValue); - const [count, setCountState] = useState(startingNumber); +class CounterQuery extends ScouterQuery< + number, + { count: number }, + { color?: string } +> { + getStartingState(props: QueryProps) { + const savedValue = queryFolder.getItem(props.name); + let initialValue: number = props.defaultValue || 0; + if (savedValue) { + initialValue = parseInt(savedValue); + } + return { count: initialValue }; + } - function setCount(newCount: number) { - queryFolder.setItem(name, newCount + ""); - setCountState(newCount); + getInitialValue(): number { + return 0; } - useEffect(() => { - if (!queryFolder.getItem(name)) { - queryFolder.setItem(name, "0"); - } - }, []); + renderInput(): React.ReactNode { + const setCount = (newCount: number) => { + queryFolder.setItem(this.props.name, newCount + ""); + this.setState({ count: newCount }); + }; - return ( - <> - -

{count}

- - - - ); -}; + return ( + <> + +

{this.state.count}

+ + + + ); + } +} export default CounterQuery; diff --git a/application/src/scouter/querytypes/ListQuery.tsx b/application/src/scouter/querytypes/ListQuery.tsx index a7f3134..1a35e79 100644 --- a/application/src/scouter/querytypes/ListQuery.tsx +++ b/application/src/scouter/querytypes/ListQuery.tsx @@ -1,35 +1,34 @@ import React, { useEffect } from "react"; import { queryFolder } from "../../utils/FolderStorage"; +import ScouterQuery, { QueryProps } from "../ScouterQuery"; -interface ListQueryProps { - name: string; - required: boolean | undefined; - list: string[]; -} +class ListQuery extends ScouterQuery { + getStartingState(props: QueryProps & { list: string[] }): {} { + return {}; + } -const ListQuery: React.FC = ({ name, required, list }) => { - useEffect(() => { - if (!queryFolder.getItem(name)) { - queryFolder.setItem(name, list[0]); - } - }); - return ( - - ); -}; + renderInput(): React.ReactNode { + return ( + + ); + } + getInitialValue(props: QueryProps & { list: string[] }): string { + return props.list[0]; + } +} export default ListQuery; diff --git a/application/src/scouter/querytypes/RadioQuery.tsx b/application/src/scouter/querytypes/RadioQuery.tsx index 59163dd..eacc41f 100644 --- a/application/src/scouter/querytypes/RadioQuery.tsx +++ b/application/src/scouter/querytypes/RadioQuery.tsx @@ -1,32 +1,31 @@ import React, { useEffect } from "react"; import { queryFolder } from "../../utils/FolderStorage"; +import ScouterQuery, { QueryProps } from "../ScouterQuery"; -interface RadioQueryProps { - name: string; - required: boolean | undefined; - list: string[]; -} +class RadioQuery extends ScouterQuery { + getStartingState(props: QueryProps & { list: string[] }): {} { + return {}; + } -const RadioQuery: React.FC = ({ name, required, list }) => { - useEffect(() => { - if (!queryFolder.getItem(name)) { - queryFolder.setItem(name, ""); - } - }); - return list.map((item, index) => ( - - queryFolder.setItem(name, item)} - defaultChecked={item === queryFolder.getItem(name)} - /> - - - )); -}; + renderInput(): React.ReactNode { + return this.props.list.map((item, index) => ( + + queryFolder.setItem(this.props.name, item)} + defaultChecked={item === queryFolder.getItem(this.props.name)} + /> + + + )); + } + getInitialValue(props: QueryProps & { list: string[] }): string { + return props.list[0]; + } +} export default RadioQuery; diff --git a/application/src/scouter/tabs/PostMatch.tsx b/application/src/scouter/tabs/PostMatch.tsx index 2f14016..3d85ab5 100644 --- a/application/src/scouter/tabs/PostMatch.tsx +++ b/application/src/scouter/tabs/PostMatch.tsx @@ -1,13 +1,14 @@ import React from "react"; import ScouterQuery from "../ScouterQuery"; +import ListQuery from "../querytypes/ListQuery"; +import TextQuery from "../querytypes/TextQuery"; interface PostMatchProps {} const PostMatch: React.FC = () => { return ( <> - = () => { "Harmony Three Robots", ]} /> - - + ); }; diff --git a/application/src/scouter/tabs/PreMatch.tsx b/application/src/scouter/tabs/PreMatch.tsx index 71acc80..ee3bfd5 100644 --- a/application/src/scouter/tabs/PreMatch.tsx +++ b/application/src/scouter/tabs/PreMatch.tsx @@ -1,17 +1,18 @@ import React from "react"; -import ScouterQuery from "../ScouterQuery"; +import TextQuery from "../querytypes/TextQuery"; +import NumberQuery from "../querytypes/NumberQuery"; +import ListQuery from "../querytypes/ListQuery"; interface PreGameProps {} const PreMatch: React.FC = () => { return ( <> - - - - - + + + + diff --git a/application/src/strategy/TeamTab.tsx b/application/src/strategy/TeamTab.tsx index bc5a6ce..c6afeee 100644 --- a/application/src/strategy/TeamTab.tsx +++ b/application/src/strategy/TeamTab.tsx @@ -40,7 +40,6 @@ const TeamTab: React.FC = () => { recentMatches.splice(0, recentMatches.length - recency); } - console.log(recentMatches); const teamData = new TeamData(recentMatches); const ampAccuracy = teamData.getAccuracy("Amp Score", "Amp Miss"); diff --git a/application/src/utils/FolderStorage.tsx b/application/src/utils/FolderStorage.tsx index 8ee4676..bbf5b68 100644 --- a/application/src/utils/FolderStorage.tsx +++ b/application/src/utils/FolderStorage.tsx @@ -22,7 +22,7 @@ export default class FolderStorage implements Storage { let count = 0; for (const key in this.keys()) { if (count === index) { - return key.slice(this.prefix.length); + return key; } count++; } From 935462430870c76e7878e3da225b64c290f607d6 Mon Sep 17 00:00:00 2001 From: greenblitz Date: Sun, 1 Dec 2024 19:47:25 +0200 Subject: [PATCH 09/14] Yoni - better Queries this time actually --- .../src/scouter/querytypes/NumberQuery.tsx | 30 +++++++++++++++++++ .../src/scouter/querytypes/TextQuery.tsx | 30 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 application/src/scouter/querytypes/NumberQuery.tsx create mode 100644 application/src/scouter/querytypes/TextQuery.tsx diff --git a/application/src/scouter/querytypes/NumberQuery.tsx b/application/src/scouter/querytypes/NumberQuery.tsx new file mode 100644 index 0000000..3a6b3b0 --- /dev/null +++ b/application/src/scouter/querytypes/NumberQuery.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { queryFolder } from "../../utils/FolderStorage"; +import ScouterQuery, { QueryProps } from "../ScouterQuery"; + +class NumberQuery extends ScouterQuery { + getStartingState(props: QueryProps) { + return {}; + } + + getInitialValue(): number { + return 0; + } + + renderInput(): React.ReactNode { + return ( + + queryFolder.setItem(this.props.name, event.target.value) + } + /> + ); + } +} + +export default NumberQuery; diff --git a/application/src/scouter/querytypes/TextQuery.tsx b/application/src/scouter/querytypes/TextQuery.tsx new file mode 100644 index 0000000..428c295 --- /dev/null +++ b/application/src/scouter/querytypes/TextQuery.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { queryFolder } from "../../utils/FolderStorage"; +import ScouterQuery, { QueryProps } from "../ScouterQuery"; + +class TextQuery extends ScouterQuery { + getStartingState(props: QueryProps) { + return {}; + } + + getInitialValue(): string { + return ""; + } + + renderInput(): React.ReactNode { + return ( + + queryFolder.setItem(this.props.name, event.target.value) + } + /> + ); + } +} + +export default TextQuery; From 15dd65ffbb6b7505c23ab927bb314332022a75c3 Mon Sep 17 00:00:00 2001 From: greenblitz Date: Sun, 1 Dec 2024 19:54:38 +0200 Subject: [PATCH 10/14] Yoni - fixed storage --- application/src/utils/FolderStorage.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/application/src/utils/FolderStorage.tsx b/application/src/utils/FolderStorage.tsx index bbf5b68..c232cee 100644 --- a/application/src/utils/FolderStorage.tsx +++ b/application/src/utils/FolderStorage.tsx @@ -3,9 +3,9 @@ export default class FolderStorage implements Storage { length: number; private prefix: string; - public readonly parent: Storage; + public readonly parent: Storage | FolderStorage; - constructor(parent: Storage, prefix?: string) { + constructor(parent: Storage | FolderStorage, prefix?: string) { this.prefix = prefix ? prefix : ""; this.parent = parent; } @@ -36,7 +36,11 @@ export default class FolderStorage implements Storage { } keys() { - return Object.keys(this.parent) + const isParentFolder = this.parent.keys !== undefined; + const parentKeys = isParentFolder + ? this.parent.keys() + : Object.keys(this.parent); + return parentKeys .filter((key) => key.startsWith(this.prefix)) .map((key) => key.slice(this.prefix.length)); } @@ -56,5 +60,4 @@ export const localFolder = new FolderStorage(localStorage); export const sessionFolder = new FolderStorage(sessionStorage); export const queryFolder = localFolder.with("Queries/"); -export const matchFolder = localFolder.with("Matches/") - +export const matchFolder = localFolder.with("Matches/"); From 7750b8d8fa9467f96d252e047d268aac42e9a2e8 Mon Sep 17 00:00:00 2001 From: greenblitz Date: Sun, 1 Dec 2024 19:56:13 +0200 Subject: [PATCH 11/14] Yoni - better type --- application/src/utils/FolderStorage.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/application/src/utils/FolderStorage.tsx b/application/src/utils/FolderStorage.tsx index c232cee..d9b61f1 100644 --- a/application/src/utils/FolderStorage.tsx +++ b/application/src/utils/FolderStorage.tsx @@ -1,6 +1,4 @@ -export default class FolderStorage implements Storage { - [name: string]: any; - length: number; +export default class FolderStorage { private prefix: string; public readonly parent: Storage | FolderStorage; From a3b3bb16eb22b571a60a3302a5bb48e92b999201 Mon Sep 17 00:00:00 2001 From: greenblitz Date: Sun, 1 Dec 2024 20:03:58 +0200 Subject: [PATCH 12/14] Yoni - imports and such --- application/src/scouter/ScouterQuery.tsx | 14 ++------------ .../src/scouter/querytypes/CheckboxQuery.tsx | 2 +- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/application/src/scouter/ScouterQuery.tsx b/application/src/scouter/ScouterQuery.tsx index 7b8683a..327d15a 100644 --- a/application/src/scouter/ScouterQuery.tsx +++ b/application/src/scouter/ScouterQuery.tsx @@ -1,15 +1,5 @@ -import React, { useEffect } from "react"; -import c from "./querytypes/CheckboxQuery"; -import CounterQuery from "./querytypes/CounterQuery"; -import ListQuery from "./querytypes/ListQuery"; -import RadioQuery from "./querytypes/RadioQuery"; +import React from "react"; import { queryFolder } from "../utils/FolderStorage"; -interface ScouterQueryProps { - name: string; - queryType: "text" | "counter" | "checkbox" | "number" | "list" | "radio"; - required?: boolean | undefined; - list?: string[]; -} export interface QueryProps { name: string; @@ -20,7 +10,7 @@ export interface QueryProps { abstract class ScouterQuery< T, State extends {} = {}, - Props = {} + Props extends {} = {} > extends React.Component & Props, State> { constructor(props: QueryProps & Props) { super(props); diff --git a/application/src/scouter/querytypes/CheckboxQuery.tsx b/application/src/scouter/querytypes/CheckboxQuery.tsx index 9a0233b..27bde72 100644 --- a/application/src/scouter/querytypes/CheckboxQuery.tsx +++ b/application/src/scouter/querytypes/CheckboxQuery.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from "react"; import { queryFolder } from "../../utils/FolderStorage"; import ScouterQuery from "../ScouterQuery"; -class CheckboxQuery extends ScouterQuery { +class CheckboxQuery extends ScouterQuery { getStartingState(): {} { return {}; } From fdd24c75be9e36d7d138d3037da922302a586073 Mon Sep 17 00:00:00 2001 From: greenblitz Date: Sun, 1 Dec 2024 20:12:53 +0200 Subject: [PATCH 13/14] Yoni - nice --- application/src/TeamData.ts | 18 +++++++++++++- application/src/strategy/TeamTab.tsx | 35 +++++++--------------------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/application/src/TeamData.ts b/application/src/TeamData.ts index 9e88faf..3ba71a2 100644 --- a/application/src/TeamData.ts +++ b/application/src/TeamData.ts @@ -1,5 +1,5 @@ import { SectionData } from "./strategy/charts/PieChart"; - +import { DataPoint, PassingPoint } from "./strategy/charts/MapChart" export class TeamData { public readonly matches: Record>; [key: string]: any; @@ -108,4 +108,20 @@ export class TeamData { }); return (sum1 / (sum1 + sum2)) * 100; } + getComments(): [string, string][] { + return Object.values(this.matches) + .map((match) => [match["Comment"], match["Qual"]]) + .filter(([comment, qual]) => comment !== "") as [string, string][]; + } + + getAllPoints() { + let points: (DataPoint | PassingPoint)[] = []; + Object.values(this.matches).forEach((match) => { + const mapPoints: (DataPoint | PassingPoint)[] = JSON.parse( + match[TeamData.mapName + "/Points"] + ); + points = [...points, ...mapPoints]; + }); + return points; + } } diff --git a/application/src/strategy/TeamTab.tsx b/application/src/strategy/TeamTab.tsx index c6afeee..13bc1b0 100644 --- a/application/src/strategy/TeamTab.tsx +++ b/application/src/strategy/TeamTab.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import LineChart from "./charts/LineChart"; import PieChart from "./charts/PieChart"; -import MapChart, { DataPoint, PassingPoint } from "./charts/MapChart"; +import MapChart from "./charts/MapChart"; import { getMatchesByCriteria, FRCTeamList, @@ -14,23 +14,6 @@ import { renderStrategyNavBar } from "../App"; interface TeamTabProps {} -function getAllPoints(matches: Match[]) { - let points: (DataPoint | PassingPoint)[] = []; - matches.forEach((match) => { - const mapPoints: (DataPoint | PassingPoint)[] = JSON.parse( - match[TeamData.mapName + "/Points"] - ); - points = [...points, ...mapPoints]; - }); - return points; -} - -function getComments(matches: Match[]): [string, string][] { - return matches - .map((match) => [match["Comment"], match["Qual"]]) - .filter(([comment, qual]) => comment !== "") as [string, string][]; -} - const TeamTab: React.FC = () => { const [matches, setMatches] = useState([]); const [recency, setRecency] = useState(0); @@ -93,7 +76,7 @@ const TeamTab: React.FC = () => { width={540 * 0.8} height={240 * 0.8} imagePath={"./src/assets/Crescendo Map.png"} - dataPoints={getAllPoints(recentMatches)} + dataPoints={teamData.getAllPoints()} />

@@ -183,8 +166,8 @@ const TeamTab: React.FC = () => {

Amp Accuracy

@@ -193,8 +176,8 @@ const TeamTab: React.FC = () => {

Speaker Accuracy

@@ -202,8 +185,8 @@ const TeamTab: React.FC = () => {

Pass Accuracy

@@ -213,7 +196,7 @@ const TeamTab: React.FC = () => {

Comments

- {getComments(recentMatches).map((comment) => ( + {teamData.getComments().map((comment) => (

{comment[0] + "...Qual number" + comment[1]}

))}
From 83ce692d8038f712d7104cda4495953fd68bc51e Mon Sep 17 00:00:00 2001 From: CoolGame8 Date: Sun, 8 Dec 2024 20:25:21 +0200 Subject: [PATCH 14/14] Yuval and Yoni - added all tailwind stuff --- application/package.json | 7 + application/postcss.config.js | 6 + application/src/App.css | 121 ------- application/src/App.tsx | 106 ++++--- application/src/components/SureButton.tsx | 73 +++-- application/src/index.css | 40 +++ application/src/scouter/MatchList.tsx | 100 ++++-- application/src/scouter/ScouterQuery.tsx | 5 +- application/src/scouter/ScoutingTab.tsx | 114 +++++-- .../src/scouter/querytypes/CounterQuery.tsx | 42 ++- .../src/scouter/querytypes/MapQuery.tsx | 299 +++++++++++++----- .../src/scouter/querytypes/PassQuery.tsx | 44 +++ application/src/scouter/tabs/Autonomous.tsx | 59 +++- application/src/scouter/tabs/Teleoperated.tsx | 36 ++- application/tailwind.config.js | 48 +++ application/vite.config.ts | 3 + package-lock.json | 6 + 17 files changed, 727 insertions(+), 382 deletions(-) create mode 100644 application/postcss.config.js delete mode 100644 application/src/App.css create mode 100644 application/src/scouter/querytypes/PassQuery.tsx create mode 100644 application/tailwind.config.js create mode 100644 package-lock.json diff --git a/application/package.json b/application/package.json index 00db257..9881d5f 100644 --- a/application/package.json +++ b/application/package.json @@ -37,15 +37,22 @@ }, "devDependencies": { "@eslint/js": "^9.9.0", + "@headlessui/react": "^2.2.0", "@types/express": "^4.17.21", "@types/qrcode": "^1.5.5", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "clsx": "^2.1.1", "eslint": "^9.9.0", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.9", + "framer-motion": "^11.12.0", "globals": "^15.9.0", + "lucide-react": "^0.462.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.15", "typescript": "^5.5.4", "typescript-eslint": "^8.0.1", "vite": "^5.4.1" diff --git a/application/postcss.config.js b/application/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/application/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/application/src/App.css b/application/src/App.css deleted file mode 100644 index e073810..0000000 --- a/application/src/App.css +++ /dev/null @@ -1,121 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - - -.strategy-app { - .section { - display: inline-block; - } -} - -.scouter-query { - h3, - h2, - button { - display: inline; - } - to { - transform: rotate(360deg); - } -} - -.succesfulness { - position: absolute; - top: 80%; - left: 45%; - width: 80px; - button { - font-size: 20px; - } -} - -.map-amp { - h3, - h2, - button { - display: inline; - position: relative; - } - width: 40%; /* Adjust as needed */ - height: 20%; - padding: 20px; -} - -.speaker-auto { - h3, - h2, - button { - display: inline; - position: relative; - } - width: 70%; -} - -.map-options { - width: 55%; /* Adjust as needed */ - height: 20%; - padding: 20px; -} - -.map-buttons { - grid-gap: 0px; - display: flex; - justify-content: space-between; -} - - -.collapsible-trigger { - border-style: solid; -} - -.cool-radio-input { - height: 20px; - width: 20px; -} - -.auto-chart { - display: flex; -} - -.team-picker { - select, - input, - label { - font-size: 20px; - } -} - -.nav-bar { - background: #333; - padding: 1rem; - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 1000; -} - -.nav-bar ul { - list-style: none; - display: flex; - justify-content: center; /* Center the items */ - margin: 0; /* Reset default margin */ - padding: 0; /* Reset default padding */ -} - -.nav-bar li { - margin-right: 1rem; -} - -.nav-bar a { - color: white; - text-decoration: none; -} - -.nav-bar a:hover { - text-decoration: underline; -} diff --git a/application/src/App.tsx b/application/src/App.tsx index 848f033..b373f31 100644 --- a/application/src/App.tsx +++ b/application/src/App.tsx @@ -1,7 +1,7 @@ -import React from "react"; -import "./App.css"; - +import React, { useEffect } from "react"; import { BrowserRouter, Link, Route, Routes } from "react-router-dom"; +import { motion } from "framer-motion"; +import { ClipboardList, Users, Scan, ChevronRight, BarChart } from "lucide-react"; import ScouterTab from "./scouter/ScoutingTab"; import MatchList from "./scouter/MatchList"; import ScanningTab from "./scouter/scanner/ScanningTab"; @@ -10,66 +10,76 @@ import TeamTab from "./strategy/TeamTab"; function getHiddenImage(path: string) { return ( -
-
+
); } +const NavLink = ({ to, children, icon: Icon }: { to: string; children: React.ReactNode; icon: React.ComponentType }) => ( + + + {children} + + +); + export function renderScouterNavBar() { return ( - + + {getHiddenImage("./src/assets/Crescendo Map.png")} + {getHiddenImage("./src/assets/Blue Auto Map.png")} + {getHiddenImage("./src/assets/Red Auto Map.png")} +
+ Match List + Scout Game + Scan Match +
+
); } export function renderStrategyNavBar() { return ( - + +
+ Team Data + General +
+
); } -const App: React.FC = () => { - +function App() { + useEffect(() => { + // Enable dark mode by default + document.documentElement.classList.add('dark'); + }, []); + return ( - - - - - - - +
+
+ + } /> + } /> + } /> + } /> + } /> + +
+
- ); -}; + ); +} export default App; diff --git a/application/src/components/SureButton.tsx b/application/src/components/SureButton.tsx index 84644ab..31be93d 100644 --- a/application/src/components/SureButton.tsx +++ b/application/src/components/SureButton.tsx @@ -1,37 +1,60 @@ import React from "react"; import { useState } from "react"; - +import { motion, AnimatePresence } from "framer-motion"; +import { AlertTriangle, Check, X } from "lucide-react"; +// הי יוני מה קורה interface SureButtonProps { - name: string; onClick: () => void; + children?: React.ReactNode; + className?: string; } -const SureButton: React.FC = ({ onClick, name }) => { +const SureButton: React.FC = ({ onClick, children, className }) => { const [areYouSure, setAreYouSure] = useState(false); return ( - <> - {areYouSure ? ( - <> -

Are You Sure?

- - - - ) : ( - <> -
-
-
- - - )} - +
+ + {areYouSure ? ( + +
+ + Are you sure? +
+
+ + +
+
+ ) : null} +
+ +
); }; diff --git a/application/src/index.css b/application/src/index.css index 6119ad9..2704966 100644 --- a/application/src/index.css +++ b/application/src/index.css @@ -66,3 +66,43 @@ button:focus-visible { background-color: #f9f9f9; } } + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + body { + @apply bg-gray-50 text-gray-900 dark:bg-dark-bg dark:text-dark-text; + } + input, select { + @apply rounded-lg bg-dark-card border border-dark-border px-4 py-2; + } +} + +@layer components { + .btn { + @apply px-4 py-2 rounded-md font-medium transition-colors duration-200; + } + + .btn-primary { + @apply bg-primary-600 text-white hover:bg-primary-700 dark:bg-primary-500 dark:hover:bg-primary-600; + } + + .btn-secondary { + @apply bg-gray-200 text-gray-800 hover:bg-gray-300 dark:bg-dark-card dark:text-dark-text dark:hover:bg-gray-700; + } + + .nav-link { + @apply text-gray-700 hover:text-primary-600 transition-colors duration-200 dark:text-dark-text dark:hover:text-primary-400; + } + + .card { + @apply bg-white rounded-lg shadow-md p-6 dark:bg-dark-card dark:border dark:border-dark-border; + } + + .input { + @apply w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 + dark:bg-dark-card dark:border-dark-border dark:text-dark-text dark:focus:ring-primary-400; + } +} diff --git a/application/src/scouter/MatchList.tsx b/application/src/scouter/MatchList.tsx index 334bd4a..2028540 100644 --- a/application/src/scouter/MatchList.tsx +++ b/application/src/scouter/MatchList.tsx @@ -1,15 +1,15 @@ import { useLocation, useNavigate } from "react-router-dom"; import Collapsible from "react-collapsible"; import React, { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { Trash2, Send, ChevronDown } from "lucide-react"; import QRCodeGenerator from "../components/QRCode-Generator"; -import { fetchData, getServerHostname } from "../Utils"; +import { fetchData } from "../Utils"; import { renderScouterNavBar } from "../App"; import { matchFolder as matchesFolder } from "../utils/FolderStorage"; export const matchName = "Qual"; -const collapsibleSize = 10; - const MatchList: React.FC = () => { const location = useLocation(); const navigate = useNavigate(); @@ -41,7 +41,7 @@ const MatchList: React.FC = () => { function sendMatch(match: Record, index: number) { fetchData("Match") .then(() => { - alert("Succesfully Sent Match✅"); + alert("Successfully Sent Match✅"); removeMatch(match?.[matchName], index); }) .catch(() => { @@ -50,34 +50,76 @@ const MatchList: React.FC = () => { } return ( -
+
{renderScouterNavBar()} - {matches.length === 0 &&

No Matches Saved

} - {matches.map((match, index) => ( - + - -
- - -
- ))} +

Match List

+ {matches.length === 0 && ( + + No Matches Saved + + )} + + {matches.map((match, index) => ( + + + + {match["Team Number"]} {matchName} {match[matchName]} + + +
+ } + triggerClassName="w-full" + transitionTime={200} + > +
+
+ +
+
+ + +
+
+ + + ))} + + +
); }; + export default MatchList; diff --git a/application/src/scouter/ScouterQuery.tsx b/application/src/scouter/ScouterQuery.tsx index 327d15a..5266f50 100644 --- a/application/src/scouter/ScouterQuery.tsx +++ b/application/src/scouter/ScouterQuery.tsx @@ -5,6 +5,7 @@ export interface QueryProps { name: string; required?: boolean | undefined; defaultValue?: T; + hideLabel?: boolean; } abstract class ScouterQuery< @@ -27,7 +28,9 @@ abstract class ScouterQuery< render(): React.ReactNode { return (
-

{this.props.name}

+ {!this.props.hideLabel && ( +

{this.props.name}

+ )} {this.renderInput()}
); diff --git a/application/src/scouter/ScoutingTab.tsx b/application/src/scouter/ScoutingTab.tsx index 4f1a0a4..013cc08 100644 --- a/application/src/scouter/ScoutingTab.tsx +++ b/application/src/scouter/ScoutingTab.tsx @@ -1,5 +1,7 @@ import { useNavigate } from "react-router-dom"; import React, { useState } from "react"; +import { motion } from "framer-motion"; +import { ArrowLeft, ArrowRight, RotateCcw, Save } from "lucide-react"; import PreMatch from "./tabs/PreMatch"; import Autonomous from "./tabs/Autonomous"; import Teleoperated from "./tabs/Teleoperated"; @@ -9,9 +11,7 @@ import SureButton from "../components/SureButton"; import { queryFolder } from "../utils/FolderStorage"; const sections: React.FC[] = [PreMatch, Autonomous, Teleoperated, PostMatch]; - - -const constantValues = ["Scouter Name", "Game Side"] +const constantValues = ["Scouter Name", "Game Side"]; function ScouterTab() { const navigate = useNavigate(); @@ -20,19 +20,17 @@ function ScouterTab() { function handleSubmit() { const formValues: Record = {}; queryFolder.keys().forEach((item) => { - formValues[item] = - queryFolder.getItem(item) + ""; - if (!constantValues.includes(item)) { - queryFolder.removeItem(item); - } - }); + formValues[item] = queryFolder.getItem(item) + ""; + if (!constantValues.includes(item)) { + queryFolder.removeItem(item); + } + }); navigate("/", { state: formValues }); } function clearQueryStorage() { - queryFolder.keys() - .forEach((item) => queryFolder.removeItem(item)); + queryFolder.keys().forEach((item) => queryFolder.removeItem(item)); } function handleReset() { @@ -41,31 +39,79 @@ function ScouterTab() { } return ( -
+
{renderScouterNavBar()} -

{sections[currentSectionNumber].name}

- {sections[currentSectionNumber].apply({})} - {currentSectionNumber !== 0 && ( - - )} - {currentSectionNumber === sections.length - 1 ? ( - - ) : ( - - )} -
+
+

+ {sections[currentSectionNumber].name} +

+
+ + {currentSectionNumber !== 0 && ( + + )} + {currentSectionNumber !== sections.length - 1 && ( + + )} + +
+
+ + + {sections[currentSectionNumber].apply({})} + + +
+ + + Reset + + {currentSectionNumber === sections.length - 1 && ( + + )} +
+ +
); } diff --git a/application/src/scouter/querytypes/CounterQuery.tsx b/application/src/scouter/querytypes/CounterQuery.tsx index b5db1cb..8fc8307 100644 --- a/application/src/scouter/querytypes/CounterQuery.tsx +++ b/application/src/scouter/querytypes/CounterQuery.tsx @@ -1,4 +1,6 @@ import React from "react"; +import { motion } from "framer-motion"; +import { Minus, Plus } from "lucide-react"; import { queryFolder } from "../../utils/FolderStorage"; import ScouterQuery, { QueryProps } from "../ScouterQuery"; @@ -26,30 +28,48 @@ class CounterQuery extends ScouterQuery< this.setState({ count: newCount }); }; + const buttonBaseClass = `w-12 h-12 rounded-full flex items-center justify-center text-white transition-all duration-200 shadow-lg + hover:opacity-90 active:scale-95 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-dark-bg`; + return ( - <> + -

{this.state.count}

- + + + {this.state.count} + + + - +
); } } diff --git a/application/src/scouter/querytypes/MapQuery.tsx b/application/src/scouter/querytypes/MapQuery.tsx index 4b4f57b..8610f1a 100644 --- a/application/src/scouter/querytypes/MapQuery.tsx +++ b/application/src/scouter/querytypes/MapQuery.tsx @@ -3,6 +3,7 @@ import { Point } from "chart.js"; import React from "react"; import CounterQuery from "./CounterQuery"; import { queryFolder } from "../../utils/FolderStorage"; + interface MapQueryProps { name: string; side: "blue" | "red"; @@ -19,12 +20,14 @@ interface DataPoint extends Point { type PassingPoint = [DataPoint, Point]; const pointRadius: number = 5; +const arrowSize: number = 10; const succesfulnessOffset = [80, -60]; const crescendoButtons: Record = { Speaker: "green", - Pass: "purple", + Pass: "#8b5cf6", }; + const defaultButton = "Speaker"; const MapQuery: React.FC = ({ @@ -34,7 +37,8 @@ const MapQuery: React.FC = ({ imagePath, side, }) => { - const mapFolder = queryFolder.with(name + "/") + const mapFolder = queryFolder.with(name + "/"); + const [dataPoints, setDataPoints] = useState<(DataPoint | PassingPoint)[]>( JSON.parse(mapFolder.getItem("Points") || "[]") ); @@ -44,6 +48,9 @@ const MapQuery: React.FC = ({ const [passingPoint, setPassingPoint] = useState(); + const [showPassModal, setShowPassModal] = useState(false); + const [tempPassingLine, setTempPassingLine] = useState<{start: DataPoint, end: Point} | null>(null); + const canvasRef = useRef(null); const context = canvasRef.current ? canvasRef.current.getContext("2d") : null; @@ -63,13 +70,34 @@ const MapQuery: React.FC = ({ successfulness: successfulness, }; setLastClickedPoint(undefined); - if (pressedButton === "Pass" && successfulness) { - setPassingPoint(clickedPoint); + + if (pressedButton === "Pass") { + if (!passingPoint) { + setPassingPoint(clickedPoint); + } else { + // Store the passing line temporarily and show the success/miss modal + setTempPassingLine({ start: passingPoint, end: point }); + setShowPassModal(true); + setPassingPoint(undefined); + } return; } + setDataPoints((prev) => [...prev, clickedPoint]); } + function handlePassResult(success: boolean) { + if (tempPassingLine) { + const passLine: PassingPoint = [ + { ...tempPassingLine.start, successfulness: success }, + tempPassingLine.end + ]; + setDataPoints((prev) => [...prev, passLine]); + setTempPassingLine(null); + } + setShowPassModal(false); + } + function removeLastPoint() { dataPoints.pop(); setDataPoints((prev) => { @@ -78,22 +106,66 @@ const MapQuery: React.FC = ({ }); } + function drawArrow(fromx: number, fromy: number, tox: number, toy: number) { + if (!context) return; + + const headlen = arrowSize; + const dx = tox - fromx; + const dy = toy - fromy; + const angle = Math.atan2(dy, dx); + + // Draw the main line + context.beginPath(); + context.moveTo(fromx, fromy); + context.lineTo(tox, toy); + context.stroke(); + + // Draw the arrow head + context.beginPath(); + context.moveTo(tox, toy); + context.lineTo( + tox - headlen * Math.cos(angle - Math.PI / 6), + toy - headlen * Math.sin(angle - Math.PI / 6) + ); + context.moveTo(tox, toy); + context.lineTo( + tox - headlen * Math.cos(angle + Math.PI / 6), + toy - headlen * Math.sin(angle + Math.PI / 6) + ); + context.stroke(); + } + function drawPoints() { if (!context) { return; } context.clearRect(0, 0, width, height); + // Draw temporary pass line if in passing mode + if (passingPoint && lastClickedPoint) { + context.strokeStyle = crescendoButtons["Pass"]; + context.lineWidth = pointRadius; + drawArrow( + passingPoint.x, + passingPoint.y, + lastClickedPoint.x, + lastClickedPoint.y + ); + + // Draw highlight around start point + context.fillStyle = "rgba(139, 92, 246, 0.3)"; // Semi-transparent purple + context.beginPath(); + context.arc(passingPoint.x, passingPoint.y, pointRadius * 2, 0, 2 * Math.PI); + context.fill(); + } + for (let point of dataPoints) { const isPassing = (point as DataPoint).x === undefined; if (isPassing) { const [startPoint, endPoint] = point as PassingPoint; - context.strokeStyle = crescendoButtons["Pass"]; - context.beginPath(); - context.moveTo(startPoint.x, startPoint.y); - context.lineTo(endPoint.x, endPoint.y); + context.strokeStyle = startPoint.successfulness ? crescendoButtons["Pass"] : "#ff1493"; // Purple for success, pink for miss context.lineWidth = pointRadius; - context.stroke(); + drawArrow(startPoint.x, startPoint.y, endPoint.x, endPoint.y); } else { point = point as DataPoint; if (point.data === "Speaker" && !point.successfulness) { @@ -119,85 +191,31 @@ const MapQuery: React.FC = ({ }; if (passingPoint) { - setDataPoints((prev) => [...prev, [passingPoint, clickedPoint]]); + // Draw final pass line with arrow + setTempPassingLine({ start: passingPoint, end: clickedPoint }); + setShowPassModal(true); setPassingPoint(undefined); - setPressedButton(defaultButton); + return; + } + if (pressedButton === "Pass") { + const newPassingPoint: DataPoint = { + x: clickedPoint.x, + y: clickedPoint.y, + data: pressedButton, + successfulness: true, + }; + setPassingPoint(newPassingPoint); return; } + setLastClickedPoint(clickedPoint); } useEffect(() => { mapFolder.setItem("Points", JSON.stringify(dataPoints)); drawPoints(); - }, [dataPoints, addPoint]); - - const dataOptions = ( -
-
-
- {passingPoint ? ( -
- Set Passing Destination -
- -
- ) : ( -
- {Object.entries(crescendoButtons).map((option, index) => { - const buttonName = option[0]; - if (buttonName === pressedButton) { - return ( - - setPressedButton(buttonName)} - defaultChecked - className="cool-radio-input" - /> - - - ); - } - return ( - - setPressedButton(buttonName)} - className="cool-radio-input" - /> - - - ); - })} -
-
- -
- )} -
- ); - - const ampOptions = ( -
-

AMP

-
- -
- -
- ); + }, [dataPoints]); function getSuccesfulness(offsetLeft: number, offsetTop: number) { if (!lastClickedPoint) { @@ -205,7 +223,7 @@ const MapQuery: React.FC = ({ } return (
= ({ : 0, }} > - - -
); } + const ampOptions = ( +
+

AMP

+
+
+
CRESCENDO/Amp/Score
+
+ +
+
+
+
CRESCENDO/Amp/Miss
+
+ +
+
+
+
+ ); + + const dataOptions = ( +
+ {passingPoint ? ( +
+
Drawing Pass Line
+
Click on the field to set the pass destination
+ +
+ ) : ( +
+
+ {Object.entries(crescendoButtons).map((option, index) => { + const [buttonName, color] = option; + const isSelected = buttonName === pressedButton; + return ( + + ); + })} +
+ +
+ )} +
+ ); + return ( <>
@@ -266,6 +375,25 @@ const MapQuery: React.FC = ({ name={name} value={JSON.stringify(dataPoints)} /> + {/* Pass Success/Miss Modal */} + {showPassModal && ( +
+
+ + +
+
+ )}
{canvasRef.current && lastClickedPoint && @@ -277,4 +405,5 @@ const MapQuery: React.FC = ({ ); }; + export default MapQuery; diff --git a/application/src/scouter/querytypes/PassQuery.tsx b/application/src/scouter/querytypes/PassQuery.tsx new file mode 100644 index 0000000..3f927bd --- /dev/null +++ b/application/src/scouter/querytypes/PassQuery.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { Check, X } from "lucide-react"; + +interface PassQueryProps { + name: string; + label?: string; +} + +const PassQuery: React.FC = ({ name, label }) => { + const [attempts, setAttempts] = React.useState<{ success: number; miss: number }>({ + success: 0, + miss: 0, + }); + + return ( +
+ {label &&

{label}

} +
+ {/* Success Button */} + setAttempts(prev => ({ ...prev, success: prev.success + 1 }))} + className="flex-1 flex items-center justify-center gap-2 btn btn-primary" + > + + Success ({attempts.success}) + + + {/* Miss Button */} + setAttempts(prev => ({ ...prev, miss: prev.miss + 1 }))} + className="flex-1 flex items-center justify-center gap-2 btn btn-secondary" + > + + Miss ({attempts.miss}) + +
+
+ ); +}; + +export default PassQuery; diff --git a/application/src/scouter/tabs/Autonomous.tsx b/application/src/scouter/tabs/Autonomous.tsx index 776e96b..b34dadd 100644 --- a/application/src/scouter/tabs/Autonomous.tsx +++ b/application/src/scouter/tabs/Autonomous.tsx @@ -1,27 +1,52 @@ -import React, { useRef } from "react"; +import React from "react"; +import { motion } from "framer-motion"; +import { Target, XCircle } from "lucide-react"; import CounterQuery from "../querytypes/CounterQuery"; + interface AutonomousProps {} const Autonomous: React.FC = () => { return ( - <> -
-
-

SCORE

-
- - -
-
-

MISS

-
+ +
+ {/* Score Section */} + +
+ +

Speaker Score

+
+
+ +
+
- -
+ {/* Miss Section */} + +
+ +

Speaker Miss

+
+
+ +
+
-
- - + ); }; diff --git a/application/src/scouter/tabs/Teleoperated.tsx b/application/src/scouter/tabs/Teleoperated.tsx index a3885ba..b70fb4a 100644 --- a/application/src/scouter/tabs/Teleoperated.tsx +++ b/application/src/scouter/tabs/Teleoperated.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { motion } from "framer-motion"; import MapQuery from "../querytypes/MapQuery"; import { queryFolder } from "../../utils/FolderStorage"; @@ -6,17 +7,30 @@ interface TeleoperatedProps {} const Teleoperated: React.FC = () => { return ( - <> - - + +
+ {/* Map Section */} + + + +
+
); }; diff --git a/application/tailwind.config.js b/application/tailwind.config.js new file mode 100644 index 0000000..926ee41 --- /dev/null +++ b/application/tailwind.config.js @@ -0,0 +1,48 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + darkMode: 'class', + theme: { + extend: { + colors: { + primary: { + 50: '#f0f9ff', + 100: '#e0f2fe', + 200: '#bae6fd', + 300: '#7dd3fc', + 400: '#38bdf8', + 500: '#0ea5e9', + 600: '#0284c7', + 700: '#0369a1', + 800: '#075985', + 900: '#0c4a6e', + }, + dark: { + bg: '#111827', + card: '#1F2937', + border: '#374151', + text: '#F9FAFB', + 'text-secondary': '#9CA3AF', + } + }, + animation: { + 'fade-in': 'fadeIn 0.5s ease-in-out', + 'slide-in': 'slideIn 0.5s ease-out', + }, + keyframes: { + fadeIn: { + '0%': { opacity: '0' }, + '100%': { opacity: '1' }, + }, + slideIn: { + '0%': { transform: 'translateX(-100%)' }, + '100%': { transform: 'translateX(0)' }, + }, + }, + }, + }, + plugins: [], +} diff --git a/application/vite.config.ts b/application/vite.config.ts index 9cc50ea..70c295e 100644 --- a/application/vite.config.ts +++ b/application/vite.config.ts @@ -4,4 +4,7 @@ import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + server: { + host: true + } }); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..44b1c16 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "GBScouting", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}