diff --git a/package-lock.json b/package-lock.json index 613b622..386d72b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@vercel/edge-config": "^1.4.3", "chart.js": "^4.5.1", "embla-carousel-react": "8.6.0", + "js-sha1": "^0.7.0", "mdast-util-to-hast": "^13.2.1", "minio": "^8.0.6", "next": "^15.5.8", @@ -838,7 +839,6 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -1424,6 +1424,7 @@ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -2381,7 +2382,6 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.74.0.tgz", "integrity": "sha512-IEMM/V6gKdP+N/X31KDIczVzghDpiPWFGLNjS8Rus71KvV6y6ueLrrE/JGCHDrU+9pq5copF3iCa0YQh+9Lq9Q==", "license": "MIT", - "peer": true, "dependencies": { "@supabase/auth-js": "2.74.0", "@supabase/functions-js": "2.74.0", @@ -2726,6 +2726,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2737,6 +2738,7 @@ "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -2819,7 +2821,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", "integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2935,6 +2936,7 @@ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -2945,21 +2947,24 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", @@ -2967,6 +2972,7 @@ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -2978,7 +2984,8 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", @@ -2986,6 +2993,7 @@ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -2999,6 +3007,7 @@ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -3009,6 +3018,7 @@ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -3018,7 +3028,8 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", @@ -3026,6 +3037,7 @@ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -3043,6 +3055,7 @@ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -3057,6 +3070,7 @@ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -3070,6 +3084,7 @@ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -3085,6 +3100,7 @@ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -3095,14 +3111,16 @@ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", @@ -3138,6 +3156,7 @@ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" }, @@ -3151,7 +3170,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3169,6 +3187,7 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -3187,6 +3206,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3203,7 +3223,8 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/ajv-keywords": { "version": "3.5.2", @@ -3267,6 +3288,7 @@ "integrity": "sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "baseline-browser-mapping": "dist/cli.js" } @@ -3365,7 +3387,8 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/call-bind": { "version": "1.0.8", @@ -3505,7 +3528,6 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", - "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -3519,6 +3541,7 @@ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.0" } @@ -3751,14 +3774,14 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/embla-carousel": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -3829,7 +3852,8 @@ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -3849,6 +3873,7 @@ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -3871,6 +3896,7 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -3885,6 +3911,7 @@ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -3898,6 +3925,7 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -3908,6 +3936,7 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -3934,6 +3963,7 @@ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.x" } @@ -3973,7 +4003,8 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/fast-xml-parser": { "version": "4.5.3", @@ -4121,7 +4152,8 @@ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/gopd": { "version": "1.2.0", @@ -4494,6 +4526,7 @@ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -4509,6 +4542,7 @@ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -4529,12 +4563,19 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-sha1": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/js-sha1/-/js-sha1-0.7.0.tgz", + "integrity": "sha512-oQZ1Mo7440BfLSv9TX87VNEyU52pXPVG19F9PL3gTgNt0tVxlZ8F4O6yze3CLuLx28TxotxvlyepCNaaV0ZjMw==", + "license": "MIT" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -4876,6 +4917,7 @@ "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.11.5" }, @@ -5219,7 +5261,8 @@ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/micromark": { "version": "4.0.2", @@ -5883,14 +5926,14 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/next": { "version": "15.5.9", "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "15.5.9", "@swc/helpers": "0.5.15", @@ -5982,7 +6025,8 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/nodemailer": { "version": "7.0.11", @@ -6200,6 +6244,7 @@ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -6230,7 +6275,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.2.tgz", "integrity": "sha512-MdWVitvLbQULD+4DP8GYjZUrepGW7d+GQkNVqJEzNxE+e9WIa4egVFE/RDfVb1u9u/Jw7dNMmPB4IqxzbFYJ0w==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6250,7 +6294,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.2.tgz", "integrity": "sha512-dEoydsCp50i7kS1xHOmPXq4zQYoGWedUsvqv9H6zdif2r7yLHygyfP9qou71TulRN0d6ng9EbRVsQhSqfUc19g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -6397,6 +6440,7 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6609,6 +6653,7 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6628,6 +6673,7 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -6775,8 +6821,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -6798,6 +6843,7 @@ "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -6817,6 +6863,7 @@ "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -6870,6 +6917,7 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -6882,7 +6930,8 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { "version": "4.3.3", @@ -6890,6 +6939,7 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -6910,6 +6960,7 @@ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -6919,7 +6970,8 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/through2": { "version": "4.0.2", @@ -7122,6 +7174,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" @@ -7218,6 +7271,7 @@ "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -7250,6 +7304,7 @@ "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -7299,6 +7354,7 @@ "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" } @@ -7327,6 +7383,7 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -7339,7 +7396,8 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", @@ -7347,6 +7405,7 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/package.json b/package.json index 094f569..273a67c 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@vercel/edge-config": "^1.4.3", "chart.js": "^4.5.1", "embla-carousel-react": "8.6.0", + "js-sha1": "^0.7.0", "mdast-util-to-hast": "^13.2.1", "minio": "^8.0.6", "next": "^15.5.8", diff --git a/src/components/Hack/BaseRomErrorModal.tsx b/src/components/Hack/BaseRomErrorModal.tsx new file mode 100644 index 0000000..ee12a9f --- /dev/null +++ b/src/components/Hack/BaseRomErrorModal.tsx @@ -0,0 +1,214 @@ +"use client"; + +import React, { useEffect } from "react"; +import { FiX } from "react-icons/fi"; +import { FaCircleCheck, FaCircleXmark } from "react-icons/fa6"; + +export type BaseRomErrorModalState = + | { kind: "archive"; fileName: string; requiredExtensionPhrase: string; requiredRomName: string } + | { kind: "unrecognized"; fileName: string; requiredExtensionPhrase: string; requiredRomName: string } + | { + kind: "hash_mismatch"; + fileName: string; + selectedHash: string; + requiredHash: string; + requiredRomName: string; + matchedRomName?: string; + }; + +interface BaseRomErrorModalProps { + state: BaseRomErrorModalState; + onClose: () => void; +} + +function getTitle(state: BaseRomErrorModalState): string { + switch (state.kind) { + case "archive": + return "Extract your ROM first"; + case "unrecognized": + return "Unrecognized file type"; + case "hash_mismatch": + return "This isn't the correct base ROM"; + } +} + +function SectionLabel({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +

+ ); +} + +function HashBlock({ + label, + hash, + variant, +}: { + label: string; + hash: string; + variant: "wrong" | "correct"; +}) { + const isWrong = variant === "wrong"; + const Icon = isWrong ? FaCircleXmark : FaCircleCheck; + + return ( +
+ {label} +
+ +
+ {hash} +
+
+
+ ); +} + +function ModalBody({ state }: { state: BaseRomErrorModalState }) { + switch (state.kind) { + case "archive": + return ( +

+ Archive files like .zip need to be extracted first. After extracting, upload{" "} + {state.requiredExtensionPhrase} for {state.requiredRomName}. +

+ ); + case "unrecognized": + return ( +

+ {state.fileName} isn't a ROM file. This hack requires{" "} + {state.requiredRomName} — please select{" "} + {state.requiredExtensionPhrase}. +

+ ); + case "hash_mismatch": + return ( +
+
+ What happened +

+ {state.matchedRomName ? ( + <> + This file looks like {state.matchedRomName}, but this hack needs{" "} + {state.requiredRomName}. + + ) : ( + <> + This file doesn't match a known clean copy of {state.requiredRomName}. + + )} +

+
+ +
+ Patching against a non-matching ROM (even if that ROM is fully playable) can cause crashes, glitches, or broken saves. +
+ +
+ Compare checksums +
+ + +
+
+ +
+ Common reasons + +
+ +
+ Hackdex cannot advise on where to find ROMs. The only legal source is ripping from your own cartridge. +
+
+ ); + } +} + +const BaseRomErrorModal: React.FC = ({ state, onClose }) => { + useEffect(() => { + const html = document.documentElement; + const body = document.body; + const previousHtmlOverflow = html.style.overflow; + const previousBodyOverflow = body.style.overflow; + const previousBodyPaddingRight = body.style.paddingRight; + const scrollBarWidth = window.innerWidth - html.clientWidth; + + html.style.overflow = "hidden"; + body.style.overflow = "hidden"; + if (scrollBarWidth > 0) { + body.style.paddingRight = `${scrollBarWidth}px`; + } + + return () => { + html.style.overflow = previousHtmlOverflow; + body.style.overflow = previousBodyOverflow; + body.style.paddingRight = previousBodyPaddingRight; + }; + }, []); + + const title = getTitle(state); + + return ( +
+
+
+ + +
+

+ {title} +

+ {state.kind !== "unrecognized" && ( +

+ Selected file: {state.fileName} +

+ )} +
+ +
+
+ +
+ +
+
+
+ ); +}; + +export default BaseRomErrorModal; diff --git a/src/components/Hack/HackActions.tsx b/src/components/Hack/HackActions.tsx index ce412c8..a7e3e30 100644 --- a/src/components/Hack/HackActions.tsx +++ b/src/components/Hack/HackActions.tsx @@ -2,12 +2,19 @@ import React from "react"; import StickyActionBar from "@/components/Hack/StickyActionBar"; +import BaseRomErrorModal, { type BaseRomErrorModalState } from "@/components/Hack/BaseRomErrorModal"; import { useBaseRoms } from "@/contexts/BaseRomContext"; import { baseRoms } from "@/data/baseRoms"; import BinFile from "rom-patcher-js/rom-patcher-js/modules/BinFile.js"; import BPS from "rom-patcher-js/rom-patcher-js/modules/RomPatcher.format.bps.js"; import type { DownloadEventDetail } from "@/types/util"; import { getSignedPatchUrl, updatePatchDownloadCount } from "@/app/hack/[slug]/actions"; +import { sha1Hex } from "@/utils/hash"; +import { + formatRequiredRomExtension, + isArchiveFile, + isAnyRomExtension, +} from "@/utils/romFile"; interface HackActionsProps { title: string; @@ -37,7 +44,12 @@ const HackActions: React.FC = ({ const [patchBlob, setPatchBlob] = React.useState(null); const [patchUrl, setPatchUrl] = React.useState(null); const [termsAgreed, setTermsAgreed] = React.useState(false); + const [romErrorModal, setRomErrorModal] = React.useState(null); const baseRomName = React.useMemo(() => baseRoms.find(r => r.id === baseRomId)?.name || null, [baseRomId]); + const effectivePlatform = React.useMemo( + () => platform ?? baseRoms.find(r => r.id === baseRomId)?.platform, + [platform, baseRomId], + ); // Basic client-side bot detection React.useEffect(() => { @@ -95,23 +107,74 @@ const HackActions: React.FC = ({ async function onSelectFile(e: React.ChangeEvent) { const f = e.target.files?.[0] ?? null; - setFile(f); - if (f) { - const id = await importUploadedBlob(f); - if (!id) { - setError("That ROM doesn't match any supported base ROM."); - setStatus("idle"); - e.target.value = ""; - return; - } - if (id !== baseRomId) { - setError(`This ROM matches "${id}", but this hack requires "${baseRomName}".`); - setStatus("idle"); - e.target.value = ""; - return; - } - setStatus("ready"); + setFile(null); + if (!f) return; + + const requiredRomName = baseRomName ?? "this hack's base ROM"; + const requiredExtensionPhrase = effectivePlatform + ? formatRequiredRomExtension(effectivePlatform) + : "a ROM file"; + + const resetInput = () => { + e.target.value = ""; + setStatus("idle"); + }; + + if (isArchiveFile(f.name)) { + setRomErrorModal({ + kind: "archive", + fileName: f.name, + requiredExtensionPhrase, + requiredRomName, + }); + resetInput(); + return; } + + if (!isAnyRomExtension(f.name)) { + setRomErrorModal({ + kind: "unrecognized", + fileName: f.name, + requiredExtensionPhrase, + requiredRomName, + }); + resetInput(); + return; + } + + const selectedHash = await sha1Hex(f); + const match = baseRoms.find((r) => r.sha1.toLowerCase() === selectedHash.toLowerCase()); + const requiredHash = baseRoms.find((r) => r.id === baseRomId)?.sha1.toLowerCase() ?? ""; + + if (!match) { + setRomErrorModal({ + kind: "hash_mismatch", + fileName: f.name, + selectedHash, + requiredHash, + requiredRomName, + }); + resetInput(); + return; + } + + if (match.id !== baseRomId) { + await importUploadedBlob(f); + setRomErrorModal({ + kind: "hash_mismatch", + fileName: f.name, + selectedHash, + requiredHash, + requiredRomName, + matchedRomName: match.name, + }); + resetInput(); + return; + } + + await importUploadedBlob(f); + setFile(f); + setStatus("ready"); } async function onAgreeToTerms() { @@ -244,23 +307,31 @@ const HackActions: React.FC = ({ } return ( - (isLinked(baseRomId) ? ensurePermission(baseRomId, true) : linkRom(baseRomId))} - supported={supported} - onUploadChange={onSelectFile} - termsAgreed={termsAgreed} - /> + <> + (isLinked(baseRomId) ? ensurePermission(baseRomId, true) : linkRom(baseRomId))} + supported={supported} + onUploadChange={onSelectFile} + termsAgreed={termsAgreed} + /> + {romErrorModal && ( + setRomErrorModal(null)} + /> + )} + ); }; diff --git a/src/utils/hash.ts b/src/utils/hash.ts index 7c2256b..b021f7f 100644 --- a/src/utils/hash.ts +++ b/src/utils/hash.ts @@ -1,7 +1,7 @@ -export async function sha1Hex(blob: Blob): Promise { - const buf = await blob.arrayBuffer(); - // @ts-ignore - subtle may be undefined in non-browser - const digest = await (crypto.subtle as SubtleCrypto).digest("SHA-1", buf); +import { sha1 } from "js-sha1"; + +async function subtleSha1Hex(buf: ArrayBuffer): Promise { + const digest = await crypto.subtle.digest("SHA-1", buf); const bytes = new Uint8Array(digest); let hex = ""; for (let i = 0; i < bytes.length; i++) { @@ -10,4 +10,16 @@ export async function sha1Hex(blob: Blob): Promise { return hex; } +export async function sha1Hex(blob: Blob): Promise { + const buf = await blob.arrayBuffer(); + if (typeof crypto !== "undefined" && typeof crypto.subtle?.digest === "function") { + try { + return await subtleSha1Hex(buf); + } catch { + // crypto.subtle can fail outside a secure context (e.g. http dev) + } + } + + return sha1(buf); +} diff --git a/src/utils/romFile.ts b/src/utils/romFile.ts new file mode 100644 index 0000000..d64076c --- /dev/null +++ b/src/utils/romFile.ts @@ -0,0 +1,50 @@ +import type { Platform } from "@/data/baseRoms"; +import { platformAccept, platformAcceptAll } from "@/utils/idb"; + +const ARCHIVE_EXTENSIONS = new Set([ + ".zip", + ".7z", + ".rar", + ".tar", + ".gz", + ".gzip", + ".bz2", + ".xz", + ".cab", +]); + +export function getFileExtension(name: string): string { + const lower = name.toLowerCase(); + const lastDot = lower.lastIndexOf("."); + if (lastDot <= 0) return ""; + return lower.slice(lastDot); +} + +export function isArchiveFile(name: string): boolean { + return ARCHIVE_EXTENSIONS.has(getFileExtension(name)); +} + +function platformExtensions(platform: Platform): string[] { + return platformAccept(platform) + .split(",") + .map((ext) => ext.trim()) + .filter(Boolean); +} + +/** Any extension Hackdex accepts across all platforms (.gb, .gbc, .gba, .nds). */ +export function isAnyRomExtension(name: string): boolean { + const ext = getFileExtension(name); + if (!ext) return false; + return platformAcceptAll() + .split(",") + .map((e) => e.trim()) + .includes(ext); +} + +export function formatRequiredRomExtension(platform: Platform): string { + const exts = platformExtensions(platform); + if (exts.length === 0) return "a ROM file"; + if (exts.length === 1) return `a ${exts[0]} file`; + const formatted = exts.map((ext) => ext).join(" or "); + return `a ${formatted} file`; +}