diff --git a/package-lock.json b/package-lock.json index bea1903..5ac9906 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,9 +57,9 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", - "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { @@ -72,9 +72,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", - "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -82,21 +82,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -113,14 +113,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", - "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -243,13 +243,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -306,18 +306,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", - "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -325,9 +325,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -864,20 +864,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" }, "engines": { @@ -901,9 +901,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", "dev": true, "license": "MIT", "engines": { @@ -1516,9 +1516,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", - "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -1530,9 +1530,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", - "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -1544,9 +1544,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", - "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -1558,9 +1558,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", - "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -1572,9 +1572,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", - "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -1586,9 +1586,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", - "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -1600,9 +1600,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", - "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -1614,9 +1614,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", - "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -1628,9 +1628,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", - "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -1642,9 +1642,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", - "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -1656,9 +1656,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", - "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -1670,9 +1670,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", - "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -1684,9 +1684,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", - "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], @@ -1698,9 +1698,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", - "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -1712,9 +1712,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", - "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -1726,9 +1726,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", - "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -1740,9 +1740,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", - "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -1754,9 +1754,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", - "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -1768,9 +1768,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", - "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -1782,9 +1782,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", - "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], @@ -1796,9 +1796,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", - "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -1810,9 +1810,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", - "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -1824,9 +1824,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", - "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -1838,9 +1838,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", - "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -1852,9 +1852,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", - "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -1866,49 +1866,49 @@ ] }, "node_modules/@tailwindcss/node": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", - "lightningcss": "1.30.2", + "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.18" + "tailwindcss": "4.2.1" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-x64": "4.1.18", - "@tailwindcss/oxide-freebsd-x64": "4.1.18", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", "cpu": [ "arm64" ], @@ -1919,13 +1919,13 @@ "android" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", "cpu": [ "arm64" ], @@ -1936,13 +1936,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", "cpu": [ "x64" ], @@ -1953,13 +1953,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", "cpu": [ "x64" ], @@ -1970,13 +1970,13 @@ "freebsd" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", "cpu": [ "arm" ], @@ -1987,13 +1987,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", "cpu": [ "arm64" ], @@ -2004,13 +2004,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", "cpu": [ "arm64" ], @@ -2021,13 +2021,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", "cpu": [ "x64" ], @@ -2038,13 +2038,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", "cpu": [ "x64" ], @@ -2055,13 +2055,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -2077,21 +2077,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" + "tslib": "^2.8.1" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", "cpu": [ "arm64" ], @@ -2102,13 +2102,13 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", "cpu": [ "x64" ], @@ -2119,27 +2119,27 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", - "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz", + "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.18", - "@tailwindcss/oxide": "4.1.18", - "postcss": "^8.4.41", - "tailwindcss": "4.1.18" + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "postcss": "^8.5.6", + "tailwindcss": "4.2.1" } }, "node_modules/@tanstack/query-core": { - "version": "5.90.18", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.18.tgz", - "integrity": "sha512-rbGx6bHgPNVzutP7BEr+53UPKohpckqlMAad+To9UxTbeaQ+kC/1SDRj+QzkwbQ7qhLT/1IKp34yS6thda6fzA==", + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", "license": "MIT", "funding": { "type": "github", @@ -2147,12 +2147,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.90.18", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.18.tgz", - "integrity": "sha512-KqNZX0C5IFz4639zR1ilnQ288tQdJrMNLtzmlzyJ14xauBkhtLEy3mPU/V4KiHsr41eL1ILZbDP36TB12lYfCQ==", + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.90.18" + "@tanstack/query-core": "5.90.20" }, "funding": { "type": "github", @@ -2222,9 +2222,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", - "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "version": "20.19.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", + "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2232,9 +2232,9 @@ } }, "node_modules/@types/react": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", - "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2252,17 +2252,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz", - "integrity": "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.53.0", - "@typescript-eslint/type-utils": "8.53.0", - "@typescript-eslint/utils": "8.53.0", - "@typescript-eslint/visitor-keys": "8.53.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -2275,8 +2275,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.53.0", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -2291,16 +2291,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.0.tgz", - "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.53.0", - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/typescript-estree": "8.53.0", - "@typescript-eslint/visitor-keys": "8.53.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "engines": { @@ -2311,19 +2311,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz", - "integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.53.0", - "@typescript-eslint/types": "^8.53.0", + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "engines": { @@ -2338,14 +2338,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz", - "integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/visitor-keys": "8.53.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2356,9 +2356,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz", - "integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, "license": "MIT", "engines": { @@ -2373,15 +2373,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.0.tgz", - "integrity": "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/typescript-estree": "8.53.0", - "@typescript-eslint/utils": "8.53.0", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -2393,14 +2393,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz", - "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", "engines": { @@ -2412,18 +2412,18 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz", - "integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.53.0", - "@typescript-eslint/tsconfig-utils": "8.53.0", - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/visitor-keys": "8.53.0", + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" @@ -2440,9 +2440,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -2453,16 +2453,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.0.tgz", - "integrity": "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.53.0", - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/typescript-estree": "8.53.0" + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2472,19 +2472,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz", - "integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2494,6 +2494,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -2516,9 +2529,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -2585,9 +2598,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", @@ -2606,19 +2619,22 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.15", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", - "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { @@ -2686,9 +2702,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001764", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", - "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", "dev": true, "funding": [ { @@ -2877,21 +2893,21 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", "dev": true, "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.18.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -3008,9 +3024,9 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", "dependencies": { @@ -3020,7 +3036,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -3311,13 +3327,13 @@ } }, "node_modules/framer-motion": { - "version": "12.26.2", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.26.2.tgz", - "integrity": "sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA==", + "version": "12.34.3", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.3.tgz", + "integrity": "sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q==", "license": "MIT", "dependencies": { - "motion-dom": "^12.26.2", - "motion-utils": "^12.24.10", + "motion-dom": "^12.34.3", + "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { @@ -3647,9 +3663,9 @@ } }, "node_modules/keycloak-js": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.2.tgz", - "integrity": "sha512-ug7pNZ1xNkd7PPkerOJCEU2VnUhS7CYStDOCFJgqCNQ64h53ppxaKrh4iXH0xM8hFu5b1W6e6lsyYWqBMvaQFg==", + "version": "26.2.3", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.3.tgz", + "integrity": "sha512-widjzw/9T6bHRgEp6H/Se3NCCarU7u5CwFKBcwtu7xfA1IfdZb+7Q7/KGusAnBo34Vtls8Oz9vzSqkQvQ7+b4Q==", "license": "Apache-2.0", "workspaces": [ "test" @@ -3680,9 +3696,9 @@ } }, "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -3696,23 +3712,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", "cpu": [ "arm64" ], @@ -3731,9 +3747,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", "cpu": [ "arm64" ], @@ -3752,9 +3768,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", "cpu": [ "x64" ], @@ -3773,9 +3789,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", "cpu": [ "x64" ], @@ -3794,9 +3810,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", "cpu": [ "arm" ], @@ -3815,9 +3831,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", "cpu": [ "arm64" ], @@ -3836,9 +3852,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", "cpu": [ "arm64" ], @@ -3857,9 +3873,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", "cpu": [ "x64" ], @@ -3878,9 +3894,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", "cpu": [ "x64" ], @@ -3899,9 +3915,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", "cpu": [ "arm64" ], @@ -3920,9 +3936,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", "cpu": [ "x64" ], @@ -4023,9 +4039,9 @@ } }, "node_modules/minimatch": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", - "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -4065,18 +4081,18 @@ } }, "node_modules/motion-dom": { - "version": "12.26.2", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.26.2.tgz", - "integrity": "sha512-KLMT1BroY8oKNeliA3JMNJ+nbCIsTKg6hJpDb4jtRAJ7nCKnnpg/LTq/NGqG90Limitz3kdAnAVXecdFVGlWTw==", + "version": "12.34.3", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.3.tgz", + "integrity": "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ==", "license": "MIT", "dependencies": { - "motion-utils": "^12.24.10" + "motion-utils": "^12.29.2" } }, "node_modules/motion-utils": { - "version": "12.24.10", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.24.10.tgz", - "integrity": "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==", + "version": "12.29.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", + "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", "license": "MIT" }, "node_modules/ms": { @@ -4325,9 +4341,9 @@ } }, "node_modules/react-router": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", - "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -4347,12 +4363,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz", - "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", "license": "MIT", "dependencies": { - "react-router": "7.12.0" + "react-router": "7.13.1" }, "engines": { "node": ">=20.0.0" @@ -4373,9 +4389,9 @@ } }, "node_modules/rollup": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", - "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -4389,31 +4405,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.55.1", - "@rollup/rollup-android-arm64": "4.55.1", - "@rollup/rollup-darwin-arm64": "4.55.1", - "@rollup/rollup-darwin-x64": "4.55.1", - "@rollup/rollup-freebsd-arm64": "4.55.1", - "@rollup/rollup-freebsd-x64": "4.55.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", - "@rollup/rollup-linux-arm-musleabihf": "4.55.1", - "@rollup/rollup-linux-arm64-gnu": "4.55.1", - "@rollup/rollup-linux-arm64-musl": "4.55.1", - "@rollup/rollup-linux-loong64-gnu": "4.55.1", - "@rollup/rollup-linux-loong64-musl": "4.55.1", - "@rollup/rollup-linux-ppc64-gnu": "4.55.1", - "@rollup/rollup-linux-ppc64-musl": "4.55.1", - "@rollup/rollup-linux-riscv64-gnu": "4.55.1", - "@rollup/rollup-linux-riscv64-musl": "4.55.1", - "@rollup/rollup-linux-s390x-gnu": "4.55.1", - "@rollup/rollup-linux-x64-gnu": "4.55.1", - "@rollup/rollup-linux-x64-musl": "4.55.1", - "@rollup/rollup-openbsd-x64": "4.55.1", - "@rollup/rollup-openharmony-arm64": "4.55.1", - "@rollup/rollup-win32-arm64-msvc": "4.55.1", - "@rollup/rollup-win32-ia32-msvc": "4.55.1", - "@rollup/rollup-win32-x64-gnu": "4.55.1", - "@rollup/rollup-win32-x64-msvc": "4.55.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -4528,9 +4544,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", - "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", "license": "MIT" }, "node_modules/tapable": { @@ -4621,16 +4637,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.0.tgz", - "integrity": "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.53.0", - "@typescript-eslint/parser": "8.53.0", - "@typescript-eslint/typescript-estree": "8.53.0", - "@typescript-eslint/utils": "8.53.0" + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4640,7 +4656,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, diff --git a/server/src/main/java/de/tum/cit/memo/controller/ContributorStatsController.java b/server/src/main/java/de/tum/cit/memo/controller/ContributorStatsController.java new file mode 100644 index 0000000..09c1ee2 --- /dev/null +++ b/server/src/main/java/de/tum/cit/memo/controller/ContributorStatsController.java @@ -0,0 +1,29 @@ +package de.tum.cit.memo.controller; + +import de.tum.cit.memo.dto.ContributorStatsResponse; +import de.tum.cit.memo.service.ContributorStatsService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/users") +@RequiredArgsConstructor +@Tag(name = "Contributor Stats", description = "Endpoints for contributor statistics and gamification") +public class ContributorStatsController { + + private final ContributorStatsService contributorStatsService; + + @GetMapping("/{userId}/stats") + @Operation(summary = "Get contributor statistics", description = "Returns contribution stats including total votes, streaks, daily counts for heatmap, " + + "and earned badge IDs.") + public ResponseEntity getContributorStats(@PathVariable String userId) { + ContributorStatsResponse stats = contributorStatsService.getStats(userId); + return ResponseEntity.ok(stats); + } +} diff --git a/server/src/main/java/de/tum/cit/memo/controller/SchedulingController.java b/server/src/main/java/de/tum/cit/memo/controller/SchedulingController.java index 9558b31..f300e87 100644 --- a/server/src/main/java/de/tum/cit/memo/controller/SchedulingController.java +++ b/server/src/main/java/de/tum/cit/memo/controller/SchedulingController.java @@ -34,7 +34,7 @@ public ResponseEntity getNextRelationship( } @PostMapping("/vote") - @Operation(summary = "Submit a vote on a relationship", description = "Records the user's vote on a competency pair identified by originId + destinationId.") + @Operation(summary = "Submit a vote on a relationship", description = "Records the user's vote on a competency relationship. Requires originId and destinationId pair. MATCHES and UNRELATED votes are bidirectional.") public ResponseEntity submitVote( @RequestHeader("X-User-Id") String userId, @Valid @RequestBody VoteRequest request) { diff --git a/server/src/main/java/de/tum/cit/memo/dto/ContributorStatsResponse.java b/server/src/main/java/de/tum/cit/memo/dto/ContributorStatsResponse.java new file mode 100644 index 0000000..2d46edb --- /dev/null +++ b/server/src/main/java/de/tum/cit/memo/dto/ContributorStatsResponse.java @@ -0,0 +1,31 @@ +package de.tum.cit.memo.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ContributorStatsResponse { + + private long totalVotes; + private int currentStreak; + private int longestStreak; + private List dailyCounts; + private List earnedBadges; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class DailyCount { + private LocalDate date; + private int count; + } +} diff --git a/server/src/main/java/de/tum/cit/memo/dto/VoteRequest.java b/server/src/main/java/de/tum/cit/memo/dto/VoteRequest.java index 5915e66..025a74e 100644 --- a/server/src/main/java/de/tum/cit/memo/dto/VoteRequest.java +++ b/server/src/main/java/de/tum/cit/memo/dto/VoteRequest.java @@ -1,6 +1,8 @@ package de.tum.cit.memo.dto; +import io.swagger.v3.oas.annotations.media.Schema; import de.tum.cit.memo.enums.RelationshipType; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; @@ -13,12 +15,20 @@ @AllArgsConstructor public class VoteRequest { - @NotNull + /** + * Origin competency ID. + */ + @NotBlank(message = "originId is required") + @Schema(description = "Origin competency ID", requiredMode = Schema.RequiredMode.REQUIRED) private String originId; - @NotNull + /** + * Destination competency ID. + */ + @NotBlank(message = "destinationId is required") + @Schema(description = "Destination competency ID", requiredMode = Schema.RequiredMode.REQUIRED) private String destinationId; - @NotNull + @NotNull(message = "Relationship type is required") private RelationshipType relationshipType; } diff --git a/server/src/main/java/de/tum/cit/memo/entity/CompetencyRelationship.java b/server/src/main/java/de/tum/cit/memo/entity/CompetencyRelationship.java index ff7b2da..5c4b084 100644 --- a/server/src/main/java/de/tum/cit/memo/entity/CompetencyRelationship.java +++ b/server/src/main/java/de/tum/cit/memo/entity/CompetencyRelationship.java @@ -93,7 +93,7 @@ public void recalculateEntropy() { } double ent = 0.0; - int[] counts = { voteAssumes, voteExtends, voteMatches, voteUnrelated }; + int[] counts = {voteAssumes, voteExtends, voteMatches, voteUnrelated}; for (int count : counts) { if (count > 0) { double p = (double) count / total; diff --git a/server/src/main/java/de/tum/cit/memo/repository/CompetencyRelationshipVoteRepository.java b/server/src/main/java/de/tum/cit/memo/repository/CompetencyRelationshipVoteRepository.java index 014bd02..00042bb 100644 --- a/server/src/main/java/de/tum/cit/memo/repository/CompetencyRelationshipVoteRepository.java +++ b/server/src/main/java/de/tum/cit/memo/repository/CompetencyRelationshipVoteRepository.java @@ -2,10 +2,32 @@ import de.tum.cit.memo.entity.CompetencyRelationshipVote; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.Instant; +import java.util.List; + @Repository public interface CompetencyRelationshipVoteRepository extends JpaRepository { boolean existsByRelationshipIdAndUserId(String relationshipId, String userId); + + @Query(value = """ + SELECT CAST(v.created_at AS DATE) AS vote_date, + COUNT(*) AS vote_count + FROM competency_relationships_votes v + WHERE v.user_id = :userId + AND v.created_at >= :since + GROUP BY CAST(v.created_at AS DATE) + UNION ALL + SELECT NULL AS vote_date, + COUNT(*) FILTER (WHERE v.created_at < :since) AS vote_count + FROM competency_relationships_votes v + WHERE v.user_id = :userId + ORDER BY vote_date + """, nativeQuery = true) + List findDailyVoteCountsWithPreWindowTotal(@Param("userId") String userId, + @Param("since") Instant since); } diff --git a/server/src/main/java/de/tum/cit/memo/repository/DailyVoteCount.java b/server/src/main/java/de/tum/cit/memo/repository/DailyVoteCount.java new file mode 100644 index 0000000..f9de047 --- /dev/null +++ b/server/src/main/java/de/tum/cit/memo/repository/DailyVoteCount.java @@ -0,0 +1,13 @@ +package de.tum.cit.memo.repository; + +import java.time.LocalDate; + +/** + * Spring Data projection for daily vote counts returned by native queries. + * {@code voteDate} is {@code null} for the pre-window sentinel row. + */ +public interface DailyVoteCount { + LocalDate getVoteDate(); + + Long getVoteCount(); +} diff --git a/server/src/main/java/de/tum/cit/memo/service/CompetencyRelationshipService.java b/server/src/main/java/de/tum/cit/memo/service/CompetencyRelationshipService.java index f097c11..4c7f022 100644 --- a/server/src/main/java/de/tum/cit/memo/service/CompetencyRelationshipService.java +++ b/server/src/main/java/de/tum/cit/memo/service/CompetencyRelationshipService.java @@ -61,8 +61,9 @@ private CompetencyRelationship buildAndSave(String originId, String destinationI .destinationId(destinationId) .build(); + CompetencyRelationship saved = relationshipRepository.save(relationship); competencyRepository.incrementDegree(List.of(originId, destinationId)); - return relationshipRepository.save(relationship); + return saved; } private static void validateDistinctIds(String originId, String destinationId) { diff --git a/server/src/main/java/de/tum/cit/memo/service/ContributorStatsService.java b/server/src/main/java/de/tum/cit/memo/service/ContributorStatsService.java new file mode 100644 index 0000000..8108ad2 --- /dev/null +++ b/server/src/main/java/de/tum/cit/memo/service/ContributorStatsService.java @@ -0,0 +1,141 @@ +package de.tum.cit.memo.service; + +import de.tum.cit.memo.dto.ContributorStatsResponse; +import de.tum.cit.memo.dto.ContributorStatsResponse.DailyCount; +import de.tum.cit.memo.repository.CompetencyRelationshipVoteRepository; +import de.tum.cit.memo.repository.DailyVoteCount; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Computes contributor statistics on-the-fly from vote timestamps. + * All date calculations use UTC. + */ +@Service +@RequiredArgsConstructor +public class ContributorStatsService { + + private final CompetencyRelationshipVoteRepository voteRepository; + + @Transactional(readOnly = true) + public ContributorStatsResponse getStats(String userId) { + LocalDate today = LocalDate.now(ZoneOffset.UTC); + LocalDate since = today.minusDays(365); + Instant sinceInstant = since.atStartOfDay(ZoneOffset.UTC).toInstant(); + + // Single query: daily rows for [since, today] + one sentinel NULL row for + // pre-window total + List rawCounts = voteRepository.findDailyVoteCountsWithPreWindowTotal(userId, sinceInstant); + + long windowVotes = 0; + long preWindowVotes = 0; + for (DailyVoteCount row : rawCounts) { + if (row.getVoteDate() == null) { + // Sentinel row: holds count of votes before :since + preWindowVotes = row.getVoteCount() != null ? row.getVoteCount() : 0; + } else { + countMap.put(row.getVoteDate(), row.getVoteCount().intValue()); + windowVotes += row.getVoteCount(); + } + } + long totalVotes = windowVotes + preWindowVotes; + + // Build daily counts list for heatmap + List dailyCounts = new ArrayList<>(); + for (Map.Entry entry : countMap.entrySet()) { + dailyCounts.add(DailyCount.builder() + .date(entry.getKey()) + .count(entry.getValue()) + .build()); + } + + // Calculate streaks + int currentStreak = calculateCurrentStreak(countMap, today); + int longestStreak = calculateLongestStreak(countMap, since, today); + + // Evaluate badges + List earnedBadges = evaluateBadges(totalVotes, currentStreak, longestStreak); + + return ContributorStatsResponse.builder() + .totalVotes(totalVotes) + .currentStreak(currentStreak) + .longestStreak(longestStreak) + .dailyCounts(dailyCounts) + .earnedBadges(earnedBadges) + .build(); + } + + /** + * Current streak: count consecutive days ending at today (or yesterday). + */ + private int calculateCurrentStreak(Map countMap, LocalDate today) { + // Check if the user contributed today; if not, start from yesterday + LocalDate checkDate = countMap.containsKey(today) ? today : today.minusDays(1); + int streak = 0; + while (countMap.containsKey(checkDate) && countMap.get(checkDate) > 0) { + streak++; + checkDate = checkDate.minusDays(1); + } + return streak; + } + + /** + * Longest streak within the daily counts window. + */ + private int calculateLongestStreak(Map countMap, LocalDate since, LocalDate today) { + int longest = 0; + int current = 0; + LocalDate d = since; + while (!d.isAfter(today)) { + if (countMap.containsKey(d) && countMap.get(d) > 0) { + current++; + longest = Math.max(longest, current); + } else { + current = 0; + } + d = d.plusDays(1); + } + return longest; + } + + /** + * Evaluate which badges the user has earned. + */ + private List evaluateBadges(long totalVotes, int currentStreak, int longestStreak) { + List badges = new ArrayList<>(); + int maxStreak = Math.max(currentStreak, longestStreak); + + if (totalVotes >= 1) { + badges.add("first-steps"); + } + if (totalVotes >= 10) { + badges.add("getting-started"); + } + if (totalVotes >= 50) { + badges.add("half-century"); + } + if (totalVotes >= 100) { + badges.add("century"); + } + if (maxStreak >= 3) { + badges.add("streak-3"); + } + if (maxStreak >= 7) { + badges.add("streak-7"); + } + if (maxStreak >= 30) { + badges.add("streak-30"); + } + + return badges; + } +} diff --git a/server/src/main/java/de/tum/cit/memo/service/SchedulingService.java b/server/src/main/java/de/tum/cit/memo/service/SchedulingService.java index 6163c00..7ef33be 100644 --- a/server/src/main/java/de/tum/cit/memo/service/SchedulingService.java +++ b/server/src/main/java/de/tum/cit/memo/service/SchedulingService.java @@ -8,13 +8,16 @@ import de.tum.cit.memo.entity.CompetencyRelationship; import de.tum.cit.memo.entity.CompetencyRelationshipVote; import de.tum.cit.memo.enums.RelationshipType; +import de.tum.cit.memo.exception.InvalidOperationException; import de.tum.cit.memo.exception.ResourceNotFoundException; import de.tum.cit.memo.repository.CompetencyRelationshipRepository; import de.tum.cit.memo.repository.CompetencyRelationshipVoteRepository; import de.tum.cit.memo.repository.CompetencyRepository; +import de.tum.cit.memo.repository.UserRepository; import de.tum.cit.memo.util.IdGenerator; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,9 +34,9 @@ import java.util.stream.Stream; /** - * Handles scheduling of competency mapping tasks using two pipelines: - * - Coverage (70%): pairs up low-degree competencies to grow the graph - * - Consensus (30%): re-surfaces ambiguous relationships for extra votes + * Dual-pipeline scheduling for competency mapping tasks. + * Coverage (70%): connects low-degree nodes to grow the graph evenly. + * Consensus (30%): resurfaces ambiguous relationships for more votes. */ @Slf4j @Service @@ -50,11 +53,13 @@ public class SchedulingService { private final CompetencyRelationshipRepository relationshipRepository; private final CompetencyRelationshipVoteRepository voteRepository; private final CompetencyRepository competencyRepository; + private final UserRepository userRepository; private final Random random = new Random(); /** Returns the next pair for a user to vote on, or empty if none left. */ @Transactional public Optional getNextTask(String userId) { + assertUserExists(userId); if (random.nextDouble() < COVERAGE_WEIGHT) { log.debug("Coverage pipeline for user {}", userId); return coveragePipeline(userId); @@ -72,17 +77,19 @@ public Optional getNextTask(String userId) { @Transactional public VoteResponse submitVote(String userId, VoteRequest request) { - // Look up (or create) the relationship for this pair - CompetencyRelationship rel = relationshipRepository - .findByOriginIdAndDestinationId(request.getOriginId(), request.getDestinationId()) - .orElseGet(() -> createRelationship(request.getOriginId(), request.getDestinationId())); + assertUserExists(userId); + String originId = request.getOriginId(); + String destinationId = request.getDestinationId(); + if (originId == null || destinationId == null) { + throw new InvalidOperationException("originId and destinationId are required"); + } - if (voteRepository.existsByRelationshipIdAndUserId(rel.getId(), userId)) { - log.debug("User {} already voted on {}", userId, rel.getId()); + CompetencyRelationship rel = findOrCreateRelationship(originId, destinationId); + if (!recordVoteIfAbsent(rel.getId(), userId, request.getRelationshipType())) { + log.debug("Duplicate vote ignored for user {} on {}", userId, rel.getId()); return toVoteResponse(rel); } - saveVote(rel.getId(), userId, request.getRelationshipType()); applyVote(rel, request.getRelationshipType()); // MATCHES and UNRELATED are symmetric, so mirror the vote to B→A @@ -146,14 +153,25 @@ private List getLowDegreeCompetencyIds() { } private CompetencyRelationship createRelationship(String originId, String destId) { + if (originId.equals(destId)) { + throw new InvalidOperationException("Cannot create relationship to itself"); + } + CompetencyRelationship rel = CompetencyRelationship.builder() .id(IdGenerator.generateCuid()) .originId(originId) .destinationId(destId) .build(); + CompetencyRelationship saved = relationshipRepository.save(rel); competencyRepository.incrementDegree(List.of(originId, destId)); - return relationshipRepository.save(rel); + return saved; + } + + private CompetencyRelationship findOrCreateRelationship(String originId, String destinationId) { + return relationshipRepository + .findByOriginIdAndDestinationId(originId, destinationId) + .orElseGet(() -> createRelationship(originId, destinationId)); } private CompetencyRelationship pickWeightedByEntropy(List candidates) { @@ -176,13 +194,24 @@ private double entropyWeight(CompetencyRelationship rel) { return rel.getEntropy() / (rel.getTotalVotes() + 1.0); } - private void saveVote(String relationshipId, String userId, RelationshipType type) { - voteRepository.save(CompetencyRelationshipVote.builder() - .id(IdGenerator.generateCuid()) - .relationshipId(relationshipId) - .userId(userId) - .relationshipType(type) - .build()); + private boolean recordVoteIfAbsent(String relationshipId, String userId, RelationshipType type) { + if (voteRepository.existsByRelationshipIdAndUserId(relationshipId, userId)) { + return false; + } + + try { + voteRepository.save(CompetencyRelationshipVote.builder() + .id(IdGenerator.generateCuid()) + .relationshipId(relationshipId) + .userId(userId) + .relationshipType(type) + .build()); + return true; + } catch (DataIntegrityViolationException ex) { + log.debug("Vote already exists due to concurrent request for user {} on {}", + userId, relationshipId); + return false; + } } private void applyVote(CompetencyRelationship rel, RelationshipType type) { @@ -198,15 +227,18 @@ private void applyVote(CompetencyRelationship rel, RelationshipType type) { /** For symmetric types, also records the vote on the reverse direction. */ private void mirrorSymmetricVote(CompetencyRelationship originalRel, String userId, RelationshipType type) { - Optional reverseOpt = relationshipRepository - .findByOriginIdAndDestinationId(originalRel.getDestinationId(), originalRel.getOriginId()); + CompetencyRelationship reverse = findOrCreateRelationship( + originalRel.getDestinationId(), originalRel.getOriginId()); + if (recordVoteIfAbsent(reverse.getId(), userId, type)) { + applyVote(reverse, type); + } + } - CompetencyRelationship reverse = reverseOpt.orElseGet( - () -> createRelationship(originalRel.getDestinationId(), originalRel.getOriginId())); + // --- Response builders --- - if (!voteRepository.existsByRelationshipIdAndUserId(reverse.getId(), userId)) { - applyVote(reverse, type); - saveVote(reverse.getId(), userId, type); + private void assertUserExists(String userId) { + if (!userRepository.existsById(userId)) { + throw new ResourceNotFoundException("User not found: " + userId); } } diff --git a/server/src/main/resources/db/migration/V6__create_scheduling_tables.sql b/server/src/main/resources/db/migration/V6__create_scheduling_tables.sql index 3d64a42..3f6cb09 100644 --- a/server/src/main/resources/db/migration/V6__create_scheduling_tables.sql +++ b/server/src/main/resources/db/migration/V6__create_scheduling_tables.sql @@ -10,7 +10,7 @@ CREATE TABLE "competency_relationships" ( "vote_matches" INT NOT NULL DEFAULT 0, "vote_unrelated" INT NOT NULL DEFAULT 0, - -- precomputed entropy for consensus scheduling + -- Precomputed entropy for consensus priority (0.0 = unanimous, ~2.0 = max disagreement) "entropy" DOUBLE PRECISION NOT NULL DEFAULT 0.0, -- Total votes = vote_assumes + vote_extends + vote_matches + vote_unrelated @@ -45,7 +45,7 @@ CREATE INDEX "idx_rel_dest" ON "competency_relationships"("destination_id"); CREATE INDEX "idx_votes_rel" ON "competency_relationships_votes"("relationship_id"); CREATE INDEX "idx_votes_user" ON "competency_relationships_votes"("user_id"); --- foreign keys +-- Foreign keys ALTER TABLE "competency_relationships" ADD CONSTRAINT "fk_rel_origin" FOREIGN KEY ("origin_id") REFERENCES "competencies"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/src/main/resources/db/migration/V8__seed_additional_competencies.sql b/server/src/main/resources/db/migration/V8__seed_additional_competencies.sql new file mode 100644 index 0000000..8e3971a --- /dev/null +++ b/server/src/main/resources/db/migration/V8__seed_additional_competencies.sql @@ -0,0 +1,113 @@ +-- Additional competencies and seed data for testing +-- Adds 15 new competencies, 38 relationship pairs, and 38 backdated votes +-- for the guest user spread across the last ~8 weeks (includes a 6-day streak) + +-- ───────────────────────── Competencies ───────────────────────── + +INSERT INTO competencies (id, title, description, created_at) VALUES +('comp_alg_001', 'Algorithm Design', 'Analyze problems, design efficient algorithms using divide-and-conquer, greedy, and dynamic programming strategies, and argue correctness via invariants and induction.', CURRENT_TIMESTAMP), +('comp_tc_001', 'Type Checking and Inference', 'Implement type-checking algorithms, understand Hindley–Milner inference, and reason about type soundness in statically typed languages.', CURRENT_TIMESTAMP), +('comp_conc_001', 'Concurrency and Parallelism', 'Write concurrent programs using threads, locks, and message passing; reason about race conditions, deadlocks, and memory models.', CURRENT_TIMESTAMP), +('comp_io_001', 'I/O and Side Effects', 'Manage side effects in functional programs using monadic abstractions, distinguish pure from impure code, and sequence effectful computations.', CURRENT_TIMESTAMP), +('comp_test_001', 'Testing and Verification', 'Write unit and property-based tests, apply test-driven development, and use formal verification techniques such as pre/post-conditions.', CURRENT_TIMESTAMP), +('comp_pars_001', 'Parsing and Grammars', 'Define context-free grammars, implement recursive-descent and parser-combinator parsers, and construct abstract syntax trees from source text.', CURRENT_TIMESTAMP), +('comp_eval_001', 'Evaluation Strategies', 'Compare call-by-value, call-by-name, and lazy evaluation; implement interpreters that support different reduction strategies.', CURRENT_TIMESTAMP), +('comp_err_001', 'Error Handling', 'Model errors with option/result types instead of exceptions, propagate errors functionally, and design robust recovery strategies.', CURRENT_TIMESTAMP), +('comp_oop_001', 'Object-Oriented Design', 'Apply encapsulation, inheritance, and composition; use SOLID principles and design patterns such as Observer, Strategy, and Factory.', CURRENT_TIMESTAMP), +('comp_db_001', 'Database Fundamentals', 'Design normalized relational schemas, write SQL queries with joins and aggregations, and understand transaction isolation levels.', CURRENT_TIMESTAMP), +('comp_net_001', 'Networking Basics', 'Understand TCP/IP, HTTP, and DNS; implement client-server communication using sockets and REST APIs.', CURRENT_TIMESTAMP), +('comp_os_001', 'Operating Systems Concepts', 'Explain process scheduling, virtual memory, and file systems; write programs that interact with OS services via system calls.', CURRENT_TIMESTAMP), +('comp_sec_001', 'Software Security', 'Identify common vulnerabilities (XSS, SQL injection, buffer overflow), apply secure coding practices, and understand authentication and encryption fundamentals.', CURRENT_TIMESTAMP), +('comp_git_001', 'Version Control with Git', 'Use branching, merging, and rebasing workflows; resolve merge conflicts and collaborate effectively using pull requests.', CURRENT_TIMESTAMP), +('comp_cpl_001', 'Complexity Theory', 'Classify problems by time and space complexity using Big-O notation; understand P, NP, and NP-completeness with reduction proofs.', CURRENT_TIMESTAMP) +ON CONFLICT DO NOTHING; + +-- ───────────────────────── Relationships ───────────────────────── + +INSERT INTO competency_relationships (id, origin_id, destination_id, total_votes, vote_assumes, vote_extends, vote_matches, vote_unrelated, entropy, created_at, updated_at) VALUES +('rel_seed_01', 'comp_alg_001', 'comp_tc_001', 1, 1, 0, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '55 days', CURRENT_TIMESTAMP - INTERVAL '55 days'), +('rel_seed_02', 'comp_alg_001', 'comp_conc_001', 1, 0, 1, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '52 days', CURRENT_TIMESTAMP - INTERVAL '52 days'), +('rel_seed_03', 'comp_tc_001', 'comp_conc_001', 1, 0, 0, 0, 1, 0.0, CURRENT_TIMESTAMP - INTERVAL '50 days', CURRENT_TIMESTAMP - INTERVAL '50 days'), +('rel_seed_04', 'comp_io_001', 'comp_test_001', 1, 0, 0, 1, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '48 days', CURRENT_TIMESTAMP - INTERVAL '48 days'), +('rel_seed_05', 'comp_pars_001', 'comp_eval_001', 1, 1, 0, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '45 days', CURRENT_TIMESTAMP - INTERVAL '45 days'), +('rel_seed_06', 'comp_err_001', 'comp_oop_001', 1, 0, 1, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '43 days', CURRENT_TIMESTAMP - INTERVAL '43 days'), +('rel_seed_07', 'comp_db_001', 'comp_net_001', 1, 0, 0, 0, 1, 0.0, CURRENT_TIMESTAMP - INTERVAL '40 days', CURRENT_TIMESTAMP - INTERVAL '40 days'), +('rel_seed_08', 'comp_os_001', 'comp_sec_001', 1, 1, 0, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '38 days', CURRENT_TIMESTAMP - INTERVAL '38 days'), +('rel_seed_09', 'comp_git_001', 'comp_cpl_001', 1, 0, 0, 1, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '35 days', CURRENT_TIMESTAMP - INTERVAL '35 days'), +('rel_seed_10', 'comp_alg_001', 'comp_io_001', 1, 0, 1, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '33 days', CURRENT_TIMESTAMP - INTERVAL '33 days'), +('rel_seed_11', 'comp_tc_001', 'comp_pars_001', 1, 1, 0, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '30 days', CURRENT_TIMESTAMP - INTERVAL '30 days'), +('rel_seed_12', 'comp_conc_001', 'comp_err_001', 1, 0, 0, 0, 1, 0.0, CURRENT_TIMESTAMP - INTERVAL '28 days', CURRENT_TIMESTAMP - INTERVAL '28 days'), +('rel_seed_13', 'comp_eval_001', 'comp_db_001', 1, 0, 0, 1, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '26 days', CURRENT_TIMESTAMP - INTERVAL '26 days'), +('rel_seed_14', 'comp_net_001', 'comp_os_001', 1, 1, 0, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '24 days', CURRENT_TIMESTAMP - INTERVAL '24 days'), +('rel_seed_15', 'comp_sec_001', 'comp_git_001', 1, 0, 1, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '21 days', CURRENT_TIMESTAMP - INTERVAL '21 days'), +('rel_seed_16', 'comp_cpl_001', 'comp_test_001', 1, 0, 0, 1, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '19 days', CURRENT_TIMESTAMP - INTERVAL '19 days'), +-- 6-day streak: days 14–19 ago +('rel_seed_17', 'comp_oop_001', 'comp_db_001', 1, 1, 0, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '17 days', CURRENT_TIMESTAMP - INTERVAL '17 days'), +('rel_seed_18', 'comp_io_001', 'comp_pars_001', 1, 0, 0, 0, 1, 0.0, CURRENT_TIMESTAMP - INTERVAL '14 days', CURRENT_TIMESTAMP - INTERVAL '14 days'), +('rel_seed_19', 'comp_eval_001', 'comp_err_001', 1, 0, 1, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '12 days', CURRENT_TIMESTAMP - INTERVAL '12 days'), +('rel_seed_20', 'comp_net_001', 'comp_git_001', 1, 0, 0, 1, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '10 days', CURRENT_TIMESTAMP - INTERVAL '10 days'), +('rel_seed_21', 'comp_alg_001', 'comp_err_001', 1, 1, 0, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '7 days', CURRENT_TIMESTAMP - INTERVAL '7 days'), +('rel_seed_22', 'comp_tc_001', 'comp_db_001', 1, 0, 0, 0, 1, 0.0, CURRENT_TIMESTAMP - INTERVAL '5 days', CURRENT_TIMESTAMP - INTERVAL '5 days'), +('rel_seed_23', 'comp_conc_001', 'comp_os_001', 1, 0, 1, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '3 days', CURRENT_TIMESTAMP - INTERVAL '3 days'), +('rel_seed_24', 'comp_cpl_001', 'comp_oop_001', 1, 0, 0, 1, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '1 day', CURRENT_TIMESTAMP - INTERVAL '1 day'), +-- Extra votes on some days for varied daily counts +('rel_seed_25', 'comp_alg_001', 'comp_cpl_001', 1, 1, 0, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '28 days', CURRENT_TIMESTAMP - INTERVAL '28 days'), +('rel_seed_26', 'comp_io_001', 'comp_err_001', 1, 0, 0, 1, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '21 days', CURRENT_TIMESTAMP - INTERVAL '21 days'), +('rel_seed_27', 'comp_test_001', 'comp_eval_001', 1, 0, 1, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '14 days', CURRENT_TIMESTAMP - INTERVAL '14 days'), +('rel_seed_28', 'comp_oop_001', 'comp_sec_001', 1, 1, 0, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '7 days', CURRENT_TIMESTAMP - INTERVAL '7 days'), +('rel_seed_29', 'comp_pars_001', 'comp_cpl_001', 1, 0, 0, 1, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '5 days', CURRENT_TIMESTAMP - INTERVAL '5 days'), +('rel_seed_30', 'comp_sec_001', 'comp_db_001', 1, 0, 0, 0, 1, 0.0, CURRENT_TIMESTAMP - INTERVAL '3 days', CURRENT_TIMESTAMP - INTERVAL '3 days'), +('rel_seed_31', 'comp_git_001', 'comp_net_001', 1, 1, 0, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '1 day', CURRENT_TIMESTAMP - INTERVAL '1 day'), +('rel_seed_32', 'comp_conc_001', 'comp_test_001', 1, 0, 1, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '10 days', CURRENT_TIMESTAMP - INTERVAL '10 days'), +('rel_seed_33', 'comp_alg_001', 'comp_eval_001', 1, 0, 0, 0, 1, 0.0, CURRENT_TIMESTAMP - INTERVAL '17 days', CURRENT_TIMESTAMP - INTERVAL '17 days'), +('rel_seed_34', 'comp_test_001', 'comp_net_001', 1, 0, 0, 1, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '24 days', CURRENT_TIMESTAMP - INTERVAL '24 days'), +('rel_seed_35', 'comp_git_001', 'comp_oop_001', 1, 0, 1, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '33 days', CURRENT_TIMESTAMP - INTERVAL '33 days'), +-- Streak gap-fillers (days 15, 16, 18) +('rel_seed_36', 'comp_alg_001', 'comp_pars_001', 1, 0, 1, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '15 days', CURRENT_TIMESTAMP - INTERVAL '15 days'), +('rel_seed_37', 'comp_tc_001', 'comp_eval_001', 1, 1, 0, 0, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '16 days', CURRENT_TIMESTAMP - INTERVAL '16 days'), +('rel_seed_38', 'comp_conc_001', 'comp_git_001', 1, 0, 0, 1, 0, 0.0, CURRENT_TIMESTAMP - INTERVAL '18 days', CURRENT_TIMESTAMP - INTERVAL '18 days') +ON CONFLICT DO NOTHING; + +-- ───────────────────────── Votes ───────────────────────── + +INSERT INTO competency_relationships_votes (id, relationship_id, user_id, relationship_type, created_at) VALUES +('vote_seed_01', 'rel_seed_01', 'guest', 'ASSUMES', CURRENT_TIMESTAMP - INTERVAL '55 days'), +('vote_seed_02', 'rel_seed_02', 'guest', 'EXTENDS', CURRENT_TIMESTAMP - INTERVAL '52 days'), +('vote_seed_03', 'rel_seed_03', 'guest', 'UNRELATED', CURRENT_TIMESTAMP - INTERVAL '50 days'), +('vote_seed_04', 'rel_seed_04', 'guest', 'MATCHES', CURRENT_TIMESTAMP - INTERVAL '48 days'), +('vote_seed_05', 'rel_seed_05', 'guest', 'ASSUMES', CURRENT_TIMESTAMP - INTERVAL '45 days'), +('vote_seed_06', 'rel_seed_06', 'guest', 'EXTENDS', CURRENT_TIMESTAMP - INTERVAL '43 days'), +('vote_seed_07', 'rel_seed_07', 'guest', 'UNRELATED', CURRENT_TIMESTAMP - INTERVAL '40 days'), +('vote_seed_08', 'rel_seed_08', 'guest', 'ASSUMES', CURRENT_TIMESTAMP - INTERVAL '38 days'), +('vote_seed_09', 'rel_seed_09', 'guest', 'MATCHES', CURRENT_TIMESTAMP - INTERVAL '35 days'), +('vote_seed_10', 'rel_seed_10', 'guest', 'EXTENDS', CURRENT_TIMESTAMP - INTERVAL '33 days'), +('vote_seed_11', 'rel_seed_11', 'guest', 'ASSUMES', CURRENT_TIMESTAMP - INTERVAL '30 days'), +('vote_seed_12', 'rel_seed_12', 'guest', 'UNRELATED', CURRENT_TIMESTAMP - INTERVAL '28 days'), +('vote_seed_13', 'rel_seed_13', 'guest', 'MATCHES', CURRENT_TIMESTAMP - INTERVAL '26 days'), +('vote_seed_14', 'rel_seed_14', 'guest', 'ASSUMES', CURRENT_TIMESTAMP - INTERVAL '24 days'), +('vote_seed_15', 'rel_seed_15', 'guest', 'EXTENDS', CURRENT_TIMESTAMP - INTERVAL '21 days'), +('vote_seed_16', 'rel_seed_16', 'guest', 'MATCHES', CURRENT_TIMESTAMP - INTERVAL '19 days'), +('vote_seed_17', 'rel_seed_17', 'guest', 'ASSUMES', CURRENT_TIMESTAMP - INTERVAL '17 days'), +('vote_seed_18', 'rel_seed_18', 'guest', 'UNRELATED', CURRENT_TIMESTAMP - INTERVAL '14 days'), +('vote_seed_19', 'rel_seed_19', 'guest', 'EXTENDS', CURRENT_TIMESTAMP - INTERVAL '12 days'), +('vote_seed_20', 'rel_seed_20', 'guest', 'MATCHES', CURRENT_TIMESTAMP - INTERVAL '10 days'), +('vote_seed_21', 'rel_seed_21', 'guest', 'ASSUMES', CURRENT_TIMESTAMP - INTERVAL '7 days'), +('vote_seed_22', 'rel_seed_22', 'guest', 'UNRELATED', CURRENT_TIMESTAMP - INTERVAL '5 days'), +('vote_seed_23', 'rel_seed_23', 'guest', 'EXTENDS', CURRENT_TIMESTAMP - INTERVAL '3 days'), +('vote_seed_24', 'rel_seed_24', 'guest', 'MATCHES', CURRENT_TIMESTAMP - INTERVAL '1 day'), +('vote_seed_25', 'rel_seed_25', 'guest', 'ASSUMES', CURRENT_TIMESTAMP - INTERVAL '28 days'), +('vote_seed_26', 'rel_seed_26', 'guest', 'MATCHES', CURRENT_TIMESTAMP - INTERVAL '21 days'), +('vote_seed_27', 'rel_seed_27', 'guest', 'EXTENDS', CURRENT_TIMESTAMP - INTERVAL '14 days'), +('vote_seed_28', 'rel_seed_28', 'guest', 'ASSUMES', CURRENT_TIMESTAMP - INTERVAL '7 days'), +('vote_seed_29', 'rel_seed_29', 'guest', 'MATCHES', CURRENT_TIMESTAMP - INTERVAL '5 days'), +('vote_seed_30', 'rel_seed_30', 'guest', 'UNRELATED', CURRENT_TIMESTAMP - INTERVAL '3 days'), +('vote_seed_31', 'rel_seed_31', 'guest', 'ASSUMES', CURRENT_TIMESTAMP - INTERVAL '1 day'), +('vote_seed_32', 'rel_seed_32', 'guest', 'EXTENDS', CURRENT_TIMESTAMP - INTERVAL '10 days'), +('vote_seed_33', 'rel_seed_33', 'guest', 'UNRELATED', CURRENT_TIMESTAMP - INTERVAL '17 days'), +('vote_seed_34', 'rel_seed_34', 'guest', 'MATCHES', CURRENT_TIMESTAMP - INTERVAL '24 days'), +('vote_seed_35', 'rel_seed_35', 'guest', 'EXTENDS', CURRENT_TIMESTAMP - INTERVAL '33 days'), +-- Streak gap-fillers +('vote_seed_36', 'rel_seed_36', 'guest', 'EXTENDS', CURRENT_TIMESTAMP - INTERVAL '15 days'), +('vote_seed_37', 'rel_seed_37', 'guest', 'ASSUMES', CURRENT_TIMESTAMP - INTERVAL '16 days'), +('vote_seed_38', 'rel_seed_38', 'guest', 'MATCHES', CURRENT_TIMESTAMP - INTERVAL '18 days') +ON CONFLICT DO NOTHING; diff --git a/src/App.tsx b/src/App.tsx index 1c7a3ac..3df2686 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import { HomePage } from './pages/HomePage'; import { AboutPage } from './pages/AboutPage'; import { SessionPage } from './pages/SessionPage'; import { OnboardingPage } from './pages/OnboardingPage'; +import { DashboardPage } from './pages/DashboardPage'; export function App() { const [queryClient] = useState( @@ -27,6 +28,7 @@ export function App() { }> } /> + } /> } /> } /> } /> diff --git a/src/components/dashboard/ContributionHeatmap.tsx b/src/components/dashboard/ContributionHeatmap.tsx new file mode 100644 index 0000000..dfd53f5 --- /dev/null +++ b/src/components/dashboard/ContributionHeatmap.tsx @@ -0,0 +1,252 @@ +import { useMemo, useRef, useEffect } from 'react'; +import type { DailyCount } from '@/lib/api/contributor-stats'; +import { heatmapColor } from '@/lib/heatmap-helpers'; + +const MONTHS = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', +]; +const WEEKS_PER_COLUMN = 4; +const SQUARE_SIZE = 26; +const DEFAULT_HISTORY_DAYS = 365; +const MAX_HISTORY_DAYS = 3 * 366; +const LOCAL_DATE_KEY_PATTERN = /^\d{4}-\d{2}-\d{2}$/; + +type HeatmapWeek = { + date: Date; + count: number; +}; + +function formatDateKeyUTC(date: Date): string { + const year = date.getUTCFullYear(); + const month = String(date.getUTCMonth() + 1).padStart(2, '0'); + const day = String(date.getUTCDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +function parseDateKeyUTC(dateKey: string): Date | null { + if (!LOCAL_DATE_KEY_PATTERN.test(dateKey)) { + return null; + } + + const [year, month, day] = dateKey.split('-').map(Number); + const parsed = new Date(Date.UTC(year, month - 1, day)); + + return formatDateKeyUTC(parsed) === dateKey ? parsed : null; +} + +function startOfTodayUTC(): Date { + const now = new Date(); + return new Date( + Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()) + ); +} + +function addDaysUTC(date: Date, days: number): Date { + const next = new Date(date); + next.setUTCDate(next.getUTCDate() + days); + return next; +} + +function startOfIsoWeekUTC(date: Date): Date { + const daysSinceMonday = (date.getUTCDay() + 6) % 7; + return addDaysUTC(date, -daysSinceMonday); +} + +function endOfIsoWeekUTC(date: Date): Date { + return addDaysUTC(startOfIsoWeekUTC(date), 6); +} + +// calculate the grid data for the heatmap +function buildGrid(dailyCounts: DailyCount[]) { + const countByDate = new Map(); + let earliestDataDate: Date | null = null; + const today = startOfTodayUTC(); + + for (const dc of dailyCounts) { + const parsedDate = parseDateKeyUTC(dc.date); + if (!parsedDate || parsedDate > today) { + continue; + } + + const dateKey = formatDateKeyUTC(parsedDate); + countByDate.set(dateKey, (countByDate.get(dateKey) ?? 0) + dc.count); + if (!earliestDataDate || parsedDate < earliestDataDate) { + earliestDataDate = parsedDate; + } + } + + const defaultRangeStart = addDaysUTC(today, -DEFAULT_HISTORY_DAYS); + const oldestAllowedStart = addDaysUTC(today, -MAX_HISTORY_DAYS); + const rawRangeStart = + earliestDataDate && earliestDataDate < defaultRangeStart + ? earliestDataDate + : defaultRangeStart; + const rangeStart = + rawRangeStart < oldestAllowedStart ? oldestAllowedStart : rawRangeStart; + const startDate = startOfIsoWeekUTC(rangeStart); + const endDate = endOfIsoWeekUTC(today); + + const columns: HeatmapWeek[][] = []; + let currentColumn: HeatmapWeek[] = []; + + for ( + let currentWeekStart = startDate; + currentWeekStart <= endDate; + currentWeekStart = addDaysUTC(currentWeekStart, 7) + ) { + let weeklyCount = 0; + + // add up all the days in the week + for (let d = 0; d < 7; d++) { + const day = addDaysUTC(currentWeekStart, d); + const dateString = formatDateKeyUTC(day); + weeklyCount += countByDate.get(dateString) ?? 0; + } + + currentColumn.push({ + date: new Date(currentWeekStart), + count: weeklyCount, + }); + + // group by 4 weeks into columns + if (currentColumn.length === WEEKS_PER_COLUMN) { + columns.push(currentColumn); + currentColumn = []; + } + } + + // add whatever is left + if (currentColumn.length > 0) { + columns.push(currentColumn); + } + + return columns; +} + +// helper to figure out if we need to show a month label +function getColumnLabel( + column: HeatmapWeek[], + prevColumn: HeatmapWeek[] | undefined, + colIndex: number +) { + const currentMonth = column[0].date.getUTCMonth(); + const currentYear = column[0].date.getUTCFullYear(); + + const prevMonth = prevColumn ? prevColumn[0].date.getUTCMonth() : -1; + const isMonthChange = currentMonth !== prevMonth; + + // only show label every 3 months or on the very first column + const isQuarterlyMonth = currentMonth % 3 === 0; + const shouldShowLabel = isMonthChange && (isQuarterlyMonth || colIndex === 0); + + if (!shouldShowLabel) return null; + + // add year if its jan or the first column + const includeYear = currentMonth === 0 || colIndex === 0; + return includeYear + ? `${MONTHS[currentMonth]} ${currentYear}` + : MONTHS[currentMonth]; +} + +export function ContributionHeatmap({ + dailyCounts, +}: { + dailyCounts: DailyCount[]; +}) { + const columns = useMemo(() => buildGrid(dailyCounts), [dailyCounts]); + const scrollRef = useRef(null); + + // auto scroll to the end + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollLeft = scrollRef.current.scrollWidth; + } + }, [columns]); + + return ( +
+
+
+ {/* header row for labels */} +
+ {columns.map((col, i) => { + const label = getColumnLabel( + col, + i > 0 ? columns[i - 1] : undefined, + i + ); + + const currentYear = col[0].date.getUTCFullYear(); + const prevYear = + i > 0 ? columns[i - 1][0].date.getUTCFullYear() : currentYear; + const isYearChange = currentYear !== prevYear; + + return ( +
+ {label && ( + + {label} + + )} +
+ ); + })} +
+ + {} +
+ {columns.map((col, colIndex) => { + const currentYear = col[0].date.getUTCFullYear(); + const prevYear = + colIndex > 0 + ? columns[colIndex - 1][0].date.getUTCFullYear() + : currentYear; + const isYearChange = currentYear !== prevYear; + + // adding margin and a dashed border when the year changes + return ( +
+ {col.map((week, weekIndex) => ( +
+ ))} +
+ ); + })} +
+
+
+
+ ); +} diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index 6126062..2617246 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -2,6 +2,7 @@ import { Link, useLocation } from 'react-router-dom'; import { useTheme } from './use-theme'; import { useEffect, useState } from 'react'; import { cn } from '@/lib/utils'; +import { CircleUser } from 'lucide-react'; const navItems = [ { href: '/', label: 'Home' }, @@ -96,12 +97,13 @@ export function Navbar() {
- {/* Right side - Dark mode toggle & Login */}
- {/* Dark mode toggle */} - {/* Login button */} - Start Contributing + + Dashboard
diff --git a/src/components/session/SessionSummary.tsx b/src/components/session/SessionSummary.tsx new file mode 100644 index 0000000..846444f --- /dev/null +++ b/src/components/session/SessionSummary.tsx @@ -0,0 +1,101 @@ +import { useEffect, useRef } from 'react'; +import { Link } from 'react-router-dom'; +import { Button } from '@/components/ui/button'; + +interface SessionStats { + completed: number; + skipped: number; +} + +interface SessionSummaryProps { + stats: SessionStats; + onContinue: () => void; +} + +export function SessionSummary({ stats, onContinue }: SessionSummaryProps) { + const modalRef = useRef(null); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onContinue(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + + if (modalRef.current) { + modalRef.current.focus(); + } + + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [onContinue]); + + return ( +
+
+
+
+ +
+
+
🧠
+

+ Session Complete +

+

+ Great work! Here's what you accomplished. +

+
+ +
+
+

+ {stats.completed} +

+

+ Mappings +

+
+
+

+ {stats.skipped} +

+

+ Skipped +

+
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/lib/api/contributor-stats.ts b/src/lib/api/contributor-stats.ts new file mode 100644 index 0000000..bdde37e --- /dev/null +++ b/src/lib/api/contributor-stats.ts @@ -0,0 +1,23 @@ +import { apiClient } from './client'; + +export interface DailyCount { + date: string; + count: number; +} + +export interface ContributorStats { + totalVotes: number; + currentStreak: number; + longestStreak: number; + dailyCounts: DailyCount[]; + earnedBadges: string[]; +} + +export const contributorStatsApi = { + getStats: async (userId: string): Promise => { + const response = await apiClient.get( + `/api/users/${userId}/stats` + ); + return response.data; + }, +}; diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts deleted file mode 100644 index dac7d02..0000000 --- a/src/lib/api/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './types'; -export * from './client'; -export * from './users'; -export * from './competencies'; -export * from './learning-resources'; -export * from './competency-relationships'; -export * from './competency-resource-links'; diff --git a/src/lib/api/session-helpers.ts b/src/lib/api/session-helpers.ts index 5c79a93..5e8a7b6 100644 --- a/src/lib/api/session-helpers.ts +++ b/src/lib/api/session-helpers.ts @@ -1,7 +1,6 @@ // Wrappers around the API clients used by the session page import { competenciesApi } from './competencies'; -import { competencyRelationshipsApi } from './competency-relationships'; import { competencyResourceLinksApi } from './competency-resource-links'; import { learningResourcesApi } from './learning-resources'; import { schedulingApi } from './scheduling'; @@ -14,7 +13,10 @@ import type { LearningResource, CompetencyResourceLink, } from './types'; -import type { RelationshipType } from '@/components/session/session-constants'; +import type { + RelationshipType, + ResourceMatchType, +} from '@/components/session/session-constants'; const GUEST_USER_ID = 'guest'; @@ -125,44 +127,22 @@ export async function getRandomLearningResourceAction(): Promise<{ } } -export async function deleteCompetencyRelationshipAction( - id: string -): Promise<{ success: boolean; error?: string }> { - try { - await competencyRelationshipsApi.delete(id); - return { success: true }; - } catch (error) { - return { - success: false, - error: - error instanceof Error - ? error.message - : 'Failed to delete relationship', - }; - } -} - -export async function createCompetencyResourceLinkAction( - formData: FormData -): Promise<{ +export async function createCompetencyResourceLinkAction(input: { + competencyId: string; + resourceId: string; + userId: string; + matchType: ResourceMatchType; +}): Promise<{ success: boolean; link?: CompetencyResourceLink; error?: string; }> { try { - const competencyId = formData.get('competencyId') as string; - const resourceId = formData.get('resourceId') as string; - const matchType = formData.get('matchType') as string; - const link = await competencyResourceLinksApi.create({ - competencyId, - resourceId, - userId: GUEST_USER_ID, - matchType: matchType as - | 'UNRELATED' - | 'WEAK' - | 'GOOD_FIT' - | 'PERFECT_MATCH', + competencyId: input.competencyId, + resourceId: input.resourceId, + userId: input.userId, + matchType: input.matchType, }); return { success: true, link }; } catch (error) { diff --git a/src/lib/heatmap-helpers.ts b/src/lib/heatmap-helpers.ts new file mode 100644 index 0000000..271988e --- /dev/null +++ b/src/lib/heatmap-helpers.ts @@ -0,0 +1,7 @@ +export function heatmapColor(count: number): string { + if (count === 0) return 'bg-slate-200'; + if (count <= 2) return 'bg-blue-400'; + if (count <= 5) return 'bg-blue-500'; + if (count <= 10) return 'bg-indigo-600'; + return 'bg-indigo-800'; +} diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..485c0c9 --- /dev/null +++ b/src/pages/DashboardPage.tsx @@ -0,0 +1,320 @@ +import { useEffect, useState, useMemo } from 'react'; +import { Link } from 'react-router-dom'; +import { Button } from '@/components/ui/button'; +import { + contributorStatsApi, + type ContributorStats, +} from '@/lib/api/contributor-stats'; +import { + Flame, + Trophy, + Zap, + Mountain, + Target, + Award, + Star, +} from 'lucide-react'; +import { ContributionHeatmap } from '@/components/dashboard/ContributionHeatmap'; +import { heatmapColor } from '@/lib/heatmap-helpers'; + +const GUEST_USER_ID = 'guest'; + +interface BadgeDef { + id: string; + name: string; + description: string; + icon: React.ElementType; + gradient: string; + glowColor: string; +} + +const BADGE_DEFS: BadgeDef[] = [ + { + id: 'first-steps', + name: 'The First Node', + description: '1+ mapping', + icon: Star, + gradient: 'from-emerald-400 to-teal-500', + glowColor: 'rgba(16,185,129,0.35)', + }, + { + id: 'getting-started', + name: 'Getting Started', + description: '10+ mappings', + icon: Target, + gradient: 'from-blue-400 to-indigo-500', + glowColor: 'rgba(99,102,241,0.35)', + }, + { + id: 'half-century', + name: 'Half Century', + description: '50+ mappings', + icon: Award, + gradient: 'from-violet-400 to-purple-600', + glowColor: 'rgba(139,92,246,0.35)', + }, + { + id: 'century', + name: 'Century', + description: '100+ mappings', + icon: Trophy, + gradient: 'from-amber-400 to-orange-500', + glowColor: 'rgba(245,158,11,0.35)', + }, + { + id: 'streak-3', + name: 'On Fire', + description: '3-day streak', + icon: Flame, + gradient: 'from-orange-400 to-red-500', + glowColor: 'rgba(239,68,68,0.35)', + }, + { + id: 'streak-7', + name: 'Dedicated', + description: '7-day streak', + icon: Zap, + gradient: 'from-yellow-400 to-amber-500', + glowColor: 'rgba(245,158,11,0.35)', + }, + { + id: 'streak-30', + name: 'Monthly Mapper', + description: '30-day streak', + icon: Mountain, + gradient: 'from-cyan-400 to-blue-600', + glowColor: 'rgba(6,182,212,0.35)', + }, +]; + +function BadgeCard({ badge, earned }: { badge: BadgeDef; earned: boolean }) { + const Icon = badge.icon; + return ( +
+ {earned && ( +
+ )} +
+
+ +
+
+
+

{badge.name}

+
+

{badge.description}

+
+
+ {earned && ( +
+
+ + + +
+
+ )} +
+ ); +} + +function StatCard({ + label, + value, + icon, + accentColor, +}: { + label: string; + value: string | number; + icon: React.ReactNode; + accentColor: string; +}) { + return ( +
+
+
+
+ {icon} + + {label} + +
+

{value}

+
+
+ ); +} + +export function DashboardPage() { + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function loadStats() { + try { + setLoading(true); + const data = await contributorStatsApi.getStats(GUEST_USER_ID); + setStats(data); + } catch (err) { + setError( + err instanceof Error + ? err.message + : 'Failed to load stats. Is the server running?' + ); + } finally { + setLoading(false); + } + } + void loadStats(); + }, []); + + const earnedSet = useMemo( + () => new Set(stats?.earnedBadges ?? []), + [stats?.earnedBadges] + ); + + return ( +
+
+
+
+
+
+ +
+
+
+

+ Your Dashboard +

+

+ Track your contributions and unlock milestones. +

+
+ +
+ + {error && ( +
+

Failed to load statistics

+

{error}

+
+ )} + + {loading && !error && ( +
+ {[1, 2, 3].map(i => ( +
+ ))} +
+ )} + + {stats && !loading && ( + <> +
+
+ + + + } + accentColor="rgba(99,102,241,0.3)" + /> + 0 ? `${stats.currentStreak} day${stats.currentStreak !== 1 ? 's' : ''}` : '—'} / ${stats.longestStreak > 0 ? `${stats.longestStreak} day${stats.longestStreak !== 1 ? 's' : ''}` : '—'}`} + icon={} + accentColor="rgba(239,68,68,0.25)" + /> +
+ +
+
+

+ Contribution Activity +

+
+ Less + {[0, 2, 5, 10, 15].map(level => ( +
+ ))} + More +
+
+
+ +
+
+
+ +
+
+

+ Milestone Badges +

+ + {earnedSet.size} / {BADGE_DEFS.length} earned + +
+
+ {BADGE_DEFS.map(badge => ( + + ))} +
+
+ + )} +
+
+ ); +} diff --git a/src/pages/SessionPage.tsx b/src/pages/SessionPage.tsx index 1f20dd0..55788f3 100644 --- a/src/pages/SessionPage.tsx +++ b/src/pages/SessionPage.tsx @@ -4,7 +4,6 @@ import { getRandomCompetenciesAction, getNextRelationshipTaskAction, submitCompetencyVoteAction, - deleteCompetencyRelationshipAction, createCompetencyResourceLinkAction, deleteCompetencyResourceLinkAction, getRandomLearningResourceAction, @@ -18,7 +17,7 @@ import { import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Kbd } from '@/components/ui/kbd'; -import { ArrowRight, ArrowLeftRight, Check, Link2 } from 'lucide-react'; +import { ArrowRight, ArrowLeftRight, Check, Link2, LogOut } from 'lucide-react'; import { Card, CardDescription, @@ -45,6 +44,7 @@ import { RESOURCE_ICONS, RESOURCE_MATCH_TYPE_TEXT_COLORS, } from '@/components/session/session-constants'; +import { SessionSummary } from '@/components/session/SessionSummary'; type SessionStats = { completed: number; @@ -108,7 +108,6 @@ export function SessionPage() { Array<{ type: 'completed' | 'skipped'; mode: MappingMode; - relationshipId?: string; resourceLinkId?: string; competencies?: Competency[]; learningResource?: LearningResource; @@ -124,7 +123,7 @@ export function SessionPage() { const [isTransitioning, setIsTransitioning] = useState(false); const [swapRotation, setSwapRotation] = useState(0); const [allDone, setAllDone] = useState(false); - const [isSwapped, setIsSwapped] = useState(false); + const [showSessionSummary, setShowSessionSummary] = useState(false); const { hoveredValue: hoveredRelation, @@ -138,9 +137,6 @@ export function SessionPage() { handleMouseLeave: handleResourceMatchMouseLeave, } = useHoverWithTimeout(); - const [relationshipToDelete, setRelationshipToDelete] = useState< - string | null - >(null); const [resourceLinkToDelete, setResourceLinkToDelete] = useState< string | null >(null); @@ -182,7 +178,6 @@ export function SessionPage() { setRelation(null); setResourceMatchType(null); - setIsSwapped(false); if (mappingMode === 'competency') { if (!userId) { @@ -273,6 +268,13 @@ export function SessionPage() { void loadMappingPair(isInitialLoad); }, [mappingMode, loadMappingPair]); + const handleContinueSession = useCallback(() => { + setShowSessionSummary(false); + setStats({ completed: 0, skipped: 0 }); + setHistory([]); + void loadMappingPair(true); + }, [loadMappingPair]); + const handleAction = useCallback( async (type: 'completed' | 'skipped') => { if (type === 'completed') { @@ -292,14 +294,8 @@ export function SessionPage() { try { const startTime = Date.now(); - - const originId = isSwapped - ? competencies[1]!.id - : competencies[0]!.id; - const destinationId = isSwapped - ? competencies[0]!.id - : competencies[1]!.id; - + const originId = competencies[0]!.id; + const destinationId = competencies[1]!.id; const result = await submitCompetencyVoteAction( userId, relation, @@ -324,8 +320,6 @@ export function SessionPage() { { type: 'completed', mode: 'competency', - relationshipId: - result.voteResponse?.relationshipId ?? undefined, competencies: competencies ? [...competencies] : undefined, }, ]); @@ -354,14 +348,13 @@ export function SessionPage() { setError(null); try { - const formData = new FormData(); - formData.set('competencyId', competencies[0]!.id); - formData.set('resourceId', learningResource.id); - formData.set('userId', userId); - formData.set('matchType', resourceMatchType); - const startTime = Date.now(); - const result = await createCompetencyResourceLinkAction(formData); + const result = await createCompetencyResourceLinkAction({ + competencyId: competencies[0]!.id, + resourceId: learningResource.id, + userId, + matchType: resourceMatchType, + }); const elapsed = Date.now() - startTime; if (elapsed < 300) { @@ -429,7 +422,6 @@ export function SessionPage() { relation, resourceMatchType, userId, - isSwapped, mappingMode, loadMappingPair, ] @@ -451,9 +443,7 @@ export function SessionPage() { })); if (last.type === 'completed') { - if (last.mode === 'competency' && last.relationshipId) { - setRelationshipToDelete(last.relationshipId); - } else if (last.mode === 'resource' && last.resourceLinkId) { + if (last.mode === 'resource' && last.resourceLinkId) { setResourceLinkToDelete(last.resourceLinkId); } } @@ -481,25 +471,6 @@ export function SessionPage() { }, 300); }, [history]); - useEffect(() => { - if (relationshipToDelete) { - deleteCompetencyRelationshipAction(relationshipToDelete) - .then(result => { - if (!result.success) { - setError(result.error ?? 'Failed to delete relationship'); - } - }) - .catch(err => { - setError( - err instanceof Error ? err.message : 'Failed to delete relationship' - ); - }) - .finally(() => { - setRelationshipToDelete(null); - }); - } - }, [relationshipToDelete]); - useEffect(() => { if (resourceLinkToDelete) { deleteCompetencyResourceLinkAction(resourceLinkToDelete) @@ -667,42 +638,52 @@ export function SessionPage() {
-
- +
+
+ + +
@@ -887,7 +868,6 @@ export function SessionPage() { size="sm" onClick={() => { setSwapRotation(prev => prev + 180); - setIsSwapped(prev => !prev); setIsTransitioning(true); setTimeout(() => { if (competencies && competencies.length >= 2) { @@ -1184,6 +1164,10 @@ export function SessionPage() { )} + + {showSessionSummary && ( + + )}
); }