diff --git a/.gitignore b/.gitignore index 1ae1622..5c9b7bc 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ build/Release # Dependency directories node_modules/ jspm_packages/ +mongo-backup/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ diff --git a/README.fr-ca.md b/README.fr-ca.md new file mode 100644 index 0000000..0710768 --- /dev/null +++ b/README.fr-ca.md @@ -0,0 +1,30 @@ +[![CI/CD Pipeline for Frontend](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/frontend-deploy.yml/badge.svg)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/frontend-deploy.yml) +[![CI/CD Pipeline for Backend](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/backend-deploy.yml/badge.svg)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/backend-deploy.yml) +[![CI/CD Pipeline for Nginx Router](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/deploy.yml/badge.svg)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/deploy.yml) + + +[![en](https://img.shields.io/badge/lang-en-red.svg)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/blob/master/README.md) + +# EvalueTonSavoir + +EvalueTonSavoir est une plateforme open source et auto-hébergée qui poursuit le développement du code provenant de https://github.com/ETS-PFE004-Plateforme-sondage-minitest. Cette plateforme minimaliste est conçue comme un outil d'apprentissage et d'enseignement, offrant une solution simple et efficace pour la création de quiz utilisant le format GIFT, similaire à Moodle. + +## Fonctionnalités clés + +* Open Source et Auto-hébergé : Possédez et contrôlez vos données en déployant la plateforme sur votre propre infrastructure. +* Compatibilité GIFT : Créez des quiz facilement en utilisant le format GIFT, permettant une intégration transparente avec d'autres systèmes d'apprentissage. +* Minimaliste et Efficace : Une approche bare bones pour garantir la simplicité et la facilité d'utilisation, mettant l'accent sur l'essentiel de l'apprentissage. + +## Contribution + +Actuellement, il n'y a pas de modèle établi pour les contributions. Si vous constatez quelque chose de manquant ou si vous pensez qu'une amélioration est possible, n'hésitez pas à ouvrir un issue et/ou une PR) + +## Liens utiles + +* [Dépôt d'origine Frontend](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Frontend) +* [Dépôt d'origine Backend](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Backend) +* [Documentation (Wiki)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki) + +## License + +EvalueTonSavoir is open-sourced and licensed under the [MIT License](/LICENSE). diff --git a/README.md b/README.md index b935ea0..bb4f7dc 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,26 @@ [![CI/CD Pipeline for Backend](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/backend-deploy.yml/badge.svg)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/backend-deploy.yml) [![CI/CD Pipeline for Nginx Router](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/deploy.yml/badge.svg)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/deploy.yml) -# EvalueTonSavoir +[![fr-ca](https://img.shields.io/badge/lang-fr--ca-green.svg)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/blob/main/README.fr-ca.md) -EvalueTonSavoir est une plateforme open source et auto-hébergée qui poursuit le développement du code provenant de https://github.com/ETS-PFE004-Plateforme-sondage-minitest. Cette plateforme minimaliste est conçue comme un outil d'apprentissage et d'enseignement, offrant une solution simple et efficace pour la création de quiz utilisant le format GIFT, similaire à Moodle. +# EvalueTonSavoir -## Fonctionnalités clés +EvalueTonSavoir is an open-source and self-hosted platform that continues the development of the code from https://github.com/ETS-PFE004-Plateforme-sondage-minitest. This minimalist platform is designed as a learning and teaching tool, offering a simple and effective solution for creating quizzes using the GIFT format, similar to Moodle. -* Open Source et Auto-hébergé : Possédez et contrôlez vos données en déployant la plateforme sur votre propre infrastructure. -* Compatibilité GIFT : Créez des quiz facilement en utilisant le format GIFT, permettant une intégration transparente avec d'autres systèmes d'apprentissage. -* Minimaliste et Efficace : Une approche bare bones pour garantir la simplicité et la facilité d'utilisation, mettant l'accent sur l'essentiel de l'apprentissage. +## Key Features + +* **Open Source and Self-Hosted**: Own and control your data by deploying the platform on your own infrastructure. +* **GIFT Compatibility**: Easily create quizzes using the GIFT format, enabling seamless integration with other learning systems. +* **Minimalist and Efficient**: A bare-bones approach to ensure simplicity and ease of use, focusing on the essentials of learning. ## Contribution -Actuellement, il n'y a pas de modèle établi pour les contributions. Si vous constatez quelque chose de manquant ou si vous pensez qu'une amélioration est possible, n'hésitez pas à ouvrir un issue et/ou une PR) +Currently, there is no established model for contributions. If you notice something missing or think an improvement is possible, feel free to open an issue and/or a PR. -## Liens utiles +## Useful Links -* [Dépôt d'origine Frontend](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Frontend) -* [Dépôt d'origine Backend](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Backend) +* [Original Frontend Repository](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Frontend) +* [Original Backend Repository](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Backend) * [Documentation (Wiki)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki) ## License diff --git a/client/.env.example b/client/.env.example index 944dcbb..fa03bbb 100644 --- a/client/.env.example +++ b/client/.env.example @@ -1,2 +1,2 @@ VITE_BACKEND_URL=http://localhost:4400 -VITE_AZURE_BACKEND_URL=http://localhost:4400 \ No newline at end of file +VITE_AZURE_BACKEND_URL=http://localhost:4400 diff --git a/client/package-lock.json b/client/package-lock.json index 19cd5cd..ac8831e 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -14,19 +14,20 @@ "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.0", - "@mui/icons-material": "^6.4.6", + "@mui/icons-material": "^7.0.1", "@mui/lab": "^5.0.0-alpha.153", - "@mui/material": "^6.4.7", + "@mui/material": "^7.0.1", "@types/uuid": "^9.0.7", "axios": "^1.8.1", - "dompurify": "^3.2.3", - "esbuild": "^0.25.0", + "dompurify": "^3.2.5", + "esbuild": "^0.25.2", "gift-pegjs": "^2.0.0-beta.1", "jest-environment-jsdom": "^29.7.0", "jwt-decode": "^4.0.0", "katex": "^0.16.11", - "marked": "^14.1.2", - "nanoid": "^5.1.2", + "marked": "^15.0.8", + "nanoid": "^5.1.5", + "qrcode.react": "^4.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-modal": "^3.16.3", @@ -34,39 +35,40 @@ "remark-math": "^6.0.0", "socket.io-client": "^4.7.2", "ts-node": "^10.9.1", - "uuid": "^9.0.1", - "vite-plugin-checker": "^0.9.0" + "uuid": "^11.1.0", + "vite-plugin-checker": "^0.9.1" }, "devDependencies": { "@babel/preset-env": "^7.26.9", "@babel/preset-react": "^7.26.3", - "@babel/preset-typescript": "^7.23.3", - "@eslint/js": "^9.21.0", + "@babel/preset-typescript": "^7.27.0", + "@eslint/js": "^9.24.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.2.0", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/jest": "^29.5.13", - "@types/node": "^22.13.5", + "@types/node": "^22.14.0", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@types/react-latex": "^2.0.3", - "@typescript-eslint/eslint-plugin": "^8.25.0", - "@typescript-eslint/parser": "^8.25.0", - "@vitejs/plugin-react-swc": "^3.8.0", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "@vitejs/plugin-react-swc": "^3.8.1", "cross-env": "^7.0.3", - "eslint": "^9.21.0", + "eslint": "^9.24.0", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-jest": "^28.11.0", - "eslint-plugin-react": "^7.37.3", + "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.1.0-rc-206df66e-20240912", "eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-unused-imports": "^4.1.4", "globals": "^15.14.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", - "ts-jest": "^29.2.6", - "typescript": "^5.7.3", - "typescript-eslint": "^8.25.0", + "ts-jest": "^29.3.1", + "typescript": "^5.8.3", + "typescript-eslint": "^8.29.1", "vite": "^6.2.0", "vite-plugin-environment": "^1.1.3" } @@ -155,13 +157,12 @@ "license": "MIT" }, "node_modules/@babel/generator": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", - "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", "dependencies": { - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -201,18 +202,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz", - "integrity": "sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz", + "integrity": "sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/helper-replace-supers": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.26.9", + "@babel/traverse": "^7.27.0", "semver": "^6.3.1" }, "engines": { @@ -419,26 +419,24 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", - "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dependencies": { - "@babel/types": "^7.26.9" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1666,14 +1664,13 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.8.tgz", - "integrity": "sha512-bME5J9AC8ChwA7aEPJ6zym3w7aObZULHhbNLU0bKUhKsAkylkzUdq+0kdymh9rzi8nlNFl2bmldFBCKNJBUpuw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.0.tgz", + "integrity": "sha512-fRGGjO2UEGPjvEcyAZXRXAS8AfdaQoq7HnxAbJoAoW10B9xOKesmmndJv+Sym2a+9FHWZ9KbyyLCe9s0Sn5jtg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.27.0", "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/plugin-syntax-typescript": "^7.25.9" @@ -1873,17 +1870,16 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz", - "integrity": "sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.0.tgz", + "integrity": "sha512-vxaPFfJtHhgeOVXRKuHpHPAOgymmy8V8I65T1q53R7GCZlefKeCaTyDs3zOPHTTbmquvNlQYC5klEvWsBAtrBQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "@babel/plugin-syntax-jsx": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@babel/plugin-transform-typescript": "^7.25.9" + "@babel/plugin-transform-modules-commonjs": "^7.26.3", + "@babel/plugin-transform-typescript": "^7.27.0" }, "engines": { "node": ">=6.9.0" @@ -1893,10 +1889,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", - "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1905,30 +1900,28 @@ } }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", - "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.9", - "@babel/parser": "^7.26.9", - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1946,10 +1939,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", - "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" @@ -2134,13 +2126,12 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", "cpu": [ "ppc64" ], - "license": "MIT", "optional": true, "os": [ "aix" @@ -2150,13 +2141,12 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", "cpu": [ "arm" ], - "license": "MIT", "optional": true, "os": [ "android" @@ -2166,13 +2156,12 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "android" @@ -2182,13 +2171,12 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "android" @@ -2198,13 +2186,12 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -2214,13 +2201,12 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -2230,13 +2216,12 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -2246,13 +2231,12 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -2262,13 +2246,12 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", "cpu": [ "arm" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -2278,13 +2261,12 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -2294,13 +2276,12 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", "cpu": [ "ia32" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -2310,13 +2291,12 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", "cpu": [ "loong64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -2326,13 +2306,12 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", "cpu": [ "mips64el" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -2342,13 +2321,12 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", "cpu": [ "ppc64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -2358,13 +2336,12 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", "cpu": [ "riscv64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -2374,13 +2351,12 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", "cpu": [ "s390x" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -2390,13 +2366,12 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -2406,13 +2381,12 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "netbsd" @@ -2422,13 +2396,12 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "netbsd" @@ -2438,13 +2411,12 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "openbsd" @@ -2454,13 +2426,12 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "openbsd" @@ -2470,13 +2441,12 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "sunos" @@ -2486,13 +2456,12 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -2502,13 +2471,12 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", "cpu": [ "ia32" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -2518,13 +2486,12 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -2563,11 +2530,10 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "devOptional": true, - "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", @@ -2582,7 +2548,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "devOptional": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2593,7 +2558,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "devOptional": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2601,6 +2565,15 @@ "node": "*" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "devOptional": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", @@ -2615,11 +2588,10 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", - "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "devOptional": true, - "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -2643,7 +2615,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "devOptional": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2654,7 +2625,6 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=18" }, @@ -2667,7 +2637,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "devOptional": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2676,11 +2645,10 @@ } }, "node_modules/@eslint/js": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", - "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", + "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", "devOptional": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -2690,7 +2658,6 @@ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "devOptional": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -3397,22 +3364,20 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.7.tgz", - "integrity": "sha512-XjJrKFNt9zAKvcnoIIBquXyFyhfrHYuttqMsoDS7lM7VwufYG4fAPw4kINjBFg++fqXM2BNAuWR9J7XVIuKIKg==", - "license": "MIT", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.0.1.tgz", + "integrity": "sha512-T5DNVnSD9pMbj4Jk/Uphz+yvj9dfpl2+EqsOuJtG12HxEihNG5pd3qzX5yM1Id4dDwKRvM3dPVcxyzavTFhJeA==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" } }, "node_modules/@mui/icons-material": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.6.tgz", - "integrity": "sha512-rGJBvIQQbQAlyKYljHQ8wAQS/K2/uYwvemcpygnAmCizmCI4zSF9HQPuiG8Ql4YLZ6V/uKjA3WHIYmF/8sV+pQ==", - "license": "MIT", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.0.1.tgz", + "integrity": "sha512-x8Em7LISFQ6s/KeZj6ZKwJHq2WttRNe9KJLWFa72eQx7B53s/TzMKOEjGKB/YyhOx+bqqSv1pMvK373M4Xf07A==", "dependencies": { - "@babel/runtime": "^7.26.0" + "@babel/runtime": "^7.26.10" }, "engines": { "node": ">=14.0.0" @@ -3422,7 +3387,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^6.4.6", + "@mui/material": "^7.0.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -3474,16 +3439,15 @@ } }, "node_modules/@mui/material": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.7.tgz", - "integrity": "sha512-K65StXUeGAtFJ4ikvHKtmDCO5Ab7g0FZUu2J5VpoKD+O6Y3CjLYzRi+TMlI3kaL4CL158+FccMoOd/eaddmeRQ==", - "license": "MIT", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.0.1.tgz", + "integrity": "sha512-tQwjIIsn/UUSCHoCIQVkANuLua67h7Ro9M9gIHoGWaFbJFuF6cSO4Oda2olDVqIs4SWG+PaDChuu6SngxsaoyQ==", "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.4.7", - "@mui/system": "^6.4.7", - "@mui/types": "^7.2.21", - "@mui/utils": "^6.4.6", + "@babel/runtime": "^7.26.10", + "@mui/core-downloads-tracker": "^7.0.1", + "@mui/system": "^7.0.1", + "@mui/types": "^7.4.0", + "@mui/utils": "^7.0.1", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", @@ -3502,7 +3466,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.4.7", + "@mui/material-pigment-css": "^7.0.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -3523,13 +3487,12 @@ } }, "node_modules/@mui/material/node_modules/@mui/private-theming": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.6.tgz", - "integrity": "sha512-T5FxdPzCELuOrhpA2g4Pi6241HAxRwZudzAuL9vBvniuB5YU82HCmrARw32AuCiyTfWzbrYGGpZ4zyeqqp9RvQ==", - "license": "MIT", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.0.1.tgz", + "integrity": "sha512-1kQ7REYjjzDukuMfTbAjm3pLEhD7gUMC2bWhg9VD6f6sHzyokKzX0XHzlr3IdzNWBjPytGkzHpPIRQrUOoPLCQ==", "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.4.6", + "@babel/runtime": "^7.26.10", + "@mui/utils": "^7.0.1", "prop-types": "^15.8.1" }, "engines": { @@ -3550,12 +3513,11 @@ } }, "node_modules/@mui/material/node_modules/@mui/styled-engine": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.6.tgz", - "integrity": "sha512-vSWYc9ZLX46be5gP+FCzWVn5rvDr4cXC5JBZwSIkYk9xbC7GeV+0kCvB8Q6XLFQJy+a62bbqtmdwS4Ghi9NBlQ==", - "license": "MIT", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.0.1.tgz", + "integrity": "sha512-BeGe4xZmF7tESKhmctYrL54Kl25kGHPKVdZYM5qj5Xz76WM/poY+d8EmAqUesT6k2rbJWPp2gtOAXXinNCGunQ==", "dependencies": { - "@babel/runtime": "^7.26.0", + "@babel/runtime": "^7.26.10", "@emotion/cache": "^11.13.5", "@emotion/serialize": "^1.3.3", "@emotion/sheet": "^1.4.0", @@ -3584,16 +3546,15 @@ } }, "node_modules/@mui/material/node_modules/@mui/system": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.7.tgz", - "integrity": "sha512-7wwc4++Ak6tGIooEVA9AY7FhH2p9fvBMORT4vNLMAysH3Yus/9B9RYMbrn3ANgsOyvT3Z7nE+SP8/+3FimQmcg==", - "license": "MIT", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.0.1.tgz", + "integrity": "sha512-pK+puz0hRPHEKGlcPd80mKYD3jpyi0uVIwWffox1WZgPTQMw2dCKLcD+9ndMDJADnrKzmKlpoH756PPFh2UvWA==", "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.4.6", - "@mui/styled-engine": "^6.4.6", - "@mui/types": "^7.2.21", - "@mui/utils": "^6.4.6", + "@babel/runtime": "^7.26.10", + "@mui/private-theming": "^7.0.1", + "@mui/styled-engine": "^7.0.1", + "@mui/types": "^7.4.0", + "@mui/utils": "^7.0.1", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -3624,13 +3585,12 @@ } }, "node_modules/@mui/material/node_modules/@mui/utils": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.6.tgz", - "integrity": "sha512-43nZeE1pJF2anGafNydUcYFPtHwAqiBiauRtaMvurdrZI3YrUjHkAu43RBsxef7OFtJMXGiHFvq43kb7lig0sA==", - "license": "MIT", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.0.1.tgz", + "integrity": "sha512-SJKrrebNpmK9rJCnVL29nGPhPXQYtBZmb7Dsp0f58uIUhQfAKcBXHE4Kjs06SX4CwqeCuwEVgcHY+MgAO6XQ/g==", "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/types": "^7.2.21", + "@babel/runtime": "^7.26.10", + "@mui/types": "^7.4.0", "@types/prop-types": "^15.7.14", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -3753,10 +3713,12 @@ } }, "node_modules/@mui/types": { - "version": "7.2.21", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.21.tgz", - "integrity": "sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==", - "license": "MIT", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.0.tgz", + "integrity": "sha512-TxJ4ezEeedWHBjOmLtxI203a9DII9l4k83RXmz1PYSAmnyEcK2PglTNmJGxswC/wM5cdl9ap2h8lnXvt2swAGQ==", + "dependencies": { + "@babel/runtime": "^7.26.10" + }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -3801,7 +3763,6 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -3815,7 +3776,6 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, - "license": "MIT", "engines": { "node": ">= 8" } @@ -3825,7 +3785,6 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -4131,15 +4090,14 @@ "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.5.tgz", - "integrity": "sha512-EVY7zfpehxhTZXOfy508gb3D78ihoGGmvyiTWtlBPjgIaidP1Xw0naHMD78CWiFlZmeDjKXJufGtsEGOnZdmNA==", + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.18.tgz", + "integrity": "sha512-ORZxyCKKiqYt2iHdh1C7pfVR1GBjkuFOdwqZggQzaq0vt22DpGca+2JsUtkUoWQmWcct04v5+ScwgvsHuMObxA==", "devOptional": true, "hasInstallScript": true, - "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.19" + "@swc/types": "^0.1.21" }, "engines": { "node": ">=10" @@ -4149,16 +4107,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.11.5", - "@swc/core-darwin-x64": "1.11.5", - "@swc/core-linux-arm-gnueabihf": "1.11.5", - "@swc/core-linux-arm64-gnu": "1.11.5", - "@swc/core-linux-arm64-musl": "1.11.5", - "@swc/core-linux-x64-gnu": "1.11.5", - "@swc/core-linux-x64-musl": "1.11.5", - "@swc/core-win32-arm64-msvc": "1.11.5", - "@swc/core-win32-ia32-msvc": "1.11.5", - "@swc/core-win32-x64-msvc": "1.11.5" + "@swc/core-darwin-arm64": "1.11.18", + "@swc/core-darwin-x64": "1.11.18", + "@swc/core-linux-arm-gnueabihf": "1.11.18", + "@swc/core-linux-arm64-gnu": "1.11.18", + "@swc/core-linux-arm64-musl": "1.11.18", + "@swc/core-linux-x64-gnu": "1.11.18", + "@swc/core-linux-x64-musl": "1.11.18", + "@swc/core-win32-arm64-msvc": "1.11.18", + "@swc/core-win32-ia32-msvc": "1.11.18", + "@swc/core-win32-x64-msvc": "1.11.18" }, "peerDependencies": { "@swc/helpers": "*" @@ -4170,13 +4128,12 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.5.tgz", - "integrity": "sha512-GEd1hzEx0mSGkJYMFMGLnrGgjL2rOsOsuYWyjyiA3WLmhD7o+n/EWBDo6mzD/9aeF8dzSPC0TnW216gJbvrNzA==", + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.18.tgz", + "integrity": "sha512-K6AntdUlNMQg8aChqjeXwnVhK6d4WRZ9TgtLSTmdU0Ugll4an7QK49s9NrT7XQU91cEsVvzdr++p1bNImx0hJg==", "cpu": [ "arm64" ], - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" @@ -4186,13 +4143,12 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.5.tgz", - "integrity": "sha512-toz04z9wAClVvQSEY3xzrgyyeWBAfMWcKG4K0ugNvO56h/wczi2ZHRlnAXZW1tghKBk3z6MXqa/srfXgNhffKw==", + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.18.tgz", + "integrity": "sha512-RCRvC6Q9M5BArTvj/IzUAAYGrgxYFbTTnAtf6UX7JFq2DAn+hEwYUjmC1m0gFso9HqFU0m5QZUGfZvVmACGWUw==", "cpu": [ "x64" ], - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" @@ -4202,13 +4158,12 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.5.tgz", - "integrity": "sha512-5SjmKxXdwbBpsYGTpgeXOXMIjS563/ntRGn8Zc12H/c4VfPrRLGhgbJ/48z2XVFyBLcw7BCHZyFuVX1+ZI3W0Q==", + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.18.tgz", + "integrity": "sha512-wteAKf8YKb3jOnZFm3EzuIMzzCVXMuQOLHsz1IgEOc44/gdgNXKxaYTWAowZuej7t68tf/w0cRNMc7Le414v/g==", "cpu": [ "arm" ], - "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -4218,13 +4173,12 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.5.tgz", - "integrity": "sha512-pydIlInHRzRIwB0NHblz3Dx58H/bsi0I5F2deLf9iOmwPNuOGcEEZF1Qatc7YIjP5DFbXK+Dcz+pMUZb2cc2MQ==", + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.18.tgz", + "integrity": "sha512-hY6jJYZ6PKHSBo5OATswfyKsUgsWu9+4nDcN8liYIRRgz3E0G9wk0VUTP4cFPivBFeHWTTAGz687/Nf2aQEIpw==", "cpu": [ "arm64" ], - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -4234,13 +4188,12 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.5.tgz", - "integrity": "sha512-LhBHKjkZq5tJF1Lh0NJFpx7ROnCWLckrlIAIdSt9XfOV+zuEXJQOj+NFcM1eNk17GFfFyUMOZyGZxzYq5dveEQ==", + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.18.tgz", + "integrity": "sha512-slu0mlP2nucvQalttnapfpqpD/LlM9NHx9g3ofgsLzjObyMEBiX4ZysQ3y65U8Mjw71RNqtLd/ZmvxI6OmLdiQ==", "cpu": [ "arm64" ], - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -4250,13 +4203,12 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.5.tgz", - "integrity": "sha512-dCi4xkxXlsk5sQYb3i413Cfh7+wMJeBYTvBZTD5xh+/DgRtIcIJLYJ2tNjWC4/C2i5fj+Ze9bKNSdd8weRWZ3A==", + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.18.tgz", + "integrity": "sha512-h9a/8PA25arMCQ9t8CE8rA1s0c77z4kCZZ7dUuUkD88yEXIrARMca1IKR7of+S3slfQrf1Zlq3Ac1Fb1HVJziQ==", "cpu": [ "x64" ], - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -4266,13 +4218,12 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.5.tgz", - "integrity": "sha512-K0AC4TreM5Oo/tXNXnE/Gf5+5y/HwUdd7xvUjOpZddcX/RlsbYOKWLgOtA3fdFIuta7XC+vrGKmIhm5l70DSVQ==", + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.18.tgz", + "integrity": "sha512-0sMDJj5qUGK9QEw4lrxLxkTP/4AoKciqNzXvqbk+J9XuXN2aIv4BsR1Y7z3GwAeMFGsba2lbHLOtJlDsaqIsiA==", "cpu": [ "x64" ], - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -4282,13 +4233,12 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.5.tgz", - "integrity": "sha512-wzum8sYUsvPY7kgUfuqVYTgIPYmBC8KPksoNM1fz5UfhudU0ciQuYvUBD47GIGOevaoxhLkjPH4CB95vh1mJ9w==", + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.18.tgz", + "integrity": "sha512-zGv9HnfgBcKyt54MJRWdwRNu9BuYkAFM7bx+tWtKhd37Ef7ZX20QLs9xXl5wWDXCbsOdRxXIZgXs6PEL+Pzmrw==", "cpu": [ "arm64" ], - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -4298,13 +4248,12 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.5.tgz", - "integrity": "sha512-lco7mw0TPRTpVPR6NwggJpjdUkAboGRkLrDHjIsUaR+Y5+0m5FMMkHOMxWXAbrBS5c4ph7QErp4Lma4r9Mn5og==", + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.18.tgz", + "integrity": "sha512-uBKj0S1lYv/E2ZhxHZOxSiQwoegYmzbPRpjq6eHBZDv97mu7W3K27/lsnPbvAfQ6b6rnv8BI+EsmJ7VLQBAHBQ==", "cpu": [ "ia32" ], - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -4314,13 +4263,12 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.5.tgz", - "integrity": "sha512-E+DApLSC6JRK8VkDa4bNsBdD7Qoomx1HvKVZpOXl9v94hUZI5GMExl4vU5isvb+hPWL7rZ0NeI7ITnVLgLJRbA==", + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.18.tgz", + "integrity": "sha512-8USTRcdgeFMNBgvVXl8tz6n4+9s9m+zHsfDeBT4jPgwnq2bnLBlTUlwnPwzDxfg9nUJr6RFD4xeKfWyZZRosZg==", "cpu": [ "x64" ], - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -4333,15 +4281,13 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "devOptional": true, - "license": "Apache-2.0" + "devOptional": true }, "node_modules/@swc/types": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.19.tgz", - "integrity": "sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==", + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", + "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", "devOptional": true, - "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" } @@ -4371,7 +4317,6 @@ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", "dev": true, - "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", @@ -4409,11 +4354,10 @@ "license": "MIT" }, "node_modules/@testing-library/react": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.2.0.tgz", - "integrity": "sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", "dev": true, - "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5" }, @@ -4436,6 +4380,19 @@ } } }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -4665,12 +4622,11 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz", - "integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==", - "license": "MIT", + "version": "22.14.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", + "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/parse-json": { @@ -4771,17 +4727,16 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.25.0.tgz", - "integrity": "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.1.tgz", + "integrity": "sha512-ba0rr4Wfvg23vERs3eB+P3lfj2E+2g3lhWcCVukUuhtcdUx5lSIFZlGFEBHKr+3zizDa/TvZTptdNHVZWAkSBg==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.25.0", - "@typescript-eslint/type-utils": "8.25.0", - "@typescript-eslint/utils": "8.25.0", - "@typescript-eslint/visitor-keys": "8.25.0", + "@typescript-eslint/scope-manager": "8.29.1", + "@typescript-eslint/type-utils": "8.29.1", + "@typescript-eslint/utils": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -4797,20 +4752,19 @@ "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.25.0.tgz", - "integrity": "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.1.tgz", + "integrity": "sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.25.0", - "@typescript-eslint/types": "8.25.0", - "@typescript-eslint/typescript-estree": "8.25.0", - "@typescript-eslint/visitor-keys": "8.25.0", + "@typescript-eslint/scope-manager": "8.29.1", + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/typescript-estree": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1", "debug": "^4.3.4" }, "engines": { @@ -4822,18 +4776,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.25.0.tgz", - "integrity": "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.1.tgz", + "integrity": "sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.25.0", - "@typescript-eslint/visitor-keys": "8.25.0" + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4844,14 +4797,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.25.0.tgz", - "integrity": "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.1.tgz", + "integrity": "sha512-DkDUSDwZVCYN71xA4wzySqqcZsHKic53A4BLqmrWFFpOpNSoxX233lwGu/2135ymTCR04PoKiEEEvN1gFYg4Tw==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.25.0", - "@typescript-eslint/utils": "8.25.0", + "@typescript-eslint/typescript-estree": "8.29.1", + "@typescript-eslint/utils": "8.29.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -4864,15 +4816,14 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.25.0.tgz", - "integrity": "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.1.tgz", + "integrity": "sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -4882,14 +4833,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.25.0.tgz", - "integrity": "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.1.tgz", + "integrity": "sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.25.0", - "@typescript-eslint/visitor-keys": "8.25.0", + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -4905,7 +4855,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { @@ -4913,7 +4863,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -4922,16 +4871,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.25.0.tgz", - "integrity": "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.1.tgz", + "integrity": "sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.25.0", - "@typescript-eslint/types": "8.25.0", - "@typescript-eslint/typescript-estree": "8.25.0" + "@typescript-eslint/scope-manager": "8.29.1", + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/typescript-estree": "8.29.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4942,17 +4890,16 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.25.0.tgz", - "integrity": "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.1.tgz", + "integrity": "sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/types": "8.29.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -4968,7 +4915,6 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -4977,13 +4923,12 @@ } }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.8.0.tgz", - "integrity": "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.8.1.tgz", + "integrity": "sha512-aEUPCckHDcFyxpwFm0AIkbtv6PpUp3xTb9wYGFjtABynXjCYKkWoxX0AOK9NT9XCrdk6mBBUOeHQS+RKdcNO1A==", "dev": true, - "license": "MIT", "dependencies": { - "@swc/core": "^1.10.15" + "@swc/core": "^1.11.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" @@ -5023,7 +4968,6 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "devOptional": true, - "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -5057,7 +5001,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "devOptional": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5147,8 +5090,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "devOptional": true, - "license": "Python-2.0" + "devOptional": true }, "node_modules/aria-query": { "version": "5.3.0", @@ -5336,10 +5278,9 @@ } }, "node_modules/axios": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", - "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", - "license": "MIT", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -5654,14 +5595,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -6291,10 +6231,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", - "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", - "license": "(MPL-2.0 OR Apache-2.0)", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.5.tgz", + "integrity": "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==", "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -6608,11 +6547,10 @@ } }, "node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -6620,31 +6558,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" } }, "node_modules/escalade": { @@ -6701,18 +6639,18 @@ } }, "node_modules/eslint": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", - "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", + "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", "devOptional": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.0", "@eslint/core": "^0.12.0", - "@eslint/eslintrc": "^3.3.0", - "@eslint/js": "9.21.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.24.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -6724,7 +6662,7 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", + "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", @@ -6817,11 +6755,10 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", - "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, - "license": "MIT", "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -6833,7 +6770,7 @@ "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.8", + "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", @@ -6931,11 +6868,10 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "devOptional": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -7002,7 +6938,6 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "devOptional": true, - "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", @@ -7020,7 +6955,6 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "devOptional": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -7059,7 +6993,6 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "devOptional": true, - "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -7151,15 +7084,13 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "devOptional": true, - "license": "MIT" + "devOptional": true }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -7176,7 +7107,6 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -7203,7 +7133,6 @@ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, - "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -9507,7 +9436,6 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "devOptional": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -9589,8 +9517,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "devOptional": true, - "license": "MIT" + "devOptional": true }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -9834,10 +9761,9 @@ } }, "node_modules/marked": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.4.tgz", - "integrity": "sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg==", - "license": "MIT", + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.8.tgz", + "integrity": "sha512-rli4l2LyZqpQuRve5C0rkn6pj3hT8EWPC+zkAxFTAJLxRbENfTAhEQq9itrmf1Y81QtAX5D/MYlGlIomNgj9lA==", "bin": { "marked": "bin/marked.js" }, @@ -9957,7 +9883,6 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 8" } @@ -10494,7 +10419,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -10512,16 +10436,15 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.2.tgz", - "integrity": "sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "bin": { "nanoid": "bin/nanoid.js" }, @@ -10633,15 +10556,15 @@ } }, "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -11161,6 +11084,15 @@ ], "license": "MIT" }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -11185,8 +11117,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/react": { "version": "18.3.1", @@ -11546,7 +11477,6 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, - "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -11609,7 +11539,6 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -12359,11 +12288,10 @@ } }, "node_modules/ts-jest": { - "version": "29.2.6", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz", - "integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.1.tgz", + "integrity": "sha512-FT2PIRtZABwl6+ZCry8IY7JZ3xMuppsEV9qFVHOVe8jDzggwUZ9TsM4chyJxL9yi6LvkqcZYU3LmapEE454zBQ==", "dev": true, - "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", "ejs": "^3.1.10", @@ -12373,6 +12301,7 @@ "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.1", + "type-fest": "^4.38.0", "yargs-parser": "^21.1.1" }, "bin": { @@ -12420,6 +12349,18 @@ "node": ">=10" } }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.39.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.39.1.tgz", + "integrity": "sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -12577,10 +12518,9 @@ } }, "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==", - "license": "Apache-2.0", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12590,15 +12530,14 @@ } }, "node_modules/typescript-eslint": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.25.0.tgz", - "integrity": "sha512-TxRdQQLH4g7JkoFlYG3caW5v1S6kEkz8rqt80iQJZUYPq1zD1Ra7HfQBJJ88ABRaMvHAXnwRvRB4V+6sQ9xN5Q==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.29.1.tgz", + "integrity": "sha512-f8cDkvndhbQMPcysk6CUSGBWV+g1utqdn71P5YKwMumVMOG/5k7cHq0KyG4O52nB0oKS4aN2Tp5+wB4APJGC+w==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.25.0", - "@typescript-eslint/parser": "8.25.0", - "@typescript-eslint/utils": "8.25.0" + "@typescript-eslint/eslint-plugin": "8.29.1", + "@typescript-eslint/parser": "8.29.1", + "@typescript-eslint/utils": "8.29.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12609,7 +12548,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/unbox-primitive": { @@ -12632,10 +12571,9 @@ } }, "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==", - "license": "MIT" + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", @@ -12826,7 +12764,6 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "devOptional": true, - "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -12842,16 +12779,15 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], - "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { @@ -12911,10 +12847,9 @@ } }, "node_modules/vite": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", - "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", - "license": "MIT", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz", + "integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==", "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", @@ -12982,10 +12917,9 @@ } }, "node_modules/vite-plugin-checker": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.9.0.tgz", - "integrity": "sha512-gf/zc0KWX8ATEOgnpgAM1I+IbvWkkO80RB+FxlLtC5cabXSesbJmAUw6E+mMDDMGIT+VHAktmxJZpMTt3lSubQ==", - "license": "MIT", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.9.1.tgz", + "integrity": "sha512-neH3CSNWdkZ+zi+WPt/0y5+IO2I0UAI0NX6MaXqU/KxN1Lz6np/7IooRB6VVAMBa4nigqm1GRF6qNa4+EL5jDQ==", "dependencies": { "@babel/code-frame": "^7.26.2", "chokidar": "^4.0.3", @@ -13004,7 +12938,7 @@ "@biomejs/biome": ">=1.7", "eslint": ">=7", "meow": "^13.2.0", - "optionator": "^0.9.1", + "optionator": "^0.9.4", "stylelint": ">=16", "typescript": "*", "vite": ">=2.0.0", diff --git a/client/package.json b/client/package.json index 45352e6..aa4eecf 100644 --- a/client/package.json +++ b/client/package.json @@ -8,7 +8,7 @@ "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", - "test": "jest --colors", + "test": "jest --colors --silent", "test:watch": "jest --watch" }, "dependencies": { @@ -18,19 +18,20 @@ "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.0", - "@mui/icons-material": "^6.4.6", + "@mui/icons-material": "^7.0.1", "@mui/lab": "^5.0.0-alpha.153", - "@mui/material": "^6.4.7", + "@mui/material": "^7.0.1", "@types/uuid": "^9.0.7", "axios": "^1.8.1", - "dompurify": "^3.2.3", - "esbuild": "^0.25.0", + "dompurify": "^3.2.5", + "esbuild": "^0.25.2", "gift-pegjs": "^2.0.0-beta.1", "jest-environment-jsdom": "^29.7.0", "jwt-decode": "^4.0.0", "katex": "^0.16.11", - "marked": "^14.1.2", - "nanoid": "^5.1.2", + "marked": "^15.0.8", + "nanoid": "^5.1.5", + "qrcode.react": "^4.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-modal": "^3.16.3", @@ -38,39 +39,40 @@ "remark-math": "^6.0.0", "socket.io-client": "^4.7.2", "ts-node": "^10.9.1", - "uuid": "^9.0.1", - "vite-plugin-checker": "^0.9.0" + "uuid": "^11.1.0", + "vite-plugin-checker": "^0.9.1" }, "devDependencies": { "@babel/preset-env": "^7.26.9", "@babel/preset-react": "^7.26.3", - "@babel/preset-typescript": "^7.23.3", - "@eslint/js": "^9.21.0", + "@babel/preset-typescript": "^7.27.0", + "@eslint/js": "^9.24.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.2.0", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/jest": "^29.5.13", - "@types/node": "^22.13.5", + "@types/node": "^22.14.0", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@types/react-latex": "^2.0.3", - "@typescript-eslint/eslint-plugin": "^8.25.0", - "@typescript-eslint/parser": "^8.25.0", - "@vitejs/plugin-react-swc": "^3.8.0", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "@vitejs/plugin-react-swc": "^3.8.1", "cross-env": "^7.0.3", - "eslint": "^9.21.0", + "eslint": "^9.24.0", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-jest": "^28.11.0", - "eslint-plugin-react": "^7.37.3", + "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.1.0-rc-206df66e-20240912", "eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-unused-imports": "^4.1.4", "globals": "^15.14.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", - "ts-jest": "^29.2.6", - "typescript": "^5.7.3", - "typescript-eslint": "^8.25.0", + "ts-jest": "^29.3.1", + "typescript": "^5.8.3", + "typescript-eslint": "^8.29.1", "vite": "^6.2.0", "vite-plugin-environment": "^1.1.3" } diff --git a/client/src/Types/Images.tsx b/client/src/Types/Images.tsx new file mode 100644 index 0000000..8cfe170 --- /dev/null +++ b/client/src/Types/Images.tsx @@ -0,0 +1,17 @@ +export interface Images { + id: string; + file_content: string; + file_name: string; + mime_type: string; +} + +export interface ImagesResponse { + images: Images[]; + total: number; +} + +export interface ImagesParams { + page: number; + limit: number; + uid?: string; +} \ No newline at end of file diff --git a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResults.test.tsx b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResults.test.tsx index 3431b73..b9b6b9f 100644 --- a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResults.test.tsx +++ b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResults.test.tsx @@ -19,8 +19,8 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => }); const mockStudents: StudentType[] = [ - { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] }, - { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] }, + { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] }, + { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] }, ]; const mockShowSelectedQuestion = jest.fn(); @@ -92,4 +92,4 @@ describe('LiveResults', () => { expect(mockShowSelectedQuestion).toHaveBeenCalled(); }); -}); \ No newline at end of file +}); diff --git a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/LiveResultsTable.test.tsx b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/LiveResultsTable.test.tsx index d6f41d6..fe26173 100644 --- a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/LiveResultsTable.test.tsx +++ b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/LiveResultsTable.test.tsx @@ -20,8 +20,8 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => const mockStudents: StudentType[] = [ - { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] }, - { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] }, + { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] }, + { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] }, ]; const mockShowSelectedQuestion = jest.fn(); diff --git a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody.test.tsx b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody.test.tsx index 9d4fb5c..ce10e1b 100644 --- a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody.test.tsx +++ b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody.test.tsx @@ -20,8 +20,8 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => }); const mockStudents: StudentType[] = [ - { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] }, - { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] }, + { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] }, + { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] }, ]; const mockGetStudentGrade = jest.fn((student: StudentType) => { diff --git a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableFooter.test.tsx b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableFooter.test.tsx index 99a6dc3..dd7b54a 100644 --- a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableFooter.test.tsx +++ b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableFooter.test.tsx @@ -6,8 +6,8 @@ import LiveResultsTableFooter from 'src/components/LiveResults/LiveResultsTable/ const mockStudents: StudentType[] = [ - { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] }, - { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] }, + { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] }, + { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] }, ]; const mockGetStudentGrade = jest.fn((student: StudentType) => { @@ -52,4 +52,4 @@ describe('LiveResultsTableFooter', () => { expect(screen.getByText('50 %')).toBeInTheDocument(); }); -}); \ No newline at end of file +}); diff --git a/client/src/__tests__/components/GiftTemplate/templates/MultipleChoice.test.tsx b/client/src/__tests__/components/GiftTemplate/templates/MultipleChoice.test.tsx index bdecb06..2770267 100644 --- a/client/src/__tests__/components/GiftTemplate/templates/MultipleChoice.test.tsx +++ b/client/src/__tests__/components/GiftTemplate/templates/MultipleChoice.test.tsx @@ -29,7 +29,7 @@ const katekMock: TemplateOptions & MultipleChoiceQuestion = { formattedStem: { format: 'plain' , text: '$$\\frac{zzz}{yyy}$$'}, choices: [ { formattedText: { format: 'plain' , text: 'Choice 1'}, isCorrect: true, formattedFeedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }, - { formattedText: { format: 'plain', text: 'Choice 2' }, isCorrect: true, formattedFeedback: { format: 'plain' , text: 'Correct!'}, weight: 1 } + { formattedText: { format: 'plain', text: 'Choice 2' }, isCorrect: false, formattedFeedback: { format: 'plain' , text: 'Correct!'}, weight: 0 } ], formattedGlobalFeedback: { format: 'plain', text: 'Sample Global Feedback' } }; diff --git a/client/src/__tests__/components/GiftTemplate/templates/__snapshots__/MultipleChoice.test.tsx.snap b/client/src/__tests__/components/GiftTemplate/templates/__snapshots__/MultipleChoice.test.tsx.snap index 0a17bf6..d32efd4 100644 --- a/client/src/__tests__/components/GiftTemplate/templates/__snapshots__/MultipleChoice.test.tsx.snap +++ b/client/src/__tests__/components/GiftTemplate/templates/__snapshots__/MultipleChoice.test.tsx.snap @@ -733,7 +733,7 @@ exports[`MultipleChoice snapshot test with katex 1`] = ` <div class='multiple-choice-answers-container'> <input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"> - <span class="answer-weight-container answer-positive-weight">1%</span> + <label style=" display: inline-block; padding: 0.2em 0 0.2em 0; @@ -742,15 +742,15 @@ exports[`MultipleChoice snapshot test with katex 1`] = ` " for="idmocked-id"> Choice 2 </label> - <svg data-testid="correct-icon" style=" + <svg data-testid="incorrect-icon" style=" vertical-align: text-bottom; display: inline-block; margin-left: 0.1rem; margin-right: 0.2rem; - width: 1em; - color: hsl(120, 39%, 54%); - " role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg> + width: 0.75em; + color: hsl(2, 64%, 58%); + " role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg> <span class="feedback-container">Correct!</span> </input> </div> diff --git a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx new file mode 100644 index 0000000..5586a6a --- /dev/null +++ b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx @@ -0,0 +1,147 @@ +import React, { act } from "react"; +import "@testing-library/jest-dom"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import ImageGallery from "../../../components/ImageGallery/ImageGallery"; +import ApiService from "../../../services/ApiService"; +import { Images } from "../../../Types/Images"; +import userEvent from "@testing-library/user-event"; + +jest.mock("../../../services/ApiService"); + +const mockImages: Images[] = [ + { id: "1", file_name: "image1.jpg", mime_type: "image/jpeg", file_content: "mockBase64Content1" }, + { id: "2", file_name: "image2.jpg", mime_type: "image/jpeg", file_content: "mockBase64Content2" }, + { id: "3", file_name: "image3.jpg", mime_type: "image/jpeg", file_content: "mockBase64Content3" }, +]; + +beforeAll(() => { + global.URL.createObjectURL = jest.fn(() => 'mockedObjectUrl'); + Object.assign(navigator, { + clipboard: { + writeText: jest.fn(), + }, + }); +}); + +describe("ImageGallery", () => { + let mockHandleDelete: jest.Mock; + + beforeEach(async () => { + (ApiService.getUserImages as jest.Mock).mockResolvedValue({ images: mockImages, total: 3 }); + (ApiService.deleteImage as jest.Mock).mockResolvedValue(true); + (ApiService.uploadImage as jest.Mock).mockResolvedValue('mockImageUrl'); + await act(async () => { + render(); + }); + mockHandleDelete = jest.fn(); + }); + + it("should render images correctly", async () => { + await act(async () => { + await screen.findByText("Galerie"); + }); + + expect(screen.getByAltText("Image image1.jpg")).toBeInTheDocument(); + expect(screen.getByAltText("Image image2.jpg")).toBeInTheDocument(); + }); + + it("should handle copy action", async () => { + const handleCopyMock = jest.fn(); + await act(async () => { + render(); + }); + + const copyButtons = await waitFor(() => screen.findAllByTestId(/gallery-tab-copy-/)); + await act(async () => { + fireEvent.click(copyButtons[0]); + }); + + expect(navigator.clipboard.writeText).toHaveBeenCalled(); + }); + + it("should delete an image and update the gallery", async () => { + const fetchImagesMock = jest.fn().mockResolvedValue({ images: mockImages.filter((image) => image.id !== "1"), total: 2 }); + + (ApiService.getUserImages as jest.Mock).mockImplementation(fetchImagesMock); + + await act(async () => { + render(); + }); + + await act(async () => { + await screen.findByAltText("Image image1.jpg"); + }); + + const deleteButtons = await waitFor(() => screen.findAllByTestId(/gallery-tab-delete-/)); + fireEvent.click(deleteButtons[0]); + + await waitFor(() => { + expect(screen.getByText("Voulez-vous supprimer cette image?")).toBeInTheDocument(); + }); + + const confirmDeleteButton = screen.getByText("Delete"); + await act(async () => { + fireEvent.click(confirmDeleteButton); + }); + + await waitFor(() => { + expect(ApiService.deleteImage).toHaveBeenCalledWith("1"); + }); + + await waitFor(() => { + expect(screen.queryByAltText("Image image1.jpg")).toBeNull(); + expect(screen.getByText("Image supprimée avec succès!")).toBeInTheDocument(); + }); + }); + + it("should upload an image and display success message", async () => { + const importTab = screen.getByRole("tab", { name: /import/i }); + fireEvent.click(importTab); + + const fileInputs = await screen.findAllByTestId("file-input"); + const fileInput = fileInputs[1]; + + expect(fileInput).toBeInTheDocument(); + + const file = new File(["image"], "image.jpg", { type: "image/jpeg" }); + await userEvent.upload(fileInput, file); + + + await waitFor(() => screen.getByAltText("Preview")); + const previewImage = screen.getByAltText("Preview"); + + expect(previewImage).toBeInTheDocument(); + + const uploadButton = screen.getByRole('button', { name: /téléverser/i }); + fireEvent.click(uploadButton); + const successMessage = await screen.findByText(/téléversée avec succès/i); + expect(successMessage).toBeInTheDocument(); + }); + + it("should close the image preview dialog when close button is clicked", async () => { + const imageCard = screen.getByAltText("Image image1.jpg"); + fireEvent.click(imageCard); + + const dialogImage = await screen.findByAltText("Enlarged view"); + expect(dialogImage).toBeInTheDocument(); + + const closeButton = screen.getByTestId("close-button"); + fireEvent.click(closeButton); + + await waitFor(() => { + expect(screen.queryByAltText("Enlarged view")).not.toBeInTheDocument(); + }); + }); + + it("should show an error message when no file is selected", async () => { + const importTab = screen.getByRole("tab", { name: /import/i }); + fireEvent.click(importTab); + const uploadButton = screen.getByRole('button', { name: /téléverser/i }); + fireEvent.click(uploadButton); + + await waitFor(() => { + expect(screen.getByText("Veuillez choisir une image à téléverser.")).toBeInTheDocument(); + }); + }); + +}); diff --git a/client/src/__tests__/components/ImageGallery/ImageGalleryModal.test.tsx b/client/src/__tests__/components/ImageGallery/ImageGalleryModal.test.tsx new file mode 100644 index 0000000..f5a65a6 --- /dev/null +++ b/client/src/__tests__/components/ImageGallery/ImageGalleryModal.test.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import ImageGalleryModal from "../../../components/ImageGallery/ImageGalleryModal/ImageGalleryModal"; +import "@testing-library/jest-dom"; + +jest.mock("../../../components/ImageGallery/ImageGallery", () => ({ + __esModule: true, + default: jest.fn(() =>
), +})); + +describe("ImageGalleryModal", () => { + + it("renders button correctly", () => { + render(); + + const button = screen.getByLabelText(/images-open/i); + expect(button).toBeInTheDocument(); + }); + + it("opens the modal when button is clicked", () => { + render(); + + const button = screen.getByRole("button", { name: /images/i }); + fireEvent.click(button); + + const dialog = screen.getByRole("dialog"); + expect(dialog).toBeInTheDocument(); + }); + + + it("closes the modal when close button is clicked", async () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: /images/i })); + + const closeButton = screen.getByRole("button", { name: /close/i }); + fireEvent.click(closeButton); + + await waitFor(() => { + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); + +}); diff --git a/client/src/__tests__/components/LiveResults/LiveResults.test.tsx b/client/src/__tests__/components/LiveResults/LiveResults.test.tsx index ce6244e..269d83b 100644 --- a/client/src/__tests__/components/LiveResults/LiveResults.test.tsx +++ b/client/src/__tests__/components/LiveResults/LiveResults.test.tsx @@ -5,7 +5,7 @@ import LiveResults from 'src/components/LiveResults/LiveResults'; import { QuestionType } from 'src/Types/QuestionType'; import { StudentType } from 'src/Types/StudentType'; import { Socket } from 'socket.io-client'; -import { BaseQuestion,parse } from 'gift-pegjs'; +import { BaseQuestion, parse } from 'gift-pegjs'; const mockSocket: Socket = { on: jest.fn(), @@ -19,19 +19,28 @@ const mockGiftQuestions = parse( `::Sample Question 1:: Question stem { =Choice 1 - ~Choice 2 - }`); + =Choice 2 + ~Choice 3 + ~Choice 4 + } + + ::Sample Question 2:: Question stem {TRUE} + `); const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => { if (question.type !== "Category") question.id = (index + 1).toString(); const newMockQuestion = question; - return {question : newMockQuestion as BaseQuestion}; + return { question: newMockQuestion as BaseQuestion }; }); +console.log(`mockQuestions: ${JSON.stringify(mockQuestions)}`); + +// each student should have a different score for the tests to pass const mockStudents: StudentType[] = [ - { id: '1', name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Choice 1', isCorrect: true }] }, - { id: '2', name: 'Student 2', answers: [{ idQuestion: 1, answer: 'Choice 2', isCorrect: false }] }, + { id: '1', name: 'Student 1', answers: [] }, + { id: '2', name: 'Student 2', answers: [{ idQuestion: 1, answer: ['Choice 3'], isCorrect: false }, { idQuestion: 2, answer: [true], isCorrect: true}] }, + { id: '3', name: 'Student 3', answers: [{ idQuestion: 1, answer: ['Choice 1', 'Choice 2'], isCorrect: true }, { idQuestion: 2, answer: [true], isCorrect: true}] }, ]; describe('LiveResults', () => { @@ -52,7 +61,7 @@ describe('LiveResults', () => { // Toggle the display of usernames back fireEvent.click(toggleUsernamesSwitch); - + // Check if the component renders the students mockStudents.forEach((student) => { expect(screen.getByText(student.name)).toBeInTheDocument(); @@ -82,82 +91,88 @@ describe('LiveResults', () => { }); }); -}); -test('calculates and displays the correct student grades', () => { - render( - - ); + test('calculates and displays the correct student grades', () => { + render( + + ); - // Toggle the display of usernames - const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms'); + // Toggle the display of usernames + const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms'); - // Toggle the display of usernames back - fireEvent.click(toggleUsernamesSwitch); - - // Check if the student grades are calculated and displayed correctly - mockStudents.forEach((student) => { - const grade = student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100; - expect(screen.getByText(`${grade.toFixed()} %`)).toBeInTheDocument(); + // Toggle the display of usernames back + fireEvent.click(toggleUsernamesSwitch); + + // Check if the student grades are calculated and displayed correctly + const getByTextInTableCellBody = (text: string) => { + const elements = screen.getAllByText(text); // Get all elements with the specified text + return elements.find((element) => element.closest('.MuiTableCell-body')); // don't get the footer element(s) + }; + mockStudents.forEach((student) => { + const grade = student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100; + const element = getByTextInTableCellBody(`${grade.toFixed()} %`); + expect(element).toBeInTheDocument(); + }); }); -}); -test('calculates and displays the class average', () => { - render( - - ); + test('calculates and displays the class average', () => { + render( + + ); - // Toggle the display of usernames - const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms'); + // Toggle the display of usernames + const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms'); - // Toggle the display of usernames back - fireEvent.click(toggleUsernamesSwitch); - - // Calculate the class average - const totalGrades = mockStudents.reduce((total, student) => { - return total + (student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100); - }, 0); - const classAverage = totalGrades / mockStudents.length; + // Toggle the display of usernames back + fireEvent.click(toggleUsernamesSwitch); - // Check if the class average is displayed correctly - const classAverageElements = screen.getAllByText(`${classAverage.toFixed()} %`); - const classAverageElement = classAverageElements.find((element) => { - return element.closest('td')?.classList.contains('MuiTableCell-footer'); - }); - expect(classAverageElement).toBeInTheDocument(); -}); + // Calculate the class average + const totalGrades = mockStudents.reduce((total, student) => { + return total + (student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100); + }, 0); + const classAverage = totalGrades / mockStudents.length; -test('displays the correct answers per question', () => { - render( - - ); - - // Check if the correct answers per question are displayed correctly - mockQuestions.forEach((_, index) => { - const correctAnswers = mockStudents.filter(student => student.answers.some(answer => answer.idQuestion === index + 1 && answer.isCorrect)).length; - const correctAnswersPercentage = (correctAnswers / mockStudents.length) * 100; - const correctAnswersElements = screen.getAllByText(`${correctAnswersPercentage.toFixed()} %`); - const correctAnswersElement = correctAnswersElements.find((element) => { - return element.closest('td')?.classList.contains('MuiTableCell-root'); + // Check if the class average is displayed correctly + const classAverageElements = screen.getAllByText(`${classAverage.toFixed()} %`); + const classAverageElement = classAverageElements.find((element) => { + return element.closest('td')?.classList.contains('MuiTableCell-footer'); }); - expect(correctAnswersElement).toBeInTheDocument(); + expect(classAverageElement).toBeInTheDocument(); }); -}); \ No newline at end of file + + test('displays the correct answers per question', () => { + render( + + ); + + // Check if the correct answers per question are displayed correctly + mockQuestions.forEach((_, index) => { + const correctAnswers = mockStudents.filter(student => student.answers.some(answer => answer.idQuestion === index + 1 && answer.isCorrect)).length; + const correctAnswersPercentage = (correctAnswers / mockStudents.length) * 100; + const correctAnswersElements = screen.getAllByText(`${correctAnswersPercentage.toFixed()} %`); + const correctAnswersElement = correctAnswersElements.find((element) => { + return element.closest('td')?.classList.contains('MuiTableCell-root'); + }); + expect(correctAnswersElement).toBeInTheDocument(); + }); + }); + +}); diff --git a/client/src/__tests__/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.test.tsx b/client/src/__tests__/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.test.tsx index 8751e8b..45e9b0a 100644 --- a/client/src/__tests__/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.test.tsx +++ b/client/src/__tests__/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.test.tsx @@ -12,14 +12,23 @@ const questions = parse( { =Choice 1 ~Choice 2 - }`) as MultipleChoiceQuestion[]; + } + + ::Sample Question 2:: Question stem + { + =Choice 1 + =Choice 2 + ~Choice 3 + } + `) as MultipleChoiceQuestion[]; -const question = questions[0]; +const questionWithOneCorrectChoice = questions[0]; +const questionWithMultipleCorrectChoices = questions[1]; describe('MultipleChoiceQuestionDisplay', () => { const mockHandleOnSubmitAnswer = jest.fn(); - const TestWrapper = ({ showAnswer }: { showAnswer: boolean }) => { + const TestWrapper = ({ showAnswer, question }: { showAnswer: boolean; question: MultipleChoiceQuestion }) => { const [showAnswerState, setShowAnswerState] = useState(showAnswer); const handleOnSubmitAnswer = (answer: AnswerType) => { @@ -38,28 +47,51 @@ describe('MultipleChoiceQuestionDisplay', () => { ); }; - const choices = question.choices; + const twoChoices = questionWithOneCorrectChoice.choices; + const threeChoices = questionWithMultipleCorrectChoices.choices; - beforeEach(() => { - render(); - }); + test('renders a question (that has only one correct choice) and its choices', () => { + render(); - test('renders the question and choices', () => { - expect(screen.getByText(question.formattedStem.text)).toBeInTheDocument(); - choices.forEach((choice) => { + expect(screen.getByText(questionWithOneCorrectChoice.formattedStem.text)).toBeInTheDocument(); + twoChoices.forEach((choice) => { expect(screen.getByText(choice.formattedText.text)).toBeInTheDocument(); }); }); + test('only allows one choice to be selected when question only has one correct answer', () => { + render(); + + const choiceButton1 = screen.getByText('Choice 1').closest('button'); + const choiceButton2 = screen.getByText('Choice 2').closest('button'); + + if (!choiceButton1 || !choiceButton2) throw new Error('Choice buttons not found'); + + // Simulate selecting multiple answers + act(() => { + fireEvent.click(choiceButton1); + }); + act(() => { + fireEvent.click(choiceButton2); + }); + + // Verify that only the last answer is selected + expect(choiceButton1.querySelector('.answer-text.selected')).not.toBeInTheDocument(); + expect(choiceButton2.querySelector('.answer-text.selected')).toBeInTheDocument(); + }); + test('does not submit when no answer is selected', () => { + render(); const submitButton = screen.getByText('Répondre'); act(() => { fireEvent.click(submitButton); }); expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled(); + mockHandleOnSubmitAnswer.mockClear(); }); test('submits the selected answer', () => { + render(); const choiceButton = screen.getByText('Choice 1').closest('button'); if (!choiceButton) throw new Error('Choice button not found'); act(() => { @@ -70,10 +102,68 @@ describe('MultipleChoiceQuestionDisplay', () => { fireEvent.click(submitButton); }); - expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith('Choice 1'); + expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(['Choice 1']); + mockHandleOnSubmitAnswer.mockClear(); + }); + + + test('renders a question (that has multiple correct choices) and its choices', () => { + render(); + expect(screen.getByText(questionWithMultipleCorrectChoices.formattedStem.text)).toBeInTheDocument(); + threeChoices.forEach((choice) => { + expect(screen.getByText(choice.formattedText.text)).toBeInTheDocument(); + }); + }); + + test('allows multiple choices to be selected when question has multiple correct answers', () => { + render(); + const choiceButton1 = screen.getByText('Choice 1').closest('button'); + const choiceButton2 = screen.getByText('Choice 2').closest('button'); + const choiceButton3 = screen.getByText('Choice 3').closest('button'); + + if (!choiceButton1 || !choiceButton2 || !choiceButton3) throw new Error('Choice buttons not found'); + + act(() => { + fireEvent.click(choiceButton1); + }); + act(() => { + fireEvent.click(choiceButton2); + }); + + expect(choiceButton1.querySelector('.answer-text.selected')).toBeInTheDocument(); + expect(choiceButton2.querySelector('.answer-text.selected')).toBeInTheDocument(); + expect(choiceButton3.querySelector('.answer-text.selected')).not.toBeInTheDocument(); // didn't click + + }); + + test('submits multiple selected answers', () => { + render(); + const choiceButton1 = screen.getByText('Choice 1').closest('button'); + const choiceButton2 = screen.getByText('Choice 2').closest('button'); + + if (!choiceButton1 || !choiceButton2) throw new Error('Choice buttons not found'); + + // Simulate selecting multiple answers + act(() => { + fireEvent.click(choiceButton1); + }); + act(() => { + fireEvent.click(choiceButton2); + }); + + // Simulate submitting the answers + const submitButton = screen.getByText('Répondre'); + act(() => { + fireEvent.click(submitButton); + }); + + // Verify that the mockHandleOnSubmitAnswer function is called with both answers + expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(['Choice 1', 'Choice 2']); + mockHandleOnSubmitAnswer.mockClear(); }); it('should show ✅ next to the correct answer and ❌ next to the wrong answers when showAnswer is true', async () => { + render(); const choiceButton = screen.getByText('Choice 1').closest('button'); if (!choiceButton) throw new Error('Choice button not found'); @@ -89,16 +179,17 @@ describe('MultipleChoiceQuestionDisplay', () => { }); // Wait for the DOM to update - const correctAnswer = screen.getByText("Choice 1").closest('button'); - expect(correctAnswer).toBeInTheDocument(); - expect(correctAnswer?.textContent).toContain('✅'); + const correctAnswer = screen.getByText("Choice 1").closest('button'); + expect(correctAnswer).toBeInTheDocument(); + expect(correctAnswer?.textContent).toContain('✅'); - const wrongAnswer1 = screen.getByText("Choice 2").closest('button'); - expect(wrongAnswer1).toBeInTheDocument(); - expect(wrongAnswer1?.textContent).toContain('❌'); + const wrongAnswer1 = screen.getByText("Choice 2").closest('button'); + expect(wrongAnswer1).toBeInTheDocument(); + expect(wrongAnswer1?.textContent).toContain('❌'); }); - it('should not show ✅ or ❌ when repondre button is not clicked', async () => { + it('should not show ✅ or ❌ when Répondre button is not clicked', async () => { + render(); const choiceButton = screen.getByText('Choice 1').closest('button'); if (!choiceButton) throw new Error('Choice button not found'); @@ -118,5 +209,5 @@ describe('MultipleChoiceQuestionDisplay', () => { expect(wrongAnswer1?.textContent).not.toContain('❌'); }); - }); +}); diff --git a/client/src/__tests__/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.test.tsx b/client/src/__tests__/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.test.tsx index 639537a..5c32547 100644 --- a/client/src/__tests__/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.test.tsx +++ b/client/src/__tests__/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.test.tsx @@ -67,6 +67,7 @@ describe('NumericalQuestion Component', () => { fireEvent.click(submitButton); expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled(); + mockHandleOnSubmitAnswer.mockClear(); }); it('submits answer correctly', () => { @@ -77,6 +78,7 @@ describe('NumericalQuestion Component', () => { fireEvent.click(submitButton); - expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(7); + expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith([7]); + mockHandleOnSubmitAnswer.mockClear(); }); }); diff --git a/client/src/__tests__/components/QuestionsDisplay/Question.test.tsx b/client/src/__tests__/components/QuestionsDisplay/Question.test.tsx index 8c7546f..142e563 100644 --- a/client/src/__tests__/components/QuestionsDisplay/Question.test.tsx +++ b/client/src/__tests__/components/QuestionsDisplay/Question.test.tsx @@ -29,23 +29,24 @@ describe('Questions Component', () => { render(); }; - describe('question type parsing', () => { - it('parses true/false question type correctly', () => { - expect(sampleTrueFalseQuestion.type).toBe('TF'); - }); + // describe('question type parsing', () => { + // it('parses true/false question type correctly', () => { + // expect(sampleTrueFalseQuestion.type).toBe('TF'); + // }); - it('parses multiple choice question type correctly', () => { - expect(sampleMultipleChoiceQuestion.type).toBe('MC'); - }); + // it('parses multiple choice question type correctly', () => { + // expect(sampleMultipleChoiceQuestion.type).toBe('MC'); + // }); - it('parses numerical question type correctly', () => { - expect(sampleNumericalQuestion.type).toBe('Numerical'); - }); + // it('parses numerical question type correctly', () => { + // expect(sampleNumericalQuestion.type).toBe('Numerical'); + // }); + + // it('parses short answer question type correctly', () => { + // expect(sampleShortAnswerQuestion.type).toBe('Short'); + // }); + // }); - it('parses short answer question type correctly', () => { - expect(sampleShortAnswerQuestion.type).toBe('Short'); - }); - }); it('renders correctly for True/False question', () => { renderComponent(sampleTrueFalseQuestion); @@ -73,7 +74,8 @@ describe('Questions Component', () => { const submitButton = screen.getByText('Répondre'); fireEvent.click(submitButton); - expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('Choice 1'); + expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(['Choice 1']); + mockHandleSubmitAnswer.mockClear(); }); it('renders correctly for Numerical question', () => { @@ -93,7 +95,8 @@ describe('Questions Component', () => { const submitButton = screen.getByText('Répondre'); fireEvent.click(submitButton); - expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(7); + expect(mockHandleSubmitAnswer).toHaveBeenCalledWith([7]); + mockHandleSubmitAnswer.mockClear(); }); it('renders correctly for Short Answer question', () => { @@ -117,7 +120,7 @@ describe('Questions Component', () => { const submitButton = screen.getByText('Répondre'); fireEvent.click(submitButton); - expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('User Input'); + expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(['User Input']); }); }); diff --git a/client/src/__tests__/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.test.tsx b/client/src/__tests__/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.test.tsx index c4326eb..57e9da5 100644 --- a/client/src/__tests__/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.test.tsx +++ b/client/src/__tests__/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.test.tsx @@ -47,6 +47,7 @@ describe('ShortAnswerQuestion Component', () => { fireEvent.click(submitButton); expect(mockHandleSubmitAnswer).not.toHaveBeenCalled(); + mockHandleSubmitAnswer.mockClear(); }); it('submits answer correctly', () => { @@ -60,6 +61,7 @@ describe('ShortAnswerQuestion Component', () => { fireEvent.click(submitButton); - expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('User Input'); + expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(['User Input']); + mockHandleSubmitAnswer.mockClear(); }); }); diff --git a/client/src/__tests__/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.test.tsx b/client/src/__tests__/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.test.tsx index e6910d4..f79d1da 100644 --- a/client/src/__tests__/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.test.tsx +++ b/client/src/__tests__/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.test.tsx @@ -56,6 +56,7 @@ describe('TrueFalseQuestion Component', () => { }); expect(mockHandleSubmitAnswer).not.toHaveBeenCalled(); + mockHandleSubmitAnswer.mockClear(); }); it('submits answer correctly for True', () => { @@ -70,7 +71,8 @@ describe('TrueFalseQuestion Component', () => { fireEvent.click(submitButton); }); - expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(true); + expect(mockHandleSubmitAnswer).toHaveBeenCalledWith([true]); + mockHandleSubmitAnswer.mockClear(); }); it('submits answer correctly for False', () => { @@ -83,7 +85,8 @@ describe('TrueFalseQuestion Component', () => { fireEvent.click(submitButton); }); - expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(false); + expect(mockHandleSubmitAnswer).toHaveBeenCalledWith([false]); + mockHandleSubmitAnswer.mockClear(); }); @@ -112,7 +115,7 @@ describe('TrueFalseQuestion Component', () => { expect(wrongAnswer1?.textContent).toContain('❌'); }); - it('should not show ✅ or ❌ when repondre button is not clicked', async () => { + it('should not show ✅ or ❌ when Répondre button is not clicked', async () => { const choiceButton = screen.getByText('Vrai').closest('button'); if (!choiceButton) throw new Error('Choice button not found'); diff --git a/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx b/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx new file mode 100644 index 0000000..7027eaf --- /dev/null +++ b/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { render, screen, fireEvent, act } from '@testing-library/react'; +import ShareQuizModal from '../../../components/ShareQuizModal/ShareQuizModal.tsx'; +import { QuizType } from '../../../Types/QuizType'; +import '@testing-library/jest-dom'; + +describe('ShareQuizModal', () => { + const mockQuiz: QuizType = { + _id: '123', + folderId: 'folder-123', + folderName: 'Test Folder', + userId: 'user-123', + title: 'Test Quiz', + content: ['Question 1', 'Question 2'], + created_at: new Date(), + updated_at: new Date(), + }; + + beforeAll(() => { + // Properly mock the clipboard API + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: jest.fn().mockImplementation(() => Promise.resolve()), + }, + writable: true, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders the share button', () => { + render(); + expect(screen.getByLabelText('partager quiz')).toBeInTheDocument(); + expect(screen.getByTestId('ShareIcon')).toBeInTheDocument(); + }); + + it('copies the quiz URL to clipboard when share button is clicked', async () => { + render(); + const shareButton = screen.getByLabelText('partager quiz'); + + await act(async () => { + fireEvent.click(shareButton); + }); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + `${window.location.origin}/teacher/share/${mockQuiz._id}` + ); + + // Check for feedback dialog content + expect(screen.getByText(/L'URL de partage pour le quiz/i)).toBeInTheDocument(); + expect(screen.getByText(mockQuiz.title)).toBeInTheDocument(); + expect(screen.getByText(/a été copiée\./i)).toBeInTheDocument(); + }); + + it('shows error message when clipboard write fails', async () => { + // Override the mock to reject + (navigator.clipboard.writeText as jest.Mock).mockRejectedValueOnce(new Error('Clipboard write failed')); + + render(); + const shareButton = screen.getByLabelText('partager quiz'); + + await act(async () => { + fireEvent.click(shareButton); + }); + + expect(screen.getByText(/Une erreur est survenue lors de la copie de l'URL\./i)).toBeInTheDocument(); + }); + + it('displays the quiz title in the success message', async () => { + render(); + const shareButton = screen.getByLabelText('partager quiz'); + + await act(async () => { + fireEvent.click(shareButton); + }); + + expect(screen.getByText(mockQuiz.title)).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/client/src/__tests__/pages/ManageRoom/IsCorrect.test.tsx b/client/src/__tests__/pages/ManageRoom/IsCorrect.test.tsx new file mode 100644 index 0000000..80748f3 --- /dev/null +++ b/client/src/__tests__/pages/ManageRoom/IsCorrect.test.tsx @@ -0,0 +1,359 @@ +import { checkIfIsCorrect } from 'src/pages/Teacher/ManageRoom/useRooms'; +import { HighLowNumericalAnswer, MultipleChoiceQuestion, MultipleNumericalAnswer, NumericalQuestion, RangeNumericalAnswer, ShortAnswerQuestion, SimpleNumericalAnswer, TrueFalseQuestion } from 'gift-pegjs'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; +import { QuestionType } from 'src/Types/QuestionType'; + +describe('checkIfIsCorrect', () => { + const mockQuestions: QuestionType[] = [ + { + question: { + id: '1', + type: 'MC', + choices: [ + { isCorrect: true, formattedText: { text: 'Answer1' } }, + { isCorrect: true, formattedText: { text: 'Answer2' } }, + { isCorrect: false, formattedText: { text: 'Answer3' } }, + ], + } as MultipleChoiceQuestion, + }, + ]; + + test('returns true when all selected answers are correct', () => { + const answer: AnswerType = ['Answer1', 'Answer2']; + const result = checkIfIsCorrect(answer, 1, mockQuestions); + expect(result).toBe(true); + }); + + test('returns false when some selected answers are incorrect', () => { + const answer: AnswerType = ['Answer1', 'Answer3']; + const result = checkIfIsCorrect(answer, 1, mockQuestions); + expect(result).toBe(false); + }); + + test('returns false when all correct answers are selected, but one incorrect is also selected', () => { + const answer: AnswerType = ['Answer1', 'Answer2', 'Answer3']; + const result = checkIfIsCorrect(answer, 1, mockQuestions); + expect(result).toBe(false); + }); + + test('returns false when no answers are selected', () => { + const answer: AnswerType = []; + const result = checkIfIsCorrect(answer, 1, mockQuestions); + expect(result).toBe(false); + }); + + test('returns false when no correct answers are provided in the question', () => { + const mockQuestionsWithNoCorrectAnswers: QuestionType[] = [ + { + question: { + id: '1', + type: 'MC', + choices: [ + { isCorrect: false, formattedText: { text: 'Answer1' } }, + { isCorrect: false, formattedText: { text: 'Answer2' } }, + ], + } as MultipleChoiceQuestion, + }, + ]; + const answer: AnswerType = ['Answer1']; + const result = checkIfIsCorrect(answer, 1, mockQuestionsWithNoCorrectAnswers); + expect(result).toBe(false); + }); + + test('returns true for a correct true/false answer', () => { + const mockQuestionsTF: QuestionType[] = [ + { + question: { + id: '2', + type: 'TF', + isTrue: true, + } as TrueFalseQuestion, + }, + ]; + const answer: AnswerType = [true]; + const result = checkIfIsCorrect(answer, 2, mockQuestionsTF); + expect(result).toBe(true); + }); + + test('returns false for an incorrect true/false answer', () => { + const mockQuestionsTF: QuestionType[] = [ + { + question: { + id: '2', + type: 'TF', + isTrue: true, + } as TrueFalseQuestion, + }, + ]; + const answer: AnswerType = [false]; + const result = checkIfIsCorrect(answer, 2, mockQuestionsTF); + expect(result).toBe(false); + }); + + test('returns false for a true/false question with no answer', () => { + const mockQuestionsTF: QuestionType[] = [ + { + question: { + id: '2', + type: 'TF', + isTrue: true, + } as TrueFalseQuestion, + }, + ]; + const answer: AnswerType = []; + const result = checkIfIsCorrect(answer, 2, mockQuestionsTF); + expect(result).toBe(false); + }); + + test('returns true for a correct true/false answer when isTrue is false', () => { + const mockQuestionsTF: QuestionType[] = [ + { + question: { + id: '3', + type: 'TF', + isTrue: false, // Correct answer is false + } as TrueFalseQuestion, + }, + ]; + const answer: AnswerType = [false]; + const result = checkIfIsCorrect(answer, 3, mockQuestionsTF); + expect(result).toBe(true); + }); + + test('returns false for an incorrect true/false answer when isTrue is false', () => { + const mockQuestionsTF: QuestionType[] = [ + { + question: { + id: '3', + type: 'TF', + isTrue: false, // Correct answer is false + } as TrueFalseQuestion, + }, + ]; + const answer: AnswerType = [true]; + const result = checkIfIsCorrect(answer, 3, mockQuestionsTF); + expect(result).toBe(false); + }); + + test('returns true for a correct short answer', () => { + const mockQuestionsShort: QuestionType[] = [ + { + question: { + id: '4', + type: 'Short', + choices: [ + { text: 'CorrectAnswer1' }, + { text: 'CorrectAnswer2' }, + ], + } as ShortAnswerQuestion, + }, + ]; + const answer: AnswerType = ['CorrectAnswer1']; + const result = checkIfIsCorrect(answer, 4, mockQuestionsShort); + expect(result).toBe(true); + }); + + test('returns false for an incorrect short answer', () => { + const mockQuestionsShort: QuestionType[] = [ + { + question: { + id: '4', + type: 'Short', + choices: [ + { text: 'CorrectAnswer1' }, + { text: 'CorrectAnswer2' }, + ], + } as ShortAnswerQuestion, + }, + ]; + const answer: AnswerType = ['WrongAnswer']; + const result = checkIfIsCorrect(answer, 4, mockQuestionsShort); + expect(result).toBe(false); + }); + + test('returns true for a correct short answer with case insensitivity', () => { + const mockQuestionsShort: QuestionType[] = [ + { + question: { + id: '4', + type: 'Short', + choices: [ + { text: 'CorrectAnswer1' }, + { text: 'CorrectAnswer2' }, + ], + } as ShortAnswerQuestion, + }, + ]; + const answer: AnswerType = ['correctanswer1']; // Lowercase version of the correct answer + const result = checkIfIsCorrect(answer, 4, mockQuestionsShort); + expect(result).toBe(true); + }); + + test('returns false for a short answer question with no answer', () => { + const mockQuestionsShort: QuestionType[] = [ + { + question: { + id: '4', + type: 'Short', + choices: [ + { text: 'CorrectAnswer1' }, + { text: 'CorrectAnswer2' }, + ], + } as ShortAnswerQuestion, + }, + ]; + const answer: AnswerType = []; + const result = checkIfIsCorrect(answer, 4, mockQuestionsShort); + expect(result).toBe(false); + }); + + + test('returns true for a correct simple numerical answer', () => { + const mockQuestionsNumerical: QuestionType[] = [ + { + question: { + id: '5', + type: 'Numerical', + choices: [ + { type: 'simple', number: 42 } as SimpleNumericalAnswer, + ], + } as NumericalQuestion, + }, + ]; + const answer: AnswerType = [42]; // User's answer + const result = checkIfIsCorrect(answer, 5, mockQuestionsNumerical); + expect(result).toBe(true); + }); + + test('returns false for an incorrect simple numerical answer', () => { + const mockQuestionsNumerical: QuestionType[] = [ + { + question: { + id: '5', + type: 'Numerical', + choices: [ + { type: 'simple', number: 42 } as SimpleNumericalAnswer, + ], + } as NumericalQuestion, + }, + ]; + const answer: AnswerType = [43]; // User's answer + const result = checkIfIsCorrect(answer, 5, mockQuestionsNumerical); + expect(result).toBe(false); + }); + + test('returns true for a correct range numerical answer', () => { + const mockQuestionsNumerical: QuestionType[] = [ + { + question: { + id: '6', + type: 'Numerical', + choices: [ + { type: 'range', number: 50, range: 5 } as RangeNumericalAnswer, + ], + } as NumericalQuestion, + }, + ]; + const answer: AnswerType = [52]; // User's answer within the range (50 ± 5) + const result = checkIfIsCorrect(answer, 6, mockQuestionsNumerical); + expect(result).toBe(true); + }); + + test('returns false for an out-of-range numerical answer', () => { + const mockQuestionsNumerical: QuestionType[] = [ + { + question: { + id: '6', + type: 'Numerical', + choices: [ + { type: 'range', number: 50, range: 5 } as RangeNumericalAnswer, + ], + } as NumericalQuestion, + }, + ]; + const answer: AnswerType = [56]; // User's answer outside the range (50 ± 5) + const result = checkIfIsCorrect(answer, 6, mockQuestionsNumerical); + expect(result).toBe(false); + }); + + test('returns true for a correct high-low numerical answer', () => { + const mockQuestionsNumerical: QuestionType[] = [ + { + question: { + id: '7', + type: 'Numerical', + choices: [ + { type: 'high-low', numberHigh: 100, numberLow: 90 } as HighLowNumericalAnswer, + ], + } as NumericalQuestion, + }, + ]; + const answer: AnswerType = [95]; // User's answer within the range (90 to 100) + const result = checkIfIsCorrect(answer, 7, mockQuestionsNumerical); + expect(result).toBe(true); + }); + + test('returns false for an out-of-range high-low numerical answer', () => { + const mockQuestionsNumerical: QuestionType[] = [ + { + question: { + id: '7', + type: 'Numerical', + choices: [ + { type: 'high-low', numberHigh: 100, numberLow: 90 } as HighLowNumericalAnswer, + ], + } as NumericalQuestion, + }, + ]; + const answer: AnswerType = [105]; // User's answer outside the range (90 to 100) + const result = checkIfIsCorrect(answer, 7, mockQuestionsNumerical); + expect(result).toBe(false); + }); + + test('returns true for a correct multiple numerical answer', () => { + const mockQuestionsNumerical: QuestionType[] = [ + { + question: { + id: '8', + type: 'Numerical', + choices: [ + { + isCorrect: true, + answer: { type: 'simple', number: 42 } as SimpleNumericalAnswer, + } as MultipleNumericalAnswer, + { + isCorrect: false, + answer: { type: 'high-low', numberHigh: 100, numberLow: 90 } as HighLowNumericalAnswer, + formattedFeedback: { text: 'You guessed way too high' }, + } + ], + } as NumericalQuestion, + }, + ]; + const answer: AnswerType = [42]; // User's answer matches the correct multiple numerical answer + const result = checkIfIsCorrect(answer, 8, mockQuestionsNumerical); + expect(result).toBe(true); + }); + + test('returns false for an incorrect multiple numerical answer', () => { + const mockQuestionsNumerical: QuestionType[] = [ + { + question: { + id: '8', + type: 'Numerical', + choices: [ + { + type: 'multiple', + isCorrect: true, + answer: { type: 'simple', number: 42 } as SimpleNumericalAnswer, + } as MultipleNumericalAnswer, + ], + } as NumericalQuestion, + }, + ]; + const answer: AnswerType = [43]; // User's answer does not match the correct multiple numerical answer + const result = checkIfIsCorrect(answer, 8, mockQuestionsNumerical); + expect(result).toBe(false); + }); + +}); diff --git a/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx b/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx index 142f1f3..aca1d64 100644 --- a/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx +++ b/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx @@ -19,6 +19,11 @@ jest.mock('react-router-dom', () => ({ })); jest.mock('src/pages/Teacher/ManageRoom/RoomContext'); +jest.mock('qrcode.react', () => ({ + __esModule: true, + QRCodeCanvas: ({ value }: { value: string }) =>
{value}
, +})); + const mockSocket = { on: jest.fn(), off: jest.fn(), @@ -44,7 +49,7 @@ const mockStudents: StudentType[] = [ ]; const mockAnswerData: AnswerReceptionFromBackendType = { - answer: 'Answer1', + answer: ['Answer1'], idQuestion: 1, idUser: '1', username: 'Student 1', @@ -233,7 +238,7 @@ describe('ManageRoom', () => { await act(async () => { const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1]; - createSuccessCallback('test-room-name'); + createSuccessCallback('Test Room'); }); const launchButton = screen.getByText('Lancer'); @@ -256,6 +261,7 @@ describe('ManageRoom', () => { }); await waitFor(() => { + // console.info(consoleSpy.mock.calls); expect(consoleSpy).toHaveBeenCalledWith( 'Received answer from Student 1 for question 1: Answer1' ); @@ -293,4 +299,84 @@ describe('ManageRoom', () => { expect(screen.queryByText('Student 1')).not.toBeInTheDocument(); }); }); + + test('terminates the quiz and navigates to teacher dashboard when the "Terminer le quiz" button is clicked', async () => { + await act(async () => { + render( + + + + ); + }); + + await act(async () => { + const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1]; + createSuccessCallback('Test Room'); + }); + + fireEvent.click(screen.getByText('Lancer')); + fireEvent.click(screen.getByText('Rythme du professeur')); + fireEvent.click(screen.getAllByText('Lancer')[1]); + + await waitFor(() => { + expect(screen.getByText('Test Quiz')).toBeInTheDocument(); + }); + + const finishQuizButton = screen.getByText('Terminer le quiz'); + fireEvent.click(finishQuizButton); + + await waitFor(() => { + expect(navigate).toHaveBeenCalledWith('/teacher/dashboard'); + }); + }); + + test("Affiche la modale QR Code lorsqu’on clique sur le bouton", async () => { + render(); + + const button = screen.getByRole('button', { name: /lien de participation/i }); + fireEvent.click(button); + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + + expect(screen.getByRole('heading', { name: /Rejoindre la salle/i })).toBeInTheDocument(); + expect(screen.getByText(/Scannez ce QR code ou partagez le lien ci-dessous/i)).toBeInTheDocument(); + expect(screen.getByTestId('qr-code')).toBeInTheDocument(); + }); + + test("Ferme la modale QR Code lorsqu’on clique sur le bouton Fermer", async () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: /lien de participation/i })); + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: /fermer/i })); + + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + }); + + test('Affiche le bon lien de participation', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: /lien de participation/i })); + + const roomUrl = `${window.location.origin}/student/join-room?roomName=Test Room`; + expect(screen.getByTestId('qr-code')).toHaveTextContent(roomUrl); + }); + + test('Vérifie que le QR code contient la bonne URL', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: /lien de participation/i })); + + const roomUrl = `${window.location.origin}/student/join-room?roomName=Test Room`; + expect(screen.getByTestId('qr-code')).toHaveTextContent(roomUrl); + }); }); + diff --git a/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx b/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx index 1218694..11fe682 100644 --- a/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx +++ b/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx @@ -8,7 +8,7 @@ import { QuestionType } from 'src/Types/QuestionType'; import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService'; const mockGiftQuestions = parse( - `::Sample Question 1:: Sample Question 1 {=Option A ~Option B} + `::Sample Question 1:: Sample Question 1 {=Option A =Option B ~Option C} ::Sample Question 2:: Sample Question 2 {T}`); @@ -23,9 +23,6 @@ const mockSubmitAnswer = jest.fn(); const mockDisconnectWebSocket = jest.fn(); beforeEach(() => { - // Clear local storage before each test - // localStorage.clear(); - render( { fireEvent.click(screen.getByText('Répondre')); }); - expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); + expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A'], 1); }); test('handles shows feedback for an already answered question', async () => { @@ -65,13 +62,13 @@ describe('StudentModeQuiz', () => { act(() => { fireEvent.click(screen.getByText('Répondre')); }); - expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); + expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A'], 1); const firstButtonA = screen.getByRole("button", {name: '✅ A Option A'}); expect(firstButtonA).toBeInTheDocument(); expect(firstButtonA.querySelector('.selected')).toBeInTheDocument(); - expect(screen.getByRole("button", {name: '❌ B Option B'})).toBeInTheDocument(); + expect(screen.getByRole("button", {name: '✅ B Option B'})).toBeInTheDocument(); expect(screen.queryByText('Répondre')).not.toBeInTheDocument(); // Navigate to the next question @@ -87,12 +84,12 @@ describe('StudentModeQuiz', () => { }); expect(await screen.findByText('Sample Question 1')).toBeInTheDocument(); - // Since answers are mocked, the it doesn't recognize the question as already answered + // Since answers are mocked, it doesn't recognize the question as already answered // TODO these tests are partially faked, need to be fixed if we can mock the answers // const buttonA = screen.getByRole("button", {name: '✅ A Option A'}); const buttonA = screen.getByRole("button", {name: 'A Option A'}); expect(buttonA).toBeInTheDocument(); - // const buttonB = screen.getByRole("button", {name: '❌ B Option B'}); + // const buttonB = screen.getByRole("button", {name: '✅ B Option B'}); const buttonB = screen.getByRole("button", {name: 'B Option B'}); expect(buttonB).toBeInTheDocument(); // // "Option A" div inside the name of button should have selected class @@ -122,4 +119,30 @@ describe('StudentModeQuiz', () => { expect(screen.getByText('Sample Question 2')).toBeInTheDocument(); expect(screen.getByText('Répondre')).toBeInTheDocument(); }); + + // le test suivant est fait dans MultipleChoiceQuestionDisplay.test.tsx +// test('allows multiple answers to be selected for a question', async () => { +// // Simulate selecting multiple answers +// act(() => { +// fireEvent.click(screen.getByText('Option A')); +// }); +// act(() => { +// fireEvent.click(screen.getByText('Option B')); +// }); + +// // Simulate submitting the answers +// act(() => { +// fireEvent.click(screen.getByText('Répondre')); +// }); + +// // Verify that the mockSubmitAnswer function is called with both answers +// expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A', 'Option B'], 1); + +// // Verify that the selected answers are displayed as selected +// const buttonA = screen.getByRole('button', { name: '✅ A Option A' }); +// const buttonB = screen.getByRole('button', { name: '✅ B Option B' }); +// expect(buttonA).toBeInTheDocument(); +// expect(buttonB).toBeInTheDocument(); +// }); + }); diff --git a/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx b/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx index 6a4ec59..4cd5a8d 100644 --- a/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx +++ b/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx @@ -63,7 +63,8 @@ describe('TeacherModeQuiz', () => { act(() => { fireEvent.click(screen.getByText('Répondre')); }); - expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); + expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A'], 1); + mockSubmitAnswer.mockClear(); }); test('handles shows feedback for an already answered question', () => { @@ -74,7 +75,8 @@ describe('TeacherModeQuiz', () => { act(() => { fireEvent.click(screen.getByText('Répondre')); }); - expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); + expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A'], 1); + mockSubmitAnswer.mockClear(); mockQuestion = mockQuestions[1].question as MultipleChoiceQuestion; // Navigate to the next question by re-rendering with new props act(() => { diff --git a/client/src/__tests__/pages/Teacher/Share/Share.test.tsx b/client/src/__tests__/pages/Teacher/Share/Share.test.tsx new file mode 100644 index 0000000..9cc3203 --- /dev/null +++ b/client/src/__tests__/pages/Teacher/Share/Share.test.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { MemoryRouter, Route, Routes, useParams } from 'react-router-dom'; +import Share from '../../../../pages/Teacher/Share/Share.tsx'; +import ApiService from '../../../../services/ApiService'; +import '@testing-library/jest-dom'; + +// Mock the ApiService and react-router-dom +jest.mock('../../../../services/ApiService'); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(), + useNavigate: jest.fn(), +})); + +describe('Share Component', () => { + const mockNavigate = jest.fn(); + const mockUseParams = useParams as jest.Mock; + const mockApiService = ApiService as jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + mockUseParams.mockReturnValue({ id: 'quiz123' }); + require('react-router-dom').useNavigate.mockReturnValue(mockNavigate); + }); + + const renderComponent = (initialEntries = ['/share/quiz123']) => { + return render( + + + } /> + Dashboard
} /> + Login} /> + + + ); + }; + + it('should show loading state initially', () => { + mockApiService.getAllQuizIds.mockResolvedValue([]); + mockApiService.getUserFolders.mockResolvedValue([]); + mockApiService.getSharedQuiz.mockResolvedValue('Test Quiz'); + + renderComponent(); + expect(screen.getByText('Chargement...')).toBeInTheDocument(); + }); + + it('should redirect to login if not authenticated', async () => { + mockApiService.isLoggedIn.mockReturnValue(false); + + renderComponent(); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith('/login'); + }); + }); + + it('should show "quiz already exists" message when quiz exists', async () => { + mockApiService.isLoggedIn.mockReturnValue(true); + mockApiService.getAllQuizIds.mockResolvedValue(['quiz123']); + + renderComponent(); + + await waitFor(() => { + expect(screen.getByText('Quiz déjà existant')).toBeInTheDocument(); + expect(screen.getByText(/Le quiz que vous essayez d'importer existe déjà sur votre compte/i)).toBeInTheDocument(); + expect(screen.getByText('Retour au tableau de bord')).toBeInTheDocument(); + }); + }); + + it('should navigate to dashboard when clicking return button in "quiz exists" view', async () => { + mockApiService.isLoggedIn.mockReturnValue(true); + mockApiService.getAllQuizIds.mockResolvedValue(['quiz123']); + + renderComponent(); + + await waitFor(() => { + fireEvent.click(screen.getByText('Retour au tableau de bord')); + expect(mockNavigate).toHaveBeenCalledWith('/teacher/dashboard'); + }); + }); + + it('should show error when no folders exist', async () => { + mockApiService.isLoggedIn.mockReturnValue(true); + mockApiService.getAllQuizIds.mockResolvedValue([]); + mockApiService.getUserFolders.mockResolvedValue([]); + + renderComponent(); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith('/teacher/dashboard'); + }); + }); + +}); \ No newline at end of file diff --git a/client/src/__tests__/services/ShareQuizService.test.tsx b/client/src/__tests__/services/ShareQuizService.test.tsx new file mode 100644 index 0000000..78c1f65 --- /dev/null +++ b/client/src/__tests__/services/ShareQuizService.test.tsx @@ -0,0 +1,42 @@ +import axios from 'axios'; +import ApiService from '../../services/ApiService'; +import { ENV_VARIABLES } from '../../constants'; + +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + + describe('getSharedQuiz', () => { + it('should call the API to get a shared quiz and return the quiz data on success', async () => { + const quizId = '123'; + const quizData = 'Quiz data'; + const response = { status: 200, data: { data: quizData } }; + mockedAxios.get.mockResolvedValue(response); + + const result = await ApiService.getSharedQuiz(quizId); + + expect(mockedAxios.get).toHaveBeenCalledWith( + `${ENV_VARIABLES.VITE_BACKEND_URL}/api/quiz/getShare/${quizId}`, + { headers: expect.any(Object) } + ); + expect(result).toBe(quizData); + }); + + it('should return an error message if the API call fails', async () => { + const quizId = '123'; + const errorMessage = 'An unexpected error occurred.'; + mockedAxios.get.mockRejectedValue({ response: { data: { error: errorMessage } } }); + + const result = await ApiService.getSharedQuiz(quizId); + + expect(result).toBe(errorMessage); + }); + + it('should return a generic error message if an unexpected error occurs', async () => { + const quizId = '123'; + mockedAxios.get.mockRejectedValue(new Error('Unexpected error')); + + const result = await ApiService.getSharedQuiz(quizId); + + expect(result).toBe('An unexpected error occurred.'); + }); + }); \ No newline at end of file diff --git a/client/src/__tests__/services/getAllQuizIdsService.test.tsx b/client/src/__tests__/services/getAllQuizIdsService.test.tsx new file mode 100644 index 0000000..222e7db --- /dev/null +++ b/client/src/__tests__/services/getAllQuizIdsService.test.tsx @@ -0,0 +1,42 @@ +import axios from 'axios'; +import ApiService from '../../services/ApiService'; +import { FolderType } from '../../Types/FolderType'; +import { QuizType } from '../../Types/QuizType'; + +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +describe('ApiService', () => { + describe('getAllQuizIds', () => { + it('should return all quiz IDs from all folders', async () => { + const folders: FolderType[] = [ + { _id: 'folder1', title: 'Folder 1', userId: 'user1', created_at: new Date().toISOString() }, + { _id: 'folder2', title: 'Folder 2', userId: 'user2', created_at: new Date().toISOString() } + ]; + const quizzesFolder1: QuizType[] = [ + { _id: 'quiz1', title: 'Quiz 1', content: [], folderId: 'folder1', folderName: 'Folder 1', userId: 'user1', created_at: new Date(), updated_at: new Date() }, + { _id: 'quiz2', title: 'Quiz 2', content: [], folderId: 'folder1', folderName: 'Folder 1', userId: 'user1', created_at: new Date(), updated_at: new Date() } + ]; + const quizzesFolder2: QuizType[] = [ + { _id: 'quiz3', title: 'Quiz 3', content: [], folderId: 'folder2', folderName: 'Folder 2', userId: 'user2', created_at: new Date(), updated_at: new Date() } + ]; + + mockedAxios.get + .mockResolvedValueOnce({ status: 200, data: { data: folders } }) + .mockResolvedValueOnce({ status: 200, data: { data: quizzesFolder1 } }) + .mockResolvedValueOnce({ status: 200, data: { data: quizzesFolder2 } }); + + const result = await ApiService.getAllQuizIds(); + + expect(result).toEqual(['quiz1', 'quiz2', 'quiz3']); + }); + + it('should return an empty array if no folders are found', async () => { + mockedAxios.get.mockResolvedValueOnce({ status: 200, data: { data: [] } }); + + const result = await ApiService.getAllQuizIds(); + + expect(result).toEqual([]); + }); + }); +}); \ No newline at end of file diff --git a/client/src/components/GiftTemplate/templates/MultipleChoiceAnswersTemplate.ts b/client/src/components/GiftTemplate/templates/MultipleChoiceAnswersTemplate.ts index 2bc423d..76c151d 100644 --- a/client/src/components/GiftTemplate/templates/MultipleChoiceAnswersTemplate.ts +++ b/client/src/components/GiftTemplate/templates/MultipleChoiceAnswersTemplate.ts @@ -13,14 +13,14 @@ type AnswerFeedbackOptions = TemplateOptions & Pick isCorrect === true).length === 0; + const hasManyCorrectChoices = choices.filter(({ isCorrect }) => isCorrect === true).length > 1; const prompt = `Choisir une réponse${ - isMultipleAnswer ? ` ou plusieurs` : `` + hasManyCorrectChoices ? ` ou plusieurs` : `` }:`; const result = choices .map(({ weight, isCorrect, formattedText, formattedFeedback }) => { @@ -32,12 +32,12 @@ export default function MultipleChoiceAnswersTemplate({ choices }: MultipleChoic const inputId = `id${nanoid(6)}`; const isPositiveWeight = (weight != undefined) && (weight > 0); - const isCorrectOption = isMultipleAnswer ? isPositiveWeight : isCorrect; + const isCorrectOption = hasManyCorrectChoices ? isPositiveWeight || isCorrect : isCorrect; return `
${AnswerWeight({ weight: weight })}
)} diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx index 7d2b4b2..461d59f 100644 --- a/client/src/components/ImageGallery/ImageGallery.tsx +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -22,8 +22,8 @@ import CloseIcon from "@mui/icons-material/Close"; import { ImageType } from "../../Types/ImageType"; import ApiService from "../../services/ApiService"; import { Upload } from "@mui/icons-material"; -import { ENV_VARIABLES } from '../../constants'; import { escapeForGIFT } from "src/utils/giftUtils"; +import { ENV_VARIABLES } from "src/constants"; interface ImagesProps { handleCopy?: (id: string) => void; @@ -83,9 +83,9 @@ const ImageGallery: React.FC = ({ handleCopy, handleDelete }) => { const defaultHandleCopy = (id: string) => { if (navigator.clipboard) { - const link = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`; - const imgTag = `[markdown]![alt_text](${escapeForGIFT(link)} "texte de l'infobulle")`; - setSnackbarMessage("Le lien Markdown de l’image a été copié dans le presse-papiers"); + const link = `${ENV_VARIABLES.BACKEND_URL}/api/image/get/${id}`; + const imgTag = `[markdown] ![texte alternatif d'écrivant l'image pour les personnes qui ne peuvent pas voir l'image](${escapeForGIFT(link)} "texte de l'infobulle (ne fonctionne pas sur écran tactile généralement)") `; + setSnackbarMessage("Le lien Markdown de l'image a été copié dans le presse-papiers"); setSnackbarSeverity("success"); setSnackbarOpen(true); navigator.clipboard.writeText(imgTag); @@ -147,7 +147,7 @@ const ImageGallery: React.FC = ({ handleCopy, handleDelete }) => { return ( setTabValue(newValue)}> - + {tabValue === 0 && ( diff --git a/client/src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultTableFooter.tsx b/client/src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultTableFooter.tsx index a24694e..a1c250c 100644 --- a/client/src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultTableFooter.tsx +++ b/client/src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultTableFooter.tsx @@ -51,7 +51,7 @@ const LiveResultsTableFooter: React.FC = ({ borderWidth: 1, borderColor: 'rgba(224, 224, 224, 1)', fontWeight: 'bold', - color: 'rgba(0, 0, 0)' + color: 'rgba(0, 0, 0)', }} > {students.length > 0 @@ -67,7 +67,7 @@ const LiveResultsTableFooter: React.FC = ({ borderColor: 'rgba(224, 224, 224, 1)', fontWeight: 'bold', fontSize: '1rem', - color: 'rgba(0, 0, 0)' + color: 'rgba(0, 0, 0)', }} > {students.length > 0 ? `${classAverage.toFixed()} %` : '-'} @@ -76,4 +76,4 @@ const LiveResultsTableFooter: React.FC = ({ ); }; -export default LiveResultsTableFooter; \ No newline at end of file +export default LiveResultsTableFooter; diff --git a/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx index df46193..d023a40 100644 --- a/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx @@ -15,76 +15,115 @@ interface Props { const MultipleChoiceQuestionDisplay: React.FC = (props) => { const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; - const [answer, setAnswer] = useState(passedAnswer || ''); + console.log('MultipleChoiceQuestionDisplay: passedAnswer', JSON.stringify(passedAnswer)); + const [answer, setAnswer] = useState(() => { + if (passedAnswer && passedAnswer.length > 0) { + return passedAnswer; + } + return []; + }); let disableButton = false; - if(handleOnSubmitAnswer === undefined){ + if (handleOnSubmitAnswer === undefined) { disableButton = true; } useEffect(() => { - if (passedAnswer !== undefined) { - setAnswer(passedAnswer); - } - }, [passedAnswer]); + console.log('MultipleChoiceQuestionDisplay: passedAnswer', JSON.stringify(passedAnswer)); + if (passedAnswer !== undefined) { + setAnswer(passedAnswer); + } else { + setAnswer([]); + } + }, [passedAnswer, question.id]); const handleOnClickAnswer = (choice: string) => { - setAnswer(choice); + setAnswer((prevAnswer) => { + console.log(`handleOnClickAnswer -- setAnswer(): prevAnswer: ${prevAnswer}, choice: ${choice}`); + const correctAnswersCount = question.choices.filter((c) => c.isCorrect).length; + + if (correctAnswersCount === 1) { + // If only one correct answer, replace the current selection + return prevAnswer.includes(choice) ? [] : [choice]; + } else { + // Allow multiple selections if there are multiple correct answers + if (prevAnswer.includes(choice)) { + // Remove the choice if it's already selected + return prevAnswer.filter((selected) => selected !== choice); + } else { + // Add the choice if it's not already selected + return [...prevAnswer, choice]; + } + } + }); }; + const alpha = Array.from(Array(26)).map((_e, i) => i + 65); const alphabet = alpha.map((x) => String.fromCharCode(x)); - return ( + return (
- {question.choices.map((choice, i) => { - const selected = answer === choice.formattedText.text ? 'selected' : ''; + console.log(`answer: ${answer}, choice: ${choice.formattedText.text}`); + const selected = answer.includes(choice.formattedText.text) ? 'selected' : ''; return (
-
); })}
{question.formattedGlobalFeedback && showAnswer && (
-
-
+
+
)} - {!showAnswer && handleOnSubmitAnswer && ( - )}
diff --git a/client/src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.tsx index 525501d..be28f57 100644 --- a/client/src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.tsx @@ -17,7 +17,7 @@ interface Props { const NumericalQuestionDisplay: React.FC = (props) => { const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; - const [answer, setAnswer] = useState(passedAnswer || ''); + const [answer, setAnswer] = useState(passedAnswer || []); const correctAnswers = question.choices; let correctAnswer = ''; @@ -69,7 +69,7 @@ const NumericalQuestionDisplay: React.FC = (props) => { id={question.formattedStem.text} name={question.formattedStem.text} onChange={(e: React.ChangeEvent) => { - setAnswer(e.target.valueAsNumber); + setAnswer([e.target.valueAsNumber]); }} inputProps={{ 'data-testid': 'number-input' }} /> @@ -87,7 +87,7 @@ const NumericalQuestionDisplay: React.FC = (props) => { handleOnSubmitAnswer && handleOnSubmitAnswer(answer) } - disabled={answer === "" || isNaN(answer as number)} + disabled={answer === undefined || answer === null || isNaN(answer[0] as number)} > Répondre diff --git a/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx index 28876f9..4b15e4d 100644 --- a/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx @@ -16,7 +16,7 @@ interface Props { const ShortAnswerQuestionDisplay: React.FC = (props) => { const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; - const [answer, setAnswer] = useState(passedAnswer || ''); + const [answer, setAnswer] = useState(passedAnswer || []); useEffect(() => { if (passedAnswer !== undefined) { @@ -58,7 +58,7 @@ const ShortAnswerQuestionDisplay: React.FC = (props) => { id={question.formattedStem.text} name={question.formattedStem.text} onChange={(e) => { - setAnswer(e.target.value); + setAnswer([e.target.value]); }} disabled={showAnswer} aria-label="short-answer-input" @@ -72,7 +72,7 @@ const ShortAnswerQuestionDisplay: React.FC = (props) => { handleOnSubmitAnswer && handleOnSubmitAnswer(answer) } - disabled={answer === null || answer === ''} + disabled={answer === null || answer === undefined || answer.length === 0} > Répondre diff --git a/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx index 8908338..7decbab 100644 --- a/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx @@ -1,5 +1,5 @@ // TrueFalseQuestion.tsx -import React, { useState,useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import '../questionStyle.css'; import { Button } from '@mui/material'; import { TrueFalseQuestion } from 'gift-pegjs'; @@ -8,37 +8,37 @@ import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; interface Props { question: TrueFalseQuestion; - handleOnSubmitAnswer?: (answer: AnswerType) => void; + handleOnSubmitAnswer?: (answer: AnswerType) => void; showAnswer?: boolean; passedAnswer?: AnswerType; } const TrueFalseQuestionDisplay: React.FC = (props) => { - const { question, showAnswer, handleOnSubmitAnswer, passedAnswer} = + const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; + const [answer, setAnswer] = useState(() => { + + if (passedAnswer && (passedAnswer[0] === true || passedAnswer[0] === false)) { + return passedAnswer[0]; + } + + return undefined; + }); + let disableButton = false; - if(handleOnSubmitAnswer === undefined){ + if (handleOnSubmitAnswer === undefined) { disableButton = true; } useEffect(() => { - console.log("passedAnswer", answer); - if (passedAnswer === true || passedAnswer === false) { - setAnswer(passedAnswer); - } else { - setAnswer(undefined); - } - }, [passedAnswer, question.id]); - - const [answer, setAnswer] = useState(() => { - - if (passedAnswer === true || passedAnswer === false) { - return passedAnswer; + console.log("passedAnswer", passedAnswer); + if (passedAnswer && (passedAnswer[0] === true || passedAnswer[0] === false)) { + setAnswer(passedAnswer[0]); + } else { + setAnswer(undefined); } - - return undefined; - }); + }, [passedAnswer, question.id]); const handleOnClickAnswer = (choice: boolean) => { setAnswer(choice); @@ -49,7 +49,7 @@ const TrueFalseQuestionDisplay: React.FC = (props) => { return (
-
+
{question.formattedGlobalFeedback && showAnswer && ( @@ -95,8 +93,7 @@ const TrueFalseQuestionDisplay: React.FC = (props) => { + + + + ); +}; + +export default ShareQuizModal; \ No newline at end of file diff --git a/client/src/components/StudentWaitPage/StudentWaitPage.tsx b/client/src/components/StudentWaitPage/StudentWaitPage.tsx index c5de4f2..df4e7b8 100644 --- a/client/src/components/StudentWaitPage/StudentWaitPage.tsx +++ b/client/src/components/StudentWaitPage/StudentWaitPage.tsx @@ -26,8 +26,7 @@ const StudentWaitPage: React.FC = ({ students, launchQuiz, setQuizMode }) variant="contained" onClick={handleLaunchClick} startIcon={} - fullWidth - sx={{ fontWeight: 600, fontSize: 20 }} + sx={{ fontWeight: 600, fontSize: 20, width: 'auto' }} > Lancer diff --git a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx index 8925c09..ca67aba 100644 --- a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx +++ b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx @@ -116,7 +116,7 @@ const TeacherModeQuiz: React.FC = ({ diff --git a/client/src/constants.tsx b/client/src/constants.tsx index 0cbbb9f..6fb5537 100644 --- a/client/src/constants.tsx +++ b/client/src/constants.tsx @@ -2,9 +2,8 @@ const ENV_VARIABLES = { MODE: process.env.MODE || "production", VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "", - IMG_URL: process.env.MODE == "development" ? process.env.VITE_BACKEND_URL : process.env.VITE_IMG_URL, - BACKEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.BACKEND_PORT}`:''}` : process.env.VITE_BACKEND_URL || '', - FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}`:''}` : '' + BACKEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.BACKEND_PORT}` : ''}` : process.env.VITE_BACKEND_URL || '', + FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}` : ''}` : '' }; console.log(`ENV_VARIABLES.VITE_BACKEND_URL=${ENV_VARIABLES.VITE_BACKEND_URL}`); diff --git a/client/src/pages/AuthManager/providers/SimpleLogin/Login.tsx b/client/src/pages/AuthManager/providers/SimpleLogin/Login.tsx index ecc9a1c..e0d4988 100644 --- a/client/src/pages/AuthManager/providers/SimpleLogin/Login.tsx +++ b/client/src/pages/AuthManager/providers/SimpleLogin/Login.tsx @@ -44,7 +44,6 @@ const SimpleLogin: React.FC = () => { variant="outlined" value={email} onChange={(e) => setEmail(e.target.value)} - placeholder="Nom d'utilisateur" sx={{ marginBottom: '1rem' }} fullWidth /> @@ -55,7 +54,6 @@ const SimpleLogin: React.FC = () => { type="password" value={password} onChange={(e) => setPassword(e.target.value)} - placeholder="Nom de la salle" sx={{ marginBottom: '1rem' }} fullWidth /> diff --git a/client/src/pages/Student/JoinRoom/JoinRoom.tsx b/client/src/pages/Student/JoinRoom/JoinRoom.tsx index 96d1241..1b02104 100644 --- a/client/src/pages/Student/JoinRoom/JoinRoom.tsx +++ b/client/src/pages/Student/JoinRoom/JoinRoom.tsx @@ -5,7 +5,7 @@ import { ENV_VARIABLES } from 'src/constants'; import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz'; import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz'; -import webSocketService, { AnswerSubmissionToBackendType } from '../../../services/WebsocketService'; +import webSocketService, {AnswerSubmissionToBackendType} from '../../../services/WebsocketService'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton'; import './joinRoom.css'; @@ -13,11 +13,12 @@ import { QuestionType } from '../../../Types/QuestionType'; import { TextField } from '@mui/material'; import LoadingButton from '@mui/lab/LoadingButton'; -import LoginContainer from 'src/components/LoginContainer/LoginContainer' +import LoginContainer from 'src/components/LoginContainer/LoginContainer'; -import ApiService from '../../../services/ApiService' +import ApiService from '../../../services/ApiService'; +import { useSearchParams } from 'react-router-dom'; -export type AnswerType = string | number | boolean; +export type AnswerType = Array; const JoinRoom: React.FC = () => { const [roomName, setRoomName] = useState(''); @@ -30,6 +31,17 @@ const JoinRoom: React.FC = () => { const [answers, setAnswers] = useState([]); const [connectionError, setConnectionError] = useState(''); const [isConnecting, setIsConnecting] = useState(false); + const [isQRCodeJoin, setIsQRCodeJoin] = useState(false); + const [searchParams] = useSearchParams(); + + useEffect(() => { + const roomFromUrl = searchParams.get('roomName'); + if (roomFromUrl) { + setRoomName(roomFromUrl); + setIsQRCodeJoin(true); + console.log('Mode QR Code détecté, salle:', roomFromUrl); + } + }, [searchParams]); useEffect(() => { handleCreateSocket(); @@ -42,7 +54,7 @@ const JoinRoom: React.FC = () => { console.log(`JoinRoom: useEffect: questions: ${JSON.stringify(questions)}`); setAnswers(questions ? Array(questions.length).fill({} as AnswerSubmissionToBackendType) : []); }, [questions]); - + const handleCreateSocket = () => { console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_URL}`); @@ -101,7 +113,7 @@ const JoinRoom: React.FC = () => { }; const disconnect = () => { -// localStorage.clear(); + // localStorage.clear(); webSocketService.disconnect(); setSocket(null); setQuestion(undefined); @@ -198,21 +210,25 @@ const JoinRoom: React.FC = () => { default: return ( - - setRoomName(e.target.value.toUpperCase())} - placeholder="Nom de la salle" - sx={{ marginBottom: '1rem' }} - fullWidth={true} - onKeyDown={handleReturnKey} - /> + title={isQRCodeJoin ? `Rejoindre la salle ${roomName}` : 'Rejoindre une salle'} + error={connectionError} + > + {/* Afficher champ salle SEULEMENT si pas de QR code */} + {!isQRCodeJoin && ( + setRoomName(e.target.value.toUpperCase())} + placeholder="Nom de la salle" + sx={{ marginBottom: '1rem' }} + fullWidth={true} + onKeyDown={handleReturnKey} + /> + )} + {/* Champ username toujours visible */} { onClick={handleSocket} variant="contained" sx={{ marginBottom: `${connectionError && '2rem'}` }} - disabled={!username || !roomName} - >Rejoindre - + disabled={!username || (isQRCodeJoin && !roomName)} + > + {isQRCodeJoin ? 'Rejoindre avec QR Code' : 'Rejoindre'} + ); } diff --git a/client/src/pages/Teacher/Dashboard/Dashboard.tsx b/client/src/pages/Teacher/Dashboard/Dashboard.tsx index cdae619..d9c91e2 100644 --- a/client/src/pages/Teacher/Dashboard/Dashboard.tsx +++ b/client/src/pages/Teacher/Dashboard/Dashboard.tsx @@ -38,10 +38,9 @@ import { Upload, FolderCopy, ContentCopy, - Edit, - Share - // DriveFileMove + Edit } from '@mui/icons-material'; +import ShareQuizModal from 'src/components/ShareQuizModal/ShareQuizModal'; // Create a custom-styled Card component const CustomCard = styled(Card)({ @@ -66,6 +65,7 @@ const Dashboard: React.FC = () => { const [selectedRoom, selectRoom] = useState(); // menu const [errorMessage, setErrorMessage] = useState(''); const [showErrorDialog, setShowErrorDialog] = useState(false); + const [isSearchVisible, setIsSearchVisible] = useState(false); // Filter quizzes based on search term // const filteredQuizzes = quizzes.filter(quiz => @@ -105,7 +105,7 @@ const Dashboard: React.FC = () => { fetchData(); }, []); - + useEffect(() => { if (rooms.length > 0 && !selectedRoom) { selectRoom(rooms[rooms.length - 1]); @@ -121,41 +121,44 @@ const Dashboard: React.FC = () => { } }; - // Créer une salle - const createRoom = async (title: string) => { - // Créer la salle et récupérer l'objet complet - const newRoom = await ApiService.createRoom(title); - - // Mettre à jour la liste des salles - const updatedRooms = await ApiService.getUserRooms(); - setRooms(updatedRooms as RoomType[]); - - // Sélectionner la nouvelle salle avec son ID - selectRoomByName(newRoom); // Utiliser l'ID de l'objet retourné - }; + const toggleSearchVisibility = () => { + setIsSearchVisible(!isSearchVisible); + }; + // Créer une salle + const createRoom = async (title: string) => { + // Créer la salle et récupérer l'objet complet + const newRoom = await ApiService.createRoom(title); - // Sélectionner une salle - const selectRoomByName = (roomId: string) => { - const room = rooms.find(r => r._id === roomId); - selectRoom(room); - localStorage.setItem('selectedRoomId', roomId); - }; + // Mettre à jour la liste des salles + const updatedRooms = await ApiService.getUserRooms(); + setRooms(updatedRooms as RoomType[]); - const handleCreateRoom = async () => { - if (newRoomTitle.trim()) { - try { + // Sélectionner la nouvelle salle avec son ID + selectRoomByName(newRoom); // Utiliser l'ID de l'objet retourné + }; + + // Sélectionner une salle + const selectRoomByName = (roomId: string) => { + const room = rooms.find((r) => r._id === roomId); + selectRoom(room); + localStorage.setItem('selectedRoomId', roomId); + }; + + const handleCreateRoom = async () => { + if (newRoomTitle.trim()) { + try { await createRoom(newRoomTitle); const userRooms = await ApiService.getUserRooms(); setRooms(userRooms as RoomType[]); - setOpenAddRoomDialog(false); - setNewRoomTitle(''); - } catch (error) { - setErrorMessage(error instanceof Error ? error.message : "Erreur inconnue"); - setShowErrorDialog(true); - } - } - }; + setOpenAddRoomDialog(false); + setNewRoomTitle(''); + } catch (error) { + setErrorMessage(error instanceof Error ? error.message : 'Erreur inconnue'); + setShowErrorDialog(true); + } + } + }; const handleSelectFolder = (event: React.ChangeEvent) => { setSelectedFolderId(event.target.value); @@ -398,57 +401,68 @@ const Dashboard: React.FC = () => { } else { const randomSixDigit = Math.floor(100000 + Math.random() * 900000); navigate(`/teacher/manage-room/${quiz._id}/${randomSixDigit}`); - } - }; - - const handleShareQuiz = async (quiz: QuizType) => { - try { - const email = prompt( - `Veuillez saisir l'email de la personne avec qui vous souhaitez partager ce quiz`, - '' - ); - - if (email) { - const result = await ApiService.ShareQuiz(quiz._id, email); - - if (!result) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`); - return; - } - - window.alert(`Quiz partagé avec succès!`); - } - } catch (error) { - console.error('Erreur lors du partage du quiz:', error); } }; return (
-
Tableau de bord
+ {/* Conteneur pour le titre et le sélecteur de salle */} +
+ {/* Titre tableau de bord */} +
+ Tableau de bord +
-
- - handleSelectRoom(e)} + id="room-select" + style={{ + padding: '8px 12px', + fontSize: '14px', + borderRadius: '8px', + border: '1px solid #ccc', + backgroundColor: '#fff', + maxWidth: '200px', + cursor: 'pointer', + fontWeight: '500' + }} + > + - ))} - - - + {rooms.map((room) => ( + + ))} + + +
- {selectedRoom && ( -
-

Salle sélectionnée: {selectedRoom.title}

-
- )} - + {/* Dialog pour créer une salle */} setOpenAddRoomDialog(false)}> Créer une nouvelle salle @@ -473,24 +487,17 @@ const Dashboard: React.FC = () => { -
- - - - - - ) - }} - /> -
+
+ {/* Conteneur principal avec les actions et la liste des quiz */}
{ color="primary" value={selectedFolderId} onChange={handleSelectFolder} + sx={{ + padding: '6px 12px', + maxWidth: '180px', + borderRadius: '8px', + borderColor: '#e0e0e0', + '&:hover': { borderColor: '#5271FF' } + }} > - - - {folders.map((folder: FolderType) => ( + + {folders.map((folder) => ( ))} @@ -520,65 +532,130 @@ const Dashboard: React.FC = () => {
- - {' '} - {' '} - + + {' '} + {' '} +
- - {' '} - {' '} - + + {' '} + {' '} +
- - {' '} - {' '} - + + {' '} + {' '} +
-
- +
+
+ {!isSearchVisible ? ( + + + + ) : ( + + + + + + ) + }} + /> + )} +
- + {/* À droite : les boutons */} +
+ + + +
+
{Object.keys(quizzesByFolder).map((folderName) => ( @@ -587,14 +664,16 @@ const Dashboard: React.FC = () => { {quizzesByFolder[folderName].map((quiz: QuizType) => (
- +
@@ -603,7 +682,7 @@ const Dashboard: React.FC = () => {
- + downloadTxtFile(quiz)} @@ -613,7 +692,7 @@ const Dashboard: React.FC = () => { - + handleEditQuiz(quiz)} @@ -623,7 +702,7 @@ const Dashboard: React.FC = () => { - + handleDuplicateQuiz(quiz)} @@ -632,27 +711,20 @@ const Dashboard: React.FC = () => { {' '} +
+ +
- + handleRemoveQuiz(quiz)} > {' '} {' '} - - - handleShareQuiz(quiz)} - > - {' '} - {' '} - -
))} diff --git a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx index 89f822a..2b220da 100644 --- a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx +++ b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx @@ -1,5 +1,5 @@ // EditorQuiz.tsx -import React, { useState, useEffect, useRef, CSSProperties } from 'react'; +import React, { useState, useEffect, CSSProperties } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { FolderType } from '../../../Types/FolderType'; @@ -9,14 +9,15 @@ import GiftCheatSheet from 'src/components/GIFTCheatSheet/GiftCheatSheet'; import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview'; import { QuizType } from '../../../Types/QuizType'; - +import SaveIcon from '@mui/icons-material/Save'; import './editorQuiz.css'; -import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material'; +import { Button, TextField, NativeSelect, Divider } from '@mui/material'; import ReturnButton from 'src/components/ReturnButton/ReturnButton'; +import ImageGalleryModal from 'src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal'; import ApiService from '../../../services/ApiService'; import { escapeForGIFT } from '../../../utils/giftUtils'; -import { Upload } from '@mui/icons-material'; +import { ENV_VARIABLES } from 'src/constants'; interface EditQuizParams { id: string; @@ -38,8 +39,6 @@ const QuizForm: React.FC = () => { const handleSelectFolder = (event: React.ChangeEvent) => { setSelectedFolder(event.target.value); }; - const fileInputRef = useRef(null); - const [dialogOpen, setDialogOpen] = useState(false); const [showScrollButton, setShowScrollButton] = useState(false); const scrollToTop = () => { @@ -166,86 +165,75 @@ const QuizForm: React.FC = () => { return
Chargement...
; } - const handleSaveImage = async () => { - try { - const inputElement = document.getElementById('file-input') as HTMLInputElement; - - if (!inputElement?.files || inputElement.files.length === 0) { - setDialogOpen(true); - return; - } - - if (!inputElement.files || inputElement.files.length === 0) { - window.alert("Veuillez d'abord choisir une image à téléverser.") - return; - } - - const imageUrl = await ApiService.uploadImage(inputElement.files[0]); - - // Check for errors - if(imageUrl.indexOf("ERROR") >= 0) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) - return; - } - - setImageLinks(prevLinks => [...prevLinks, imageUrl]); - - // Reset the file input element - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } - } catch (error) { - window.alert(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`) - - } - }; - const handleCopyToClipboard = async (link: string) => { navigator.clipboard.writeText(link); } - return ( -
+ const handleCopyImage = (id: string) => { + const escLink = `${ENV_VARIABLES.BACKEND_URL}/api/image/get/${id}`; + setImageLinks(prevLinks => [...prevLinks, escLink]); + } -
+ return ( +
+
-
Éditeur de quiz
+ +
-
+
+
Éditeur de quiz
{/*

Éditeur

*/} - -
- {folders.map((folder: FolderType) => ( - - ))} - - - @@ -258,37 +246,11 @@ const QuizForm: React.FC = () => { onEditorChange={handleUpdatePreview} />
-
- - setDialogOpen(false)} > - Erreur - - Veuillez d'abord choisir une image à téléverser. - - - - - +
+

Mes images :

+
- -

Mes images :

+
(Voir section
@@ -302,7 +264,7 @@ const QuizForm: React.FC = () => {
    {imageLinks.map((link, index) => { - const imgTag = `![alt_text](${escapeForGIFT(link)} "texte de l'infobulle")`; + const imgTag = `[markdown]![alt_text](${escapeForGIFT(link)} "texte de l'infobulle") {T}`; return (
  • { const navigate = useNavigate(); const [socket, setSocket] = useState(null); const [students, setStudents] = useState([]); - const { quizId = '', roomName = '' } = useParams<{ quizId: string, roomName: string }>(); + const { quizId = '', roomName = '' } = useParams<{ quizId: string; roomName: string }>(); const [quizQuestions, setQuizQuestions] = useState(); const [quiz, setQuiz] = useState(null); const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher'); const [connectingError, setConnectingError] = useState(''); const [currentQuestion, setCurrentQuestion] = useState(undefined); const [quizStarted, setQuizStarted] = useState(false); - const [formattedRoomName, setFormattedRoomName] = useState(""); + const [formattedRoomName, setFormattedRoomName] = useState(''); const [newlyConnectedUser, setNewlyConnectedUser] = useState(null); + const roomUrl = `${window.location.origin}/student/join-room?roomName=${roomName}`; + const [showQrModal, setShowQrModal] = useState(false); + const [copied, setCopied] = useState(false); - // Handle the newly connected user in useEffect, because it needs state info + const handleCopy = () => { + navigator.clipboard.writeText(roomUrl).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }; + + // Handle the newly connected user in useEffect, because it needs state info // not available in the socket.on() callback useEffect(() => { if (newlyConnectedUser) { console.log(`Handling newly connected user: ${newlyConnectedUser.name}`); setStudents((prevStudents) => [...prevStudents, newlyConnectedUser]); - + // only send nextQuestion if the quiz has started if (!quizStarted) { console.log(`!quizStarted: returning.... `); return; } - + if (quizMode === 'teacher') { webSocketService.nextQuestion({ roomName: formattedRoomName, @@ -65,7 +80,7 @@ const ManageRoom: React.FC = () => { } else { console.error('Invalid quiz mode:', quizMode); } - + // Reset the newly connected user state setNewlyConnectedUser(null); } @@ -82,6 +97,15 @@ const ManageRoom: React.FC = () => { verifyLogin(); }, []); + useEffect(() => { + if (!roomName) { + console.error('Room name is missing!'); + return; + } + + console.log(`Joining room: ${roomName}`); + }, [roomName]); + useEffect(() => { if (!roomName || !quizId) { window.alert( @@ -96,7 +120,7 @@ const ManageRoom: React.FC = () => { return () => { disconnectWebSocket(); }; - }, [roomName, navigate]); + }, [roomName, navigate]); useEffect(() => { if (quizId) { @@ -143,8 +167,8 @@ const ManageRoom: React.FC = () => { setFormattedRoomName(roomNameUpper); console.log(`Creating WebSocket room named ${roomNameUpper}`); - /** - * ATTENTION: Lire les variables d'état dans + /** + * ATTENTION: Lire les variables d'état dans * les .on() n'est pas une bonne pratique. * Les valeurs sont celles au moment de la création * de la fonction et non au moment de l'exécution. @@ -184,7 +208,6 @@ const ManageRoom: React.FC = () => { }; useEffect(() => { - if (socket) { console.log(`Listening for submit-answer-room in room ${formattedRoomName}`); socket.on('submit-answer-room', (answerData: AnswerReceptionFromBackendType) => { @@ -258,10 +281,12 @@ const ManageRoom: React.FC = () => { if (nextQuestionIndex === undefined || nextQuestionIndex > quizQuestions.length - 1) return; setCurrentQuestion(quizQuestions[nextQuestionIndex]); - webSocketService.nextQuestion({roomName: formattedRoomName, - questions: quizQuestions, - questionIndex: nextQuestionIndex, - isLaunch: false}); + webSocketService.nextQuestion({ + roomName: formattedRoomName, + questions: quizQuestions, + questionIndex: nextQuestionIndex, + isLaunch: false + }); }; const previousQuestion = () => { @@ -271,7 +296,12 @@ const ManageRoom: React.FC = () => { if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return; setCurrentQuestion(quizQuestions[prevQuestionIndex]); - webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex: prevQuestionIndex, isLaunch: false}); + webSocketService.nextQuestion({ + roomName: formattedRoomName, + questions: quizQuestions, + questionIndex: prevQuestionIndex, + isLaunch: false + }); }; const initializeQuizQuestion = () => { @@ -299,7 +329,12 @@ const ManageRoom: React.FC = () => { } setCurrentQuestion(quizQuestions[0]); - webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex: 0, isLaunch: true}); + webSocketService.nextQuestion({ + roomName: formattedRoomName, + questions: quizQuestions, + questionIndex: 0, + isLaunch: true + }); }; const launchStudentMode = () => { @@ -336,73 +371,26 @@ const ManageRoom: React.FC = () => { if (quiz?.content && quizQuestions) { setCurrentQuestion(quizQuestions[questionIndex]); if (quizMode === 'teacher') { - webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex, isLaunch: false}); + webSocketService.nextQuestion({ + roomName: formattedRoomName, + questions: quizQuestions, + questionIndex, + isLaunch: false + }); } } }; + const finishQuiz = () => { + disconnectWebSocket(); + navigate('/teacher/dashboard'); + }; + const handleReturn = () => { disconnectWebSocket(); navigate('/teacher/dashboard'); }; - function checkIfIsCorrect( - answer: AnswerType, - idQuestion: number, - questions: QuestionType[] - ): boolean { - const questionInfo = questions.find((q) => - q.question.id ? q.question.id === idQuestion.toString() : false - ) as QuestionType | undefined; - - const answerText = answer.toString(); - if (questionInfo) { - const question = questionInfo.question as ParsedGIFTQuestion; - if (question.type === 'TF') { - return ( - (question.isTrue && answerText == 'true') || - (!question.isTrue && answerText == 'false') - ); - } else if (question.type === 'MC') { - return question.choices.some( - (choice) => choice.isCorrect && choice.formattedText.text === answerText - ); - } else if (question.type === 'Numerical') { - if (isHighLowNumericalAnswer(question.choices[0])) { - const choice = question.choices[0]; - const answerNumber = parseFloat(answerText); - if (!isNaN(answerNumber)) { - return ( - answerNumber <= choice.numberHigh && answerNumber >= choice.numberLow - ); - } - } - if (isRangeNumericalAnswer(question.choices[0])) { - const answerNumber = parseFloat(answerText); - const range = question.choices[0].range; - const correctAnswer = question.choices[0].number; - if (!isNaN(answerNumber)) { - return ( - answerNumber <= correctAnswer + range && - answerNumber >= correctAnswer - range - ); - } - } - if (isSimpleNumericalAnswer(question.choices[0])) { - const answerNumber = parseFloat(answerText); - if (!isNaN(answerNumber)) { - return answerNumber === question.choices[0].number; - } - } - } else if (question.type === 'Short') { - return question.choices.some( - (choice) => choice.text.toUpperCase() === answerText.toUpperCase() - ); - } - } - return false; - } - if (!formattedRoomName) { return (
    @@ -427,32 +415,95 @@ const ManageRoom: React.FC = () => { return (
    -

    Salle : {formattedRoomName}

    -
    + {/* En-tête avec bouton Disconnect à gauche et QR code à droite */} +
    + +
    + + setShowQrModal(false)} + aria-labelledby="qr-modal-title" + > + + Rejoindre la salle: {formattedRoomName} + + + + Scannez ce QR code ou partagez le lien ci-dessous pour rejoindre la salle : + + +
    + +
    + +
    +

    URL de participation :

    +

    {roomUrl}

    + +
    +
    + + + +
    + +
    - {( +

    + Salle : {formattedRoomName}
    - + {' '} {students.length}/60
    - )} +

    @@ -487,7 +538,6 @@ const ManageRoom: React.FC = () => { )} @@ -529,6 +579,11 @@ const ManageRoom: React.FC = () => {
    )} +
    + +
    ) : ( { if (!context) throw new Error('useRooms must be used within a RoomProvider'); return context; }; + +/** + * Checks if the answer is correct - logic varies by type of question! + * True/False: answer must match the isTrue property + * Multiple Choice: answer must match the correct choice(s) + * Numerical: answer must be within the range or equal to the number (for each type of correct answer) + * Short Answer: answer must match the correct choice(s) (case-insensitive) + * @param answer + * @param idQuestion + * @param questions + * @returns + */ +export function checkIfIsCorrect( + answer: AnswerType, + idQuestion: number, + questions: QuestionType[] +): boolean { + const questionInfo = questions.find((q) => + q.question.id ? q.question.id === idQuestion.toString() : false + ) as QuestionType | undefined; + + const simpleAnswerText = answer.toString(); + if (questionInfo) { + const question = questionInfo.question as ParsedGIFTQuestion; + if (question.type === 'TF') { + return ( + (question.isTrue && simpleAnswerText == 'true') || + (!question.isTrue && simpleAnswerText == 'false') + ); + } else if (question.type === 'MC') { + const correctChoices = question.choices.filter((choice) => choice.isCorrect + /* || (choice.weight && choice.weight > 0)*/ // handle weighted answers + ); + const multipleAnswers = Array.isArray(answer) ? answer : [answer as string]; + if (correctChoices.length === 0) { + return false; + } + // check if all (and only) correct choices are in the multipleAnswers array + return correctChoices.length === multipleAnswers.length && correctChoices.every( + (choice) => multipleAnswers.includes(choice.formattedText.text) + ); + } else if (question.type === 'Numerical') { + if (isMultipleNumericalAnswer(question.choices[0])) { // Multiple numerical answers + // check to see if answer[0] is a match for any of the choices that isCorrect + const correctChoices = question.choices.filter((choice) => isMultipleNumericalAnswer(choice) && choice.isCorrect); + if (correctChoices.length === 0) { // weird case where there are multiple numerical answers but none are correct + return false; + } + return correctChoices.some((choice) => { + // narrow choice to MultipleNumericalAnswer type + const multipleNumericalChoice = choice as MultipleNumericalAnswer; + return isCorrectNumericalAnswer(multipleNumericalChoice.answer, simpleAnswerText); + }); + } + if (isHighLowNumericalAnswer(question.choices[0])) { + // const choice = question.choices[0]; + // const answerNumber = parseFloat(simpleAnswerText); + // if (!isNaN(answerNumber)) { + // return ( + // answerNumber <= choice.numberHigh && answerNumber >= choice.numberLow + // ); + // } + return isCorrectNumericalAnswer(question.choices[0], simpleAnswerText); + } + if (isRangeNumericalAnswer(question.choices[0])) { + // const answerNumber = parseFloat(simpleAnswerText); + // const range = question.choices[0].range; + // const correctAnswer = question.choices[0].number; + // if (!isNaN(answerNumber)) { + // return ( + // answerNumber <= correctAnswer + range && + // answerNumber >= correctAnswer - range + // ); + // } + return isCorrectNumericalAnswer(question.choices[0], simpleAnswerText); + } + if (isSimpleNumericalAnswer(question.choices[0])) { + // const answerNumber = parseFloat(simpleAnswerText); + // if (!isNaN(answerNumber)) { + // return answerNumber === question.choices[0].number; + // } + return isCorrectNumericalAnswer(question.choices[0], simpleAnswerText); + } + } else if (question.type === 'Short') { + return question.choices.some( + (choice) => choice.text.toUpperCase() === simpleAnswerText.toUpperCase() + ); + } + } + return false; +} + +/** +* Determines if a numerical answer is correct based on the type of numerical answer. +* @param correctAnswer The correct answer (of type NumericalAnswer). +* @param userAnswer The user's answer (as a string or number). +* @returns True if the user's answer is correct, false otherwise. +*/ +export function isCorrectNumericalAnswer( + correctAnswer: NumericalAnswer, + userAnswer: string | number +): boolean { + const answerNumber = typeof userAnswer === 'string' ? parseFloat(userAnswer) : userAnswer; + + if (isNaN(answerNumber)) { + return false; // User's answer is not a valid number + } + + if (isSimpleNumericalAnswer(correctAnswer)) { + // Exact match for simple numerical answers + return answerNumber === correctAnswer.number; + } + + if (isRangeNumericalAnswer(correctAnswer)) { + // Check if the user's answer is within the range + const { number, range } = correctAnswer; + return answerNumber >= number - range && answerNumber <= number + range; + } + + if (isHighLowNumericalAnswer(correctAnswer)) { + // Check if the user's answer is within the high-low range + const { numberLow, numberHigh } = correctAnswer; + return answerNumber >= numberLow && answerNumber <= numberHigh; + } + + // if (isMultipleNumericalAnswer(correctAnswer)) { + // // Check if the user's answer matches any of the multiple numerical answers + // return correctAnswer.answer.some((choice) => + // isCorrectNumericalAnswer(choice, answerNumber) + // ); + // } + + return false; // Default to false if the answer type is not recognized +} diff --git a/client/src/pages/Teacher/Share/Share.tsx b/client/src/pages/Teacher/Share/Share.tsx index 0dc4fe7..2004b4d 100644 --- a/client/src/pages/Teacher/Share/Share.tsx +++ b/client/src/pages/Teacher/Share/Share.tsx @@ -1,66 +1,72 @@ -// EditorQuiz.tsx import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; - import { FolderType } from '../../../Types/FolderType'; - - import './share.css'; -import { Button, NativeSelect } from '@mui/material'; +import { Button, NativeSelect, Typography, Box } from '@mui/material'; import ReturnButton from 'src/components/ReturnButton/ReturnButton'; - import ApiService from '../../../services/ApiService'; +import SaveIcon from '@mui/icons-material/Save'; const Share: React.FC = () => { - console.log('Component rendered'); const navigate = useNavigate(); const { id } = useParams(); const [quizTitle, setQuizTitle] = useState(''); const [selectedFolder, setSelectedFolder] = useState(''); - const [folders, setFolders] = useState([]); + const [quizExists, setQuizExists] = useState(false); + const [loading, setLoading] = useState(true); useEffect(() => { const fetchData = async () => { - console.log("QUIZID : " + id) - if (!id) { - window.alert(`Une erreur est survenue.\n Le quiz n'a pas été trouvé\nVeuillez réessayer plus tard`) - console.error('Quiz not found for id:', id); + try { + if (!id) { + console.error('Quiz not found for id:', id); + navigate('/teacher/dashboard'); + return; + } + + if (!ApiService.isLoggedIn()) { + navigate("/login"); + return; + } + + const quizIds = await ApiService.getAllQuizIds(); + + if (quizIds.includes(id)) { + setQuizExists(true); + setLoading(false); + return; + } + + const userFolders = await ApiService.getUserFolders(); + + if (userFolders.length == 0) { + navigate('/teacher/dashboard'); + return; + } + + setFolders(userFolders as FolderType[]); + + const title = await ApiService.getSharedQuiz(id); + + if (!title) { + console.error('Quiz not found for id:', id); + navigate('/teacher/dashboard'); + return; + } + + setQuizTitle(title); + setLoading(false); + } catch (error) { + console.error('Error fetching data:', error); + setLoading(false); navigate('/teacher/dashboard'); - return; } - - if (!ApiService.isLoggedIn()) { - window.alert(`Vous n'êtes pas connecté.\nVeuillez vous connecter et revenir à ce lien`); - navigate("/login"); - return; - } - - const userFolders = await ApiService.getUserFolders(); - - if (userFolders.length == 0) { - window.alert(`Vous n'avez aucun dossier.\nVeuillez en créer un et revenir à ce lien`) - navigate('/teacher/dashboard'); - return; - } - - setFolders(userFolders as FolderType[]); - - const title = await ApiService.getSharedQuiz(id); - - if (!title) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) - console.error('Quiz not found for id:', id); - navigate('/teacher/dashboard'); - return; - } - - setQuizTitle(title); }; fetchData(); - }, []); + }, [id, navigate]); const handleSelectFolder = (event: React.ChangeEvent) => { setSelectedFolder(event.target.value); @@ -68,14 +74,12 @@ const Share: React.FC = () => { const handleQuizSave = async () => { try { - if (selectedFolder == '') { alert("Veuillez choisir un dossier"); return; } if (!id) { - window.alert(`Une erreur est survenue.\n Le quiz n'a pas été trouvé\nVeuillez réessayer plus tard`) console.error('Quiz not found for id:', id); navigate('/teacher/dashboard'); return; @@ -85,49 +89,92 @@ const Share: React.FC = () => { navigate('/teacher/dashboard'); } catch (error) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) console.log(error) } }; + if (loading) { + return
    Chargement...
    ; + } + + if (quizExists) { + return ( +
    +
    + +
    +
    Quiz déjà existant
    +
    +
    +
    + +
    + + + Le quiz que vous essayez d'importer existe déjà sur votre compte. + + + + + + Si vous souhaitiez créer une copie de ce quiz, + vous pouvez utiliser la fonction "Dupliquer" disponible + dans votre tableau de bord. + + +
    +
    + ); + } + return (
    -
    - -
    Importer quiz: {quizTitle}
    - +
    +
    Importation du Quiz: {quizTitle}
    +
    + Vous êtes sur le point d'importer le quiz {quizTitle}, choisissez un dossier dans lequel enregistrer ce nouveau quiz. +
    +
    - -
    - +
    - {folders.map((folder: FolderType) => ( ))} - -
    -
    -
    ); }; -export default Share; +export default Share; \ No newline at end of file diff --git a/client/src/pages/Teacher/Share/share.css b/client/src/pages/Teacher/Share/share.css index 119b645..146f318 100644 --- a/client/src/pages/Teacher/Share/share.css +++ b/client/src/pages/Teacher/Share/share.css @@ -3,19 +3,58 @@ display: flex; flex-direction: row; justify-content: space-between; - align-content: stretch + align-content: stretch; } + .quizImport .importHeader .returnButton { flex-basis: 10%; - display: flex; justify-content: center; } -.quizImport .importHeader .title { +.quizImport .importHeader .titleContainer { flex-basis: auto; + text-align: center; /* Center the title and subtitle */ +} + +.quizImport .importHeader .mainTitle { + font-size: 44px; /* Larger font size for the main title */ + font-weight: bold; /* Remove bold */ + color: #333; /* Slightly paler color */ +} + +.quizImport .importHeader .subTitle { + font-size: 14px; /* Smaller font size for the subtitle */ + color: #666; /* Pale gray color */ + margin-top: 8px; /* Add some space between the title and subtitle */ } .quizImport .importHeader .dumb { flex-basis: 10%; +} + +.quizImport .editSection { + display: flex; + justify-content: center; + align-items: center; + margin-top: 20px; +} + +.quizImport .editSection .formContainer { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; /* Adds space between the select and button */ + width: 100%; + max-width: 400px; /* Limits the width of the form container */ +} + +.quizImport .editSection .folderSelect { + width: 100%; /* Ensures the select element takes the full width of its container */ +} + +.quizImport .editSection .saveButton { + width: 100%; /* Makes the button take the full width of its container */ + padding: 10px 20px; /* Increases the button's padding for a larger appearance */ + font-size: 16px; /* Increases the font size for better visibility */ } \ No newline at end of file diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index 2661d24..8aa6ea5 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -76,8 +76,6 @@ class ApiService { return false; } - console.log("ApiService: isLoggedIn: Token:", token); - // Update token expiry this.saveToken(token); @@ -93,7 +91,6 @@ class ApiService { } try { - console.log("ApiService: isLoggedInTeacher: Token:", token); const decodedToken = jwtDecode(token) as { roles: string[] }; /////// REMOVE BELOW @@ -105,7 +102,6 @@ class ApiService { const userRoles = decodedToken.roles; const requiredRole = 'teacher'; - console.log("ApiService: isLoggedInTeacher: UserRoles:", userRoles); if (!userRoles || !userRoles.includes(requiredRole)) { return false; } @@ -165,6 +161,22 @@ class ApiService { return object.username; } + public getUserID(): string { + const token = localStorage.getItem("jwt"); + + if (!token) { + return ""; + } + + const jsonObj = jwtDecode(token) as { userId: string }; + + if (!jsonObj.userId) { + return ""; + } + + return jsonObj.userId; + } + // Route to know if rooms need authentication to join public async getRoomsRequireAuth(): Promise { const url: string = this.constructRequestUrl(`/auth/getRoomsRequireAuth`); @@ -201,7 +213,6 @@ class ApiService { const result: AxiosResponse = await axios.post(url, body, { headers: headers }); - console.log(result); if (result.status == 200) { //window.location.href = result.request.responseURL; window.location.href = '/login'; @@ -213,7 +224,6 @@ class ApiService { return true; } catch (error) { - console.log("Error details: ", error); if (axios.isAxiosError(error)) { const err = error as AxiosError; @@ -576,7 +586,6 @@ public async login(email: string, password: string): Promise { const headers = this.constructRequestHeaders(); const body = { folderId }; - console.log(headers); const result: AxiosResponse = await axios.post(url, body, { headers: headers }); if (result.status !== 200) { @@ -866,36 +875,6 @@ public async login(email: string, password: string): Promise { } } - async ShareQuiz(quizId: string, email: string): Promise { - try { - if (!quizId || !email) { - throw new Error(`quizId and email are required.`); - } - - const url: string = this.constructRequestUrl(`/quiz/Share`); - const headers = this.constructRequestHeaders(); - const body = { quizId, email }; - - const result: AxiosResponse = await axios.put(url, body, { headers: headers }); - - if (result.status !== 200) { - throw new Error(`Update and share quiz failed. Status: ${result.status}`); - } - - return true; - } catch (error) { - console.log("Error details: ", error); - - if (axios.isAxiosError(error)) { - const err = error as AxiosError; - const data = err.response?.data as { error: string } | undefined; - return data?.error || 'Unknown server error during request.'; - } - - return `An unexpected error occurred.`; - } - } - async getSharedQuiz(quizId: string): Promise { try { if (!quizId) { @@ -1192,33 +1171,11 @@ public async login(email: string, password: string): Promise { } } - public async getUsers(): Promise { - try { - - const url: string = this.constructRequestUrl(`/admin/getUsers`); - const headers = this.constructRequestHeaders(); - const result: AxiosResponse = await axios.get(url, { headers }); - - if (result.status !== 200) { - throw new Error(`L'obtention des titres des salles a échoué. Status: ${result.status}`); - } - - return result.data.users; - } catch (error) { - console.log("Error details: ", error); - - if (axios.isAxiosError(error)) { - const err = error as AxiosError; - const data = err.response?.data as { error: string } | undefined; - const msg = data?.error || 'Erreur serveur inconnue lors de la requête.'; - throw new Error(`L'enregistrement a échoué. Status: ${msg}`); - } - - throw new Error(`ERROR : Une erreur inattendue s'est produite.`); - } + public async getImages(page: number, limit: number): Promise { + return this.isAdmin() ? this.getAllImages(page, limit) : this.getUserImages(page, limit); } - public async getImages(page: number, limit: number): Promise { + public async getAllImages(page: number, limit: number): Promise { try { const url: string = this.constructRequestUrl(`/admin/getImages`); const headers = this.constructRequestHeaders(); @@ -1247,7 +1204,126 @@ public async login(email: string, password: string): Promise { } } + public async getUserImages(page: number, limit: number): Promise { + try { + const url: string = this.constructRequestUrl(`/image/getUserImages`); + const headers = this.constructRequestHeaders(); + let params : ImagesParams = { page: page, limit: limit }; + + const uid = this.getUserID(); + if(uid !== ''){ + params.uid = uid; + } + + const result: AxiosResponse = await axios.get(url, { params: params, headers: headers }); + + if (result.status !== 200) { + throw new Error(`L'affichage des images de l'utilisateur a échoué. Status: ${result.status}`); + } + const images = result.data; + + return images; + + } catch (error) { + console.log("Error details: ", error); + + if (axios.isAxiosError(error)) { + const err = error as AxiosError; + const data = err.response?.data as { error: string } | undefined; + const msg = data?.error || 'Erreur serveur inconnue lors de la requête.'; + throw new Error(`L'enregistrement a échoué. Status: ${msg}`); + } + + throw new Error(`ERROR : Une erreur inattendue s'est produite.`); + } + } + public async deleteImage(imgId: string): Promise { + return this.isAdmin() ? this.deleteAnyImage(imgId) : this.deleteUserImage(imgId); + } + + public async deleteUserImage(imgId: string): Promise { + try { + const url: string = this.constructRequestUrl(`/image/delete`); + const headers = this.constructRequestHeaders(); + const uid = this.getUserID(); + let params = { uid: uid, imgId: imgId }; + + const result: AxiosResponse = await axios.delete(url, { params: params, headers: headers }); + + if (result.status !== 200) { + throw new Error(`La suppression de l'image a échoué. Status: ${result.status}`); + } + + const deleted = result.data.deleted; + return deleted; + + } catch (error) { + console.log("Error details: ", error); + + if (axios.isAxiosError(error)) { + const err = error as AxiosError; + const data = err.response?.data as { error: string } | undefined; + const msg = data?.error || 'Erreur serveur inconnue lors de la requête.'; + throw new Error(`L'enregistrement a échoué. Status: ${msg}`); + } + + throw new Error(`ERROR : Une erreur inattendue s'est produite.`); + } + } + + public async getAllQuizIds(): Promise { + try { + const folders = await this.getUserFolders(); + + const allQuizIds: string[] = []; + + if (Array.isArray(folders)) { + for (const folder of folders) { + const folderQuizzes = await this.getFolderContent(folder._id); + + if (Array.isArray(folderQuizzes)) { + allQuizIds.push(...folderQuizzes.map(quiz => quiz._id)); + } + } + } else { + console.error('Failed to get user folders:', folders); + } + + return allQuizIds; + } catch (error) { + console.error('Failed to get all quiz ids:', error); + throw error; + } + } + + public async getUsers(): Promise { + try { + + const url: string = this.constructRequestUrl(`/admin/getUsers`); + const headers = this.constructRequestHeaders(); + const result: AxiosResponse = await axios.get(url, { headers }); + + if (result.status !== 200) { + throw new Error(`L'obtention des titres des salles a échoué. Status: ${result.status}`); + } + + return result.data.users; + } catch (error) { + console.log("Error details: ", error); + + if (axios.isAxiosError(error)) { + const err = error as AxiosError; + const data = err.response?.data as { error: string } | undefined; + const msg = data?.error || 'Erreur serveur inconnue lors de la requête.'; + throw new Error(`L'enregistrement a échoué. Status: ${msg}`); + } + + throw new Error(`ERROR : Une erreur inattendue s'est produite.`); + } + } + + public async deleteAnyImage(imgId: string): Promise { try { const url: string = this.constructRequestUrl(`/admin/deleteImage`); const headers = this.constructRequestHeaders(); diff --git a/docker-compose-local.yaml b/docker-compose-local.yaml index 0fc505b..f9fc01f 100644 --- a/docker-compose-local.yaml +++ b/docker-compose-local.yaml @@ -55,7 +55,7 @@ services: image: mongo container_name: mongo ports: - - "27017:27017" + - "27019:27017" tty: true volumes: - mongodb_data:/data/db diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..144d2f8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,329 @@ +{ + "name": "EvalueTonSavoir", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "axios-mock-adapter": "^2.1.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "peer": true + }, + "node_modules/axios": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", + "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", + "peer": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios-mock-adapter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.1.0.tgz", + "integrity": "sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "peerDependencies": { + "axios": ">= 0.17.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "peer": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "peer": true, + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "peer": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a8332a4 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "axios-mock-adapter": "^2.1.0" + } +} diff --git a/server/__tests__/image.test.js b/server/__tests__/image.test.js index 488d27b..7ce4ad1 100644 --- a/server/__tests__/image.test.js +++ b/server/__tests__/image.test.js @@ -7,6 +7,9 @@ // const BASE_URL = '/image' +const Images = require('../models/images'); +const ObjectId = require('mongodb').ObjectId; + describe.skip("POST /upload", () => { describe("when the jwt is not sent", () => { @@ -64,3 +67,289 @@ describe.skip("GET /get", () => { }) }) + +jest.mock('mongodb', () => { + const originalModule = jest.requireActual('mongodb'); + return { + ...originalModule, + ObjectId: { + ...originalModule.ObjectId, + createFromHexString: jest.fn().mockReturnValue('507f191e810c19729de860ea'), // Return a valid 24-character ObjectId string + }, + }; +}); + +describe('Images', () => { + let db; + let images; + let dbConn; + let mockImagesCollection; + let mockFindCursor; + + beforeEach(() => { + + const mockImagesCursor = { + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + toArray: jest.fn() + }; + + const mockFilesCursor = { + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + toArray: jest.fn() + }; + + mockImagesCollection = { + insertOne: jest.fn().mockResolvedValue({ insertedId: 'image123' }), + findOne: jest.fn(), + find: jest.fn().mockReturnValue(mockImagesCursor), + countDocuments: jest.fn(), + deleteOne: jest.fn() + }; + + mockFilesCollection = { + find: jest.fn().mockReturnValue(mockFilesCursor) + }; + + dbConn = { + collection: jest.fn((name) => { + if (name === 'images') { + return mockImagesCollection; + } else if (name === 'files') { + return mockFilesCollection; + } + }) + }; + + db = { + connect: jest.fn().mockResolvedValue(), + getConnection: jest.fn().mockReturnValue(dbConn) + }; + + images = new Images(db); + }); + + describe('upload', () => { + it('should upload an image and return the inserted ID', async () => { + const testFile = { originalname: 'test.png', buffer: Buffer.from('dummydata'), mimetype: 'image/png' }; + const userId = 'user123'; + + const result = await images.upload(testFile, userId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(dbConn.collection).toHaveBeenCalledWith('images'); + expect(mockImagesCollection.insertOne).toHaveBeenCalledWith({ + userId: userId, + file_name: 'test.png', + file_content: testFile.buffer.toString('base64'), + mime_type: 'image/png', + created_at: expect.any(Date) + }); + expect(result).toBe('image123'); + }); + }); + + describe('get', () => { + it('should retrieve the image if found', async () => { + const imageId = '65d9a8f9b5e8d1a5e6a8c9f0'; + const testImage = { + file_name: 'test.png', + file_content: Buffer.from('dummydata').toString('base64'), + mime_type: 'image/png' + }; + mockImagesCollection.findOne.mockResolvedValue(testImage); + + const result = await images.get(imageId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(dbConn.collection).toHaveBeenCalledWith('images'); + expect(mockImagesCollection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromHexString(imageId) }); + expect(result).toEqual({ + file_name: 'test.png', + file_content: Buffer.from('dummydata'), + mime_type: 'image/png' + }); + }); + + it('should return null if image is not found', async () => { + const imageId = '65d9a8f9b5e8d1a5e6a8c9f0'; + mockImagesCollection.findOne.mockResolvedValue(null); + + const result = await images.get(imageId); + + expect(result).toBeNull(); + }); + }); + + describe('getImages', () => { + it('should retrieve a paginated list of images', async () => { + const mockImages = [ + { _id: '1', userId: 'user1', file_name: 'image1.png', file_content: Buffer.from('data1'), mime_type: 'image/png' }, + { _id: '2', userId: 'user2', file_name: 'image2.png', file_content: Buffer.from('data2'), mime_type: 'image/png' } + ]; + + mockImagesCollection.countDocuments.mockResolvedValue(2); + // Create a mock cursor for images collection + const mockFindCursor = { + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + toArray: jest.fn().mockResolvedValue(mockImages), // Return mock images when toArray is called + }; + + // Mock the find method to return the mock cursor + mockImagesCollection.find.mockReturnValue(mockFindCursor); + const result = await images.getImages(1, 10); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(dbConn.collection).toHaveBeenCalledWith('images'); + expect(mockImagesCollection.find).toHaveBeenCalledWith({}); + expect(mockFindCursor.sort).toHaveBeenCalledWith({ created_at: 1 }); + expect(mockFindCursor.skip).toHaveBeenCalledWith(0); + expect(mockFindCursor.limit).toHaveBeenCalledWith(10); + expect(result).toEqual({ + images: [ + { id: '1', user: 'user1', file_name: 'image1.png', file_content: 'ZGF0YTE=', mime_type: 'image/png' }, + { id: '2', user: 'user2', file_name: 'image2.png', file_content: 'ZGF0YTI=', mime_type: 'image/png' } + ], + total: 2, + }); + }); + + it('should return an empty array if no images are found', async () => { + mockImagesCollection.countDocuments.mockResolvedValue(0); + + const result = await images.getImages(1, 10); + + expect(result).toEqual({ images: [], total: 0 }); + }); + }); + + describe('getUserImages', () => { + it('should return empty images array when no images exist', async () => { + mockImagesCollection.countDocuments.mockResolvedValue(0); + + const result = await images.getUserImages(1, 10, 'user123'); + + expect(result).toEqual({ images: [], total: 0 }); + expect(db.connect).toHaveBeenCalled(); + expect(mockImagesCollection.countDocuments).toHaveBeenCalledWith({ userId: 'user123' }); + }); + + it('should return images when they exist', async () => { + const mockImages = [ + { + _id: 'img1', + userId: 'user123', + file_name: 'image1.png', + file_content: Buffer.from('testdata'), + mime_type: 'image/png' + } + ]; + + mockImagesCollection.countDocuments.mockResolvedValue(1); + mockImagesCollection.find.mockReturnValue({ + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + toArray: jest.fn().mockResolvedValue(mockImages) + }); + + const result = await images.getUserImages(1, 10, 'user123'); + + expect(result).toEqual({ + images: [ + { + id: 'img1', + user: 'user123', + file_name: 'image1.png', + file_content: Buffer.from('testdata').toString('base64'), + mime_type: 'image/png' + } + ], + total: 1 + }); + expect(db.connect).toHaveBeenCalled(); + expect(mockImagesCollection.countDocuments).toHaveBeenCalledWith({ userId: 'user123' }); + }); + }); + describe('delete', () => { + it('should not delete the image when it exists in the files collection', async () => { + const uid = 'user123'; + const imgId = '507f191e810c19729de860ea'; // A valid 24-character ObjectId string + + // Mock the files collection cursor to simulate an image found + const mockFilesCursor = { + toArray: jest.fn().mockResolvedValue([{ _id: imgId }]) // Image found + }; + + mockFilesCollection.find.mockReturnValue(mockFilesCursor); + mockImagesCollection.deleteOne.mockResolvedValue({ deletedCount: 0 }); + + const result = await images.delete(uid, imgId); + + // Ensure the files collection is queried + expect(dbConn.collection).toHaveBeenCalledWith('files'); + expect(mockFilesCollection.find).toHaveBeenCalledWith({ + userId: uid, + content: { $regex: new RegExp(`/api/image/get/${imgId}`) }, + }); + + // Ensure the images collection is queried for deletion + expect(dbConn.collection).toHaveBeenCalledWith('files'); + expect(mockImagesCollection.deleteOne).not.toHaveBeenCalledWith({ + _id: ObjectId.createFromHexString(imgId), // Ensure the ObjectId is created correctly + }); + + expect(result).toEqual({ deleted: false }); + }); + + it('should delete the image if not found in the files collection', async () => { + const uid = 'user123'; + const imgId = '507f191e810c19729de860ea'; + + // Mock the files collection cursor to simulate the image not being found + const mockFindCursor = { + toArray: jest.fn().mockResolvedValue([]) // Empty array means image not found + }; + + mockFilesCollection.find.mockReturnValue(mockFindCursor); + mockImagesCollection.deleteOne.mockResolvedValue({ deletedCount: 1 }); + + const result = await images.delete(uid, imgId); + + // Ensure the deleteOne is not called if the image is not found + expect(mockImagesCollection.deleteOne).toHaveBeenCalled(); + expect(result).toEqual({ deleted: true }); + }); + + it('should return false if the delete operation fails in the images collection', async () => { + const uid = 'user123'; + const imgId = '507f191e810c19729de860ea'; + + // Mock the files collection cursor to simulate the image being found + const mockFindCursor = { + toArray: jest.fn().mockResolvedValue([{ _id: imgId }]) // Image found + }; + + mockFilesCollection.find.mockReturnValue(mockFindCursor); + mockImagesCollection.deleteOne.mockResolvedValue({ deletedCount: 0 }); // Simulate failure + + const result = await images.delete(uid, imgId); + + // Ensure the images collection deletion is called + expect(mockImagesCollection.deleteOne).not.toHaveBeenCalledWith({ + _id: ObjectId.createFromHexString(imgId), // Ensure the ObjectId is created correctly + }); + + expect(result).toEqual({ deleted: false }); + }); + + }); +}); \ No newline at end of file diff --git a/server/controllers/images.js b/server/controllers/images.js index b77ed96..7de02ad 100644 --- a/server/controllers/images.js +++ b/server/controllers/images.js @@ -50,6 +50,58 @@ class ImagesController { } }; + getImages = async (req, res, next) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 5; + const images = await this.images.getImages(page, limit); + + if (!images || images.length === 0) { + throw new AppError(IMAGE_NOT_FOUND); + } + + res.setHeader('Content-Type', 'application/json'); + return res.status(200).json(images); + } catch (error) { + return next(error); + } + }; + + getUserImages = async (req, res, next) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 5; + const uid = req.query.uid; + const images = await this.images.getUserImages(page, limit, uid); + + if (!images || images.length === 0) { + throw new AppError(IMAGE_NOT_FOUND); + } + + res.setHeader('Content-Type', 'application/json'); + return res.status(200).json(images); + } catch (error) { + return next(error); + } + }; + + delete = async (req, res, next) => { + try { + const uid = req.query.uid; + const imgId = req.query.imgId; + + if (!uid || !imgId) { + throw new AppError(MISSING_REQUIRED_PARAMETER); + } + const images = await this.images.delete(uid, imgId); + + res.setHeader('Content-Type', 'application/json'); + return res.status(200).json(images); + } catch (error) { + return next(error); + } + }; + } module.exports = ImagesController; diff --git a/server/models/images.js b/server/models/images.js index 67a6583..e3b6afd 100644 --- a/server/models/images.js +++ b/server/models/images.js @@ -42,6 +42,84 @@ class Images { }; } + async getImages(page, limit) { + await this.db.connect() + const conn = this.db.getConnection(); + + const imagesCollection = conn.collection('images'); + + + const total = await imagesCollection.countDocuments(); + if (!total || total === 0) return { images: [], total }; + + const result = await imagesCollection.find({}) + .sort({ created_at: 1 }) + .skip((page - 1) * limit) + .limit(limit) + .toArray(); + + const objImages = result.map(image => ({ + id: image._id, + user: image.userId, + file_name: image.file_name, + file_content: image.file_content.toString('base64'), + mime_type: image.mime_type + })); + + let respObj = { + images: objImages, + total: total + } + + return respObj; + } + + async getUserImages(page, limit, uid) { + await this.db.connect() + const conn = this.db.getConnection(); + const imagesCollection = conn.collection('images'); + const total = await imagesCollection.countDocuments({ userId: uid }); + if (!total || total === 0) return { images: [], total }; + + const result = await imagesCollection.find({ userId: uid }) + .sort({ created_at: -1 }) + .skip((page - 1) * limit) + .limit(limit) + .toArray(); + + const objImages = result.map(image => ({ + id: image._id, + user: image.userId, + file_name: image.file_name, + file_content: image.file_content.toString('base64'), + mime_type: image.mime_type + })); + + let respObj = { + images: objImages, + total: total + } + + return respObj; + } + + async delete(uid, imgId) { + let resp = false; + await this.db.connect() + const conn = this.db.getConnection(); + const quizColl = conn.collection('files'); + const rgxImg = new RegExp(`/api/image/get/${imgId}`); + + const result = await quizColl.find({ userId: uid, content: { $regex: rgxImg }}).toArray(); + if(!result || result.length < 1){ + const imgsColl = conn.collection('images'); + const isDeleted = await imgsColl.deleteOne({ _id: ObjectId.createFromHexString(imgId) }); + if(isDeleted){ + resp = true; + } + } + return { deleted: resp }; + } } module.exports = Images; diff --git a/server/package-lock.json b/server/package-lock.json index 0f006f8..66bd45a 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -24,6 +24,7 @@ "passport-oauth2": "^1.8.0", "passport-openidconnect": "^0.1.2", "patch-package": "^8.0.0", + "qrcode.react": "^4.2.0", "socket.io": "^4.7.2", "socket.io-client": "^4.7.2" }, @@ -38,7 +39,7 @@ "supertest": "^6.3.4" }, "engines": { - "node": "20.x" + "node": "22.x" } }, "node_modules/@ampproject/remapping": { @@ -55,68 +56,20 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/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/code-frame/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/code-frame/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/code-frame/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/code-frame/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/compat-data": { "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", @@ -342,19 +295,21 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -369,88 +324,28 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "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/parser": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", - "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.10" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -636,14 +531,15 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" @@ -704,14 +600,14 @@ "dev": true }, "node_modules/@babel/types": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", - "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -4772,7 +4668,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { "version": "3.14.1", @@ -5954,6 +5851,15 @@ } ] }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -5998,6 +5904,16 @@ "node": ">= 0.8" } }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -6683,15 +6599,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", diff --git a/server/package.json b/server/package.json index cd0348b..0eaf6d2 100644 --- a/server/package.json +++ b/server/package.json @@ -28,6 +28,7 @@ "passport-oauth2": "^1.8.0", "passport-openidconnect": "^0.1.2", "patch-package": "^8.0.0", + "qrcode.react": "^4.2.0", "socket.io": "^4.7.2", "socket.io-client": "^4.7.2" }, @@ -42,7 +43,7 @@ "supertest": "^6.3.4" }, "engines": { - "node": "20.x" + "node": "22.x" }, "jest": { "testEnvironment": "node", diff --git a/server/routers/images.js b/server/routers/images.js index 06e2830..94d2802 100644 --- a/server/routers/images.js +++ b/server/routers/images.js @@ -12,5 +12,8 @@ const upload = multer({ storage: storage }); router.post("/upload", jwt.authenticate, upload.single('image'), asyncHandler(images.upload)); router.get("/get/:id", asyncHandler(images.get)); +router.get("/getImages", asyncHandler(images.getImages)); +router.get("/getUserImages", jwt.authenticate, asyncHandler(images.getUserImages)); +router.delete("/delete", jwt.authenticate, asyncHandler(images.delete)); module.exports = router;