diff --git a/.gitignore b/.gitignore index 445a7479..152b88ed 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,7 @@ yarn-error.log* /certs/production.pem /certs/production-key.pem - +.env.*.local .env.local .env diff --git a/components/SignInMenu.tsx b/components/SignInMenu.tsx index 66d17c34..148db538 100644 --- a/components/SignInMenu.tsx +++ b/components/SignInMenu.tsx @@ -1,4 +1,5 @@ import Container from "@/components/Container"; +import { useCurrentSession } from "@/lib/client/useCurrentSession"; import { signIn } from "next-auth/react"; import { useRouter } from "next/router"; import { useCallback, useEffect, useRef, useState } from "react"; @@ -14,6 +15,8 @@ const errorMessages: { [error: string]: string } = { }; function SignInCard() { + const { status, session } = useCurrentSession(); + const router = useRouter(); const emailRef = useRef(null); const { executeRecaptcha } = useGoogleReCaptcha(); @@ -57,49 +60,58 @@ function SignInCard() { return (
-

Sign In

- {error &&

{error}

} -

Choose a login provider

- - You currently have to sign-in - using either your{" "} - original sign-in method or - your email. - -
- - - - - -
-
-

Email Sign In

- - -
+ {status === "authenticated" ? ( + <> +

Welcome back, {session.user!.name}

+ + + ) : ( + <> +

Sign In

+ {error &&

{error}

} +

Choose a login provider

+ + You currently have to + sign-in using either your{" "} + original sign-in method or + your email. + +
+ + + + + +
+
+

Email Sign In

+ + +
+ + )}
); diff --git a/components/competition/CompHeaderCard.tsx b/components/competition/CompHeaderCard.tsx index 012fd8ca..11802934 100644 --- a/components/competition/CompHeaderCard.tsx +++ b/components/competition/CompHeaderCard.tsx @@ -1,11 +1,22 @@ +import ClientApi from "@/lib/api/ClientApi"; import { NotLinkedToTba } from "@/lib/client/ClientUtils"; import { Competition, Match, Report } from "@/lib/Types"; import { useState } from "react"; import { BiExport } from "react-icons/bi"; + import { FaCalendarDay } from "react-icons/fa"; -import { MdAutoGraph, MdQueryStats, MdCoPresent } from "react-icons/md"; +import { + MdAutoGraph, + MdQueryStats, + MdCoPresent, + MdCloudSync, +} from "react-icons/md"; import ViewMatchesModal from "../ViewMatchesModal"; import { User } from "../../lib/Types"; +import toast from "react-hot-toast"; +import { syncCompData } from "@/lib/api/ClientApiUtils"; + +const api = new ClientApi(); export default function CompHeaderCard({ comp, @@ -21,16 +32,73 @@ export default function CompHeaderCard({ matchPathway: string; }) { const [viewMatches, setViewMatches] = useState(false); + const [syncingOfflineData, setSyncingOfflineData] = useState(false); async function toggleViewMatches() { setViewMatches(!viewMatches); } + async function syncComp() { + if (!comp) return; + + const toastId = toast.loading("Caching offline pages..."); + setSyncingOfflineData(true); + new Promise(async (resolve, reject) => { + await syncCompData(api, comp._id!.toString()); + + const totalItemsToSync = comp?.pitReports.length || 0; + let itemsSynced = 0; + await Promise.all( + comp?.pitReports.map(async (report) => { + await fetch(`${location.href}/pit/${report}`); + itemsSynced++; + toast.loading( + `Caching offline pages... (${itemsSynced}/${totalItemsToSync})`, + { + id: toastId, + }, + ); + }), + ); + + console.log("Cached all offline pages!"); + toast.success("Cached all offline pages!", { id: toastId }); + + // Finally block doesn't run for some reason + setSyncingOfflineData(false); + resolve(true); + }) + .catch((err) => { + toast.error(`Error syncing offline data. Error: ${err}`, { + id: toastId, + }); + + // Finally block doesn't run for some reason + setSyncingOfflineData(false); + }) + .finally(() => setSyncingOfflineData(false)); + } + return (

{comp?.name}

+
+ {syncingOfflineData ? ( + + ) : ( + + )} +
diff --git a/components/competition/MatchScheduleCard.tsx b/components/competition/MatchScheduleCard.tsx index 5e3a5bbc..0e374d9b 100644 --- a/components/competition/MatchScheduleCard.tsx +++ b/components/competition/MatchScheduleCard.tsx @@ -7,14 +7,209 @@ import { Team, User, } from "@/lib/Types"; -import Link from "next/link"; import { BsGearFill } from "react-icons/bs"; import { FaSync, FaCheck, FaInfoCircle } from "react-icons/fa"; import { MdErrorOutline } from "react-icons/md"; import Avatar from "../Avatar"; import Loading from "../Loading"; import { AdvancedSession } from "@/lib/client/useCurrentSession"; -import { useEffect } from "react"; + +function MatchCard({ + match, + index, + isManager, + openEditMatchModal, + remindUserOnSlack, + session, + team, + seasonSlug, + compSlug, + reportsById, + usersById, +}: { + match: Match; + index: number; + isManager: boolean | undefined; + openEditMatchModal: (match: Match) => void; + remindUserOnSlack: (userId: string) => void; + session: AdvancedSession; + team: Team | undefined; + seasonSlug: string | undefined; + compSlug: string | undefined; + reportsById: { [id: string]: Report }; + usersById: { [id: string]: User }; +}) { + return ( +
+
+
+

Match {match.number}

+ {isManager && ( + + )} +
+
+
+ {match.reports.map((reportId) => { + const report = reportsById[reportId]; + + if (!report) + return ( + + ); + + const submitted = report.submitted; + const mine = session && report.user === session.user?._id; + const ours = report.robotNumber === team?.number; + let color = !submitted + ? report.color === AllianceColor.Red + ? "bg-red-500" + : "bg-blue-500" + : "bg-slate-500"; + color = ours + ? !report.submitted + ? "bg-purple-500" + : "bg-purple-300" + : color; + + const timeSinceCheckIn = + report.checkInTimestamp && + (new Date().getTime() - + new Date(report.checkInTimestamp as any).getTime()) / + 1000; + + return ( + +

{report.robotNumber}

+
+ ); + })} +
+ +
+ {match.reports.map((reportId) => { + const report = reportsById[reportId]; + const user = usersById[report?.user!]; + + return ( +
+ {user ? ( + remindUserOnSlack(user._id!)} + gearSize={25} + /> + ) : ( +
+ )} +
+ ); + })} +
+
+
+ {match.subjectiveScouter && usersById[match.subjectiveScouter] ? ( +
+ {match.assignedSubjectiveScouterHasSubmitted ? ( + + ) : ( + match.subjectiveReportsCheckInTimestamps && + getIdsInProgressFromTimestamps( + match.subjectiveReportsCheckInTimestamps, + ).includes(match.subjectiveScouter) && ( +
+ +
+ ) + )} + {isManager && usersById[match.subjectiveScouter ?? ""]?.slackId ? ( + + ) : ( +
+ Subjective Scouter:{" "} + {usersById[match.subjectiveScouter ?? ""].name}{" "} +
+ )} +
+ ) : ( +
No subjective scouter assigned
+ )} +
+ +
+
+ + Add Subjective Report ( + {`${match.subjectiveReports ? match.subjectiveReports.length : 0} submitted, + ${ + match.subjectiveReportsCheckInTimestamps + ? getIdsInProgressFromTimestamps( + match.subjectiveReportsCheckInTimestamps, + ).length + : 0 + } in progress`} + ) + +
+ ); +} export default function MatchScheduleCard(props: { team: Team | undefined; @@ -127,186 +322,20 @@ export default function MatchScheduleCard(props: {
{displayedMatches.map((match, index) => ( -
-
-
-

- Match {match.number} -

- {isManager && ( - - )} -
-
-
- {match.reports.map((reportId) => { - const report = reportsById[reportId]; - - if (!report) - return ( - - ); - - const submitted = report.submitted; - const mine = - session && report.user === session.user?._id; - const ours = report.robotNumber === team?.number; - let color = !submitted - ? report.color === AllianceColor.Red - ? "bg-red-500" - : "bg-blue-500" - : "bg-slate-500"; - color = ours - ? !report.submitted - ? "bg-purple-500" - : "bg-purple-300" - : color; - - const timeSinceCheckIn = - report.checkInTimestamp && - (new Date().getTime() - - new Date( - report.checkInTimestamp as any, - ).getTime()) / - 1000; - - return ( - -

{report.robotNumber}

-
- ); - })} -
- -
- {match.reports.map((reportId) => { - const report = reportsById[reportId]; - const user = usersById[report?.user!]; - - return ( -
- {user ? ( - remindUserOnSlack(user._id!)} - gearSize={25} - /> - ) : ( -
- )} -
- ); - })} -
-
-
- {match.subjectiveScouter && - usersById[match.subjectiveScouter] ? ( -
- {match.assignedSubjectiveScouterHasSubmitted ? ( - - ) : ( - match.subjectiveReportsCheckInTimestamps && - getIdsInProgressFromTimestamps( - match.subjectiveReportsCheckInTimestamps, - ).includes(match.subjectiveScouter) && ( -
- -
- ) - )} - {isManager && - usersById[match.subjectiveScouter ?? ""] - ?.slackId ? ( - - ) : ( -
- Subjective Scouter:{" "} - { - usersById[match.subjectiveScouter ?? ""].name - }{" "} -
- )} -
- ) : ( -
No subjective scouter assigned
- )} -
- -
-
- - Add Subjective Report ( - {`${match.subjectiveReports ? match.subjectiveReports.length : 0} submitted, - ${ - match.subjectiveReportsCheckInTimestamps - ? getIdsInProgressFromTimestamps( - match.subjectiveReportsCheckInTimestamps, - ).length - : 0 - } in progress`} - ) - -
+ match={match} + index={index} + isManager={isManager} + openEditMatchModal={openEditMatchModal} + remindUserOnSlack={remindUserOnSlack} + session={session} + team={team} + seasonSlug={seasonSlug} + compSlug={comp?.slug} + reportsById={reportsById} + usersById={usersById} + /> ))}
diff --git a/components/forms/Checkboxes.tsx b/components/forms/Checkboxes.tsx index 26ac5cb7..d8130988 100644 --- a/components/forms/Checkboxes.tsx +++ b/components/forms/Checkboxes.tsx @@ -1,5 +1,4 @@ import { QuantData, Pitreport } from "@/lib/Types"; -import { Crescendo } from "@/lib/games"; export type CheckboxProps = { label: string; dataKey: string; diff --git a/components/stats/TeamPage.tsx b/components/stats/TeamPage.tsx index a05da6ea..8439726e 100644 --- a/components/stats/TeamPage.tsx +++ b/components/stats/TeamPage.tsx @@ -15,8 +15,7 @@ import Heatmap from "@/components/stats/Heatmap"; import TeamStats from "@/components/stats/TeamStats"; import Summary from "@/components/stats/Summary"; import SmallGraph from "@/components/stats/SmallGraph"; -import Loading from "../Loading"; -import { Crescendo, games } from "@/lib/games"; +import { games } from "@/lib/games"; import { GameId } from "@/lib/client/GameId"; import { FrcDrivetrain } from "@/lib/Enums"; diff --git a/lib/Auth.ts b/lib/Auth.ts index 813f972d..10efc0d8 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -136,13 +136,9 @@ export const AuthenticationOptions: AuthOptions = { (typedUser as User).lastSignInDateTime?.toDateString() !== today.toDateString() ) { - db.updateObjectById( - CollectionId.Users, - new ObjectId(typedUser._id?.toString()), - { - lastSignInDateTime: today, - }, - ); + db.updateObjectById(CollectionId.Users, new ObjectId(typedUser._id), { + lastSignInDateTime: today, + }); } new ResendUtils().createContact(typedUser as User); diff --git a/lib/MongoDB.ts b/lib/MongoDB.ts index f1ae7e1a..11c71cf8 100644 --- a/lib/MongoDB.ts +++ b/lib/MongoDB.ts @@ -10,7 +10,7 @@ import DbInterface, { import { default as BaseMongoDbInterface } from "mongo-anywhere/MongoDbInterface"; import CachedDbInterface from "./client/dbinterfaces/CachedDbInterface"; import { cacheOptions } from "./client/dbinterfaces/CachedDbInterface"; -import { findObjectBySlugLookUp } from "./slugToId"; +import findObjectBySlugLookUp from "./slugToId"; import { loadEnvConfig } from "@next/env"; let uri = process.env.MONGODB_URI ?? process.env.FALLBACK_MONGODB_URI; diff --git a/lib/api/AccessLevels.ts b/lib/api/AccessLevels.ts index df6e5039..75dbd821 100644 --- a/lib/api/AccessLevels.ts +++ b/lib/api/AccessLevels.ts @@ -161,6 +161,35 @@ namespace AccessLevels { }; } + export async function IfSeasonOwnerBySlug( + req: NextApiRequest, + res: NextResponse, + { userPromise, db }: UserAndDb, + seasonSlug: string, + ) { + const user = await userPromise; + if (!user) { + return { authorized: false, authData: undefined }; + } + + const season = await ( + await db + ).findObjectBySlug(CollectionId.Seasons, seasonSlug); + if (!season) { + return { authorized: false, authData: undefined }; + } + + const team = await getTeamFromSeason(await db, season); + if (!team) { + return { authorized: false, authData: undefined }; + } + + return { + authorized: team.owners.includes(user._id?.toString()!), + authData: { team, season }, + }; + } + export async function IfMatchOwner( req: NextApiRequest, res: NextResponse, diff --git a/lib/api/ClientApi.ts b/lib/api/ClientApi.ts index e3d5c45b..0af9c371 100644 --- a/lib/api/ClientApi.ts +++ b/lib/api/ClientApi.ts @@ -13,7 +13,6 @@ import { Pitreport, QuantData, Season, - SubjectiveReport, SubjectiveReportSubmissionType, Team, User, @@ -31,6 +30,11 @@ import { deleteComp, deleteSeason, generatePitReports, + getSeasonFromComp, + getTeamFromComp, + getTeamFromMatch, + getTeamFromReport, + getTeamFromSeason, onTeam, ownsTeam, } from "./ApiUtils"; @@ -42,25 +46,31 @@ import { assignScoutersToCompetitionMatches, generateReportsForMatch, } from "../CompetitionHandling"; -import { CenterStage, Crescendo, games, IntoTheDeep } from "../games"; +import { games } from "../games"; import { Statbotics } from "../Statbotics"; import { TheBlueAlliance } from "../TheBlueAlliance"; import { SlackNotLinkedError } from "./Errors"; import { _id } from "@next-auth/mongodb-adapter"; -import toast from "react-hot-toast"; -import { RequestHelper } from "unified-api"; import { createNextRoute, NextApiTemplate } from "unified-api-nextjs"; -import { Report } from "../Types"; +import { Report, SubjectiveReport } from "../Types"; import Logger from "../client/Logger"; - -const requestHelper = new RequestHelper( - process.env.NEXT_PUBLIC_API_URL ?? "", // Replace undefined when env is not present (ex: for testing builds) - (url, error) => { - const msg = `Failed API request: ${url}. Details: ${error}`; - console.error(msg); - toast.error(msg); - }, -); +import LocalStorageDbInterface from "../client/dbinterfaces/LocalStorageDbInterface"; +import findObjectBySlugLookUp, { slugToId } from "../slugToId"; +import GearboxRequestHelper from "./GearboxRequestHelper"; +import LocalApiDependencies from "./LocalApiDependencies"; +import { match } from "assert"; +import { + findObjectByIdFallback, + findObjectBySlugFallback, + saveObjectAfterResponse, + ShouldOverwrite, +} from "./ClientApiUtils"; +import CenterStage from "../games/CenterStage"; +import Crescendo from "../games/Crescendo"; +import IntoTheDeep from "../games/IntoTheDeep"; +import Reefscape from "../games/Reefscape"; + +const requestHelper = new GearboxRequestHelper(); const logger = new Logger(["API"]); @@ -633,7 +643,8 @@ export default class ClientApi extends NextApiTemplate { [string, boolean, boolean], { quantReports: Report[]; pitReports: { [team: number]: Pitreport[] } }, ApiDependencies, - { team: Team; comp: Competition } + { team: Team; comp: Competition }, + LocalApiDependencies >({ isAuthorized: (req, res, deps, [compId]) => AccessLevels.IfOnTeamThatOwnsComp(req, res, deps, compId), @@ -703,13 +714,100 @@ export default class ClientApi extends NextApiTemplate { pitReports: pitReports, }); }, + fallback: async ({ dbPromise }, [compId, submitted, usePublicData]) => { + const db = await dbPromise; + + const comp = await db.findObjectById( + CollectionId.Competitions, + new ObjectId(compId), + ); + + if (!comp) + return { + quantReports: [], + pitReports: {}, + }; + + const usedComps = + usePublicData && comp.tbaId !== NotLinkedToTba + ? await db.findObjects(CollectionId.Competitions, { + publicData: true, + tbaId: comp.tbaId, + gameId: comp.gameId, + }) + : []; + + usedComps.push(comp); + + const [reports, pitReports] = await Promise.all([ + ( + await db.findObjects(CollectionId.Reports, { + match: { $in: usedComps.flatMap((m) => m.matches) }, + submitted: submitted ? true : { $exists: true }, + }) + ) + // Filter out comments from other competitions + .map((report) => + comp.matches.includes(report.match) + ? report + : { ...report, data: { ...report.data, comments: "" } }, + ), + ( + await db.findObjects(CollectionId.PitReports, { + _id: { + $in: usedComps + .flatMap((m) => m.pitReports) + .map((id) => new ObjectId(id)), + }, + submitted: submitted ? true : { $exists: true }, + }) + ) + .map((pitReport) => + comp.pitReports.includes(pitReport._id!.toString()) + ? pitReport + : ({ + ...pitReport, + data: { ...pitReport.data, comments: "" }, + } as Pitreport), + ) + .reduce( + (dict, pitReport) => { + dict[pitReport.teamNumber] ??= []; + dict[pitReport.teamNumber].push(pitReport); + return dict; + }, + {} as { [team: number]: Pitreport[] }, + ), + ]); + + return { + quantReports: reports, + pitReports: pitReports, + }; + }, + afterResponse: async (deps, res, ranFallback) => { + saveObjectAfterResponse( + deps, + CollectionId.Reports, + res.quantReports, + ranFallback, + ); + saveObjectAfterResponse( + deps, + CollectionId.PitReports, + Object.values(res.pitReports).flat(), + ranFallback, + ShouldOverwrite.PitReport, + ); + }, }); allCompetitionMatches = createNextRoute< [string], Match[], ApiDependencies, - { team: Team; comp: Competition } + { team: Team; comp: Competition }, + LocalApiDependencies >({ isAuthorized: (req, res, deps, [compId]) => AccessLevels.IfOnTeamThatOwnsComp(req, res, deps, compId), @@ -727,6 +825,24 @@ export default class ClientApi extends NextApiTemplate { }); return res.status(200).send(matches); }, + afterResponse: async (deps, res, ranFallback) => + saveObjectAfterResponse(deps, CollectionId.Matches, res, ranFallback), + fallback: async ({ dbPromise }, [compId]) => { + const db = await dbPromise; + + const comp = await db.findObjectById( + CollectionId.Competitions, + new ObjectId(compId), + ); + + if (!comp) return []; + + const matches = await db.findObjects(CollectionId.Matches, { + _id: { $in: comp.matches.map((matchId) => new ObjectId(matchId)) }, + }); + + return matches; + }, }); getTeamsAtComp = createNextRoute< @@ -1077,11 +1193,13 @@ export default class ClientApi extends NextApiTemplate { Crescendo.QuantitativeData, CenterStage.QuantitativeData, IntoTheDeep.QuantitativeData, + Reefscape.QuantitativeData, ]; const pitReportTypes = [ Crescendo.PitData, CenterStage.PitData, IntoTheDeep.PitData, + Reefscape.PitData, ]; const dataPointsPerReport = @@ -1304,6 +1422,9 @@ export default class ClientApi extends NextApiTemplate { max: rankings?.length, }); }, + fallback: () => { + return Promise.resolve({ place: "?", max: "?" }); + }, }); getPitReports = createNextRoute< @@ -1940,7 +2061,8 @@ export default class ClientApi extends NextApiTemplate { [string, Match[]], SubjectiveReport[], ApiDependencies, - { team: Team; comp: Competition } + { team: Team; comp: Competition }, + LocalApiDependencies >({ isAuthorized: (req, res, deps, [compId]) => AccessLevels.IfOnTeamThatOwnsComp(req, res, deps, compId), @@ -1973,6 +2095,21 @@ export default class ClientApi extends NextApiTemplate { return res.status(200).send(reports); }, + afterResponse: async (deps, reports, ranFallback) => + saveObjectAfterResponse( + deps, + CollectionId.SubjectiveReports, + reports, + ranFallback, + ), + fallback: async ({ dbPromise }, [compId, matches]) => { + const db = await dbPromise; + + const matchIds = matches.map((match) => match._id?.toString()); + return db.findObjects(CollectionId.SubjectiveReports, { + match: { $in: matchIds }, + }); + }, }); removeUserFromTeam = createNextRoute< @@ -2045,7 +2182,8 @@ export default class ClientApi extends NextApiTemplate { [string], User | undefined, ApiDependencies, - void + void, + LocalApiDependencies >({ isAuthorized: AccessLevels.AlwaysAuthorized, handler: async (req, res, { db: dbPromise }, authData, [id]) => { @@ -2056,13 +2194,16 @@ export default class ClientApi extends NextApiTemplate { ); return res.status(200).send(user); }, + afterResponse: async (deps, user, ranFallback) => + saveObjectAfterResponse(deps, CollectionId.Users, user, ranFallback), }); findBulkUsersById = createNextRoute< [string[]], User[], ApiDependencies, - void + void, + LocalApiDependencies >({ isAuthorized: AccessLevels.AlwaysAuthorized, handler: async (req, res, { db: dbPromise }, authData, [ids]) => { @@ -2072,13 +2213,18 @@ export default class ClientApi extends NextApiTemplate { }); return res.status(200).send(users); }, + afterResponse: async (deps, users, ranFallback) => + saveObjectAfterResponse(deps, CollectionId.Users, users, ranFallback), + fallback: async (deps, [ids]) => + findObjectByIdFallback(deps, CollectionId.Users, ids), }); findTeamById = createNextRoute< [string], Team | undefined, ApiDependencies, - void + void, + LocalApiDependencies >({ isAuthorized: AccessLevels.AlwaysAuthorized, handler: async (req, res, { db: dbPromise }, authData, [id]) => { @@ -2089,13 +2235,37 @@ export default class ClientApi extends NextApiTemplate { ); return res.status(200).send(team); }, + afterResponse: async (deps, team, ranFallback) => + saveObjectAfterResponse(deps, CollectionId.Teams, team, ranFallback), + fallback: async (deps, [id]) => + findObjectByIdFallback(deps, CollectionId.Teams, id), + }); + + findTeamBySlug = createNextRoute< + [string], + Team | undefined, + ApiDependencies, + void, + LocalApiDependencies + >({ + isAuthorized: AccessLevels.AlwaysAuthorized, + handler: async (req, res, { db: dbPromise }, authData, [slug]) => { + const db = await dbPromise; + const team = await db.findObjectBySlug(CollectionId.Teams, slug); + return res.status(200).send(team); + }, + afterResponse: async (deps, res, ranFallback) => + saveObjectAfterResponse(deps, CollectionId.Teams, res, ranFallback), + fallback: async (deps, [slug]) => + findObjectBySlugFallback(deps, CollectionId.Teams, slug), }); findTeamByNumberAndLeague = createNextRoute< [number, League], Team | undefined, ApiDependencies, - void + void, + LocalApiDependencies >({ isAuthorized: AccessLevels.AlwaysAuthorized, handler: async ( @@ -2119,13 +2289,31 @@ export default class ClientApi extends NextApiTemplate { return res.status(200).send(team); }, + afterResponse: async (deps, team, ranFallback) => + saveObjectAfterResponse(deps, CollectionId.Teams, team, ranFallback), + fallback: async ({ dbPromise }, [number, league]) => { + const db = await dbPromise; + + const query = + league === League.FRC + ? { + number: number, + $or: [{ league: league }, { tbaId: { $exists: true } }], + } + : { number: number, league: league }; + + const team = await db.findObject(CollectionId.Teams, query); + + return team; + }, }); findSeasonById = createNextRoute< [string], Season | undefined, ApiDependencies, - void + void, + LocalApiDependencies >({ isAuthorized: AccessLevels.AlwaysAuthorized, handler: async (req, res, { db: dbPromise }, authData, [id]) => { @@ -2136,13 +2324,18 @@ export default class ClientApi extends NextApiTemplate { ); return res.status(200).send(season); }, + afterResponse: async (deps, season, ranFallback) => + saveObjectAfterResponse(deps, CollectionId.Seasons, season, ranFallback), + fallback: async (deps, [id]) => + findObjectByIdFallback(deps, CollectionId.Seasons, id), }); findCompetitionById = createNextRoute< [string], Competition | undefined, ApiDependencies, - void + void, + LocalApiDependencies >({ isAuthorized: AccessLevels.AlwaysAuthorized, handler: async (req, res, { db: dbPromise }, authData, [id]) => { @@ -2153,13 +2346,23 @@ export default class ClientApi extends NextApiTemplate { ); return res.status(200).send(competition); }, + afterResponse: async (deps, competition, ranFallback) => + saveObjectAfterResponse( + deps, + CollectionId.Competitions, + competition, + ranFallback, + ), + fallback: async (deps, [id]) => + findObjectByIdFallback(deps, CollectionId.Competitions, id), }); findMatchById = createNextRoute< [string], Match | undefined, ApiDependencies, - void + void, + LocalApiDependencies >({ isAuthorized: AccessLevels.AlwaysAuthorized, handler: async (req, res, { db: dbPromise }, authData, [id]) => { @@ -2170,13 +2373,18 @@ export default class ClientApi extends NextApiTemplate { ); return res.status(200).send(match); }, + afterResponse: async (deps, match, ranFallback) => + saveObjectAfterResponse(deps, CollectionId.Matches, match, ranFallback), + fallback: async (deps, [id]) => + findObjectByIdFallback(deps, CollectionId.Matches, id), }); findReportById = createNextRoute< [string], Report | undefined, ApiDependencies, - void + void, + LocalApiDependencies >({ isAuthorized: AccessLevels.AlwaysAuthorized, handler: async (req, res, { db: dbPromise }, authData, [id]) => { @@ -2187,26 +2395,41 @@ export default class ClientApi extends NextApiTemplate { ); return res.status(200).send(report); }, + afterResponse: async (deps, report, ranFallback) => + saveObjectAfterResponse(deps, CollectionId.Reports, report, ranFallback), + fallback: async (deps, [id]) => + findObjectByIdFallback(deps, CollectionId.Reports, id), }); findPitreportById = createNextRoute< [string], Pitreport | undefined, ApiDependencies, - { team: Team; comp: Competition; pitReport: Pitreport } + { team: Team; comp: Competition; pitReport: Pitreport }, + LocalApiDependencies >({ isAuthorized: (req, res, deps, [id]) => AccessLevels.IfOnTeamThatOwnsPitReport(req, res, deps, id), handler: async (req, res, deps, { pitReport }, args) => { return res.status(200).send(pitReport); }, + afterResponse: async (deps, pitReport, ranFallback) => + saveObjectAfterResponse( + deps, + CollectionId.PitReports, + pitReport, + ranFallback, + ), + fallback: async (deps, [id]) => + findObjectByIdFallback(deps, CollectionId.PitReports, id), }); findBulkPitReportsById = createNextRoute< [string[]], Pitreport[], ApiDependencies, - { team: Team; comp: Competition; pitReport: Pitreport }[] + { team: Team; comp: Competition; pitReport: Pitreport }[], + LocalApiDependencies >({ isAuthorized: async (req, res, deps, [ids]) => { const reports = await Promise.all( @@ -2223,6 +2446,16 @@ export default class ClientApi extends NextApiTemplate { handler: async (req, res, deps, authData, args) => { return res.status(200).send(authData.map((report) => report.pitReport)); }, + afterResponse: async (deps, res, ranFallback) => + saveObjectAfterResponse( + deps, + CollectionId.PitReports, + res, + ranFallback, + ShouldOverwrite.PitReport, + ), + fallback: async (deps, [pitReportIds]) => + findObjectByIdFallback(deps, CollectionId.PitReports, pitReportIds), }); updateUser = createNextRoute< @@ -2333,7 +2566,8 @@ export default class ClientApi extends NextApiTemplate { [string, object], { result: string }, ApiDependencies, - { team: Team; comp: Competition } + { team: Team; comp: Competition }, + LocalApiDependencies >({ isAuthorized: (req, res, deps, [pitreportId]) => AccessLevels.IfOnTeamThatOwnsPitReport(req, res, deps, pitreportId), @@ -2353,6 +2587,20 @@ export default class ClientApi extends NextApiTemplate { ); return res.status(200).send({ result: "success" }); }, + beforeCall: async ({ dbPromise }, [pitreportId, newReport]) => { + if (typeof pitreportId != "string") + throw new Error("Pit report ID is not a string"); + + // Await the promise to ensure we don't leave the page before updating the report + await ( + await dbPromise + ).addOrUpdateObject(CollectionId.PitReports, { + _id: new ObjectId(pitreportId) as any as string, + ...newReport, + } as Pitreport); + + return Promise.resolve([pitreportId, newReport]); + }, }); speedTest = createNextRoute< @@ -2710,6 +2958,369 @@ export default class ClientApi extends NextApiTemplate { }, }); + findCompSeasonAndTeamByCompSlug = createNextRoute< + [string], + { comp: Competition; season: Season; team: Team } | undefined, + ApiDependencies, + { team: Team; season: Season; comp: Competition }, + LocalApiDependencies + >({ + isAuthorized: async (req, res, { db, userPromise }, [compSlug]) => { + const user = await userPromise; + if (!user) return { authorized: false, authData: undefined }; + + const comp = await findObjectBySlugLookUp( + await db, + CollectionId.Competitions, + compSlug, + ); + + if (!comp) return { authorized: false, authData: undefined }; + + const team = await getTeamFromComp(await db, comp); + + if (!team || !team.users.includes(user._id!.toString())) + return { authorized: false, authData: undefined }; + + const season = await getSeasonFromComp(await db, comp); + + if (!season) return { authorized: false, authData: undefined }; + + return { + authorized: true, + authData: { + comp, + season, + team, + }, + }; + }, + handler: async (req, res, deps, authData, args) => { + return res.status(200).send(authData); + }, + fallback: async ({ dbPromise }, [compSlug]) => { + const db = await dbPromise; + + const comp = await findObjectBySlugLookUp( + db, + CollectionId.Competitions, + compSlug, + ); + + if (!comp) return undefined; + + const team = await getTeamFromComp(db, comp); + + if (!team) return undefined; + + const season = await getSeasonFromComp(db, comp); + + if (!season) return undefined; + + return { + comp, + season, + team, + }; + }, + afterResponse: async ({ dbPromise }, res, ranFallback) => { + if (ranFallback || !res) return Promise.resolve(); + + const db = await dbPromise; + + const { comp, season, team } = res; + + await Promise.all([ + db.addOrUpdateObject(CollectionId.Competitions, comp), + db.addOrUpdateObject(CollectionId.Seasons, season), + db.addOrUpdateObject(CollectionId.Teams, team), + ]); + }, + }); + + getSeasonPageData = createNextRoute< + [string], + { team?: Team; season?: Season; comps?: Competition[] }, + ApiDependencies, + { team: Team; season: Season }, + LocalApiDependencies + >({ + isAuthorized: (req, res, deps, [seasonSlug]) => + AccessLevels.IfSeasonOwnerBySlug(req, res, deps, seasonSlug), + handler: async ( + req, + res, + { db: dbPromise }, + { team, season }, + [seasonId], + ) => { + const db = await dbPromise; + + const comps = await db.findObjects(CollectionId.Competitions, { + _id: { $in: season.competitions.map((id) => new ObjectId(id)) }, + }); + + return res.status(200).send({ team, season, comps }); + }, + afterResponse: async (deps, res, ranFallback) => { + if (!res || !ranFallback) return; + + const { team, season, comps } = res; + + saveObjectAfterResponse(deps, CollectionId.Teams, team, ranFallback); + saveObjectAfterResponse(deps, CollectionId.Seasons, season, ranFallback); + saveObjectAfterResponse( + deps, + CollectionId.Competitions, + comps, + ranFallback, + ); + }, + fallback: async ({ dbPromise }, [seasonSlug]) => { + const db = await dbPromise; + + const season = await db.findObjectBySlug( + CollectionId.Seasons, + seasonSlug, + ); + + if (!season) + return { + team: undefined, + season: undefined, + comps: undefined, + }; + + const comps = await db.findObjects(CollectionId.Competitions, { + _id: { $in: season.competitions.map((id) => new ObjectId(id)) }, + }); + + const team = await getTeamFromSeason(db, season); + + if (!team) + return { + team: undefined, + season: undefined, + comps: undefined, + }; + + return { team, season, comps }; + }, + }); + + getPitReportPageData = createNextRoute< + [string], + { + pitReport?: Pitreport; + compName?: string; + usersTeamNumber?: number; + }, + ApiDependencies, + { team: Team; comp: Competition; pitReport: Pitreport }, + LocalApiDependencies + >({ + isAuthorized: (req, res, deps, [pitReportId]) => + AccessLevels.IfOnTeamThatOwnsPitReport(req, res, deps, pitReportId), + handler: async ( + req, + res, + { db: dbPromise }, + { team, comp, pitReport }, + [pitReportId], + ) => { + const db = await dbPromise; + + const season = await getSeasonFromComp(db, comp); + console.log("Season", season); + + if (!season) return res.status(404).send({ error: "Season not found" }); + + return res.status(200).send({ + pitReport, + compName: comp.name, + usersTeamNumber: team.number, + }); + }, + afterResponse: async (deps, res, ranFallback) => { + if (!res || ranFallback) return; + + const { pitReport, compName, usersTeamNumber: teamNumber } = res; + if (!pitReport) return; + + saveObjectAfterResponse( + deps, + CollectionId.PitReports, + pitReport, + ranFallback, + ); + + deps.localStorage.set(`${pitReport._id!.toString()}.compName`, compName); + deps.localStorage.set( + `${pitReport._id!.toString()}.teamNumber`, + teamNumber, + ); + }, + fallback: async ({ dbPromise, localStorage }, [pitReportId]) => { + const db = await dbPromise; + + const pitReport = await db.findObjectById( + CollectionId.PitReports, + new ObjectId(pitReportId), + ); + if (!pitReport) return {}; + + const [compName, teamNumber] = await Promise.all([ + localStorage.get(`${pitReport._id!.toString()}.compName`), + localStorage.get(`${pitReport._id!.toString()}.teamNumber`), + ]); + + return { pitReport, compName, usersTeamNumber: teamNumber }; + }, + }); + + syncCompData = createNextRoute< + [string], + { + comp: Competition | undefined; + pitReports: Pitreport[]; + matches: Match[]; + reports: Report[]; + subjectiveReports: SubjectiveReport[]; + }, + ApiDependencies, + { team: Team; comp: Competition }, + LocalApiDependencies + >({ + isAuthorized: (req, res, deps, [compId]) => + AccessLevels.IfOnTeamThatOwnsComp(req, res, deps, compId), + handler: async (req, res, { db: dbPromise }, { team, comp }, [compId]) => { + const db = await dbPromise; + + const pitReportPromise = db.findObjects(CollectionId.PitReports, { + _id: { $in: comp.pitReports.map((id) => new ObjectId(id)) }, + }); + + const matchesPromise = db.findObjects(CollectionId.Matches, { + _id: { $in: comp.matches.map((id) => new ObjectId(id)) }, + }); + + const reportsPromise = matchesPromise.then((matches) => + db.findObjects(CollectionId.Reports, { + _id: { $in: matches.flatMap((match) => match.reports) }, + }), + ); + + const subjectiveReportsPromise = matchesPromise.then((matches) => + db.findObjects(CollectionId.SubjectiveReports, { + _id: { $in: matches.flatMap((match) => match.subjectiveReports) }, + }), + ); + + const [pitReports, matches, reports, subjectiveReports] = + await Promise.all([ + pitReportPromise, + matchesPromise, + reportsPromise, + subjectiveReportsPromise, + ]); + + return res.status(200).send({ + comp, + pitReports, + matches, + reports, + subjectiveReports, + }); + }, + afterResponse: async (deps, res, ranFallback) => { + if (!res || ranFallback) return; + + const { comp, pitReports, matches, reports, subjectiveReports } = res; + + saveObjectAfterResponse( + deps, + CollectionId.Competitions, + comp, + ranFallback, + ); + saveObjectAfterResponse( + deps, + CollectionId.PitReports, + pitReports, + ranFallback, + ); + saveObjectAfterResponse(deps, CollectionId.Matches, matches, ranFallback); + saveObjectAfterResponse(deps, CollectionId.Reports, reports, ranFallback); + saveObjectAfterResponse( + deps, + CollectionId.SubjectiveReports, + subjectiveReports, + ranFallback, + ); + }, + fallback: async ({ dbPromise }, [compId]) => { + const db = await dbPromise; + + const comp = await db.findObjectById( + CollectionId.Competitions, + new ObjectId(compId), + ); + + if (!comp) + return { + comp: undefined, + pitReports: [], + matches: [], + reports: [], + subjectiveReports: [], + }; + + const pitReportPromise = db.findObjects(CollectionId.PitReports, { + _id: { $in: comp.pitReports.map((id) => new ObjectId(id)) }, + }); + + const matchesPromise = db.findObjects(CollectionId.Matches, { + _id: { $in: comp.matches.map((id) => new ObjectId(id)) }, + }); + + const reportsPromise = matchesPromise.then((matches) => + db.findObjects(CollectionId.Reports, { + _id: { + $in: matches + .flatMap((match) => match.reports) + .map((id) => new ObjectId(id)), + }, + }), + ); + + const subjectiveReportsPromise = matchesPromise.then((matches) => + db.findObjects(CollectionId.SubjectiveReports, { + _id: { + $in: matches + .flatMap((match) => match.subjectiveReports) + .map((id) => new ObjectId(id)), + }, + }), + ); + + const [pitReports, matches, reports, subjectiveReports] = + await Promise.all([ + pitReportPromise, + matchesPromise, + reportsPromise, + subjectiveReportsPromise, + ]); + + return { + comp, + pitReports, + matches, + reports, + subjectiveReports, + }; + }, + }); + /** * Creates a user and session, and then returns the session token. Used in E2E tests. */ diff --git a/lib/api/ClientApiUtils.ts b/lib/api/ClientApiUtils.ts new file mode 100644 index 00000000..0ac79389 --- /dev/null +++ b/lib/api/ClientApiUtils.ts @@ -0,0 +1,193 @@ +/** + * @tested_by tests/lib/api/ClientApiUtils.test.ts + */ +import { ObjectId } from "bson"; +import CollectionId, { + CollectionIdToType, + SluggedCollectionId, +} from "../client/CollectionId"; +import DbInterface from "../client/dbinterfaces/DbInterface"; +import ClientApi from "./ClientApi"; +import LocalStorageDbInterface from "../client/dbinterfaces/LocalStorageDbInterface"; +import toast from "react-hot-toast"; +import { Pitreport } from "../Types"; + +/** + * Return true when the local object should be overwritten by the remote object. + */ +export const ShouldOverwrite = Object.freeze({ + PitReport: (local: Pitreport, remote: Pitreport) => + !local.submitted || remote.submitted, +}); + +/** + * Checks if the local object should be overwritten by the remote object. + * If yes, updates the local object in the database. + */ +async function updateIfShouldOverwrite< + TId extends CollectionId, + TObj extends CollectionIdToType, +>( + db: DbInterface, + collectionId: TId, + remoteObject: TObj | undefined, + shouldOverwrite?: (local: TObj, remote: TObj) => boolean, +): Promise { + if (!remoteObject) return; + + if (shouldOverwrite) { + const localObject = await db.findObjectById( + collectionId, + new ObjectId(remoteObject._id), + ); + + if (localObject && !shouldOverwrite(localObject, remoteObject)) { + return; + } + } + + await db.addOrUpdateObject(collectionId, remoteObject); +} + +export async function saveObjectAfterResponse< + TId extends CollectionId, + TObj extends CollectionIdToType, +>( + { dbPromise }: { dbPromise: Promise }, + collectionId: TId, + object: TObj | (TObj | undefined)[] | undefined, + ranFallback: boolean, + shouldOverwrite?: (local: TObj, remote: TObj) => boolean, +) { + if (ranFallback) return; + + const db = await dbPromise; + + if (Array.isArray(object)) + await Promise.all( + object + .filter((obj) => obj !== undefined) + .map((obj) => + updateIfShouldOverwrite(db, collectionId, obj, shouldOverwrite), + ), + ); + else if (object) { + await updateIfShouldOverwrite(db, collectionId, object, shouldOverwrite); + } +} + +type AllowUndefinedIfNotArray = TArr extends any[] + ? TEle[] + : TEle | undefined; + +export async function findObjectByIdFallback< + TCollectionId extends CollectionId, + TIdArg extends string | string[], + TObj extends AllowUndefinedIfNotArray< + TIdArg, + CollectionIdToType + >, +>( + { dbPromise }: { dbPromise: Promise }, + collectionId: TCollectionId, + id: TIdArg, +): Promise { + const db = await dbPromise; + if (Array.isArray(id)) { + return db.findObjects(collectionId, { + _id: { $in: id.map((i) => new ObjectId(i)) }, + }) as Promise; + } + return db.findObjectById(collectionId, new ObjectId(id)) as Promise; +} + +export async function findObjectBySlugFallback< + TCollectionId extends SluggedCollectionId, + TSlugArg extends string | string[], + TObj extends AllowUndefinedIfNotArray< + TSlugArg, + CollectionIdToType + >, +>( + { dbPromise }: { dbPromise: Promise }, + collectionId: TCollectionId, + slug: TSlugArg, +): Promise { + const db = await dbPromise; + if (Array.isArray(slug)) { + return Promise.all( + slug.map((s) => db.findObjectBySlug(collectionId, s)), + ).then((objs) => objs.filter((obj) => obj != undefined)) as Promise; + } + return db.findObjectBySlug(collectionId, slug) as Promise; +} + +export async function syncCompData(api: ClientApi, compId: string) { + const toastId = toast.loading("Syncing offline data..."); + + if (!window.navigator.onLine) { + toast.error("Cannot sync offline data: No internet connection.", { + id: toastId, + }); + throw new Error("Cannot sync offline data: No internet connection."); + } + + const comp = await api.findCompetitionById(compId).catch((err) => { + toast.error("Error fetching competition: " + err, { id: toastId }); + throw new Error("Error fetching competition: " + err); + }); + + if (!comp) { + toast.error("Competition not found: " + compId, { id: toastId }); + throw new Error("Competition not found: " + compId); + } + + const localDb = new LocalStorageDbInterface(); + localDb.init(); + + const totalItemsToSync = comp.pitReports.length; + let itemsSynced = 0; + + const remotePitReportsById = ( + await api.findBulkPitReportsById(comp.pitReports) + ).reduce( + (acc, report) => { + acc[report._id!.toString()] = report; + return acc; + }, + {} as Record>, + ); + + async function syncPitReport(pitReportId: string) { + const localPitReport = await localDb.findObjectById( + CollectionId.PitReports, + new ObjectId(pitReportId), + ); + + if (!localPitReport) return; + + const remotePitReport = remotePitReportsById[pitReportId]; + + if ( + localPitReport.submitted && + (!remotePitReport || !remotePitReport!.submitted) + ) + await api.updatePitreport(pitReportId, localPitReport); + } + + await Promise.all( + comp.pitReports.map((report) => + syncPitReport(report).finally(() => { + itemsSynced++; + toast.loading( + `Syncing offline data... (${itemsSynced}/${totalItemsToSync})`, + { + id: toastId, + }, + ); + }), + ), + ); + + toast.success("Synced all offline data!", { id: toastId }); +} diff --git a/lib/api/GearboxRequestHelper.ts b/lib/api/GearboxRequestHelper.ts new file mode 100644 index 00000000..b7f4b655 --- /dev/null +++ b/lib/api/GearboxRequestHelper.ts @@ -0,0 +1,40 @@ +import toast from "react-hot-toast"; +import { RequestHelper } from "unified-api"; +import LocalStorageDbInterface from "../client/dbinterfaces/LocalStorageDbInterface"; +import LocalApiDependencies from "./LocalApiDependencies"; +import { + LocalStorage, + UnavailableLocalStorage, +} from "../client/LocalStorageInterface"; +import InMemoryDbInterface from "../client/dbinterfaces/InMemoryDbInterface"; + +export default class GearboxRequestHelper extends RequestHelper { + constructor() { + super( + process.env.NEXT_PUBLIC_API_URL ?? "", // Replace undefined when env is not present (ex: for testing builds) + (url) => + toast.error( + `Failed API request: ${url}. If this is an error, please contact the developers.`, + ), + ); + } + + async getLocalDependencies() { + const dbPromise = (async () => { + const db = + typeof window !== "undefined" + ? new LocalStorageDbInterface() + : new InMemoryDbInterface(); + await db.init(); + return db; + })(); + + return { + dbPromise, + localStorage: + typeof window !== "undefined" + ? new LocalStorage(window.localStorage) + : new UnavailableLocalStorage(), + } as LocalApiDependencies; + } +} diff --git a/lib/api/LocalApiDependencies.ts b/lib/api/LocalApiDependencies.ts new file mode 100644 index 00000000..5e855621 --- /dev/null +++ b/lib/api/LocalApiDependencies.ts @@ -0,0 +1,9 @@ +import DbInterface from "../client/dbinterfaces/DbInterface"; +import LocalStorageInterface from "../client/LocalStorageInterface"; + +type LocalApiDependencies = { + dbPromise: Promise; + localStorage: LocalStorageInterface; +}; + +export default LocalApiDependencies; diff --git a/lib/client/ClientUtils.ts b/lib/client/ClientUtils.ts index 10f471ca..d150a6e9 100644 --- a/lib/client/ClientUtils.ts +++ b/lib/client/ClientUtils.ts @@ -1,4 +1,6 @@ +import { games } from "../games"; import { Pitreport } from "../Types"; +import { GameId } from "./GameId"; import { MostCommonValue } from "./StatsMath"; export function getIdsInProgressFromTimestamps(timestamps: { diff --git a/lib/client/LocalStorageInterface.ts b/lib/client/LocalStorageInterface.ts new file mode 100644 index 00000000..c54ad167 --- /dev/null +++ b/lib/client/LocalStorageInterface.ts @@ -0,0 +1,46 @@ +export default interface LocalStorageInterface { + get: (key: string) => Promise; + set: (key: string, value: any) => Promise; +} + +export class MockLocalStorage implements LocalStorageInterface { + private storage: Record = {}; + + async get(key: string): Promise { + return this.storage[key]; + } + + async set(key: string, value: any): Promise { + this.storage[key] = value; + } +} + +export class LocalStorage implements LocalStorageInterface { + private storage: Storage; + + constructor(storage: Storage) { + this.storage = storage; + } + + async get(key: string): Promise { + const value = this.storage.getItem(key); + if (value) { + return JSON.parse(value) as T; + } + return undefined; + } + + async set(key: string, value: any): Promise { + this.storage.setItem(key, JSON.stringify(value)); + } +} + +export class UnavailableLocalStorage implements LocalStorageInterface { + async get(key: string): Promise { + throw new Error("Local storage is unavailable"); + } + + async set(key: string, value: any): Promise { + throw new Error("Local storage is unavailable"); + } +} diff --git a/lib/client/dbinterfaces/CachedDbInterface.ts b/lib/client/dbinterfaces/CachedDbInterface.ts index 2ae214e5..2d006921 100644 --- a/lib/client/dbinterfaces/CachedDbInterface.ts +++ b/lib/client/dbinterfaces/CachedDbInterface.ts @@ -9,7 +9,7 @@ import DbInterface, { import { default as BaseCachedDbInterface } from "mongo-anywhere/CachedDbInterface"; import NodeCache from "node-cache"; import { CacheOperation } from "mongo-anywhere/CachedDbInterface"; -import { findObjectBySlugLookUp } from "@/lib/slugToId"; +import findObjectBySlugLookUp from "@/lib/slugToId"; export const cacheOptions: NodeCache.Options = { stdTTL: 1 * 60, @@ -74,4 +74,11 @@ export default class CachedDbInterface >(collection: TId, slug: string): Promise { return findObjectBySlugLookUp(this, collection, slug); } + + addOrUpdateObject< + TId extends CollectionId, + TObj extends CollectionIdToType, + >(collection: TId, object: TObj): Promise { + return super.addOrUpdateObject(collection, object); + } } diff --git a/lib/client/dbinterfaces/DbInterface.ts b/lib/client/dbinterfaces/DbInterface.ts index 698a8a63..267a2e12 100644 --- a/lib/client/dbinterfaces/DbInterface.ts +++ b/lib/client/dbinterfaces/DbInterface.ts @@ -4,7 +4,6 @@ import CollectionId, { SluggedCollectionId, } from "../CollectionId"; import { default as BaseDbInterface } from "mongo-anywhere/DbInterface"; -import slugToId from "@/lib/slugToId"; export type WithStringOrObjectIdId = Omit & { _id?: ObjectId | string; @@ -63,4 +62,12 @@ export default interface DbInterface collection: CollectionId, query: object, ): Promise; + + addOrUpdateObject< + TId extends CollectionId, + TObj extends CollectionIdToType, + >( + collection: TId, + object: TObj, + ): Promise; } diff --git a/lib/client/dbinterfaces/InMemoryDbInterface.ts b/lib/client/dbinterfaces/InMemoryDbInterface.ts index 779f931f..23707ac9 100644 --- a/lib/client/dbinterfaces/InMemoryDbInterface.ts +++ b/lib/client/dbinterfaces/InMemoryDbInterface.ts @@ -7,7 +7,7 @@ import DbInterface, { WithStringOrObjectIdId, } from "@/lib/client/dbinterfaces/DbInterface"; import { default as BaseInMemoryDbInterface } from "mongo-anywhere/InMemoryDbInterface"; -import slugToId, { findObjectBySlugLookUp } from "@/lib/slugToId"; +import findObjectBySlugLookUp from "@/lib/slugToId"; export default class InMemoryDbInterface extends BaseInMemoryDbInterface< @@ -65,4 +65,11 @@ export default class InMemoryDbInterface >(collection: TId, slug: string): Promise { return findObjectBySlugLookUp(this, collection, slug); } + + addOrUpdateObject< + TId extends CollectionId, + TObj extends CollectionIdToType, + >(collection: TId, object: TObj): Promise { + return super.addOrUpdateObject(collection, object); + } } diff --git a/lib/client/dbinterfaces/LocalStorageDbInterface.ts b/lib/client/dbinterfaces/LocalStorageDbInterface.ts index b7975a2d..f4a536b6 100644 --- a/lib/client/dbinterfaces/LocalStorageDbInterface.ts +++ b/lib/client/dbinterfaces/LocalStorageDbInterface.ts @@ -7,7 +7,7 @@ import DbInterface, { WithStringOrObjectIdId, } from "@/lib/client/dbinterfaces/DbInterface"; import { default as BaseLocalStorageDbInterface } from "mongo-anywhere/LocalStorageDbInterface"; -import { findObjectBySlugLookUp } from "@/lib/slugToId"; +import findObjectBySlugLookUp from "@/lib/slugToId"; export default class LocalStorageDbInterface extends BaseLocalStorageDbInterface< @@ -65,4 +65,11 @@ export default class LocalStorageDbInterface >(collection: TId, slug: string): Promise { return findObjectBySlugLookUp(this, collection, slug); } + + addOrUpdateObject< + TId extends CollectionId, + TObj extends CollectionIdToType, + >(collection: TId, object: TObj): Promise { + return super.addOrUpdateObject(collection, object); + } } diff --git a/lib/sw.ts b/lib/client/sw.ts similarity index 68% rename from lib/sw.ts rename to lib/client/sw.ts index feea1757..0337e394 100644 --- a/lib/sw.ts +++ b/lib/client/sw.ts @@ -1,6 +1,6 @@ -import { defaultCache } from "@serwist/next/worker"; import type { PrecacheEntry, SerwistGlobalConfig } from "serwist"; import { Serwist } from "serwist"; +import { defaultCache } from "@serwist/next/worker"; declare global { interface WorkerGlobalScope extends SerwistGlobalConfig { @@ -14,7 +14,7 @@ const serwist = new Serwist({ precacheEntries: self.__SW_MANIFEST, skipWaiting: true, clientsClaim: true, - navigationPreload: true, + navigationPreload: false, // Not 100% sure this should be disabled, but it made stuff work runtimeCaching: defaultCache, fallbacks: { entries: [ @@ -26,4 +26,11 @@ const serwist = new Serwist({ }, }); +// self.addEventListener("fetch", async (event) => { +// console.log(event.request); +// event.respondWith( +// await serwist.handleRequest({ request: event.request, event: event })!, +// ); +// }); + serwist.addEventListeners(); diff --git a/lib/games.ts b/lib/games.ts index 756ddc0d..290ea1ac 100644 --- a/lib/games.ts +++ b/lib/games.ts @@ -1,39 +1,14 @@ -import { Dot } from "@/components/stats/Heatmap"; -import { - CenterStageEnums, - Defense, - FrcDrivetrain, - IntakeTypes, - IntoTheDeepEnums, - ReefscapeEnums, -} from "./Enums"; -import { Badge, FormLayoutProps, PitStatsLayout, StatsLayout } from "./Layout"; -import { - Report, - Game, - League, - PitReportData, - QuantData, - Pitreport, - FieldPos, -} from "./Types"; +import { Defense, FrcDrivetrain } from "./Enums"; +import { Badge } from "./Layout"; +import { Report, Game, PitReportData, QuantData, Pitreport } from "./Types"; import { GameId } from "./client/GameId"; -import { - AmpAutoPoints, - AmpTeleopPoints, - BooleanAverage, - GetMinimum, - MostCommonValue, - NumericalTotal, - Round, - SpeakerAutoPoints, - SpeakerTeleopPoints, - TrapPoints, -} from "./client/StatsMath"; -import { report } from "process"; -import { GetMaximum } from "./client/StatsMath"; +import { MostCommonValue } from "./client/StatsMath"; +import Crescendo from "./games/Crescendo"; +import IntoTheDeep from "./games/IntoTheDeep"; +import CenterStage from "./games/CenterStage"; +import Reefscape from "./games/Reefscape"; -function getBaseBadges( +export function getBaseBadges( pitReport: Pitreport | undefined, quantitativeReports: Report[] | undefined, ) { @@ -76,1816 +51,27 @@ function getBaseBadges( return badges; } -// Data keys use upper camel case so they can be used as labels in the forms - -export namespace Crescendo { - export class QuantitativeData extends QuantData { - AutoScoredAmp: number = 0; // # of times scored in the amp - AutoMissedAmp: number = 0; - AutoScoredSpeaker: number = 0; - AutoMissedSpeaker: number = 0; - MovedOut: boolean = false; - - TeleopScoredAmp: number = 0; - TeleopMissedAmp: number = 0; - TeleopScoredSpeaker: number = 0; - TeleopMissedSpeaker: number = 0; - TeleopScoredTrap: number = 0; - TeleopMissedTrap: number = 0; - TeleopPassed: number = 0; - - Coopertition: boolean = false; // true if used any point in match - ClimbedStage: boolean = false; - ParkedStage: boolean = false; - UnderStage: boolean = false; - - intakeType: IntakeTypes = IntakeTypes.Human; - } - - export class PitData extends PitReportData { - intakeType: IntakeTypes = IntakeTypes.None; - canClimb: boolean = false; - fixedShooter: boolean = false; - canScoreAmp: boolean = false; - canScoreSpeaker: boolean = false; - canScoreFromDistance: boolean = false; - underBumperIntake: boolean = false; - autoNotes: number = 0; - } - - const pitReportLayout: FormLayoutProps = { - Intake: ["intakeType"], - Shooter: [ - "canScoreAmp", - "canScoreSpeaker", - "fixedShooter", - "canScoreFromDistance", - ], - Climber: ["canClimb"], - Auto: [{ key: "autoNotes", type: "number" }], - }; - - const quantitativeReportLayout: FormLayoutProps = { - Auto: [ - "MovedOut", - [ - ["AutoScoredAmp", "AutoMissedAmp"], - ["AutoScoredSpeaker", "AutoMissedSpeaker"], - ], - ], - Teleop: [ - [ - ["TeleopScoredAmp", "TeleopMissedAmp"], - ["TeleopScoredSpeaker", "TeleopMissedSpeaker"], - ["TeleopScoredTrap", "TeleopMissedTrap"], - ], - [[{ key: "TeleopPassed", label: "Notes Passed" }]], - "Defense", - ], - Summary: [ - { key: "Coopertition", label: "Coopertition Activated" }, - "ClimbedStage", - "ParkedStage", - { key: "UnderStage", label: "Went Under Stage" }, - ], - }; - - const statsLayout: StatsLayout = { - sections: { - Auto: [ - { - stats: [ - { label: "Avg Scored Amp Shots", key: "AutoScoredAmp" }, - { label: "Avg Missed Amp Shots", key: "AutoMissedAmp" }, - ], - label: "Overall Amp Accuracy", - }, - { - stats: [ - { label: "Avg Scored Speaker Shots", key: "AutoScoredSpeaker" }, - { label: "Avg Missed Speaker Shots", key: "AutoMissedSpeaker" }, - ], - label: "Overall Speaker Accuracy", - }, - ], - Teleop: [ - { - stats: [ - { label: "Avg Scored Amp Shots", key: "TeleopScoredAmp" }, - { label: "Avg Missed Amp Shots", key: "TeleopMissedAmp" }, - ], - label: "Overall Amp Accuracy", - }, - { - stats: [ - { label: "Avg Scored Speaker Shots", key: "TeleopScoredSpeaker" }, - { label: "Avg Missed Speaker Shots", key: "TeleopMissedSpeaker" }, - ], - label: "Overall Speaker Accuracy", - }, - { - stats: [ - { label: "Avg Scored Trap Shots", key: "TeleopScoredTrap" }, - { label: "Avg Missed Trap Shots", key: "TeleopMissedTrap" }, - ], - label: "Overall Trap Accuracy", - }, - { - key: "TeleopPassed", - label: "Notes Passed", - }, - ], - }, - getGraphDots: ( - quantReports: Report[], - pitReport?: Pitreport, - ) => { - return []; - }, - }; - - const pitStatsLayout: PitStatsLayout = { - overallSlideStats: [ - { - label: "Avg Notes Scored", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - return ( - quantitativeReports.reduce( - (acc, report) => - acc + - report.data.AutoScoredSpeaker + - report.data.TeleopMissedSpeaker + - report.data.AutoScoredAmp + - report.data.TeleopScoredAmp + - report.data.TeleopScoredTrap, - 0, - ) / quantitativeReports.length - ); - }, - }, - { - label: "Teleop Speaker Accuracy", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - const scores = quantitativeReports.map( - (report) => report.data.TeleopScoredSpeaker, - ); - const misses = quantitativeReports.map( - (report) => report.data.TeleopMissedSpeaker, - ); - - const scoreCount = scores.reduce((acc, score) => acc + score, 0); - const missCount = misses.reduce((acc, miss) => acc + miss, 0); - - return scoreCount / (scoreCount + missCount); - }, - }, - { - label: "Teleop Amp Accuracy", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - const scores = quantitativeReports.map( - (report) => report.data.TeleopScoredAmp, - ); - const misses = quantitativeReports.map( - (report) => report.data.TeleopMissedAmp, - ); - - const scoreCount = scores.reduce((acc, score) => acc + score, 0); - const missCount = misses.reduce((acc, miss) => acc + miss, 0); - - return scoreCount / (scoreCount + missCount); - }, - }, - { - label: "Avg Notes in Trap", - key: "TeleopScoredTrap", - }, - ], - individualSlideStats: [ - { - label: "Avg Teleop Points", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - const speakerAuto = - NumericalTotal("AutoScoredSpeaker", quantitativeReports) * - SpeakerAutoPoints; - const speakerTeleop = - NumericalTotal("TeleopScoredAmp", quantitativeReports) * - SpeakerTeleopPoints; - const ampAuto = - NumericalTotal("AutoScoredAmp", quantitativeReports) * - AmpAutoPoints; - const ampTeleop = - NumericalTotal("TeleopScoredAmp", quantitativeReports) * - AmpTeleopPoints; - const trap = - NumericalTotal("TeleopScoredTrap", quantitativeReports) * - TrapPoints; - - return ( - Round(speakerAuto + speakerTeleop + ampAuto + ampTeleop + trap) / - quantitativeReports.length - ); - }, - }, - { - label: "Avg Auto Points", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - const speakerAuto = - NumericalTotal("AutoScoredSpeaker", quantitativeReports) * - SpeakerAutoPoints; - const ampAuto = - NumericalTotal("AutoScoredAmp", quantitativeReports) * - AmpAutoPoints; - - return Round(speakerAuto + ampAuto) / quantitativeReports.length; - }, - }, - { - label: "Avg Speaker Points", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - const speakerAuto = - NumericalTotal("AutoScoredSpeaker", quantitativeReports) * - SpeakerAutoPoints; - const speakerTeleop = - NumericalTotal("TeleopScoredAmp", quantitativeReports) * - SpeakerTeleopPoints; - - return ( - Round(speakerAuto + speakerTeleop) / quantitativeReports.length - ); - }, - }, - { - label: "Avg Amp Points", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - const ampAuto = - NumericalTotal("AutoScoredAmp", quantitativeReports) * - AmpAutoPoints; - const ampTeleop = - NumericalTotal("TeleopScoredAmp", quantitativeReports) * - AmpTeleopPoints; - - return Round(ampAuto + ampTeleop) / quantitativeReports.length; - }, - }, - ], - robotCapabilities: [ - { - label: "Intake Type", - key: "intakeType", - }, - ], - graphStat: { - label: "Avg Notes Scored", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - return ( - quantitativeReports.reduce( - (acc, report) => - acc + - report.data.AutoScoredSpeaker + - report.data.TeleopMissedSpeaker + - report.data.AutoScoredAmp + - report.data.TeleopScoredAmp + - report.data.TeleopScoredTrap, - 0, - ) / quantitativeReports.length - ); - }, - }, - }; - - function getAvgPoints(reports: Report[] | undefined) { - if (!reports) return 0; - - const speakerAuto = - NumericalTotal("AutoScoredSpeaker", reports) * SpeakerAutoPoints; - const speakerTeleop = - NumericalTotal("TeleopScoredAmp", reports) * SpeakerTeleopPoints; - const ampAuto = NumericalTotal("AutoScoredAmp", reports) * AmpAutoPoints; - const ampTeleop = - NumericalTotal("TeleopScoredAmp", reports) * AmpTeleopPoints; - const trap = NumericalTotal("TeleopScoredTrap", reports) * TrapPoints; - - return ( - Round(speakerAuto + speakerTeleop + ampAuto + ampTeleop + trap) / - reports.length - ); - } - - function getBadges( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - card: boolean, - ) { - const pitData = pitReport?.data; - const badges = getBaseBadges(pitReport, quantitativeReports); - - const intake = pitData?.intakeType; - const cooperates = BooleanAverage( - "Coopertition", - quantitativeReports ?? [], - ); - const climbs = BooleanAverage("ClimbedStage", quantitativeReports ?? []); - const parks = BooleanAverage("ParkedStage", quantitativeReports ?? []); - const understage = BooleanAverage("UnderStage", quantitativeReports ?? []); - - if (pitReport?.submitted && intake) { - const intakeBadge: Badge = { text: intake, color: "primary" }; - if (intake === IntakeTypes.Human) { - intakeBadge.color = "warning"; - } else if (intake === IntakeTypes.Both) { - intakeBadge.color = "secondary"; - } else if (intake === IntakeTypes.None) { - intakeBadge.color = "warning"; - intakeBadge.text = "No Intake"; - } - - badges.push(intakeBadge); - } - - if (cooperates) badges.push({ text: "Cooperates", color: "success" }); - - if (climbs) badges.push({ text: "Climbs", color: "accent" }); - - if (parks) badges.push({ text: "Parks", color: "primary" }); - - if (understage) - badges.push({ text: "Can Go Under Stage", color: "success" }); - - return badges; - } - - export const game = new Game( - "Crescendo", - 2024, - League.FRC, - QuantitativeData, - PitData, - pitReportLayout, - quantitativeReportLayout, - statsLayout, - pitStatsLayout, - "Crescendo", - "https://www.firstinspires.org/sites/default/files/uploads/resource_library/frc/crescendo/crescendo.png", - "", - getBadges, - getAvgPoints, - ); -} - -export namespace CenterStage { - export class QuantitativeData extends QuantData { - AutoScoredBackstage: number = 0; - AutoScoredBackdrop: number = 0; - AutoPlacedPixelOnSpikeMark: boolean = false; - AutoParked: boolean = false; - - TeleopScoredBackstage: number = 0; - Mosaics: number = 0; - SetLinesReached: number = 0; - - LandingZoneReached: number = 0; - EndgameParked: boolean = false; - EndgameClimbed: boolean = false; - } - - export class PitData extends PitReportData { - AutoBackstageSideExists: boolean = false; - AutoBackstageParkingLocation: CenterStageEnums.CenterStageParkingLocation = - CenterStageEnums.CenterStageParkingLocation.NotApplicable; - AutoBackstageCanPlacePurplePixel: boolean = false; - AutoBackstageCanPlaceYellowPixelOnBackboard: boolean = false; - AutoBackstageCanPark: boolean = false; - AutoBackstageWhitePixels: number = 0; - AutoBackstageAdjustableToFitOurs: CenterStageEnums.AutoAdjustable = - CenterStageEnums.AutoAdjustable.NoNeed; - - AutoAudienceSideExists: boolean = false; - AutoAudienceParkingLocation: CenterStageEnums.CenterStageParkingLocation = - CenterStageEnums.CenterStageParkingLocation.NotApplicable; - AutoAudienceCanPlacePurplePixel: boolean = false; - AutoAudienceCanPlaceYellowPixelOnBackboard: boolean = false; - AutoAudienceCanPark: boolean = false; - AutoAudienceWhitePixels: number = 0; - AutoAudienceAdjustableToFitOurs: CenterStageEnums.AutoAdjustable = - CenterStageEnums.AutoAdjustable.NoNeed; - - AutoSidePreference: CenterStageEnums.AutoSidePreference = - CenterStageEnums.AutoSidePreference.NoPreference; - - CanPlaceOnBackboard: boolean = false; - CanPickUpFromStack: boolean = false; - PixelsMovedAtOnce: number = 0; - - EndgameCanLaunchDrone: boolean = false; - EndgameCanHang: boolean = false; - EndgameCanPark: boolean = false; - } - - const quantitativeReportLayout: FormLayoutProps = { - Auto: [ - [["AutoScoredBackstage"], ["AutoScoredBackdrop"]], - "AutoPlacedPixelOnSpikeMark", - "AutoParked", - ], - Teleop: [[["TeleopScoredBackstage"], ["Mosaics"], ["SetLinesReached"]]], - Endgame: [[["LandingZoneReached"]], "EndgameParked", "EndgameClimbed"], - }; - - const pitReportLayout: FormLayoutProps = { - "Backstage Auto": [ - { key: "AutoBackstageSideExists", label: "Has Auto?" }, - { key: "AutoBackstageParkingLocation", label: "Parking Location" }, - { - key: "AutoBackstageCanPlacePurplePixel", - label: "Can Place Purple Pixel?", - }, - { - key: "AutoBackstageCanPlaceYellowPixelOnBackboard", - label: "Can Place Yellow Pixel on Backboard?", - }, - { key: "AutoBackstageCanPark", label: "Can Park?" }, - { key: "AutoBackstageWhitePixels", label: "White Pixels Place" }, - { - key: "AutoBackstageAdjustableToFitOurs", - label: "Adjustable to Fit Our Auto?", - }, - ], - "Audience Auto": [ - { key: "AutoAudienceSideExists", label: "Has Auto?" }, - { key: "AutoAudienceParkingLocation", label: "Parking Location" }, - { - key: "AutoAudienceCanPlacePurplePixel", - label: "Can Place Purple Pixel?", - }, - { - key: "AutoAudienceCanPlaceYellowPixelOnBackboard", - label: "Can Place Yellow Pixel on Backboard?", - }, - { key: "AutoAudienceCanPark", label: "Can Park?" }, - { key: "AutoAudienceWhitePixels", label: "White Pixels Place" }, - { - key: "AutoAudienceAdjustableToFitOurs", - label: "Adjustable to Fit Our Auto?", - }, - ], - Auto: ["AutoSidePreference"], - Teleop: ["CanPlaceOnBackboard", "CanPickUpFromStack", "PixelsMovedAtOnce"], - Endgame: [ - { key: "EndgameCanLaunchDrone", label: "Can Launch Drone?" }, - { key: "EndgameCanHang", label: "Can Hang?" }, - { key: "EndgameCanPark", label: "Can Park?" }, - ], - }; - - const statsLayout: StatsLayout = { - sections: { - Auto: [ - { - stats: [ - { label: "Avg Scored Backstage", key: "AutoScoredBackstage" }, - { label: "Avg Scored Backdrop", key: "AutoScoredBackdrop" }, - ], - label: "Overall Auto Accuracy", - }, - ], - Teleop: [ - { - label: "Avg Scored Backstage", - key: "TeleopScoredBackstage", - }, - { - label: "Avg Mosaics", - key: "Mosaics", - }, - { - label: "Avg Set Lines Reached", - key: "SetLinesReached", - }, - ], - Endgame: [ - { - label: "Avg Landing Zone Reached", - key: "LandingZoneReached", - }, - ], - }, - getGraphDots: ( - quantReports: Report[], - pitReport?: Pitreport, - ) => { - return []; - }, - }; - - const pitStatsLayout: PitStatsLayout = { - overallSlideStats: [ - { - label: "Avg Props", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - return ( - quantitativeReports.reduce( - (acc, report) => - acc + report.data.HasTeamProp + report.data.HasDrone, - 0, - ) / quantitativeReports.length - ); - }, - }, - ], - individualSlideStats: [ - { - label: "Avg Auto Points", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - const autoBackstage = NumericalTotal( - "AutoScoredBackstage", - quantitativeReports, - ); - const autoBackdrop = NumericalTotal( - "AutoScoredBackdrop", - quantitativeReports, - ); - - return ( - Round(autoBackstage + autoBackdrop) / quantitativeReports.length - ); - }, - }, - { - label: "Avg Teleop Points", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - const teleopBackstage = NumericalTotal( - "TeleopScoredBackstage", - quantitativeReports, - ); - const mosaics = NumericalTotal("Mosaics", quantitativeReports); - const setLines = NumericalTotal( - "SetLinesReached", - quantitativeReports, - ); - - return ( - Round(teleopBackstage + mosaics + setLines) / - quantitativeReports.length - ); - }, - }, - { - label: "Avg Endgame Points", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - const landingZone = NumericalTotal( - "LandingZoneReached", - quantitativeReports, - ); - - return Round(landingZone) / quantitativeReports.length; - }, - }, - ], - robotCapabilities: [ - { - label: "Has Team Prop", - key: "HasTeamProp", - }, - { - label: "Has Drone", - key: "HasDrone", - }, - ], - graphStat: { - label: "Avg Props", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - return ( - quantitativeReports.reduce( - (acc, report) => - acc + report.data.HasTeamProp + report.data.HasDrone, - 0, - ) / quantitativeReports.length - ); - }, - }, - }; - - function getBadges( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - card: boolean, - ) { - const badges: Badge[] = getBaseBadges(pitReport, quantitativeReports); - - if (pitReport?.data?.HasDrone) - badges.push({ text: "Has Drone", color: "primary" }); - if (pitReport?.data?.HasTeamProp) - badges.push({ text: "Has Team Prop", color: "info" }); - if (pitReport?.data?.AutoBackstageSideExists) - badges.push({ text: "Has Auto Backstage", color: "success" }); - if (pitReport?.data?.AutoAudienceSideExists) - badges.push({ text: "Has Auto Audience", color: "success" }); - if (pitReport?.data?.AutoBackstageCanPlacePurplePixel) - badges.push({ - text: card - ? "Purple Pixel Backstage Auto" - : "Can Place Purple Pixel In Backstage Auto", - color: "accent", - }); - if (pitReport?.data?.AutoBackstageCanPlaceYellowPixelOnBackboard) - badges.push({ - text: card - ? "Yellow Pixel Backstage Auto" - : "Can Place Yellow Pixel On Backboard In Backstage Auto", - color: "accent", - }); - if (pitReport?.data?.AutoAudienceCanPlacePurplePixel) - badges.push({ - text: card - ? "Purple Pixel Audience Auto" - : "Can Place Purple Pixel In Audience Auto", - color: "secondary", - }); - if (pitReport?.data?.AutoAudienceCanPlaceYellowPixelOnBackboard) - badges.push({ - text: card - ? "Yellow Pixel Audience Auto" - : "Can Place Yellow Pixel On Backboard In Audience Auto", - color: "secondary", - }); - if (pitReport?.data?.AutoBackstageCanPark) - badges.push({ text: "Can Park In Backstage Auto", color: "accent" }); - if (pitReport?.data?.AutoAudienceCanPark) - badges.push({ text: "Can Park In Audience Auto", color: "secondary" }); - if (pitReport?.data?.CanPlaceOnBackboard) - badges.push({ text: "Can Place On Backboard", color: "primary" }); - if (pitReport?.data?.CanPickUpFromStack) - badges.push({ text: "Can Pick Up From Stack", color: "info" }); - if (pitReport?.data?.EndgameCanLaunchDrone) - badges.push({ text: "Can Launch Drone", color: "success" }); - if (pitReport?.data?.EndgameCanHang) - badges.push({ text: "Can Hang", color: "accent" }); - if (pitReport?.data?.EndgameCanPark) - badges.push({ text: "Can Park", color: "primary" }); - - return badges; - } - - /** NOT ACCURATE, just for demo */ - function getAvgPoints(reports: Report[] | undefined) { - console.log("Getting avg points"); - - if (!reports) return 0; - - const autoBackstage = NumericalTotal("AutoScoredBackstage", reports); - const autoBackdrop = NumericalTotal("AutoScoredBackdrop", reports); - const teleopBackstage = NumericalTotal("TeleopScoredBackstage", reports); - const mosaics = NumericalTotal("Mosaics", reports); - const setLines = NumericalTotal("SetLinesReached", reports); - const landingZone = NumericalTotal("LandingZoneReached", reports); - - return ( - Round( - autoBackstage + - autoBackdrop + - teleopBackstage + - mosaics + - setLines + - landingZone, - ) / Math.max(reports.length, 1) - ); - } - - export const game = new Game( - "Center Stage", - 2024, - League.FTC, - QuantitativeData, - PitData, - pitReportLayout, - quantitativeReportLayout, - statsLayout, - pitStatsLayout, - "CenterStage", - "https://www.firstinspires.org/sites/default/files/uploads/resource_library/ftc/centerstage/centerstage.png", - "", - getBadges, - getAvgPoints, - ); -} - -export namespace IntoTheDeep { - export class QuantitativeData extends QuantData { - StartedWith: IntoTheDeepEnums.StartedWith = - IntoTheDeepEnums.StartedWith.Nothing; - - AutoScoredNetZone: number = 0; - AutoScoredLowNet: number = 0; - AutoScoredHighNet: number = 0; - AutoScoredLowRung: number = 0; - AutoScoredHighRung: number = 0; - - TeleopScoredNetZone: number = 0; - TeleopScoredLowNet: number = 0; - TeleopScoredHighNet: number = 0; - TeleopScoredLowRung: number = 0; - TeleopScoredHighRung: number = 0; - - EndgameLevelClimbed: IntoTheDeepEnums.EndgameLevelClimbed = - IntoTheDeepEnums.EndgameLevelClimbed.None; - } - - export class PitData extends PitReportData { - CanPlaceInLowerBasket: boolean = false; - CanPlaceInUpperBasket: boolean = false; - CanPlaceOnLowerRung: boolean = false; - CanPlaceOnUpperRung: boolean = false; - HighestHangLevel: IntoTheDeepEnums.EndgameLevelClimbed = - IntoTheDeepEnums.EndgameLevelClimbed.None; - SamplesScoredInAuto: number = 0; - SpecimensScoredInAuto: number = 0; - AutonomousStrategy: string = ""; - AutoStartPreferred: FieldPos = FieldPos.Zero; - AutoEndPreferred: FieldPos = FieldPos.Zero; - GameStrategy: string = ""; - } - - const pitReportLayout: FormLayoutProps = { - Capabilities: [ - { key: "CanPlaceInLowerBasket", label: "Can Place in Lower Basket?" }, - { key: "CanPlaceInUpperBasket", label: "Can Place in Upper Basket?" }, - { key: "CanPlaceOnLowerRung", label: "Can Place on Lower Rung?" }, - { key: "CanPlaceOnUpperRung", label: "Can Place on Upper Rung?" }, - { key: "HighestHangLevel", label: "Highest Hang Level" }, - ], - Auto: [ - { key: "SamplesScoredInAuto", label: "Samples Scored in Auto" }, - { key: "SpecimensScoredInAuto", label: "Specimens Scored in Auto" }, - { key: "AutonomousStrategy", label: "Autonomous Strategy" }, - { key: "AutoStartPreferred", label: "Preferred Auto Start Position" }, - { key: "AutoEndPreferred", label: "Preferred Auto End Position" }, - ], - General: [{ key: "GameStrategy", label: "Game Strategy" }], - }; - - const quantitativeReportLayout: FormLayoutProps = { - "Pre-Match": ["StartedWith"], - Auto: [ - [["AutoScoredNetZone"], ["AutoScoredLowNet"], ["AutoScoredHighNet"]], - [["AutoScoredLowRung"], ["AutoScoredHighRung"]], - ], - "Teleop & Endgame": [ - [ - ["TeleopScoredNetZone"], - ["TeleopScoredLowNet"], - ["TeleopScoredHighNet"], - ], - [["TeleopScoredLowRung"], ["TeleopScoredHighRung"]], - "EndgameLevelClimbed", - ], - }; - - const statsLayout: StatsLayout = { - sections: { - Auto: [ - { - key: "AutoScoredNetZone", - label: "Avg Scored Net Zone", - }, - { - stats: [ - { label: "Avg Scored Low Net", key: "AutoScoredLowNet" }, - { label: "Avg Scored High Net", key: "AutoScoredHighNet" }, - ], - label: "Overall Auto % in Low Net", - }, - { - stats: [ - { label: "Avg Scored Low Rung", key: "AutoScoredLowRung" }, - { label: "Avg Scored High Rung", key: "AutoScoredHighRung" }, - ], - label: "Overall Auto % on Low Rung", - }, - ], - Teleop: [ - { - key: "TeleopScoredNetZone", - label: "Avg Scored Net Zone", - }, - { - stats: [ - { label: "Avg Scored Low Net", key: "TeleopScoredLowNet" }, - { label: "Avg Scored High Net", key: "TeleopScoredHighNet" }, - ], - label: "Overall Teleop % in Low Net", - }, - { - stats: [ - { label: "Avg Scored Low Rung", key: "TeleopScoredLowRung" }, - { label: "Avg Scored High Rung", key: "TeleopScoredHighRung" }, - ], - label: "Overall Teleop % on Low Rung", - }, - ], - Endgame: [ - { - label: "Avg Level Climbed", - key: "EndgameLevelClimbed", - }, - ], - }, - getGraphDots: ( - quantReports: Report[], - pitReport?: Pitreport, - ) => { - return [ - { - ...pitReport?.data?.AutoStartPreferred, - color: { r: 255, g: 0, b: 0, a: 255 }, - size: 10, - label: "Red dot is preferred auto start", - }, - { - ...pitReport?.data?.AutoEndPreferred, - color: { r: 0, g: 0, b: 255, a: 255 }, - size: 10, - label: "Blue dot is preferred auto end", - }, - ]; - }, - }; - - const pitStatsLayout: PitStatsLayout = { - overallSlideStats: [ - { - label: "Avg Samples Scored in Auto", - key: "SamplesScoredInAuto", - }, - { - label: "Avg Specimens Scored in Auto", - key: "SpecimensScoredInAuto", - }, - ], - individualSlideStats: [ - { - label: "Avg Auto Points", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - const netZone = NumericalTotal( - "AutoScoredNetZone", - quantitativeReports, - ); - const lowNet = NumericalTotal( - "AutoScoredLowNet", - quantitativeReports, - ); - const highNet = NumericalTotal( - "AutoScoredHighNet", - quantitativeReports, - ); - const lowRung = NumericalTotal( - "AutoScoredLowRung", - quantitativeReports, - ); - const highRung = NumericalTotal( - "AutoScoredHighRung", - quantitativeReports, - ); - - return ( - Round(netZone + lowNet + highNet + lowRung + highRung) / - quantitativeReports.length - ); - }, - }, - { - label: "Avg Teleop Points", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - const netZone = NumericalTotal( - "TeleopScoredNetZone", - quantitativeReports, - ); - const lowNet = NumericalTotal( - "TeleopScoredLowNet", - quantitativeReports, - ); - const highNet = NumericalTotal( - "TeleopScoredHighNet", - quantitativeReports, - ); - const lowRung = NumericalTotal( - "TeleopScoredLowRung", - quantitativeReports, - ); - const highRung = NumericalTotal( - "TeleopScoredHighRung", - quantitativeReports, - ); - - return ( - Round(netZone + lowNet + highNet + lowRung + highRung) / - quantitativeReports.length - ); - }, - }, - { - label: "Avg Endgame Points", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - const climbed = NumericalTotal( - "EndgameLevelClimbed", - quantitativeReports, - ); - - return Round(climbed) / quantitativeReports.length; - }, - }, - ], - robotCapabilities: [ - { - label: "Can Place in Lower Basket", - key: "CanPlaceInLowerBasket", - }, - { - label: "Can Place in Upper Basket", - key: "CanPlaceInUpperBasket", - }, - { - label: "Can Place on Lower Rung", - key: "CanPlaceOnLowerRung", - }, - { - label: "Can Place on Upper Rung", - key: "CanPlaceOnUpperRung", - }, - ], - graphStat: { - label: "Avg Samples Scored in Auto", - key: "SamplesScoredInAuto", - }, - }; - - function getBadges( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - card: boolean, - ) { - const badges: Badge[] = getBaseBadges(pitReport, quantitativeReports); - - if (pitReport?.data?.CanPlaceInLowerBasket) - badges.push({ text: "Can Place in Lower Basket", color: "primary" }); - if (pitReport?.data?.CanPlaceInUpperBasket) - badges.push({ text: "Can Place in Upper Basket", color: "info" }); - if (pitReport?.data?.CanPlaceOnLowerRung) - badges.push({ text: "Can Place on Lower Rung", color: "success" }); - if (pitReport?.data?.CanPlaceOnUpperRung) - badges.push({ text: "Can Place on Upper Rung", color: "warning" }); - - return badges; - } - - function getAvgPoints(reports: Report[] | undefined) { - if (!reports) return 0; - - let totalPoints = 0; - for (const report of reports.map((r) => r.data)) { - switch (report.EndgameLevelClimbed) { - case IntoTheDeepEnums.EndgameLevelClimbed.Parked: - case IntoTheDeepEnums.EndgameLevelClimbed.TouchedLowRung: - totalPoints += 3; - break; - case IntoTheDeepEnums.EndgameLevelClimbed.LowLevelClimb: - totalPoints += 15; - break; - case IntoTheDeepEnums.EndgameLevelClimbed.HighLevelClimb: - totalPoints += 30; - break; +/** + * Given a pit report, returns the game ID of the game it is for. + */ +export function detectGameIdFromPitReport( + pitReport: Pitreport, +): GameId | undefined { + if (!pitReport.data) return undefined; + + for (const gameId in games) { + let match = true; + for (const key in new games[gameId as GameId].pitDataType()) { + if (!(key in pitReport.data)) { + match = false; + break; } - - totalPoints += - (report.AutoScoredNetZone + report.TeleopScoredNetZone) * 2; - totalPoints += (report.AutoScoredLowNet + report.TeleopScoredLowNet) * 4; - totalPoints += - (report.AutoScoredHighNet + report.TeleopScoredHighNet) * 8; - totalPoints += - (report.AutoScoredLowRung + report.TeleopScoredLowRung) * 6; - totalPoints += - (report.AutoScoredHighRung + report.TeleopScoredHighRung) * 10; } - // Avoid divide by 0! - return totalPoints / Math.max(reports.length, 1); - } - - export const game = new Game( - "Into the Deep", - 2025, - League.FTC, - QuantitativeData, - PitData, - pitReportLayout, - quantitativeReportLayout, - statsLayout, - pitStatsLayout, - "IntoTheDeep", - "https://info.firstinspires.org/hubfs/Dive/into-the-deep.svg", - "invert", - getBadges, - getAvgPoints, - ); -} - -namespace Reefscape { - export class QuantitativeData extends QuantData { - AutoMovedPastStartingline: boolean = false; - - AutoCoralScoredLevelOne: number = 0; - AutoCoralScoredLevelTwo: number = 0; - AutoCoralScoredLevelThree: number = 0; - AutoCoralScoredLevelFour: number = 0; - - AutoAlgaeRemovedFromReef: number = 0; - AutoAlgaeScoredProcessor: number = 0; - AutoAlgaeScoredNet: number = 0; - - TeleopCoralScoredLevelOne: number = 0; - TeleopCoralScoredLevelTwo: number = 0; - TeleopCoralScoredLevelThree: number = 0; - TeleopCoralScoredLevelFour: number = 0; - - TeleopAlgaeRemovedFromReef: number = 0; - TeleopAlgaeScoredProcessor: number = 0; - TeleopAlgaeScoredNet: number = 0; - - EndgameClimbStatus: ReefscapeEnums.EndgameClimbStatus = - ReefscapeEnums.EndgameClimbStatus.None; - EndGameDefenseStatus: Defense = Defense.None; - } - - export class PitData extends PitReportData { - CanDriveUnderShallowCage: boolean = false; - GroundIntake: boolean = false; - DriveThroughDeepCage: ReefscapeEnums.DriveThroughDeepCage = - ReefscapeEnums.DriveThroughDeepCage.No; - AutoCapabilities: ReefscapeEnums.AutoCapabilities = - ReefscapeEnums.AutoCapabilities.NoAuto; - CanRemoveAlgae: boolean = false; - CanScoreAlgaeInProcessor: boolean = false; - CanScoreAlgaeInNet: boolean = false; - CanScoreCoral1: boolean = false; - CanScoreCoral2: boolean = false; - CanScoreCoral3: boolean = false; - CanScoreCoral4: boolean = false; - AlgaeScoredAuto: number = 0; - CoralScoredAuto: number = 0; - Climbing: ReefscapeEnums.Climbing = ReefscapeEnums.Climbing.No; - } - - const pitReportLayout: FormLayoutProps = { - Capabilities: [ - { - key: "CanDriveUnderShallowCage", - label: "Can Drive Under Shallow Cage?", - }, - { key: "GroundIntake", label: "Has Ground Intake?" }, - { key: "CanRemoveAlgae", label: "Can Remove Algae?" }, - { - key: "CanScoreAlgaeInProcessor", - label: "Can Score Algae in Processor?", - }, - { key: "CanScoreAlgaeInNet", label: "Can Score Algae in Net?" }, - { key: "Climbing", label: "Climbing?" }, - { key: "CanScoreCoral1", label: "Can Score Coral at L1?" }, - { key: "CanScoreCoral2", label: "Can Score Coral at L2?" }, - { key: "CanScoreCoral3", label: "Can Score Coral at L3?" }, - { key: "CanScoreCoral4", label: "Can Score Coral at L4?" }, - ], - "Auto (Describe more in comments)": [ - { key: "AutoCapabilities", label: "Auto Capabilities?" }, - { key: "CoralScoredAuto", label: "Average Coral Scored In Auto" }, - { key: "AlgaeScoredAuto", label: "Average Algae Scored In Auto" }, - ], - }; - - const quantitativeReportLayout: FormLayoutProps = { - Auto: [ - { key: "AutoMovedPastStartingLine", label: "Moved Past Starting Line" }, - [ - [ - { - key: "AutoCoralScoredLevelOne", - label: "Coral Scored Level One (Auto)", - }, - { - key: "AutoCoralScoredLevelThree", - label: "Coral Scored Level Three (Auto)", - }, - ], - [ - { - key: "AutoCoralScoredLevelTwo", - label: "Coral Scored Level Two (Auto)", - }, - { - key: "AutoCoralScoredLevelFour", - label: "Coral Scored Level Four (Auto)", - }, - ], - ], - [ - [ - { - key: "AutoAlgaeRemovedFromReef", - label: "Algae Removed From Reef (Auto)", - }, - ], - [ - { - key: "AutoAlgaeScoredProcessor", - label: "Algae Scored Processor (Auto)", - }, - ], - [{ key: "AutoAlgaeScoredNet", label: "Algae Scored Net (Auto)" }], - ], - ], - Teleop: [ - [ - [ - { - key: "TeleopCoralScoredLevelOne", - label: "Coral Scored Level One (Teleop)", - }, - { - key: "TeleopCoralScoredLevelThree", - label: "Coral Scored Level Three (Teleop)", - }, - ], - [ - { - key: "TeleopCoralScoredLevelTwo", - label: "Coral Scored Level Two (Teleop)", - }, - { - key: "TeleopCoralScoredLevelFour", - label: "Coral Scored Level Four (Teleop)", - }, - ], - ], - [ - [ - { - key: "TeleopAlgaeRemovedFromReef", - label: "Algae Removed From Reef (Teleop)", - }, - ], - [ - { - key: "TeleopAlgaeScoredProcessor", - label: "Algae Scored Processor (Teleop)", - }, - ], - [{ key: "TeleopAlgaeScoredNet", label: "Algae Scored Net (Teleop)" }], - ], - ], - "Post Match": ["EndgameClimbStatus", "Defense"], - }; - - const statsLayout: StatsLayout = { - sections: { - Auto: [ - { - key: "AutoCoralScoredLevelOne", - label: "Avg Amt Of Coral Scored Level One Auto", - }, - { - label: "> Min Auto L1 Coral", - get(pitData, quantitativeReports) { - return GetMinimum(quantitativeReports!, "AutoCoralScoredLevelOne"); - }, - }, - { - label: "> Max Auto L1 Coral", - get(pitData, quantitativeReports) { - return GetMaximum(quantitativeReports!, "AutoCoralScoredLevelOne"); - }, - }, - { - key: "AutoCoralScoredLevelTwo", - label: "Avg Amt Of Coral Scored Level Two Auto", - }, - { - label: "> Min Auto L2 Coral", - get(pitData, quantitativeReports) { - return GetMinimum(quantitativeReports!, "AutoCoralScoredLevelTwo"); - }, - }, - { - label: "> Max Auto L2 Coral", - get(pitData, quantitativeReports) { - return GetMaximum(quantitativeReports!, "AutoCoralScoredLevelTwo"); - }, - }, - { - key: "AutoCoralScoredLevelThree", - label: "Avg Amt Of Coral Scored Level Three Auto", - }, - { - label: "> Min Auto L3 Coral", - get(pitData, quantitativeReports) { - return GetMinimum( - quantitativeReports!, - "AutoCoralScoredLevelThree", - ); - }, - }, - { - label: "> Max Auto L3 Coral", - get(pitData, quantitativeReports) { - return GetMaximum( - quantitativeReports!, - "AutoCoralScoredLevelThree", - ); - }, - }, - { - key: "AutoCoralScoredLevelFour", - label: "Avg Amt Of Coral Scored Level Four Auto", - }, - { - label: "> Min Auto L4 Coral", - get(pitData, quantitativeReports) { - return GetMinimum(quantitativeReports!, "AutoCoralScoredLevelFour"); - }, - }, - { - label: "> Max Auto L4 Coral", - get(pitData, quantitativeReports) { - return GetMaximum(quantitativeReports!, "AutoCoralScoredLevelFour"); - }, - }, - { - label: "Avg Auto Coral", - get(pitData, quantitativeReports) { - if (!quantitativeReports) return 0; - - return ( - quantitativeReports?.reduce( - (acc, report) => - acc + - report.data.AutoCoralScoredLevelOne + - report.data.AutoCoralScoredLevelTwo + - report.data.AutoCoralScoredLevelThree + - report.data.AutoCoralScoredLevelFour, - 0, - ) / quantitativeReports?.length - ); - }, - }, - { - key: "AutoAlgaeRemovedFromReef", - label: "Avg Amt of Algae Removed From Reef", - }, - { - label: "> Min Algae Removed From Reef", - get(pitData, quantitativeReports) { - return GetMinimum(quantitativeReports!, "AutoAlgaeRemovedFromReef"); - }, - }, - { - label: "> Max Algae Removed From Reef", - get(pitData, quantitativeReports) { - return GetMaximum(quantitativeReports!, "AutoAlgaeRemovedFromReef"); - }, - }, - { - key: "AutoAlgaeScoredProcessor", - label: "Avg Amt of Algae Scored Processor Auto", - }, - { - label: "> Min Algae Scored In Processor", - get(pitData, quantitativeReports) { - return GetMinimum(quantitativeReports!, "AutoAlgaeScoredProcessor"); - }, - }, - { - label: "> Max Algae Scored In Processor", - get(pitData, quantitativeReports) { - return GetMaximum(quantitativeReports!, "AutoAlgaeScoredProcessor"); - }, - }, - { - key: "AutoAlgaeScoredNet", - label: "Avg Amt of Algae Scored Net Auto", - }, - { - label: "> Min Algae Scored In Net", - get(pitData, quantitativeReports) { - return GetMinimum(quantitativeReports!, "AutoAlgaeScoredNet"); - }, - }, - { - label: "> Max Algae Scored In Net", - get(pitData, quantitativeReports) { - return GetMaximum(quantitativeReports!, "AutoAlgaeScoredNet"); - }, - }, - ], - Teleop: [ - { - key: "TeleopCoralScoredLevelOne", - label: "Avg Amt Of Coral Scored Level One Teleop", - }, - { - label: "> Min L1 Coral Scored", - get(pitData, quantitativeReports) { - return GetMinimum( - quantitativeReports!, - "TeleopCoralScoredLevelOne", - ); - }, - }, - { - label: "> Max L1 Coral Scored", - get(pitData, quantitativeReports) { - return GetMaximum( - quantitativeReports!, - "TeleopCoralScoredLevelOne", - ); - }, - }, - { - key: "TeleopCoralScoredLevelTwo", - label: "Avg Amt Of Coral Scored Level Two Teleop", - }, - { - label: "> Min L2 Coral Scored", - get(pitData, quantitativeReports) { - return GetMinimum( - quantitativeReports!, - "TeleopCoralScoredLevelTwo", - ); - }, - }, - { - label: "> Max L2 Coral Scored", - get(pitData, quantitativeReports) { - return GetMaximum( - quantitativeReports!, - "TeleopCoralScoredLevelTwo", - ); - }, - }, - { - key: "TeleopCoralScoredLevelThree", - label: "Avg Amt Of Coral Scored Level Three Teleop", - }, - { - label: "> Min L3 Coral Scored", - get(pitData, quantitativeReports) { - return GetMinimum( - quantitativeReports!, - "TeleopCoralScoredLevelThree", - ); - }, - }, - { - label: "> Max L3 Coral Scored", - get(pitData, quantitativeReports) { - return GetMaximum( - quantitativeReports!, - "TeleopCoralScoredLevelThree", - ); - }, - }, - { - key: "TeleopCoralScoredLevelFour", - label: "Avg Amt Of Coral Scored Level Four Teleop", - }, - { - label: "> Min L4 Coral Scored", - get(pitData, quantitativeReports) { - return GetMinimum( - quantitativeReports!, - "TeleopCoralScoredLevelFour", - ); - }, - }, - { - label: "> Max L4 Coral Scored", - get(pitData, quantitativeReports) { - return GetMaximum( - quantitativeReports!, - "TeleopCoralScoredLevelFour", - ); - }, - }, - { - label: "Avg Teleop Coral", - get(pitData, quantitativeReports) { - if (!quantitativeReports) return 0; - - return ( - quantitativeReports?.reduce( - (acc, report) => - acc + - report.data.TeleopCoralScoredLevelOne + - report.data.TeleopCoralScoredLevelTwo + - report.data.TeleopCoralScoredLevelThree + - report.data.TeleopCoralScoredLevelFour, - 0, - ) / quantitativeReports?.length - ); - }, - }, - { - key: "TeleopAlgaeRemovedFromReef", - label: "Avg Amt of Algae Removed From Reef", - }, - { - label: "> Min Algae Removed From Reef", - get(pitData, quantitativeReports) { - return GetMinimum( - quantitativeReports!, - "TeleopAlgaeRemovedFromReef", - ); - }, - }, - { - label: "> Max Algae Removed From Reef", - get(pitData, quantitativeReports) { - return GetMaximum( - quantitativeReports!, - "TeleopAlgaeRemovedFromReef", - ); - }, - }, - { - key: "TeleopAlgaeScoredProcessor", - label: "Avg Amt of Algae Scored Processor Teleop", - }, - { - label: "> Min Algae Scored In Processor", - get(pitData, quantitativeReports) { - return GetMinimum( - quantitativeReports!, - "TeleopAlgaeScoredProcessor", - ); - }, - }, - { - label: "> Max Algae Scored In Processor", - get(pitData, quantitativeReports) { - return GetMaximum( - quantitativeReports!, - "TeleopAlgaeScoredProcessor", - ); - }, - }, - { - key: "TeleopAlgaeScoredNet", - label: "Avg Amt of Algae Scored Net Teleop", - }, - { - label: "> Min Algae Scored In Net", - get(pitData, quantitativeReports) { - return GetMinimum(quantitativeReports!, "TeleopAlgaeScoredNet"); - }, - }, - { - label: "> Max Algae Scored In Net", - get(pitData, quantitativeReports) { - return GetMaximum(quantitativeReports!, "TeleopAlgaeScoredNet"); - }, - }, - ], - }, - getGraphDots: function ( - quantitativeReports: Report[], - pitReport?: Pitreport | undefined, - ): Dot[] { - return []; - }, - }; - - const pitStatsLayout: PitStatsLayout = { - overallSlideStats: [ - { - label: "Average Algae Scored In Auto", - key: "AlgaeScoredAuto", - }, - { - label: "Average Coral Scored In Auto", - key: "CoralScoredAuto", - }, - ], - individualSlideStats: [ - { - label: "Average Auto Points", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - const CoralLvlOne = NumericalTotal( - "AutoCoralScoredLevelOne", - quantitativeReports, - ); - - const CoralLvlTwo = NumericalTotal( - "AutoCoralScoredLevelTwo", - quantitativeReports, - ); - - const CoralLvlThree = NumericalTotal( - "AutoCoralScoredLevelThree", - quantitativeReports, - ); - - const CoralLvlFour = NumericalTotal( - "AutoCoralScoredLevelFour", - quantitativeReports, - ); - - const AlgaeNet = NumericalTotal( - "AutoAlgaeScoredNet", - quantitativeReports, - ); - - const AlgaeProcessor = NumericalTotal( - "AutoAlgaeScoredProcessor", - quantitativeReports, - ); - - return ( - (CoralLvlOne + - CoralLvlTwo + - CoralLvlThree + - CoralLvlFour + - AlgaeNet + - AlgaeProcessor) / - quantitativeReports.length - ); - }, - }, - { - label: "Average Teleop Points", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - const CoralLvlOne = NumericalTotal( - "TeleopCoralScoredLevelOne", - quantitativeReports, - ); - - const CoralLvlTwo = NumericalTotal( - "TeleopCoralScoredLevelTwo", - quantitativeReports, - ); - - const CoralLvlThree = NumericalTotal( - "TeleopCoralScoredLevelThree", - quantitativeReports, - ); - - const CoralLvlFour = NumericalTotal( - "TeleopCoralScoredLevelFour", - quantitativeReports, - ); - - const AlgaeNet = NumericalTotal( - "TeleopAlgaeScoredNet", - quantitativeReports, - ); - - const AlgaeProcessor = NumericalTotal( - "TeleopAlgaeScoredProcessor", - quantitativeReports, - ); - - return ( - (CoralLvlOne + - CoralLvlTwo + - CoralLvlThree + - CoralLvlFour + - AlgaeNet + - AlgaeProcessor) / - quantitativeReports.length - ); - }, - }, - { - label: "Avg Endgame Points", - get: ( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - ) => { - if (!quantitativeReports) return 0; - - const climbed = NumericalTotal( - "EndgameClimbStatus", - quantitativeReports, - ); - - return Round(climbed) / quantitativeReports.length; - }, - }, - ], - robotCapabilities: [ - { - key: "CanDriveUnderShallowCage", - label: "Can Drive Under Shallow Cage?", - }, - { key: "CanRemoveAlgae", label: "Can Remove Algae?" }, - { - key: "CanScoreAlgaeInProcessor", - label: "Can Score Algae in Processor?", - }, - { key: "CanScoreAlgaeInNet", label: "Can Score Algae in Net?" }, - { key: "Climbing", label: "Climbing?" }, - ], - graphStat: { - label: "Average Algae Scored In The Net During Teleop", - key: "TeleopAlgaeScoredNet", - }, - }; - - function getBadges( - pitReport: Pitreport | undefined, - quantitativeReports: Report[] | undefined, - card: boolean, - ) { - const badges: Badge[] = getBaseBadges(pitReport, quantitativeReports); - - if (pitReport?.data?.CanRemoveAlgae) - badges.push({ text: "Can Remove Algae", color: "primary" }); - if (pitReport?.data?.GroundIntake) - badges.push({ text: "Ground Intake", color: "primary" }); - if (pitReport?.data?.CanScoreAlgaeInNet) - badges.push({ text: "Can Score Algae Net", color: "secondary" }); - if (pitReport?.data?.CanScoreAlgaeInProcessor) - badges.push({ text: "Can Score Algae Processor", color: "success" }); - if (pitReport?.data?.CanDriveUnderShallowCage) - badges.push({ text: "Can Drive Under Shallow Cage", color: "info" }); - - if (pitReport?.data?.CanScoreCoral1) - badges.push({ text: "L1 Coral", color: "info" }); - if (pitReport?.data?.CanScoreCoral2) - badges.push({ text: "L2 Coral", color: "secondary" }); - if (pitReport?.data?.CanScoreCoral3) - badges.push({ text: "L3 Coral", color: "primary" }); - if (pitReport?.data?.CanScoreCoral4) - badges.push({ text: "L4 Coral", color: "accent" }); - if ( - !( - pitReport?.data?.CanScoreCoral1 || - pitReport?.data?.CanScoreCoral2 || - pitReport?.data?.CanScoreCoral3 || - pitReport?.data?.CanScoreCoral4 - ) - ) - badges.push({ text: "No Coral", color: "warning" }); - if (pitReport?.data?.Climbing === ReefscapeEnums.Climbing.Deep) - badges.push({ text: "Deep Climb", color: "secondary" }); - else if (pitReport?.data?.Climbing === ReefscapeEnums.Climbing.Shallow) - badges.push({ text: "Shallow Climb", color: "primary" }); - - return badges; - } - - function getAvgPoints(reports: Report[] | undefined) { - if (!reports) return 0; - - let totalPoints = 0; - - for (const report of reports.map((r) => r.data)) { - switch (report.EndgameClimbStatus) { - case ReefscapeEnums.EndgameClimbStatus.None: - break; - case ReefscapeEnums.EndgameClimbStatus.Park: - totalPoints += 2; - break; - case ReefscapeEnums.EndgameClimbStatus.High: - totalPoints += 6; - break; - case ReefscapeEnums.EndgameClimbStatus.Low: - totalPoints += 12; - break; - } - - totalPoints += - (report.TeleopCoralScoredLevelOne + report.TeleopAlgaeScoredProcessor) * - 2; - totalPoints += - (report.AutoCoralScoredLevelOne + report.TeleopCoralScoredLevelTwo) * 3; - totalPoints += - (report.AutoCoralScoredLevelTwo + - report.TeleopCoralScoredLevelThree + - report.AutoAlgaeScoredNet + - report.TeleopAlgaeScoredNet) * - 4; - totalPoints += report.TeleopCoralScoredLevelFour * 5; - totalPoints += - (report.AutoAlgaeScoredProcessor + report.AutoCoralScoredLevelThree) * - 6; - totalPoints += report.AutoCoralScoredLevelFour * 7; + if (match) { + return gameId as GameId; } - - return totalPoints / Math.max(reports.length, 1); } - - export const game = new Game( - "Reefscape", - 2025, - League.FRC, - QuantitativeData, - PitData, - pitReportLayout, - quantitativeReportLayout, - statsLayout, - pitStatsLayout, - "Reefscape", - "https://info.firstinspires.org/hubfs/Dive/reef-scape.svg", - "invert", - getBadges, - getAvgPoints, - ); } export const games: { [id in GameId]: Game } = Object.freeze({ diff --git a/lib/games/CenterStage.ts b/lib/games/CenterStage.ts new file mode 100644 index 00000000..a6430afa --- /dev/null +++ b/lib/games/CenterStage.ts @@ -0,0 +1,376 @@ +import { NumericalTotal, Round } from "../client/StatsMath"; +import { CenterStageEnums } from "../Enums"; +import { getBaseBadges } from "../games"; +import { FormLayoutProps, StatsLayout, PitStatsLayout, Badge } from "../Layout"; +import { + QuantData, + PitReportData, + Pitreport, + Game, + League, + Report, +} from "../Types"; + +namespace CenterStage { + export class QuantitativeData extends QuantData { + AutoScoredBackstage: number = 0; + AutoScoredBackdrop: number = 0; + AutoPlacedPixelOnSpikeMark: boolean = false; + AutoParked: boolean = false; + + TeleopScoredBackstage: number = 0; + Mosaics: number = 0; + SetLinesReached: number = 0; + + LandingZoneReached: number = 0; + EndgameParked: boolean = false; + EndgameClimbed: boolean = false; + } + + export class PitData extends PitReportData { + AutoBackstageSideExists: boolean = false; + AutoBackstageParkingLocation: CenterStageEnums.CenterStageParkingLocation = + CenterStageEnums.CenterStageParkingLocation.NotApplicable; + AutoBackstageCanPlacePurplePixel: boolean = false; + AutoBackstageCanPlaceYellowPixelOnBackboard: boolean = false; + AutoBackstageCanPark: boolean = false; + AutoBackstageWhitePixels: number = 0; + AutoBackstageAdjustableToFitOurs: CenterStageEnums.AutoAdjustable = + CenterStageEnums.AutoAdjustable.NoNeed; + + AutoAudienceSideExists: boolean = false; + AutoAudienceParkingLocation: CenterStageEnums.CenterStageParkingLocation = + CenterStageEnums.CenterStageParkingLocation.NotApplicable; + AutoAudienceCanPlacePurplePixel: boolean = false; + AutoAudienceCanPlaceYellowPixelOnBackboard: boolean = false; + AutoAudienceCanPark: boolean = false; + AutoAudienceWhitePixels: number = 0; + AutoAudienceAdjustableToFitOurs: CenterStageEnums.AutoAdjustable = + CenterStageEnums.AutoAdjustable.NoNeed; + + AutoSidePreference: CenterStageEnums.AutoSidePreference = + CenterStageEnums.AutoSidePreference.NoPreference; + + CanPlaceOnBackboard: boolean = false; + CanPickUpFromStack: boolean = false; + PixelsMovedAtOnce: number = 0; + + EndgameCanLaunchDrone: boolean = false; + EndgameCanHang: boolean = false; + EndgameCanPark: boolean = false; + } + + const quantitativeReportLayout: FormLayoutProps = { + Auto: [ + [["AutoScoredBackstage"], ["AutoScoredBackdrop"]], + "AutoPlacedPixelOnSpikeMark", + "AutoParked", + ], + Teleop: [[["TeleopScoredBackstage"], ["Mosaics"], ["SetLinesReached"]]], + Endgame: [[["LandingZoneReached"]], "EndgameParked", "EndgameClimbed"], + }; + + const pitReportLayout: FormLayoutProps = { + "Backstage Auto": [ + { key: "AutoBackstageSideExists", label: "Has Auto?" }, + { key: "AutoBackstageParkingLocation", label: "Parking Location" }, + { + key: "AutoBackstageCanPlacePurplePixel", + label: "Can Place Purple Pixel?", + }, + { + key: "AutoBackstageCanPlaceYellowPixelOnBackboard", + label: "Can Place Yellow Pixel on Backboard?", + }, + { key: "AutoBackstageCanPark", label: "Can Park?" }, + { key: "AutoBackstageWhitePixels", label: "White Pixels Place" }, + { + key: "AutoBackstageAdjustableToFitOurs", + label: "Adjustable to Fit Our Auto?", + }, + ], + "Audience Auto": [ + { key: "AutoAudienceSideExists", label: "Has Auto?" }, + { key: "AutoAudienceParkingLocation", label: "Parking Location" }, + { + key: "AutoAudienceCanPlacePurplePixel", + label: "Can Place Purple Pixel?", + }, + { + key: "AutoAudienceCanPlaceYellowPixelOnBackboard", + label: "Can Place Yellow Pixel on Backboard?", + }, + { key: "AutoAudienceCanPark", label: "Can Park?" }, + { key: "AutoAudienceWhitePixels", label: "White Pixels Place" }, + { + key: "AutoAudienceAdjustableToFitOurs", + label: "Adjustable to Fit Our Auto?", + }, + ], + Auto: ["AutoSidePreference"], + Teleop: ["CanPlaceOnBackboard", "CanPickUpFromStack", "PixelsMovedAtOnce"], + Endgame: [ + { key: "EndgameCanLaunchDrone", label: "Can Launch Drone?" }, + { key: "EndgameCanHang", label: "Can Hang?" }, + { key: "EndgameCanPark", label: "Can Park?" }, + ], + }; + + const statsLayout: StatsLayout = { + sections: { + Auto: [ + { + stats: [ + { label: "Avg Scored Backstage", key: "AutoScoredBackstage" }, + { label: "Avg Scored Backdrop", key: "AutoScoredBackdrop" }, + ], + label: "Overall Auto Accuracy", + }, + ], + Teleop: [ + { + label: "Avg Scored Backstage", + key: "TeleopScoredBackstage", + }, + { + label: "Avg Mosaics", + key: "Mosaics", + }, + { + label: "Avg Set Lines Reached", + key: "SetLinesReached", + }, + ], + Endgame: [ + { + label: "Avg Landing Zone Reached", + key: "LandingZoneReached", + }, + ], + }, + getGraphDots: ( + quantReports: Report[], + pitReport?: Pitreport, + ) => { + return []; + }, + }; + + const pitStatsLayout: PitStatsLayout = { + overallSlideStats: [ + { + label: "Avg Props", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + return ( + quantitativeReports.reduce( + (acc, report) => + acc + report.data.HasTeamProp + report.data.HasDrone, + 0, + ) / quantitativeReports.length + ); + }, + }, + ], + individualSlideStats: [ + { + label: "Avg Auto Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const autoBackstage = NumericalTotal( + "AutoScoredBackstage", + quantitativeReports, + ); + const autoBackdrop = NumericalTotal( + "AutoScoredBackdrop", + quantitativeReports, + ); + + return ( + Round(autoBackstage + autoBackdrop) / quantitativeReports.length + ); + }, + }, + { + label: "Avg Teleop Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const teleopBackstage = NumericalTotal( + "TeleopScoredBackstage", + quantitativeReports, + ); + const mosaics = NumericalTotal("Mosaics", quantitativeReports); + const setLines = NumericalTotal( + "SetLinesReached", + quantitativeReports, + ); + + return ( + Round(teleopBackstage + mosaics + setLines) / + quantitativeReports.length + ); + }, + }, + { + label: "Avg Endgame Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const landingZone = NumericalTotal( + "LandingZoneReached", + quantitativeReports, + ); + + return Round(landingZone) / quantitativeReports.length; + }, + }, + ], + robotCapabilities: [ + { + label: "Has Team Prop", + key: "HasTeamProp", + }, + { + label: "Has Drone", + key: "HasDrone", + }, + ], + graphStat: { + label: "Avg Props", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + return ( + quantitativeReports.reduce( + (acc, report) => + acc + report.data.HasTeamProp + report.data.HasDrone, + 0, + ) / quantitativeReports.length + ); + }, + }, + }; + + function getBadges( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + card: boolean, + ) { + const badges: Badge[] = getBaseBadges(pitReport, quantitativeReports); + + if (pitReport?.data?.HasDrone) + badges.push({ text: "Has Drone", color: "primary" }); + if (pitReport?.data?.HasTeamProp) + badges.push({ text: "Has Team Prop", color: "info" }); + if (pitReport?.data?.AutoBackstageSideExists) + badges.push({ text: "Has Auto Backstage", color: "success" }); + if (pitReport?.data?.AutoAudienceSideExists) + badges.push({ text: "Has Auto Audience", color: "success" }); + if (pitReport?.data?.AutoBackstageCanPlacePurplePixel) + badges.push({ + text: card + ? "Purple Pixel Backstage Auto" + : "Can Place Purple Pixel In Backstage Auto", + color: "accent", + }); + if (pitReport?.data?.AutoBackstageCanPlaceYellowPixelOnBackboard) + badges.push({ + text: card + ? "Yellow Pixel Backstage Auto" + : "Can Place Yellow Pixel On Backboard In Backstage Auto", + color: "accent", + }); + if (pitReport?.data?.AutoAudienceCanPlacePurplePixel) + badges.push({ + text: card + ? "Purple Pixel Audience Auto" + : "Can Place Purple Pixel In Audience Auto", + color: "secondary", + }); + if (pitReport?.data?.AutoAudienceCanPlaceYellowPixelOnBackboard) + badges.push({ + text: card + ? "Yellow Pixel Audience Auto" + : "Can Place Yellow Pixel On Backboard In Audience Auto", + color: "secondary", + }); + if (pitReport?.data?.AutoBackstageCanPark) + badges.push({ text: "Can Park In Backstage Auto", color: "accent" }); + if (pitReport?.data?.AutoAudienceCanPark) + badges.push({ text: "Can Park In Audience Auto", color: "secondary" }); + if (pitReport?.data?.CanPlaceOnBackboard) + badges.push({ text: "Can Place On Backboard", color: "primary" }); + if (pitReport?.data?.CanPickUpFromStack) + badges.push({ text: "Can Pick Up From Stack", color: "info" }); + if (pitReport?.data?.EndgameCanLaunchDrone) + badges.push({ text: "Can Launch Drone", color: "success" }); + if (pitReport?.data?.EndgameCanHang) + badges.push({ text: "Can Hang", color: "accent" }); + if (pitReport?.data?.EndgameCanPark) + badges.push({ text: "Can Park", color: "primary" }); + + return badges; + } + + /** NOT ACCURATE, just for demo */ + function getAvgPoints(reports: Report[] | undefined) { + console.log("Getting avg points"); + + if (!reports) return 0; + + const autoBackstage = NumericalTotal("AutoScoredBackstage", reports); + const autoBackdrop = NumericalTotal("AutoScoredBackdrop", reports); + const teleopBackstage = NumericalTotal("TeleopScoredBackstage", reports); + const mosaics = NumericalTotal("Mosaics", reports); + const setLines = NumericalTotal("SetLinesReached", reports); + const landingZone = NumericalTotal("LandingZoneReached", reports); + + return ( + Round( + autoBackstage + + autoBackdrop + + teleopBackstage + + mosaics + + setLines + + landingZone, + ) / Math.max(reports.length, 1) + ); + } + + export const game = new Game( + "Center Stage", + 2024, + League.FTC, + QuantitativeData, + PitData, + pitReportLayout, + quantitativeReportLayout, + statsLayout, + pitStatsLayout, + "CenterStage", + "https://www.firstinspires.org/sites/default/files/uploads/resource_library/ftc/centerstage/centerstage.png", + "", + getBadges, + getAvgPoints, + ); +} + +export default CenterStage; diff --git a/lib/games/Crescendo.ts b/lib/games/Crescendo.ts new file mode 100644 index 00000000..12d2e42f --- /dev/null +++ b/lib/games/Crescendo.ts @@ -0,0 +1,417 @@ +import { + NumericalTotal, + SpeakerAutoPoints, + SpeakerTeleopPoints, + AmpAutoPoints, + AmpTeleopPoints, + TrapPoints, + Round, + BooleanAverage, +} from "../client/StatsMath"; +import { IntakeTypes } from "../Enums"; +import { getBaseBadges } from "../games"; +import { FormLayoutProps, StatsLayout, PitStatsLayout, Badge } from "../Layout"; +import { + QuantData, + PitReportData, + Pitreport, + Game, + League, + Report, +} from "../Types"; + +namespace Crescendo { + export class QuantitativeData extends QuantData { + AutoScoredAmp: number = 0; // # of times scored in the amp + AutoMissedAmp: number = 0; + AutoScoredSpeaker: number = 0; + AutoMissedSpeaker: number = 0; + MovedOut: boolean = false; + + TeleopScoredAmp: number = 0; + TeleopMissedAmp: number = 0; + TeleopScoredSpeaker: number = 0; + TeleopMissedSpeaker: number = 0; + TeleopScoredTrap: number = 0; + TeleopMissedTrap: number = 0; + TeleopPassed: number = 0; + + Coopertition: boolean = false; // true if used any point in match + ClimbedStage: boolean = false; + ParkedStage: boolean = false; + UnderStage: boolean = false; + + intakeType: IntakeTypes = IntakeTypes.Human; + } + + export class PitData extends PitReportData { + intakeType: IntakeTypes = IntakeTypes.None; + canClimb: boolean = false; + fixedShooter: boolean = false; + canScoreAmp: boolean = false; + canScoreSpeaker: boolean = false; + canScoreFromDistance: boolean = false; + underBumperIntake: boolean = false; + autoNotes: number = 0; + } + + const pitReportLayout: FormLayoutProps = { + Intake: ["intakeType"], + Shooter: [ + "canScoreAmp", + "canScoreSpeaker", + "fixedShooter", + "canScoreFromDistance", + ], + Climber: ["canClimb"], + Auto: [{ key: "autoNotes", type: "number" }], + }; + + const quantitativeReportLayout: FormLayoutProps = { + Auto: [ + "MovedOut", + [ + ["AutoScoredAmp", "AutoMissedAmp"], + ["AutoScoredSpeaker", "AutoMissedSpeaker"], + ], + ], + Teleop: [ + [ + ["TeleopScoredAmp", "TeleopMissedAmp"], + ["TeleopScoredSpeaker", "TeleopMissedSpeaker"], + ["TeleopScoredTrap", "TeleopMissedTrap"], + ], + [[{ key: "TeleopPassed", label: "Notes Passed" }]], + "Defense", + ], + Summary: [ + { key: "Coopertition", label: "Coopertition Activated" }, + "ClimbedStage", + "ParkedStage", + { key: "UnderStage", label: "Went Under Stage" }, + ], + }; + + const statsLayout: StatsLayout = { + sections: { + Auto: [ + { + stats: [ + { label: "Avg Scored Amp Shots", key: "AutoScoredAmp" }, + { label: "Avg Missed Amp Shots", key: "AutoMissedAmp" }, + ], + label: "Overall Amp Accuracy", + }, + { + stats: [ + { label: "Avg Scored Speaker Shots", key: "AutoScoredSpeaker" }, + { label: "Avg Missed Speaker Shots", key: "AutoMissedSpeaker" }, + ], + label: "Overall Speaker Accuracy", + }, + ], + Teleop: [ + { + stats: [ + { label: "Avg Scored Amp Shots", key: "TeleopScoredAmp" }, + { label: "Avg Missed Amp Shots", key: "TeleopMissedAmp" }, + ], + label: "Overall Amp Accuracy", + }, + { + stats: [ + { label: "Avg Scored Speaker Shots", key: "TeleopScoredSpeaker" }, + { label: "Avg Missed Speaker Shots", key: "TeleopMissedSpeaker" }, + ], + label: "Overall Speaker Accuracy", + }, + { + stats: [ + { label: "Avg Scored Trap Shots", key: "TeleopScoredTrap" }, + { label: "Avg Missed Trap Shots", key: "TeleopMissedTrap" }, + ], + label: "Overall Trap Accuracy", + }, + { + key: "TeleopPassed", + label: "Notes Passed", + }, + ], + }, + getGraphDots: ( + quantReports: Report[], + pitReport?: Pitreport, + ) => { + return []; + }, + }; + + const pitStatsLayout: PitStatsLayout = { + overallSlideStats: [ + { + label: "Avg Notes Scored", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + return ( + quantitativeReports.reduce( + (acc, report) => + acc + + report.data.AutoScoredSpeaker + + report.data.TeleopMissedSpeaker + + report.data.AutoScoredAmp + + report.data.TeleopScoredAmp + + report.data.TeleopScoredTrap, + 0, + ) / quantitativeReports.length + ); + }, + }, + { + label: "Teleop Speaker Accuracy", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const scores = quantitativeReports.map( + (report) => report.data.TeleopScoredSpeaker, + ); + const misses = quantitativeReports.map( + (report) => report.data.TeleopMissedSpeaker, + ); + + const scoreCount = scores.reduce((acc, score) => acc + score, 0); + const missCount = misses.reduce((acc, miss) => acc + miss, 0); + + return scoreCount / (scoreCount + missCount); + }, + }, + { + label: "Teleop Amp Accuracy", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const scores = quantitativeReports.map( + (report) => report.data.TeleopScoredAmp, + ); + const misses = quantitativeReports.map( + (report) => report.data.TeleopMissedAmp, + ); + + const scoreCount = scores.reduce((acc, score) => acc + score, 0); + const missCount = misses.reduce((acc, miss) => acc + miss, 0); + + return scoreCount / (scoreCount + missCount); + }, + }, + { + label: "Avg Notes in Trap", + key: "TeleopScoredTrap", + }, + ], + individualSlideStats: [ + { + label: "Avg Teleop Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const speakerAuto = + NumericalTotal("AutoScoredSpeaker", quantitativeReports) * + SpeakerAutoPoints; + const speakerTeleop = + NumericalTotal("TeleopScoredAmp", quantitativeReports) * + SpeakerTeleopPoints; + const ampAuto = + NumericalTotal("AutoScoredAmp", quantitativeReports) * + AmpAutoPoints; + const ampTeleop = + NumericalTotal("TeleopScoredAmp", quantitativeReports) * + AmpTeleopPoints; + const trap = + NumericalTotal("TeleopScoredTrap", quantitativeReports) * + TrapPoints; + + return ( + Round(speakerAuto + speakerTeleop + ampAuto + ampTeleop + trap) / + quantitativeReports.length + ); + }, + }, + { + label: "Avg Auto Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const speakerAuto = + NumericalTotal("AutoScoredSpeaker", quantitativeReports) * + SpeakerAutoPoints; + const ampAuto = + NumericalTotal("AutoScoredAmp", quantitativeReports) * + AmpAutoPoints; + + return Round(speakerAuto + ampAuto) / quantitativeReports.length; + }, + }, + { + label: "Avg Speaker Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const speakerAuto = + NumericalTotal("AutoScoredSpeaker", quantitativeReports) * + SpeakerAutoPoints; + const speakerTeleop = + NumericalTotal("TeleopScoredAmp", quantitativeReports) * + SpeakerTeleopPoints; + + return ( + Round(speakerAuto + speakerTeleop) / quantitativeReports.length + ); + }, + }, + { + label: "Avg Amp Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const ampAuto = + NumericalTotal("AutoScoredAmp", quantitativeReports) * + AmpAutoPoints; + const ampTeleop = + NumericalTotal("TeleopScoredAmp", quantitativeReports) * + AmpTeleopPoints; + + return Round(ampAuto + ampTeleop) / quantitativeReports.length; + }, + }, + ], + robotCapabilities: [ + { + label: "Intake Type", + key: "intakeType", + }, + ], + graphStat: { + label: "Avg Notes Scored", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + return ( + quantitativeReports.reduce( + (acc, report) => + acc + + report.data.AutoScoredSpeaker + + report.data.TeleopMissedSpeaker + + report.data.AutoScoredAmp + + report.data.TeleopScoredAmp + + report.data.TeleopScoredTrap, + 0, + ) / quantitativeReports.length + ); + }, + }, + }; + + function getAvgPoints(reports: Report[] | undefined) { + if (!reports) return 0; + + const speakerAuto = + NumericalTotal("AutoScoredSpeaker", reports) * SpeakerAutoPoints; + const speakerTeleop = + NumericalTotal("TeleopScoredAmp", reports) * SpeakerTeleopPoints; + const ampAuto = NumericalTotal("AutoScoredAmp", reports) * AmpAutoPoints; + const ampTeleop = + NumericalTotal("TeleopScoredAmp", reports) * AmpTeleopPoints; + const trap = NumericalTotal("TeleopScoredTrap", reports) * TrapPoints; + + return ( + Round(speakerAuto + speakerTeleop + ampAuto + ampTeleop + trap) / + reports.length + ); + } + + function getBadges( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + card: boolean, + ) { + const pitData = pitReport?.data; + const badges = getBaseBadges(pitReport, quantitativeReports); + + const intake = pitData?.intakeType; + const cooperates = BooleanAverage( + "Coopertition", + quantitativeReports ?? [], + ); + const climbs = BooleanAverage("ClimbedStage", quantitativeReports ?? []); + const parks = BooleanAverage("ParkedStage", quantitativeReports ?? []); + const understage = BooleanAverage("UnderStage", quantitativeReports ?? []); + + if (pitReport?.submitted && intake) { + const intakeBadge: Badge = { text: intake, color: "primary" }; + if (intake === IntakeTypes.Human) { + intakeBadge.color = "warning"; + } else if (intake === IntakeTypes.Both) { + intakeBadge.color = "secondary"; + } else if (intake === IntakeTypes.None) { + intakeBadge.color = "warning"; + intakeBadge.text = "No Intake"; + } + + badges.push(intakeBadge); + } + + if (cooperates) badges.push({ text: "Cooperates", color: "success" }); + + if (climbs) badges.push({ text: "Climbs", color: "accent" }); + + if (parks) badges.push({ text: "Parks", color: "primary" }); + + if (understage) + badges.push({ text: "Can Go Under Stage", color: "success" }); + + return badges; + } + + export const game = new Game( + "Crescendo", + 2024, + League.FRC, + QuantitativeData, + PitData, + pitReportLayout, + quantitativeReportLayout, + statsLayout, + pitStatsLayout, + "Crescendo", + "https://www.firstinspires.org/sites/default/files/uploads/resource_library/frc/crescendo/crescendo.png", + "", + getBadges, + getAvgPoints, + ); +} + +export default Crescendo; diff --git a/lib/games/IntoTheDeep.ts b/lib/games/IntoTheDeep.ts new file mode 100644 index 00000000..fe726485 --- /dev/null +++ b/lib/games/IntoTheDeep.ts @@ -0,0 +1,349 @@ +import { NumericalTotal, Round } from "../client/StatsMath"; +import { IntoTheDeepEnums } from "../Enums"; +import { getBaseBadges } from "../games"; +import { FormLayoutProps, StatsLayout, PitStatsLayout, Badge } from "../Layout"; +import { + QuantData, + PitReportData, + FieldPos, + Pitreport, + Game, + League, + Report, +} from "../Types"; + +namespace IntoTheDeep { + export class QuantitativeData extends QuantData { + StartedWith: IntoTheDeepEnums.StartedWith = + IntoTheDeepEnums.StartedWith.Nothing; + + AutoScoredNetZone: number = 0; + AutoScoredLowNet: number = 0; + AutoScoredHighNet: number = 0; + AutoScoredLowRung: number = 0; + AutoScoredHighRung: number = 0; + + TeleopScoredNetZone: number = 0; + TeleopScoredLowNet: number = 0; + TeleopScoredHighNet: number = 0; + TeleopScoredLowRung: number = 0; + TeleopScoredHighRung: number = 0; + + EndgameLevelClimbed: IntoTheDeepEnums.EndgameLevelClimbed = + IntoTheDeepEnums.EndgameLevelClimbed.None; + } + + export class PitData extends PitReportData { + CanPlaceInLowerBasket: boolean = false; + CanPlaceInUpperBasket: boolean = false; + CanPlaceOnLowerRung: boolean = false; + CanPlaceOnUpperRung: boolean = false; + HighestHangLevel: IntoTheDeepEnums.EndgameLevelClimbed = + IntoTheDeepEnums.EndgameLevelClimbed.None; + SamplesScoredInAuto: number = 0; + SpecimensScoredInAuto: number = 0; + AutonomousStrategy: string = ""; + AutoStartPreferred: FieldPos = FieldPos.Zero; + AutoEndPreferred: FieldPos = FieldPos.Zero; + GameStrategy: string = ""; + } + + const pitReportLayout: FormLayoutProps = { + Capabilities: [ + { key: "CanPlaceInLowerBasket", label: "Can Place in Lower Basket?" }, + { key: "CanPlaceInUpperBasket", label: "Can Place in Upper Basket?" }, + { key: "CanPlaceOnLowerRung", label: "Can Place on Lower Rung?" }, + { key: "CanPlaceOnUpperRung", label: "Can Place on Upper Rung?" }, + { key: "HighestHangLevel", label: "Highest Hang Level" }, + ], + Auto: [ + { key: "SamplesScoredInAuto", label: "Samples Scored in Auto" }, + { key: "SpecimensScoredInAuto", label: "Specimens Scored in Auto" }, + { key: "AutonomousStrategy", label: "Autonomous Strategy" }, + { key: "AutoStartPreferred", label: "Preferred Auto Start Position" }, + { key: "AutoEndPreferred", label: "Preferred Auto End Position" }, + ], + General: [{ key: "GameStrategy", label: "Game Strategy" }], + }; + + const quantitativeReportLayout: FormLayoutProps = { + "Pre-Match": ["StartedWith"], + Auto: [ + [["AutoScoredNetZone"], ["AutoScoredLowNet"], ["AutoScoredHighNet"]], + [["AutoScoredLowRung"], ["AutoScoredHighRung"]], + ], + "Teleop & Endgame": [ + [ + ["TeleopScoredNetZone"], + ["TeleopScoredLowNet"], + ["TeleopScoredHighNet"], + ], + [["TeleopScoredLowRung"], ["TeleopScoredHighRung"]], + "EndgameLevelClimbed", + ], + }; + + const statsLayout: StatsLayout = { + sections: { + Auto: [ + { + key: "AutoScoredNetZone", + label: "Avg Scored Net Zone", + }, + { + stats: [ + { label: "Avg Scored Low Net", key: "AutoScoredLowNet" }, + { label: "Avg Scored High Net", key: "AutoScoredHighNet" }, + ], + label: "Overall Auto % in Low Net", + }, + { + stats: [ + { label: "Avg Scored Low Rung", key: "AutoScoredLowRung" }, + { label: "Avg Scored High Rung", key: "AutoScoredHighRung" }, + ], + label: "Overall Auto % on Low Rung", + }, + ], + Teleop: [ + { + key: "TeleopScoredNetZone", + label: "Avg Scored Net Zone", + }, + { + stats: [ + { label: "Avg Scored Low Net", key: "TeleopScoredLowNet" }, + { label: "Avg Scored High Net", key: "TeleopScoredHighNet" }, + ], + label: "Overall Teleop % in Low Net", + }, + { + stats: [ + { label: "Avg Scored Low Rung", key: "TeleopScoredLowRung" }, + { label: "Avg Scored High Rung", key: "TeleopScoredHighRung" }, + ], + label: "Overall Teleop % on Low Rung", + }, + ], + Endgame: [ + { + label: "Avg Level Climbed", + key: "EndgameLevelClimbed", + }, + ], + }, + getGraphDots: ( + quantReports: Report[], + pitReport?: Pitreport, + ) => { + return [ + { + ...pitReport?.data?.AutoStartPreferred, + color: { r: 255, g: 0, b: 0, a: 255 }, + size: 10, + label: "Red dot is preferred auto start", + }, + { + ...pitReport?.data?.AutoEndPreferred, + color: { r: 0, g: 0, b: 255, a: 255 }, + size: 10, + label: "Blue dot is preferred auto end", + }, + ]; + }, + }; + + const pitStatsLayout: PitStatsLayout = { + overallSlideStats: [ + { + label: "Avg Samples Scored in Auto", + key: "SamplesScoredInAuto", + }, + { + label: "Avg Specimens Scored in Auto", + key: "SpecimensScoredInAuto", + }, + ], + individualSlideStats: [ + { + label: "Avg Auto Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const netZone = NumericalTotal( + "AutoScoredNetZone", + quantitativeReports, + ); + const lowNet = NumericalTotal( + "AutoScoredLowNet", + quantitativeReports, + ); + const highNet = NumericalTotal( + "AutoScoredHighNet", + quantitativeReports, + ); + const lowRung = NumericalTotal( + "AutoScoredLowRung", + quantitativeReports, + ); + const highRung = NumericalTotal( + "AutoScoredHighRung", + quantitativeReports, + ); + + return ( + Round(netZone + lowNet + highNet + lowRung + highRung) / + quantitativeReports.length + ); + }, + }, + { + label: "Avg Teleop Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const netZone = NumericalTotal( + "TeleopScoredNetZone", + quantitativeReports, + ); + const lowNet = NumericalTotal( + "TeleopScoredLowNet", + quantitativeReports, + ); + const highNet = NumericalTotal( + "TeleopScoredHighNet", + quantitativeReports, + ); + const lowRung = NumericalTotal( + "TeleopScoredLowRung", + quantitativeReports, + ); + const highRung = NumericalTotal( + "TeleopScoredHighRung", + quantitativeReports, + ); + + return ( + Round(netZone + lowNet + highNet + lowRung + highRung) / + quantitativeReports.length + ); + }, + }, + { + label: "Avg Endgame Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const climbed = NumericalTotal( + "EndgameLevelClimbed", + quantitativeReports, + ); + + return Round(climbed) / quantitativeReports.length; + }, + }, + ], + robotCapabilities: [ + { + label: "Can Place in Lower Basket", + key: "CanPlaceInLowerBasket", + }, + { + label: "Can Place in Upper Basket", + key: "CanPlaceInUpperBasket", + }, + { + label: "Can Place on Lower Rung", + key: "CanPlaceOnLowerRung", + }, + { + label: "Can Place on Upper Rung", + key: "CanPlaceOnUpperRung", + }, + ], + graphStat: { + label: "Avg Samples Scored in Auto", + key: "SamplesScoredInAuto", + }, + }; + + function getBadges( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + card: boolean, + ) { + const badges: Badge[] = getBaseBadges(pitReport, quantitativeReports); + + if (pitReport?.data?.CanPlaceInLowerBasket) + badges.push({ text: "Can Place in Lower Basket", color: "primary" }); + if (pitReport?.data?.CanPlaceInUpperBasket) + badges.push({ text: "Can Place in Upper Basket", color: "info" }); + if (pitReport?.data?.CanPlaceOnLowerRung) + badges.push({ text: "Can Place on Lower Rung", color: "success" }); + if (pitReport?.data?.CanPlaceOnUpperRung) + badges.push({ text: "Can Place on Upper Rung", color: "warning" }); + + return badges; + } + + function getAvgPoints(reports: Report[] | undefined) { + if (!reports) return 0; + + let totalPoints = 0; + for (const report of reports.map((r) => r.data)) { + switch (report.EndgameLevelClimbed) { + case IntoTheDeepEnums.EndgameLevelClimbed.Parked: + case IntoTheDeepEnums.EndgameLevelClimbed.TouchedLowRung: + totalPoints += 3; + break; + case IntoTheDeepEnums.EndgameLevelClimbed.LowLevelClimb: + totalPoints += 15; + break; + case IntoTheDeepEnums.EndgameLevelClimbed.HighLevelClimb: + totalPoints += 30; + break; + } + + totalPoints += + (report.AutoScoredNetZone + report.TeleopScoredNetZone) * 2; + totalPoints += (report.AutoScoredLowNet + report.TeleopScoredLowNet) * 4; + totalPoints += + (report.AutoScoredHighNet + report.TeleopScoredHighNet) * 8; + totalPoints += + (report.AutoScoredLowRung + report.TeleopScoredLowRung) * 6; + totalPoints += + (report.AutoScoredHighRung + report.TeleopScoredHighRung) * 10; + } + + // Avoid divide by 0! + return totalPoints / Math.max(reports.length, 1); + } + + export const game = new Game( + "Into the Deep", + 2025, + League.FTC, + QuantitativeData, + PitData, + pitReportLayout, + quantitativeReportLayout, + statsLayout, + pitStatsLayout, + "IntoTheDeep", + "https://info.firstinspires.org/hubfs/Dive/into-the-deep.svg", + "invert", + getBadges, + getAvgPoints, + ); +} + +export default IntoTheDeep; diff --git a/lib/games/Reefscape.ts b/lib/games/Reefscape.ts new file mode 100644 index 00000000..4b7b9ff8 --- /dev/null +++ b/lib/games/Reefscape.ts @@ -0,0 +1,740 @@ +import { Dot } from "@/components/stats/Heatmap"; +import { + GetMinimum, + GetMaximum, + NumericalTotal, + Round, +} from "../client/StatsMath"; +import { ReefscapeEnums, Defense } from "../Enums"; +import { getBaseBadges } from "../games"; +import { FormLayoutProps, StatsLayout, PitStatsLayout, Badge } from "../Layout"; +import { + QuantData, + PitReportData, + Pitreport, + Game, + League, + Report, +} from "../Types"; + +namespace Reefscape { + export class QuantitativeData extends QuantData { + AutoMovedPastStartingline: boolean = false; + + AutoCoralScoredLevelOne: number = 0; + AutoCoralScoredLevelTwo: number = 0; + AutoCoralScoredLevelThree: number = 0; + AutoCoralScoredLevelFour: number = 0; + + AutoAlgaeRemovedFromReef: number = 0; + AutoAlgaeScoredProcessor: number = 0; + AutoAlgaeScoredNet: number = 0; + + TeleopCoralScoredLevelOne: number = 0; + TeleopCoralScoredLevelTwo: number = 0; + TeleopCoralScoredLevelThree: number = 0; + TeleopCoralScoredLevelFour: number = 0; + + TeleopAlgaeRemovedFromReef: number = 0; + TeleopAlgaeScoredProcessor: number = 0; + TeleopAlgaeScoredNet: number = 0; + + EndgameClimbStatus: ReefscapeEnums.EndgameClimbStatus = + ReefscapeEnums.EndgameClimbStatus.None; + EndGameDefenseStatus: Defense = Defense.None; + } + + export class PitData extends PitReportData { + CanDriveUnderShallowCage: boolean = false; + GroundIntake: boolean = false; + DriveThroughDeepCage: ReefscapeEnums.DriveThroughDeepCage = + ReefscapeEnums.DriveThroughDeepCage.No; + AutoCapabilities: ReefscapeEnums.AutoCapabilities = + ReefscapeEnums.AutoCapabilities.NoAuto; + CanRemoveAlgae: boolean = false; + CanScoreAlgaeInProcessor: boolean = false; + CanScoreAlgaeInNet: boolean = false; + CanScoreCoral1: boolean = false; + CanScoreCoral2: boolean = false; + CanScoreCoral3: boolean = false; + CanScoreCoral4: boolean = false; + AlgaeScoredAuto: number = 0; + CoralScoredAuto: number = 0; + Climbing: ReefscapeEnums.Climbing = ReefscapeEnums.Climbing.No; + } + + const pitReportLayout: FormLayoutProps = { + Capabilities: [ + { + key: "CanDriveUnderShallowCage", + label: "Can Drive Under Shallow Cage?", + }, + { key: "GroundIntake", label: "Has Ground Intake?" }, + { key: "CanRemoveAlgae", label: "Can Remove Algae?" }, + { + key: "CanScoreAlgaeInProcessor", + label: "Can Score Algae in Processor?", + }, + { key: "CanScoreAlgaeInNet", label: "Can Score Algae in Net?" }, + { key: "Climbing", label: "Climbing?" }, + { key: "CanScoreCoral1", label: "Can Score Coral at L1?" }, + { key: "CanScoreCoral2", label: "Can Score Coral at L2?" }, + { key: "CanScoreCoral3", label: "Can Score Coral at L3?" }, + { key: "CanScoreCoral4", label: "Can Score Coral at L4?" }, + ], + "Auto (Describe more in comments)": [ + { key: "AutoCapabilities", label: "Auto Capabilities?" }, + { key: "CoralScoredAuto", label: "Average Coral Scored In Auto" }, + { key: "AlgaeScoredAuto", label: "Average Algae Scored In Auto" }, + ], + }; + + const quantitativeReportLayout: FormLayoutProps = { + Auto: [ + { key: "AutoMovedPastStartingLine", label: "Moved Past Starting Line" }, + [ + [ + { + key: "AutoCoralScoredLevelOne", + label: "Coral Scored Level One (Auto)", + }, + { + key: "AutoCoralScoredLevelThree", + label: "Coral Scored Level Three (Auto)", + }, + ], + [ + { + key: "AutoCoralScoredLevelTwo", + label: "Coral Scored Level Two (Auto)", + }, + { + key: "AutoCoralScoredLevelFour", + label: "Coral Scored Level Four (Auto)", + }, + ], + ], + [ + [ + { + key: "AutoAlgaeRemovedFromReef", + label: "Algae Removed From Reef (Auto)", + }, + ], + [ + { + key: "AutoAlgaeScoredProcessor", + label: "Algae Scored Processor (Auto)", + }, + ], + [{ key: "AutoAlgaeScoredNet", label: "Algae Scored Net (Auto)" }], + ], + ], + Teleop: [ + [ + [ + { + key: "TeleopCoralScoredLevelOne", + label: "Coral Scored Level One (Teleop)", + }, + { + key: "TeleopCoralScoredLevelThree", + label: "Coral Scored Level Three (Teleop)", + }, + ], + [ + { + key: "TeleopCoralScoredLevelTwo", + label: "Coral Scored Level Two (Teleop)", + }, + { + key: "TeleopCoralScoredLevelFour", + label: "Coral Scored Level Four (Teleop)", + }, + ], + ], + [ + [ + { + key: "TeleopAlgaeRemovedFromReef", + label: "Algae Removed From Reef (Teleop)", + }, + ], + [ + { + key: "TeleopAlgaeScoredProcessor", + label: "Algae Scored Processor (Teleop)", + }, + ], + [{ key: "TeleopAlgaeScoredNet", label: "Algae Scored Net (Teleop)" }], + ], + ], + "Post Match": ["EndgameClimbStatus", "Defense"], + }; + + const statsLayout: StatsLayout = { + sections: { + Auto: [ + { + key: "AutoCoralScoredLevelOne", + label: "Avg Amt Of Coral Scored Level One Auto", + }, + { + label: "> Min Auto L1 Coral", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoCoralScoredLevelOne"); + }, + }, + { + label: "> Max Auto L1 Coral", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoCoralScoredLevelOne"); + }, + }, + { + key: "AutoCoralScoredLevelTwo", + label: "Avg Amt Of Coral Scored Level Two Auto", + }, + { + label: "> Min Auto L2 Coral", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoCoralScoredLevelTwo"); + }, + }, + { + label: "> Max Auto L2 Coral", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoCoralScoredLevelTwo"); + }, + }, + { + key: "AutoCoralScoredLevelThree", + label: "Avg Amt Of Coral Scored Level Three Auto", + }, + { + label: "> Min Auto L3 Coral", + get(pitData, quantitativeReports) { + return GetMinimum( + quantitativeReports!, + "AutoCoralScoredLevelThree", + ); + }, + }, + { + label: "> Max Auto L3 Coral", + get(pitData, quantitativeReports) { + return GetMaximum( + quantitativeReports!, + "AutoCoralScoredLevelThree", + ); + }, + }, + { + key: "AutoCoralScoredLevelFour", + label: "Avg Amt Of Coral Scored Level Four Auto", + }, + { + label: "> Min Auto L4 Coral", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoCoralScoredLevelFour"); + }, + }, + { + label: "> Max Auto L4 Coral", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoCoralScoredLevelFour"); + }, + }, + { + label: "Avg Auto Coral", + get(pitData, quantitativeReports) { + if (!quantitativeReports) return 0; + + return ( + quantitativeReports?.reduce( + (acc, report) => + acc + + report.data.AutoCoralScoredLevelOne + + report.data.AutoCoralScoredLevelTwo + + report.data.AutoCoralScoredLevelThree + + report.data.AutoCoralScoredLevelFour, + 0, + ) / quantitativeReports?.length + ); + }, + }, + { + key: "AutoAlgaeRemovedFromReef", + label: "Avg Amt of Algae Removed From Reef", + }, + { + label: "> Min Algae Removed From Reef", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoAlgaeRemovedFromReef"); + }, + }, + { + label: "> Max Algae Removed From Reef", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoAlgaeRemovedFromReef"); + }, + }, + { + key: "AutoAlgaeScoredProcessor", + label: "Avg Amt of Algae Scored Processor Auto", + }, + { + label: "> Min Algae Scored In Processor", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoAlgaeScoredProcessor"); + }, + }, + { + label: "> Max Algae Scored In Processor", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoAlgaeScoredProcessor"); + }, + }, + { + key: "AutoAlgaeScoredNet", + label: "Avg Amt of Algae Scored Net Auto", + }, + { + label: "> Min Algae Scored In Net", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoAlgaeScoredNet"); + }, + }, + { + label: "> Max Algae Scored In Net", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoAlgaeScoredNet"); + }, + }, + ], + Teleop: [ + { + key: "TeleopCoralScoredLevelOne", + label: "Avg Amt Of Coral Scored Level One Teleop", + }, + { + label: "> Min L1 Coral Scored", + get(pitData, quantitativeReports) { + return GetMinimum( + quantitativeReports!, + "TeleopCoralScoredLevelOne", + ); + }, + }, + { + label: "> Max L1 Coral Scored", + get(pitData, quantitativeReports) { + return GetMaximum( + quantitativeReports!, + "TeleopCoralScoredLevelOne", + ); + }, + }, + { + key: "TeleopCoralScoredLevelTwo", + label: "Avg Amt Of Coral Scored Level Two Teleop", + }, + { + label: "> Min L2 Coral Scored", + get(pitData, quantitativeReports) { + return GetMinimum( + quantitativeReports!, + "TeleopCoralScoredLevelTwo", + ); + }, + }, + { + label: "> Max L2 Coral Scored", + get(pitData, quantitativeReports) { + return GetMaximum( + quantitativeReports!, + "TeleopCoralScoredLevelTwo", + ); + }, + }, + { + key: "TeleopCoralScoredLevelThree", + label: "Avg Amt Of Coral Scored Level Three Teleop", + }, + { + label: "> Min L3 Coral Scored", + get(pitData, quantitativeReports) { + return GetMinimum( + quantitativeReports!, + "TeleopCoralScoredLevelThree", + ); + }, + }, + { + label: "> Max L3 Coral Scored", + get(pitData, quantitativeReports) { + return GetMaximum( + quantitativeReports!, + "TeleopCoralScoredLevelThree", + ); + }, + }, + { + key: "TeleopCoralScoredLevelFour", + label: "Avg Amt Of Coral Scored Level Four Teleop", + }, + { + label: "> Min L4 Coral Scored", + get(pitData, quantitativeReports) { + return GetMinimum( + quantitativeReports!, + "TeleopCoralScoredLevelFour", + ); + }, + }, + { + label: "> Max L4 Coral Scored", + get(pitData, quantitativeReports) { + return GetMaximum( + quantitativeReports!, + "TeleopCoralScoredLevelFour", + ); + }, + }, + { + label: "Avg Teleop Coral", + get(pitData, quantitativeReports) { + if (!quantitativeReports) return 0; + + return ( + quantitativeReports?.reduce( + (acc, report) => + acc + + report.data.TeleopCoralScoredLevelOne + + report.data.TeleopCoralScoredLevelTwo + + report.data.TeleopCoralScoredLevelThree + + report.data.TeleopCoralScoredLevelFour, + 0, + ) / quantitativeReports?.length + ); + }, + }, + { + key: "TeleopAlgaeRemovedFromReef", + label: "Avg Amt of Algae Removed From Reef", + }, + { + label: "> Min Algae Removed From Reef", + get(pitData, quantitativeReports) { + return GetMinimum( + quantitativeReports!, + "TeleopAlgaeRemovedFromReef", + ); + }, + }, + { + label: "> Max Algae Removed From Reef", + get(pitData, quantitativeReports) { + return GetMaximum( + quantitativeReports!, + "TeleopAlgaeRemovedFromReef", + ); + }, + }, + { + key: "TeleopAlgaeScoredProcessor", + label: "Avg Amt of Algae Scored Processor Teleop", + }, + { + label: "> Min Algae Scored In Processor", + get(pitData, quantitativeReports) { + return GetMinimum( + quantitativeReports!, + "TeleopAlgaeScoredProcessor", + ); + }, + }, + { + label: "> Max Algae Scored In Processor", + get(pitData, quantitativeReports) { + return GetMaximum( + quantitativeReports!, + "TeleopAlgaeScoredProcessor", + ); + }, + }, + { + key: "TeleopAlgaeScoredNet", + label: "Avg Amt of Algae Scored Net Teleop", + }, + { + label: "> Min Algae Scored In Net", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "TeleopAlgaeScoredNet"); + }, + }, + { + label: "> Max Algae Scored In Net", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "TeleopAlgaeScoredNet"); + }, + }, + ], + }, + getGraphDots: function ( + quantitativeReports: Report[], + pitReport?: Pitreport | undefined, + ): Dot[] { + return []; + }, + }; + + const pitStatsLayout: PitStatsLayout = { + overallSlideStats: [ + { + label: "Average Algae Scored In Auto", + key: "AlgaeScoredAuto", + }, + { + label: "Average Coral Scored In Auto", + key: "CoralScoredAuto", + }, + ], + individualSlideStats: [ + { + label: "Average Auto Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const CoralLvlOne = NumericalTotal( + "AutoCoralScoredLevelOne", + quantitativeReports, + ); + + const CoralLvlTwo = NumericalTotal( + "AutoCoralScoredLevelTwo", + quantitativeReports, + ); + + const CoralLvlThree = NumericalTotal( + "AutoCoralScoredLevelThree", + quantitativeReports, + ); + + const CoralLvlFour = NumericalTotal( + "AutoCoralScoredLevelFour", + quantitativeReports, + ); + + const AlgaeNet = NumericalTotal( + "AutoAlgaeScoredNet", + quantitativeReports, + ); + + const AlgaeProcessor = NumericalTotal( + "AutoAlgaeScoredProcessor", + quantitativeReports, + ); + + return ( + (CoralLvlOne + + CoralLvlTwo + + CoralLvlThree + + CoralLvlFour + + AlgaeNet + + AlgaeProcessor) / + quantitativeReports.length + ); + }, + }, + { + label: "Average Teleop Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const CoralLvlOne = NumericalTotal( + "TeleopCoralScoredLevelOne", + quantitativeReports, + ); + + const CoralLvlTwo = NumericalTotal( + "TeleopCoralScoredLevelTwo", + quantitativeReports, + ); + + const CoralLvlThree = NumericalTotal( + "TeleopCoralScoredLevelThree", + quantitativeReports, + ); + + const CoralLvlFour = NumericalTotal( + "TeleopCoralScoredLevelFour", + quantitativeReports, + ); + + const AlgaeNet = NumericalTotal( + "TeleopAlgaeScoredNet", + quantitativeReports, + ); + + const AlgaeProcessor = NumericalTotal( + "TeleopAlgaeScoredProcessor", + quantitativeReports, + ); + + return ( + (CoralLvlOne + + CoralLvlTwo + + CoralLvlThree + + CoralLvlFour + + AlgaeNet + + AlgaeProcessor) / + quantitativeReports.length + ); + }, + }, + { + label: "Avg Endgame Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const climbed = NumericalTotal( + "EndgameClimbStatus", + quantitativeReports, + ); + + return Round(climbed) / quantitativeReports.length; + }, + }, + ], + robotCapabilities: [ + { + key: "CanDriveUnderShallowCage", + label: "Can Drive Under Shallow Cage?", + }, + { key: "CanRemoveAlgae", label: "Can Remove Algae?" }, + { + key: "CanScoreAlgaeInProcessor", + label: "Can Score Algae in Processor?", + }, + { key: "CanScoreAlgaeInNet", label: "Can Score Algae in Net?" }, + { key: "Climbing", label: "Climbing?" }, + ], + graphStat: { + label: "Average Algae Scored In The Net During Teleop", + key: "TeleopAlgaeScoredNet", + }, + }; + + function getBadges( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + card: boolean, + ) { + const badges: Badge[] = getBaseBadges(pitReport, quantitativeReports); + + if (pitReport?.data?.CanRemoveAlgae) + badges.push({ text: "Can Remove Algae", color: "primary" }); + if (pitReport?.data?.GroundIntake) + badges.push({ text: "Ground Intake", color: "primary" }); + if (pitReport?.data?.CanScoreAlgaeInNet) + badges.push({ text: "Can Score Algae Net", color: "secondary" }); + if (pitReport?.data?.CanScoreAlgaeInProcessor) + badges.push({ text: "Can Score Algae Processor", color: "success" }); + if (pitReport?.data?.CanDriveUnderShallowCage) + badges.push({ text: "Can Drive Under Shallow Cage", color: "info" }); + + if (pitReport?.data?.CanScoreCoral1) + badges.push({ text: "L1 Coral", color: "info" }); + if (pitReport?.data?.CanScoreCoral2) + badges.push({ text: "L2 Coral", color: "secondary" }); + if (pitReport?.data?.CanScoreCoral3) + badges.push({ text: "L3 Coral", color: "primary" }); + if (pitReport?.data?.CanScoreCoral4) + badges.push({ text: "L4 Coral", color: "accent" }); + if ( + !( + pitReport?.data?.CanScoreCoral1 || + pitReport?.data?.CanScoreCoral2 || + pitReport?.data?.CanScoreCoral3 || + pitReport?.data?.CanScoreCoral4 + ) + ) + badges.push({ text: "No Coral", color: "warning" }); + if (pitReport?.data?.Climbing === ReefscapeEnums.Climbing.Deep) + badges.push({ text: "Deep Climb", color: "secondary" }); + else if (pitReport?.data?.Climbing === ReefscapeEnums.Climbing.Shallow) + badges.push({ text: "Shallow Climb", color: "primary" }); + + return badges; + } + + function getAvgPoints(reports: Report[] | undefined) { + if (!reports) return 0; + + let totalPoints = 0; + + for (const report of reports.map((r) => r.data)) { + switch (report.EndgameClimbStatus) { + case ReefscapeEnums.EndgameClimbStatus.None: + break; + case ReefscapeEnums.EndgameClimbStatus.Park: + totalPoints += 2; + break; + case ReefscapeEnums.EndgameClimbStatus.High: + totalPoints += 6; + break; + case ReefscapeEnums.EndgameClimbStatus.Low: + totalPoints += 12; + break; + } + + totalPoints += + (report.TeleopCoralScoredLevelOne + report.TeleopAlgaeScoredProcessor) * + 2; + totalPoints += + (report.AutoCoralScoredLevelOne + report.TeleopCoralScoredLevelTwo) * 3; + totalPoints += + (report.AutoCoralScoredLevelTwo + + report.TeleopCoralScoredLevelThree + + report.AutoAlgaeScoredNet + + report.TeleopAlgaeScoredNet) * + 4; + totalPoints += report.TeleopCoralScoredLevelFour * 5; + totalPoints += + (report.AutoAlgaeScoredProcessor + report.AutoCoralScoredLevelThree) * + 6; + totalPoints += report.AutoCoralScoredLevelFour * 7; + } + + return totalPoints / Math.max(reports.length, 1); + } + + export const game = new Game( + "Reefscape", + 2025, + League.FRC, + QuantitativeData, + PitData, + pitReportLayout, + quantitativeReportLayout, + statsLayout, + pitStatsLayout, + "Reefscape", + "https://info.firstinspires.org/hubfs/Dive/reef-scape.svg", + "invert", + getBadges, + getAvgPoints, + ); +} + +export default Reefscape; diff --git a/lib/slugToId.ts b/lib/slugToId.ts index d0a3c14f..bef3fe06 100644 --- a/lib/slugToId.ts +++ b/lib/slugToId.ts @@ -19,11 +19,17 @@ function getSlugLookup() { return global.slugLookup; } -async function slugToId( +/** + * You are probably looking for findObjectBySlugLookUp! + */ +export async function slugToId( db: DbInterface | Promise, - collection: SluggedCollectionId, + collection: TId, slug: string, -): Promise<{ id: ObjectId | undefined; object: object | undefined }> { +): Promise<{ + id: ObjectId | undefined; + object: CollectionIdToType | undefined; +}> { if (db instanceof Promise) { db = await db; } @@ -48,7 +54,7 @@ async function slugToId( return { id: collectionSlugs.get(slug), object: undefined }; } -export async function findObjectBySlugLookUp< +export default async function findObjectBySlugLookUp< TId extends SluggedCollectionId, TObj extends CollectionIdToType, >(db: DbInterface, collection: TId, slug: string): Promise { @@ -63,5 +69,3 @@ export async function findObjectBySlugLookUp< return await db.findObjectById(collection, id); } - -export default slugToId; diff --git a/lib/testutils/TestUtils.ts b/lib/testutils/TestUtils.ts index 74dce5e5..804cceb1 100644 --- a/lib/testutils/TestUtils.ts +++ b/lib/testutils/TestUtils.ts @@ -111,6 +111,20 @@ export async function getTestApiParams< ]; } +export async function getTestAuthParams>( + res: TestRes, + deps: + | Partial + | Partial<{ + db: DbInterface; + user: Partial; + resend: ResendInterface; + }>, + args: TArgs, +) { + return (await getTestApiParams(res, deps, args, {})).slice(0, 4); +} + export function getTestRollbar(): RollbarInterface { return { error: jest.fn(), @@ -164,6 +178,9 @@ export async function createTestDocuments(db: DbInterface) { } export namespace PlaywrightUtils { + /** + * You cannot use LocalStorage with the API returned this function! + */ export function getTestClientApi() { const api = new ClientApi(); diff --git a/next.config.ts b/next.config.ts index b495f998..62063e60 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,8 +2,9 @@ import packageConfig from "./package.json"; import withSerwistInit from "@serwist/next"; const withSerwist = withSerwistInit({ - swSrc: "lib/sw.ts", + swSrc: "lib/client/sw.ts", swDest: "public/sw.js", + cacheOnNavigation: true, additionalPrecacheEntries: [ { url: "/offline", revision: new Date().toDateString() }, ], diff --git a/package-lock.json b/package-lock.json index e1d3d706..fbeeaeaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "cross-env": "^7.0.3", "daisyui": "^4.12.22", "jest": "^29.7.0", + "madge": "^8.0.0", "postcss": "^8.5.3", "prettier": "3.5.3", "serwist": "^9.0.11", @@ -338,18 +339,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -464,10 +465,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, + "dependencies": { + "@babel/types": "^7.27.0" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -754,14 +758,13 @@ } }, "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -793,6 +796,19 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@dependents/detective-less": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@dependents/detective-less/-/detective-less-5.0.1.tgz", + "integrity": "sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ==", + "dev": true, + "dependencies": { + "gonzales-pe": "^4.3.0", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", @@ -1894,9 +1910,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -2595,6 +2611,92 @@ "tslib": "^2.8.0" } }, + "node_modules/@ts-graphviz/adapter": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ts-graphviz/adapter/-/adapter-2.0.6.tgz", + "integrity": "sha512-kJ10lIMSWMJkLkkCG5gt927SnGZcBuG0s0HHswGzcHTgvtUe7yk5/3zTEr0bafzsodsOq5Gi6FhQeV775nC35Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "dependencies": { + "@ts-graphviz/common": "^2.1.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-graphviz/ast": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@ts-graphviz/ast/-/ast-2.0.7.tgz", + "integrity": "sha512-e6+2qtNV99UT6DJSoLbHfkzfyqY84aIuoV8Xlb9+hZAjgpum8iVHprGeAMQ4rF6sKUAxrmY8rfF/vgAwoPc3gw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "dependencies": { + "@ts-graphviz/common": "^2.1.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-graphviz/common": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ts-graphviz/common/-/common-2.1.5.tgz", + "integrity": "sha512-S6/9+T6x8j6cr/gNhp+U2olwo1n0jKj/682QVqsh7yXWV6ednHYqxFw0ZsY3LyzT0N8jaZ6jQY9YD99le3cmvg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-graphviz/core": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@ts-graphviz/core/-/core-2.0.7.tgz", + "integrity": "sha512-w071DSzP94YfN6XiWhOxnLpYT3uqtxJBDYdh6Jdjzt+Ce6DNspJsPQgpC7rbts/B8tEkq0LHoYuIF/O5Jh5rPg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "dependencies": { + "@ts-graphviz/ast": "^2.0.7", + "@ts-graphviz/common": "^2.1.5" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -3099,6 +3201,166 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, + "node_modules/@typescript-eslint/types": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.0.tgz", + "integrity": "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.0.tgz", + "integrity": "sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.0.tgz", + "integrity": "sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.31.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.13", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.48", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "dev": true + }, "node_modules/@yudiel/react-qr-scanner": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@yudiel/react-qr-scanner/-/react-qr-scanner-2.2.1.tgz", @@ -3228,6 +3490,12 @@ "node": ">= 8" } }, + "node_modules/app-module-path": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/app-module-path/-/app-module-path-2.2.0.tgz", + "integrity": "sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==", + "dev": true + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -3416,6 +3684,15 @@ "node": ">=0.8" } }, + "node_modules/ast-module-types": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ast-module-types/-/ast-module-types-6.0.1.tgz", + "integrity": "sha512-WHw67kLXYbZuHTmcdbIrVArCq5wxo6NEuj3hiYAWr8mwJeC+C2mMCIBIWCiDoCye/OF/xelc+teJ1ERoWmnEIA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -3648,6 +3925,26 @@ "zxing-wasm": "^2.0.1" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -3668,6 +3965,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/bootstrap": { "version": "5.3.5", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz", @@ -3782,6 +4090,30 @@ "node": ">=14.20.1" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3966,6 +4298,30 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -4117,6 +4473,12 @@ "node": ">=4.0.0" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4375,6 +4737,15 @@ } } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4388,13 +4759,34 @@ "node": ">=0.10.0" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "gopd": "^1.0.1" }, "engines": { @@ -4439,6 +4831,33 @@ "underscore": "*" } }, + "node_modules/dependency-tree": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/dependency-tree/-/dependency-tree-11.1.1.tgz", + "integrity": "sha512-pnkCd8VGOq70EVaEQxDC9mZCjCwYj4yG4j8h+PEJswuWp+rdE6p8zbtVvWk+yPwaVimOjlhNi782U9K5KOU9MQ==", + "dev": true, + "dependencies": { + "commander": "^12.1.0", + "filing-cabinet": "^5.0.3", + "precinct": "^12.2.0", + "typescript": "^5.7.3" + }, + "bin": { + "dependency-tree": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/dependency-tree/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4466,6 +4885,138 @@ "node": ">=8" } }, + "node_modules/detective-amd": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/detective-amd/-/detective-amd-6.0.1.tgz", + "integrity": "sha512-TtyZ3OhwUoEEIhTFoc1C9IyJIud3y+xYkSRjmvCt65+ycQuc3VcBrPRTMWoO/AnuCyOB8T5gky+xf7Igxtjd3g==", + "dev": true, + "dependencies": { + "ast-module-types": "^6.0.1", + "escodegen": "^2.1.0", + "get-amd-module-type": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "bin": { + "detective-amd": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-cjs": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/detective-cjs/-/detective-cjs-6.0.1.tgz", + "integrity": "sha512-tLTQsWvd2WMcmn/60T2inEJNhJoi7a//PQ7DwRKEj1yEeiQs4mrONgsUtEJKnZmrGWBBmE0kJ1vqOG/NAxwaJw==", + "dev": true, + "dependencies": { + "ast-module-types": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-es6": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/detective-es6/-/detective-es6-5.0.1.tgz", + "integrity": "sha512-XusTPuewnSUdoxRSx8OOI6xIA/uld/wMQwYsouvFN2LAg7HgP06NF1lHRV3x6BZxyL2Kkoih4ewcq8hcbGtwew==", + "dev": true, + "dependencies": { + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-postcss": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/detective-postcss/-/detective-postcss-7.0.1.tgz", + "integrity": "sha512-bEOVpHU9picRZux5XnwGsmCN4+8oZo7vSW0O0/Enq/TO5R2pIAP2279NsszpJR7ocnQt4WXU0+nnh/0JuK4KHQ==", + "dev": true, + "dependencies": { + "is-url": "^1.2.4", + "postcss-values-parser": "^6.0.2" + }, + "engines": { + "node": "^14.0.0 || >=16.0.0" + }, + "peerDependencies": { + "postcss": "^8.4.47" + } + }, + "node_modules/detective-sass": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/detective-sass/-/detective-sass-6.0.1.tgz", + "integrity": "sha512-jSGPO8QDy7K7pztUmGC6aiHkexBQT4GIH+mBAL9ZyBmnUIOFbkfZnO8wPRRJFP/QP83irObgsZHCoDHZ173tRw==", + "dev": true, + "dependencies": { + "gonzales-pe": "^4.3.0", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-scss": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/detective-scss/-/detective-scss-5.0.1.tgz", + "integrity": "sha512-MAyPYRgS6DCiS6n6AoSBJXLGVOydsr9huwXORUlJ37K3YLyiN0vYHpzs3AdJOgHobBfispokoqrEon9rbmKacg==", + "dev": true, + "dependencies": { + "gonzales-pe": "^4.3.0", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-stylus": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/detective-stylus/-/detective-stylus-5.0.1.tgz", + "integrity": "sha512-Dgn0bUqdGbE3oZJ+WCKf8Dmu7VWLcmRJGc6RCzBgG31DLIyai9WAoEhYRgIHpt/BCRMrnXLbGWGPQuBUrnF0TA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-typescript": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/detective-typescript/-/detective-typescript-14.0.0.tgz", + "integrity": "sha512-pgN43/80MmWVSEi5LUuiVvO/0a9ss5V7fwVfrJ4QzAQRd3cwqU1SfWGXJFcNKUqoD5cS+uIovhw5t/0rSeC5Mw==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "^8.23.0", + "ast-module-types": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "typescript": "^5.4.4" + } + }, + "node_modules/detective-vue2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/detective-vue2/-/detective-vue2-2.2.0.tgz", + "integrity": "sha512-sVg/t6O2z1zna8a/UIV6xL5KUa2cMTQbdTIIvqNM0NIPswp52fe43Nwmbahzj3ww4D844u/vC2PYfiGLvD3zFA==", + "dev": true, + "dependencies": { + "@dependents/detective-less": "^5.0.1", + "@vue/compiler-sfc": "^3.5.13", + "detective-es6": "^5.0.1", + "detective-sass": "^6.0.1", + "detective-scss": "^5.0.1", + "detective-stylus": "^5.0.1", + "detective-typescript": "^14.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "typescript": "^5.4.4" + } + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -4664,9 +5215,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -4917,6 +5468,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz", @@ -5586,6 +6158,12 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5767,6 +6345,66 @@ "node": ">=10" } }, + "node_modules/filing-cabinet": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/filing-cabinet/-/filing-cabinet-5.0.3.tgz", + "integrity": "sha512-PlPcMwVWg60NQkhvfoxZs4wEHjhlOO/y7OAm4sKM60o1Z9nttRY4mcdQxp/iZ+kg/Vv6Hw1OAaTbYVM9DA9pYg==", + "dev": true, + "dependencies": { + "app-module-path": "^2.2.0", + "commander": "^12.1.0", + "enhanced-resolve": "^5.18.0", + "module-definition": "^6.0.1", + "module-lookup-amd": "^9.0.3", + "resolve": "^1.22.10", + "resolve-dependency-path": "^4.0.1", + "sass-lookup": "^6.1.0", + "stylus-lookup": "^6.1.0", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3" + }, + "bin": { + "filing-cabinet": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/filing-cabinet/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/filing-cabinet/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/filing-cabinet/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -5955,6 +6593,19 @@ "rbush": "^3.0.1" } }, + "node_modules/get-amd-module-type": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-amd-module-type/-/get-amd-module-type-6.0.1.tgz", + "integrity": "sha512-MtjsmYiCXcYDDrGqtNbeIYdAl85n+5mSv2r3FbzER/YV3ZILw4HNNIw34HuV5pyl0jzs6GFYU1VHVEefhgcNHQ==", + "dev": true, + "dependencies": { + "ast-module-types": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -5982,6 +6633,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -6039,15 +6696,16 @@ } }, "node_modules/glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, @@ -6097,6 +6755,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gonzales-pe": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz", + "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "gonzales": "bin/gonzales.js" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/goober": { "version": "2.1.16", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", @@ -6305,6 +6978,26 @@ "resolved": "https://registry.npmjs.org/idb-wrapper/-/idb-wrapper-1.7.2.tgz", "integrity": "sha512-zfNREywMuf0NzDo9mVsL0yegjsirJxHpKHvWcyRozIqQy89g0a3U+oBPOCN4cc0oCiOuYgZHimzaW/R46G1Mpg==" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6373,6 +7066,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, "node_modules/internal-slot": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", @@ -6485,10 +7184,9 @@ } }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "license": "MIT", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dependencies": { "hasown": "^2.0.2" }, @@ -6593,6 +7291,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -6638,6 +7345,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -6653,6 +7369,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-set": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", @@ -6738,6 +7463,36 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "dev": true + }, + "node_modules/is-url-superb": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-url-superb/-/is-url-superb-4.0.0.tgz", + "integrity": "sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -8078,6 +8833,22 @@ "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -8100,6 +8871,62 @@ "node": ">=10" } }, + "node_modules/madge": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/madge/-/madge-8.0.0.tgz", + "integrity": "sha512-9sSsi3TBPhmkTCIpVQF0SPiChj1L7Rq9kU2KDG1o6v2XH9cCw086MopjVCD+vuoL5v8S77DTbVopTO8OUiQpIw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "commander": "^7.2.0", + "commondir": "^1.0.1", + "debug": "^4.3.4", + "dependency-tree": "^11.0.0", + "ora": "^5.4.1", + "pluralize": "^8.0.0", + "pretty-ms": "^7.0.1", + "rc": "^1.2.8", + "stream-to-array": "^2.3.0", + "ts-graphviz": "^2.1.2", + "walkdir": "^0.4.1" + }, + "bin": { + "madge": "bin/cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://www.paypal.me/pahen" + }, + "peerDependencies": { + "typescript": "^5.4.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/madge/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -8233,6 +9060,49 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/module-definition": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-6.0.1.tgz", + "integrity": "sha512-FeVc50FTfVVQnolk/WQT8MX+2WVcDnTGiq6Wo+/+lJ2ET1bRVi3HG3YlJUfqagNMc/kUlFSoR96AJkxGpKz13g==", + "dev": true, + "dependencies": { + "ast-module-types": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "bin": { + "module-definition": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/module-lookup-amd": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/module-lookup-amd/-/module-lookup-amd-9.0.4.tgz", + "integrity": "sha512-DWJEuLVvjxh5b8wrvJC5wr2a7qo7pOWXIgdCBNazU416kcIyzO4drxvlqKhsHzYwxcC4cWuhoK+MiWCKCGnv7A==", + "dev": true, + "dependencies": { + "commander": "^12.1.0", + "glob": "^7.2.3", + "requirejs": "^2.3.7", + "requirejs-config-file": "^4.0.0" + }, + "bin": { + "lookup-amd": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/module-lookup-amd/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/mongo-anywhere": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/mongo-anywhere/-/mongo-anywhere-1.1.15.tgz", @@ -8513,6 +9383,18 @@ "dev": true, "license": "MIT" }, + "node_modules/node-source-walk": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/node-source-walk/-/node-source-walk-7.0.1.tgz", + "integrity": "sha512-3VW/8JpPqPvnJvseXowjZcirPisssnBuDikk6JIZ8jQzF7KJQX52iPFX4RYYxLycYH7IbMRSPUOga/esVjy5Yg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.26.7" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/node-walker": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/node-walker/-/node-walker-0.1.0.tgz", @@ -8773,6 +9655,29 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -8850,6 +9755,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/parseley": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", @@ -9072,6 +9986,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/polygon-clipping": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/polygon-clipping/-/polygon-clipping-0.15.7.tgz", @@ -9233,6 +10156,23 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/postcss-values-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz", + "integrity": "sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==", + "dev": true, + "dependencies": { + "color-name": "^1.1.4", + "is-url-superb": "^4.0.0", + "quote-unquote": "^1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "postcss": "^8.2.9" + } + }, "node_modules/preact": { "version": "10.24.3", "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", @@ -9253,6 +10193,44 @@ "preact": ">=10" } }, + "node_modules/precinct": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/precinct/-/precinct-12.2.0.tgz", + "integrity": "sha512-NFBMuwIfaJ4SocE9YXPU/n4AcNSoFMVFjP72nvl3cx69j/ke61/hPOWFREVxLkFhhEGnA8ZuVfTqJBa+PK3b5w==", + "dev": true, + "dependencies": { + "@dependents/detective-less": "^5.0.1", + "commander": "^12.1.0", + "detective-amd": "^6.0.1", + "detective-cjs": "^6.0.1", + "detective-es6": "^5.0.1", + "detective-postcss": "^7.0.1", + "detective-sass": "^6.0.1", + "detective-scss": "^5.0.1", + "detective-stylus": "^5.0.1", + "detective-typescript": "^14.0.0", + "detective-vue2": "^2.2.0", + "module-definition": "^6.0.1", + "node-source-walk": "^7.0.1", + "postcss": "^8.5.1", + "typescript": "^5.7.3" + }, + "bin": { + "precinct": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/precinct/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -9293,6 +10271,21 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" }, + "node_modules/pretty-ms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "dev": true, + "dependencies": { + "parse-ms": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -9394,6 +10387,12 @@ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" }, + "node_modules/quote-unquote": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quote-unquote/-/quote-unquote-1.0.0.tgz", + "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==", + "dev": true + }, "node_modules/raf-schd": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", @@ -9407,6 +10406,30 @@ "quickselect": "^2.0.0" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -9680,6 +10703,20 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -9811,6 +10848,32 @@ "node": ">=0.10.0" } }, + "node_modules/requirejs": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.7.tgz", + "integrity": "sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw==", + "dev": true, + "bin": { + "r_js": "bin/r.js", + "r.js": "bin/r.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/requirejs-config-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/requirejs-config-file/-/requirejs-config-file-4.0.0.tgz", + "integrity": "sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==", + "dev": true, + "dependencies": { + "esprima": "^4.0.0", + "stringify-object": "^3.2.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/resend": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/resend/-/resend-4.3.0.tgz", @@ -9824,17 +10887,20 @@ } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -9860,6 +10926,15 @@ "node": ">=8" } }, + "node_modules/resolve-dependency-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/resolve-dependency-path/-/resolve-dependency-path-4.0.1.tgz", + "integrity": "sha512-YQftIIC4vzO9UMhO/sCgXukNyiwVRCVaxiWskCBy7Zpqkplm8kTAISZ8O1MoKW1ca6xzgLUBjZTcDgypXvXxiQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -9886,6 +10961,25 @@ "node": ">=10" } }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -10009,6 +11103,31 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sass-lookup": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sass-lookup/-/sass-lookup-6.1.0.tgz", + "integrity": "sha512-Zx+lVyoWqXZxHuYWlTA17Z5sczJ6braNT2C7rmClw+c4E7r/n911Zwss3h1uHI9reR5AgHZyNHF7c2+VIp5AUA==", + "dev": true, + "dependencies": { + "commander": "^12.1.0", + "enhanced-resolve": "^5.18.0" + }, + "bin": { + "sass-lookup": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/sass-lookup/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -10349,6 +11468,15 @@ "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" }, + "node_modules/stream-to-array": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/stream-to-array/-/stream-to-array-2.3.0.tgz", + "integrity": "sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==", + "dev": true, + "dependencies": { + "any-promise": "^1.1.0" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -10357,6 +11485,15 @@ "node": ">=10.0.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -10531,6 +11668,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -10606,6 +11757,30 @@ } } }, + "node_modules/stylus-lookup": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/stylus-lookup/-/stylus-lookup-6.1.0.tgz", + "integrity": "sha512-5QSwgxAzXPMN+yugy61C60PhoANdItfdjSEZR8siFwz7yL9jTmV0UBKDCfn3K8GkGB4g0Y9py7vTCX8rFu4/pQ==", + "dev": true, + "dependencies": { + "commander": "^12.1.0" + }, + "bin": { + "stylus-lookup": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/stylus-lookup/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -10794,15 +11969,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10855,6 +12021,31 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-graphviz": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/ts-graphviz/-/ts-graphviz-2.1.6.tgz", + "integrity": "sha512-XyLVuhBVvdJTJr2FJJV2L1pc4MwSjMhcunRVgDE9k4wbb2ee7ORYnPewxMWUav12vxyfUM686MSGsqnVRIInuw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "dependencies": { + "@ts-graphviz/adapter": "^2.0.6", + "@ts-graphviz/ast": "^2.0.7", + "@ts-graphviz/common": "^2.1.5", + "@ts-graphviz/core": "^2.0.7" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -11287,6 +12478,15 @@ "extsprintf": "^1.2.0" } }, + "node_modules/walkdir": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", + "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -11304,6 +12504,15 @@ "loose-envify": "^1.0.0" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index 93953def..bcb1beac 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "docker-prune": "docker container prune", "lint": "eslint . --max-warnings=0", "prettier-fix": "prettier --write ./**/*.{js,tsx,jsx,json,ts}", - "prettier-check": "prettier --check ./**/*.{js,tsx,jsx,json,ts}" + "prettier-check": "prettier --check ./**/*.{js,tsx,jsx,json,ts}", + "madge": "madge lib pages tests --extensions ts,tsx " }, "dependencies": { "@next-auth/mongodb-adapter": "^1.1.3", @@ -77,6 +78,7 @@ "cross-env": "^7.0.3", "daisyui": "^4.12.22", "jest": "^29.7.0", + "madge": "^8.0.0", "postcss": "^8.5.3", "prettier": "3.5.3", "serwist": "^9.0.11", diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx index ecb101e1..1eec895d 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback } from "react"; import ClientApi from "@/lib/api/ClientApi"; import { @@ -10,39 +10,27 @@ import { User, Team, Competition, - CompPicklistGroup, Season, } from "@/lib/Types"; import { useCurrentSession } from "@/lib/client/useCurrentSession"; import useInterval from "@/lib/client/useInterval"; -import { - NotLinkedToTba, - download, - getIdsInProgressFromTimestamps, - makeObjSerializeable, -} from "@/lib/client/ClientUtils"; -import { games } from "@/lib/games"; -import { defaultGameId } from "@/lib/client/GameId"; -import { GetServerSideProps } from "next"; -import UrlResolver from "@/lib/UrlResolver"; import Container from "@/components/Container"; import CompHeaderCard from "@/components/competition/CompHeaderCard"; import EditMatchModal from "@/components/competition/EditMatchModal"; import InsightsAndSettingsCard from "@/components/competition/InsightsAndSettingsCard"; import MatchScheduleCard from "@/components/competition/MatchScheduleCard"; import PitScoutingCard from "@/components/competition/PitScoutingCard"; +import { useRouter } from "next/router"; const api = new ClientApi(); -export default function CompetitionIndex({ - team, - competition: comp, - season, -}: { - team: Team | undefined; - competition: Competition | undefined; - season: Season | undefined; -}) { +export default function CompetitionIndex() { + const router = useRouter(); + + const [team, setTeam] = useState(); + const [season, setSeason] = useState(); + const [comp, setComp] = useState(); + const { session, status } = useCurrentSession(); const isManager = (session?.user?._id !== undefined && @@ -76,7 +64,6 @@ export default function CompetitionIndex({ const [loadingScoutStats, setLoadingScoutStats] = useState(false); const [loadingUsers, setLoadingUsers] = useState(false); - const [submissionRate, setSubmissionRate] = useState(0); const [submittedReports, setSubmittedReports] = useState( undefined, ); @@ -100,6 +87,26 @@ export default function CompetitionIndex({ string | undefined >(); + // Load team, season, and competition + useEffect(() => { + const compSlug = router.query?.competitonSlug as string | undefined; + + if (!compSlug) return; + + api.findCompSeasonAndTeamByCompSlug(compSlug).then((res) => { + if (!res) { + alert("Competition not found"); + return; + } + + const { team, season, comp } = res; + + setTeam(team); + setSeason(season); + setComp(comp); + }); + }, [router.query.competitonSlug]); + const regeneratePitReports = useCallback(async () => { console.log("Regenerating pit reports..."); const { pitReports: pitReportIds } = await api.regeneratePitReports( @@ -132,6 +139,8 @@ export default function CompetitionIndex({ const loadMatches = useCallback( async (silent: boolean = false) => { + if (!comp) return; + if (!silent) setLoadingMatches(true); window.location.hash = ""; @@ -166,11 +175,13 @@ export default function CompetitionIndex({ if (!silent) setLoadingMatches(false); }, - [comp?._id], + [comp], ); const loadReports = useCallback( async (silent: boolean = false) => { + if (!comp) return; + const scoutingStats = (reps: Report[]) => { if (!silent) setLoadingScoutStats(true); let submittedCount = 0; @@ -208,7 +219,7 @@ export default function CompetitionIndex({ scoutingStats(newReports); }, - [comp?._id], + [comp], ); useEffect(() => { @@ -477,20 +488,3 @@ export default function CompetitionIndex({ ); } - -export const getServerSideProps: GetServerSideProps = async (context) => { - const resolved = await UrlResolver(context, 3); - if ("redirect" in resolved) { - return resolved; - } - - return { - props: { - competition: resolved.competition - ? makeObjSerializeable(resolved.competition) - : null, - team: resolved.team ? makeObjSerializeable(resolved.team) : null, - season: resolved.season ? makeObjSerializeable(resolved.season) : null, - }, - }; -}; diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[...pitreportId].tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[pitReportId].tsx similarity index 53% rename from pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[...pitreportId].tsx rename to pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[pitReportId].tsx index cefe76a4..0186466d 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[...pitreportId].tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[pitReportId].tsx @@ -1,6 +1,6 @@ import ClientApi from "@/lib/api/ClientApi"; import { useCurrentSession } from "@/lib/client/useCurrentSession"; -import { FormLayout, FormElement, BlockElement } from "@/lib/Layout"; +import { FormElement, BlockElement } from "@/lib/Layout"; import { AllianceColor, FieldPos, @@ -8,45 +8,101 @@ import { Pitreport, PitReportData, } from "@/lib/Types"; -import { useState, useCallback, Fragment } from "react"; +import { useState, useCallback, Fragment, useEffect } from "react"; import { FaRobot } from "react-icons/fa"; import { Analytics } from "@/lib/client/Analytics"; -import { - camelCaseToTitleCase, - makeObjSerializeable, -} from "@/lib/client/ClientUtils"; -import { GameId } from "@/lib/client/GameId"; -import CollectionId from "@/lib/client/CollectionId"; -import { games } from "@/lib/games"; -import { getDatabase } from "@/lib/MongoDB"; -import UrlResolver, { serializeDatabaseObject } from "@/lib/UrlResolver"; -import { ObjectId } from "bson"; -import { GetServerSideProps } from "next"; +import { camelCaseToTitleCase } from "@/lib/client/ClientUtils"; +import { games, detectGameIdFromPitReport } from "@/lib/games"; import Flex from "@/components/Flex"; import Checkbox from "@/components/forms/Checkboxes"; import FieldPositionSelector from "@/components/forms/FieldPositionSelector"; import ImageUpload from "@/components/forms/ImageUpload"; import Card from "@/components/Card"; import Container from "@/components/Container"; +import { useRouter } from "next/router"; +import Loading from "@/components/Loading"; +import toast from "react-hot-toast"; const api = new ClientApi(); -export default function PitReportForm(props: { - pitReport: Pitreport; - layout: FormLayout; - compId?: string; - usersteamNumber?: number; - compName?: string; - username?: string; - game: Game; -}) { +enum LoadState { + WaitingForQuery = "Waiting for query...", + Fetching = "Fetching...", + Loaded = "Loaded", + Failed = "Failed", +} + +export default function PitReportForm() { const { session } = useCurrentSession(); + const router = useRouter(); + + const [loadStatus, setLoadStatus] = useState( + LoadState.WaitingForQuery, + ); + + const [pitReport, setPitReport] = useState(); + const [compName, setCompName] = useState(); + const [usersTeamNumber, setTeamNumber] = useState(); + const [game, setGame] = useState(); + + const username = session?.user?.name; + + useEffect(() => { + // Fetch page data + setLoadStatus(LoadState.Fetching); + + const pitReportId = router.query.pitReportId as string; + if (!pitReportId) { + return; + } + + let teamNumber: number | undefined = undefined; + + const promise = api + .getPitReportPageData(pitReportId as string) + .then((data) => { + if (!data) { + setLoadStatus(LoadState.Failed); + throw new Error("No data returned from API"); + } + + if (!data.pitReport) { + setLoadStatus(LoadState.Failed); + throw new Error("No pit report found"); + } + + const gameId = detectGameIdFromPitReport(data.pitReport); + if (!gameId) { + setLoadStatus(LoadState.Failed); + throw new Error("No game found for pit report"); + } - const [pitreport, setPitreport] = useState(props.pitReport); + teamNumber = data.pitReport?.teamNumber; + + setLoadStatus(LoadState.Loaded); + + setPitReport(data.pitReport); + setCompName(data.compName); + setTeamNumber(data.usersTeamNumber); + setGame(gameId ? games[gameId] : undefined); + + console.log("data:", data); + }); + + toast.promise(promise, { + loading: "Loading pit report...", + success: () => { + return `Loaded pit report for team ${teamNumber}`; + }, + error: (err) => { + return `Failed to load pit report: ${err}`; + }, + }); + }, [router.query]); const setCallback = useCallback( (key: any, value: boolean | string | number | object) => { - setPitreport((old) => { + setPitReport((old) => { let copy = structuredClone(old); //@ts-expect-error copy.data[key] = value; @@ -57,30 +113,41 @@ export default function PitReportForm(props: { ); async function submit() { + if (!pitReport) return; + // Remove _id from object - const { _id, ...report } = pitreport; + const { _id, ...report } = pitReport; - console.log("Submitting pitreport", report); - api - .updatePitreport(props.pitReport?._id!, { + const promise = api + .updatePitreport(router.query.pitReportId as string, { ...report, submitted: true, submitter: session?.user?._id, }) .then(() => { Analytics.pitReportSubmitted( - pitreport.teamNumber, - props.usersteamNumber ?? -1, - props.compName ?? "Unknown", - props.username ?? "Unknown", + pitReport.teamNumber, + usersTeamNumber ?? -1, + compName ?? "Unknown", + username ?? "Unknown", ); - }) - .finally(() => { + + // Redirect to the comp page location.href = location.href.substring( 0, location.href.lastIndexOf("/pit"), ); }); + + toast.promise(promise, { + loading: "Submitting pit report...", + success: () => { + return `Submitted pit report for team ${pitReport.teamNumber}`; + }, + error: (err) => { + return `Failed to submit pit report: ${err}`; + }, + }); } function getComponent( @@ -94,7 +161,7 @@ export default function PitReportForm(props: { return ( ); @@ -105,7 +172,7 @@ export default function PitReportForm(props: { key={index} label={element.label ?? (element.key as string)} dataKey={key} - data={pitreport} + data={pitReport!} callback={setCallback} divider={!isLastInHeader} /> @@ -123,7 +190,7 @@ export default function PitReportForm(props: { setCallback(key, e.target.value)} type="number" className="input input-bordered" @@ -136,7 +203,7 @@ export default function PitReportForm(props: { return (