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: {
})}
-
{report.submitted ? (
-

+ //

+
) : (
)}
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 ? (
+ pit.submitted ? (
+

+ ) : (
+ <>>
+ )
) : (
<>>
)}
@@ -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);
+ });
+});