diff --git a/apps/scouting/backend/build.ts b/apps/scouting/backend/build.ts index 0f4439e..3890f2a 100644 --- a/apps/scouting/backend/build.ts +++ b/apps/scouting/backend/build.ts @@ -2,7 +2,7 @@ import { build, context } from "esbuild"; import { spawn } from "child_process"; -const isDev = process.env.NODE_ENV === "DEV"; +const isDev = process.env.NODE_ENV !== "DEV"; const bundlePath = "dist/bundle.js"; diff --git a/apps/scouting/backend/src/routes/index.ts b/apps/scouting/backend/src/routes/index.ts index ee6a9c0..d72b133 100644 --- a/apps/scouting/backend/src/routes/index.ts +++ b/apps/scouting/backend/src/routes/index.ts @@ -12,6 +12,7 @@ import { compareRouter } from "./compare-router"; import { tinderRouter } from "./tinder-router"; import { superScoutRouter } from "./super-scout-router"; import { picklistRouter } from "./picklist-router"; +import { pitScoutRouter } from "./pit-scout-router"; export const apiRouter = Router(); @@ -26,6 +27,7 @@ apiRouter.use("/compare", compareRouter); apiRouter.use("/tinder", tinderRouter); apiRouter.use("/super", superScoutRouter); apiRouter.use("/picklist", picklistRouter); +apiRouter.use("/pit", pitScoutRouter); apiRouter.get("/health", (req, res) => { res.status(StatusCodes.OK).send({ message: "Healthy!" }); diff --git a/apps/scouting/backend/src/routes/pit-scout-router.ts b/apps/scouting/backend/src/routes/pit-scout-router.ts new file mode 100644 index 0000000..ab4eac4 --- /dev/null +++ b/apps/scouting/backend/src/routes/pit-scout-router.ts @@ -0,0 +1,49 @@ +//בס"ד + +import { Router } from "express"; +import { flow, pipe } from "fp-ts/lib/function"; +import { getDb } from "../middleware/db"; +import { bind, bindTo, fromEither, map } from "fp-ts/lib/TaskEither"; +import { + createBodyVerificationPipe, + flatTryCatch, + foldResponse, +} from "@repo/flow-utils"; +import { right as rightEither } from "fp-ts/lib/Either"; +import { mongofyQuery } from "@repo/flow-utils"; +import { PitScout, pitScoutCodec } from "@repo/scouting_types"; +import { StatusCodes } from "http-status-codes"; + +export const pitScoutRouter = Router(); + +export const getPitCollection = flow( + getDb, + map((db) => db.collection("pit")), +); + +pitScoutRouter.post("/", async (req, res) => { + await pipe( + rightEither(req), + createBodyVerificationPipe(pitScoutCodec), + fromEither, + bindTo("pitScout"), + bind("collection", getPitCollection), + map(({ pitScout, collection }) => collection.insertOne(pitScout)), + foldResponse(res), + )(); +}); + +pitScoutRouter.get("/", async (req, res) => { + await flow( + getPitCollection, + flatTryCatch( + (collection) => collection.find(mongofyQuery(req.query)).toArray(), + () => ({ + status: StatusCodes.INTERNAL_SERVER_ERROR, + reason: "Error Inserting Forms To Collection ", + }), + ), + bindTo("forms"), + foldResponse(res), + )(); +}); diff --git a/apps/scouting/frontend/src/App.tsx b/apps/scouting/frontend/src/App.tsx index bd14466..150af98 100644 --- a/apps/scouting/frontend/src/App.tsx +++ b/apps/scouting/frontend/src/App.tsx @@ -13,6 +13,7 @@ import { CURRENT_COMPETITION } from "@repo/scouting_types"; import { StrategyNavigationBar } from "./strategy/components/StrategyNavBar"; import { SuperScoutTab } from "./strategy/tabs/super-scout/SuperScoutTab"; import { Tinder } from "./strategy/tabs/Tinder"; +import { PitScoutTab } from "./strategy/tabs/pit-scout/PitScoutTab"; const App: FC = () => { return ( @@ -30,6 +31,7 @@ const App: FC = () => { } /> } /> } /> + } /> } /> diff --git a/apps/scouting/frontend/src/strategy/components/MetricsChart.tsx b/apps/scouting/frontend/src/strategy/components/MetricsChart.tsx index b04c05e..0b79894 100644 --- a/apps/scouting/frontend/src/strategy/components/MetricsChart.tsx +++ b/apps/scouting/frontend/src/strategy/components/MetricsChart.tsx @@ -6,7 +6,7 @@ import type { TeamData } from "@repo/scouting_types"; const NUMBER_OF_DIGITS = 2; const Metric: FC<{ name: string; - value: number; + value?: number; colors: string; onClick?: () => void; }> = ({ name, value, colors, onClick }) => ( @@ -19,7 +19,7 @@ const Metric: FC<{ {name} - {value.toFixed(NUMBER_OF_DIGITS)} + {value?.toFixed(NUMBER_OF_DIGITS)} ); diff --git a/apps/scouting/frontend/src/strategy/tabs/pit-scout/BooleanStats.tsx b/apps/scouting/frontend/src/strategy/tabs/pit-scout/BooleanStats.tsx new file mode 100644 index 0000000..1b64aa2 --- /dev/null +++ b/apps/scouting/frontend/src/strategy/tabs/pit-scout/BooleanStats.tsx @@ -0,0 +1,53 @@ +//בס"ד + +import type { FC } from "react"; +import type { + PitScout, + PitScoutBoolean, + PitScoutBooleanKey, + PitScoutBooleanMetric, +} from "@repo/scouting_types"; + +interface BooleanStatsProps { + statKey: PitScoutBooleanKey; + label: string; + form: PitScout; + setBoolForm: (key: PitScoutBooleanKey, value: PitScoutBooleanMetric) => void; +} + +export const BooleanStats: FC = ({ + statKey, + label, + form, + setBoolForm, +}) => { + return ( +
+ + {label} + +
+ + +
+
+ ); +}; diff --git a/apps/scouting/frontend/src/strategy/tabs/pit-scout/NumberStats.tsx b/apps/scouting/frontend/src/strategy/tabs/pit-scout/NumberStats.tsx new file mode 100644 index 0000000..6531d87 --- /dev/null +++ b/apps/scouting/frontend/src/strategy/tabs/pit-scout/NumberStats.tsx @@ -0,0 +1,44 @@ +//בס"ד + +import type { + PitScout, + PitScoutNumberKey, + SuperScout, +} from "@repo/scouting_types"; +import type { FC } from "react"; + +interface NumberStatsProps { + statKey: PitScoutNumberKey; + label: string; + placeholder: string; + form: PitScout; + setNumberForm: (key: PitScoutNumberKey, value: number) => void; +} + +export const NumberStats: FC = ({ + statKey, + label, + placeholder, + form, + setNumberForm, +}) => { + return ( +
+ + + setNumberForm(statKey, parseFloat(event.target.value)) + } + placeholder={placeholder} + /> +
+ ); +}; diff --git a/apps/scouting/frontend/src/strategy/tabs/pit-scout/PitScoutTab.tsx b/apps/scouting/frontend/src/strategy/tabs/pit-scout/PitScoutTab.tsx new file mode 100644 index 0000000..30b5529 --- /dev/null +++ b/apps/scouting/frontend/src/strategy/tabs/pit-scout/PitScoutTab.tsx @@ -0,0 +1,191 @@ +//בס"ד + +import { useState, type FC } from "react"; +import type { + PitScout, + PitScoutBoolean, + PitScoutBooleanKey, + PitScoutBooleanMetric, + PitScoutNumber, + PitScoutNumberKey, +} from "@repo/scouting_types"; +import { NumberStats } from "./NumberStats"; +import { BooleanStats } from "./BooleanStats"; + +const NUMBER_FIELDS: { + statKey: PitScoutNumberKey; + label: string; + placeholder: string; +}[] = [ + { + statKey: "robotWeight", + label: "Robot Weight (lbs)", + placeholder: "e.g. 120", + }, + { statKey: "ballCapacity", label: "Ball Capacity", placeholder: "e.g. 50" }, +]; + +const PIT_SCOUT_URL = "/api/v1/pit/"; + +const BOOLEAN_FIELDS: { statKey: PitScoutBooleanKey; label: string }[] = [ + { statKey: "hasTurret", label: "Has turret?" }, + { statKey: "canPassTrench", label: "Can pass trench?" }, + { statKey: "canPassBumpEasily", label: "Can pass bump easily?" }, +]; + +const initialState: PitScout = { + teamNumber: 0, + numberMetrics: { robotWeight: undefined, ballCapacity: undefined }, + booleanMetrics: { + hasTurret: undefined, + canPassTrench: undefined, + canPassBumpEasily: undefined, + }, + extraInfo: undefined, +}; + +export const PitScoutTab: FC = () => { + const [form, setForm] = useState(initialState); + const [status, setStatus] = useState< + "idle" | "loading" | "success" | "error" + >("idle"); + const [errorMsg, setErrorMsg] = useState(""); + + const setNumberForm = (key: PitScoutNumberKey, value: number) => + setForm((form) => ({ + ...form, + numberMetrics: { ...form.numberMetrics, [key]: value || undefined }, + })); + const setBoolForm = (key: PitScoutBooleanKey, value: PitScoutBooleanMetric) => + setForm((form) => ({ + ...form, + booleanMetrics: { + ...form.booleanMetrics, + [key]: form.booleanMetrics[key] === value ? undefined : value, + }, + })); + + const setExtraForm = (value: string) => + setForm((form) => ({ ...form, extraInfo: value || undefined })); + + const handleSubmit = async () => { + if (!form.teamNumber) { + setStatus("error"); + setErrorMsg("Team number is required."); + return; + } + + setStatus("loading"); + try { + const res = await fetch(PIT_SCOUT_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(form), + }); + + if (res.ok) { + setStatus("success"); + setForm(initialState); + } else { + const text = await res.text(); + setStatus("error"); + setErrorMsg(text || "Submission failed."); + } + } catch (error) { + setStatus("error"); + setErrorMsg(error instanceof Error ? error.message : "Network error."); + } + }; + + return ( +
+
+

+ Team Identification +

+
+ + + setForm((form) => ({ + ...form, + teamNumber: parseInt(event.target.value) || 0, + })) + } + placeholder="0000" + /> +
+
+ +
+ {NUMBER_FIELDS.map(({ statKey: key, label, placeholder }) => ( + + ))} +
+ +
+

+ Mechanical Capabilities +

+
+ {BOOLEAN_FIELDS.map(({ statKey: key, label }) => ( + + ))} +
+
+ +
+

+ extra information +

+