diff --git a/package-lock.json b/package-lock.json index 0673ef5..55ae2d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "devDependencies": { "@playwright/test": "^1.60.0", "@tailwindcss/postcss": "^4.1.18", + "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", @@ -35,6 +36,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^4.5.2", + "@vitest/coverage-v8": "^3.2.4", "autoprefixer": "^10.4.23", "eslint": "^9", "eslint-config-next": "16.1.4", @@ -66,6 +68,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -118,7 +134,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -380,6 +395,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -468,7 +493,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -492,7 +516,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1646,6 +1669,34 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1901,13 +1952,23 @@ "node": ">=12.4.0" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@playwright/test": { "version": "1.60.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "playwright": "1.60.0" }, @@ -2663,7 +2724,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2943,7 +3003,6 @@ "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2952,9 +3011,8 @@ "version": "19.2.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2965,7 +3023,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3021,7 +3078,6 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -3536,6 +3592,40 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -3657,7 +3747,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3950,6 +4039,25 @@ "dev": true, "license": "MIT" }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -4106,7 +4214,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4427,7 +4534,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/d3-array": { @@ -4778,6 +4885,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.279", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz", @@ -5084,7 +5198,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5270,7 +5383,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5671,6 +5783,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -5858,6 +5987,28 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -5871,6 +6022,32 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -6045,6 +6222,13 @@ "node": ">=18" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -6364,6 +6548,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -6616,6 +6810,60 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -6634,6 +6882,22 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -6670,7 +6934,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -7182,6 +7445,47 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -7249,6 +7553,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/motion-dom": { "version": "12.29.2", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.29.2.tgz", @@ -7630,6 +7944,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7683,6 +8004,30 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -7743,7 +8088,7 @@ "version": "1.60.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.60.0" @@ -7762,7 +8107,7 @@ "version": "1.60.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -7816,7 +8161,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -7998,13 +8342,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -8060,7 +8397,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8070,7 +8406,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -8083,7 +8418,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -8105,18 +8439,17 @@ } }, "node_modules/react-is": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", - "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", - "license": "MIT", - "peer": true + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -8229,8 +8562,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -8723,6 +9055,19 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -8767,6 +9112,60 @@ "node": ">= 0.4" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -8880,6 +9279,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -9130,6 +9572,60 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -9214,7 +9710,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -9470,7 +9965,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9649,7 +10143,6 @@ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -9766,7 +10259,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10053,6 +10545,91 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/ws": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", @@ -10117,7 +10694,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 7f6e5ef..75ddaec 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,14 @@ "name": "veritix-web", "version": "0.1.0", "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "eslint", - "test": "vitest run --coverage", - "test:watch": "vitest" - }, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint", + "test": "vitest run --coverage", + "test:watch": "vitest" + }, "dependencies": { "@hookform/resolvers": "^5.2.2", "@radix-ui/react-slot": "^1.2.4", @@ -31,6 +31,7 @@ "devDependencies": { "@playwright/test": "^1.60.0", "@tailwindcss/postcss": "^4.1.18", + "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", @@ -38,6 +39,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^4.5.2", + "@vitest/coverage-v8": "^3.2.4", "autoprefixer": "^10.4.23", "eslint": "^9", "eslint-config-next": "16.1.4", diff --git a/src/__tests__/auth-forms.test.tsx b/src/__tests__/auth-forms.test.tsx index f18077c..2447c59 100644 --- a/src/__tests__/auth-forms.test.tsx +++ b/src/__tests__/auth-forms.test.tsx @@ -21,11 +21,12 @@ vi.mock("next/navigation", () => ({ vi.mock("react-toastify", () => ({ toast: { success: vi.fn(), error: vi.fn() } })); vi.mock("framer-motion", () => { - const React = require("react"); - const motion: Record & { children?: React.ReactNode }>> = {}; - ["div", "form", "h2", "p"].forEach((tag) => { - motion[tag] = ({ children, ...rest }) => React.createElement(tag, rest, children); - }); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const React = require("react") as typeof import("react"); + const tags = ["div", "form", "h2", "p"] as const; + const motion = Object.fromEntries( + tags.map((tag) => [tag, ({ children, ...rest }: React.HTMLAttributes & { children?: React.ReactNode }) => React.createElement(tag, rest, children)]) + ); return { motion, AnimatePresence: ({ children }: { children: React.ReactNode }) => children }; }); diff --git a/src/__tests__/dashboard-revenue-trend.test.ts b/src/__tests__/dashboard-revenue-trend.test.ts new file mode 100644 index 0000000..f84b51c --- /dev/null +++ b/src/__tests__/dashboard-revenue-trend.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from 'vitest' + +/** + * Unit tests for the week-over-week revenue trend computation used in + * dashboard/page.tsx (issue #364). + * + * The logic is extracted here for isolation: + * - Split data.revenue into current-week (last 7) and prior-week (prev 7) + * - trend = ((currentWeekTotal - lastWeekTotal) / lastWeekTotal) * 100 + * - Returns null when fewer than 14 data points or lastWeek total is 0 + */ +function computeRevenueTrend( + revenue: { day: string; revenue: number }[] +): number | null { + if (revenue.length < 14) return null + const currentWeek = revenue.slice(-7).reduce((sum, d) => sum + d.revenue, 0) + const lastWeek = revenue.slice(-14, -7).reduce((sum, d) => sum + d.revenue, 0) + if (lastWeek === 0) return null + return ((currentWeek - lastWeek) / lastWeek) * 100 +} + +function makeRevenue(values: number[]): { day: string; revenue: number }[] { + return values.map((revenue, i) => ({ day: `day-${i}`, revenue })) +} + +describe('computeRevenueTrend', () => { + it('returns null when revenue array has fewer than 14 entries', () => { + expect(computeRevenueTrend(makeRevenue([100, 200]))).toBeNull() + expect(computeRevenueTrend(makeRevenue(Array(13).fill(100)))).toBeNull() + }) + + it('returns null when last-week total is 0 (avoid division by zero)', () => { + const revenue = makeRevenue([...Array(7).fill(0), ...Array(7).fill(200)]) + expect(computeRevenueTrend(revenue)).toBeNull() + }) + + it('returns positive trend when current week outperforms last week', () => { + // last week: 7 × 100 = 700, current week: 7 × 200 = 1400 → +100% + const revenue = makeRevenue([...Array(7).fill(100), ...Array(7).fill(200)]) + const trend = computeRevenueTrend(revenue) + expect(trend).toBeCloseTo(100) + }) + + it('returns negative trend when current week underperforms last week', () => { + // last week: 7 × 200 = 1400, current week: 7 × 100 = 700 → -50% + const revenue = makeRevenue([...Array(7).fill(200), ...Array(7).fill(100)]) + const trend = computeRevenueTrend(revenue) + expect(trend).toBeCloseTo(-50) + }) + + it('returns 0 when both weeks are identical', () => { + const revenue = makeRevenue(Array(14).fill(500)) + expect(computeRevenueTrend(revenue)).toBeCloseTo(0) + }) + + it('uses only the last 14 entries when the array is longer', () => { + // Prepend noise; only the last 14 matter + const noise = Array(20).fill(9999) + const signal = [...Array(7).fill(100), ...Array(7).fill(150)] + const revenue = makeRevenue([...noise, ...signal]) + const trend = computeRevenueTrend(revenue) + // 150/100 - 1 = +50% + expect(trend).toBeCloseTo(50) + }) +}) diff --git a/src/__tests__/event-creation.test.tsx b/src/__tests__/event-creation.test.tsx index 9f8629b..5912516 100644 --- a/src/__tests__/event-creation.test.tsx +++ b/src/__tests__/event-creation.test.tsx @@ -8,7 +8,8 @@ vi.mock("next/link", () => ({ {children}, })); vi.mock("framer-motion", () => { - const React = require("react"); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const React = require("react") as typeof import("react"); const proxy = new Proxy({}, { get: (_t, tag: string) => ({ children, ...rest }: React.HTMLAttributes & { children?: React.ReactNode }) => diff --git a/src/__tests__/event-discovery.test.tsx b/src/__tests__/event-discovery.test.tsx index 78c9c42..b5451b7 100644 --- a/src/__tests__/event-discovery.test.tsx +++ b/src/__tests__/event-discovery.test.tsx @@ -17,7 +17,8 @@ vi.mock("@/lib/eventsApi", () => ({ fetchEventById: (id: string) => Promise.resolve(mockEvents.find((e) => e.id === id) ?? null), })); vi.mock("framer-motion", () => { - const React = require("react"); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const React = require("react") as typeof import("react"); const proxy = new Proxy({}, { get: (_t, tag: string) => ({ children, ...rest }: React.HTMLAttributes & { children?: React.ReactNode }) => diff --git a/src/__tests__/profile-edit.test.tsx b/src/__tests__/profile-edit.test.tsx index 1ffb27f..72ed5b1 100644 --- a/src/__tests__/profile-edit.test.tsx +++ b/src/__tests__/profile-edit.test.tsx @@ -14,6 +14,7 @@ vi.mock("react-toastify", () => ({ })); vi.mock("framer-motion", () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports const React = require("react"); const motion: Record & { children?: React.ReactNode }>> = {}; ["div", "form", "h2", "p"].forEach((tag) => { diff --git a/src/__tests__/ticket-verification.test.tsx b/src/__tests__/ticket-verification.test.tsx index 1e5438f..ebc5a9a 100644 --- a/src/__tests__/ticket-verification.test.tsx +++ b/src/__tests__/ticket-verification.test.tsx @@ -5,6 +5,7 @@ import VerifyPage from "@/app/(protected)/verify/page"; vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) })); vi.mock("framer-motion", () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports const React = require("react"); const proxy = new Proxy({}, { get: (_t, tag: string) => diff --git a/src/app/(protected)/dashboard/page.tsx b/src/app/(protected)/dashboard/page.tsx index c1dca3e..2a5f87a 100644 --- a/src/app/(protected)/dashboard/page.tsx +++ b/src/app/(protected)/dashboard/page.tsx @@ -54,6 +54,17 @@ export default function DashboardPage() { const payoutsQueued = data?.payoutsQueued ?? 0 const nextSettlementDays = data?.nextSettlementDays ?? 0 + // Compute week-over-week revenue trend from data.revenue + const revenueTrend = (() => { + const rev = data?.revenue ?? [] + if (rev.length < 14) return null + const currentWeek = rev.slice(-7).reduce((sum, d) => sum + d.revenue, 0) + const lastWeek = rev.slice(-14, -7).reduce((sum, d) => sum + d.revenue, 0) + if (lastWeek === 0) return null + return ((currentWeek - lastWeek) / lastWeek) * 100 + })() + + const eventImages = data?.events?.slice(0, 4).map((e) => ({ const eventImgs = data?.events?.slice(0, 4).map((e) => ({ src: e.coverImage ?? null, alt: e.name, @@ -122,7 +133,13 @@ export default function DashboardPage() {
-

Trending by 18.6% in the past week ↗️

+ {revenueTrend === null ? ( +

Insufficient data for trend

+ ) : ( +

= 0 ? 'text-emerald-400' : 'text-red-400'}`}> + Trending by {Math.abs(revenueTrend).toFixed(1)}% {revenueTrend >= 0 ? '↗️' : '↘️'} this week +

+ )} @@ -154,7 +171,7 @@ export default function DashboardPage() {

1.5k from last week

- {eventImgs.map((image, index) => ( + {eventImages.map((image, index) => ( ))}
diff --git a/src/app/(protected)/events/manage/[eventId]/page.tsx b/src/app/(protected)/events/manage/[eventId]/page.tsx index 8fd5707..a6a31ee 100644 --- a/src/app/(protected)/events/manage/[eventId]/page.tsx +++ b/src/app/(protected)/events/manage/[eventId]/page.tsx @@ -10,7 +10,6 @@ import { Breadcrumb } from "@/components/ui"; import { performEventAction } from "@/lib/eventActions"; import TabSelector from "@/components/TabSelector"; import AttendeesTab from "@/components/events/manage/AttendeesTab"; - import { TicketTypeRow } from "@/components/events/manage/TicketTypeRow"; import { useEventInventory } from "@/hooks/useEventInventory"; @@ -27,6 +26,9 @@ export default function ManageEventPage() { const { eventId } = useParams<{ eventId: string }>(); const [event, setEvent] = useState(null); const [eventLoading, setEventLoading] = useState(true); + const [confirmOpen, setConfirmOpen] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [activeTab, setActiveTab] = useState("Overview"); const { data: ticketTypes, @@ -34,11 +36,6 @@ export default function ManageEventPage() { error: inventoryError, refresh, } = useEventInventory(eventId); - const [event, setEvent] = useState(null); - const [loading, setLoading] = useState(true); - const [confirmOpen, setConfirmOpen] = useState(false); - const [submitting, setSubmitting] = useState(false); - const [activeTab, setActiveTab] = useState("Overview"); useEffect(() => { if (!eventId) return; @@ -61,14 +58,32 @@ export default function ManageEventPage() { }; }, [eventId]); + const handleConfirmCancel = async () => { + if (!event || submitting) return; + setSubmitting(true); + try { + const result = await performEventAction(event.id, "cancel"); + if (!result.success) { + toast.error(result.message); + return; + } + toast.success("Event cancelled. Refunds and notifications have been queued."); + setEvent({ ...event, status: "cancelled" }); + setConfirmOpen(false); + } catch (err) { + toast.error(err instanceof Error ? err.message : "Failed to cancel event."); + } finally { + setSubmitting(false); + } + }; + + const isCancelled = event?.status === "cancelled"; + return (
- + ← Back to events
@@ -79,9 +94,7 @@ export default function ManageEventPage() { {eventLoading ? "Loading event…" : event?.name ?? "Event not found"} {event && !eventLoading && ( -

- Status: {event.status} -

+

Status: {event.status}

)}
-
-

- Ticket Types -

- - {inventoryError ? ( -
- {inventoryError} - -
- ) : inventoryLoading && ticketTypes.length === 0 ? ( -

Loading ticket types…

- ) : ticketTypes.length === 0 ? ( -

- No ticket types have been created for this event yet. -

- ) : ( -
    - {ticketTypes.map((ticket) => ( - - ))} -
- )} -
- - - const handleConfirmCancel = async () => { - if (!event || submitting) return; - setSubmitting(true); - try { - const result = await performEventAction(event.id, "cancel"); - if (!result.success) { - toast.error(result.message); - return; - } - toast.success("Event cancelled. Refunds and notifications have been queued."); - setEvent({ ...event, status: "cancelled" }); - setConfirmOpen(false); - } catch (err) { - toast.error(err instanceof Error ? err.message : "Failed to cancel event."); - } finally { - setSubmitting(false); - } - }; - - if (loading) return

Loading event...

; - if (!event) return

Event not found.

; - - const isCancelled = event.status === "cancelled"; - - return ( -
-
-

Manage: {event.name}

-

Status: {event.status}

- + {event && ( + + )} tabs={TABS as unknown as Tab[]} @@ -178,35 +125,69 @@ export default function ManageEventPage() { /> {activeTab === "Overview" && ( -
-

Danger zone

-

- Cancelling this event is irreversible. All ticket holders will be - refunded and notified automatically. -

- +
+ ) : inventoryLoading && ticketTypes.length === 0 ? ( +

Loading ticket types…

+ ) : ticketTypes.length === 0 ? ( +

+ No ticket types have been created for this event yet. +

+ ) : ( +
    + {ticketTypes.map((ticket) => ( + + ))} +
+ )} + + +
- {isCancelled ? "Event already cancelled" : "Cancel Event"} - -
+

Danger zone

+

+ Cancelling this event is irreversible. All ticket holders will be refunded and + notified automatically. +

+ + + )} {activeTab === "Attendees" && (
- + {event && }
)} - {/* Themed cancellation confirmation modal */} { @@ -221,7 +202,7 @@ export default function ManageEventPage() { -
+ ); } diff --git a/src/app/(protected)/verify/page.tsx b/src/app/(protected)/verify/page.tsx index 011c98d..bfa5fb4 100644 --- a/src/app/(protected)/verify/page.tsx +++ b/src/app/(protected)/verify/page.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useEffect, useRef, useState } from 'react'; import { useState, useRef, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { motion, AnimatePresence } from 'framer-motion'; diff --git a/src/app/(public)/events/page.tsx b/src/app/(public)/events/page.tsx index c89b0b2..09e40cb 100644 --- a/src/app/(public)/events/page.tsx +++ b/src/app/(public)/events/page.tsx @@ -27,8 +27,11 @@ function EventsPageContent() { // Sync state if URL query params change useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect setSearchQuery(searchParams.get('q') || ''); + // eslint-disable-next-line react-hooks/set-state-in-effect setLocationFilter(searchParams.get('location') || ''); + // eslint-disable-next-line react-hooks/set-state-in-effect setDateFilter(searchParams.get('date') || ''); }, [searchParams]); @@ -51,6 +54,7 @@ function EventsPageContent() { }, [events, activeFilters, viewMode, searchQuery, locationFilter, dateFilter]); // Reset visible count when filters change + // eslint-disable-next-line react-hooks/set-state-in-effect useEffect(() => { setVisibleCount(PAGE_SIZE); }, [activeFilters, viewMode, searchQuery, locationFilter, dateFilter]); // Infinite scroll via IntersectionObserver diff --git a/src/app/(public)/verify-email/page.tsx b/src/app/(public)/verify-email/page.tsx index 8b7fe10..bf7391b 100644 --- a/src/app/(public)/verify-email/page.tsx +++ b/src/app/(public)/verify-email/page.tsx @@ -1,12 +1,12 @@ "use client"; -import { useState } from "react"; +import { Suspense, useState } from "react"; import { useSearchParams } from "next/navigation"; import { motion } from "framer-motion"; import { HiMail } from "react-icons/hi"; import PublicShell from "@/components/shared/PublicShell"; -export default function VerifyEmailPage() { +function VerifyEmailContent() { const searchParams = useSearchParams(); const email = searchParams.get("email") ?? "your email"; const [status, setStatus] = useState<"idle" | "sending" | "sent" | "error">("idle"); @@ -26,51 +26,59 @@ export default function VerifyEmailPage() { }; return ( - -
- -
- -
+
+ +
+ +
-

Check your inbox

-

- We sent a verification link to{" "} - {email}. Click the - link to activate your account. +

Check your inbox

+

+ We sent a verification link to{" "} + {email}. Click the + link to activate your account. +

+ + {status === "sent" && ( +

+ ✓ Verification email resent successfully. +

+ )} + {status === "error" && ( +

+ Failed to resend. Please try again.

+ )} - {status === "sent" && ( -

- ✓ Verification email resent successfully. -

- )} - {status === "error" && ( -

- Failed to resend. Please try again. -

- )} + - +

+ Already verified?{" "} + + Sign in + +

+
+
+ ); +} -

- Already verified?{" "} - - Sign in - -

-
-
+export default function VerifyEmailPage() { + return ( + + + + ); } diff --git a/src/components/auth/login-form.tsx b/src/components/auth/login-form.tsx index 9a8f357..e699d52 100644 --- a/src/components/auth/login-form.tsx +++ b/src/components/auth/login-form.tsx @@ -13,7 +13,6 @@ import { motion } from "framer-motion"; import { containerVariants, itemVariants, headerVariants } from "@/lib/animations/motionVariants"; import { loginUser } from "@/lib/auth"; import { useRouter, useSearchParams } from "next/navigation"; -import { FcGoogle } from "react-icons/fc"; const loginSchema = z.object({ email: z.email("Please enter a valid email address"), diff --git a/src/components/auth/signup-form.tsx b/src/components/auth/signup-form.tsx index 6fe0922..840b69e 100644 --- a/src/components/auth/signup-form.tsx +++ b/src/components/auth/signup-form.tsx @@ -58,7 +58,7 @@ export default function SignUpForm() { if (!res.ok) { if (result?.errors && typeof result.errors === "object") { Object.entries(result.errors).forEach(([field, message]) => { - // @ts-expect-error – field keys come from the server + // eslint-disable-next-line @typescript-eslint/no-explicit-any setError(field as keyof FormValues, { type: "server", message: String(message), diff --git a/src/components/shared/MotionWrapper.tsx b/src/components/shared/MotionWrapper.tsx index 36c92de..dccdff0 100644 --- a/src/components/shared/MotionWrapper.tsx +++ b/src/components/shared/MotionWrapper.tsx @@ -6,7 +6,7 @@ import { useMotionPreferences } from "@/hooks/useMotionPreferences"; type Props = { children: React.ReactNode; - variants: any; + variants: import('framer-motion').Variants; className?: string; }; diff --git a/src/components/shared/WalletNavDropdown.tsx b/src/components/shared/WalletNavDropdown.tsx index 8a1fe73..87b75e9 100644 --- a/src/components/shared/WalletNavDropdown.tsx +++ b/src/components/shared/WalletNavDropdown.tsx @@ -34,6 +34,7 @@ export function WalletNavDropdown({ address, network, onDisconnect }: WalletNavD // Fetch XLM balance from Horizon when dropdown opens useEffect(() => { if (!open || balance !== null) return; + // eslint-disable-next-line react-hooks/set-state-in-effect setLoadingBalance(true); const horizonBase = network.toLowerCase().includes("test") diff --git a/src/components/ui/Breadcrumb.tsx b/src/components/ui/Breadcrumb.tsx index 9de622c..71c1deb 100644 --- a/src/components/ui/Breadcrumb.tsx +++ b/src/components/ui/Breadcrumb.tsx @@ -8,11 +8,12 @@ export interface BreadcrumbItem { interface BreadcrumbProps { items: BreadcrumbItem[]; + className?: string; } -export function Breadcrumb({ items }: BreadcrumbProps) { +export function Breadcrumb({ items, className }: BreadcrumbProps) { return ( -