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..29d9257d 100644 --- a/.env.test +++ b/.env.test @@ -1,4 +1,7 @@ -NEXT_PUBLIC_API_URL=http://localhost:3000/api +NEXTAUTH_URL=http://localhost:3000/api/ +NEXTAUTH_SECRET=testsecret + +NEXT_PUBLIC_API_URL=/api/ DEVELOPER_EMAILS=["test@gmail.com"] @@ -6,4 +9,14 @@ 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 +API_URL=/api/ +API_KEY=gearboxiscool + +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&appName=mongosh+2.5.10 +ENV_FILE=.env.test + +DB=playwright_tests diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4f83cda2..01b9e600 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,7 +6,7 @@ version: 2 updates: - package-ecosystem: "npm" # See documentation for possible values directory: "/" # Location of package manifests - assignees: ["renatodellosso", "BanEvading"] + assignees: ["Tr01ler"] schedule: day: "monday" interval: "weekly" 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/.github/workflows/increment_version.yml b/.github/workflows/increment_version.yml index a021286e..46ca6d3d 100644 --- a/.github/workflows/increment_version.yml +++ b/.github/workflows/increment_version.yml @@ -10,7 +10,7 @@ on: jobs: increment: runs-on: ubuntu-latest - if: + if: steps: - name: Checkout uses: actions/checkout@v4 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 1249c1a7..f12f8f3c 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,10 +1,7 @@ { "recommendations": [ - "ms-azuretools.vscode-docker", - "formulahendry.docker-explorer", "esbenp.prettier-vscode", "github.vscode-github-actions", - "mongodb.mongodb-vscode", "pmneo.tsimporter", "austenc.tailwind-docs", "bradlc.vscode-tailwindcss" diff --git a/LICENSE.md b/LICENSE.md index cbe5ad16..11976172 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -67,90 +67,88 @@ Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. - Section 1 -- Definitions. - a. Adapted Material means material subject to Copyright and Similar - Rights that is derived from or based upon the Licensed Material - and in which the Licensed Material is translated, altered, - arranged, transformed, or otherwise modified in a manner requiring - permission under the Copyright and Similar Rights held by the - Licensor. For purposes of this Public License, where the Licensed - Material is a musical work, performance, or sound recording, - Adapted Material is always produced where the Licensed Material is - synched in timed relation with a moving image. - - b. Adapter's License means the license You apply to Your Copyright - and Similar Rights in Your contributions to Adapted Material in - accordance with the terms and conditions of this Public License. - - c. BY-NC-SA Compatible License means a license listed at - creativecommons.org/compatiblelicenses, approved by Creative - Commons as essentially the equivalent of this Public License. - - d. Copyright and Similar Rights means copyright and/or similar rights - closely related to copyright including, without limitation, - performance, broadcast, sound recording, and Sui Generis Database - Rights, without regard to how the rights are labeled or - categorized. For purposes of this Public License, the rights - specified in Section 2(b)(1)-(2) are not Copyright and Similar - Rights. - - e. Effective Technological Measures means those measures that, in the - absence of proper authority, may not be circumvented under laws - fulfilling obligations under Article 11 of the WIPO Copyright - Treaty adopted on December 20, 1996, and/or similar international - agreements. - - f. Exceptions and Limitations means fair use, fair dealing, and/or - any other exception or limitation to Copyright and Similar Rights - that applies to Your use of the Licensed Material. - - g. License Elements means the license attributes listed in the name - of a Creative Commons Public License. The License Elements of this - Public License are Attribution, NonCommercial, and ShareAlike. - - h. Licensed Material means the artistic or literary work, database, - or other material to which the Licensor applied this Public - License. - - i. Licensed Rights means the rights granted to You subject to the - terms and conditions of this Public License, which are limited to - all Copyright and Similar Rights that apply to Your use of the - Licensed Material and that the Licensor has authority to license. - - j. Licensor means the individual(s) or entity(ies) granting rights - under this Public License. - - k. NonCommercial means not primarily intended for or directed towards - commercial advantage or monetary compensation. For purposes of - this Public License, the exchange of the Licensed Material for - other material subject to Copyright and Similar Rights by digital - file-sharing or similar means is NonCommercial provided there is - no payment of monetary compensation in connection with the - exchange. - - l. Share means to provide material to the public by any means or - process that requires permission under the Licensed Rights, such - as reproduction, public display, public performance, distribution, - dissemination, communication, or importation, and to make material - available to the public including in ways that members of the - public may access the material from a place and at a time - individually chosen by them. - - m. Sui Generis Database Rights means rights other than copyright - resulting from Directive 96/9/EC of the European Parliament and of - the Council of 11 March 1996 on the legal protection of databases, - as amended and/or succeeded, as well as other essentially - equivalent rights anywhere in the world. - - n. You means the individual or entity exercising the Licensed Rights - under this Public License. Your has a corresponding meaning. - +a. Adapted Material means material subject to Copyright and Similar +Rights that is derived from or based upon the Licensed Material +and in which the Licensed Material is translated, altered, +arranged, transformed, or otherwise modified in a manner requiring +permission under the Copyright and Similar Rights held by the +Licensor. For purposes of this Public License, where the Licensed +Material is a musical work, performance, or sound recording, +Adapted Material is always produced where the Licensed Material is +synched in timed relation with a moving image. + +b. Adapter's License means the license You apply to Your Copyright +and Similar Rights in Your contributions to Adapted Material in +accordance with the terms and conditions of this Public License. + +c. BY-NC-SA Compatible License means a license listed at +creativecommons.org/compatiblelicenses, approved by Creative +Commons as essentially the equivalent of this Public License. + +d. Copyright and Similar Rights means copyright and/or similar rights +closely related to copyright including, without limitation, +performance, broadcast, sound recording, and Sui Generis Database +Rights, without regard to how the rights are labeled or +categorized. For purposes of this Public License, the rights +specified in Section 2(b)(1)-(2) are not Copyright and Similar +Rights. + +e. Effective Technological Measures means those measures that, in the +absence of proper authority, may not be circumvented under laws +fulfilling obligations under Article 11 of the WIPO Copyright +Treaty adopted on December 20, 1996, and/or similar international +agreements. + +f. Exceptions and Limitations means fair use, fair dealing, and/or +any other exception or limitation to Copyright and Similar Rights +that applies to Your use of the Licensed Material. + +g. License Elements means the license attributes listed in the name +of a Creative Commons Public License. The License Elements of this +Public License are Attribution, NonCommercial, and ShareAlike. + +h. Licensed Material means the artistic or literary work, database, +or other material to which the Licensor applied this Public +License. + +i. Licensed Rights means the rights granted to You subject to the +terms and conditions of this Public License, which are limited to +all Copyright and Similar Rights that apply to Your use of the +Licensed Material and that the Licensor has authority to license. + +j. Licensor means the individual(s) or entity(ies) granting rights +under this Public License. + +k. NonCommercial means not primarily intended for or directed towards +commercial advantage or monetary compensation. For purposes of +this Public License, the exchange of the Licensed Material for +other material subject to Copyright and Similar Rights by digital +file-sharing or similar means is NonCommercial provided there is +no payment of monetary compensation in connection with the +exchange. + +l. Share means to provide material to the public by any means or +process that requires permission under the Licensed Rights, such +as reproduction, public display, public performance, distribution, +dissemination, communication, or importation, and to make material +available to the public including in ways that members of the +public may access the material from a place and at a time +individually chosen by them. + +m. Sui Generis Database Rights means rights other than copyright +resulting from Directive 96/9/EC of the European Parliament and of +the Council of 11 March 1996 on the legal protection of databases, +as amended and/or succeeded, as well as other essentially +equivalent rights anywhere in the world. + +n. You means the individual or entity exercising the Licensed Rights +under this Public License. Your has a corresponding meaning. Section 2 -- Scope. - a. License grant. +a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, @@ -211,7 +209,7 @@ Section 2 -- Scope. the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). - b. Other rights. +b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, @@ -233,13 +231,12 @@ Section 2 -- Scope. the Licensed Material is used other than for NonCommercial purposes. - Section 3 -- License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. - a. Attribution. +a. Attribution. 1. If You Share the Licensed Material (including in modified form), You must: @@ -280,7 +277,7 @@ following conditions. information required by Section 3(a)(1)(A) to the extent reasonably practicable. - b. ShareAlike. +b. ShareAlike. In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. @@ -299,69 +296,66 @@ following conditions. Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. - Section 4 -- Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: - a. for the avoidance of doubt, Section 2(a)(1) grants You the right - to extract, reuse, reproduce, and Share all or a substantial - portion of the contents of the database for NonCommercial purposes - only; +a. for the avoidance of doubt, Section 2(a)(1) grants You the right +to extract, reuse, reproduce, and Share all or a substantial +portion of the contents of the database for NonCommercial purposes +only; - b. if You include all or a substantial portion of the database - contents in a database in which You have Sui Generis Database - Rights, then the database in which You have Sui Generis Database - Rights (but not its individual contents) is Adapted Material, - including for purposes of Section 3(b); and +b. if You include all or a substantial portion of the database +contents in a database in which You have Sui Generis Database +Rights, then the database in which You have Sui Generis Database +Rights (but not its individual contents) is Adapted Material, +including for purposes of Section 3(b); and - c. You must comply with the conditions in Section 3(a) if You Share - all or a substantial portion of the contents of the database. +c. You must comply with the conditions in Section 3(a) if You Share +all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. - Section 5 -- Disclaimer of Warranties and Limitation of Liability. - a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE - EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS - AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF - ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, - IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, - WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR - PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, - ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT - KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT - ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. - - b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE - TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, - NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, - INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, - COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR - USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN - ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR - DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR - IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. - - c. The disclaimer of warranties and limitation of liability provided - above shall be interpreted in a manner that, to the extent - possible, most closely approximates an absolute disclaimer and - waiver of all liability. - +a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE +EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS +AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF +ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, +IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, +WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, +ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT +KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT +ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + +b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE +TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, +NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, +INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, +COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR +USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN +ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR +DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR +IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + +c. The disclaimer of warranties and limitation of liability provided +above shall be interpreted in a manner that, to the extent +possible, most closely approximates an absolute disclaimer and +waiver of all liability. Section 6 -- Term and Termination. - a. This Public License applies for the term of the Copyright and - Similar Rights licensed here. However, if You fail to comply with - this Public License, then Your rights under this Public License - terminate automatically. +a. This Public License applies for the term of the Copyright and +Similar Rights licensed here. However, if You fail to comply with +this Public License, then Your rights under this Public License +terminate automatically. - b. Where Your right to use the Licensed Material has terminated under - Section 6(a), it reinstates: +b. Where Your right to use the Licensed Material has terminated under +Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the @@ -373,47 +367,45 @@ Section 6 -- Term and Termination. right the Licensor may have to seek remedies for Your violations of this Public License. - c. For the avoidance of doubt, the Licensor may also offer the - Licensed Material under separate terms or conditions or stop - distributing the Licensed Material at any time; however, doing so - will not terminate this Public License. - - d. Sections 1, 5, 6, 7, and 8 survive termination of this Public - License. +c. For the avoidance of doubt, the Licensor may also offer the +Licensed Material under separate terms or conditions or stop +distributing the Licensed Material at any time; however, doing so +will not terminate this Public License. +d. Sections 1, 5, 6, 7, and 8 survive termination of this Public +License. Section 7 -- Other Terms and Conditions. - a. The Licensor shall not be bound by any additional or different - terms or conditions communicated by You unless expressly agreed. - - b. Any arrangements, understandings, or agreements regarding the - Licensed Material not stated herein are separate from and - independent of the terms and conditions of this Public License. +a. The Licensor shall not be bound by any additional or different +terms or conditions communicated by You unless expressly agreed. +b. Any arrangements, understandings, or agreements regarding the +Licensed Material not stated herein are separate from and +independent of the terms and conditions of this Public License. Section 8 -- Interpretation. - a. For the avoidance of doubt, this Public License does not, and - shall not be interpreted to, reduce, limit, restrict, or impose - conditions on any use of the Licensed Material that could lawfully - be made without permission under this Public License. - - b. To the extent possible, if any provision of this Public License is - deemed unenforceable, it shall be automatically reformed to the - minimum extent necessary to make it enforceable. If the provision - cannot be reformed, it shall be severed from this Public License - without affecting the enforceability of the remaining terms and - conditions. - - c. No term or condition of this Public License will be waived and no - failure to comply consented to unless expressly agreed to by the - Licensor. - - d. Nothing in this Public License constitutes or may be interpreted - as a limitation upon, or waiver of, any privileges and immunities - that apply to the Licensor or You, including from the legal - processes of any jurisdiction or authority. +a. For the avoidance of doubt, this Public License does not, and +shall not be interpreted to, reduce, limit, restrict, or impose +conditions on any use of the Licensed Material that could lawfully +be made without permission under this Public License. + +b. To the extent possible, if any provision of this Public License is +deemed unenforceable, it shall be automatically reformed to the +minimum extent necessary to make it enforceable. If the provision +cannot be reformed, it shall be severed from this Public License +without affecting the enforceability of the remaining terms and +conditions. + +c. No term or condition of this Public License will be waived and no +failure to comply consented to unless expressly agreed to by the +Licensor. + +d. Nothing in this Public License constitutes or may be interpreted +as a limitation upon, or waiver of, any privileges and immunities +that apply to the Licensor or You, including from the legal +processes of any jurisdiction or authority. ======================================================================= diff --git a/README.md b/README.md index e05a572b..fe8c3973 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Gearbox: Intuitive, Powerful Scouting at [4026.org](https://4026.org) The final incarnation of Scout Janssen. - Rewritten fully in Typescript, written to be easy to maintain and modular. Features full feature parity with SJ2, whilst remaining simpler, faster and cooler. +Used by 190+ teams worldwide to collect 380,000+ datapoints across 230+ competitions. ## Features @@ -32,10 +32,10 @@ Features full feature parity with SJ2, whilst remaining simpler, faster and cool - Node.js - NPM - A MongoDB instance - - We use Atlas + - We use Mongo Atlas - An SSL certificate saved as `certs/key.pem` and `certs/cert.pem` - Can be generated with OpenSSL -- Secrets: +- Secrets in a `.env` file (see [`environment.d.ts`](environment.d.ts) for a full list): - A Blue Alliance API key - An Orange Alliance API key - A Google OAuth client ID and secret @@ -67,18 +67,84 @@ See the [Gearbox-Terraform](https://github.com/Decatur-Robotics/Gearbox-Terrafor #### Tests -1. Run `npm run test` +Gearbox has both unit tests (via Jest) and E2E tests (via Playwright). + +Unit tests are run with `npm run test`. +E2E tests are run with `npm run e2e`. #### Scripts -There's a few scripts in the /scripts folder that can be run with `npx tsx scripts/.ts`. +There's a few scripts in the `/scripts` folder that can be run with `npx tsx scripts/.ts`. ## Contributing -You've made it past set up and are ready to contibure to the future of scouting - here's how. +You've made it past set up and are ready to contribute to the future of scouting - here's how. We recommend you start with issues labelled `good first issue` to get a feel for the codebase. Fork the repo (unless you're part of Decatur Robotics, in which case make a new branch) and then make a pull request to the main branch. We'll review it and, if it looks good, merge it. +## Other Repositories + +Our Terraform code is in a separate repository, [Gearbox-Terraform](https://github.com/Decatur-Robotics/Gearbox-Terraform). + +We've also developed several packages that we use (available through NPM): + +- [mongo-anywhere](https://github.com/Decatur-Robotics/mongo-anywhere) - Provides dependency injection and mocks for MongoDB. Gearbox has wrappers around this package's `DbInterface` types. +- [unified-api](https://github.com/Decatur-Robotics/unified-api) - Provides handling and structure for API routes. +- [unified-api-nextjs](https://github.com/Decatur-Robotics/unified-api-nextjs) - Provides types and templates for Next.js API routes. +- [omit-call-signature](https://github.com/Decatur-Robotics/omit-call-signature) - Provides a type for removing call signatures from another type and a type for removing constructor signatures from a class type. Gearbox doesn't directly use this package, but the `unified-api` package does. + +## Tools Used + +### Codebase + +- Typescript +- Next.js +- NextAuth +- MongoDB + +### Testing + +- Jest +- Playwright + +### Dev Tools + +- GitHub Actions +- Prettier +- ESLint + +### Hosting & Infrastructure + +See the [Gearbox-Terraform](https://github.com/Decatur-Robotics/Gearbox-Terraform) repository for more details. + +- Terraform (stored in the [Gearbox-Terraform](https://github.com/Decatur-Robotics/Gearbox-Terraform) repository) +- HashiCorp Managed Terraform (to apply the Terraform code) +- AWS ECS +- AWS S3 (to store secrets) +- Cloudflare (for DNS) +- MongoDB Atlas +- Docker +- GitHub Actions +- GitHub Container Registry + +### External APIs + +- The Blue Alliance API (for match data) +- The Orange Alliance API (for match data) +- Rollbar (for error tracking and deployment notifications) +- Resend (for email sending) +- Google Analytics +- Google OAuth (for authentication) +- Slack OAuth (for authentication) + +## History + +Gearbox is internally known as Scout Janssen 3 (SJ3). It is the third iteration of the Scout Janssen project, which started in 2019. A brief timeline of the project is as follows: + +- 2019-2020: [Scout Janssen 1](https://github.com/Decatur-Robotics/Scout-Janssen), developed by team members Hayden, Keon, and Carter. Used a Django backend with a plain HTML/CSS/JS frontend. +- 2020-2023: [Scout Janssen 2](https://github.com/Decatur-Robotics/Scout-Janssen-2), developed by Theo, Hayden, and Carter with help from Avyn and Renato. Unlike other iterations, SJ2 used GraphQL, Redis, and Socket.io. +- 2023-present: Gearbox (SJ3), developed by Theo, Renato, Davis, and Colin. + ## Contibutors 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: { > {"Avatar"}
{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/Container.tsx b/components/Container.tsx index 0cc63f42..e9bb660a 100644 --- a/components/Container.tsx +++ b/components/Container.tsx @@ -317,7 +317,7 @@ export default function Container(props: ContainerProps) { "w-16 h-16 btn btn-ghost " + (selected ? "border-2 border-accent" : "border-2") } - key={team._id.toString()} + key={team._id?.toString()} onClick={() => { setSelectedTeamIndex(index); }} 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 ( + + +
+ +
+ + setNewAvatar(e.target.value)} + placeholder="Enter new avatar url" + /> +
+ + +
+
+
+ ); +} 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. +
+
+
+ {myMatches + .filter((matchData) => showSubmittedReports || !matchData.completed) + .map((matchData, index) => ( + + ))} +
+ + + ); +} diff --git a/components/competition/CompHeaderCard.tsx b/components/competition/CompHeaderCard.tsx index 2222446d..012fd8ca 100644 --- a/components/competition/CompHeaderCard.tsx +++ b/components/competition/CompHeaderCard.tsx @@ -1,13 +1,31 @@ import { NotLinkedToTba } from "@/lib/client/ClientUtils"; -import { Competition } from "@/lib/Types"; +import { Competition, Match, Report } from "@/lib/Types"; +import { useState } from "react"; import { BiExport } from "react-icons/bi"; +import { FaCalendarDay } from "react-icons/fa"; import { MdAutoGraph, MdQueryStats, MdCoPresent } from "react-icons/md"; +import ViewMatchesModal from "../ViewMatchesModal"; +import { User } from "../../lib/Types"; export default function CompHeaderCard({ comp, + matches, + reports, + user, + matchPathway, }: { comp: Competition | undefined; + matches: Match[]; + reports: Report[]; + user: User | null; + matchPathway: string; }) { + const [viewMatches, setViewMatches] = useState(false); + + async function toggleViewMatches() { + setViewMatches(!viewMatches); + } + return (
@@ -36,8 +54,24 @@ export default function CompHeaderCard({ > Pit Stats +
+
+ {viewMatches && user && ( + + )}
); } 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" )} +
; reports: Report[]; pitReports: Pitreport[]; subjectiveReports: SubjectiveReport[]; @@ -138,13 +139,12 @@ export default function TeamPage(props: { [key: number]: SubjectiveReport[]; }>({}); - const teamNumbers = Array.from( - new Set([ - ...Object.keys(teamReports), - ...Object.keys(pitReports), - ...Object.keys(teamSubjectiveReports), - ]), - ); + const teamNumbers = props.teams; + [ + ...Object.keys(teamReports), + ...Object.keys(pitReports), + ...Object.keys(teamSubjectiveReports), + ].forEach((team) => teamNumbers.add(Number(team))); const [selectedTeam, setSelectedTeam] = useState(); const selectedReports = teamReports[selectedTeam ? selectedTeam : 0]; @@ -212,9 +212,9 @@ export default function TeamPage(props: { }); // Find teams not in team ranking - const missingTeams = teamNumbers.filter( - (team) => !teamRanking.includes(team), - ); + const missingTeams = Array.from(teamNumbers) + .filter((team) => !teamRanking.includes(team.toString())) + .map((team) => team.toString()); return (
diff --git a/environment.d.ts b/environment.d.ts index 77881c1f..ee2020d9 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -56,6 +56,10 @@ declare global { DEPLOY_ID: string; + BASE_URL_FOR_PLAYWRIGHT: string | undefined; + ENABLE_TEST_SIGNIN_ROUTE: string | undefined; + FALLBACK_MONGODB_URI: string | undefined; + NODE_ENV: "development" | "production"; } } diff --git a/eslint.config.mjs b/eslint.config.mjs index e0462f2b..e8345844 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -20,7 +20,7 @@ const config = [ }, }, { - // Ignores has to go in its own config object + // Ignores has to go in its own config object ignores: ["coverage/**/*", ".next/**/*"], }, ]; diff --git a/index.ts b/index.ts index c88f6195..7b0a3eeb 100644 --- a/index.ts +++ b/index.ts @@ -8,22 +8,26 @@ import { createServer as createServerHttp, } from "http"; import Logger from "./lib/client/Logger"; -import { configDotenv } from "dotenv"; -import getRollbar from "./lib/client/RollbarUtils"; -import reportDeploymentToRollbar from "./lib/reportDeploymentToRollbar"; +import { loadEnvConfig } from "@next/env"; +import getRollbar, { + reportDeploymentToRollbar, +} from "./lib/client/RollbarUtils"; -configDotenv(); +const projectDir = process.cwd(); +loadEnvConfig(projectDir); const logger = new Logger(["STARTUP"]); logger.log("Starting server..."); -const dev = process.env.NODE_ENV !== "production"; +const mode = process.env.NODE_ENV; -logger.debug("Constants set"); +logger.info("Constants set. Using mode:", mode); const useHttps = - existsSync("./certs/key.pem") && existsSync("./certs/cert.pem"); + mode !== "test" && + existsSync("./certs/key.pem") && + existsSync("./certs/cert.pem"); const httpsOptions = useHttps ? { @@ -32,10 +36,13 @@ const httpsOptions = useHttps } : {}; -const port = useHttps ? 443 : 80; +const port = useHttps ? 443 : mode == "test" ? 3000 : 80; logger.debug(`Using port ${port}`); -const app = next({ dev, port }); +const app = next({ + dev: mode == "development", + port, +}); const handle = app.getRequestHandler(); logger.debug("App preparing..."); diff --git a/jest.config.ts b/jest.config.ts index e16b603b..9bca3a92 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -143,7 +143,7 @@ const config: Config = { // runner: "jest-runner", // The paths to modules that run some code to configure or set up the testing environment before each test - setupFiles: ["/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/Enums.ts b/lib/Enums.ts index ccd7a6f0..0014013b 100644 --- a/lib/Enums.ts +++ b/lib/Enums.ts @@ -109,3 +109,47 @@ export namespace ReefscapeEnums { Shallow = "Shallow", } } + +export namespace RebuiltEnums { + export enum ClimbingCapabilities { + No = "No", + FirstLevel = "FirstLevel", + SecondLevel = "SecondLevel", + ThirdLevel = "ThirdLevel", + } + + export enum DriveOverBump { + No = "No", + Slow = "Slow", + Fast = "Fast", + } + + export enum DriveUnderTrench { + No = "No", + Slow = "Slow", + Fast = "Fast", + } + + export enum LevelClimbed { + No = "No", + First = "First", + Second = "Second", + Third = "Third", + } +} + +export namespace DecodeEnums { + export enum EndgameParkStatus { + No = "No", + Partial = "Partial", + Full = "Full", + TwoBotPark = "Two Bot Park", + } + + export enum AutoStatus { + NoAuto = "No Auto", + MovePastStart = "Move Past Start", + ScoreOneArtifact = "Score One Artifact", + ScoreMultipleArtifacts = "Score Multiple Artifacts", + } +} diff --git a/lib/Gearbox.code-workspace b/lib/Gearbox.code-workspace new file mode 100644 index 00000000..0268e976 --- /dev/null +++ b/lib/Gearbox.code-workspace @@ -0,0 +1,9 @@ +{ + "folders": [ + { + "name": "Gearbox", + "path": "..", + }, + ], + "settings": {}, +} diff --git a/lib/Layout.ts b/lib/Layout.ts index 01ef80d1..ccd8d0f9 100644 --- a/lib/Layout.ts +++ b/lib/Layout.ts @@ -10,6 +10,8 @@ import { IntoTheDeepEnums, FtcDrivetrain, ReefscapeEnums, + RebuiltEnums, + DecodeEnums, } from "./Enums"; import { PitReportData, QuantData, Pitreport, Report, League } from "./Types"; @@ -217,6 +219,13 @@ export function keyToType( ReefscapeEnums.Climbing, ReefscapeEnums.DriveThroughDeepCage, ReefscapeEnums.EndgameClimbStatus, + //RebuiltEnums.AutoAbilities, + RebuiltEnums.ClimbingCapabilities, + RebuiltEnums.DriveOverBump, + RebuiltEnums.DriveUnderTrench, + RebuiltEnums.LevelClimbed, + DecodeEnums.AutoStatus, + DecodeEnums.EndgameParkStatus, ]; if (key === "Defense") return Defense; @@ -231,6 +240,15 @@ export function keyToType( if (key == "DriveThroughDeepCage") return ReefscapeEnums.DriveThroughDeepCage; if (key == "EndgameClimbStatus") return ReefscapeEnums.EndgameClimbStatus; + //if (key == "AutoAbilities") return RebuiltEnums.AutoAbilities; + if (key == "ClimbingCapabilities") return RebuiltEnums.ClimbingCapabilities; + if (key == "DriveOverBump") return RebuiltEnums.DriveOverBump; + if (key == "DiveUnderTrench") return RebuiltEnums.DriveUnderTrench; + if (key == "LevelClimbed") return RebuiltEnums.LevelClimbed; + + if (key == "EndgameParkStatusDecode") return DecodeEnums.EndgameParkStatus; + if (key == "AutoStatus") return DecodeEnums.AutoStatus; + for (const e of enums) { if (Object.values(e).includes(exampleData[key])) return e; } 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 96bc0685..2bf4827c 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"; @@ -41,7 +42,7 @@ import { assignScoutersToCompetitionMatches, generateReportsForMatch, } from "../CompetitionHandling"; -import { CenterStage, Crescendo, games, IntoTheDeep } from "../games"; +import { CenterStage, Crescendo, games, IntoTheDeep, Rebuilt } from "../games"; import { Statbotics } from "../Statbotics"; import { TheBlueAlliance } from "../TheBlueAlliance"; import { SlackNotLinkedError } from "./Errors"; @@ -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"]); @@ -213,7 +214,7 @@ export default class ClientApi extends NextApiTemplate { if (number <= 0) { return res.status(200).send(undefined); } - + console.log("Getting autofill data for team:", number, league); res .status(200) .send( @@ -630,7 +631,7 @@ export default class ClientApi extends NextApiTemplate { competitionReports = createNextRoute< [string, boolean, boolean], - Report[], + { quantReports: Report[]; pitReports: { [team: number]: Pitreport[] } }, ApiDependencies, { team: Team; comp: Competition } >({ @@ -656,19 +657,51 @@ export default class ClientApi extends NextApiTemplate { if (usePublicData && !comp.publicData) usedComps.push(comp); - const reports = ( - await db.findObjects(CollectionId.Reports, { - match: { $in: usedComps.flatMap((m) => m.matches) }, - submitted: submitted ? true : { $exists: true }, - }) - ) - // Filter out comments from other competitions - .map((report) => - comp.matches.includes(report.match) - ? report - : { ...report, data: { ...report.data, comments: "" } }, - ); - return res.status(200).send(reports); + const [reports, pitReports] = await Promise.all([ + ( + await db.findObjects(CollectionId.Reports, { + match: { $in: usedComps.flatMap((m) => m.matches) }, + submitted: submitted ? true : { $exists: true }, + }) + ) + // Filter out comments from other competitions + .map((report) => + comp.matches.includes(report.match) + ? report + : { ...report, data: { ...report.data, comments: "" } }, + ), + ( + await db.findObjects(CollectionId.PitReports, { + _id: { + $in: usedComps + .flatMap((m) => m.pitReports) + .map((id) => new ObjectId(id)), + }, + submitted: submitted ? true : { $exists: true }, + }) + ) + .map((pitReport) => + comp.pitReports.includes(pitReport._id!.toString()) + ? pitReport + : ({ + ...pitReport, + data: { ...pitReport.data, comments: "" }, + } as Pitreport), + ) + .reduce( + (dict, pitReport) => { + dict[pitReport.teamNumber] ??= []; + dict[pitReport.teamNumber].push(pitReport); + return dict; + }, + {} as { [team: number]: Pitreport[] }, + ), + ]); + + return res.status(200).send({ + quantReports: reports, + pitReports: pitReports, + }); }, }); @@ -696,6 +729,28 @@ export default class ClientApi extends NextApiTemplate { }, }); + getTeamsAtComp = createNextRoute< + [string], + number[], + ApiDependencies, + { team: Team; comp: Competition } + >({ + isAuthorized: (req, res, deps, [compId]) => + AccessLevels.IfOnTeamThatOwnsComp(req, res, deps, compId), + handler: async (req, res, { db: dbPromise }, { team, comp }, [compId]) => { + const db = await dbPromise; + + const pitReports = await db.findObjects(CollectionId.PitReports, { + _id: { + $in: comp.pitReports.map((pitReportId) => new ObjectId(pitReportId)), + }, + }); + return res + .status(200) + .send(pitReports.map((pitReport) => pitReport.teamNumber)); + }, + }); + matchReports = createNextRoute< [string], Report[], @@ -1069,7 +1124,7 @@ export default class ClientApi extends NextApiTemplate { }, }); - exportCompAsCsv = createNextRoute< + exportCompDataAsCsv = createNextRoute< [string], { csv: string }, ApiDependencies, @@ -1129,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 }, @@ -2567,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/ClientUtils.ts b/lib/client/ClientUtils.ts index e0f85d77..10f471ca 100644 --- a/lib/client/ClientUtils.ts +++ b/lib/client/ClientUtils.ts @@ -1,3 +1,6 @@ +import { Pitreport } from "../Types"; +import { MostCommonValue } from "./StatsMath"; + export function getIdsInProgressFromTimestamps(timestamps: { [id: string]: string; }) { @@ -151,3 +154,37 @@ export function promisify( export async function wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } + +export function mergePitReports(reports: Pitreport[]) { + if (reports.length === 0) { + throw new Error("Cannot merge 0 pit reports"); + } + + if (reports.length === 1) { + return reports[0]; + } + + const dataKeys = removeDuplicates( + reports.reduce( + (acc, report) => [...acc, ...Object.keys(report.data!)], + [] as string[], + ), + ); + + const newReport = { + teamNumber: reports[0].teamNumber, + data: dataKeys.reduce( + (acc, key) => { + acc[key] = MostCommonValue(key, reports); + return acc; + }, + {} as Record, + ), + } as Pitreport; + + for (const report of reports) { + if (report.data?.comments) newReport.data!.comments = report.data.comments; + } + + return newReport; +} diff --git a/lib/client/GameId.ts b/lib/client/GameId.ts index d2f946cc..71c169e5 100644 --- a/lib/client/GameId.ts +++ b/lib/client/GameId.ts @@ -3,6 +3,8 @@ export enum GameId { CenterStage = "CenterStage", IntoTheDeep = "IntoTheDeep", Reefscape = "Reefscape", + Rebuilt = "Rebuilt", + Decode = "Decode", } -export const defaultGameId = GameId.Reefscape; +export const defaultGameId = GameId.Rebuilt; 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 8464971e..21470c91 100644 --- a/lib/client/StatsMath.ts +++ b/lib/client/StatsMath.ts @@ -6,11 +6,16 @@ export const AmpAutoPoints = 2; export const AmpTeleopPoints = 1; export const TrapPoints = 5; +export const ArtifactPoints = 3; +export const MotifArtifactPoints = 5; +export const OverflowArtifactPoints = 1; +export const DepotArtifactPoints = 1; + type Selector = ((r: T) => number) | (keyof T & string); function getSelection( selector: Selector, - report: Report, + report: Record, ) { return typeof selector === "string" ? report.data[selector] @@ -44,7 +49,9 @@ export function NumericalTotal( reports: Report[], ) { let sum = 0; - reports?.forEach((report) => (sum += getSelection(selector, report))); + reports?.forEach( + (report) => (sum += Number(getSelection(selector, report) || 0)), + ); return Round(sum); } @@ -53,13 +60,13 @@ export function NumericalTotal( */ export function MostCommonValue( selector: Selector, - reports: Report[], + objects: Record[], ) { // Get a list of all values of the specified field let values: string[] = []; - reports?.forEach((report) => { + objects?.forEach((report) => { const val = getSelection(selector, report); - values.push((val as any).toString?.() ?? JSON.stringify(val)); + values.push((val as any)?.toString?.() ?? JSON.stringify(val)); }); // Count the occurrences of each value @@ -146,3 +153,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 = Number(quantitativeReports[0].data[stat]); + for (let repo of quantitativeReports) { + if (Number(repo.data[stat]) < minimum) { + minimum = Number(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 (Number(repo.data[stat]) > maximum) { + maximum = Number(repo.data[stat]); + } + } + return maximum; +} diff --git a/lib/dev/FakeData.ts b/lib/dev/FakeData.ts index 9782f082..c9e71d49 100644 --- a/lib/dev/FakeData.ts +++ b/lib/dev/FakeData.ts @@ -9,8 +9,8 @@ import { ObjectId } from "bson"; import CollectionId from "../client/CollectionId"; import DbInterface from "../client/dbinterfaces/DbInterface"; -const firstNameMaleURL = "https://www.randomlists.com/data/names-male.json"; -const firstNameFemaleURL = "https://www.randomlists.com/data/names-female.json"; +const firstNameMaleURL = "https://www.randomlists.com/male-names"; +const firstNameFemaleURL = "https://www.randomlists.com/female-names"; var cachedFirstNames: string[] = []; var cachedLastNames: string[] = []; @@ -43,7 +43,7 @@ export async function fakeUser( db: DbInterface, teamId: ObjectId | undefined, ): Promise { - const name = await randomName(); + const name = String(Math.random() * 101); const user = new User( name, "totallyrealemail@gmail.com", diff --git a/lib/games.ts b/lib/games.ts index fff434e3..33bfe09e 100644 --- a/lib/games.ts +++ b/lib/games.ts @@ -1,10 +1,12 @@ import { Dot } from "@/components/stats/Heatmap"; import { CenterStageEnums, + DecodeEnums, Defense, FrcDrivetrain, IntakeTypes, IntoTheDeepEnums, + RebuiltEnums, ReefscapeEnums, } from "./Enums"; import { Badge, FormLayoutProps, PitStatsLayout, StatsLayout } from "./Layout"; @@ -21,14 +23,23 @@ import { GameId } from "./client/GameId"; import { AmpAutoPoints, AmpTeleopPoints, + ArtifactPoints, BooleanAverage, + DepotArtifactPoints, + GetMinimum, MostCommonValue, + MotifArtifactPoints, NumericalTotal, + OverflowArtifactPoints, Round, SpeakerAutoPoints, SpeakerTeleopPoints, TrapPoints, } from "./client/StatsMath"; +import { report } from "process"; +import { GetMaximum } from "./client/StatsMath"; +import { getMaxListeners } from "events"; +//import { ClimbingCapabilities, ClimbingAbilities, DriverSkillLevel } from './Enums'; function getBaseBadges( pitReport: Pitreport | undefined, @@ -1323,23 +1334,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 +1426,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 +1582,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 +1857,9 @@ namespace Reefscape { break; } - totalPoints += report.TeleopCoralScoredLevelOne * 2; + totalPoints += + (report.TeleopCoralScoredLevelOne + report.TeleopAlgaeScoredProcessor) * + 2; totalPoints += (report.AutoCoralScoredLevelOne + report.TeleopCoralScoredLevelTwo) * 3; totalPoints += @@ -1649,9 +1870,7 @@ namespace Reefscape { 4; totalPoints += report.TeleopCoralScoredLevelFour * 5; totalPoints += - (report.AutoAlgaeScoredProcessor + - report.TeleopAlgaeScoredProcessor + - report.AutoCoralScoredLevelThree) * + (report.AutoAlgaeScoredProcessor + report.AutoCoralScoredLevelThree) * 6; totalPoints += report.AutoCoralScoredLevelFour * 7; } @@ -1677,9 +1896,696 @@ namespace Reefscape { ); } +export namespace Rebuilt { + export class QuantitativeData extends QuantData { + AutoFuelPointsOne: number = 0; + AutoFuelPointsFive: number = 0; + AutoFuelPointsTen: number = 0; + + FuelPointsOne: number = 0; + FuelPointsFive: number = 0; + FuelPointsTen: number = 0; + + AutoClimbedLevelOne: boolean = false; + + EngameDefenseStatus: Defense = Defense.None; + LevelClimbed: RebuiltEnums.LevelClimbed = RebuiltEnums.LevelClimbed.No; + } + export class PitData extends PitReportData { + CanDriveOverBump: boolean = false; + CanDriveUnderTrench: boolean = false; + CanDeClimb: boolean = false; + HopperVolume: number = 0; + RobotWeight: number = 0; + RobotWidth: number = 0; + RobotLength: number = 0; + AutoAbilities: string = ""; + ClimbingCapabilities: RebuiltEnums.ClimbingCapabilities = + RebuiltEnums.ClimbingCapabilities.No; + } + const pitReportLayout: FormLayoutProps = { + Capabilities: [ + { key: "CanDriveOverBump", label: "Can Drive Over Bump?" }, + { key: "CanDriveUnderTrench", label: "Can Drive Under Trench?" }, + { key: "CanDeClimb", label: "Can De-Climb?" }, + { key: "ClimbingCapabilities", label: "Climbing?" }, + { key: "HopperVolume", label: "Hopper Volume?" }, + ], + Auto: [{ key: "AutoAbilities", label: "Auto Capabilities?" }], + }; + const quantitativeReportLayout: FormLayoutProps = { + Auto: [ + { key: "AutoClimbedLevelOne", label: "Climbed Level One (Auto)" }, + [ + [ + { + key: "AutoFuelPointsOne", + label: "Auto One point", + }, + ], + [ + { + key: "AutoFuelPointsFive", + label: "Auto Five points", + }, + ], + [ + { + key: "AutoFuelPointsTen", + label: "Auto Ten points", + }, + ], + ], + ], + Teleop: [ + [ + [ + { + key: "FuelPointsOne", + label: "Teleop One point", + }, + ], + [ + { + key: "FuelPointsFive", + label: "Teleop Five points", + }, + ], + [ + { + key: "FuelPointsTen", + label: "TeleopTen points", + }, + ], + ], + ], + "Post Match": ["LevelClimbed", "EngameDefenseStatus"], + }; + + const statsLayout: StatsLayout = { + sections: { + Auto: [ + { + label: "Total Auto Fuel Points Scored By Alliance", + get(pitData, quantitativeReports) { + return ( + NumericalTotal("AutoFuelPointsOne", quantitativeReports!) + + NumericalTotal("AutoFuelPointsFive", quantitativeReports!) * 5 + + NumericalTotal("AutoFuelPointsTen", quantitativeReports!) * 10 + ); + }, + }, + { + label: "< Min Auto Fuel Points Scored By Alliance", + get(pitData, quantitativeReports) { + return ( + GetMinimum(quantitativeReports!, "AutoFuelPointsOne") + + GetMinimum(quantitativeReports!, "AutoFuelPointsFive") * 5 + + GetMinimum(quantitativeReports!, "AutoFuelPointsTen") * 10 + ); + }, + }, + { + label: "< Max Auto Fuel Points Scored By Alliance", + get(pitData, quantitativeReports) { + return ( + GetMaximum(quantitativeReports!, "AutoFuelPointsOne") + + GetMaximum(quantitativeReports!, "AutoFuelPointsFive") * 5 + + GetMaximum(quantitativeReports!, "AutoFuelPointsTen") * 10 + ); + }, + }, + ], + Teleop: [ + { + label: "Total Fuel Points Scored By Alliance", + get(pitData, quantitativeReports) { + return ( + NumericalTotal("FuelPointsOne", quantitativeReports!) + + NumericalTotal("FuelPointsFive", quantitativeReports!) * 5 + + NumericalTotal("FuelPointsTen", quantitativeReports!) * 10 + ); + }, + }, + { + label: "< Min Fuel Points Scored By Alliance", + get(pitData, quantitativeReports) { + return ( + GetMinimum(quantitativeReports!, "FuelPointsOne") + + GetMinimum(quantitativeReports!, "FuelPointsFive") * 5 + + GetMinimum(quantitativeReports!, "FuelPointsTen") * 10 + ); + }, + }, + { + label: "< Max Fuel Points Scored By Alliance", + get(pitData, quantitativeReports) { + return ( + GetMaximum(quantitativeReports!, "FuelPointsOne") + + GetMaximum(quantitativeReports!, "FuelPointsFive") * 5 + + GetMaximum(quantitativeReports!, "FuelPointsTen") * 10 + ); + }, + }, + ], + }, + getGraphDots: function ( + quantitativeReports: Report[], + pitReport?: Pitreport | undefined, + ): Dot[] { + return []; + }, + }; + + const pitStatsLayout: PitStatsLayout = { + overallSlideStats: [ + { + label: "Estamate Hopper Volume", + key: "HopperVolume", + }, + { + label: "RobotWeight", + key: "RobotWeight", + }, + { + label: "Robot Width", + key: "RobotWidth", + }, + { + label: "Robot Length", + key: "RobotLength", + }, + ], + individualSlideStats: [ + { + label: "Average Auto Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const TotalAutoAllianceFuelPoints = + NumericalTotal("AutoFuelPointsOne", quantitativeReports) + + NumericalTotal("AutoFuelPointsFive", quantitativeReports) * 5 + + NumericalTotal("AutoFuelPointsTen", quantitativeReports) * 10; + return TotalAutoAllianceFuelPoints / quantitativeReports.length; + }, + }, + { + label: "Average Teleop Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const TotalTeleopAllianceFuelPoints = + NumericalTotal("FuelPointsOne", quantitativeReports) + + NumericalTotal("FuelPointsFive", quantitativeReports) * 5 + + NumericalTotal("FuelPointsTen", quantitativeReports) * 10; + return TotalTeleopAllianceFuelPoints / quantitativeReports.length; + }, + }, + { + label: "Ave Endgame Stats", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) return 0; + + const climb = NumericalTotal("LevelClimbed", quantitativeReports); + return Round(climb) / quantitativeReports.length; + }, + }, + ], + robotCapabilities: [ + { key: "CanDriveOverBump", label: "Can Drive Over Bump?" }, + { key: "CanDriveUnderTrench", label: "Can Drive Under Trench?" }, + { key: "CanDeClimb", label: "Can De-Climb?" }, + { key: "ClimbingCapabilities", label: "Climbing?" }, + ], + graphStat: { + label: "Average Fuel Scored In Hopper", + key: "TeleopTotalPoints", + }, + }; + + function getBadges( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + card: boolean, + ) { + const badges: Badge[] = getBaseBadges(pitReport, quantitativeReports); + + if (pitReport?.data?.CanDriveOverBump) + badges.push({ text: "Can Drive Over Bump", color: "accent" }); + if (pitReport?.data?.CanDriveUnderTrench) + badges.push({ text: "Can Drive Under Trench", color: "accent" }); + if (pitReport?.data?.CanDeClimb) + badges.push({ text: "Can Declimb", color: "accent" }); + + if ( + pitReport?.data?.ClimbingCapabilities === + RebuiltEnums.ClimbingCapabilities.FirstLevel + ) + badges.push({ text: "Can Climb First Level", color: "accent" }); + else if ( + pitReport?.data?.ClimbingCapabilities === + RebuiltEnums.ClimbingCapabilities.SecondLevel + ) + badges.push({ text: "Can Climb Second Level", color: "accent" }); + else if ( + pitReport?.data?.ClimbingCapabilities === + RebuiltEnums.ClimbingCapabilities.ThirdLevel + ) + badges.push({ text: "Can Climb Third Level", color: "accent" }); + + return badges; + } + + function getAvgPoints(reports: Report[] | undefined) { + if (!reports) return 0; + + let totalPoints = 0; + + for (const report of reports.map((r) => r.data)) { + switch (report.LevelClimbed) { + case RebuiltEnums.LevelClimbed.No: + break; + case RebuiltEnums.LevelClimbed.First: + totalPoints += 10; + break; + case RebuiltEnums.LevelClimbed.Second: + totalPoints += 20; + break; + case RebuiltEnums.LevelClimbed.Third: + totalPoints += 30; + break; + } + totalPoints += + Number(report.FuelPointsOne + report.AutoFuelPointsOne) + + Number(report.FuelPointsFive + report.AutoFuelPointsOne) * 5 + + Number(report.FuelPointsTen + report.AutoFuelPointsOne) * 10; + } + return totalPoints / Math.max(reports.length, 1); + } + export const game = new Game( + "Rebuilt", + 2026, + League.FRC, + QuantitativeData, + PitData, + pitReportLayout, + quantitativeReportLayout, + statsLayout, + pitStatsLayout, + "Rebuilt", + "https://www.firstinspires.org/hs-fs/hubfs/image-library/web/frc_rebuilt_1240x860.webp?width=630", + "invert", + getBadges, + getAvgPoints, + ); +} + +export namespace Decode { + export class QuantitativeData extends QuantData { + AutoMovedPastStartingLine: boolean = false; + + AutoArtifactsClassified: number = 0; + AutoOverflowArtifacts: number = 0; + AutoMotifArtifacts: number = 0; + + TeleopArtifactsClassified: number = 0; + TeleopOverflowArtifacts: number = 0; + TeleopMotifArtifacts: number = 0; + TeleopDepotArtifacts: number = 0; + + EndgameParkStatusDecode: DecodeEnums.EndgameParkStatus = + DecodeEnums.EndgameParkStatus.No; + EndgameDefense: Defense = Defense.None; + } + + export class PitData extends PitReportData { + CanScoreClassifier: boolean = false; + CanScoreDepot: boolean = false; + CanOpenGate: boolean = false; + CanParkWithOtherBots: boolean = false; + + ArtifactsScoredAuto: number = 0; + AutoAccountsForMotif: boolean = false; + AutoAbilities: DecodeEnums.AutoStatus = DecodeEnums.AutoStatus.NoAuto; + } + + const pitReportLayout: FormLayoutProps = { + Capabilities: [ + { key: "CanScoreClassifier", label: "Can Score Classifier?" }, + { key: "CanScoreDepot", label: "Can Score Depot?" }, + { key: "CanOpenGate", label: "Can Score Gate?" }, + { key: "CanParkWithOtherBots", label: "Can Park With Other Bots?" }, + ], + Auto: [ + { key: "ArtifactsScoredAuto", label: "Average Auto Artifacts" }, + { key: "AutoAccountsForMotif", label: "Auto Accounts For Motif?" }, + { key: "AutoAbilities", label: "Other Auto Scoring Capabilities" }, + ], + }; + + const quantitativeReportLayout: FormLayoutProps = { + Auto: [ + { key: "AutoMovedPastStartingLine", label: "Moved Past Starting line" }, + [ + [ + { + key: "AutoArtifactsClassified", + label: "Artifacts Classified (Auto)", + }, + ], + [{ key: "AutoOverflowArtifacts", label: "Overflow Artifacts (Auto)" }], + [{ key: "AutoMotifArtifacts", label: "Motif Artifacts (Auto)" }], + ], + ], + Teleop: [ + [ + [ + { + key: "TeleopArtifactsClassified", + label: "Artifacts Classified (Teleop)", + }, + { + key: "TeleopOverflowArtifacts", + label: "Overflow Artifacts (Teleop)", + }, + ], + [ + { + key: "TeleopMotifArtifacts", + label: "Motif Artifacts (Teleop)", + }, + { + key: "TeleopDepotArtifacts", + label: "Depot Artifacts (Teleop)", + }, + ], + ], + ], + Endgame: ["Defense", "EndgameParkStatusDecode"], + }; + + const statsLayout: StatsLayout = { + sections: { + Auto: [ + { + key: "AutoArtifactsClassified", + label: "Average Amt Of Artifacts Classified Auto", + }, + { + label: "> Min Artifacts Classified Auto", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoArtifactsClassified"); + }, + }, + { + label: "> Max Artifacts Classified Auto", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoArtifactsClassified"); + }, + }, + { + key: "AutoOverflowArtifacts", + label: "Average Amt Of Overflow Artifacts Classified Auto", + }, + { + label: "> Min Overflow Artifacts Classified Auto", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoOverflowArtifacts"); + }, + }, + { + label: "> Max Overflow Artifacts Classified Auto", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoOverflowArtifacts"); + }, + }, + { + key: "AutoMotifArtifacts", + label: "Average Amt Of Motif Artifacts Classified Auto", + }, + { + label: "> Min Motif Artifacts Classified Auto", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoMotifArtifacts"); + }, + }, + { + label: "> Max Motif Artifacts Classified Auto", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoMotifArtifacts"); + }, + }, + ], + Teleop: [ + { + key: "TeleopArtifactsClassified", + label: "Average Amt Of Artifacts Classified Teleop", + }, + { + label: "> Min Artifacts Classified Teleop", + get(pitData, quantitativeReports) { + return GetMinimum( + quantitativeReports!, + "TeleopArtifactsClassified", + ); + }, + }, + { + label: "> Max Artifacts Classified Teleop", + get(pitData, quantitativeReports) { + return GetMaximum( + quantitativeReports!, + "TeleopArtifactsClassified", + ); + }, + }, + { + key: "TeleopOverflowArtifacts", + label: "Average Amt Of Overflow Artifacts Classified Teleop", + }, + { + label: "> Min Overflow Artifacts Classified Teleop", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "TeleopOverflowArtifacts"); + }, + }, + { + label: "> Max Overflow Artifacts Classified Teleop", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "TeleopOverflowArtifacts"); + }, + }, + { + key: "TeleopMotifArtifacts", + label: "Average Amt Of Motif Artifacts Classified Teleop", + }, + { + label: "> Min Motif Artifacts Classified Teleop", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "TeleopMotifArtifacts"); + }, + }, + { + label: "> Max Motif Artifacts Classified Teleop", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "TeleopMotifArtifacts"); + }, + }, + { + key: "TeleopDepotArtifacts", + label: "Average Amt of Artifacts In Depot Teleop", + }, + { + label: "> Min Depot Artifacts Teleop", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "TeleopDepotArtifacts"); + }, + }, + { + label: "> Max Depot Artifacts Teleop", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "TeleopDepotArtifacts"); + }, + }, + ], + }, + getGraphDots: function ( + quantitativeReports: Report[], + pitReport?: Pitreport | undefined, + ): Dot[] { + return []; + }, + }; + + const pitStatsLayout: PitStatsLayout = { + overallSlideStats: [ + { label: "Artifacts Scored Auto", key: "ArtifactsScoredAuto" }, + ], + individualSlideStats: [ + { + label: "Average Auto Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) { + return 0; + } + + const Artifacts = + NumericalTotal("AutoArtifactsClassified", quantitativeReports) * + ArtifactPoints; + + const MotifArtifacts = + NumericalTotal("AutoMotifArtifacts", quantitativeReports) * + MotifArtifactPoints; + + const OverflowArtifacts = + NumericalTotal("AutoOverflowArtifacts", quantitativeReports) * + OverflowArtifactPoints; + + return ( + (Artifacts + MotifArtifacts + OverflowArtifacts) / + quantitativeReports.length + ); + }, + }, + { + label: "Average Teleop Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) { + return 0; + } + + const Artifacts = + NumericalTotal("TeleopArtifactsClassified", quantitativeReports) * + ArtifactPoints; + + const MotifArtifacts = + NumericalTotal("TeleopMotifArtifacts", quantitativeReports) * + MotifArtifactPoints; + + const OverflowArtifacts = + NumericalTotal("TeleopOverflowArtifacts", quantitativeReports) * + OverflowArtifactPoints; + + const DepotArtifacts = + NumericalTotal("TeleopDepotArtifacts", quantitativeReports) * + DepotArtifactPoints; + + return ( + (Artifacts + MotifArtifacts + OverflowArtifacts + DepotArtifacts) / + quantitativeReports.length + ); + }, + }, + ], + robotCapabilities: [ + { key: "CanScoreClassifier", label: "Can Score Classifier?" }, + { key: "CanScoreDepot", label: "Can Score Depot?" }, + { key: "CanOpenGate", label: "Can Open Gate?" }, + { key: "CanParkWithOtherBots", label: "Can Park With Other Bots" }, + ], + graphStat: { + label: "TeleopArtifactsClassified", + key: "TeleopArtifactsClassified", + }, + }; + + function getBadges( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + card: boolean, + ) { + const badges: Badge[] = getBaseBadges(pitReport, quantitativeReports); + + if (pitReport?.data?.CanOpenGate) + badges.push({ text: "Can Open Gate", color: "info" }); + if (pitReport?.data?.CanScoreDepot) + badges.push({ text: "Can Score Depot", color: "secondary" }); + if (pitReport?.data?.CanScoreClassifier) + badges.push({ text: "Can Score Classifier", color: "primary" }); + if (pitReport?.data?.CanParkWithOtherBots) + badges.push({ text: "Can Double Park", color: "success" }); + + if ( + !(pitReport?.data?.CanScoreDepot || pitReport?.data?.CanScoreClassifier) + ) + badges.push({ text: "Cannot Score", color: "warning" }); + + return badges; + } + + function getAvgPoints(reports: Report[] | undefined) { + if (!reports) return 0; + + let totalPoints = 0; + + for (const report of reports.map((r) => r.data)) { + switch (report.EndgameParkStatus) { + case DecodeEnums.EndgameParkStatus.No: + break; + case DecodeEnums.EndgameParkStatus.Partial: + totalPoints += 5; + break; + case DecodeEnums.EndgameParkStatus.Full: + totalPoints += 10; + break; + case DecodeEnums.EndgameParkStatus.TwoBotPark: + totalPoints += 20; + break; + } + + totalPoints += + (report.AutoArtifactsClassified + report.TeleopArtifactsClassified) * + ArtifactPoints; + totalPoints += + (report.AutoMotifArtifacts + report.TeleopMotifArtifacts) * + MotifArtifactPoints; + totalPoints += + (report.AutoOverflowArtifacts + report.TeleopOverflowArtifacts) * + OverflowArtifactPoints; + totalPoints += report.TeleopDepotArtifacts * DepotArtifactPoints; + } + + return totalPoints / Math.max(reports.length, 1); + } + + export const game = new Game( + "Decode", + 2026, + League.FTC, + QuantitativeData, + PitData, + pitReportLayout, + quantitativeReportLayout, + statsLayout, + pitStatsLayout, + "Decode", + "https://info.firstinspires.org/hs-fs/hubfs/2026%20Season/Season%20Assets/first_age_ftc_decode_logo_vertical_rgb_fullcolor.png?width=237&height=348&name=first_age_ftc_decode_logo_vertical_rgb_fullcolor.png", + "invert", + getBadges, + getAvgPoints, + ); +} + export const games: { [id in GameId]: Game } = Object.freeze({ + [GameId.Rebuilt]: Rebuilt.game, [GameId.Reefscape]: Reefscape.game, [GameId.IntoTheDeep]: IntoTheDeep.game, [GameId.Crescendo]: Crescendo.game, [GameId.CenterStage]: CenterStage.game, + [GameId.Decode]: Decode.game, }); 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 3ee4c646..2194d31c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,73 +1,75 @@ { "name": "sj3", - "version": "1.2.23", + "version": "1.3.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sj3", - "version": "1.2.23", + "version": "1.3.6", "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", "@yudiel/react-qr-scanner": "^2.2.1", - "bootstrap": "^5.3.3", + "bootstrap": "^5.3.5", "browser-image-compression": "^2.0.2", "bson": "^5.0.0", - "dotenv": "^16.4.7", + "dotenv": "^16.5.0", "eslint": "9.18.0", - "eslint-config-next": "15.2.2", - "formidable": "^3.5.2", - "jose": "^6.0.8", + "eslint-config-next": "15.2.4", + "formidable": "^3.5.4", + "jose": "^6.0.10", "levenary": "^1.1.1", - "minimongo": "^6.19.0", - "mongo-anywhere": "^1.1.11", + "minimongo": "^7.0.0", + "mongo-anywhere": "^1.1.15", "mongodb": "^5.0.0", - "next": "^15.2.3", + "next": "^15.2.6", "next-auth": "^4.24.11", "next-seo": "^6.6.0", + "nodemailer": "^7.0.13", "omit-call-signature": "^1.0.15", "react": "18.3.1", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^2.10.9", - "react-chartjs-2": "^5.2.0", + "react-chartjs-2": "^5.3.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "18.3.1", "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.3.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" + "typescript": "5.8.3", + "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.52.0", "@types/formidable": "^3.4.5", "@types/jest": "^29.5.14", - "@types/node": "^22.13.0", + "@types/node": "^22.19.3", "@types/react": "^18.3.8", "autoprefixer": "^10.4.21", "cross-env": "^7.0.3", "daisyui": "^4.12.22", "jest": "^29.7.0", "postcss": "^8.5.3", - "prettier": "3.5.0", - "serwist": "^9.0.11", + "prettier": "3.5.3", + "serwist": "^9.0.13", "tailwindcss": "^3.4.17", "ts-jest": "^29.2.5" } @@ -106,13 +108,15 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -337,19 +341,21 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -364,109 +370,28 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@babel/types": "^7.29.0" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -709,14 +634,15 @@ } }, "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -753,14 +679,14 @@ } }, "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -792,6 +718,16 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", @@ -1263,9 +1199,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" } @@ -1280,81 +1218,541 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.13.0", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "license": "Apache-2.0", + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, "engines": { - "node": ">=18.18" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.22" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", - "license": "Apache-2.0", + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.18" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], @@ -1450,10 +1848,11 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -1930,15 +2329,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.5.12", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.12.tgz", + "integrity": "sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==", "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 +2372,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.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.12.tgz", + "integrity": "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==", "cpu": [ "arm64" ], @@ -1989,9 +2388,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.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.12.tgz", + "integrity": "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==", "cpu": [ "x64" ], @@ -2005,9 +2404,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.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.12.tgz", + "integrity": "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==", "cpu": [ "arm64" ], @@ -2021,9 +2420,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.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.12.tgz", + "integrity": "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==", "cpu": [ "arm64" ], @@ -2037,9 +2436,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.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.12.tgz", + "integrity": "sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==", "cpu": [ "x64" ], @@ -2053,9 +2452,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.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.12.tgz", + "integrity": "sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==", "cpu": [ "x64" ], @@ -2069,9 +2468,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.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.12.tgz", + "integrity": "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==", "cpu": [ "arm64" ], @@ -2085,9 +2484,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.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.12.tgz", + "integrity": "sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==", "cpu": [ "x64" ], @@ -2100,6 +2499,18 @@ "node": ">= 10" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2132,11 +2543,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", @@ -2145,6 +2551,15 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2154,6 +2569,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.1.tgz", + "integrity": "sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.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 +2625,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.6", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.6.tgz", + "integrity": "sha512-zNueW5Wn/4jNC1c5LFgXzbUdv5Lhms+FWjOvWAhal7gx5YVf0q6dPJ0dnR70+ifo59gcMLwCZEaTS9EEuUhKvQ==", + "license": "MIT", "dependencies": { "html-to-text": "9.0.5", - "js-beautify": "^1.14.11", + "prettier": "3.5.3", "react-promise-suspense": "0.3.4" }, "engines": { @@ -2278,6 +2710,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 +2720,17 @@ } }, "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.5.4", + "resolved": "https://registry.npmjs.org/@serwist/build/-/build-9.5.4.tgz", + "integrity": "sha512-FTiNsNb3luKsLIxjKCvkPiqFZSbx7yVNOFGSUhp4lyfzgnelT1M3/lMC88kLiak90emkuFjSkQgwa6OnyhMZlQ==", + "license": "MIT", "dependencies": { + "@serwist/utils": "9.5.4", "common-tags": "1.8.2", - "glob": "10.4.5", + "glob": "10.5.0", "pretty-bytes": "6.1.1", "source-map": "0.8.0-beta.0", - "zod": "3.23.8" + "zod": "4.3.6" }, "engines": { "node": ">=18.0.0" @@ -2310,17 +2745,20 @@ } }, "node_modules/@serwist/build/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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/@serwist/build/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==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -2337,11 +2775,12 @@ } }, "node_modules/@serwist/build/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -2354,6 +2793,8 @@ "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "license": "BSD-3-Clause", "dependencies": { "whatwg-url": "^7.0.0" }, @@ -2365,6 +2806,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 +2814,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,54 +2829,55 @@ } }, "node_modules/@serwist/next": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/@serwist/next/-/next-9.0.11.tgz", - "integrity": "sha512-Aa26qgJxnaxbGK8JYAWqAPeDcyfu0xjYUPpvSfc3d9GrIl9G9cdflkcsvcbmJGnOb3Oc4tHN4wncYO6cIKb2wQ==", + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/@serwist/next/-/next-9.5.4.tgz", + "integrity": "sha512-bkPkvMs4GfNV3C3tSZg9O5mb+B7Rq8zLpkZNF2hV96zep6d/ujA7yBp8o8bD1rmLb7I++ifsIMQzH2a+vVc/ag==", + "license": "MIT", "dependencies": { - "@serwist/build": "9.0.11", - "@serwist/webpack-plugin": "9.0.11", - "@serwist/window": "9.0.11", - "chalk": "5.3.0", - "glob": "10.4.5", - "serwist": "9.0.11", - "zod": "3.23.8" + "@serwist/build": "9.5.4", + "@serwist/utils": "9.5.4", + "@serwist/webpack-plugin": "9.5.4", + "@serwist/window": "9.5.4", + "browserslist": "4.28.1", + "glob": "10.5.0", + "kolorist": "1.8.0", + "semver": "7.7.3", + "serwist": "9.5.4", + "zod": "4.3.6" }, "engines": { "node": ">=18.0.0" }, "peerDependencies": { + "@serwist/cli": "^9.5.4", "next": ">=14.0.0", + "react": ">=18.0.0", "typescript": ">=5.0.0" }, "peerDependenciesMeta": { + "@serwist/cli": { + "optional": true + }, "typescript": { "optional": true } } }, "node_modules/@serwist/next/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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "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==", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/@serwist/next/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==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -2449,11 +2894,12 @@ } }, "node_modules/@serwist/next/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -2462,14 +2908,30 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@serwist/utils": { + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/@serwist/utils/-/utils-9.5.4.tgz", + "integrity": "sha512-uyriGQF1qjNEHXXfsd8XJ5kfK3/MezEaUw//XdHjZeJ0LvLamrgnLJGQQoyJqUfEPCiJ4jJwc4uYMB9LjLiHxA==", + "license": "MIT", + "peerDependencies": { + "browserslist": ">=4" + }, + "peerDependenciesMeta": { + "browserslist": { + "optional": true + } + } + }, "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.5.4", + "resolved": "https://registry.npmjs.org/@serwist/webpack-plugin/-/webpack-plugin-9.5.4.tgz", + "integrity": "sha512-Zmrce/fuKIBYPOFlCSutIk2rE3949ihFjrXiiR3SNOhnBhxYDOcf6oz3zXFQDLAaih8wdjDL4PlzPEqKg1OmJA==", + "license": "MIT", "dependencies": { - "@serwist/build": "9.0.11", + "@serwist/build": "9.5.4", + "@serwist/utils": "9.5.4", "pretty-bytes": "6.1.1", - "zod": "3.23.8" + "zod": "4.3.6" }, "engines": { "node": ">=18.0.0" @@ -2488,12 +2950,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.5.4", + "resolved": "https://registry.npmjs.org/@serwist/window/-/window-9.5.4.tgz", + "integrity": "sha512-52t2G+TgiWDdRwGG0ArU28uy6/oQYICQfNLHs4ywybyS6mHy3BxHFl+JjB5vhg8znIG1LMpGvOmS5b7AuPVYDw==", + "license": "MIT", "dependencies": { "@types/trusted-types": "2.0.7", - "serwist": "9.0.11" + "serwist": "9.5.4" }, "peerDependencies": { "typescript": ">=5.0.0" @@ -2533,11 +2996,6 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" - }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2939,12 +3397,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.0.tgz", - "integrity": "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==", + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/p5": { @@ -3012,7 +3470,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 +3522,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", @@ -3104,9 +3555,10 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3606,6 +4058,15 @@ "zxing-wasm": "^2.0.1" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -3627,9 +4088,9 @@ } }, "node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz", + "integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==", "funding": [ { "type": "github", @@ -3640,6 +4101,7 @@ "url": "https://opencollective.com/bootstrap" } ], + "license": "MIT", "peerDependencies": { "@popperjs/core": "^2.11.8" } @@ -3650,9 +4112,10 @@ "integrity": "sha512-N/toHA87JcmAHPqU8Qt7YnhFK6W2WUpdq5M1k/JqLdTqtts7sHEMZhFjFWTvvR2poKF7Qki0qknhIPIr5I7TIQ==" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3678,10 +4141,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "dev": true, + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -3698,10 +4160,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -3745,17 +4208,6 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -3801,9 +4253,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001703", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz", - "integrity": "sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ==", + "version": "1.0.30001768", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001768.tgz", + "integrity": "sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==", "funding": [ { "type": "opencollective", @@ -4004,20 +4456,6 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4034,17 +4472,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4069,6 +4496,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 +4506,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", @@ -4317,15 +4736,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "node_modules/decache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/decache/-/decache-3.1.0.tgz", - "integrity": "sha512-p7D6wJ5EJFFq1CcF2lu1XeqKFLBob8jRQGNAvFLTsV3CbSKBl3VtliAVlUIGz2i9H6kEFnI2Amaft5ZopIG2Fw==", - "optional": true, - "dependencies": { - "find": "^0.2.4" - } - }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -4413,9 +4823,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "optional": true, "engines": { @@ -4447,9 +4857,10 @@ "dev": true }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -4492,6 +4903,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 +4922,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 +4944,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", @@ -4540,9 +4955,10 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -4557,58 +4973,11 @@ }, "node_modules/ecc-jsbn": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dependencies": { - "jsbn": "~0.1.0", - "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" + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" } }, "node_modules/ejs": { @@ -4627,10 +4996,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.114", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz", - "integrity": "sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==", - "dev": true, + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "license": "ISC" }, "node_modules/emittery": { @@ -4686,6 +5054,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" }, @@ -4907,7 +5276,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "engines": { "node": ">=6" } @@ -4982,12 +5350,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", @@ -5199,9 +5567,9 @@ } }, "node_modules/eslint-config-next/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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -5220,12 +5588,12 @@ } }, "node_modules/eslint-config-next/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5499,6 +5867,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", @@ -5744,19 +6121,21 @@ } }, "node_modules/filelist/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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -5775,15 +6154,6 @@ "node": ">=8" } }, - "node_modules/find": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/find/-/find-0.2.9.tgz", - "integrity": "sha512-7a4/LCiInB9xYMnAUEjLilL9FKclwbwK7VlXw+h5jMvT2TDFeYFCHM24O1XdnC/on/hx8mxVO3FTQkyHZnOghQ==", - "optional": true, - "dependencies": { - "traverse-chain": "~0.1.0" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5849,16 +6219,33 @@ "node": "*" } }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/formidable": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", - "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", "license": "MIT", "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", - "hexoid": "^2.0.0", "once": "^1.4.0" }, + "engines": { + "node": ">=14.0.0" + }, "funding": { "url": "https://ko-fi.com/tunnckoCore/commissions" } @@ -6216,15 +6603,6 @@ "node": ">= 0.4" } }, - "node_modules/hexoid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", - "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -6243,6 +6621,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 +6644,7 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", @@ -6296,9 +6676,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.3", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz", + "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", + "license": "ISC" }, "node_modules/idb-wrapper": { "version": "1.7.2", @@ -6373,11 +6754,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", @@ -6399,10 +6775,14 @@ "loose-envify": "^1.0.0" } }, - "node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } }, "node_modules/is-array-buffer": { "version": "3.0.4", @@ -7828,9 +8208,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 +8221,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", @@ -7921,9 +8232,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -8040,6 +8351,12 @@ "node": ">=6" } }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "license": "MIT" + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -8062,6 +8379,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" } @@ -8130,9 +8448,10 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -8148,7 +8467,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", @@ -8252,9 +8572,10 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -8272,9 +8593,10 @@ } }, "node_modules/minimongo": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/minimongo/-/minimongo-6.19.0.tgz", - "integrity": "sha512-DF2FLEJ6vTJSz7QsWrm2FMq2UR2yowwXPKhgQ69mxT5KOm/bYcrVv1RDw3zyeeYw/I7a/2MkiRWyV1EglniONA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/minimongo/-/minimongo-7.0.0.tgz", + "integrity": "sha512-kFF0oXLr8GKv4mZ2HCMNSwNjpDsGtXr5WfdnBcYwzSAvgS3HNf2UMbbhrBDG5ElYJ0im7QAQxFkSj8S66g0cvg==", + "license": "LGPLv3", "dependencies": { "@turf/boolean-crosses": "^6.0.1", "@turf/boolean-point-in-polygon": "^6.0.1", @@ -8305,9 +8627,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", @@ -8315,6 +8637,33 @@ "node-cache": "^5.1.2" } }, + "node_modules/mongo-anywhere/node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "license": "MIT" + }, + "node_modules/mongo-anywhere/node_modules/minimongo": { + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/minimongo/-/minimongo-6.19.0.tgz", + "integrity": "sha512-DF2FLEJ6vTJSz7QsWrm2FMq2UR2yowwXPKhgQ69mxT5KOm/bYcrVv1RDw3zyeeYw/I7a/2MkiRWyV1EglniONA==", + "license": "LGPLv3", + "dependencies": { + "@turf/boolean-crosses": "^6.0.1", + "@turf/boolean-point-in-polygon": "^6.0.1", + "@turf/boolean-within": "^6.0.1", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/intersect": "^6.1.3", + "@turf/nearest-point-on-line": "^6.5.0", + "async": "^1.4.2", + "bowser": "^0.7.1", + "idb-wrapper": "^1.4.1", + "jquery": "^3.6.0", + "js-sha1": "^0.6.0", + "lodash": "^4.0.0" + } + }, "node_modules/mongodb": { "version": "5.9.2", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", @@ -8403,15 +8752,13 @@ "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.5.12", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.12.tgz", + "integrity": "sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==", "license": "MIT", "dependencies": { - "@next/env": "15.2.3", - "@swc/counter": "0.1.3", + "@next/env": "15.5.12", "@swc/helpers": "0.5.15", - "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -8423,19 +8770,19 @@ "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", - "sharp": "^0.33.5" + "@next/swc-darwin-arm64": "15.5.12", + "@next/swc-darwin-x64": "15.5.12", + "@next/swc-linux-arm64-gnu": "15.5.12", + "@next/swc-linux-arm64-musl": "15.5.12", + "@next/swc-linux-x64-gnu": "15.5.12", + "@next/swc-linux-x64-musl": "15.5.12", + "@next/swc-win32-arm64-msvc": "15.5.12", + "@next/swc-win32-x64-msvc": "15.5.12", + "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", + "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", @@ -8457,9 +8804,9 @@ } }, "node_modules/next-auth": { - "version": "4.24.11", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz", - "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", + "version": "4.24.13", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.13.tgz", + "integrity": "sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==", "license": "ISC", "dependencies": { "@babel/runtime": "^7.20.13", @@ -8473,9 +8820,9 @@ "uuid": "^8.3.2" }, "peerDependencies": { - "@auth/core": "0.34.2", - "next": "^12.2.5 || ^13 || ^14 || ^15", - "nodemailer": "^6.6.5", + "@auth/core": "0.34.3", + "next": "^12.2.5 || ^13 || ^14 || ^15 || ^16", + "nodemailer": "^7.0.7", "react": "^17.0.2 || ^18 || ^19", "react-dom": "^17.0.2 || ^18 || ^19" }, @@ -8551,10 +8898,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "license": "MIT" }, "node_modules/node-walker": { @@ -8566,30 +8912,14 @@ } }, "node_modules/nodemailer": { - "version": "6.9.16", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", - "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz", + "integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==", "license": "MIT-0", - "optional": true, - "peer": true, "engines": { "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", @@ -8912,6 +9242,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" @@ -8974,6 +9305,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" } @@ -9081,6 +9413,52 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.1.tgz", + "integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.1.tgz", + "integrity": "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==", + "devOptional": true, + "license": "Apache-2.0", + "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==", + "hasInstallScript": true, + "license": "MIT", + "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", @@ -9271,10 +9649,9 @@ } }, "node_modules/prettier": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.0.tgz", - "integrity": "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==", - "dev": true, + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -9290,6 +9667,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" }, @@ -9337,11 +9715,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", @@ -9377,9 +9750,10 @@ "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==" }, "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.5.tgz", + "integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.6" } @@ -9482,12 +9856,13 @@ } }, "node_modules/react-chartjs-2": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", - "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", "peerDependencies": { "chart.js": "^4.1.1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-dnd": { @@ -9575,9 +9950,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": "*" } @@ -9616,6 +9992,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" } @@ -9623,7 +10000,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", @@ -9790,19 +10168,6 @@ "resolved": "https://registry.npmjs.org/request-ip/-/request-ip-3.3.0.tgz", "integrity": "sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA==" }, - "node_modules/request/node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, "node_modules/request/node_modules/uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", @@ -9822,12 +10187,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.3.0", + "resolved": "https://registry.npmjs.org/resend/-/resend-4.3.0.tgz", + "integrity": "sha512-4OBHeusMVSl0vcba2J3AaGzdZ1SXAAhX/Wkcwobe16AHmlW9h3li8wG62Fhvlsc61e+wlQoxcwJZP6WrBTbghQ==", "license": "MIT", "dependencies": { - "@react-email/render": "1.0.1" + "@react-email/render": "1.0.6" }, "engines": { "node": ">=18" @@ -9911,9 +10276,10 @@ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" }, "node_modules/rollbar": { - "version": "2.26.4", - "resolved": "https://registry.npmjs.org/rollbar/-/rollbar-2.26.4.tgz", - "integrity": "sha512-JKmrj6riYm9ZPJisgxljgH4uCsvjMHDHXrinDF7aAFaP+eoF51HomVPtLcDTYLsrJ568aKVNLUhedFajONBwSg==", + "version": "2.26.5", + "resolved": "https://registry.npmjs.org/rollbar/-/rollbar-2.26.5.tgz", + "integrity": "sha512-4Of0ALl5+CU2glyDy5dWMRRy9Ty81DrY2r46ucbqjtCikbgHoWJNGXbQUWpDaLxsc8Q71LT/yj1bPb9NHbJIFQ==", + "license": "MIT", "dependencies": { "async": "~3.2.3", "console-polyfill": "0.3.0", @@ -9922,9 +10288,6 @@ "lru-cache": "~2.2.1", "request-ip": "~3.3.0", "source-map": "^0.5.7" - }, - "optionalDependencies": { - "decache": "^3.0.5" } }, "node_modules/rollbar/node_modules/lru-cache": { @@ -10037,6 +10400,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" }, @@ -10045,9 +10409,9 @@ } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10057,11 +10421,13 @@ } }, "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.5.4", + "resolved": "https://registry.npmjs.org/serwist/-/serwist-9.5.4.tgz", + "integrity": "sha512-uTHBzpIeA6rE3oyRt392MbtNQDs2JVZelKD1KkT18UkhX6HRwCeassoI1Nd1h52DqYqa7ZfBeldJ4awy+PYrnQ==", + "license": "MIT", "dependencies": { - "idb": "8.0.0" + "@serwist/utils": "9.5.4", + "idb": "8.0.3" }, "peerDependencies": { "typescript": ">=5.0.0" @@ -10104,16 +10470,16 @@ } }, "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", "optional": true, "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -10122,25 +10488,30 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/shebang-command": { @@ -10190,23 +10561,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "optional": true, - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT", - "optional": true - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -10258,15 +10612,16 @@ } }, "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", "dependencies": { - "ip": "^2.0.0", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, @@ -10357,14 +10712,6 @@ "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -10637,19 +10984,22 @@ } }, "node_modules/sucrase/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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/sucrase/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==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -10666,12 +11016,13 @@ } }, "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -10802,15 +11153,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10845,12 +11187,6 @@ "node": ">=12" } }, - "node_modules/traverse-chain": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", - "integrity": "sha512-up6Yvai4PYKhpNp5PkYtx50m3KbwQrqDwbuZP/ItyL64YEWHAvH6Md83LFLV/GRSk/BoUVwwgUzX6SOQSbsfAg==", - "optional": true - }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -11130,9 +11466,10 @@ } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11170,37 +11507,38 @@ } }, "node_modules/underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" }, "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": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -11664,9 +12002,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": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 9500f4bd..3b42e51d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sj3", - "version": "1.2.23", + "version": "1.3.6", "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,64 +21,66 @@ }, "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", "@yudiel/react-qr-scanner": "^2.2.1", - "bootstrap": "^5.3.3", + "bootstrap": "^5.3.5", "browser-image-compression": "^2.0.2", "bson": "^5.0.0", "dependencies": "^0.0.1", - "dotenv": "^16.4.7", + "dotenv": "^16.5.0", "eslint": "9.18.0", - "eslint-config-next": "15.2.2", - "formidable": "^3.5.2", - "jose": "^6.0.8", + "eslint-config-next": "15.2.4", + "formidable": "^3.5.4", + "jose": "^6.0.10", "levenary": "^1.1.1", - "minimongo": "^6.19.0", - "mongo-anywhere": "^1.1.11", + "minimongo": "^7.0.0", + "mongo-anywhere": "^1.1.15", "mongodb": "^5.0.0", - "next": "^15.2.3", + "next": "^15.2.6", "next-auth": "^4.24.11", "next-seo": "^6.6.0", + "nodemailer": "^7.0.13", "omit-call-signature": "^1.0.15", "react": "18.3.1", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^2.10.9", - "react-chartjs-2": "^5.2.0", + "react-chartjs-2": "^5.3.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "18.3.1", "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.3.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" + "typescript": "5.8.3", + "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.52.0", "@types/formidable": "^3.4.5", "@types/jest": "^29.5.14", - "@types/node": "^22.13.0", + "@types/node": "^22.19.3", "@types/react": "^18.3.8", "autoprefixer": "^10.4.21", "cross-env": "^7.0.3", "daisyui": "^4.12.22", "jest": "^29.7.0", "postcss": "^8.5.3", - "prettier": "3.5.0", - "serwist": "^9.0.11", + "prettier": "3.5.3", + "serwist": "^9.0.13", "tailwindcss": "^3.4.17", "ts-jest": "^29.2.5" } diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx index 91442621..ecb101e1 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx @@ -186,7 +186,7 @@ export default function CompetitionIndex({ if (!silent) setLoadingReports(true); - let newReports: Report[] = await api.competitionReports( + let { quantReports: newReports } = await api.competitionReports( comp?._id!, false, false, @@ -405,7 +405,13 @@ export default function CompetitionIndex({ >
- + { - const newReports = (await api.competitionReports( + const { quantReports: newReports } = await api.competitionReports( comp._id!, true, usePublicData, - )) as Report[]; + ); const rankings = await api.compRankings(comp.tbaId!); diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx index 6f342c26..6bee5996 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx @@ -1,5 +1,5 @@ import ClientApi from "@/lib/api/ClientApi"; -import { NotLinkedToTba } from "@/lib/client/ClientUtils"; +import { mergePitReports, NotLinkedToTba } from "@/lib/client/ClientUtils"; import { defaultGameId } from "@/lib/client/GameId"; import { Competition, @@ -41,6 +41,9 @@ export default function Stats(props: { >(props.subjectiveReports); const [page, setPage] = useState(0); const [usePublicData, setUsePublicData] = useState(true); + const [fetchedTeamNumbers, setFetchedTeamNumbers] = useState([]); + const [externalPitReportCount, setExternalPitReportCount] = useState(0); + const [internalPitReportCount, setInternalPitReportCount] = useState(0); useEffect(() => { const i = setInterval(() => { @@ -58,22 +61,39 @@ export default function Stats(props: { const promises = [ api .competitionReports(props.competition._id!, true, usePublicData) - .then((data) => setReports(data)), - pitReports.length === 0 && - props.competition._id && - api.getPitReports(props.competition._id).then((data) => { - setPitReports(data); + .then((data) => { + setReports(data.quantReports); + + const newPitReports: Pitreport[] = []; + let internalPitReportCount = 0, + externalPitReportCount = 0; + for (const teamPitReports of Object.values(data.pitReports)) { + newPitReports.push(mergePitReports(teamPitReports)); + + teamPitReports.forEach((r) => { + if (props.competition.pitReports.includes(r._id!.toString())) { + internalPitReportCount++; + } else { + externalPitReportCount++; + } + }); + } + + setPitReports(newPitReports); + setInternalPitReportCount(internalPitReportCount); + setExternalPitReportCount(externalPitReportCount); }), api .getSubjectiveReportsForComp(props.competition._id!) .then(setSubjectiveReports), + api.getTeamsAtComp(props.competition._id!).then(setFetchedTeamNumbers), ].flat(); await Promise.all(promises); setUpdate(Date.now()); setUpdating(false); - }, [pitReports.length, props.competition._id, usePublicData]); + }, [props.competition._id, props.competition.pitReports, usePublicData]); useEffect(() => { resync(); @@ -85,14 +105,15 @@ export default function Stats(props: { subjectiveReports.forEach((r) => Object.keys(r.robotComments).forEach((c) => teams.add(+c)), ); //+str converts to number + fetchedTeamNumbers.forEach((t) => teams.add(t)); - let internalReportCount = 0, - externalReportCount = 0; + let internalQuantReportCount = 0, + externalQuantReportCount = 0; reports.forEach((r) => { if (props.competition.matches.includes(r.match)) { - internalReportCount++; + internalQuantReportCount++; } else { - externalReportCount++; + externalQuantReportCount++; } }); @@ -110,12 +131,17 @@ export default function Stats(props: { > {usePublicData ? (
- Using public data ({internalReportCount} internal reports +{" "} - {externalReportCount} external reports) + Using public data ({internalQuantReportCount} internal quant + reports + {externalQuantReportCount} external quant reports,{" "} + {internalPitReportCount} internal pit reports +{" "} + {externalPitReportCount} external pit reports, and{" "} + {subjectiveReports.length} subjective reports)
) : (
- Not using public data ({internalReportCount} internal reports) + Not using public data ({internalQuantReportCount} internal + reports, {internalPitReportCount} internal pit reports,{" "} + {subjectiveReports.length} subjective reports)
)}
(Click to toggle)
@@ -170,6 +196,7 @@ export default function Stats(props: { {page === 0 && ( { return resolved; } + console.log(resolved); + return { props: { ...resolved, + season: serializeDatabaseObject(resolved.season), }, }; diff --git a/pages/[teamSlug]/[seasonSlug]/index.tsx b/pages/[teamSlug]/[seasonSlug]/index.tsx index 50c97d8b..f76edbaa 100644 --- a/pages/[teamSlug]/[seasonSlug]/index.tsx +++ b/pages/[teamSlug]/[seasonSlug]/index.tsx @@ -72,10 +72,13 @@ export default function Home(props: SeasonPageProps) { {owner && ( )}
diff --git a/pages/createTeam.tsx b/pages/createTeam.tsx index 0859d0ac..70c03fed 100644 --- a/pages/createTeam.tsx +++ b/pages/createTeam.tsx @@ -100,7 +100,10 @@ export default function CreateTeam() { >
+ {editingAvatar && ( + + )} ); } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..66c9923d --- /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 : 1, + repeatEach: process.env.CI ? 10 : 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/public/fields/DecodeBlue.png b/public/fields/DecodeBlue.png new file mode 100644 index 00000000..4ece8d32 Binary files /dev/null and b/public/fields/DecodeBlue.png differ diff --git a/public/fields/DecodeRed.png b/public/fields/DecodeRed.png new file mode 100644 index 00000000..4ece8d32 Binary files /dev/null and b/public/fields/DecodeRed.png differ diff --git a/public/fields/RebuiltBlue.png b/public/fields/RebuiltBlue.png new file mode 100644 index 00000000..68cf5423 Binary files /dev/null and b/public/fields/RebuiltBlue.png differ diff --git a/public/fields/RebuiltRed.png b/public/fields/RebuiltRed.png new file mode 100644 index 00000000..d6681949 Binary files /dev/null and b/public/fields/RebuiltRed.png differ diff --git a/styles/globals.css b/styles/globals.css index d1cd3c0e..df011e23 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -2,27 +2,26 @@ @tailwind components; @tailwind utilities; - @layer components { - .background-grid { - background-size: 50px 50px; - background-image: - linear-gradient(to right, white 1px, transparent 1px), - linear-gradient(to bottom, white 1px, transparent 1px); - } + .background-grid { + background-size: 50px 50px; + background-image: + linear-gradient(to right, white 1px, transparent 1px), + linear-gradient(to bottom, white 1px, transparent 1px); + } } @keyframes float { 0% { - box-shadow: 0 5px 15px 0px rgba(0,0,0,0.6); + box-shadow: 0 5px 15px 0px rgba(0, 0, 0, 0.6); transform: translatey(0px); } 50% { - box-shadow: 0 25px 15px 0px rgba(0,0,0,0.2); + box-shadow: 0 25px 15px 0px rgba(0, 0, 0, 0.2); transform: translatey(-20px); } 100% { - box-shadow: 0 5px 15px 0px rgba(0,0,0,0.6); + box-shadow: 0 5px 15px 0px rgba(0, 0, 0, 0.6); transform: translatey(0px); } } @@ -56,4 +55,4 @@ border-radius: 10px; } -*/ \ No newline at end of file +*/ 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 72% rename from tests/lib/client/ClientUtils.test.ts rename to tests/unit/lib/client/ClientUtils.test.ts index e91fdeef..01d93949 100644 --- a/tests/lib/client/ClientUtils.test.ts +++ b/tests/unit/lib/client/ClientUtils.test.ts @@ -1,5 +1,6 @@ import { camelCaseToTitleCase, + mergePitReports, promisify, removeDuplicates, removeWhitespaceAndMakeLowerCase, @@ -7,6 +8,8 @@ import { toDict, wait, } from "@/lib/client/ClientUtils"; +import { Motors } from "@/lib/Enums"; +import { Pitreport } from "@/lib/Types"; test(camelCaseToTitleCase.name, () => { expect(camelCaseToTitleCase("notLinkedToTba")).toBe("Not Linked To Tba"); @@ -113,3 +116,69 @@ describe(wait.name, () => { } }); }); + +describe(mergePitReports.name, () => { + test("Takes the most common value for each key in data", () => { + const reports: Pitreport[] = [ + { + teamNumber: 1, + data: { + motorType: Motors.CIMs, + }, + } as any, + { + teamNumber: 1, + data: { + motorType: Motors.CIMs, + }, + } as any, + { + teamNumber: 1, + data: { + motorType: Motors.Falcons, + }, + } as any, + ]; + + const merged = mergePitReports(reports); + + expect(merged).toEqual({ + teamNumber: 1, + data: { + motorType: Motors.CIMs, + }, + }); + }); + + test("Merges comments when only one report has a comment", () => { + const reports: Pitreport[] = [ + { + teamNumber: 1, + data: { + comments: "", + }, + } as any, + { + teamNumber: 1, + data: { + comments: "", + }, + } as any, + { + teamNumber: 1, + data: { + comments: "comment 3", + }, + } as any, + ]; + + const merged = mergePitReports(reports); + + expect(merged).toEqual({ + teamNumber: 1, + data: { + comments: "comment 3", + }, + }); + }); +}); 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