diff --git a/components/Avatar.tsx b/components/Avatar.tsx index 3b38e26b..7455a912 100644 --- a/components/Avatar.tsx +++ b/components/Avatar.tsx @@ -8,6 +8,7 @@ export default function Avatar(props: { imgHeightOverride?: string | undefined; showLevel?: boolean | undefined; borderThickness?: number | undefined; + animation?: string | undefined; onClick?: () => void | undefined; className?: string | undefined; online?: boolean; @@ -21,7 +22,7 @@ export default function Avatar(props: { return (
{(props.showLevel ?? true) && (
diff --git a/components/Container.tsx b/components/Container.tsx index e3c29d1c..0cc63f42 100644 --- a/components/Container.tsx +++ b/components/Container.tsx @@ -22,6 +22,7 @@ import Banner, { DiscordBanner } from "./Banner"; import { stat } from "fs"; import { forceOfflineMode } from "@/lib/client/ClientUtils"; import Head from "next/head"; +import SignInMenu from "./SignInMenu"; const api = new ClientApi(); @@ -253,23 +254,7 @@ export default function Container(props: ContainerProps) {
{showAuthBlock ? ( -
-
-
- -

Wait a minute...

-

You need to sign in first!

-
- - - -
-
-
-
+ ) : ( <> {props.notForMobile && !accepted && onMobile ? ( diff --git a/components/SignInMenu.tsx b/components/SignInMenu.tsx new file mode 100644 index 00000000..1c9ae53b --- /dev/null +++ b/components/SignInMenu.tsx @@ -0,0 +1,112 @@ +import Container from "@/components/Container"; +import { signIn } from "next-auth/react"; +import { useRouter } from "next/router"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { + GoogleReCaptchaProvider, + useGoogleReCaptcha, +} from "react-google-recaptcha-v3"; +import { FaGoogle, FaSlack } from "react-icons/fa"; + +const errorMessages: { [error: string]: string } = { + oauthcallback: "Failed to sign in with OAuth provider.", + callback: "A server-side error occurred during sign in.", +}; + +function SignInCard() { + const router = useRouter(); + const emailRef = useRef(null); + const { executeRecaptcha } = useGoogleReCaptcha(); + + const [error, setError] = useState(router.query.error as string); + + useEffect(() => { + if (router.query.error) { + const error = (router.query.error as string).toLowerCase(); + const message = + (error in errorMessages ? errorMessages[error] : error) + + " Try clearing your cookies and then signing in again."; + + setError(message); + } + }, [router.query.error]); + + function signInWithCallbackUrl(provider: string, options?: object) { + const callbackUrl = router.query.callbackUrl as string; + + signIn(provider, { callbackUrl, ...options }); + } + async function logInWithEmail() { + const email = emailRef.current?.value; + + if (!email) { + setError("Email is required"); + return; + } + + if (!executeRecaptcha) { + setError("Recaptcha not available"); + return; + } + + const captchaToken = await executeRecaptcha(); + + signInWithCallbackUrl("email", { email, captchaToken }); + } + + return ( +
+
+

Sign In

+ {error &&

{error}

} +

Choose a login provider

+
+ + + + + +
+
+

Email Sign In

+ + +
+
+
+ ); +} + +export default function SignInMenu() { + return ( + +
+ +
+
+ ); +} diff --git a/components/competition/InsightsAndSettingsCard.tsx b/components/competition/InsightsAndSettingsCard.tsx index 65de627d..5062d365 100644 --- a/components/competition/InsightsAndSettingsCard.tsx +++ b/components/competition/InsightsAndSettingsCard.tsx @@ -6,9 +6,16 @@ import { Competition, MatchType, Pitreport, Report, Team } from "@/lib/Types"; import Link from "next/link"; import { ChangeEvent, useState } from "react"; import { BsGearFill, BsClipboard2Check } from "react-icons/bs"; -import { FaSync, FaBinoculars, FaUserCheck, FaDatabase } from "react-icons/fa"; +import { + FaSync, + FaBinoculars, + FaUserCheck, + FaDatabase, + FaTrash, +} from "react-icons/fa"; import { FaUserGroup } from "react-icons/fa6"; import ClientApi from "@/lib/api/ClientApi"; +import toast from "react-hot-toast"; const api = new ClientApi(); @@ -49,10 +56,10 @@ export default function InsightsAndSettingsCard(props: { const [newCompName, setNewCompName] = useState(comp?.name); const [newCompTbaId, setNewCompTbaId] = useState(comp?.tbaId); const [exportPending, setExportPending] = useState(false); - const [teamToAdd, setTeamToAdd] = useState(0); + const [teamToAdd, setTeamToAdd] = useState(); const [blueAlliance, setBlueAlliance] = useState([]); const [redAlliance, setRedAlliance] = useState([]); - const [matchNumber, setMatchNumber] = useState(undefined); + const [matchNumber, setMatchNumber] = useState(); const exportAsCsv = async () => { setExportPending(true); @@ -132,6 +139,28 @@ export default function InsightsAndSettingsCard(props: { .finally(() => location.reload()); } + function deleteComp() { + if (!comp?._id) return; + + const confirmKey = `delete-comp-${comp.slug}`; + if ( + prompt( + `If you are sure you want to IRREVOCABLY delete this competition and all data associated with it, type "${confirmKey}"`, + ) === confirmKey + ) { + toast.promise( + api.deleteComp(comp._id).finally(() => { + window.location.href = `/${team?.slug}/${seasonSlug}`; + }), + { + loading: "Deleting competition...", + success: "Competition deleted successfully!", + error: "Error deleting competition.", + }, + ); + } else toast.error("Competition not deleted."); + } + return (
@@ -391,12 +420,19 @@ export default function InsightsAndSettingsCard(props: { )} -
+
+
+
) : (
diff --git a/components/competition/MatchScheduleCard.tsx b/components/competition/MatchScheduleCard.tsx index 87837ec8..5e3a5bbc 100644 --- a/components/competition/MatchScheduleCard.tsx +++ b/components/competition/MatchScheduleCard.tsx @@ -188,7 +188,7 @@ export default function MatchScheduleCard(props: { @@ -227,7 +227,7 @@ export default function MatchScheduleCard(props: { })}
-
+
{match.subjectiveScouter && usersById[match.subjectiveScouter] ? (
@@ -267,30 +267,32 @@ export default function MatchScheduleCard(props: { ) : (
Subjective Scouter:{" "} - {usersById[match.subjectiveScouter ?? ""].name}{" "} -
- -
+ { + usersById[match.subjectiveScouter ?? ""].name + }{" "}
)}
) : ( -
- No subjective scouter assigned{" "} -
- -
-
+
No subjective scouter assigned
)} +
+ +
Add Subjective Report ( diff --git a/components/competition/PitScoutingCard.tsx b/components/competition/PitScoutingCard.tsx index 79c87ca2..87cf61e7 100644 --- a/components/competition/PitScoutingCard.tsx +++ b/components/competition/PitScoutingCard.tsx @@ -2,7 +2,7 @@ import { NotLinkedToTba } from "@/lib/client/ClientUtils"; import { Competition, Pitreport } from "@/lib/Types"; import Link from "next/link"; import { BsGearFill } from "react-icons/bs"; -import { FaRobot } from "react-icons/fa"; +import { FaCheck, FaRobot } from "react-icons/fa"; export default function PitScoutingCard(props: { pitreports: Pitreport[]; @@ -49,13 +49,14 @@ export default function PitScoutingCard(props: {
{report.submitted ? ( - {`Team + // {`Team + ) : ( )} diff --git a/components/stats/SmallGraph.tsx b/components/stats/SmallGraph.tsx index 02bcd9d8..31e6dd35 100644 --- a/components/stats/SmallGraph.tsx +++ b/components/stats/SmallGraph.tsx @@ -1,4 +1,4 @@ -import { Defense } from "@/lib/Enums"; +import { Defense, ReefscapeEnums } from "@/lib/Enums"; import { Report } from "@/lib/Types"; import ClientApi from "@/lib/api/ClientApi"; @@ -13,6 +13,7 @@ import { } from "chart.js"; import { useEffect, useState } from "react"; import { Bar } from "react-chartjs-2"; +import { removeDuplicates } from "../../lib/client/ClientUtils"; ChartJS.register( CategoryScale, @@ -66,17 +67,17 @@ export default function SmallGraph(props: { ), ); - interface Datapoint { - x: number; - y: number; - } + const [dataset, setDataset] = useState< + Set<{ + matchNumber: number; + data: Record; + }> + >(new Set()); - const [datapoints, setDataPoints] = useState(null); const [currentTeam, setCurrentTeam] = useState(0); function dataToNumber(key: string, data: any): number { if (key === "Defense") { - let n = 0; switch (data) { case Defense.None: return 0; @@ -85,18 +86,31 @@ export default function SmallGraph(props: { case Defense.Full: return 1; } + } else if (key === "EndgameClimbStatus") { + switch (data) { + case ReefscapeEnums.EndgameClimbStatus.None: + return 0; + case ReefscapeEnums.EndgameClimbStatus.Park: + return 0.33; + case ReefscapeEnums.EndgameClimbStatus.High: + return 0.66; + case ReefscapeEnums.EndgameClimbStatus.Low: + return 1; + } } return data; } + const datasetArr = Array.from(dataset); + const data = { - labels: datapoints?.map((point) => point.x) ?? [], + labels: removeDuplicates( + datasetArr.map((point) => point.matchNumber) ?? [], + ), datasets: [ { label: key, - data: props.selectedReports?.map((report) => - dataToNumber(key, report.data[key]), - ), + data: datasetArr.map((report) => dataToNumber(key, report.data[key])), backgroundColor: "rgba(255, 99, 132, 0.5)", }, ], @@ -105,23 +119,27 @@ export default function SmallGraph(props: { useEffect(() => { if (!props.selectedReports) return; - setDataPoints([]); setCurrentTeam(props.team); - for (const report of props.selectedReports) { - api.findMatchById(report.match).then((match) => { + + const newDataset: typeof dataset = new Set(); + + Promise.all( + props.selectedReports.map(async (report) => { + const match = await api.findMatchById(report.match); if (!match) return; - setDataPoints((prev) => - [ - ...(prev ?? []), - { - x: match.number, - y: dataToNumber(key, report.data[key]), - }, - ].sort((a, b) => a.x - b.x), - ); - }); - } + newDataset.add({ + matchNumber: match.number, + data: report.data, + }); + }), + ).then(() => + setDataset( + new Set( + Array.from(newDataset).sort((a, b) => a.matchNumber - b.matchNumber), + ), + ), + ); }, [key, currentTeam, props.selectedReports, props.team]); if (!props.selectedReports) { @@ -151,10 +169,7 @@ export default function SmallGraph(props: { point.x) ?? [], - }} + data={data} />
); diff --git a/lib/CompetitionHandling.ts b/lib/CompetitionHandling.ts index e8720aa6..8ea481b5 100644 --- a/lib/CompetitionHandling.ts +++ b/lib/CompetitionHandling.ts @@ -15,6 +15,8 @@ import { games } from "./games"; import { GameId } from "./client/GameId"; import CollectionId from "./client/CollectionId"; import DbInterface from "./client/dbinterfaces/DbInterface"; +import { _id } from "@next-auth/mongodb-adapter"; +import { match } from "assert"; type ScheduleMatch = { subjectiveScouter?: string; @@ -85,12 +87,16 @@ export async function assignScoutersToCompetitionMatches( games[comp.gameId].league == League.FRC ? 6 : 4, ); + const matches = await db.findObjects(CollectionId.Matches, { + _id: { $in: comp.matches.map((m) => new ObjectId(m)) }, + }); + + matches.sort((a, b) => a.number - b.number); + const promises: Promise[] = []; - for (let i = 0; i < comp.matches.length; i++) { + for (let i = 0; i < matches.length; i++) { // Filter out the subjective scouter that will be assigned to this match - promises.push( - assignScoutersToMatch(db, comp.matches[i], comp.gameId, schedule[i]), - ); + promises.push(assignScoutersToMatch(db, matches[i], schedule[i])); } await Promise.all(promises); @@ -99,19 +105,9 @@ export async function assignScoutersToCompetitionMatches( async function assignScoutersToMatch( db: DbInterface, - matchId: string, - gameId: GameId, + match: Match, schedule: ScheduleMatch, ) { - const match = await db.findObjectById( - CollectionId.Matches, - new ObjectId(matchId), - ); - - if (!match) { - throw new Error(`Match not found: ${matchId}`); - } - match.subjectiveScouter = schedule.subjectiveScouter; const existingReportPromises = match.reports.map((r) => diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index 1867b94b..b2ca5758 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -270,7 +270,17 @@ export default function DbInterfaceAuthAdapter( rollbar.warn("Account already exists when linking account", { account, }); - return format.from(existingAccount); + + let formattedAccount: AdapterAccount; + + // Sometimes gives an error about not finding toHexString. + try { + formattedAccount = format.from(existingAccount); + } catch (e) { + account.userId = new ObjectId(account.userId) as any; + formattedAccount = format.from(account); + } + return formattedAccount; } await db.addObject(CollectionId.Accounts, account); diff --git a/lib/Enums.ts b/lib/Enums.ts index 4c8dc4a0..ccd7a6f0 100644 --- a/lib/Enums.ts +++ b/lib/Enums.ts @@ -103,14 +103,6 @@ export namespace ReefscapeEnums { ScoreMoreThanOneCoral = "ScoreMoreThanOneCoral", } - export enum CoralLevel { - None = "None", - L1 = "L1", - L2 = "L2", - L3 = "L3", - L4 = "L4", - } - export enum Climbing { No = "No", Deep = "Deep", diff --git a/lib/Layout.ts b/lib/Layout.ts index c52cb358..01ef80d1 100644 --- a/lib/Layout.ts +++ b/lib/Layout.ts @@ -230,7 +230,6 @@ export function keyToType( if (key == "Climbing") return ReefscapeEnums.Climbing; if (key == "DriveThroughDeepCage") return ReefscapeEnums.DriveThroughDeepCage; if (key == "EndgameClimbStatus") return ReefscapeEnums.EndgameClimbStatus; - if (key == "HighestCoralLevel") return ReefscapeEnums.CoralLevel; for (const e of enums) { if (Object.values(e).includes(exampleData[key])) return e; diff --git a/lib/Types.ts b/lib/Types.ts index d006e7f0..32cf938f 100644 --- a/lib/Types.ts +++ b/lib/Types.ts @@ -254,7 +254,7 @@ export class Game< league: League, ) { const finalLayout: typeof layout = { - Image: [{ key: "image", type: "image" }], + // Image: [{ key: "image", type: "image" }], Drivetrain: ["drivetrain"], }; diff --git a/lib/api/ApiUtils.ts b/lib/api/ApiUtils.ts index 4f47703d..269d52b6 100644 --- a/lib/api/ApiUtils.ts +++ b/lib/api/ApiUtils.ts @@ -1,3 +1,6 @@ +/** + * @tested_by tests/lib/api/ApiUtils.test.ts + */ import { ObjectId } from "bson"; import CollectionId from "../client/CollectionId"; import DbInterface from "../client/dbinterfaces/DbInterface"; @@ -164,3 +167,123 @@ export async function addXp(db: DbInterface, userId: string, xp: number) { level: newLevel, }); } + +export async function deleteReport( + db: DbInterface, + reportId: string, + match?: Match, +) { + if (match) { + await db.updateObjectById(CollectionId.Matches, new ObjectId(match._id), { + reports: match.reports.filter((id) => id !== reportId), + }); + } + + await db.deleteObjectById(CollectionId.Reports, new ObjectId(reportId)); +} + +export async function deleteSubjectiveReport( + db: DbInterface, + reportId: string, + match?: Match, +) { + if (match) { + await db.updateObjectById(CollectionId.Matches, new ObjectId(match._id), { + subjectiveReports: match.subjectiveReports.filter( + (id) => id !== reportId, + ), + }); + } + + await db.deleteObjectById( + CollectionId.SubjectiveReports, + new ObjectId(reportId), + ); +} + +export async function deleteMatch( + db: DbInterface, + matchId: string, + comp?: Competition, +) { + const match = await db.findObjectById( + CollectionId.Matches, + new ObjectId(matchId), + ); + + if (!match) return; + + if (comp) { + db.updateObjectById(CollectionId.Competitions, new ObjectId(comp._id), { + matches: comp.matches.filter((id) => id !== match._id?.toString()), + }); + } + + await Promise.all([ + ...match.reports.map(async (reportId) => deleteReport(db, reportId, match)), + ...match.subjectiveReports.map(async (reportId) => + deleteSubjectiveReport(db, reportId, match), + ), + ]); + + await db.deleteObjectById(CollectionId.Matches, new ObjectId(match._id)); +} + +export async function deletePitReport( + db: DbInterface, + reportId: string, + comp?: Competition, +) { + if (comp) { + db.updateObjectById(CollectionId.Competitions, new ObjectId(comp._id), { + pitReports: comp.pitReports.filter((id) => id !== reportId.toString()), + }); + } + + await db.deleteObjectById(CollectionId.PitReports, new ObjectId(reportId)); +} + +export async function deleteComp(db: DbInterface, comp: Competition) { + const season = await getSeasonFromComp(db, comp); + + if (season) { + db.updateObjectById(CollectionId.Seasons, new ObjectId(season._id), { + competitions: season.competitions.filter( + (id) => id !== comp._id?.toString(), + ), + }); + } + + await Promise.all([ + ...comp.matches.map(async (matchId) => deleteMatch(db, matchId, comp)), + ...comp.pitReports.map(async (reportId) => + deletePitReport(db, reportId, comp), + ), + ]); + + await db.deleteObjectById(CollectionId.Competitions, new ObjectId(comp._id)); +} + +export async function deleteSeason(db: DbInterface, season: Season) { + const team = await getTeamFromSeason(db, season); + + if (team) { + db.updateObjectById(CollectionId.Teams, new ObjectId(team._id), { + seasons: team.seasons.filter((id) => id !== season._id?.toString()), + }); + } + + await Promise.all([ + ...season.competitions.map(async (compId) => { + const comp = await db.findObjectById( + CollectionId.Competitions, + new ObjectId(compId), + ); + if (comp) { + await deleteComp(db, comp); + } + }), + ]); + + await db.deleteObjectById(CollectionId.Seasons, new ObjectId(season._id)); +} diff --git a/lib/api/ClientApi.ts b/lib/api/ClientApi.ts index ae88257f..96bc0685 100644 --- a/lib/api/ClientApi.ts +++ b/lib/api/ClientApi.ts @@ -24,7 +24,10 @@ import { import { NotLinkedToTba, removeDuplicates } from "../client/ClientUtils"; import { addXp, + deleteComp, + deleteSeason, generatePitReports, + getSeasonFromComp, getTeamFromMatch, getTeamFromReport, onTeam, @@ -2534,4 +2537,34 @@ export default class ClientApi extends NextApiTemplate { return res.status(200).send({ result: "success" }); }, }); + + deleteComp = createNextRoute< + [string], + { result: string }, + ApiDependencies, + { team: Team; comp: Competition } + >({ + isAuthorized: (req, res, deps, [compId]) => + AccessLevels.IfCompOwner(req, res, deps, compId), + handler: async (req, res, { db }, { team, comp }, [compId]) => { + await deleteComp(await db, comp); + + return res.status(200).send({ result: "success" }); + }, + }); + + deleteSeason = createNextRoute< + [string], + { result: string }, + ApiDependencies, + { team: Team; season: Season } + >({ + isAuthorized: (req, res, deps, [seasonId]) => + AccessLevels.IfSeasonOwner(req, res, deps, seasonId), + handler: async (req, res, { db }, { team, season }, [seasonId]) => { + await deleteSeason(await db, season); + + return res.status(200).send({ result: "success" }); + }, + }); } diff --git a/lib/games.ts b/lib/games.ts index ee33ba1e..f20fa8d9 100644 --- a/lib/games.ts +++ b/lib/games.ts @@ -1189,6 +1189,7 @@ namespace Reefscape { EndgameClimbStatus: ReefscapeEnums.EndgameClimbStatus = ReefscapeEnums.EndgameClimbStatus.None; + EndGameDefenseStatus: Defense = Defense.None; } export class PitData extends PitReportData { @@ -1200,11 +1201,13 @@ namespace Reefscape { 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; - HighestCoralLevel: ReefscapeEnums.CoralLevel = - ReefscapeEnums.CoralLevel.None; } const pitReportLayout: FormLayoutProps = { @@ -1220,7 +1223,10 @@ namespace Reefscape { }, { key: "CanScoreAlgaeInNet", label: "Can Score Algae in Net?" }, { key: "Climbing", label: "Climbing?" }, - { key: "HighestCoralLevel", label: "Highest Coral Level" }, + { 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?" }, @@ -1309,7 +1315,7 @@ namespace Reefscape { [{ key: "TeleopAlgaeScoredNet", label: "Algae Scored Net (Teleop)" }], ], ], - "Post Match": ["EndgameClimbStatus"], + "Post Match": ["EndgameClimbStatus", "Defense"], }; const statsLayout: StatsLayout = { @@ -1582,23 +1588,27 @@ namespace Reefscape { if (pitReport?.data?.CanDriveUnderShallowCage) badges.push({ text: "Can Drive Under Shallow Cage", color: "info" }); - switch (pitReport?.data?.HighestCoralLevel) { - case ReefscapeEnums.CoralLevel.None: - badges.push({ text: "No Coral", color: "warning" }); - break; - case ReefscapeEnums.CoralLevel.L1: - badges.push({ text: "L1 Coral", color: "info" }); - break; - case ReefscapeEnums.CoralLevel.L2: - badges.push({ text: "L2 Coral", color: "secondary" }); - break; - case ReefscapeEnums.CoralLevel.L3: - badges.push({ text: "L3 Coral", color: "primary" }); - break; - case ReefscapeEnums.CoralLevel.L4: - badges.push({ text: "L4 Coral", color: "accent" }); - break; - } + 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; } diff --git a/lib/testutils/TestUtils.ts b/lib/testutils/TestUtils.ts index 7514764a..d7f80896 100644 --- a/lib/testutils/TestUtils.ts +++ b/lib/testutils/TestUtils.ts @@ -1,6 +1,14 @@ import { NextApiResponse } from "next"; import { ObjectId } from "bson"; -import { User } from "../Types"; +import { + Competition, + Match, + Pitreport, + Season, + SubjectiveReport, + User, + Report, +} from "../Types"; import ApiDependencies from "../api/ApiDependencies"; import CollectionId from "../client/CollectionId"; import DbInterface from "../client/dbinterfaces/DbInterface"; @@ -109,3 +117,46 @@ export function getTestRollbar(): RollbarInterface { debug: jest.fn(), }; } + +/** + * Creates a set of test documents for the database. + * This includes a report, subjective report, match, pit report, competition, season, and team. + */ +export async function createTestDocuments(db: DbInterface) { + const matchId = new ObjectId(); + + const report = await db.addObject(CollectionId.Reports, { + match: matchId.toString(), + } as any as Report); + + const subjectiveReport = await db.addObject( + CollectionId.SubjectiveReports, + {} as any as SubjectiveReport, + ); + + const match = await db.addObject(CollectionId.Matches, { + _id: matchId, + reports: [report._id!.toString()], + subjectiveReports: [subjectiveReport._id!.toString()], + } as any as Match); + + const pitReport = await db.addObject( + CollectionId.PitReports, + {} as any as Pitreport, + ); + + const comp = await db.addObject(CollectionId.Competitions, { + matches: [match._id!.toString()], + pitReports: [pitReport._id!.toString()], + } as any as Competition); + + const season = await db.addObject(CollectionId.Seasons, { + competitions: [comp._id!.toString()], + } as any as Season); + + const team = await db.addObject(CollectionId.Teams, { + seasons: [season._id!.toString()], + } as any as any); + + return { report, subjectiveReport, match, pitReport, comp, season, team }; +} diff --git a/package-lock.json b/package-lock.json index 3aef143c..e0c88b87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sj3", - "version": "1.2.17", + "version": "1.2.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sj3", - "version": "1.2.17", + "version": "1.2.22", "license": "CC BY-NC-SA 4.0", "dependencies": { "dependencies": "^0.0.1", @@ -21,14 +21,14 @@ "bson": "^5.0.0", "dotenv": "^16.4.7", "eslint": "9.18.0", - "eslint-config-next": "15.1.6", + "eslint-config-next": "15.2.2", "formidable": "^3.5.2", - "jose": "^5.9.6", + "jose": "^6.0.8", "levenary": "^1.1.1", "minimongo": "^6.19.0", "mongo-anywhere": "^1.1.11", "mongodb": "^5.0.0", - "next": "^15.1.6", + "next": "^15.2.3", "next-auth": "^4.24.11", "next-seo": "^6.6.0", "omit-call-signature": "^1.0.15", @@ -49,7 +49,7 @@ "rollbar": "^2.26.4", "string-similarity-js": "^2.1.4", "ts-node": "^10.9.2", - "tsx": "^4.19.2", + "tsx": "^4.19.3", "typescript": "5.7.3", "unified-api-nextjs": "^1.0.9" }, @@ -65,7 +65,7 @@ "cross-env": "^7.0.3", "daisyui": "^4.12.22", "jest": "^29.7.0", - "postcss": "^8.5.1", + "postcss": "^8.5.3", "prettier": "3.5.0", "serwist": "^9.0.11", "tailwindcss": "^3.4.17", @@ -697,9 +697,10 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -791,13 +792,398 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -1544,15 +1930,15 @@ } }, "node_modules/@next/env": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.6.tgz", - "integrity": "sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==", + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.3.tgz", + "integrity": "sha512-a26KnbW9DFEUsSxAxKBORR/uD9THoYoKbkpFywMN/AFvboTt94b8+g/07T8J6ACsdLag8/PDU60ov4rPxRAixw==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.6.tgz", - "integrity": "sha512-+slMxhTgILUntZDGNgsKEYHUvpn72WP1YTlkmEhS51vnVd7S9jEEy0n9YAMcI21vUG4akTw9voWH02lrClt/yw==", + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.2.2.tgz", + "integrity": "sha512-1+BzokFuFQIfLaRxUKf2u5In4xhPV7tUgKcK53ywvFl6+LXHWHpFkcV7VNeKlyQKUotwiq4fy/aDNF9EiUp4RQ==", "license": "MIT", "dependencies": { "fast-glob": "3.3.1" @@ -1587,9 +1973,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.6.tgz", - "integrity": "sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==", + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.3.tgz", + "integrity": "sha512-uaBhA8aLbXLqwjnsHSkxs353WrRgQgiFjduDpc7YXEU0B54IKx3vU+cxQlYwPCyC8uYEEX7THhtQQsfHnvv8dw==", "cpu": [ "arm64" ], @@ -1603,9 +1989,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.6.tgz", - "integrity": "sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==", + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.3.tgz", + "integrity": "sha512-pVwKvJ4Zk7h+4hwhqOUuMx7Ib02u3gDX3HXPKIShBi9JlYllI0nU6TWLbPT94dt7FSi6mSBhfc2JrHViwqbOdw==", "cpu": [ "x64" ], @@ -1619,9 +2005,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.6.tgz", - "integrity": "sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==", + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.3.tgz", + "integrity": "sha512-50ibWdn2RuFFkOEUmo9NCcQbbV9ViQOrUfG48zHBCONciHjaUKtHcYFiCwBVuzD08fzvzkWuuZkd4AqbvKO7UQ==", "cpu": [ "arm64" ], @@ -1635,9 +2021,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.6.tgz", - "integrity": "sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==", + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.3.tgz", + "integrity": "sha512-2gAPA7P652D3HzR4cLyAuVYwYqjG0mt/3pHSWTCyKZq/N/dJcUAEoNQMyUmwTZWCJRKofB+JPuDVP2aD8w2J6Q==", "cpu": [ "arm64" ], @@ -1651,9 +2037,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.6.tgz", - "integrity": "sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==", + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.3.tgz", + "integrity": "sha512-ODSKvrdMgAJOVU4qElflYy1KSZRM3M45JVbeZu42TINCMG3anp7YCBn80RkISV6bhzKwcUqLBAmOiWkaGtBA9w==", "cpu": [ "x64" ], @@ -1667,9 +2053,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.6.tgz", - "integrity": "sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==", + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.3.tgz", + "integrity": "sha512-ZR9kLwCWrlYxwEoytqPi1jhPd1TlsSJWAc+H/CJHmHkf2nD92MQpSRIURR1iNgA/kuFSdxB8xIPt4p/T78kwsg==", "cpu": [ "x64" ], @@ -1683,9 +2069,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.6.tgz", - "integrity": "sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==", + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.3.tgz", + "integrity": "sha512-+G2FrDcfm2YDbhDiObDU/qPriWeiz/9cRR0yMWJeTLGGX6/x8oryO3tt7HhodA1vZ8r2ddJPCjtLcpaVl7TE2Q==", "cpu": [ "arm64" ], @@ -1699,9 +2085,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.6.tgz", - "integrity": "sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==", + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.3.tgz", + "integrity": "sha512-gHYS9tc+G2W0ZC8rBL+H6RdtXIyk40uLiaos0yj5US85FNhbFEndMA2nW3z47nzOWiSvXTZ5kBClc3rD0zJg0w==", "cpu": [ "x64" ], @@ -4478,10 +4864,11 @@ } }, "node_modules/esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -4489,30 +4876,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" } }, "node_modules/escalade": { @@ -4594,12 +4982,12 @@ } }, "node_modules/eslint-config-next": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.1.6.tgz", - "integrity": "sha512-Wd1uy6y7nBbXUSg9QAuQ+xYEKli5CgUhLjz1QHW11jLDis5vK5XB3PemL6jEmy7HrdhaRFDz+GTZ/3FoH+EUjg==", + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.2.2.tgz", + "integrity": "sha512-g34RI7RFS4HybYFwGa/okj+8WZM+/fy+pEM+aqRQoVvM4gQhKrd4wIEddKmlZfWD75j8LTwB5zwkmNv3DceH1A==", "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.1.6", + "@next/eslint-plugin-next": "15.2.2", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -7440,9 +7828,10 @@ } }, "node_modules/jose": { - "version": "5.9.6", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", - "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==", + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.8.tgz", + "integrity": "sha512-EyUPtOKyTYq+iMOszO42eobQllaIjJnwkZ2U93aJzNyPibCy7CEvT9UQnaCVB51IAd49gbNdCew1c0LcLTCB2g==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } @@ -8014,12 +8403,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "node_modules/next": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/next/-/next-15.1.6.tgz", - "integrity": "sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q==", + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-15.2.3.tgz", + "integrity": "sha512-x6eDkZxk2rPpu46E1ZVUWIBhYCLszmUY6fvHBFcbzJ9dD+qRX6vcHusaqqDlnY+VngKzKbAiG2iRCkPbmi8f7w==", "license": "MIT", "dependencies": { - "@next/env": "15.1.6", + "@next/env": "15.2.3", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", @@ -8034,14 +8423,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.1.6", - "@next/swc-darwin-x64": "15.1.6", - "@next/swc-linux-arm64-gnu": "15.1.6", - "@next/swc-linux-arm64-musl": "15.1.6", - "@next/swc-linux-x64-gnu": "15.1.6", - "@next/swc-linux-x64-musl": "15.1.6", - "@next/swc-win32-arm64-msvc": "15.1.6", - "@next/swc-win32-x64-msvc": "15.1.6", + "@next/swc-darwin-arm64": "15.2.3", + "@next/swc-darwin-x64": "15.2.3", + "@next/swc-linux-arm64-gnu": "15.2.3", + "@next/swc-linux-arm64-musl": "15.2.3", + "@next/swc-linux-x64-gnu": "15.2.3", + "@next/swc-linux-x64-musl": "15.2.3", + "@next/swc-win32-arm64-msvc": "15.2.3", + "@next/swc-win32-x64-msvc": "15.2.3", "sharp": "^0.33.5" }, "peerDependencies": { @@ -8710,9 +9099,9 @@ } }, "node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, "funding": [ { @@ -10605,12 +10994,12 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/tsx": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", - "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", "license": "MIT", "dependencies": { - "esbuild": "~0.23.0", + "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "bin": { diff --git a/package.json b/package.json index 49e4247f..cdcfe611 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sj3", - "version": "1.2.17", + "version": "1.2.22", "private": true, "repository": "https://github.com/Decatur-Robotics/Gearbox", "license": "CC BY-NC-SA 4.0", @@ -30,14 +30,14 @@ "dependencies": "^0.0.1", "dotenv": "^16.4.7", "eslint": "9.18.0", - "eslint-config-next": "15.1.6", + "eslint-config-next": "15.2.2", "formidable": "^3.5.2", - "jose": "^5.9.6", + "jose": "^6.0.8", "levenary": "^1.1.1", "minimongo": "^6.19.0", "mongo-anywhere": "^1.1.11", "mongodb": "^5.0.0", - "next": "^15.1.6", + "next": "^15.2.3", "next-auth": "^4.24.11", "next-seo": "^6.6.0", "omit-call-signature": "^1.0.15", @@ -58,7 +58,7 @@ "rollbar": "^2.26.4", "string-similarity-js": "^2.1.4", "ts-node": "^10.9.2", - "tsx": "^4.19.2", + "tsx": "^4.19.3", "typescript": "5.7.3", "unified-api-nextjs": "^1.0.9" }, @@ -74,7 +74,7 @@ "cross-env": "^7.0.3", "daisyui": "^4.12.22", "jest": "^29.7.0", - "postcss": "^8.5.1", + "postcss": "^8.5.3", "prettier": "3.5.0", "serwist": "^9.0.11", "tailwindcss": "^3.4.17", diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pitstats.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pitstats.tsx index fd53b208..dfec166a 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pitstats.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pitstats.tsx @@ -27,6 +27,7 @@ import { games } from "@/lib/games"; import { PitStatsLayout, Badge } from "@/lib/Layout"; import CollectionId from "@/lib/client/CollectionId"; import { matchesMiddleware } from "next/dist/shared/lib/router/router"; +import { Round } from "../../../../lib/client/StatsMath"; const api = new ClientApi(); @@ -131,7 +132,8 @@ function TeamSlide(props: { return (

- {stat.label}: {stat.value}{" "} + {stat.label}:{" "} + {Round(stat.value)}{" "} (Ranked #{stat.rank}/{stat.maxRanking}) @@ -189,12 +191,16 @@ function TeamSlide(props: {

- {pit.submitted ? ( - {pit.teamNumber.toString()} + {pit ? ( + pit.submitted ? ( + {pit.teamNumber.toString()} + ) : ( + <> + ) ) : ( <> )} @@ -474,7 +480,10 @@ export default function Pitstats(props: { competition: Competition }) { {!reports ? (

Loading...

) : Object.keys(reports).length === 0 ? ( -

No data.

+

+ No data (try creating pit reports for your event's teams then + try again). +

) : (
{currentSlide === -1 ? : slides[currentSlide]} diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx index 1a588ba2..6f342c26 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx @@ -86,6 +86,16 @@ export default function Stats(props: { Object.keys(r.robotComments).forEach((c) => teams.add(+c)), ); //+str converts to number + let internalReportCount = 0, + externalReportCount = 0; + reports.forEach((r) => { + if (props.competition.matches.includes(r.match)) { + internalReportCount++; + } else { + externalReportCount++; + } + }); + return ( setUsePublicData(!usePublicData)} > {usePublicData ? ( -
Using public data
+
+ Using public data ({internalReportCount} internal reports +{" "} + {externalReportCount} external reports) +
) : ( -
Not using public data
+
+ Not using public data ({internalReportCount} internal reports) +
)}
(Click to toggle)
diff --git a/pages/[teamSlug]/[seasonSlug]/index.tsx b/pages/[teamSlug]/[seasonSlug]/index.tsx index 42a9a29b..50c97d8b 100644 --- a/pages/[teamSlug]/[seasonSlug]/index.tsx +++ b/pages/[teamSlug]/[seasonSlug]/index.tsx @@ -11,8 +11,9 @@ import { getDatabase } from "@/lib/MongoDB"; import CollectionId from "@/lib/client/CollectionId"; import CompetitionCard from "@/components/CompetitionCard"; import Loading from "@/components/Loading"; -import { FaPlus } from "react-icons/fa"; +import { FaPlus, FaTrash } from "react-icons/fa"; import { ObjectId } from "bson"; +import toast from "react-hot-toast"; const api = new ClientApi(); @@ -29,6 +30,28 @@ export default function Home(props: SeasonPageProps) { const comps = props.competitions; const owner = team?.owners.includes(session?.user?._id as string); + function deleteSeason() { + if (!season?._id) return; + + const confirmKey = `delete-season-${season.slug}`; + if ( + prompt( + `If you are sure you want to IRREVOCABLY delete this season and all data associated with it, including competitions, type "${confirmKey}"`, + ) === confirmKey + ) { + toast.promise( + api.deleteSeason(season._id).finally(() => { + window.location.href = `/${team?.slug}`; + }), + { + loading: "Deleting season...", + success: "Season deleted successfully!", + error: "Error deleting season.", + }, + ); + } else toast.error("Season not deleted."); + } + return ( The {season.year} Season -
+ {owner && ( + + )} +
diff --git a/pages/signin.tsx b/pages/signin.tsx index 74e03cc5..cea2c640 100644 --- a/pages/signin.tsx +++ b/pages/signin.tsx @@ -1,103 +1,5 @@ import Container from "@/components/Container"; -import { signIn } from "next-auth/react"; -import { useRouter } from "next/router"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { - GoogleReCaptchaProvider, - useGoogleReCaptcha, -} from "react-google-recaptcha-v3"; -import { FaGoogle, FaSlack } from "react-icons/fa"; - -const errorMessages: { [error: string]: string } = { - oauthcallback: "Failed to sign in with OAuth provider.", - callback: "A server-side error occurred during sign in.", -}; - -function SignInCard() { - const router = useRouter(); - const emailRef = useRef(null); - const { executeRecaptcha } = useGoogleReCaptcha(); - - const [error, setError] = useState(router.query.error as string); - - useEffect(() => { - if (router.query.error) { - const error = (router.query.error as string).toLowerCase(); - const message = - (error in errorMessages ? errorMessages[error] : error) + - " Try clearing your cookies and then signing in again."; - - setError(message); - } - }, [router.query.error]); - - function signInWithCallbackUrl(provider: string, options?: object) { - const callbackUrl = router.query.callbackUrl as string; - - signIn(provider, { callbackUrl, ...options }); - } - async function logInWithEmail() { - const email = emailRef.current?.value; - - if (!email) { - setError("Email is required"); - return; - } - - if (!executeRecaptcha) { - setError("Recaptcha not available"); - return; - } - - const captchaToken = await executeRecaptcha(); - - signInWithCallbackUrl("email", { email, captchaToken }); - } - - return ( -
-
-

Sign In

- {error &&

{error}

} -

Choose a login provider

-
- - - - - -
-
-

Email Sign In

- - -
-
-
- ); -} +import SignInMenu from "@/components/SignInMenu"; export default function SignIn() { return ( @@ -105,13 +7,7 @@ export default function SignIn() { requireAuthentication={false} title="Sign In" > - -
- -
-
+ ); } diff --git a/tailwind.config.js b/tailwind.config.js index 2bb2fb70..0a1bc6b7 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -10,6 +10,12 @@ module.exports = { ], theme: { extend: { + keyframes: { + borderFlash: { + "0%, 100%": { borderColor: "transparent" }, + "50%": { borderColor: "red" }, + }, + }, dropShadow: { glowWeak: ["0 0px 20px oklch(65.69% 0.196 275.75 / .6)"], glowStrong: [ @@ -22,6 +28,7 @@ module.exports = { "spin-slow": "spin 3s linear infinite", float: "float 4s ease-in-out infinite", "float-offset": "float 2s ease-in-out infinite", + borderFlash: "borderFlash 2s linear infinite", }, }, blur: { diff --git a/tests/lib/api/ApiUtils.test.ts b/tests/lib/api/ApiUtils.test.ts new file mode 100644 index 00000000..5d8610ce --- /dev/null +++ b/tests/lib/api/ApiUtils.test.ts @@ -0,0 +1,478 @@ +import { + deleteComp, + deleteMatch, + deletePitReport, + deleteReport, + deleteSeason, + deleteSubjectiveReport, +} from "@/lib/api/ApiUtils"; +import CollectionId from "@/lib/client/CollectionId"; +import { + createTestDocuments, + getTestApiUtils, +} from "@/lib/testutils/TestUtils"; +import { _id } from "@next-auth/mongodb-adapter"; + +describe(deleteReport.name, () => { + test("Deletes the report", async () => { + const { db } = await getTestApiUtils(); + + const { report, match } = await createTestDocuments(db); + + await deleteReport(db, report._id!.toString(), match); + + const found = await db.findObjectById( + CollectionId.Reports, + report._id! as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.Reports, {})).toBe(0); + }); + + test("Removes the report from the match", async () => { + const { db } = await getTestApiUtils(); + + const { report, match } = await createTestDocuments(db); + + await deleteReport(db, report._id!.toString(), match); + + const updatedMatch = await db.findObjectById( + CollectionId.Matches, + match._id as any, + ); + + expect(updatedMatch?.reports.length).toBe(0); + }); + + test("Does not fail if not given a match", async () => { + const { db } = await getTestApiUtils(); + + const { report } = await createTestDocuments(db); + + await deleteReport(db, report._id!.toString()); + + const found = await db.findObjectById( + CollectionId.Reports, + report._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.Reports, {})).toBe(0); + }); + + test("Does not remove report from match if not given a match", async () => { + const { db } = await getTestApiUtils(); + + const { report, match } = await createTestDocuments(db); + + await deleteReport(db, report._id!.toString()); + + const updatedMatch = await db.findObjectById( + CollectionId.Matches, + match._id as any, + ); + + expect(updatedMatch?.reports).toStrictEqual([report._id!.toString()]); + }); +}); + +describe(deleteSubjectiveReport.name, () => { + test("Deletes the report", async () => { + const { db } = await getTestApiUtils(); + + const { subjectiveReport, match } = await createTestDocuments(db); + + await deleteSubjectiveReport(db, subjectiveReport._id!.toString(), match); + + const found = await db.findObjectById( + CollectionId.SubjectiveReports, + subjectiveReport._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.SubjectiveReports, {})).toBe(0); + }); + + test("Removes the report from the match", async () => { + const { db } = await getTestApiUtils(); + + const { subjectiveReport, match } = await createTestDocuments(db); + + await deleteSubjectiveReport(db, subjectiveReport._id!.toString(), match); + + const updatedMatch = await db.findObjectById( + CollectionId.Matches, + match._id as any, + ); + + expect(updatedMatch?.subjectiveReports.length).toBe(0); + }); + + test("Does not fail if not given a match", async () => { + const { db } = await getTestApiUtils(); + + const { subjectiveReport } = await createTestDocuments(db); + + await deleteSubjectiveReport(db, subjectiveReport._id!.toString()); + + const found = await db.findObjectById( + CollectionId.SubjectiveReports, + subjectiveReport._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.SubjectiveReports, {})).toBe(0); + }); + + test("Does not remove report from match if not given a match", async () => { + const { db } = await getTestApiUtils(); + + const { subjectiveReport, match } = await createTestDocuments(db); + + await deleteSubjectiveReport(db, subjectiveReport._id!.toString()); + + const updatedMatch = await db.findObjectById( + CollectionId.Matches, + match._id as any, + ); + + expect(updatedMatch?.subjectiveReports).toStrictEqual([ + subjectiveReport._id!.toString(), + ]); + }); +}); + +describe(deleteMatch.name, () => { + test("Deletes the match", async () => { + const { db } = await getTestApiUtils(); + + const { match } = await createTestDocuments(db); + + await deleteMatch(db, match._id!.toString()); + + const found = await db.findObjectById( + CollectionId.Matches, + match._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.Matches, {})).toBe(0); + }); + + test("Removes the match from the competition", async () => { + const { db } = await getTestApiUtils(); + const { match, comp } = await createTestDocuments(db); + + await deleteMatch(db, match._id!.toString(), comp); + + const updatedComp = await db.findObjectById( + CollectionId.Competitions, + comp._id as any, + ); + + expect(updatedComp?.matches.length).toBe(0); + }); + + test("Does not fail if not given a competition", async () => { + const { db } = await getTestApiUtils(); + const { match } = await createTestDocuments(db); + + await deleteMatch(db, match._id!.toString()); + + const found = await db.findObjectById( + CollectionId.Matches, + match._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.Matches, {})).toBe(0); + }); + + test("Does not remove match from competition if not given a competition", async () => { + const { db } = await getTestApiUtils(); + const { match, comp } = await createTestDocuments(db); + + await deleteMatch(db, match._id!.toString()); + + const updatedComp = await db.findObjectById( + CollectionId.Competitions, + comp._id as any, + ); + + expect(updatedComp?.matches).toStrictEqual([match._id!.toString()]); + }); + + test("Deletes reports", async () => { + const { db } = await getTestApiUtils(); + const { match, report } = await createTestDocuments(db); + + await deleteMatch(db, match._id!.toString()); + + const found = await db.findObjectById( + CollectionId.Reports, + report._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.Reports, {})).toBe(0); + }); + + test("Deletes subjective reports", async () => { + const { db } = await getTestApiUtils(); + const { match, subjectiveReport } = await createTestDocuments(db); + + await deleteMatch(db, match._id!.toString()); + + const found = await db.findObjectById( + CollectionId.SubjectiveReports, + subjectiveReport._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.SubjectiveReports, {})).toBe(0); + }); +}); + +describe(deletePitReport.name, () => { + test("Deletes the pit report", async () => { + const { db } = await getTestApiUtils(); + const { pitReport, comp } = await createTestDocuments(db); + + await deletePitReport(db, pitReport._id!.toString(), comp); + + const found = await db.findObjectById( + CollectionId.PitReports, + pitReport._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.PitReports, {})).toBe(0); + }); + + test("Removes the pit report from the competition", async () => { + const { db } = await getTestApiUtils(); + const { pitReport, comp } = await createTestDocuments(db); + + await deletePitReport(db, pitReport._id!.toString(), comp); + + const updatedComp = await db.findObjectById( + CollectionId.Competitions, + comp._id as any, + ); + + expect(updatedComp?.pitReports.length).toBe(0); + }); + + test("Does not fail if not given a competition", async () => { + const { db } = await getTestApiUtils(); + const { pitReport } = await createTestDocuments(db); + + await deletePitReport(db, pitReport._id!.toString()); + + const found = await db.findObjectById( + CollectionId.PitReports, + pitReport._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.PitReports, {})).toBe(0); + }); + + test("Does not remove pit report from competition if not given a competition", async () => { + const { db } = await getTestApiUtils(); + const { pitReport, comp } = await createTestDocuments(db); + + await deletePitReport(db, pitReport._id!.toString()); + + const updatedComp = await db.findObjectById( + CollectionId.Competitions, + comp._id as any, + ); + + expect(updatedComp?.pitReports).toStrictEqual([pitReport._id!.toString()]); + }); +}); + +describe(deleteComp.name, () => { + test("Deletes the competition", async () => { + const { db } = await getTestApiUtils(); + const { comp } = await createTestDocuments(db); + + await deleteComp(db, comp); + + const found = await db.findObjectById( + CollectionId.Competitions, + comp._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.Competitions, {})).toBe(0); + }); + + test("Deletes matches", async () => { + const { db } = await getTestApiUtils(); + const { match, comp } = await createTestDocuments(db); + + await deleteComp(db, comp); + + const found = await db.findObjectById( + CollectionId.Matches, + match._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.Matches, {})).toBe(0); + }); + + test("Deletes reports", async () => { + const { db } = await getTestApiUtils(); + const { report, comp } = await createTestDocuments(db); + + await deleteComp(db, comp); + + const found = await db.findObjectById( + CollectionId.Reports, + report._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.Reports, {})).toBe(0); + }); + + test("Deletes subjective reports", async () => { + const { db } = await getTestApiUtils(); + const { subjectiveReport, comp } = await createTestDocuments(db); + + await deleteComp(db, comp); + + const found = await db.findObjectById( + CollectionId.SubjectiveReports, + subjectiveReport._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.SubjectiveReports, {})).toBe(0); + }); + + test("Deletes pit reports", async () => { + const { db } = await getTestApiUtils(); + const { pitReport, comp } = await createTestDocuments(db); + + await deleteComp(db, comp); + + const found = await db.findObjectById( + CollectionId.PitReports, + pitReport._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.PitReports, {})).toBe(0); + }); +}); + +describe(deleteSeason.name, () => { + test("Deletes the season", async () => { + const { db } = await getTestApiUtils(); + const { season } = await createTestDocuments(db); + + await deleteSeason(db, season); + + const found = await db.findObjectById( + CollectionId.Seasons, + season._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.Seasons, {})).toBe(0); + }); + + test("Remove the season from the team", async () => { + const { db } = await getTestApiUtils(); + const { season, team } = await createTestDocuments(db); + + await deleteSeason(db, season); + + const updatedTeam = await db.findObjectById( + CollectionId.Teams, + team._id as any, + ); + + expect(updatedTeam?.seasons.length).toBe(0); + }); + + test("Deletes competitions", async () => { + const { db } = await getTestApiUtils(); + const { comp, season } = await createTestDocuments(db); + + await deleteSeason(db, season); + + const found = await db.findObjectById( + CollectionId.Competitions, + comp._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.Competitions, {})).toBe(0); + }); + + test("Deletes matches", async () => { + const { db } = await getTestApiUtils(); + const { match, season } = await createTestDocuments(db); + + await deleteSeason(db, season); + + const found = await db.findObjectById( + CollectionId.Matches, + match._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.Matches, {})).toBe(0); + }); + + test("Deletes reports", async () => { + const { db } = await getTestApiUtils(); + const { report, season } = await createTestDocuments(db); + + await deleteSeason(db, season); + + const found = await db.findObjectById( + CollectionId.Reports, + report._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.Reports, {})).toBe(0); + }); + + test("Deletes subjective reports", async () => { + const { db } = await getTestApiUtils(); + const { subjectiveReport, season } = await createTestDocuments(db); + + await deleteSeason(db, season); + + const found = await db.findObjectById( + CollectionId.SubjectiveReports, + subjectiveReport._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.SubjectiveReports, {})).toBe(0); + }); + + test("Deletes pit reports", async () => { + const { db } = await getTestApiUtils(); + const { pitReport, season } = await createTestDocuments(db); + + await deleteSeason(db, season); + + const found = await db.findObjectById( + CollectionId.PitReports, + pitReport._id as any, + ); + + expect(found).toBeUndefined(); + expect(await db.countObjects(CollectionId.PitReports, {})).toBe(0); + }); +});