diff --git a/.env.production b/.env.production
index 4bc8765d..8b0a7a3d 100644
--- a/.env.production
+++ b/.env.production
@@ -3,4 +3,6 @@
NEXT_PUBLIC_API_URL=/api/
NEXT_PUBLIC_SLACK_CLIENT_ID=10831824934.7404945710466
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=G-1BFJYBDC76
-NEXT_PUBLIC_RECAPTCHA_KEY=6Le63OUqAAAAABxxDrbaU9OywDLLHqutVwbw7a9d
\ No newline at end of file
+NEXT_PUBLIC_RECAPTCHA_KEY=6Le63OUqAAAAABxxDrbaU9OywDLLHqutVwbw7a9d
+
+ENV_FILE=.env.production
\ No newline at end of file
diff --git a/.env.test b/.env.test
index 3eae1f85..f373e69d 100644
--- a/.env.test
+++ b/.env.test
@@ -1,4 +1,7 @@
-NEXT_PUBLIC_API_URL=http://localhost:3000/api
+NEXTAUTH_URL=http://localhost:3000/
+NEXTAUTH_SECRET=testsecret
+
+NEXT_PUBLIC_API_URL=/api/
DEVELOPER_EMAILS=["test@gmail.com"]
@@ -6,4 +9,12 @@ TOA_URL=https://example.com
TOA_APP_ID=123
TOA_KEY=456
-DEFAULT_IMAGE=https://example.com/default.jpg
\ No newline at end of file
+DEFAULT_IMAGE=https://example.com/default.jpg
+
+BASE_URL_FOR_PLAYWRIGHT=http://localhost:3000/
+ENABLE_TEST_SIGNIN_ROUTE=true
+FALLBACK_MONGODB_URI=mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000
+
+ENV_FILE=.env.test
+
+DB=playwright_tests
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 211da336..987b7490 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -66,3 +66,9 @@ jobs:
- name: Lint
run: npm run lint
+
+ e2e_test:
+ uses: ./.github/workflows/e2e_test.yml
+ permissions:
+ contents: read
+ pull-requests: write
diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml
new file mode 100644
index 00000000..e79336ca
--- /dev/null
+++ b/.github/workflows/e2e_test.yml
@@ -0,0 +1,71 @@
+name: Playwright Tests
+on: [workflow_dispatch, workflow_call]
+jobs:
+ e2e_tests:
+ timeout-minutes: 60
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ # Make sure to require each shard in GitHub!
+ shardIndex: [1, 2, 3, 4]
+ shardTotal: [4]
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: lts/*
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Install Playwright browsers
+ run: npx playwright install --with-deps
+
+ - name: Start MongoDB
+ uses: supercharge/mongodb-github-action@1.12.0
+ with:
+ mongodb-version: "8.0"
+
+ - name: Run Playwright tests
+ run: npx cross-env NODE_ENV=test playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
+
+ - name: Upload blob report to GitHub Actions Artifacts
+ if: ${{ !cancelled() }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: blob-report-${{ matrix.shardIndex }}
+ path: blob-report
+ retention-days: 1
+
+ merge_reports:
+ # Merge reports after playwright-tests, even if some shards have failed
+ if: ${{ !cancelled() }}
+ needs: [e2e_tests]
+
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: lts/*
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Download blob reports from GitHub Actions Artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: all-blob-reports
+ pattern: blob-report-*
+ merge-multiple: true
+
+ - name: Merge into HTML Report
+ run: npx playwright merge-reports --reporter html ./all-blob-reports
+
+ - name: Upload HTML report
+ uses: actions/upload-artifact@v4
+ with:
+ name: html-report--attempt-${{ github.run_attempt }}
+ path: playwright-report
+ retention-days: 14
diff --git a/.gitignore b/.gitignore
index 40dfa349..445a7479 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,4 +41,11 @@ next-env.d.ts
# PWA
public/sw.js
-public/swe-worker*
\ No newline at end of file
+public/swe-worker*
+
+# Playwright
+node_modules/
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 0f35dd6f..23f4fa17 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -8,6 +8,7 @@
"pmneo.tsimporter",
"austenc.tailwind-docs",
"bradlc.vscode-tailwindcss",
- "Orta.vscode-jest"
+ "Orta.vscode-jest",
+ "ms-playwright.playwright"
]
}
diff --git a/components/Avatar.tsx b/components/Avatar.tsx
index 7455a912..82fe40ad 100644
--- a/components/Avatar.tsx
+++ b/components/Avatar.tsx
@@ -3,7 +3,7 @@ import { levelToClassName } from "@/lib/Xp";
import { BsGearFill } from "react-icons/bs";
export default function Avatar(props: {
- user?: { image: string | undefined; level: number; admin?: boolean };
+ user?: { image: string | undefined; level?: number; admin?: boolean };
scale?: string | undefined; // Use "scale-75" for 75% scale, etc.
imgHeightOverride?: string | undefined;
showLevel?: boolean | undefined;
@@ -13,6 +13,7 @@ export default function Avatar(props: {
className?: string | undefined;
online?: boolean;
gearSize?: number;
+ altText?: string;
}) {
const { session, status } = useCurrentSession();
const user = props.user ?? session?.user;
@@ -24,8 +25,8 @@ export default function Avatar(props: {
- {(props.showLevel ?? true) && (
-
+ {props.showLevel && (
+
LVL: {user?.level}
)}
@@ -34,12 +35,12 @@ export default function Avatar(props: {
>
{admin ? (
-
+
) : (
diff --git a/components/Card.tsx b/components/Card.tsx
index b584664a..7ae27ac0 100644
--- a/components/Card.tsx
+++ b/components/Card.tsx
@@ -14,6 +14,7 @@ export default function Card(props: CardProps) {
return (
{color ? (
diff --git a/components/EditAvatarModal.tsx b/components/EditAvatarModal.tsx
new file mode 100644
index 00000000..8ba8e85e
--- /dev/null
+++ b/components/EditAvatarModal.tsx
@@ -0,0 +1,68 @@
+import { useState } from "react";
+import Card from "./Card";
+import ClientApi from "@/lib/api/ClientApi";
+import Avatar from "./Avatar";
+import Image from "next/image";
+import { Session } from "inspector/promises";
+import toast from "react-hot-toast";
+
+const api = new ClientApi();
+export default function EditAvatarModal(props: {
+ currentImg: string;
+ close: () => void;
+}) {
+ const [newAvatar, setNewAvatar] = useState
(props.currentImg);
+
+ async function updateAvatar() {
+ toast.promise(
+ api.changePFP(newAvatar).then(() => location.reload()),
+ {
+ loading: "Updating profile picture...",
+ success: "Successfully updated profile picture!",
+ error: "Failed to update profile picture!",
+ },
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/components/SignInMenu.tsx b/components/SignInMenu.tsx
index 1c9ae53b..66d17c34 100644
--- a/components/SignInMenu.tsx
+++ b/components/SignInMenu.tsx
@@ -59,7 +59,13 @@ function SignInCard() {
Sign In
{error &&
{error}
}
-
Choose a login provider
+
Choose a login provider
+
+ You currently have to sign-in
+ using either your{" "}
+ original sign-in method or
+ your email.
+
);
}
diff --git a/components/competition/InsightsAndSettingsCard.tsx b/components/competition/InsightsAndSettingsCard.tsx
index 5062d365..0cfd21b9 100644
--- a/components/competition/InsightsAndSettingsCard.tsx
+++ b/components/competition/InsightsAndSettingsCard.tsx
@@ -64,7 +64,7 @@ export default function InsightsAndSettingsCard(props: {
const exportAsCsv = async () => {
setExportPending(true);
- const res = await api.exportCompAsCsv(comp?._id!).catch((e) => {
+ const res = await api.exportCompDataAsCsv(comp?._id!).catch((e) => {
console.error(e);
return { csv: undefined };
});
@@ -82,6 +82,31 @@ export default function InsightsAndSettingsCard(props: {
setExportPending(false);
};
+ async function exportScheduleAsCsv() {
+ setExportPending(true);
+
+ const res = await api.exportCompScheduleAsCsv(comp?._id!).catch((e) => {
+ console.error(e);
+ return { csv: undefined };
+ });
+
+ if (!res) {
+ console.error("failed to export");
+ }
+
+ if (res.csv) {
+ download(
+ `${comp?.name ?? "Competition"}Schedule.csv`,
+ res.csv,
+ "text/csv",
+ );
+ } else {
+ console.error("No CSV data returned from server");
+ }
+
+ setExportPending(false);
+ }
+
const createMatch = async () => {
try {
await api.createMatch(
@@ -255,6 +280,18 @@ export default function InsightsAndSettingsCard(props: {
"Export Scouting Data as CSV"
)}
+
+ {exportPending ? (
+
+ ) : (
+ "Export Scouting Schedule as CSV"
+ )}
+
/lib/testutils/setup.ts"],
+ setupFiles: ["
/lib/testutils/JestSetup.ts"],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
@@ -164,10 +164,7 @@ const config: Config = {
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
- // testMatch: [
- // "**/__tests__/**/*.[jt]s?(x)",
- // "**/?(*.)+(spec|test).[tj]s?(x)"
- // ],
+ testMatch: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(test).[tj]s?(x)"],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
diff --git a/lib/CompetitionHandling.ts b/lib/CompetitionHandling.ts
index 8ea481b5..75ee044c 100644
--- a/lib/CompetitionHandling.ts
+++ b/lib/CompetitionHandling.ts
@@ -10,7 +10,7 @@ import {
League,
} from "./Types";
import { ObjectId } from "bson";
-import { rotateArray } from "./client/ClientUtils";
+import { rotateArray, shuffleArray } from "./client/ClientUtils";
import { games } from "./games";
import { GameId } from "./client/GameId";
import CollectionId from "./client/CollectionId";
@@ -29,6 +29,7 @@ export function generateSchedule(
matchCount: number,
robotsPerMatch: number,
) {
+ scouters = shuffleArray(scouters);
const schedule = [];
for (let i = 0; i < matchCount; i++) {
const subjectiveScouter =
diff --git a/lib/MongoDB.ts b/lib/MongoDB.ts
index d196dca9..f1ae7e1a 100644
--- a/lib/MongoDB.ts
+++ b/lib/MongoDB.ts
@@ -11,22 +11,29 @@ import { default as BaseMongoDbInterface } from "mongo-anywhere/MongoDbInterface
import CachedDbInterface from "./client/dbinterfaces/CachedDbInterface";
import { cacheOptions } from "./client/dbinterfaces/CachedDbInterface";
import { findObjectBySlugLookUp } from "./slugToId";
+import { loadEnvConfig } from "@next/env";
-if (!process.env.MONGODB_URI) {
+let uri = process.env.MONGODB_URI ?? process.env.FALLBACK_MONGODB_URI;
+
+if (!uri) {
// Necessary to allow connections from files running outside of Next
- require("dotenv").config();
+ const projectDir = process.cwd();
+ loadEnvConfig(projectDir);
+
+ uri = process.env.MONGODB_URI ?? process.env.FALLBACK_MONGODB_URI;
- if (!process.env.MONGODB_URI)
- console.error('Invalid/Missing environment variable: "MONGODB_URI"');
+ if (!uri)
+ console.warn(
+ 'Invalid/Missing environment variables: "MONGODB_URI", "FALLBACK_MONGODB_URI". Using default connection string.',
+ );
}
-const uri = process.env.MONGODB_URI ?? "mongodb://localhost:27017";
const options: MongoClientOptions = { maxPoolSize: 3 };
let client;
let clientPromise: Promise;
-if (!global.clientPromise) {
+if (uri && !global.clientPromise) {
client = new MongoClient(uri, options);
global.clientPromise = client.connect();
}
@@ -37,21 +44,19 @@ export { clientPromise };
export async function getDatabase(
useCache: boolean = true,
): Promise {
- if (!global.interface) {
- await clientPromise;
+ if (global.interface) return global.interface; // Return the existing instance if already created
- const mongo = new MongoDBInterface(clientPromise);
+ await clientPromise;
- const dbInterface = useCache
- ? new CachedDbInterface(mongo, cacheOptions)
- : mongo;
- await dbInterface.init();
- global.interface = dbInterface;
+ const mongo = new MongoDBInterface(clientPromise);
- return dbInterface;
- }
+ const dbInterface = useCache
+ ? new CachedDbInterface(mongo, cacheOptions)
+ : mongo;
+ await dbInterface.init();
+ global.interface = dbInterface;
- return global.interface;
+ return dbInterface;
}
export class MongoDBInterface
diff --git a/lib/ResendUtils.ts b/lib/ResendUtils.ts
index a2a0f468..8efd88fd 100644
--- a/lib/ResendUtils.ts
+++ b/lib/ResendUtils.ts
@@ -11,10 +11,11 @@ export interface ResendInterface {
}
export class ResendUtils implements ResendInterface {
- private static resend: Resend;
+ private static resend: Resend | undefined;
constructor() {
- ResendUtils.resend ??= new Resend(process.env.SMTP_PASSWORD);
+ if (process.env.SMTP_PASSWORD)
+ ResendUtils.resend ??= new Resend(process.env.SMTP_PASSWORD);
}
async createContact(rawUser: NextAuthUser) {
@@ -31,7 +32,7 @@ export class ResendUtils implements ResendInterface {
const nameParts = user.name?.split(" ");
- const res = await ResendUtils.resend.contacts.create({
+ const res = await ResendUtils.resend?.contacts.create({
email: user.email,
firstName: nameParts[0],
lastName: nameParts.length > 1 ? nameParts[1] : "",
@@ -39,7 +40,7 @@ export class ResendUtils implements ResendInterface {
audienceId: process.env.RESEND_AUDIENCE_ID,
});
- if (!res.data?.id) {
+ if (!res?.data?.id) {
console.error("Failed to create contact for", user.email);
console.error(res);
return;
@@ -64,7 +65,7 @@ export class ResendUtils implements ResendInterface {
return;
}
- ResendUtils.resend.emails.send({
+ ResendUtils.resend?.emails.send({
from: "Gearbox Server ",
to: JSON.parse(process.env.DEVELOPER_EMAILS), // Environment variables are always strings, so we need to parse it
subject,
diff --git a/lib/TheBlueAlliance.ts b/lib/TheBlueAlliance.ts
index c5c3baf9..3fe3456a 100644
--- a/lib/TheBlueAlliance.ts
+++ b/lib/TheBlueAlliance.ts
@@ -155,6 +155,8 @@ export namespace TheBlueAlliance {
}
async request(suburl: string): Promise {
+ if (!this.apiKey) return;
+
var res = await fetch(this.baseUrl + suburl, {
method: "GET",
headers: {
@@ -334,7 +336,7 @@ export namespace TheBlueAlliance {
async allCompetitionsToPairings(year: number) {
var allCompetitions = await this.req.getEvents(year);
var pairings: CompetitonNameIdPair[] = [];
- allCompetitions.forEach((comp) => {
+ allCompetitions?.forEach((comp) => {
pairings.push({ name: comp.name, tbaId: comp.key });
});
diff --git a/lib/Types.ts b/lib/Types.ts
index 32cf938f..8127ae02 100644
--- a/lib/Types.ts
+++ b/lib/Types.ts
@@ -32,6 +32,9 @@ export interface Session extends NextAuthSession {
_id: string;
sessionToken: string;
userId: ObjectId;
+ /**
+ * Should actually be a Date
+ */
expires: string;
}
diff --git a/lib/api/ClientApi.ts b/lib/api/ClientApi.ts
index fe0d800b..e3d5c45b 100644
--- a/lib/api/ClientApi.ts
+++ b/lib/api/ClientApi.ts
@@ -21,15 +21,16 @@ import {
LeaderboardTeam,
LinkedList,
} from "@/lib/Types";
-import { NotLinkedToTba, removeDuplicates } from "../client/ClientUtils";
+import {
+ NotLinkedToTba,
+ removeDuplicates,
+ toDict,
+} from "../client/ClientUtils";
import {
addXp,
deleteComp,
deleteSeason,
generatePitReports,
- getSeasonFromComp,
- getTeamFromMatch,
- getTeamFromReport,
onTeam,
ownsTeam,
} from "./ApiUtils";
@@ -51,14 +52,14 @@ import { RequestHelper } from "unified-api";
import { createNextRoute, NextApiTemplate } from "unified-api-nextjs";
import { Report } from "../Types";
import Logger from "../client/Logger";
-import getRollbar, { RollbarInterface } from "../client/RollbarUtils";
const requestHelper = new RequestHelper(
process.env.NEXT_PUBLIC_API_URL ?? "", // Replace undefined when env is not present (ex: for testing builds)
- (url) =>
- toast.error(
- `Failed API request: ${url}. If this is an error, please contact the developers.`,
- ),
+ (url, error) => {
+ const msg = `Failed API request: ${url}. Details: ${error}`;
+ console.error(msg);
+ toast.error(msg);
+ },
);
const logger = new Logger(["API"]);
@@ -1123,7 +1124,7 @@ export default class ClientApi extends NextApiTemplate {
},
});
- exportCompAsCsv = createNextRoute<
+ exportCompDataAsCsv = createNextRoute<
[string],
{ csv: string },
ApiDependencies,
@@ -1183,6 +1184,93 @@ export default class ClientApi extends NextApiTemplate {
},
});
+ exportCompScheduleAsCsv = createNextRoute<
+ [string],
+ { csv: string },
+ ApiDependencies,
+ { team: Team; comp: Competition }
+ >({
+ isAuthorized: (req, res, deps, [compId]) =>
+ AccessLevels.IfCompOwner(req, res, deps, compId),
+ handler: async (
+ req,
+ res,
+ { db: dbPromise, userPromise },
+ { team, comp },
+ [compId],
+ ) => {
+ const db = await dbPromise;
+
+ const matches = await db.findObjects(CollectionId.Matches, {
+ _id: { $in: comp.matches.map((matchId) => new ObjectId(matchId)) },
+ });
+ const reports = await db.findObjects(CollectionId.Reports, {
+ match: { $in: matches.map((match) => match?._id?.toString()) },
+ });
+
+ if (reports.length == 0) {
+ return res
+ .status(200)
+ .send({ error: "No reports found for competition" });
+ }
+
+ const users = await db.findObjects(CollectionId.Users, {
+ _id: {
+ $in: reports
+ .map((r) => r.user)
+ .concat(matches.map((m) => m.subjectiveScouter))
+ .flat()
+ .map((id) => new ObjectId(id)),
+ },
+ });
+
+ const reportsById = toDict(reports);
+ const usersById = toDict(users);
+
+ interface Row {
+ matchNumber: string;
+ quantScouters: string[];
+ subjectiveScouter: string;
+ }
+
+ const rows: Row[] = [
+ // Headers
+ {
+ matchNumber: "Match #",
+ quantScouters: matches[0].reports.map(
+ (_, index) => `Scouter ${index + 1}`,
+ ),
+ subjectiveScouter: "Subjective Scouter",
+ },
+ ];
+
+ for (const match of matches) {
+ rows.push({
+ matchNumber: match.number.toString(),
+ quantScouters: match.reports.map((id) =>
+ reportsById[id].user ? usersById[reportsById[id].user].name! : "",
+ ),
+ subjectiveScouter: match.subjectiveScouter
+ ? usersById[match.subjectiveScouter].name!
+ : "",
+ });
+ }
+
+ const headers = Object.values(rows[0]).flat();
+
+ let csv = "";
+ for (const row of rows) {
+ csv +=
+ Object.values(row)
+ .flat()
+ .map((str) => str.replace(",", ""))
+ .join(",") + "\n";
+ }
+
+ res.status(200).send({ csv });
+ },
+ });
+
teamCompRanking = createNextRoute<
[string, number],
{ place: number | string; max: number | string },
@@ -2621,4 +2709,55 @@ export default class ClientApi extends NextApiTemplate {
return res.status(200).send({ result: "success" });
},
});
+
+ /**
+ * Creates a user and session, and then returns the session token. Used in E2E tests.
+ */
+ testSignIn = createNextRoute<
+ [],
+ {
+ sessionToken: string;
+ user: User;
+ },
+ ApiDependencies,
+ void
+ >({
+ isAuthorized: () =>
+ Promise.resolve({
+ authorized: process.env.ENABLE_TEST_SIGNIN_ROUTE === "true",
+ authData: undefined,
+ }),
+ handler: async (req, res, { db: dbPromise }, authData, args) => {
+ const db = await dbPromise;
+
+ const user = await db.addObject(
+ CollectionId.Users,
+ new User(
+ "Test User",
+ "test@gmail.com",
+ process.env.DEFAULT_IMAGE,
+ false,
+ await GenerateSlug(db, CollectionId.Users, "Test User"),
+ [],
+ [],
+ undefined,
+ 0,
+ 0,
+ ),
+ );
+
+ const session = await db.addObject(CollectionId.Sessions, {
+ sessionToken: crypto.randomUUID().toString(),
+ userId: user._id as unknown as ObjectId,
+ expires: new Date(
+ Date.now() + 7 * 24 * 60 * 60 * 1000,
+ ) as unknown as string, // 1 week expiration
+ });
+
+ return res.status(200).send({
+ sessionToken: session.sessionToken,
+ user,
+ });
+ },
+ });
}
diff --git a/lib/client/RollbarUtils.ts b/lib/client/RollbarUtils.ts
index 984f3d1b..e2bd0fc1 100644
--- a/lib/client/RollbarUtils.ts
+++ b/lib/client/RollbarUtils.ts
@@ -13,6 +13,14 @@ export interface RollbarInterface {
export default function getRollbar(): RollbarInterface {
if (global.rollbar) return global.rollbar;
+ if (!process.env.ROLLBAR_TOKEN) {
+ return {
+ error: (...args: any[]) => console.error(...args),
+ warn: (...args: any[]) => console.warn(...args),
+ info: (...args: any[]) => console.info(...args),
+ debug: (...args: any[]) => console.debug(...args),
+ };
+ }
const rollbar = new Rollbar({
accessToken: process.env.ROLLBAR_TOKEN,
@@ -23,3 +31,34 @@ export default function getRollbar(): RollbarInterface {
global.rollbar = rollbar;
return rollbar;
}
+
+export function reportDeploymentToRollbar() {
+ const deployId = process.env.DEPLOY_ID;
+
+ if (!deployId) {
+ getRollbar().error("Missing deployId in environment variables");
+ return;
+ }
+
+ if (!process.env.ROLLBAR_TOKEN) {
+ console.warn("ROLLBAR_TOKEN is not set. Cannot report deployment.");
+ return;
+ }
+
+ const url = "https://api.rollbar.com/api/1/deploy/" + deployId;
+ const options = {
+ method: "PATCH",
+ headers: {
+ accept: "application/json",
+ "content-type": "application/json",
+ "X-Rollbar-Access-Token": process.env.ROLLBAR_TOKEN,
+ },
+ body: JSON.stringify({
+ status: "succeeded",
+ }),
+ };
+
+ fetch(url, options)
+ .then(() => console.log("Deployment reported to Rollbar"))
+ .catch((err) => getRollbar().error(err));
+}
diff --git a/lib/client/StatsMath.ts b/lib/client/StatsMath.ts
index 855d0b68..6fd009f7 100644
--- a/lib/client/StatsMath.ts
+++ b/lib/client/StatsMath.ts
@@ -146,3 +146,33 @@ export function ComparativePercentMulti(
return results;
}
+
+//Takes a list of Quantitative reports and a stat and returns the minimum value recorded for said stat
+export function GetMinimum(
+ quantitativeReports: Report[],
+ stat: string,
+) {
+ if (!quantitativeReports) return 0;
+ let minimum = quantitativeReports[0].data[stat];
+ for (let repo of quantitativeReports) {
+ if (repo.data[stat] < minimum) {
+ minimum = repo.data[stat];
+ }
+ }
+ return minimum;
+}
+
+//Takes a list of Quantitative reports and a stat and returns the maximum value recorded for said stat
+export function GetMaximum(
+ quantitativeReports: Report[],
+ stat: string,
+) {
+ if (!quantitativeReports) return 0;
+ let maximum = 0;
+ for (let repo of quantitativeReports) {
+ if (repo.data[stat] > maximum) {
+ maximum = repo.data[stat];
+ }
+ }
+ return maximum;
+}
diff --git a/lib/games.ts b/lib/games.ts
index fff434e3..756ddc0d 100644
--- a/lib/games.ts
+++ b/lib/games.ts
@@ -22,6 +22,7 @@ import {
AmpAutoPoints,
AmpTeleopPoints,
BooleanAverage,
+ GetMinimum,
MostCommonValue,
NumericalTotal,
Round,
@@ -29,6 +30,8 @@ import {
SpeakerTeleopPoints,
TrapPoints,
} from "./client/StatsMath";
+import { report } from "process";
+import { GetMaximum } from "./client/StatsMath";
function getBaseBadges(
pitReport: Pitreport | undefined,
@@ -1323,23 +1326,76 @@ namespace Reefscape {
const statsLayout: StatsLayout = {
sections: {
Auto: [
- { key: "AutoMovedPastStaringLine", label: "Avg Auto Moves Past Start" },
{
key: "AutoCoralScoredLevelOne",
label: "Avg Amt Of Coral Scored Level One Auto",
},
+ {
+ label: "> Min Auto L1 Coral",
+ get(pitData, quantitativeReports) {
+ return GetMinimum(quantitativeReports!, "AutoCoralScoredLevelOne");
+ },
+ },
+ {
+ label: "> Max Auto L1 Coral",
+ get(pitData, quantitativeReports) {
+ return GetMaximum(quantitativeReports!, "AutoCoralScoredLevelOne");
+ },
+ },
{
key: "AutoCoralScoredLevelTwo",
label: "Avg Amt Of Coral Scored Level Two Auto",
},
+ {
+ label: "> Min Auto L2 Coral",
+ get(pitData, quantitativeReports) {
+ return GetMinimum(quantitativeReports!, "AutoCoralScoredLevelTwo");
+ },
+ },
+ {
+ label: "> Max Auto L2 Coral",
+ get(pitData, quantitativeReports) {
+ return GetMaximum(quantitativeReports!, "AutoCoralScoredLevelTwo");
+ },
+ },
{
key: "AutoCoralScoredLevelThree",
label: "Avg Amt Of Coral Scored Level Three Auto",
},
+ {
+ label: "> Min Auto L3 Coral",
+ get(pitData, quantitativeReports) {
+ return GetMinimum(
+ quantitativeReports!,
+ "AutoCoralScoredLevelThree",
+ );
+ },
+ },
+ {
+ label: "> Max Auto L3 Coral",
+ get(pitData, quantitativeReports) {
+ return GetMaximum(
+ quantitativeReports!,
+ "AutoCoralScoredLevelThree",
+ );
+ },
+ },
{
key: "AutoCoralScoredLevelFour",
label: "Avg Amt Of Coral Scored Level Four Auto",
},
+ {
+ label: "> Min Auto L4 Coral",
+ get(pitData, quantitativeReports) {
+ return GetMinimum(quantitativeReports!, "AutoCoralScoredLevelFour");
+ },
+ },
+ {
+ label: "> Max Auto L4 Coral",
+ get(pitData, quantitativeReports) {
+ return GetMaximum(quantitativeReports!, "AutoCoralScoredLevelFour");
+ },
+ },
{
label: "Avg Auto Coral",
get(pitData, quantitativeReports) {
@@ -1362,33 +1418,140 @@ namespace Reefscape {
key: "AutoAlgaeRemovedFromReef",
label: "Avg Amt of Algae Removed From Reef",
},
+ {
+ label: "> Min Algae Removed From Reef",
+ get(pitData, quantitativeReports) {
+ return GetMinimum(quantitativeReports!, "AutoAlgaeRemovedFromReef");
+ },
+ },
+ {
+ label: "> Max Algae Removed From Reef",
+ get(pitData, quantitativeReports) {
+ return GetMaximum(quantitativeReports!, "AutoAlgaeRemovedFromReef");
+ },
+ },
{
key: "AutoAlgaeScoredProcessor",
label: "Avg Amt of Algae Scored Processor Auto",
},
+ {
+ label: "> Min Algae Scored In Processor",
+ get(pitData, quantitativeReports) {
+ return GetMinimum(quantitativeReports!, "AutoAlgaeScoredProcessor");
+ },
+ },
+ {
+ label: "> Max Algae Scored In Processor",
+ get(pitData, quantitativeReports) {
+ return GetMaximum(quantitativeReports!, "AutoAlgaeScoredProcessor");
+ },
+ },
{
key: "AutoAlgaeScoredNet",
label: "Avg Amt of Algae Scored Net Auto",
},
+ {
+ label: "> Min Algae Scored In Net",
+ get(pitData, quantitativeReports) {
+ return GetMinimum(quantitativeReports!, "AutoAlgaeScoredNet");
+ },
+ },
+ {
+ label: "> Max Algae Scored In Net",
+ get(pitData, quantitativeReports) {
+ return GetMaximum(quantitativeReports!, "AutoAlgaeScoredNet");
+ },
+ },
],
Teleop: [
- { key: "GroundIntake", label: "Has Ground Intake?" },
{
key: "TeleopCoralScoredLevelOne",
label: "Avg Amt Of Coral Scored Level One Teleop",
},
+ {
+ label: "> Min L1 Coral Scored",
+ get(pitData, quantitativeReports) {
+ return GetMinimum(
+ quantitativeReports!,
+ "TeleopCoralScoredLevelOne",
+ );
+ },
+ },
+ {
+ label: "> Max L1 Coral Scored",
+ get(pitData, quantitativeReports) {
+ return GetMaximum(
+ quantitativeReports!,
+ "TeleopCoralScoredLevelOne",
+ );
+ },
+ },
{
key: "TeleopCoralScoredLevelTwo",
label: "Avg Amt Of Coral Scored Level Two Teleop",
},
+ {
+ label: "> Min L2 Coral Scored",
+ get(pitData, quantitativeReports) {
+ return GetMinimum(
+ quantitativeReports!,
+ "TeleopCoralScoredLevelTwo",
+ );
+ },
+ },
+ {
+ label: "> Max L2 Coral Scored",
+ get(pitData, quantitativeReports) {
+ return GetMaximum(
+ quantitativeReports!,
+ "TeleopCoralScoredLevelTwo",
+ );
+ },
+ },
{
key: "TeleopCoralScoredLevelThree",
label: "Avg Amt Of Coral Scored Level Three Teleop",
},
+ {
+ label: "> Min L3 Coral Scored",
+ get(pitData, quantitativeReports) {
+ return GetMinimum(
+ quantitativeReports!,
+ "TeleopCoralScoredLevelThree",
+ );
+ },
+ },
+ {
+ label: "> Max L3 Coral Scored",
+ get(pitData, quantitativeReports) {
+ return GetMaximum(
+ quantitativeReports!,
+ "TeleopCoralScoredLevelThree",
+ );
+ },
+ },
{
key: "TeleopCoralScoredLevelFour",
label: "Avg Amt Of Coral Scored Level Four Teleop",
},
+ {
+ label: "> Min L4 Coral Scored",
+ get(pitData, quantitativeReports) {
+ return GetMinimum(
+ quantitativeReports!,
+ "TeleopCoralScoredLevelFour",
+ );
+ },
+ },
+ {
+ label: "> Max L4 Coral Scored",
+ get(pitData, quantitativeReports) {
+ return GetMaximum(
+ quantitativeReports!,
+ "TeleopCoralScoredLevelFour",
+ );
+ },
+ },
{
label: "Avg Teleop Coral",
get(pitData, quantitativeReports) {
@@ -1411,14 +1574,62 @@ namespace Reefscape {
key: "TeleopAlgaeRemovedFromReef",
label: "Avg Amt of Algae Removed From Reef",
},
+ {
+ label: "> Min Algae Removed From Reef",
+ get(pitData, quantitativeReports) {
+ return GetMinimum(
+ quantitativeReports!,
+ "TeleopAlgaeRemovedFromReef",
+ );
+ },
+ },
+ {
+ label: "> Max Algae Removed From Reef",
+ get(pitData, quantitativeReports) {
+ return GetMaximum(
+ quantitativeReports!,
+ "TeleopAlgaeRemovedFromReef",
+ );
+ },
+ },
{
key: "TeleopAlgaeScoredProcessor",
label: "Avg Amt of Algae Scored Processor Teleop",
},
+ {
+ label: "> Min Algae Scored In Processor",
+ get(pitData, quantitativeReports) {
+ return GetMinimum(
+ quantitativeReports!,
+ "TeleopAlgaeScoredProcessor",
+ );
+ },
+ },
+ {
+ label: "> Max Algae Scored In Processor",
+ get(pitData, quantitativeReports) {
+ return GetMaximum(
+ quantitativeReports!,
+ "TeleopAlgaeScoredProcessor",
+ );
+ },
+ },
{
key: "TeleopAlgaeScoredNet",
label: "Avg Amt of Algae Scored Net Teleop",
},
+ {
+ label: "> Min Algae Scored In Net",
+ get(pitData, quantitativeReports) {
+ return GetMinimum(quantitativeReports!, "TeleopAlgaeScoredNet");
+ },
+ },
+ {
+ label: "> Max Algae Scored In Net",
+ get(pitData, quantitativeReports) {
+ return GetMaximum(quantitativeReports!, "TeleopAlgaeScoredNet");
+ },
+ },
],
},
getGraphDots: function (
@@ -1638,7 +1849,9 @@ namespace Reefscape {
break;
}
- totalPoints += report.TeleopCoralScoredLevelOne * 2;
+ totalPoints +=
+ (report.TeleopCoralScoredLevelOne + report.TeleopAlgaeScoredProcessor) *
+ 2;
totalPoints +=
(report.AutoCoralScoredLevelOne + report.TeleopCoralScoredLevelTwo) * 3;
totalPoints +=
@@ -1649,9 +1862,7 @@ namespace Reefscape {
4;
totalPoints += report.TeleopCoralScoredLevelFour * 5;
totalPoints +=
- (report.AutoAlgaeScoredProcessor +
- report.TeleopAlgaeScoredProcessor +
- report.AutoCoralScoredLevelThree) *
+ (report.AutoAlgaeScoredProcessor + report.AutoCoralScoredLevelThree) *
6;
totalPoints += report.AutoCoralScoredLevelFour * 7;
}
diff --git a/lib/reportDeploymentToRollbar.ts b/lib/reportDeploymentToRollbar.ts
deleted file mode 100644
index cfe4e577..00000000
--- a/lib/reportDeploymentToRollbar.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import getRollbar from "./client/RollbarUtils";
-
-export default function reportDeploymentToRollbar() {
- const deployId = process.env.DEPLOY_ID;
-
- if (!deployId) {
- getRollbar().error("Missing gitSha or deployId in environment variables");
- return;
- }
-
- const url = "https://api.rollbar.com/api/1/deploy/" + deployId;
- const options = {
- method: "PATCH",
- headers: {
- accept: "application/json",
- "content-type": "application/json",
- "X-Rollbar-Access-Token": process.env.ROLLBAR_TOKEN,
- },
- body: JSON.stringify({
- status: "succeeded",
- }),
- };
-
- fetch(url, options)
- .then(() => console.log("Deployment reported to Rollbar"))
- .catch((err) => getRollbar().error(err));
-}
diff --git a/lib/testutils/JestSetup.ts b/lib/testutils/JestSetup.ts
new file mode 100644
index 00000000..61989856
--- /dev/null
+++ b/lib/testutils/JestSetup.ts
@@ -0,0 +1,4 @@
+import { loadEnvConfig } from "@next/env";
+
+const projectDir = process.cwd();
+loadEnvConfig(projectDir);
diff --git a/lib/testutils/PlaywrightSetup.ts b/lib/testutils/PlaywrightSetup.ts
new file mode 100644
index 00000000..d45ecea6
--- /dev/null
+++ b/lib/testutils/PlaywrightSetup.ts
@@ -0,0 +1,6 @@
+import { loadEnvConfig } from "@next/env";
+
+export default function setup() {
+ const projectDir = process.cwd();
+ loadEnvConfig(projectDir);
+}
diff --git a/lib/testutils/TestUtils.ts b/lib/testutils/TestUtils.ts
index d7f80896..74dce5e5 100644
--- a/lib/testutils/TestUtils.ts
+++ b/lib/testutils/TestUtils.ts
@@ -17,6 +17,8 @@ import { ResendInterface } from "../ResendUtils";
import { SlackInterface } from "../SlackClient";
import { NextResponse } from "unified-api-nextjs";
import { RollbarInterface } from "../client/RollbarUtils";
+import { BrowserContext, Page } from "@playwright/test";
+import ClientApi from "../api/ClientApi";
export class TestRes extends NextResponse {
status = jest.fn((code) => this);
@@ -160,3 +162,71 @@ export async function createTestDocuments(db: DbInterface) {
return { report, subjectiveReport, match, pitReport, comp, season, team };
}
+
+export namespace PlaywrightUtils {
+ export function getTestClientApi() {
+ const api = new ClientApi();
+
+ // Relative requests don't work in Playwright apparently
+ if (
+ process.env.BASE_URL_FOR_PLAYWRIGHT &&
+ !api.requestHelper.baseUrl.startsWith(process.env.BASE_URL_FOR_PLAYWRIGHT)
+ ) {
+ api.requestHelper.baseUrl =
+ process.env.BASE_URL_FOR_PLAYWRIGHT + api.requestHelper.baseUrl;
+ }
+
+ return api;
+ }
+
+ /**
+ * Will reload the page
+ */
+ export async function signUp(page: Page) {
+ const { sessionToken, user } = await getTestClientApi().testSignIn();
+
+ if (!sessionToken || !user) {
+ throw new Error("Failed to sign in");
+ }
+
+ await signIn(page, sessionToken);
+
+ return {
+ sessionToken,
+ user,
+ };
+ }
+
+ /**
+ * Will reload the page
+ */
+ export async function signIn(page: Page, sessionToken: string) {
+ await page.context().addCookies([
+ {
+ name: "next-auth.session-token",
+ value: sessionToken,
+ path: "/",
+ domain: "localhost",
+ sameSite: "Lax",
+ httpOnly: true,
+ secure: true,
+ expires: Math.floor(Date.now() / 1000) + 60 * 60 * 24, // 1 day expiration
+ },
+ ]);
+
+ // It sometimes requires a reload and a fetch to get sign ins to register
+ await getUser(page);
+ await page.reload();
+ }
+
+ export async function getUser(page: Page) {
+ const res = await page.context().request.get("/api/auth/session");
+
+ if (res.ok()) {
+ const { user } = await res.json();
+ return user as User;
+ } else {
+ throw new Error("Failed to get user");
+ }
+ }
+}
diff --git a/lib/testutils/setup.ts b/lib/testutils/setup.ts
deleted file mode 100644
index 967c95da..00000000
--- a/lib/testutils/setup.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import * as dotenv from "dotenv";
-
-dotenv.config({ path: ".env.test" });
diff --git a/package-lock.json b/package-lock.json
index 3536a2fc..71f9e500 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,17 @@
{
"name": "sj3",
- "version": "1.2.24",
+ "version": "1.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sj3",
- "version": "1.2.24",
+ "version": "1.3.2",
"license": "CC BY-NC-SA 4.0",
"dependencies": {
"dependencies": "^0.0.1",
"@next-auth/mongodb-adapter": "^1.1.3",
- "@serwist/next": "^9.0.11",
+ "@serwist/next": "^9.0.13",
"@types/http-proxy": "^1.17.15",
"@types/react-dom": "18.3.1",
"@types/socket.io-client": "^3.0.0",
@@ -21,14 +21,14 @@
"bson": "^5.0.0",
"dotenv": "^16.4.7",
"eslint": "9.18.0",
- "eslint-config-next": "15.2.2",
+ "eslint-config-next": "15.2.4",
"formidable": "^3.5.2",
- "jose": "^6.0.8",
+ "jose": "^6.0.10",
"levenary": "^1.1.1",
"minimongo": "^7.0.0",
- "mongo-anywhere": "^1.1.11",
+ "mongo-anywhere": "^1.1.15",
"mongodb": "^5.0.0",
- "next": "^15.2.3",
+ "next": "^15.2.4",
"next-auth": "^4.24.11",
"next-seo": "^6.6.0",
"omit-call-signature": "^1.0.15",
@@ -42,21 +42,22 @@
"react-ga4": "^2.1.0",
"react-google-recaptcha-v3": "^1.10.1",
"react-hot-toast": "^2.5.1",
- "react-icons": "^5.4.0",
+ "react-icons": "^5.5.0",
"react-p5": "^1.4.1",
"react-qr-code": "^2.0.15",
- "resend": "^4.1.2",
+ "resend": "^4.2.0",
"rollbar": "^2.26.4",
"string-similarity-js": "^2.1.4",
"ts-node": "^10.9.2",
"tsx": "^4.19.3",
"typescript": "5.7.3",
- "unified-api-nextjs": "^1.0.9"
+ "unified-api-nextjs": "^1.1.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
- "@eslint/js": "^9.17.0",
+ "@eslint/js": "^9.24.0",
"@jest/globals": "^29.7.0",
+ "@playwright/test": "^1.51.1",
"@types/formidable": "^3.4.5",
"@types/jest": "^29.5.14",
"@types/node": "^22.13.13",
@@ -1263,9 +1264,11 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.18.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz",
- "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==",
+ "version": "9.24.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz",
+ "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==",
+ "dev": true,
+ "license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
@@ -1930,15 +1933,15 @@
}
},
"node_modules/@next/env": {
- "version": "15.2.3",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.3.tgz",
- "integrity": "sha512-a26KnbW9DFEUsSxAxKBORR/uD9THoYoKbkpFywMN/AFvboTt94b8+g/07T8J6ACsdLag8/PDU60ov4rPxRAixw==",
+ "version": "15.2.4",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz",
+ "integrity": "sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
- "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==",
+ "version": "15.2.4",
+ "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.2.4.tgz",
+ "integrity": "sha512-O8ScvKtnxkp8kL9TpJTTKnMqlkZnS+QxwoQnJwPGBxjBbzd6OVVPEJ5/pMNrktSyXQD/chEfzfFzYLM6JANOOQ==",
"license": "MIT",
"dependencies": {
"fast-glob": "3.3.1"
@@ -1973,9 +1976,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "15.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.3.tgz",
- "integrity": "sha512-uaBhA8aLbXLqwjnsHSkxs353WrRgQgiFjduDpc7YXEU0B54IKx3vU+cxQlYwPCyC8uYEEX7THhtQQsfHnvv8dw==",
+ "version": "15.2.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.4.tgz",
+ "integrity": "sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==",
"cpu": [
"arm64"
],
@@ -1989,9 +1992,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "15.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.3.tgz",
- "integrity": "sha512-pVwKvJ4Zk7h+4hwhqOUuMx7Ib02u3gDX3HXPKIShBi9JlYllI0nU6TWLbPT94dt7FSi6mSBhfc2JrHViwqbOdw==",
+ "version": "15.2.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.4.tgz",
+ "integrity": "sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==",
"cpu": [
"x64"
],
@@ -2005,9 +2008,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "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==",
+ "version": "15.2.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.4.tgz",
+ "integrity": "sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==",
"cpu": [
"arm64"
],
@@ -2021,9 +2024,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "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==",
+ "version": "15.2.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.4.tgz",
+ "integrity": "sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==",
"cpu": [
"arm64"
],
@@ -2037,9 +2040,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "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==",
+ "version": "15.2.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.4.tgz",
+ "integrity": "sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==",
"cpu": [
"x64"
],
@@ -2053,9 +2056,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "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==",
+ "version": "15.2.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.4.tgz",
+ "integrity": "sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==",
"cpu": [
"x64"
],
@@ -2069,9 +2072,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "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==",
+ "version": "15.2.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.4.tgz",
+ "integrity": "sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==",
"cpu": [
"arm64"
],
@@ -2085,9 +2088,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "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==",
+ "version": "15.2.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.4.tgz",
+ "integrity": "sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==",
"cpu": [
"x64"
],
@@ -2132,11 +2135,6 @@
"node": ">= 8"
}
},
- "node_modules/@one-ini/wasm": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
- "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
- },
"node_modules/@panva/hkdf": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
@@ -2154,6 +2152,21 @@
"node": ">=14"
}
},
+ "node_modules/@playwright/test": {
+ "version": "1.51.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.1.tgz",
+ "integrity": "sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==",
+ "devOptional": true,
+ "dependencies": {
+ "playwright": "1.51.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -2194,12 +2207,13 @@
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA=="
},
"node_modules/@react-email/render": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.1.tgz",
- "integrity": "sha512-W3gTrcmLOVYnG80QuUp22ReIT/xfLsVJ+n7ghSlG2BITB8evNABn1AO2rGQoXuK84zKtDAlxCdm3hRyIpZdGSA==",
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.5.tgz",
+ "integrity": "sha512-CA69HYXPk21HhtAXATIr+9JJwpDNmAFCvdMUjWmeoD1+KhJ9NAxusMRxKNeibdZdslmq3edaeOKGbdQ9qjK8LQ==",
+ "license": "MIT",
"dependencies": {
"html-to-text": "9.0.5",
- "js-beautify": "^1.14.11",
+ "prettier": "3.4.2",
"react-promise-suspense": "0.3.4"
},
"engines": {
@@ -2210,6 +2224,21 @@
"react-dom": "^18.0 || ^19.0 || ^19.0.0-rc"
}
},
+ "node_modules/@react-email/render/node_modules/prettier": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
+ "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/@restart/hooks": {
"version": "0.4.16",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
@@ -2278,6 +2307,7 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
"integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
+ "license": "MIT",
"dependencies": {
"domhandler": "^5.0.3",
"selderee": "^0.11.0"
@@ -2287,15 +2317,16 @@
}
},
"node_modules/@serwist/build": {
- "version": "9.0.11",
- "resolved": "https://registry.npmjs.org/@serwist/build/-/build-9.0.11.tgz",
- "integrity": "sha512-mYBBpm6hWN40J/Sj2aNncVgl/o7Ch/CfyHHl3XvEecbowRuJNv8BfIch+Tvwlfd1cqBqNc65kCYvCL2XJms8DA==",
+ "version": "9.0.13",
+ "resolved": "https://registry.npmjs.org/@serwist/build/-/build-9.0.13.tgz",
+ "integrity": "sha512-Hoc6llxFmnsE8z5Cs95UmRRhRyoNh44OdrMWWPPX8BpW19z0CK/qnBquptjyBIe46jjoOxsPHK0Tt7oZOV4Mbw==",
+ "license": "MIT",
"dependencies": {
"common-tags": "1.8.2",
"glob": "10.4.5",
"pretty-bytes": "6.1.1",
"source-map": "0.8.0-beta.0",
- "zod": "3.23.8"
+ "zod": "3.24.2"
},
"engines": {
"node": ">=18.0.0"
@@ -2313,6 +2344,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -2321,6 +2353,7 @@
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
@@ -2340,6 +2373,7 @@
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -2354,6 +2388,7 @@
"version": "0.8.0-beta.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",
"integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==",
+ "license": "BSD-3-Clause",
"dependencies": {
"whatwg-url": "^7.0.0"
},
@@ -2365,6 +2400,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
"integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==",
+ "license": "MIT",
"dependencies": {
"punycode": "^2.1.0"
}
@@ -2372,12 +2408,14 @@
"node_modules/@serwist/build/node_modules/webidl-conversions": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
- "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="
+ "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==",
+ "license": "BSD-2-Clause"
},
"node_modules/@serwist/build/node_modules/whatwg-url": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
"integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==",
+ "license": "MIT",
"dependencies": {
"lodash.sortby": "^4.7.0",
"tr46": "^1.0.1",
@@ -2385,17 +2423,18 @@
}
},
"node_modules/@serwist/next": {
- "version": "9.0.11",
- "resolved": "https://registry.npmjs.org/@serwist/next/-/next-9.0.11.tgz",
- "integrity": "sha512-Aa26qgJxnaxbGK8JYAWqAPeDcyfu0xjYUPpvSfc3d9GrIl9G9cdflkcsvcbmJGnOb3Oc4tHN4wncYO6cIKb2wQ==",
- "dependencies": {
- "@serwist/build": "9.0.11",
- "@serwist/webpack-plugin": "9.0.11",
- "@serwist/window": "9.0.11",
- "chalk": "5.3.0",
+ "version": "9.0.13",
+ "resolved": "https://registry.npmjs.org/@serwist/next/-/next-9.0.13.tgz",
+ "integrity": "sha512-nI2N4oSEHJGH0YUsE4m1obPfudO3DZ/pXiHPaisw+28YjgkMqD6ePfhzeHGO07ahPmIUiyHca9VNO8OfarbQ1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@serwist/build": "9.0.13",
+ "@serwist/webpack-plugin": "9.0.13",
+ "@serwist/window": "9.0.13",
+ "chalk": "5.4.1",
"glob": "10.4.5",
- "serwist": "9.0.11",
- "zod": "3.23.8"
+ "serwist": "9.0.13",
+ "zod": "3.24.2"
},
"engines": {
"node": ">=18.0.0"
@@ -2419,9 +2458,10 @@
}
},
"node_modules/@serwist/next/node_modules/chalk": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
- "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
+ "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
+ "license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
@@ -2463,13 +2503,14 @@
}
},
"node_modules/@serwist/webpack-plugin": {
- "version": "9.0.11",
- "resolved": "https://registry.npmjs.org/@serwist/webpack-plugin/-/webpack-plugin-9.0.11.tgz",
- "integrity": "sha512-ZTxVJqv7nKlPWmhC3oMt7tZc0ssnd7SYr6L8zZLLF7ltJUub9TMyh1K1zL5TJtsr8DyfALNFPwPA/s2yjYSk+w==",
+ "version": "9.0.13",
+ "resolved": "https://registry.npmjs.org/@serwist/webpack-plugin/-/webpack-plugin-9.0.13.tgz",
+ "integrity": "sha512-Z+Eve8dckM2FulRCa7Cj7VCF3EOP7QkRA76tI742olF7J2sYZSm3t9Ex13jDxTMmYiU1AxLq6V6gEMIdRAetVw==",
+ "license": "MIT",
"dependencies": {
- "@serwist/build": "9.0.11",
+ "@serwist/build": "9.0.13",
"pretty-bytes": "6.1.1",
- "zod": "3.23.8"
+ "zod": "3.24.2"
},
"engines": {
"node": ">=18.0.0"
@@ -2488,12 +2529,13 @@
}
},
"node_modules/@serwist/window": {
- "version": "9.0.11",
- "resolved": "https://registry.npmjs.org/@serwist/window/-/window-9.0.11.tgz",
- "integrity": "sha512-hUJVNxZbqfnJbUTfuJew7SR3f1KBIv/4nwmP1YRxjR6EMthUfN4bVSLzWLW8/u3h5bNaVh7sOhopUVC8m1HTCQ==",
+ "version": "9.0.13",
+ "resolved": "https://registry.npmjs.org/@serwist/window/-/window-9.0.13.tgz",
+ "integrity": "sha512-Cf3RizPuFInDcLt0P1Y5QzG1sA5mW131/PZfMYE3yBuNUSGNgOQGlYuLdwDOWPHgECYoVb/da8pspdQNKs0O5g==",
+ "license": "MIT",
"dependencies": {
"@types/trusted-types": "2.0.7",
- "serwist": "9.0.11"
+ "serwist": "9.0.13"
},
"peerDependencies": {
"typescript": ">=5.0.0"
@@ -3012,7 +3054,8 @@
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
- "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT"
},
"node_modules/@types/warning": {
"version": "3.0.3",
@@ -3063,14 +3106,6 @@
"react-dom": "^17 || ^18 || ^19"
}
},
- "node_modules/abbrev": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
- "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
- "engines": {
- "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
- }
- },
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
@@ -4069,6 +4104,7 @@
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
"integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
+ "license": "MIT",
"engines": {
"node": ">=4.0.0"
}
@@ -4078,15 +4114,6 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
- "node_modules/config-chain": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
- "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
- "dependencies": {
- "ini": "^1.3.4",
- "proto-list": "~1.2.1"
- }
- },
"node_modules/console-polyfill": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/console-polyfill/-/console-polyfill-0.3.0.tgz",
@@ -4492,6 +4519,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
@@ -4510,12 +4538,14 @@
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
- ]
+ ],
+ "license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
@@ -4530,6 +4560,7 @@
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
@@ -4564,53 +4595,6 @@
"safer-buffer": "^2.1.0"
}
},
- "node_modules/editorconfig": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
- "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
- "dependencies": {
- "@one-ini/wasm": "0.1.1",
- "commander": "^10.0.0",
- "minimatch": "9.0.1",
- "semver": "^7.5.3"
- },
- "bin": {
- "editorconfig": "bin/editorconfig"
- },
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/editorconfig/node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/editorconfig/node_modules/commander": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
- "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/editorconfig/node_modules/minimatch": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
- "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
@@ -4686,6 +4670,7 @@
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
@@ -4982,12 +4967,12 @@
}
},
"node_modules/eslint-config-next": {
- "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==",
+ "version": "15.2.4",
+ "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.2.4.tgz",
+ "integrity": "sha512-v4gYjd4eYIme8qzaJItpR5MMBXJ0/YV07u7eb50kEnlEmX7yhOjdUdzz70v4fiINYRjLf8X8TbogF0k7wlz6sA==",
"license": "MIT",
"dependencies": {
- "@next/eslint-plugin-next": "15.2.2",
+ "@next/eslint-plugin-next": "15.2.4",
"@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",
@@ -5499,6 +5484,15 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/eslint/node_modules/@eslint/js": {
+ "version": "9.18.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz",
+ "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
"node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
@@ -6243,6 +6237,7 @@
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
"integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
+ "license": "MIT",
"dependencies": {
"@selderee/plugin-htmlparser2": "^0.11.0",
"deepmerge": "^4.3.1",
@@ -6265,6 +6260,7 @@
"url": "https://github.com/sponsors/fb55"
}
],
+ "license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
@@ -6296,9 +6292,10 @@
}
},
"node_modules/idb": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz",
- "integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw=="
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.2.tgz",
+ "integrity": "sha512-CX70rYhx7GDDQzwwQMDwF6kDRQi5vVs6khHUumDrMecBylKkwvZ8HWvKV08AGb7VbpoGCWUQ4aHzNDgoUiOIUg==",
+ "license": "ISC"
},
"node_modules/idb-wrapper": {
"version": "1.7.2",
@@ -6373,11 +6370,6 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
- "node_modules/ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
- },
"node_modules/internal-slot": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
@@ -7828,9 +7820,9 @@
}
},
"node_modules/jose": {
- "version": "6.0.8",
- "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.8.tgz",
- "integrity": "sha512-EyUPtOKyTYq+iMOszO42eobQllaIjJnwkZ2U93aJzNyPibCy7CEvT9UQnaCVB51IAd49gbNdCew1c0LcLTCB2g==",
+ "version": "6.0.10",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.10.tgz",
+ "integrity": "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
@@ -7841,75 +7833,6 @@
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
},
- "node_modules/js-beautify": {
- "version": "1.15.1",
- "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz",
- "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==",
- "dependencies": {
- "config-chain": "^1.1.13",
- "editorconfig": "^1.0.4",
- "glob": "^10.3.3",
- "js-cookie": "^3.0.5",
- "nopt": "^7.2.0"
- },
- "bin": {
- "css-beautify": "js/bin/css-beautify.js",
- "html-beautify": "js/bin/html-beautify.js",
- "js-beautify": "js/bin/js-beautify.js"
- },
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/js-beautify/node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/js-beautify/node_modules/glob": {
- "version": "10.4.5",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
- "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/js-beautify/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/js-cookie": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
- "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
- "engines": {
- "node": ">=14"
- }
- },
"node_modules/js-sha1": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/js-sha1/-/js-sha1-0.6.0.tgz",
@@ -8062,6 +7985,7 @@
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
"integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==",
+ "license": "MIT",
"funding": {
"url": "https://ko-fi.com/killymxi"
}
@@ -8148,7 +8072,8 @@
"node_modules/lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
- "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="
+ "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==",
+ "license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
@@ -8306,9 +8231,9 @@
}
},
"node_modules/mongo-anywhere": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/mongo-anywhere/-/mongo-anywhere-1.1.11.tgz",
- "integrity": "sha512-wM5FMS7sj6vZAEw9XaRaWFYqNeA8slKcQxxWdhyN4a8xGBrOxN2U4wF33kuAkYTgqsdy5fxS3NSF4KkQAUq5Zg==",
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/mongo-anywhere/-/mongo-anywhere-1.1.15.tgz",
+ "integrity": "sha512-wN6E/jN0lae5EqAeaAaE5fdUdb+ZchZKib3FWGOOOQUYZvTv2ino9Aii3GK+uIMxNdnm6N//B0bEGEeCNbBy1g==",
"dependencies": {
"bson": "^5.0.0",
"minimongo": "^6.19.0",
@@ -8431,12 +8356,12 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
},
"node_modules/next": {
- "version": "15.2.3",
- "resolved": "https://registry.npmjs.org/next/-/next-15.2.3.tgz",
- "integrity": "sha512-x6eDkZxk2rPpu46E1ZVUWIBhYCLszmUY6fvHBFcbzJ9dD+qRX6vcHusaqqDlnY+VngKzKbAiG2iRCkPbmi8f7w==",
+ "version": "15.2.4",
+ "resolved": "https://registry.npmjs.org/next/-/next-15.2.4.tgz",
+ "integrity": "sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==",
"license": "MIT",
"dependencies": {
- "@next/env": "15.2.3",
+ "@next/env": "15.2.4",
"@swc/counter": "0.1.3",
"@swc/helpers": "0.5.15",
"busboy": "1.6.0",
@@ -8451,14 +8376,14 @@
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
},
"optionalDependencies": {
- "@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",
+ "@next/swc-darwin-arm64": "15.2.4",
+ "@next/swc-darwin-x64": "15.2.4",
+ "@next/swc-linux-arm64-gnu": "15.2.4",
+ "@next/swc-linux-arm64-musl": "15.2.4",
+ "@next/swc-linux-x64-gnu": "15.2.4",
+ "@next/swc-linux-x64-musl": "15.2.4",
+ "@next/swc-win32-arm64-msvc": "15.2.4",
+ "@next/swc-win32-x64-msvc": "15.2.4",
"sharp": "^0.33.5"
},
"peerDependencies": {
@@ -8604,20 +8529,6 @@
"node": ">=6.0.0"
}
},
- "node_modules/nopt": {
- "version": "7.2.1",
- "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
- "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
- "dependencies": {
- "abbrev": "^2.0.0"
- },
- "bin": {
- "nopt": "bin/nopt.js"
- },
- "engines": {
- "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
- }
- },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -8940,6 +8851,7 @@
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
"integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
+ "license": "MIT",
"dependencies": {
"leac": "^0.6.0",
"peberminta": "^0.9.0"
@@ -9002,6 +8914,7 @@
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
"integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==",
+ "license": "MIT",
"funding": {
"url": "https://ko-fi.com/killymxi"
}
@@ -9109,6 +9022,50 @@
"node": ">=8"
}
},
+ "node_modules/playwright": {
+ "version": "1.51.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz",
+ "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==",
+ "devOptional": true,
+ "dependencies": {
+ "playwright-core": "1.51.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.51.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz",
+ "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==",
+ "devOptional": true,
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/polygon-clipping": {
"version": "0.15.7",
"resolved": "https://registry.npmjs.org/polygon-clipping/-/polygon-clipping-0.15.7.tgz",
@@ -9318,6 +9275,7 @@
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
"integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==",
+ "license": "MIT",
"engines": {
"node": "^14.13.1 || >=16.0.0"
},
@@ -9365,11 +9323,6 @@
"react": ">=0.14.0"
}
},
- "node_modules/proto-list": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
- "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="
- },
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@@ -9603,9 +9556,10 @@
}
},
"node_modules/react-icons": {
- "version": "5.4.0",
- "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
- "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==",
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
+ "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
+ "license": "MIT",
"peerDependencies": {
"react": "*"
}
@@ -9644,6 +9598,7 @@
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz",
"integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==",
+ "license": "MIT",
"dependencies": {
"fast-deep-equal": "^2.0.1"
}
@@ -9651,7 +9606,8 @@
"node_modules/react-promise-suspense/node_modules/fast-deep-equal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
- "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="
+ "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==",
+ "license": "MIT"
},
"node_modules/react-qr-code": {
"version": "2.0.15",
@@ -9850,12 +9806,12 @@
}
},
"node_modules/resend": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/resend/-/resend-4.1.2.tgz",
- "integrity": "sha512-km0btrAj/BqIaRlS+SoLNMaCAUUWEgcEvZpycfVvoXEwAHCxU+vp/ikxPgKRkyKyiR2iDcdUq5uIBTDK9oSSSQ==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/resend/-/resend-4.2.0.tgz",
+ "integrity": "sha512-s6ogU+BBYH1H6Zl926cpddtLRAJWeFv5cIKcuHLWk1QYhFTbpFJlhqx31pnN2f0CB075KFSrc1Xf6HG690wzuw==",
"license": "MIT",
"dependencies": {
- "@react-email/render": "1.0.1"
+ "@react-email/render": "1.0.5"
},
"engines": {
"node": ">=18"
@@ -10065,6 +10021,7 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
"integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
+ "license": "MIT",
"dependencies": {
"parseley": "^0.12.0"
},
@@ -10085,11 +10042,12 @@
}
},
"node_modules/serwist": {
- "version": "9.0.11",
- "resolved": "https://registry.npmjs.org/serwist/-/serwist-9.0.11.tgz",
- "integrity": "sha512-oUuyOuIFZoj+ZYdwWnPYVsndOw5CoGsTy106mHJ2wxjnz01TrDhg2l26yrsoJek1qT/Doi0pzknYLLXTIFs5og==",
+ "version": "9.0.13",
+ "resolved": "https://registry.npmjs.org/serwist/-/serwist-9.0.13.tgz",
+ "integrity": "sha512-BF3bmzYdOVT2lF3iHV0044NqTO6q6GAiqrYpc7L9EPYQXZHOy22WajKaHLvCdvpm2Jpji4SsxUL8/uC1WSCZ5g==",
+ "license": "MIT",
"dependencies": {
- "idb": "8.0.0"
+ "idb": "8.0.2"
},
"peerDependencies": {
"typescript": ">=5.0.0"
@@ -11208,20 +11166,20 @@
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
},
"node_modules/unified-api": {
- "version": "1.0.19",
- "resolved": "https://registry.npmjs.org/unified-api/-/unified-api-1.0.19.tgz",
- "integrity": "sha512-8fQ/fnOTHtEzNTQPPQ+vlFP3jq4vje+7D8adn1yh2TETIkEvgaVvxFbfG4BKyYyp1ZRJxvLH42kYz14WlU8/6A==",
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/unified-api/-/unified-api-1.1.4.tgz",
+ "integrity": "sha512-g9ATByEgoQq2HVrF2X4DYCPW2xqSw+xBh3n6SoqCXwuN6fj6P31lFViaK1yHu9iy2mTHHdQW6IApj+KKNrbShg==",
"dependencies": {
"omit-call-signature": "^1.0.16"
}
},
"node_modules/unified-api-nextjs": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/unified-api-nextjs/-/unified-api-nextjs-1.0.10.tgz",
- "integrity": "sha512-EkW0g3shBuLkbCkX/I3uQHq+ug31rzsHNGEXnOZc2UHypaD5OfQjKV5irwwGtc0iucwYzkDvKKd8aX6dgXXTkQ==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/unified-api-nextjs/-/unified-api-nextjs-1.1.3.tgz",
+ "integrity": "sha512-6y9eMJgJArjJ+K9XpTx2IXic1HQFWYyixPnwZUe07zH1DEMTbq57oOrPHon8kqYT2PUbbyy2P47JjmXkaGxNjA==",
"dependencies": {
"next": "^15.1.2",
- "unified-api": "^1.0.19"
+ "unified-api": "^1.1.4"
}
},
"node_modules/update-browserslist-db": {
@@ -11692,9 +11650,10 @@
}
},
"node_modules/zod": {
- "version": "3.23.8",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
- "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
+ "version": "3.24.2",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
+ "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
+ "license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/package.json b/package.json
index fc646276..9c4bdceb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "sj3",
- "version": "1.2.24",
+ "version": "1.3.2",
"private": true,
"repository": "https://github.com/Decatur-Robotics/Gearbox",
"license": "CC BY-NC-SA 4.0",
@@ -10,6 +10,8 @@
"start": "cross-env NODE_ENV=production npx tsx index.ts",
"restart": "next build && cross-env NODE_ENV=production npx tsx index.ts",
"test": "jest",
+ "e2e": "cross-env NODE_ENV=test playwright test",
+ "e2e-start-server": "cross-env NODE_ENV=test next build && cross-env NODE_ENV=test npx tsx index.ts",
"docker-build": "docker build -t gearbox .",
"docker-start": "docker run -i -t -p 80:80 gearbox",
"docker-prune": "docker container prune",
@@ -19,7 +21,7 @@
},
"dependencies": {
"@next-auth/mongodb-adapter": "^1.1.3",
- "@serwist/next": "^9.0.11",
+ "@serwist/next": "^9.0.13",
"@types/http-proxy": "^1.17.15",
"@types/react-dom": "18.3.1",
"@types/socket.io-client": "^3.0.0",
@@ -30,14 +32,14 @@
"dependencies": "^0.0.1",
"dotenv": "^16.4.7",
"eslint": "9.18.0",
- "eslint-config-next": "15.2.2",
+ "eslint-config-next": "15.2.4",
"formidable": "^3.5.2",
- "jose": "^6.0.8",
+ "jose": "^6.0.10",
"levenary": "^1.1.1",
"minimongo": "^7.0.0",
- "mongo-anywhere": "^1.1.11",
+ "mongo-anywhere": "^1.1.15",
"mongodb": "^5.0.0",
- "next": "^15.2.3",
+ "next": "^15.2.4",
"next-auth": "^4.24.11",
"next-seo": "^6.6.0",
"omit-call-signature": "^1.0.15",
@@ -51,21 +53,22 @@
"react-ga4": "^2.1.0",
"react-google-recaptcha-v3": "^1.10.1",
"react-hot-toast": "^2.5.1",
- "react-icons": "^5.4.0",
+ "react-icons": "^5.5.0",
"react-p5": "^1.4.1",
"react-qr-code": "^2.0.15",
- "resend": "^4.1.2",
+ "resend": "^4.2.0",
"rollbar": "^2.26.4",
"string-similarity-js": "^2.1.4",
"ts-node": "^10.9.2",
"tsx": "^4.19.3",
"typescript": "5.7.3",
- "unified-api-nextjs": "^1.0.9"
+ "unified-api-nextjs": "^1.1.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
- "@eslint/js": "^9.17.0",
+ "@eslint/js": "^9.24.0",
"@jest/globals": "^29.7.0",
+ "@playwright/test": "^1.51.1",
"@types/formidable": "^3.4.5",
"@types/jest": "^29.5.14",
"@types/node": "^22.13.13",
diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx
index 3a01e4ee..ecb101e1 100644
--- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx
+++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx
@@ -405,7 +405,13 @@ export default function CompetitionIndex({
>
-
+
- Delete Season
+
+ Delete
+
Season
+
)}
diff --git a/pages/profile.tsx b/pages/profile.tsx
index b3db52db..c2f74d32 100644
--- a/pages/profile.tsx
+++ b/pages/profile.tsx
@@ -22,6 +22,8 @@ import { signOut } from "next-auth/react";
import XpProgressBar from "@/components/XpProgressBar";
import { HiPencilAlt } from "react-icons/hi";
import toast from "react-hot-toast";
+import EditAvatarModal from "@/components/EditAvatarModal";
+import { close } from "fs";
const api = new ClientApi();
@@ -43,6 +45,8 @@ export default function Profile(props: { teamList: Team[] }) {
const [editingName, setEditingName] = useState(false);
const [newName, setNewName] = useState();
+ const [editingAvatar, setEditingAvatar] = useState(false);
+
useEffect(() => {
const loadTeams = async () => {
setLoadingTeams(true);
@@ -69,6 +73,10 @@ export default function Profile(props: { teamList: Team[] }) {
Analytics.requestedToJoinTeam(teamNumber, user?.name ?? "Unknown User");
};
+ async function toggleEditingAvatarModal() {
+ setEditingAvatar(!editingAvatar);
+ }
+
async function toggleEditingName() {
setEditingName(!editingName);
@@ -91,7 +99,7 @@ export default function Profile(props: { teamList: Team[] }) {
hideMenu={false}
title="Profile"
>
-
+ {/* */}
setNewName(e.target.value)}
defaultValue={newName}
+ placeholder="New Name"
className="input"
/>
) : (
{user?.name}
)}
@@ -120,7 +130,13 @@ export default function Profile(props: { teamList: Team[] }) {
className="space-x-4 max-sm:flex-col max-sm:items-center"
>
-
+
+
+ Edit Avatar
+
signOut()}
className="btn btn-primary mt-2"
@@ -268,6 +284,12 @@ export default function Profile(props: { teamList: Team[] }) {
+ {editingAvatar && (
+
+ )}
);
}
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 00000000..a8c50cd6
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,84 @@
+import { defineConfig, devices } from "@playwright/test";
+import { loadEnvConfig } from "@next/env";
+
+const projectDir = process.cwd();
+loadEnvConfig(projectDir);
+
+const baseURL = process.env.BASE_URL_FOR_PLAYWRIGHT; // Default base URL for tests
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: "./tests/e2e",
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 5 : 0,
+ repeatEach: process.env.CI ? 5 : 1,
+ /* Opt out of parallel tests on CI. */
+ workers: process.env.CI ? 4 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: process.env.CI ? "blob" : "html",
+
+ globalSetup: require.resolve("./lib/testutils/PlaywrightSetup"),
+
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ // baseURL: 'http://127.0.0.1:3000',
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: "on-first-retry",
+ video: "retain-on-failure", // Record video only for failed tests
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: "chromium",
+ use: { ...devices["Desktop Chrome"], baseURL },
+ },
+
+ {
+ name: "firefox",
+ use: { ...devices["Desktop Firefox"], baseURL },
+ },
+
+ {
+ name: "webkit",
+ use: { ...devices["Desktop Safari"], baseURL },
+ },
+
+ /* Test against mobile viewports. */
+ {
+ name: "Mobile Chrome",
+ use: { ...devices["Pixel 5"], baseURL },
+ },
+ {
+ name: "Mobile Safari",
+ use: { ...devices["iPhone 12"], baseURL },
+ },
+
+ /* Test against branded browsers. */
+ {
+ name: "Microsoft Edge",
+ use: { ...devices["Desktop Edge"], channel: "msedge", baseURL },
+ },
+ {
+ name: "Google Chrome",
+ use: { ...devices["Desktop Chrome"], channel: "chrome", baseURL },
+ },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: "npm run e2e-start-server",
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ timeout: 5 * 60 * 1000, // 5 minutes,
+ stdout: "pipe",
+ },
+});
diff --git a/tests/e2e/index.spec.ts b/tests/e2e/index.spec.ts
new file mode 100644
index 00000000..c6c0cecb
--- /dev/null
+++ b/tests/e2e/index.spec.ts
@@ -0,0 +1,53 @@
+import { test, expect } from "@playwright/test";
+
+test("Has title", async ({ page }) => {
+ await page.goto("/");
+
+ // Expect a title "to contain" a substring.
+ await expect(page).toHaveTitle(/Gearbox/i);
+});
+
+test("Has Gearbox header", async ({ page }) => {
+ await page.goto("/");
+
+ // Expect an h1 "to contain" a substring.
+ await expect(
+ page.getByRole("heading", { name: "Gearbox" }).first(),
+ ).toBeVisible();
+});
+
+test("Has get started link", async ({ page }) => {
+ await page.goto("/");
+
+ // Click the get started link.
+ await expect(page.getByRole("link", { name: "Get started" })).toHaveText(
+ "Get Started",
+ );
+});
+
+test("Has build time", async ({ page }) => {
+ await page.goto("/");
+
+ await expect(page.getByText(/Build Time:/i).first()).toBeVisible();
+
+ expect(
+ await page
+ .getByText(/Build Time:/i)
+ .first()
+ .textContent()
+ .then((text) => {
+ const timeString = text!.split(": ")[1];
+ const time = new Date(timeString);
+
+ return time.getTime();
+ }),
+ ).toBeGreaterThan(new Date().getTime() - 60 * 15 * 1000);
+});
+
+test("Has link to Decatur Robotics website", async ({ page }) => {
+ await page.goto("/");
+
+ await expect(
+ page.locator("a[href='https://www.decaturrobotics.org/our-team']"),
+ ).toBeVisible();
+});
diff --git a/tests/e2e/misc.spec.ts b/tests/e2e/misc.spec.ts
new file mode 100644
index 00000000..2dbe3fae
--- /dev/null
+++ b/tests/e2e/misc.spec.ts
@@ -0,0 +1,22 @@
+import { test, expect } from "@playwright/test";
+import { PlaywrightUtils } from "@/lib/testutils/TestUtils";
+
+test("Sign up function signs up", async ({ page, context }) => {
+ const { user } = await PlaywrightUtils.signUp(page);
+
+ const sessionToken = await context
+ .cookies()
+ .then(
+ (cookies) =>
+ cookies.find((cookie) => cookie.name === "next-auth.session-token")
+ ?.value,
+ );
+
+ expect(sessionToken).toBeDefined();
+ expect(sessionToken).not.toBe("");
+
+ const foundUser = await PlaywrightUtils.getUser(page);
+ if (foundUser) foundUser.id = user.id; // ID mismatches are normal
+
+ expect(foundUser).toEqual(user as any);
+});
diff --git a/tests/e2e/profile.spec.ts b/tests/e2e/profile.spec.ts
new file mode 100644
index 00000000..5150bd14
--- /dev/null
+++ b/tests/e2e/profile.spec.ts
@@ -0,0 +1,134 @@
+import Card from "@/components/Card";
+import { PlaywrightUtils } from "@/lib/testutils/TestUtils";
+import { test, expect } from "@playwright/test";
+
+const poSans =
+ '"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSSZk-p-TpJAKV0GsfBa3EJPWWjQPxcVTB5Rg&s"';
+test("Displays sign in page when not signed in", async ({ page }) => {
+ await page.goto("/profile");
+
+ await expect(
+ page.getByRole("heading", { name: /sign in/i }).first(),
+ ).toBeVisible();
+});
+
+test("Displays user information when signed in", async ({ page }) => {
+ const { user } = await PlaywrightUtils.signUp(page);
+
+ await page.goto("/profile");
+
+ await expect(
+ page.getByRole("heading", { name: /sign in/i }).first(),
+ ).not.toBeVisible();
+
+ await expect(page.getByText(user.email!)).toBeVisible();
+ await expect(page.getByText(user.slug!)).toBeVisible();
+ await expect(page.getByText(new RegExp(user.name!))).toBeVisible();
+});
+
+test.describe("Edit user name", () => {
+ test("Allows user to edit their name", async ({ page }) => {
+ await PlaywrightUtils.signUp(page);
+
+ await page.goto("/profile");
+
+ const editButton = page.getByTestId("edit-name-button");
+ await editButton.click();
+
+ const nameInput = page.getByPlaceholder(/new name/i);
+ await expect(nameInput).toBeVisible();
+
+ await nameInput.fill("New Name");
+ await editButton.click();
+
+ await expect(page.getByText("New Name")).toBeVisible();
+ });
+});
+
+test.describe("Edit Avatar", () => {
+ test("Edit Avatar button displays popup", async ({ page }) => {
+ await PlaywrightUtils.signUp(page);
+
+ await page.goto("/profile");
+
+ const editAvatarButton = page.getByRole("button", { name: "Edit Avatar" });
+ await editAvatarButton.click();
+
+ const editAvatarPopup = page.getByTitle("Edit Avatar");
+ await expect(editAvatarPopup).toBeVisible();
+ });
+
+ test("Cancel button closes popup", async ({ page }) => {
+ await PlaywrightUtils.signUp(page);
+
+ await page.goto("/profile");
+
+ //Edit Avatar button
+ await page.getByRole("button", { name: "Edit Avatar" }).click();
+
+ //Cancel button
+ await page.getByRole("button", { name: "Cancel" }).click();
+
+ //Edit Avatar popup
+ await expect(page.getByTitle("Edit Avatar")).not.toBeVisible();
+ });
+
+ test("Preview Image recognizes image in field", async ({ page }) => {
+ await PlaywrightUtils.signUp(page);
+
+ await page.goto("/profile");
+
+ //Edit Avatar button
+ await page.getByRole("button", { name: "Edit Avatar" }).click();
+
+ //Enter avatar url input
+ await page.getByPlaceholder("Enter new avatar url").fill(poSans);
+
+ //Avatar preview
+ await expect(page.getByAltText("New Avatar")).toHaveAttribute(
+ "src",
+ poSans,
+ );
+ });
+
+ test("Save button saves new avatar", async ({ page }) => {
+ await PlaywrightUtils.signUp(page);
+
+ await page.goto("/profile");
+
+ //Edit Avatar button
+ await page.getByRole("button", { name: "Edit Avatar" }).click();
+
+ //Enter avatar url input
+ await page.getByPlaceholder("Enter new avatar url").fill(poSans);
+
+ //Save button
+ await page.getByRole("button", { name: "Save" }).click();
+
+ //Url of user's avatar
+ await expect(page.getByAltText("Avatar").first()).toHaveAttribute(
+ "src",
+ poSans,
+ );
+ });
+
+ test("Cancel button does not save new avatar", async ({ page }) => {
+ const currentAvatar = (await PlaywrightUtils.signUp(page)).user.image;
+
+ await page.goto("/profile");
+
+ //Edit Avatar button
+ await page.getByRole("button", { name: "Edit Avatar" }).click();
+
+ //Enter avatar url input
+ await page.getByPlaceholder("Enter new avatar url").fill(poSans);
+
+ //Cancel button
+ await page.getByRole("button", { name: "Cancel" }).click();
+
+ await expect(page.getByAltText("Avatar").first()).toHaveAttribute(
+ "src",
+ currentAvatar,
+ );
+ });
+});
diff --git a/tests/lib/CompetitionHandling.test.ts b/tests/unit/lib/CompetitionHandling.test.ts
similarity index 99%
rename from tests/lib/CompetitionHandling.test.ts
rename to tests/unit/lib/CompetitionHandling.test.ts
index 8e38fc33..6b1e7162 100644
--- a/tests/lib/CompetitionHandling.test.ts
+++ b/tests/unit/lib/CompetitionHandling.test.ts
@@ -13,8 +13,8 @@ import {
Match,
Report,
Team,
+ MatchType,
} from "@/lib/Types";
-import { MatchType } from "../../lib/Types";
import { ObjectId } from "bson";
import { GameId } from "@/lib/client/GameId";
diff --git a/tests/lib/DbInterfaceAuthAdapter.test.ts b/tests/unit/lib/DbInterfaceAuthAdapter.test.ts
similarity index 100%
rename from tests/lib/DbInterfaceAuthAdapter.test.ts
rename to tests/unit/lib/DbInterfaceAuthAdapter.test.ts
diff --git a/tests/lib/Layout.test.ts b/tests/unit/lib/Layout.test.ts
similarity index 100%
rename from tests/lib/Layout.test.ts
rename to tests/unit/lib/Layout.test.ts
diff --git a/tests/lib/TheOrangeAlliance.test.ts b/tests/unit/lib/TheOrangeAlliance.test.ts
similarity index 100%
rename from tests/lib/TheOrangeAlliance.test.ts
rename to tests/unit/lib/TheOrangeAlliance.test.ts
diff --git a/tests/lib/Types.test.ts b/tests/unit/lib/Types.test.ts
similarity index 100%
rename from tests/lib/Types.test.ts
rename to tests/unit/lib/Types.test.ts
diff --git a/tests/lib/Utils.test.ts b/tests/unit/lib/Utils.test.ts
similarity index 100%
rename from tests/lib/Utils.test.ts
rename to tests/unit/lib/Utils.test.ts
diff --git a/tests/lib/Xp.test.ts b/tests/unit/lib/Xp.test.ts
similarity index 100%
rename from tests/lib/Xp.test.ts
rename to tests/unit/lib/Xp.test.ts
diff --git a/tests/lib/api/AccessLevels.test.ts b/tests/unit/lib/api/AccessLevels.test.ts
similarity index 100%
rename from tests/lib/api/AccessLevels.test.ts
rename to tests/unit/lib/api/AccessLevels.test.ts
diff --git a/tests/lib/api/ApiUtils.test.ts b/tests/unit/lib/api/ApiUtils.test.ts
similarity index 100%
rename from tests/lib/api/ApiUtils.test.ts
rename to tests/unit/lib/api/ApiUtils.test.ts
diff --git a/tests/lib/api/ClientApi.test.ts b/tests/unit/lib/api/ClientApi.test.ts
similarity index 95%
rename from tests/lib/api/ClientApi.test.ts
rename to tests/unit/lib/api/ClientApi.test.ts
index 24b6c6ad..a10478de 100644
--- a/tests/lib/api/ClientApi.test.ts
+++ b/tests/unit/lib/api/ClientApi.test.ts
@@ -1107,3 +1107,58 @@ describe(`${ClientApi.name}.${api.changeTeamNumberForReport.name}`, () => {
);
});
});
+
+describe(`${ClientApi.name}.${api.testSignIn.name}`, () => {
+ test("Returns user", async () => {
+ const { db, res } = await getTestApiUtils();
+
+ await api.testSignIn.handler(...(await getTestApiParams(res, { db }, [])));
+
+ const { user } = res.send.mock.calls[0][0] as {
+ user: User;
+ };
+
+ expect(user).toBeDefined();
+ expect(user._id).toBeDefined();
+ expect(
+ await db.findObjectById(CollectionId.Users, new ObjectId(user._id!)),
+ ).toEqual(user);
+ });
+
+ test("Returns valid sessionToken", async () => {
+ const { db, res } = await getTestApiUtils();
+
+ await api.testSignIn.handler(...(await getTestApiParams(res, { db }, [])));
+
+ const { sessionToken } = res.send.mock.calls[0][0] as {
+ sessionToken: string;
+ };
+
+ expect(sessionToken).toBeDefined();
+ expect(sessionToken).not.toBe("");
+
+ const session = await db.findObject(CollectionId.Sessions, {
+ sessionToken,
+ });
+ expect(session).toBeDefined();
+ });
+
+ test("Session has correct userId", async () => {
+ const { db, res } = await getTestApiUtils();
+
+ await api.testSignIn.handler(...(await getTestApiParams(res, { db }, [])));
+
+ const { user, sessionToken } = res.send.mock.calls[0][0] as {
+ user: User;
+ sessionToken: string;
+ };
+
+ expect(sessionToken).toBeDefined();
+
+ const session = await db.findObject(CollectionId.Sessions, {
+ sessionToken,
+ });
+ expect(session).toBeDefined();
+ expect(session?.userId).toEqual(user._id);
+ });
+});
diff --git a/tests/lib/client/ClientUtils.test.ts b/tests/unit/lib/client/ClientUtils.test.ts
similarity index 100%
rename from tests/lib/client/ClientUtils.test.ts
rename to tests/unit/lib/client/ClientUtils.test.ts
diff --git a/tests/lib/client/InputVerification.test.ts b/tests/unit/lib/client/InputVerification.test.ts
similarity index 100%
rename from tests/lib/client/InputVerification.test.ts
rename to tests/unit/lib/client/InputVerification.test.ts
diff --git a/tests/lib/client/Picklist.test.ts b/tests/unit/lib/client/Picklist.test.ts
similarity index 100%
rename from tests/lib/client/Picklist.test.ts
rename to tests/unit/lib/client/Picklist.test.ts
diff --git a/tests/lib/client/StatsMath.test.ts b/tests/unit/lib/client/StatsMath.test.ts
similarity index 100%
rename from tests/lib/client/StatsMath.test.ts
rename to tests/unit/lib/client/StatsMath.test.ts
diff --git a/tests/lib/dev/FakeData.test.ts b/tests/unit/lib/dev/FakeData.test.ts
similarity index 100%
rename from tests/lib/dev/FakeData.test.ts
rename to tests/unit/lib/dev/FakeData.test.ts
diff --git a/tests/lib/slugToId.test.ts b/tests/unit/lib/slugToId.test.ts
similarity index 100%
rename from tests/lib/slugToId.test.ts
rename to tests/unit/lib/slugToId.test.ts