diff --git a/package.json b/package.json index 4ded62c5..a6ab3990 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "devDependencies": { "@commitlint/cli": "^21.0.1", "@next/eslint-plugin-next": "^16.1.1", - "@solvro/config": "^2.1.1", + "@solvro/config": "^2.3.3", "@tailwindcss/typography": "^0.5.19", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5777a13..dbbb4e6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -196,8 +196,8 @@ importers: specifier: ^16.1.1 version: 16.2.4 '@solvro/config': - specifier: ^2.1.1 - version: 2.3.0(@next/eslint-plugin-next@16.2.4)(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(class-validator@0.15.1)(eslint@9.39.4(jiti@2.6.1))(prettier@3.8.3)(typescript@5.9.3) + specifier: ^2.3.3 + version: 2.3.3(@next/eslint-plugin-next@16.2.4)(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(class-validator@0.15.1)(eslint@9.39.4(jiti@2.6.1))(prettier@3.8.3)(ts-declaration-location@1.0.7(typescript@5.9.3))(typescript@5.9.3) '@tailwindcss/typography': specifier: ^0.5.19 version: 0.5.19(tailwindcss@4.2.4) @@ -585,12 +585,12 @@ packages: '@types/react': optional: true - '@clack/core@1.3.0': - resolution: {integrity: sha512-xJPHpAmEQUBrXSLx0gF+q5K/IyihXpsHZcha+jB+tyahsKRK3Dxo4D0coZDewHo12NhiuzC3dTtMPbm53GEAAA==} + '@clack/core@1.3.1': + resolution: {integrity: sha512-fT1qHVGAag4IEkrupZ6lRRbNCs1vS9P01KB/sG8zKgvUztbYtFBtQpjSITNwooDZ83tpsPzP0mRNs1/KVszCRA==} engines: {node: '>= 20.12.0'} - '@clack/prompts@1.3.0': - resolution: {integrity: sha512-GgcWwRCs/xPtaqlMy8qRhPnZf9vlWcWZNHAitnVQ3yk7JmSralSiq5q07yaffYE8SogtDm7zFeKccx1QNVARpw==} + '@clack/prompts@1.4.0': + resolution: {integrity: sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==} engines: {node: '>= 20.12.0'} '@commitlint/cli@21.0.1': @@ -598,9 +598,9 @@ packages: engines: {node: '>=22.12.0'} hasBin: true - '@commitlint/config-conventional@20.5.3': - resolution: {integrity: sha512-j34Qqeaa152chJgz2ysyk0BCpHenJn1lV0Rx0VXf8k3ccQcED+48EZrzMvo9jLmJUyBrrBwvu89I+2er4gW7QQ==} - engines: {node: '>=v18'} + '@commitlint/config-conventional@21.0.1': + resolution: {integrity: sha512-gRorrkfWOh/+V5X8GYWWbQvrzPczopGMS4CCNrQdHkK4xWElv82BDvIsDhJZWTlI7TazOlYea6VATufCsFs+sw==} + engines: {node: '>=22.12.0'} '@commitlint/config-validator@21.0.1': resolution: {integrity: sha512-Zd2UFdndeMMaW2O96HK0tdfT4gOImUvidMpAd/pws2zZ4m1nrAZ/9b/v2JYuE8fs86GpXv9F7LNaIuCIWhY+pA==} @@ -658,10 +658,6 @@ packages: resolution: {integrity: sha512-4esUYqzY7K0FCgcJ/1xWEZekV7Ch4yZT1+xjEb7KzqbJ05XEkxHVsTfC8ADKNNtlCE2pj98KEbPGZWw9WwEnVw==} engines: {node: '>=22.12.0'} - '@commitlint/types@20.5.0': - resolution: {integrity: sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==} - engines: {node: '>=v18'} - '@commitlint/types@21.0.1': resolution: {integrity: sha512-4u7w8jcoCUFWhjWnASYzZHAP34OqOtuFBN87nQmFvqda03YU0T6z+yB4w0gSAMpekiRqqGk5rt+qSlW+a2vSEg==} engines: {node: '>=22.12.0'} @@ -714,8 +710,8 @@ packages: resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} - '@darraghor/eslint-plugin-nestjs-typed@7.1.32': - resolution: {integrity: sha512-mWqyMug07KE+brxUNyJTMbrNC1X0Vw8Nr05mX+r80C0SUvy+ReHacO+SlZLu6ssX03m3tHswi4TmN5mM1VPkJg==} + '@darraghor/eslint-plugin-nestjs-typed@7.1.33': + resolution: {integrity: sha512-QMF7CAzCmG6sy4fJakBZRS93XZy2u5h+0BmCIe6RJ384x/rxDB+LEDB/HY7SuidK9I0pOZkEmdeua4tG/7aBsA==} engines: {node: '>=22.0.0'} peerDependencies: '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 @@ -918,8 +914,8 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/compat@2.0.5': - resolution: {integrity: sha512-IbHDbHJfkVNv6xjlET8AIVo/K1NQt7YT4Rp6ok/clyBGcpRx1l6gv0Rq3vBvYfPJIZt6ODf66Zq08FJNDpnzgg==} + '@eslint/compat@2.1.0': + resolution: {integrity: sha512-LgaSCymEpw7tF53xvDw9SNsraPb1IBHxpdABIOM0hW8UAlP8znrjYtuxfR58FSJ3L9BhwD+FaPRFQpZq84Nh6g==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} peerDependencies: eslint: ^8.40 || 9 || 10 @@ -2282,8 +2278,8 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@solvro/config@2.3.0': - resolution: {integrity: sha512-rgia1EDJsEGfklQ6c5ueBDnKN8+q0xdMhcIlkPxFaE//+K9fQMgJCFm6HXjJaHkl8SDN6MQrL4aboqopZmEj7w==} + '@solvro/config@2.3.3': + resolution: {integrity: sha512-ZOL4zLaXjwYZxKAAlmiHGx41okTb79T9L6iYreIxl5begHlMIauJf5zWxyVqgj4IL683aMsPQSgd2n6rhxj50g==} engines: {node: ^20.19.0 || >=22.12.0, npm: ^11.11.1} hasBin: true peerDependencies: @@ -2506,8 +2502,8 @@ packages: engines: {node: '>=18'} hasBin: true - '@tanstack/eslint-plugin-query@5.100.8': - resolution: {integrity: sha512-Hn2zEnjAPPBdHvFgSmcG8rUuskl2IS49LDjziF4CW8i8EmNT4/CkKudOckawOqzvDm/hjqZy0VFjyrQI1ZSw7Q==} + '@tanstack/eslint-plugin-query@5.100.13': + resolution: {integrity: sha512-zrEREA5FEozwaxLfSDdp7uohRNrNQi/BM3DKu39ILnPD/vcl5Z9PFa41tYIZ+GyUGcF0G/ZLrQg8KQvSQG8EPg==} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ^5.4.0 || ^6.0.0 @@ -2663,6 +2659,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -2729,63 +2728,63 @@ packages: '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} - '@typescript-eslint/eslint-plugin@8.59.1': - resolution: {integrity: sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==} + '@typescript-eslint/eslint-plugin@8.59.4': + resolution: {integrity: sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.59.1 + '@typescript-eslint/parser': ^8.59.4 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.59.1': - resolution: {integrity: sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==} + '@typescript-eslint/parser@8.59.4': + resolution: {integrity: sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.59.1': - resolution: {integrity: sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==} + '@typescript-eslint/project-service@8.59.4': + resolution: {integrity: sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.59.1': - resolution: {integrity: sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==} + '@typescript-eslint/scope-manager@8.59.4': + resolution: {integrity: sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.59.1': - resolution: {integrity: sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==} + '@typescript-eslint/tsconfig-utils@8.59.4': + resolution: {integrity: sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.59.1': - resolution: {integrity: sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==} + '@typescript-eslint/type-utils@8.59.4': + resolution: {integrity: sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.59.1': - resolution: {integrity: sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==} + '@typescript-eslint/types@8.59.4': + resolution: {integrity: sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.59.1': - resolution: {integrity: sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==} + '@typescript-eslint/typescript-estree@8.59.4': + resolution: {integrity: sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.59.1': - resolution: {integrity: sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==} + '@typescript-eslint/utils@8.59.4': + resolution: {integrity: sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.59.1': - resolution: {integrity: sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==} + '@typescript-eslint/visitor-keys@8.59.4': + resolution: {integrity: sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -3033,8 +3032,8 @@ packages: brace-expansion@2.1.0: resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -3046,8 +3045,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - builtin-modules@5.1.0: - resolution: {integrity: sha512-c5JxaDrzwRjq3WyJkI1AGR5xy6Gr6udlt7sQPbl09+3ckB+Zo2qqQ2KhCTBr7Q8dHB43bENGYEk4xddrFH/b7A==} + builtin-modules@5.2.0: + resolution: {integrity: sha512-02yxLeyxF4dNl6SlY6/5HfRSrSdZ/sCPoxy2kZNP5dZZX8LSAD9aE2gtJIUgWrsQTiMPl3mxESyrobSwvRGisQ==} engines: {node: '>=18.20'} bundle-name@4.1.0: @@ -3546,6 +3545,10 @@ packages: resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.22.0: + resolution: {integrity: sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==} + engines: {node: '>=10.13.0'} + entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -3584,8 +3587,8 @@ packages: es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} es-set-tostringtag@2.1.0: @@ -3668,8 +3671,8 @@ packages: eslint-import-resolver-webpack: optional: true - eslint-plugin-antfu@3.2.2: - resolution: {integrity: sha512-Qzixht2Dmd/pMbb5EnKqw2V8TiWHbotPlsORO8a+IzCLFwE0RxK8a9k4DCTFPzBwyxJzH+0m2Mn8IUGeGQkyUw==} + eslint-plugin-antfu@3.2.3: + resolution: {integrity: sha512-U2fnz/H0gFPxpuC7QpaHa0Jv2AgCZ5hunp36SOP/yWo8yFzgvMh8X4pZ4uN4IKoqtBhk7G3HuVa93Urf51+sZg==} peerDependencies: eslint: '*' @@ -3701,11 +3704,18 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-n@17.24.0: - resolution: {integrity: sha512-/gC7/KAYmfNnPNOb3eu8vw+TdVnV0zhdQwexsw6FLXbhzroVj20vRn2qL8lDWDGnAQ2J8DhdfvXxX9EoxvERvw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-plugin-n@18.0.1: + resolution: {integrity: sha512-q3ARhk+eZRc7myR0KHx+R3/GJeOHF+Ir6PK95Pu2tEX8Sl/4BIpmmVLva2kPrjC2gCmn6WHlHm+3yeo6Rxhycw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} peerDependencies: - eslint: '>=8.23.0' + eslint: '>=8.57.1' + ts-declaration-location: ^1.0.6 + typescript: '>=5.0.0' + peerDependenciesMeta: + ts-declaration-location: + optional: true + typescript: + optional: true eslint-plugin-prettier@5.5.5: resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} @@ -3732,8 +3742,8 @@ packages: peerDependencies: eslint: ^9 || ^10 - eslint-plugin-react-you-might-not-need-an-effect@0.9.3: - resolution: {integrity: sha512-44cce7LndBnpDRWBTQ8p7ircIdl2rJBP5+V9Ik64E935UB47uA9ZMU1Uv160lAMhtvoPYqXBjQ+tojr5JF3mFQ==} + eslint-plugin-react-you-might-not-need-an-effect@0.10.1: + resolution: {integrity: sha512-IK0s/+ShN0bkur5moKCu/lfx2D/9uIeozje8Wv2/XnYdmswa17pDg02aUuytEPb8Gf0eueiQFf/QsvOHHcvujg==} engines: {node: '>=14.0.0'} peerDependencies: eslint: '>=8.40.0' @@ -3750,6 +3760,12 @@ packages: peerDependencies: eslint: '>=9.38.0' + eslint-plugin-unicorn@64.0.0: + resolution: {integrity: sha512-rNZwalHh8i0UfPlhNwg5BTUO1CMdKNmjqe+TgzOTZnpKoi8VBgsW7u9qCHIdpxEzZ1uwrJrPF0uRb7l//K38gA==} + engines: {node: ^20.10.0 || >=21.0.0} + peerDependencies: + eslint: '>=9.38.0' + eslint-plugin-unused-imports@4.4.1: resolution: {integrity: sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==} peerDependencies: @@ -3895,8 +3911,8 @@ packages: fast-uri@3.1.2: resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} - fast-wrap-ansi@0.2.0: - resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} fastest-levenshtein@1.0.16: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} @@ -4316,8 +4332,8 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} is-data-view@1.0.2: @@ -4598,8 +4614,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libphonenumber-js@1.12.42: - resolution: {integrity: sha512-oKQFPTibqQwZZkChCDVMFVJXMZdyJNqDWZWYNn8BgyAaK/6yFJEowxCY0RVFirRyWP63hMRuKlkSEd9qlvbWXg==} + libphonenumber-js@1.13.3: + resolution: {integrity: sha512-xMkdAMqcyG7iN2WZZmGIfWbYxW4orRkny+0/AXIbwL0xll2zkDX0Vzo/BXFa6+7mh2UvJl9MbcTtHk0YXkFtBA==} lie@3.1.1: resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} @@ -4693,8 +4709,8 @@ packages: resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} engines: {node: '>=20.0.0'} - local-pkg@1.1.2: - resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + local-pkg@1.2.1: + resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} engines: {node: '>=14'} localforage@1.10.0: @@ -4729,6 +4745,10 @@ packages: resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} engines: {node: 20 || >=22} + lru-cache@11.5.0: + resolution: {integrity: sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -5069,8 +5089,8 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-deep-merge@2.0.0: - resolution: {integrity: sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==} + object-deep-merge@2.0.1: + resolution: {integrity: sha512-aKttDKcU3pyZqKcCkDhsMn70WmZFG2JGDQLP9EcLyTSIFQRCPWLAmBZRLJnrVUrhPG1jETEEbfdgbNtJf1LyMg==} object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} @@ -5297,8 +5317,8 @@ packages: prettier: optional: true - prettier-plugin-tailwindcss@0.7.4: - resolution: {integrity: sha512-UKii4RjY05SNt/WQi6/NcOn/LsT0/ILLXsxygjbRg5/YZelsSu5jTqorYHPDGq4nZy5q5hpCu+XdGZ1xaJEQgw==} + prettier-plugin-tailwindcss@0.8.0: + resolution: {integrity: sha512-V8ITGH87yuBDF6JpEZTOVlUz/saAwqb8f3HRgUj8Lh+tGCcrmorhsLpYqzygwFwK0PE2Ib6Mv3M7T/uE2tZV1g==} engines: {node: '>=20.19'} peerDependencies: '@ianvs/prettier-plugin-sort-imports': '*' @@ -5633,8 +5653,8 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve@2.0.0-next.6: - resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} engines: {node: '>= 0.4'} hasBin: true @@ -5717,6 +5737,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} @@ -6141,8 +6166,8 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.59.1: - resolution: {integrity: sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==} + typescript-eslint@8.59.4: + resolution: {integrity: sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -6614,7 +6639,7 @@ snapshots: eslint-plugin-prettier: 5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(prettier@3.8.3) eslint-plugin-unicorn: 63.0.0(eslint@9.39.4(jiti@2.6.1)) prettier: 3.8.3 - typescript-eslint: 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + typescript-eslint: 8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - '@types/eslint' - supports-color @@ -6622,7 +6647,7 @@ snapshots: '@adonisjs/eslint-plugin@2.2.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) micromatch: 4.0.8 read-package-up: 12.0.0 @@ -7003,16 +7028,16 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - '@clack/core@1.3.0': + '@clack/core@1.3.1': dependencies: - fast-wrap-ansi: 0.2.0 + fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@clack/prompts@1.3.0': + '@clack/prompts@1.4.0': dependencies: - '@clack/core': 1.3.0 + '@clack/core': 1.3.1 fast-string-width: 3.0.2 - fast-wrap-ansi: 0.2.0 + fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 '@commitlint/cli@21.0.1(@types/node@20.19.39)(conventional-commits-parser@6.4.0)(typescript@5.9.3)': @@ -7030,9 +7055,9 @@ snapshots: - conventional-commits-parser - typescript - '@commitlint/config-conventional@20.5.3': + '@commitlint/config-conventional@21.0.1': dependencies: - '@commitlint/types': 20.5.0 + '@commitlint/types': 21.0.1 conventional-changelog-conventionalcommits: 9.3.1 '@commitlint/config-validator@21.0.1': @@ -7055,7 +7080,7 @@ snapshots: '@commitlint/is-ignored@21.0.1': dependencies: '@commitlint/types': 21.0.1 - semver: 7.7.4 + semver: 7.8.1 '@commitlint/lint@21.0.1': dependencies: @@ -7118,11 +7143,6 @@ snapshots: dependencies: escalade: 3.2.0 - '@commitlint/types@20.5.0': - dependencies: - conventional-commits-parser: 6.4.0 - picocolors: 1.1.1 - '@commitlint/types@21.0.1': dependencies: conventional-commits-parser: 6.4.0 @@ -7132,7 +7152,7 @@ snapshots: dependencies: '@simple-libs/child-process-utils': 1.0.2 '@simple-libs/stream-utils': 1.2.0 - semver: 7.7.4 + semver: 7.8.1 optionalDependencies: conventional-commits-parser: 6.4.0 @@ -7160,15 +7180,15 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} - '@darraghor/eslint-plugin-nestjs-typed@7.1.32(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(class-validator@0.15.1)(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@darraghor/eslint-plugin-nestjs-typed@7.1.33(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(class-validator@0.15.1)(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/parser': 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.59.1 - '@typescript-eslint/type-utils': 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/type-utils': 8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) class-validator: 0.15.1 eslint: 9.39.4(jiti@2.6.1) - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.6.1)) glob: 13.0.0 reflect-metadata: 0.2.2 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -7205,8 +7225,8 @@ snapshots: '@es-joy/jsdoccomment@0.86.0': dependencies: - '@types/estree': 1.0.8 - '@typescript-eslint/types': 8.59.1 + '@types/estree': 1.0.9 + '@typescript-eslint/types': 8.59.4 comment-parser: 1.4.6 esquery: 1.7.0 jsdoc-type-pratt-parser: 7.2.0 @@ -7304,7 +7324,7 @@ snapshots: '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@2.0.5(eslint@9.39.4(jiti@2.6.1))': + '@eslint/compat@2.1.0(eslint@9.39.4(jiti@2.6.1))': dependencies: '@eslint/core': 1.2.1 optionalDependencies: @@ -7514,7 +7534,7 @@ snapshots: '@inquirer/figures': 2.0.5 '@inquirer/type': 4.0.5(@types/node@20.19.39) cli-width: 4.1.0 - fast-wrap-ansi: 0.2.0 + fast-wrap-ansi: 0.2.2 mute-stream: 3.0.0 signal-exit: 4.1.0 optionalDependencies: @@ -8570,46 +8590,46 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} - '@solvro/config@2.3.0(@next/eslint-plugin-next@16.2.4)(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(class-validator@0.15.1)(eslint@9.39.4(jiti@2.6.1))(prettier@3.8.3)(typescript@5.9.3)': + '@solvro/config@2.3.3(@next/eslint-plugin-next@16.2.4)(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(class-validator@0.15.1)(eslint@9.39.4(jiti@2.6.1))(prettier@3.8.3)(ts-declaration-location@1.0.7(typescript@5.9.3))(typescript@5.9.3)': dependencies: '@adonisjs/eslint-config': 3.0.0(eslint@9.39.4(jiti@2.6.1))(prettier@3.8.3)(typescript@5.9.3) - '@clack/prompts': 1.3.0 - '@commitlint/config-conventional': 20.5.3 - '@darraghor/eslint-plugin-nestjs-typed': 7.1.32(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(class-validator@0.15.1)(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@clack/prompts': 1.4.0 + '@commitlint/config-conventional': 21.0.1 + '@darraghor/eslint-plugin-nestjs-typed': 7.1.33(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(class-validator@0.15.1)(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@9.39.4(jiti@2.6.1)) '@eslint/config-helpers': 0.4.2 '@eslint/js': 9.39.4 '@next/eslint-plugin-next': 16.2.4 '@solvro/utils': 1.6.0 - '@tanstack/eslint-plugin-query': 5.100.8(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@tanstack/eslint-plugin-query': 5.100.13(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@trivago/prettier-plugin-sort-imports': 6.0.2(prettier@3.8.3) - '@typescript-eslint/eslint-plugin': 8.59.1(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) commander: 14.0.3 eslint: 9.39.4(jiti@2.6.1) eslint-config-flat-gitignore: 2.3.0(eslint@9.39.4(jiti@2.6.1)) eslint-config-prettier: 10.1.8(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-antfu: 3.2.2(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-antfu: 3.2.3(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsdoc: 62.9.0(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-n: 17.24.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-n: 18.0.1(eslint@9.39.4(jiti@2.6.1))(ts-declaration-location@1.0.7(typescript@5.9.3))(typescript@5.9.3) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react-refresh: 0.5.2(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-react-you-might-not-need-an-effect: 0.9.3(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-unicorn: 63.0.0(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-react-you-might-not-need-an-effect: 0.10.1(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-unicorn: 64.0.0(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)) execa: 9.6.1 find-up-simple: 1.0.1 globals: 17.6.0 is-interactive: 2.0.0 - local-pkg: 1.1.2 + local-pkg: 1.2.1 package-manager-detector: 1.6.0 picocolors: 1.1.1 prettier-plugin-packagejson: 3.0.2(prettier@3.8.3) - prettier-plugin-tailwindcss: 0.7.4(@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.3))(prettier@3.8.3) - semver: 7.7.4 - typescript-eslint: 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + prettier-plugin-tailwindcss: 0.8.0(@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.3))(prettier@3.8.3) + semver: 7.8.1 + typescript-eslint: 8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - '@ianvs/prettier-plugin-sort-imports' - '@prettier/plugin-hermes' @@ -8637,6 +8657,7 @@ snapshots: - prettier-plugin-svelte - supports-color - svelte + - ts-declaration-location - typescript '@solvro/utils@1.6.0': {} @@ -8648,7 +8669,7 @@ snapshots: '@stylistic/eslint-plugin@5.10.0(eslint@9.39.4(jiti@2.6.1))': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) - '@typescript-eslint/types': 8.59.1 + '@typescript-eslint/types': 8.59.4 eslint: 9.39.4(jiti@2.6.1) eslint-visitor-keys: 4.2.1 espree: 10.4.0 @@ -8789,9 +8810,9 @@ snapshots: '@tanstack/devtools-event-client@0.4.3': {} - '@tanstack/eslint-plugin-query@5.100.8(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.100.13(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) optionalDependencies: typescript: 5.9.3 @@ -8950,10 +8971,12 @@ snapshots: '@types/estree-jsx@1.0.5': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -9011,14 +9034,14 @@ snapshots: '@types/validator@13.15.10': {} - '@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.59.1 - '@typescript-eslint/type-utils': 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.59.1 + '@typescript-eslint/parser': 8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/type-utils': 8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.4 eslint: 9.39.4(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 @@ -9027,41 +9050,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.59.1 - '@typescript-eslint/types': 8.59.1 - '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.59.1 + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.4 debug: 4.4.3 eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.59.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.59.4(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.59.1(typescript@5.9.3) - '@typescript-eslint/types': 8.59.1 + '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@5.9.3) + '@typescript-eslint/types': 8.59.4 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.59.1': + '@typescript-eslint/scope-manager@8.59.4': dependencies: - '@typescript-eslint/types': 8.59.1 - '@typescript-eslint/visitor-keys': 8.59.1 + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/visitor-keys': 8.59.4 - '@typescript-eslint/tsconfig-utils@8.59.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.59.4(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.59.1 - '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.4(jiti@2.6.1) ts-api-utils: 2.5.0(typescript@5.9.3) @@ -9069,37 +9092,37 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.59.1': {} + '@typescript-eslint/types@8.59.4': {} - '@typescript-eslint/typescript-estree@8.59.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.59.4(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.59.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.59.1(typescript@5.9.3) - '@typescript-eslint/types': 8.59.1 - '@typescript-eslint/visitor-keys': 8.59.1 + '@typescript-eslint/project-service': 8.59.4(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@5.9.3) + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/visitor-keys': 8.59.4 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.7.4 + semver: 7.8.1 tinyglobby: 0.2.16 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.59.1 - '@typescript-eslint/types': 8.59.1 - '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.59.1': + '@typescript-eslint/visitor-keys@8.59.4': dependencies: - '@typescript-eslint/types': 8.59.1 + '@typescript-eslint/types': 8.59.4 eslint-visitor-keys: 5.0.1 '@ungap/structured-clone@1.3.0': {} @@ -9247,7 +9270,7 @@ snapshots: call-bound: 1.0.4 define-properties: 1.2.1 es-abstract: 1.24.2 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 is-string: 1.1.1 math-intrinsics: 1.1.0 @@ -9258,7 +9281,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 es-shim-unscopables: 1.1.0 array.prototype.findlastindex@1.2.6: @@ -9268,7 +9291,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 es-shim-unscopables: 1.1.0 array.prototype.flat@1.3.3: @@ -9388,7 +9411,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.5: + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -9404,7 +9427,7 @@ snapshots: node-releases: 2.0.38 update-browserslist-db: 1.2.3(browserslist@4.28.2) - builtin-modules@5.1.0: {} + builtin-modules@5.2.0: {} bundle-name@4.1.0: dependencies: @@ -9467,7 +9490,7 @@ snapshots: class-validator@0.15.1: dependencies: '@types/validator': 13.15.10 - libphonenumber-js: 1.12.42 + libphonenumber-js: 1.13.3 validator: 13.15.35 class-variance-authority@0.7.1: @@ -9861,6 +9884,11 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 + enhanced-resolve@5.22.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + entities@6.0.1: {} entities@8.0.0: {} @@ -9885,7 +9913,7 @@ snapshots: data-view-byte-offset: 1.0.1 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 es-set-tostringtag: 2.1.0 es-to-primitive: 1.3.0 function.prototype.name: 1.1.8 @@ -9955,7 +9983,7 @@ snapshots: es-module-lexer@2.1.0: {} - es-object-atoms@1.1.1: + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -10020,11 +10048,11 @@ snapshots: eslint-compat-utils@0.5.1(eslint@9.39.4(jiti@2.6.1)): dependencies: eslint: 9.39.4(jiti@2.6.1) - semver: 7.7.4 + semver: 7.8.1 eslint-config-flat-gitignore@2.3.0(eslint@9.39.4(jiti@2.6.1)): dependencies: - '@eslint/compat': 2.0.5(eslint@9.39.4(jiti@2.6.1)) + '@eslint/compat': 2.1.0(eslint@9.39.4(jiti@2.6.1)) eslint: 9.39.4(jiti@2.6.1) eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.6.1)): @@ -10034,22 +10062,22 @@ snapshots: eslint-import-resolver-node@0.3.10: dependencies: debug: 3.2.7 - is-core-module: 2.16.1 - resolve: 2.0.0-next.6 + is-core-module: 2.16.2 + resolve: 2.0.0-next.7 transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 transitivePeerDependencies: - supports-color - eslint-plugin-antfu@3.2.2(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-antfu@3.2.3(eslint@9.39.4(jiti@2.6.1)): dependencies: eslint: 9.39.4(jiti@2.6.1) @@ -10060,7 +10088,7 @@ snapshots: eslint: 9.39.4(jiti@2.6.1) eslint-compat-utils: 0.5.1(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -10071,9 +10099,9 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.6.1)) hasown: 2.0.3 - is-core-module: 2.16.1 + is-core-module: 2.16.2 is-glob: 4.0.3 minimatch: 3.1.5 object.fromentries: 2.0.8 @@ -10083,7 +10111,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -10101,9 +10129,9 @@ snapshots: espree: 11.2.0 esquery: 1.7.0 html-entities: 2.6.0 - object-deep-merge: 2.0.0 + object-deep-merge: 2.0.1 parse-imports-exports: 0.2.4 - semver: 7.7.4 + semver: 7.8.1 spdx-expression-parse: 4.0.0 to-valid-identifier: 1.0.0 transitivePeerDependencies: @@ -10128,20 +10156,20 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-n@17.24.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-n@18.0.1(eslint@9.39.4(jiti@2.6.1))(ts-declaration-location@1.0.7(typescript@5.9.3))(typescript@5.9.3): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) - enhanced-resolve: 5.21.0 + enhanced-resolve: 5.22.0 eslint: 9.39.4(jiti@2.6.1) eslint-plugin-es-x: 7.8.0(eslint@9.39.4(jiti@2.6.1)) get-tsconfig: 4.14.0 globals: 15.15.0 globrex: 0.1.2 ignore: 5.3.2 - semver: 7.7.4 + semver: 7.8.1 + optionalDependencies: ts-declaration-location: 1.0.7(typescript@5.9.3) - transitivePeerDependencies: - - typescript + typescript: 5.9.3 eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(prettier@3.8.3): dependencies: @@ -10158,8 +10186,8 @@ snapshots: '@babel/parser': 7.29.3 eslint: 9.39.4(jiti@2.6.1) hermes-parser: 0.25.1 - zod: 4.4.2 - zod-validation-error: 4.0.2(zod@4.4.2) + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) transitivePeerDependencies: - supports-color @@ -10167,7 +10195,7 @@ snapshots: dependencies: eslint: 9.39.4(jiti@2.6.1) - eslint-plugin-react-you-might-not-need-an-effect@0.9.3(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-react-you-might-not-need-an-effect@0.10.1(eslint@9.39.4(jiti@2.6.1)): dependencies: eslint: 9.39.4(jiti@2.6.1) globals: 16.5.0 @@ -10189,7 +10217,7 @@ snapshots: object.fromentries: 2.0.8 object.values: 1.2.1 prop-types: 15.8.1 - resolve: 2.0.0-next.6 + resolve: 2.0.0-next.7 semver: 6.3.1 string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 @@ -10211,14 +10239,34 @@ snapshots: pluralize: 8.0.0 regexp-tree: 0.1.27 regjsparser: 0.13.1 - semver: 7.7.4 + semver: 7.8.1 strip-indent: 4.1.1 - eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-unicorn@64.0.0(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + change-case: 5.4.4 + ci-info: 4.4.0 + clean-regexp: 1.0.0 + core-js-compat: 3.49.0 + eslint: 9.39.4(jiti@2.6.1) + find-up-simple: 1.0.1 + globals: 17.6.0 + indent-string: 5.0.0 + is-builtin-module: 5.0.0 + jsesc: 3.1.0 + pluralize: 8.0.0 + regexp-tree: 0.1.27 + regjsparser: 0.13.1 + semver: 7.8.1 + strip-indent: 4.1.1 + + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)): dependencies: eslint: 9.39.4(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.59.1(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint-scope@8.4.0: dependencies: @@ -10419,7 +10467,7 @@ snapshots: fast-uri@3.1.2: {} - fast-wrap-ansi@0.2.0: + fast-wrap-ansi@0.2.2: dependencies: fast-string-width: 3.0.2 @@ -10524,7 +10572,7 @@ snapshots: call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 function-bind: 1.1.2 get-proto: 1.0.1 gopd: 1.2.0 @@ -10539,7 +10587,7 @@ snapshots: get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-stream@6.0.1: {} @@ -10732,7 +10780,7 @@ snapshots: hosted-git-info@9.0.3: dependencies: - lru-cache: 11.3.5 + lru-cache: 11.5.0 html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0): dependencies: @@ -10860,11 +10908,11 @@ snapshots: is-builtin-module@5.0.0: dependencies: - builtin-modules: 5.1.0 + builtin-modules: 5.2.0 is-callable@1.2.7: {} - is-core-module@2.16.1: + is-core-module@2.16.2: dependencies: hasown: 2.0.3 @@ -11006,7 +11054,7 @@ snapshots: iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 get-proto: 1.0.1 has-symbols: 1.1.0 @@ -11121,7 +11169,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libphonenumber-js@1.12.42: {} + libphonenumber-js@1.13.3: {} lie@3.1.1: dependencies: @@ -11200,7 +11248,7 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.2 - local-pkg@1.1.2: + local-pkg@1.2.1: dependencies: mlly: 1.8.2 pkg-types: 2.3.1 @@ -11239,6 +11287,8 @@ snapshots: lru-cache@11.3.5: {} + lru-cache@11.5.0: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -11666,7 +11716,7 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 5.0.6 minimatch@3.1.5: dependencies: @@ -11784,7 +11834,7 @@ snapshots: normalize-package-data@8.0.0: dependencies: hosted-git-info: 9.0.3 - semver: 7.7.4 + semver: 7.8.1 validate-npm-package-license: 3.0.4 npm-run-path@4.0.1: @@ -11798,7 +11848,7 @@ snapshots: object-assign@4.1.1: {} - object-deep-merge@2.0.0: {} + object-deep-merge@2.0.1: {} object-inspect@1.13.4: {} @@ -11811,7 +11861,7 @@ snapshots: call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 has-symbols: 1.1.0 object-keys: 1.1.1 @@ -11820,14 +11870,14 @@ snapshots: call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 object.fromentries@2.0.8: dependencies: call-bind: 1.0.9 define-properties: 1.2.1 es-abstract: 1.24.2 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 object.groupby@1.0.3: dependencies: @@ -11840,7 +11890,7 @@ snapshots: call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 obug@2.1.1: {} @@ -11967,7 +12017,7 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.3.5 + lru-cache: 11.5.0 minipass: 7.1.3 path-to-regexp@6.3.0: {} @@ -12051,7 +12101,7 @@ snapshots: optionalDependencies: prettier: 3.8.3 - prettier-plugin-tailwindcss@0.7.4(@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.3))(prettier@3.8.3): + prettier-plugin-tailwindcss@0.8.0(@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.3))(prettier@3.8.3): dependencies: prettier: 3.8.3 optionalDependencies: @@ -12369,7 +12419,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 get-proto: 1.0.1 which-builtin-type: 1.2.1 @@ -12456,10 +12506,10 @@ snapshots: resolve-pkg-maps@1.0.0: {} - resolve@2.0.0-next.6: + resolve@2.0.0-next.7: dependencies: es-errors: 1.3.0 - is-core-module: 2.16.1 + is-core-module: 2.16.2 node-exports-info: 1.6.0 object-keys: 1.1.1 path-parse: 1.0.7 @@ -12566,6 +12616,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.1: {} + send@1.2.1: dependencies: debug: 4.4.3 @@ -12615,7 +12667,7 @@ snapshots: dependencies: dunder-proto: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 setimmediate@1.0.5: {} @@ -12760,7 +12812,7 @@ snapshots: detect-newline: 4.0.1 git-hooks-list: 4.2.1 is-plain-obj: 4.1.0 - semver: 7.7.4 + semver: 7.8.1 sort-object-keys: 2.1.0 tinyglobby: 0.2.16 @@ -12840,7 +12892,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 gopd: 1.2.0 has-symbols: 1.1.0 @@ -12861,7 +12913,7 @@ snapshots: define-data-property: 1.1.4 define-properties: 1.2.1 es-abstract: 1.24.2 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 has-property-descriptors: 1.0.2 string.prototype.trimend@1.0.9: @@ -12869,13 +12921,13 @@ snapshots: call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 string.prototype.trimstart@1.0.8: dependencies: call-bind: 1.0.9 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 string_decoder@1.1.1: dependencies: @@ -13011,6 +13063,7 @@ snapshots: dependencies: picomatch: 4.0.4 typescript: 5.9.3 + optional: true ts-morph@26.0.0: dependencies: @@ -13093,12 +13146,12 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.59.1(@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -13500,9 +13553,9 @@ snapshots: dependencies: zod: 3.25.76 - zod-validation-error@4.0.2(zod@4.4.2): + zod-validation-error@4.0.2(zod@4.4.3): dependencies: - zod: 4.4.2 + zod: 4.4.3 zod@3.25.76: {} diff --git a/src/app/ai/chat/route.ts b/src/app/ai/chat/route.ts index 41ab66ea..10f863e1 100644 --- a/src/app/ai/chat/route.ts +++ b/src/app/ai/chat/route.ts @@ -8,6 +8,11 @@ import { env } from "@/env"; import { resolveImages } from "@/lib/ai/images"; import { chatModel } from "@/lib/ai/model"; import type { LabeledImage } from "@/lib/ai/prompts"; +import { + checkRateLimit, + createRateLimitExceededResponse, + createRateLimitHeaders, +} from "@/lib/ai/rate-limit"; import { API_URL } from "@/lib/api"; import { AUTH_COOKIES } from "@/lib/auth/constants"; import { PermissionAction, hasPermission } from "@/lib/auth/permissions"; @@ -72,6 +77,16 @@ export async function POST(request: Request) { return new Response("AI features require a full account", { status: 403 }); } + const rateLimitResult = checkRateLimit(user.user_id, "ai-chat", { + limit: env.AI_CHAT_RATE_LIMIT, + window: env.AI_CHAT_RATE_WINDOW, + }); + const rateLimitHeaders = createRateLimitHeaders(rateLimitResult); + + if (!rateLimitResult.allowed) { + return createRateLimitExceededResponse(rateLimitResult); + } + const { messages, system, @@ -233,5 +248,7 @@ export async function POST(request: Request) { }, }); - return result.toUIMessageStreamResponse(); + return result.toUIMessageStreamResponse({ + headers: rateLimitHeaders, + }); } diff --git a/src/app/ai/explain/route.ts b/src/app/ai/explain/route.ts index eb225c86..5414000c 100644 --- a/src/app/ai/explain/route.ts +++ b/src/app/ai/explain/route.ts @@ -4,6 +4,11 @@ import { env } from "@/env"; import { resolveImages } from "@/lib/ai/images"; import { chatModel } from "@/lib/ai/model"; import type { LabeledImage } from "@/lib/ai/prompts"; +import { + checkRateLimit, + createRateLimitExceededResponse, + createRateLimitHeaders, +} from "@/lib/ai/rate-limit"; import { PermissionAction, hasPermission } from "@/lib/auth/permissions"; import { getServerCurrentUser } from "@/lib/auth/utils.server"; @@ -22,6 +27,16 @@ export async function POST(request: Request) { return new Response("AI features require a full account", { status: 403 }); } + const rateLimitResult = checkRateLimit(user.user_id, "ai-explain", { + limit: env.AI_EXPLAIN_RATE_LIMIT, + window: env.AI_EXPLAIN_RATE_WINDOW, + }); + const rateLimitHeaders = createRateLimitHeaders(rateLimitResult); + + if (!rateLimitResult.allowed) { + return createRateLimitExceededResponse(rateLimitResult); + } + const { system, prompt, images } = (await request.json()) as { system: string; prompt: string; @@ -47,5 +62,7 @@ export async function POST(request: Request) { : prompt, }); - return result.toTextStreamResponse(); + return result.toTextStreamResponse({ + headers: rateLimitHeaders, + }); } diff --git a/src/app/edit-quiz/[quizId]/client.tsx b/src/app/edit-quiz/[quizId]/client.tsx index b2dd9113..6581d015 100644 --- a/src/app/edit-quiz/[quizId]/client.tsx +++ b/src/app/edit-quiz/[quizId]/client.tsx @@ -2,17 +2,21 @@ import { useQueryClient } from "@tanstack/react-query"; import { useRouter, useSearchParams } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { toast } from "sonner"; import { Loader } from "@/components/loader"; +import { + quizDetailQueryKey, + quizQueryKey, +} from "@/components/quiz/helpers/utils"; import { QuizEditor } from "@/components/quiz/quiz-editor"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Card, CardContent } from "@/components/ui/card"; +import { useQuiz } from "@/hooks/use-quiz"; import type { QuizFormData } from "@/lib/schemas/quiz.schema"; import { prepareQuizForSubmission } from "@/lib/schemas/quiz.schema"; import { getQuizService } from "@/services"; -import type { Quiz } from "@/types/quiz"; interface EditQuizPageClientProps { quizId: string; @@ -27,51 +31,47 @@ function EditQuizPageContent({ const router = useRouter(); const searchParameters = useSearchParams(); - const [initialQuiz, setInitialQuiz] = useState(null); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(true); + const { + data: initialQuiz, + isLoading, + isError, + } = useQuiz(quizId, { + enabled: quizId.trim() !== "", + }); + const isInvalidQuizId = quizId.trim() === ""; if (typeof document !== "undefined") { document.title = "Edytuj quiz - Testownik Solvro"; } useEffect(() => { - async function fetchQuiz() { - if (quizId.trim() === "") { - setError("Nieprawidłowy identyfikator quizu."); - setLoading(false); - return; - } - try { - const data: Quiz = await getQuizService().getQuiz(quizId); - setInitialQuiz(data); - setTimeout(() => { - const scrollTo = searchParameters.get("scroll_to"); - const hashId = window.location.hash.slice(1); - const id = scrollTo ?? hashId; - if (id !== "") { - const element = document.querySelector(`#${id}`); - if (element !== null) { - element.scrollIntoView({ behavior: "smooth" }); - if (window.location.hash) { - window.history.replaceState( - null, - "", - window.location.pathname + window.location.search, - ); - } - } + if (initialQuiz == null) { + return; + } + + const timeout = setTimeout(() => { + const scrollTo = searchParameters.get("scroll_to"); + const hashId = window.location.hash.slice(1); + const id = scrollTo ?? hashId; + if (id !== "") { + const element = document.querySelector(`#${id}`); + if (element !== null) { + element.scrollIntoView({ behavior: "smooth" }); + if (window.location.hash) { + window.history.replaceState( + null, + "", + window.location.pathname + window.location.search, + ); } - }, 100); - } catch { - setError("Wystąpił błąd podczas ładowania quizu."); - } finally { - setLoading(false); + } } - } + }, 100); - void fetchQuiz(); - }, [quizId, searchParameters]); + return () => { + clearTimeout(timeout); + }; + }, [initialQuiz, searchParameters]); async function handleSave(data: QuizFormData): Promise { if (quizId.trim() === "") { @@ -80,7 +80,8 @@ function EditQuizPageContent({ } const payload = prepareQuizForSubmission(data); try { - await getQuizService().updateQuiz(quizId, payload); + const updatedQuiz = await getQuizService().updateQuiz(quizId, payload); + queryClient.setQueryData(quizQueryKey(quizId), updatedQuiz); toast.success("Quiz został zaktualizowany."); return true; } catch { @@ -92,7 +93,9 @@ function EditQuizPageContent({ async function handleSaveAndClose(data: QuizFormData): Promise { const ok = await handleSave(data); if (ok) { - await queryClient.refetchQueries({ queryKey: ["quiz", quizId] }); + await queryClient.refetchQueries({ + queryKey: quizDetailQueryKey(quizId), + }); const navigation = window.navigation as Navigation | null; if (navigation?.canGoBack === true) { @@ -104,7 +107,7 @@ function EditQuizPageContent({ return ok; } - if (loading) { + if (isLoading) { return ( @@ -119,9 +122,13 @@ function EditQuizPageContent({ return ( <> - {error !== null && error !== "" ? ( + {isInvalidQuizId || isError ? ( - {error} + + {isInvalidQuizId + ? "Nieprawidłowy identyfikator quizu." + : "Wystąpił błąd podczas ładowania quizu."} + ) : null} { setSettings({ ...settings, [name]: value }); try { diff --git a/src/app/quiz/[quizId]/stats/client.tsx b/src/app/quiz/[quizId]/stats/client.tsx new file mode 100644 index 00000000..f7e1da3f --- /dev/null +++ b/src/app/quiz/[quizId]/stats/client.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { ArrowLeftIcon } from "lucide-react"; + +import { HardestQuestionsChart } from "@/components/quiz/stats/hardest-questions-chart"; +import { HourlyChart } from "@/components/quiz/stats/hourly-chart"; +import { ScoreChart } from "@/components/quiz/stats/score-chart"; +import { SessionsChart } from "@/components/quiz/stats/sessions-chart"; +import { StatsCardErrorBoundaryGroup } from "@/components/quiz/stats/stats-card-error-boundary"; +import { StatsInfoCard } from "@/components/quiz/stats/stats-info-card"; +import { StatsTable } from "@/components/quiz/stats/stats-table"; +import { StudyTimeChart } from "@/components/quiz/stats/study-time-chart"; +import { ButtonLink } from "@/components/ui/button"; +import { useQuizMetadata, useQuizStats } from "@/hooks/use-quiz-stats"; + +interface StatsPageClientProps { + quizId: string; +} + +export function StatsPageClient({ quizId }: StatsPageClientProps) { + const { data: quiz } = useQuizMetadata(quizId); + const { data: myStats } = useQuizStats(quizId, "me"); + + if (quiz === undefined) { + return null; + } + + const canViewAll = quiz.can_edit === true; + + return ( +
+
+ + + +

Statystyki: {quiz.title}

+
+ +
+ + + + +
+ +
+ + + + + + +
+ + + + +
+ ); +} diff --git a/src/app/quiz/[quizId]/stats/page.tsx b/src/app/quiz/[quizId]/stats/page.tsx new file mode 100644 index 00000000..4c4a5d81 --- /dev/null +++ b/src/app/quiz/[quizId]/stats/page.tsx @@ -0,0 +1,80 @@ +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; +import type { Metadata } from "next"; +import { cookies } from "next/headers"; +import { Suspense } from "react"; + +import { Loader } from "@/components/loader"; +import { Card, CardContent } from "@/components/ui/card"; +import { quizStatsKeys } from "@/hooks/use-quiz-stats"; +import { API_URL } from "@/lib/api"; +import { AUTH_COOKIES } from "@/lib/auth/constants"; +import { PermissionAction, hasPermission } from "@/lib/auth/permissions"; +import { getServerCurrentUser } from "@/lib/auth/utils.server"; +import { getQueryClient } from "@/lib/query-client"; +import { QuizService } from "@/services/quiz.service"; + +import { StatsPageClient } from "./client"; + +export const metadata: Metadata = { + title: "Statystyki quizu", +}; + +export default async function StatsPage({ + params, +}: PageProps<"/quiz/[quizId]/stats">) { + const { quizId } = await params; + const cookieStore = await cookies(); + const accessToken = cookieStore.get(AUTH_COOKIES.ACCESS_TOKEN)?.value; + const user = await getServerCurrentUser(); + + if (user === null) { + throw new Error("Wymagane logowanie (401)"); + } + + if (!hasPermission(user.account_type, PermissionAction.VIEW_QUIZ_STATS)) { + throw new Error("Brak dostępu (403)"); + } + + const queryClient = getQueryClient(); + await queryClient.fetchQuery({ + queryKey: quizStatsKeys.metadata(quizId), + queryFn: async () => + new QuizService(API_URL, {}, accessToken).getQuizMetadata(quizId), + }); + + await Promise.all([ + queryClient.prefetchQuery({ + queryKey: quizStatsKeys.aggregated(quizId, "me"), + queryFn: async () => + new QuizService(API_URL, {}, accessToken).getQuizStats(quizId, "me"), + }), + queryClient.prefetchQuery({ + queryKey: quizStatsKeys.timeline(quizId, "me", 30), + queryFn: async () => + new QuizService(API_URL, {}, accessToken).getQuizTimeline( + quizId, + "me", + 30, + ), + }), + ]); + + return ( + + + +
+

Ładowanie statystyk...

+ +
+
+
+ } + > + + + + ); +} diff --git a/src/app/quizzes/client.tsx b/src/app/quizzes/client.tsx index 0cf93825..da827718 100644 --- a/src/app/quizzes/client.tsx +++ b/src/app/quizzes/client.tsx @@ -41,7 +41,7 @@ import { useSharedQuizzes, useUserQuizzes } from "@/hooks/use-quizzes"; import { PermissionAction } from "@/lib/auth/permissions"; import { prepareQuizForDownload } from "@/lib/quiz-download"; import { getQuizService } from "@/services"; -import type { QuizMetadata, SharedQuiz } from "@/types/quiz"; +import type { QuizBase, QuizMetadata, SharedQuiz } from "@/types/quiz"; interface QuizzesPageContentProps { userId?: string; @@ -141,7 +141,7 @@ function QuizzesPageContent({ userId }: QuizzesPageContentProps) { try { const fullQuiz = await getQuizService().getQuiz(quiz.id); // Create a downloadable version - const downloadableQuiz = prepareQuizForDownload(fullQuiz); + const downloadableQuiz = await prepareQuizForDownload(fullQuiz); const url = window.URL.createObjectURL( new Blob([JSON.stringify(downloadableQuiz, null, 2)], { type: "application/json", @@ -158,13 +158,13 @@ function QuizzesPageContent({ userId }: QuizzesPageContentProps) { } }; - const updateQuiz = (quiz: QuizMetadata) => { + const updateQuiz = (quiz: QuizBase) => { queryClient.setQueryData( ["user-quizzes"], (old: QuizMetadata[] | undefined) => { return old === undefined ? [] - : old.map((q) => (q.id === quiz.id ? quiz : q)); + : old.map((q) => (q.id === quiz.id ? { ...q, ...quiz } : q)); }, ); }; diff --git a/src/app/search-in-quiz/[quizId]/client.tsx b/src/app/search-in-quiz/[quizId]/client.tsx index 801664fe..4b33c9bb 100644 --- a/src/app/search-in-quiz/[quizId]/client.tsx +++ b/src/app/search-in-quiz/[quizId]/client.tsx @@ -2,14 +2,13 @@ import { distance } from "fastest-levenshtein"; import { AlertCircleIcon } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import type { ReactNode } from "react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; -import { getQuizService } from "@/services"; -import type { Quiz } from "@/types/quiz"; +import { useQuiz } from "@/hooks/use-quiz"; interface SearchInQuizPageClientProps { quizId: string; @@ -43,37 +42,20 @@ const highlightMatch = (text: string, q: string): ReactNode => { }; function SearchInQuizPageContent({ quizId }: { quizId: string }) { - const [quiz, setQuiz] = useState(null); const [query, setQuery] = useState(""); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const { + data: quiz, + isLoading, + isError, + } = useQuiz(quizId, { + enabled: quizId.trim() !== "", + }); + const isInvalidQuizId = quizId.trim() === ""; if (typeof document !== "undefined") { document.title = `Wyszukaj w quizach - ${quiz?.title ?? "Ładowanie..."} - Testownik Solvro`; } - useEffect(() => { - const fetchQuiz = async () => { - if (quizId.trim() === "") { - setError("Nieprawidłowy identyfikator quizu."); - setLoading(false); - return; - } - - setLoading(true); - try { - const data = await getQuizService().getQuiz(quizId); - setQuiz(data); - } catch { - setError("Wystąpił błąd podczas ładowania quizu."); - } finally { - setLoading(false); - } - }; - - void fetchQuiz(); - }, [quizId]); - const filteredQuestions = (() => { if (quiz == null || !query.trim()) { return quiz?.questions ?? []; @@ -171,16 +153,20 @@ function SearchInQuizPageContent({ quizId }: { quizId: string }) { .toSorted((a, b) => a.relevance - b.relevance); })(); - if (loading) { + if (isLoading) { return
Ładowanie...
; } - if (error != null) { + if (isInvalidQuizId || isError) { return ( Wystąpił błąd: - {error} + + {isInvalidQuizId + ? "Nieprawidłowy identyfikator quizu." + : "Wystąpił błąd podczas ładowania quizu."} + ); } diff --git a/src/components/account-level-badge.tsx b/src/components/account-level-badge.tsx index 10a1b3e2..4207b425 100644 --- a/src/components/account-level-badge.tsx +++ b/src/components/account-level-badge.tsx @@ -1,10 +1,12 @@ -import { CrownIcon } from "lucide-react"; +import { CrownIcon, MedalIcon } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; +import { ACCOUNT_LEVEL } from "@/types/user"; +import type { AccountLevel } from "@/types/user"; interface AccountLevelBadgeProps { - accountLevel: "basic" | "gold"; + accountLevel: AccountLevel; className?: string; } @@ -12,19 +14,30 @@ export function AccountLevelBadge({ accountLevel, className, }: AccountLevelBadgeProps) { - if (accountLevel !== "gold") { + if (accountLevel === ACCOUNT_LEVEL.BASIC) { return null; } + const config = + accountLevel === ACCOUNT_LEVEL.GOLD + ? { + label: "Gold", + Icon: CrownIcon, + className: + "border border-amber-300/50 bg-gradient-to-r from-amber-200/60 via-yellow-200/80 to-amber-300/70 text-amber-900 shadow-[0_4px_14px_-10px_rgba(217,119,6,0.95)] dark:border-amber-300/30 dark:from-amber-300/20 dark:via-yellow-300/25 dark:to-amber-300/15 dark:text-amber-200", + } + : { + label: "Silver", + Icon: MedalIcon, + className: + "border border-slate-300/70 bg-gradient-to-r from-slate-100/80 via-white to-slate-200/80 text-slate-800 shadow-[0_4px_14px_-10px_rgba(100,116,139,0.9)] dark:border-slate-200/30 dark:from-slate-200/20 dark:via-white/20 dark:to-slate-300/15 dark:text-slate-100", + }; + const { Icon } = config; + return ( - - - Gold + + + {config.label} ); } diff --git a/src/components/ai/ai-explain-card.tsx b/src/components/ai/ai-explain-card.tsx index 0a777199..d6045367 100644 --- a/src/components/ai/ai-explain-card.tsx +++ b/src/components/ai/ai-explain-card.tsx @@ -8,7 +8,7 @@ import { SquareIcon, XIcon, } from "lucide-react"; -import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { MarkdownRenderer } from "@/components/markdown-renderer"; import { Button } from "@/components/ui/button"; @@ -95,8 +95,10 @@ export function AiExplainCard({ onAnswerHints, }: AiExplainCardProps) { const startedRef = useRef(false); + // eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler const previousQuestionId = useRef(question.id); const lastHintsRef = useRef(""); + const [retryAfter, setRetryAfter] = useState(null); const system = useMemo( () => @@ -116,33 +118,93 @@ export function AiExplainCard({ const images = useMemo(() => collectQuestionImages(question), [question]); + const fetchWithRateLimitHandling = useCallback( + async (input, init) => { + const response = await fetch(input, init); + if (response.status !== 429) { + return response; + } + + const retryAfterHeader = response.headers.get("Retry-After"); + const parsedRetryAfter = + retryAfterHeader === null + ? null + : Number.parseInt(retryAfterHeader, 10); + const nextRetryAfter = + parsedRetryAfter === null || Number.isNaN(parsedRetryAfter) + ? 60 + : Math.max(1, parsedRetryAfter); + + setRetryAfter(nextRetryAfter); + + return new Response( + `Osiągnięto limit zapytań AI. Spróbuj ponownie za ${nextRetryAfter.toString()} s.`, + { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }, + ); + }, + [], + ); + const { completion, isLoading, error, complete, stop } = useCompletion({ api: "/ai/explain", streamProtocol: "text", + fetch: fetchWithRateLimitHandling, onError: () => { startedRef.current = false; }, }); - const handleStart = useCallback(() => { + const startCompletion = useCallback(() => { + setRetryAfter(null); startedRef.current = true; void complete(prompt, { body: { system, images } }); }, [complete, prompt, system, images]); + const handleStart = useCallback(() => { + if (retryAfter !== null && retryAfter > 0) { + return; + } + startCompletion(); + }, [retryAfter, startCompletion]); + + /* eslint-disable react-you-might-not-need-an-effect/no-event-handler */ useEffect(() => { if (previousQuestionId.current !== question.id) { previousQuestionId.current = question.id; onClose(); } }, [question.id, onClose]); + /* eslint-enable react-you-might-not-need-an-effect/no-event-handler */ + /* eslint-disable react-you-might-not-need-an-effect/no-derived-state */ useEffect(() => { if (!startedRef.current) { - handleStart(); + startCompletion(); + } + }, [startCompletion]); + /* eslint-enable react-you-might-not-need-an-effect/no-derived-state */ + + useEffect(() => { + if (retryAfter === null || retryAfter <= 0) { + return; } - }, [handleStart]); - /* eslint-disable react-you-might-not-need-an-effect/no-pass-data-to-parent */ + const timeout = window.setTimeout(() => { + setRetryAfter((current) => + current === null || current <= 1 ? 0 : current - 1, + ); + }, 1000); + + return () => { + window.clearTimeout(timeout); + }; + }, [retryAfter]); + + /* eslint-disable react-you-might-not-need-an-effect/no-event-handler, react-you-might-not-need-an-effect/no-pass-data-to-parent */ useEffect(() => { if (!questionChecked && completion !== "" && onAnswerHints !== undefined) { const { answerHints } = parseHints(completion); @@ -153,7 +215,7 @@ export function AiExplainCard({ } } }, [completion, questionChecked, onAnswerHints]); - /* eslint-enable react-you-might-not-need-an-effect/no-pass-data-to-parent */ + /* eslint-enable react-you-might-not-need-an-effect/no-event-handler, react-you-might-not-need-an-effect/no-pass-data-to-parent */ const { displayContent, hasAnswerHintsOnly } = useMemo(() => { if (questionChecked) { @@ -176,6 +238,12 @@ export function AiExplainCard({ }, [completion, questionChecked]); const title = questionChecked ? "Wyjaśnienie AI" : "Wskazówka AI"; + const errorMessage = + retryAfter === null + ? error?.message + : retryAfter > 0 + ? `Osiągnięto limit podpowiedzi AI. Spróbuj ponownie za ${retryAfter.toString()} s.` + : "Limit podpowiedzi AI minął. Możesz spróbować ponownie."; return ( - Nie udało się wygenerować{" "} - {questionChecked ? "wyjaśnienia" : "wskazówki"}. + {errorMessage ?? + `Nie udało się wygenerować ${ + questionChecked ? "wyjaśnienia" : "wskazówki" + }.`} { @@ -160,10 +184,11 @@ export function SettingsForm({ settings, onSettingChange }: SettingsFormProps) { { - const value = event_.target.value; + onChange={(_event) => { + const value = _event.target.value; const numberValue = Math.floor(Number(value)); setLocalWrongAnswerReoccurrences(numberValue.toString()); if (!Number.isNaN(numberValue) && numberValue >= 0) { @@ -199,15 +224,37 @@ export function SettingsForm({ settings, onSettingChange }: SettingsFormProps) {
+
+
+ +
+
+
+ +
+
+
- { - const value = event_.target.value; - const numberValue = Math.floor(Number(value)); - setLocalMaxQuestionReoccurrences(numberValue.toString()); - if (!Number.isNaN(numberValue) && numberValue >= 1) { - debouncedSave("max_question_reoccurrences", numberValue); - } - }} - aria-invalid={(() => { - const numberValue = Number.parseInt( - localMaxQuestionReoccurrences, - ); - return Number.isNaN(numberValue) || numberValue < 1; - })()} - className="h-8 w-16 [appearance:textfield] text-center font-semibold [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" - /> + {isMaxReoccurrencesEnabled ? ( + { + const value = _event.target.value; + const numberValue = Math.floor(Number(value)); + setLocalMaxQuestionReoccurrences(numberValue.toString()); + if (!Number.isNaN(numberValue) && numberValue >= 1) { + debouncedSave("max_question_reoccurrences", numberValue); + } + }} + aria-invalid={(() => { + const numberValue = Number.parseInt( + localMaxQuestionReoccurrences, + ); + return Number.isNaN(numberValue) || numberValue < 1; + })()} + className="h-8 w-16 [appearance:textfield] text-center font-semibold [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" + /> + ) : ( +
+ + +
+ )} + } > Wyszukaj w quizie @@ -210,7 +206,24 @@ export function QuizInfoCard({ Kopiuj link do quizu ) : null} - {isCreator ? null : ( + {canViewStats ? ( + + + + + } + > + Statystyki quizu + + ) : null} + {canEditQuiz ? null : (
- +
- + - + void; - quiz: QuizMetadata; - setQuiz?: (quiz: QuizMetadata) => void; + quiz: QuizBase; + setQuiz?: (quiz: QuizBase) => void; } export function ShareQuizDialog({ @@ -92,6 +92,7 @@ export function ShareQuizDialog({ initialData: [], }); + /* eslint-disable react-you-might-not-need-an-effect/no-event-handler */ const { data: sharedData, isLoading: isSharedDataLoading } = useQuery({ queryKey: ["shared-quiz", quiz.id], queryFn: async () => @@ -99,6 +100,7 @@ export function ShareQuizDialog({ enabled: open && canShareQuiz, staleTime: 0, }); + /* eslint-enable react-you-might-not-need-an-effect/no-event-handler */ const loading = isSharedDataLoading || isUserGroupsLoading; diff --git a/src/components/quiz/stats/chart-card.tsx b/src/components/quiz/stats/chart-card.tsx new file mode 100644 index 00000000..0c1b2aa4 --- /dev/null +++ b/src/components/quiz/stats/chart-card.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { Badge } from "@/components/ui/badge"; +import { + Card, + CardAction, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Spinner } from "@/components/ui/spinner"; +import { cn } from "@/lib/utils"; +import type { StatsScope } from "@/types/quiz-stats"; + +import { ScopeToggle } from "./scope-toggle"; + +const DEFAULT_CHART_PLACEHOLDER_CLASS = + "aspect-video min-h-[250px] w-full rounded-lg"; + +function ChartCardContent({ + isLoading, + isRefreshing, + isEmpty, + placeholderClassName, + children, +}: { + isLoading: boolean; + isRefreshing: boolean; + isEmpty: boolean; + placeholderClassName: string; + children: React.ReactNode; +}): React.JSX.Element | React.ReactNode { + if (isLoading) { + return ; + } + + return ( +
+
+ {isEmpty ? ( +
+

Brak danych

+

+ Brak sesji w wybranym przedziale czasowym. +

+
+ ) : ( + children + )} +
+ {isRefreshing ? ( + + + Ładuję + + ) : null} +
+ ); +} + +interface ChartCardProps { + title: string; + description?: string; + scope: StatsScope; + onScopeChange: (scope: StatsScope) => void; + canViewAll: boolean; + isLoading: boolean; + isRefreshing?: boolean; + isEmpty: boolean; + placeholderClassName?: string; + toolbar?: React.ReactNode; + children: React.ReactNode; +} + +export function ChartCard({ + title, + description, + scope, + onScopeChange, + canViewAll, + isLoading, + isRefreshing = false, + isEmpty, + placeholderClassName = DEFAULT_CHART_PLACEHOLDER_CLASS, + toolbar, + children, +}: ChartCardProps) { + return ( + + +
+ {title} + {description == null ? null : ( + {description} + )} +
+ {toolbar != null || canViewAll ? ( + + {toolbar} + {canViewAll ? ( + + ) : null} + + ) : null} +
+ + + {children} + + +
+ ); +} diff --git a/src/components/quiz/stats/chart-mode-toggle.tsx b/src/components/quiz/stats/chart-mode-toggle.tsx new file mode 100644 index 00000000..621f739f --- /dev/null +++ b/src/components/quiz/stats/chart-mode-toggle.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +export type ChartMode = "timeline" | "sessions"; + +interface ChartModeToggleProps { + mode: ChartMode; + onModeChange: (mode: ChartMode) => void; + canUseSessions: boolean; +} + +export function ChartModeToggle({ + mode, + onModeChange, + canUseSessions, +}: ChartModeToggleProps) { + return ( + { + onModeChange(v as ChartMode); + }} + > + + Dni + + Sesje + + + + ); +} diff --git a/src/components/quiz/stats/hardest-questions-chart.tsx b/src/components/quiz/stats/hardest-questions-chart.tsx new file mode 100644 index 00000000..960789e7 --- /dev/null +++ b/src/components/quiz/stats/hardest-questions-chart.tsx @@ -0,0 +1,179 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { Label, Pie, PieChart } from "recharts"; + +import type { ChartConfig } from "@/components/ui/chart"; +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart"; +import { useQuizHardestQuestions } from "@/hooks/use-quiz-stats"; +import type { StatsScope } from "@/types/quiz-stats"; + +import { ChartCard } from "./chart-card"; + +interface HardestQuestionsChartProps { + quizId: string; + canViewAll: boolean; +} + +const COLORS = [ + "var(--chart-1)", + "var(--chart-2)", + "var(--chart-3)", + "var(--chart-4)", + "var(--chart-5)", + "var(--chart-6)", + "var(--chart-7)", + "var(--chart-8)", + "var(--chart-9)", + "var(--chart-10)", +]; + +export function HardestQuestionsChart({ + quizId, + canViewAll, +}: HardestQuestionsChartProps) { + const [scope, setScope] = useState("me"); + const { data, isFetching, isPending } = useQuizHardestQuestions( + quizId, + scope, + ); + const isRefreshing = isFetching && !isPending; + + const totalWrong = useMemo( + () => data?.reduce((sum, q) => sum + q.wrong_answers, 0) ?? 0, + [data], + ); + + const chartConfig = useMemo(() => { + const config: ChartConfig = {}; + if (data != null) { + for (const [index, q] of data.entries()) { + config[q.question_id] = { + label: q.question_text, + color: COLORS[index % COLORS.length], + }; + } + } + return config; + }, [data]); + + const chartData = useMemo( + () => + data?.map((q, index) => ({ + name: q.question_text, + value: q.wrong_answers, + total: q.total_answers, + fill: COLORS[index % COLORS.length], + })) ?? [], + [data], + ); + + const hasData = data != null && data.length > 0; + + return ( + +
+ + + { + const payload = item.payload as + | { total?: number } + | undefined; + return [ + `${String(value)} / ${String(payload?.total ?? "")} · `, + String(name), + ]; + }} + /> + } + /> + + {/* eslint-disable-next-line jsx-a11y/label-has-associated-control -- recharts Label, not a form label */} + + + +
+ {data?.map((q, index) => ( +
+
+ + {q.question_text} + + + {q.wrong_answers} / {q.total_answers} + +
+ ))} +
+
+ + ); +} diff --git a/src/components/quiz/stats/hourly-chart.tsx b/src/components/quiz/stats/hourly-chart.tsx new file mode 100644 index 00000000..a3757347 --- /dev/null +++ b/src/components/quiz/stats/hourly-chart.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from "recharts"; + +import type { ChartConfig } from "@/components/ui/chart"; +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart"; +import { useQuizHourly } from "@/hooks/use-quiz-stats"; +import type { StatsScope } from "@/types/quiz-stats"; + +import { ChartCard } from "./chart-card"; + +interface HourlyChartProps { + quizId: string; + canViewAll: boolean; +} + +const chartConfig = { + sessions_count: { + label: "Sesje", + color: "var(--chart-4)", + }, +} satisfies ChartConfig; + +export function HourlyChart({ quizId, canViewAll }: HourlyChartProps) { + const [scope, setScope] = useState("me"); + const { data, isFetching, isPending } = useQuizHourly(quizId, scope); + const isRefreshing = isFetching && !isPending; + + const chartData = useMemo( + () => + data?.map((d) => ({ + hour: d.hour.toString().padStart(2, "0"), + sessions_count: d.sessions_count, + })) ?? [], + [data], + ); + + const hasData = useMemo( + () => data?.some((d) => d.sessions_count > 0) ?? false, + [data], + ); + + return ( + + + + } /> + + + + + + + ); +} diff --git a/src/components/quiz/stats/scope-toggle.tsx b/src/components/quiz/stats/scope-toggle.tsx new file mode 100644 index 00000000..4e7605ad --- /dev/null +++ b/src/components/quiz/stats/scope-toggle.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import type { StatsScope } from "@/types/quiz-stats"; + +interface ScopeToggleProps { + scope: StatsScope; + onScopeChange: (scope: StatsScope) => void; +} + +export function ScopeToggle({ scope, onScopeChange }: ScopeToggleProps) { + return ( + { + onScopeChange(v as StatsScope); + }} + > + + Ty + Wszyscy + + + ); +} diff --git a/src/components/quiz/stats/score-chart.tsx b/src/components/quiz/stats/score-chart.tsx new file mode 100644 index 00000000..05566c5d --- /dev/null +++ b/src/components/quiz/stats/score-chart.tsx @@ -0,0 +1,231 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"; + +import type { ChartConfig } from "@/components/ui/chart"; +import { ChartContainer, ChartTooltip } from "@/components/ui/chart"; +import { useQuizSessions, useQuizTimeline } from "@/hooks/use-quiz-stats"; +import type { StatsScope } from "@/types/quiz-stats"; + +import { ChartCard } from "./chart-card"; +import { ChartModeToggle } from "./chart-mode-toggle"; +import type { ChartMode } from "./chart-mode-toggle"; +import { SessionTooltip } from "./session-tooltip"; +import { TimelineTooltip } from "./timeline-tooltip"; + +interface ScoreChartProps { + quizId: string; + canViewAll: boolean; +} + +const chartConfig = { + accuracy: { + label: "Wynik (%)", + color: "var(--chart-2)", + }, +} satisfies ChartConfig; + +const TIMELINE_DAYS = 30; + +function formatDateKey(date: Date): string { + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + return `${year.toString()}-${month}-${day}`; +} + +function parseDateKey(dateKey: string): Date { + const [year, month, day] = dateKey.split("-").map(Number); + return new Date(year, month - 1, day); +} + +function getDaysThroughToday(startDateKey: string): string[] { + const start = parseDateKey(startDateKey); + const end = new Date(); + start.setHours(0, 0, 0, 0); + end.setHours(0, 0, 0, 0); + + if (start > end) { + return [formatDateKey(start)]; + } + + const days = + Math.round((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1; + + return Array.from({ length: days }, (_, index) => { + const date = new Date(start); + date.setDate(start.getDate() + index); + return formatDateKey(date); + }); +} + +export function ScoreChart({ quizId, canViewAll }: ScoreChartProps) { + const [scope, setScope] = useState("me"); + const [mode, setMode] = useState("sessions"); + const canUseSessions = scope === "me"; + const { + data: raw, + isFetching: isTimelineFetching, + isPending: isTimelinePending, + } = useQuizTimeline(quizId, scope, TIMELINE_DAYS); + const { + data: sessionsRaw, + isFetching: isSessionsFetching, + isPending: isSessionsPending, + } = useQuizSessions( + quizId, + "me", + TIMELINE_DAYS, + mode === "sessions" && canUseSessions, + ); + + const effectiveMode = canUseSessions ? mode : "timeline"; + const isPending = + effectiveMode === "sessions" ? isSessionsPending : isTimelinePending; + const isRefreshing = + effectiveMode === "sessions" + ? isSessionsFetching && !isSessionsPending + : isTimelineFetching && !isTimelinePending; + + const timelineData = useMemo(() => { + const entriesByDate = new Map(raw?.map((entry) => [entry.date, entry])); + const firstNonZeroEntry = raw?.find((entry) => entry.total_answers > 0); + + if (firstNonZeroEntry === undefined) { + return []; + } + + return getDaysThroughToday(firstNonZeroEntry.date).map((date, index) => { + const entry = entriesByDate.get(date); + + return { + date, + sessionIndex: index + 1, + sessions_count: entry?.sessions_count ?? 0, + total_answers: entry?.total_answers ?? 0, + correct_answers: entry?.correct_answers ?? 0, + total_study_time_seconds: entry?.total_study_time_seconds ?? 0, + accuracy: + entry !== undefined && entry.total_answers > 0 + ? Number( + ((entry.correct_answers / entry.total_answers) * 100).toFixed( + 1, + ), + ) + : null, + }; + }); + }, [raw]); + + const sessionData = useMemo( + () => + sessionsRaw?.map((entry, index) => ({ + ...entry, + date: entry.started_at, + sessionIndex: index + 1, + sessions_count: 1, + total_study_time_seconds: entry.study_time_seconds, + accuracy: Number(entry.accuracy.toFixed(1)), + })) ?? [], + [sessionsRaw], + ); + + const data = effectiveMode === "sessions" ? sessionData : timelineData; + const hasData = + effectiveMode === "sessions" + ? sessionData.some((d) => d.total_answers > 0) + : timelineData.some((d) => d.accuracy !== null); + const handleScopeChange = (nextScope: StatsScope) => { + setScope(nextScope); + + if (nextScope === "all") { + setMode("timeline"); + } + }; + + return ( + + } + > + + + + {effectiveMode === "sessions" ? ( + `#${value.toString()}`} + interval="preserveStartEnd" + minTickGap={24} + /> + ) : ( + { + const date = new Date(value); + return `${date.getDate().toString().padStart(2, "0")}.${(date.getMonth() + 1).toString().padStart(2, "0")}`; + }} + interval="preserveStartEnd" + minTickGap={40} + /> + )} + `${v.toString()}%`} + width={45} + /> + `${String(value)}%`} + /> + ) : ( + `${String(value)}%`} + /> + ) + } + /> + d.accuracy !== null).length <= 15} + /> + + + + ); +} diff --git a/src/components/quiz/stats/session-tooltip.tsx b/src/components/quiz/stats/session-tooltip.tsx new file mode 100644 index 00000000..6b006e2c --- /dev/null +++ b/src/components/quiz/stats/session-tooltip.tsx @@ -0,0 +1,129 @@ +"use client"; + +import type { TooltipContentProps, TooltipValueType } from "recharts"; + +import { cn } from "@/lib/utils"; +import type { SessionEntry } from "@/types/quiz-stats"; + +type SessionTooltipData = Partial & { + sessionIndex?: number; +}; + +interface SessionTooltipProps extends Partial< + TooltipContentProps +> { + primaryDataKey?: string; + primaryLabel?: string; + primaryFormatter?: (value: TooltipValueType) => string; +} + +function formatDateTime(value: string | null | undefined): string { + if (value == null) { + return "-"; + } + + return new Date(value).toLocaleString("pl-PL", { + day: "2-digit", + month: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); +} + +function formatNumber(value = 0): string { + return value.toLocaleString("pl-PL"); +} + +function formatTime(seconds = 0): string { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + + if (h > 0) { + return `${h.toString()}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; + } + + return `${m.toString()}:${s.toString().padStart(2, "0")}`; +} + +function TooltipRow({ + label, + value, +}: { + label: string; + value: string; +}): React.JSX.Element { + return ( +
+ {label} + + {value} + +
+ ); +} + +export function SessionTooltip({ + active, + payload = [], + primaryDataKey, + primaryLabel, + primaryFormatter, +}: SessionTooltipProps) { + if (active !== true || payload.length === 0) { + return null; + } + + const primaryItem = + primaryDataKey === undefined + ? payload[0] + : (payload.find((item) => item.dataKey === primaryDataKey) ?? payload[0]); + const entry = primaryItem.payload as SessionTooltipData | undefined; + + if (entry === undefined) { + return null; + } + + return ( +
+
+ Sesja {entry.sessionIndex?.toLocaleString("pl-PL") ?? "-"} +
+ {primaryLabel !== undefined && + primaryItem.value !== undefined && + primaryFormatter !== undefined ? ( +
+ +
+ ) : null} +
+ + + + + + +
+
+ ); +} diff --git a/src/components/quiz/stats/sessions-chart.tsx b/src/components/quiz/stats/sessions-chart.tsx new file mode 100644 index 00000000..f8d98cbe --- /dev/null +++ b/src/components/quiz/stats/sessions-chart.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useState } from "react"; +import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"; + +import type { ChartConfig } from "@/components/ui/chart"; +import { ChartContainer, ChartTooltip } from "@/components/ui/chart"; +import { useQuizTimeline } from "@/hooks/use-quiz-stats"; +import type { StatsScope } from "@/types/quiz-stats"; + +import { ChartCard } from "./chart-card"; +import { TimelineTooltip } from "./timeline-tooltip"; + +interface SessionsChartProps { + quizId: string; + canViewAll: boolean; +} + +const chartConfig = { + sessions_count: { + label: "Sesje", + color: "var(--chart-1)", + }, +} satisfies ChartConfig; + +export function SessionsChart({ quizId, canViewAll }: SessionsChartProps) { + const [scope, setScope] = useState("me"); + const { data, isFetching, isPending } = useQuizTimeline(quizId, scope); + const isRefreshing = isFetching && !isPending; + + const hasData = data?.some((d) => d.sessions_count > 0) ?? false; + + return ( + + + + + { + const date = new Date(value); + return `${date.getDate().toString().padStart(2, "0")}.${(date.getMonth() + 1).toString().padStart(2, "0")}`; + }} + interval="preserveStartEnd" + minTickGap={40} + /> + + + Number(value).toLocaleString("pl-PL") + } + /> + } + /> + + + + + ); +} diff --git a/src/components/quiz/stats/stats-card-error-boundary.tsx b/src/components/quiz/stats/stats-card-error-boundary.tsx new file mode 100644 index 00000000..a35e5a6d --- /dev/null +++ b/src/components/quiz/stats/stats-card-error-boundary.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { AlertTriangleIcon } from "lucide-react"; +import { Children, Component } from "react"; +import type { ReactNode } from "react"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +interface StatsCardErrorBoundaryProps { + resetKeys?: readonly unknown[]; + children: ReactNode; +} + +interface StatsCardErrorBoundaryState { + hasError: boolean; +} + +function areResetKeysEqual( + previousResetKeys: readonly unknown[], + resetKeys: readonly unknown[], +): boolean { + return ( + previousResetKeys.length === resetKeys.length && + previousResetKeys.every((key, index) => Object.is(key, resetKeys[index])) + ); +} + +export class StatsCardErrorBoundary extends Component< + StatsCardErrorBoundaryProps, + StatsCardErrorBoundaryState +> { + state: StatsCardErrorBoundaryState = { + hasError: false, + }; + + static getDerivedStateFromError(): StatsCardErrorBoundaryState { + return { hasError: true }; + } + + componentDidUpdate( + previousProps: Readonly, + ): void { + const previousResetKeys = previousProps.resetKeys ?? []; + const resetKeys = this.props.resetKeys ?? []; + + if ( + this.state.hasError && + !areResetKeysEqual(previousResetKeys, resetKeys) + ) { + this.setState({ hasError: false }); + } + } + + render(): ReactNode { + if (this.state.hasError) { + return ( + + + + + Sekcja statystyk + + + + Nie udało się wyświetlić tej sekcji statystyk. + + + ); + } + + return this.props.children; + } +} + +interface StatsCardErrorBoundaryGroupProps { + resetKeys?: readonly unknown[]; + children: ReactNode; +} + +export function StatsCardErrorBoundaryGroup({ + resetKeys, + children, +}: StatsCardErrorBoundaryGroupProps): ReactNode { + return Children.map(children, (child) => { + if (child == null) { + return child; + } + + return ( + + {child} + + ); + }); +} diff --git a/src/components/quiz/stats/stats-info-card.tsx b/src/components/quiz/stats/stats-info-card.tsx new file mode 100644 index 00000000..5e8fdc19 --- /dev/null +++ b/src/components/quiz/stats/stats-info-card.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { format } from "date-fns"; +import { + CalendarIcon, + ClockIcon, + FileTextIcon, + PencilIcon, + UserIcon, +} from "lucide-react"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import type { QuizMetadata } from "@/types/quiz"; + +interface StatsInfoCardProps { + quiz: QuizMetadata; + lastActivityAt: string | null; +} + +function InfoRow({ + icon: Icon, + label, + value, +}: { + icon: React.ComponentType<{ className?: string }>; + label: string; + value: string | null | undefined; +}) { + return ( +
+ +
+ {label} + {value ?? "—"} +
+
+ ); +} + +function formatDate(dateString: string | null | undefined): string | null { + if (dateString == null) { + return null; + } + return format(new Date(dateString), "dd.MM.yyyy HH:mm"); +} + +export function StatsInfoCard({ quiz, lastActivityAt }: StatsInfoCardProps) { + return ( + + + + + Informacje + + + + + + + + + + + ); +} diff --git a/src/components/quiz/stats/stats-table.tsx b/src/components/quiz/stats/stats-table.tsx new file mode 100644 index 00000000..f1ad3ba8 --- /dev/null +++ b/src/components/quiz/stats/stats-table.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { useQuizStats } from "@/hooks/use-quiz-stats"; +import type { QuizStats } from "@/types/quiz-stats"; + +interface StatsTableProps { + quizId: string; + canViewAll: boolean; +} + +function formatTime(seconds: number): string { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + if (h > 0) { + return `${h.toString()}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; + } + return `${m.toString()}:${s.toString().padStart(2, "0")}`; +} + +function formatPercentage(value: number): string { + return `${value.toFixed(1)}%`; +} + +interface MetricRow { + label: string; + description?: string; + getValue: (stats: QuizStats) => string; +} + +const METRICS: MetricRow[] = [ + { + label: "Liczba uruchomień", + getValue: (s) => s.sessions_count.toString(), + }, + { + label: "Łączna liczba odpowiedzi", + getValue: (s) => s.total_answers.toString(), + }, + { + label: "Poprawne odpowiedzi", + getValue: (s) => s.correct_answers.toString(), + }, + { + label: "Błędne odpowiedzi", + getValue: (s) => s.wrong_answers.toString(), + }, + { + label: "Średni czas rozwiązywania", + getValue: (s) => formatTime(s.average_study_time_seconds), + }, + { + label: "Łączny czas", + getValue: (s) => formatTime(s.total_study_time_seconds), + }, + { + label: "Średni wynik", + description: "z pierwszych odpowiedzi", + getValue: (s) => formatPercentage(s.first_answer_accuracy), + }, +]; + +function StatCell({ isLoading, value }: { isLoading: boolean; value: string }) { + if (isLoading) { + return ; + } + return {value}; +} + +function MetricLabel({ metric }: { metric: MetricRow }) { + return ( +
+
{metric.label}
+ {metric.description == null ? null : ( +
+ {metric.description} +
+ )} +
+ ); +} + +function UniqueUsersDescription({ + isLoading, + value, +}: { + isLoading: boolean; + value?: number | null; +}) { + if (isLoading) { + return ; + } + return ( + + {value == null + ? null + : `Liczba unikalnych użytkowników: ${value.toString()}`} + + ); +} + +export function StatsTable({ quizId, canViewAll }: StatsTableProps) { + const { data: myStats, isPending: myLoading } = useQuizStats(quizId, "me"); + const { data: allStats, isPending: allLoading } = useQuizStats( + quizId, + "all", + canViewAll, + ); + + return ( + + + Statystyki + {canViewAll ? ( + + + + ) : null} + + + + + + Metryka + {canViewAll ? ( + Wszyscy + ) : null} + Ty + + + + {METRICS.map((metric) => ( + + + + + {canViewAll ? ( + + + + ) : null} + + + + + ))} + +
+
+
+ ); +} diff --git a/src/components/quiz/stats/study-time-chart.tsx b/src/components/quiz/stats/study-time-chart.tsx new file mode 100644 index 00000000..63ba55b9 --- /dev/null +++ b/src/components/quiz/stats/study-time-chart.tsx @@ -0,0 +1,251 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"; + +import type { ChartConfig } from "@/components/ui/chart"; +import { ChartContainer, ChartTooltip } from "@/components/ui/chart"; +import { useQuizSessions, useQuizTimeline } from "@/hooks/use-quiz-stats"; +import type { StatsScope } from "@/types/quiz-stats"; + +import { ChartCard } from "./chart-card"; +import { ChartModeToggle } from "./chart-mode-toggle"; +import type { ChartMode } from "./chart-mode-toggle"; +import { SessionTooltip } from "./session-tooltip"; +import { TimelineTooltip } from "./timeline-tooltip"; + +interface StudyTimeChartProps { + quizId: string; + canViewAll: boolean; +} + +const chartConfig = { + avgTime: { + label: "Średni czas", + color: "var(--chart-3)", + }, +} satisfies ChartConfig; + +const TIMELINE_DAYS = 30; + +function formatMinutes(minutes: number): string { + const m = Math.floor(minutes); + const s = Math.round((minutes - m) * 60); + return `${m.toString()}:${s.toString().padStart(2, "0")}`; +} + +function formatDateKey(date: Date): string { + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + return `${year.toString()}-${month}-${day}`; +} + +function parseDateKey(dateKey: string): Date { + const [year, month, day] = dateKey.split("-").map(Number); + return new Date(year, month - 1, day); +} + +function getDaysThroughToday(startDateKey: string): string[] { + const start = parseDateKey(startDateKey); + const end = new Date(); + start.setHours(0, 0, 0, 0); + end.setHours(0, 0, 0, 0); + + if (start > end) { + return [formatDateKey(start)]; + } + + const days = + Math.round((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1; + + return Array.from({ length: days }, (_, index) => { + const date = new Date(start); + date.setDate(start.getDate() + index); + return formatDateKey(date); + }); +} + +export function StudyTimeChart({ quizId, canViewAll }: StudyTimeChartProps) { + const [scope, setScope] = useState("me"); + const [mode, setMode] = useState("sessions"); + const canUseSessions = scope === "me"; + const { + data: raw, + isFetching: isTimelineFetching, + isPending: isTimelinePending, + } = useQuizTimeline(quizId, scope, TIMELINE_DAYS); + const { + data: sessionsRaw, + isFetching: isSessionsFetching, + isPending: isSessionsPending, + } = useQuizSessions( + quizId, + "me", + TIMELINE_DAYS, + mode === "sessions" && canUseSessions, + ); + + const effectiveMode = canUseSessions ? mode : "timeline"; + const isPending = + effectiveMode === "sessions" ? isSessionsPending : isTimelinePending; + const isRefreshing = + effectiveMode === "sessions" + ? isSessionsFetching && !isSessionsPending + : isTimelineFetching && !isTimelinePending; + + const timelineData = useMemo(() => { + const entriesByDate = new Map(raw?.map((entry) => [entry.date, entry])); + const firstNonZeroEntry = raw?.find( + (entry) => entry.sessions_count > 0 && entry.total_study_time_seconds > 0, + ); + + if (firstNonZeroEntry === undefined) { + return []; + } + + return getDaysThroughToday(firstNonZeroEntry.date).map((date, index) => { + const entry = entriesByDate.get(date); + const avgTime = + entry !== undefined && entry.sessions_count > 0 + ? Number( + ( + entry.total_study_time_seconds / + entry.sessions_count / + 60 + ).toFixed(2), + ) + : null; + + return { + date, + sessionIndex: index + 1, + sessions_count: entry?.sessions_count ?? 0, + total_answers: entry?.total_answers ?? 0, + correct_answers: entry?.correct_answers ?? 0, + total_study_time_seconds: entry?.total_study_time_seconds ?? 0, + avgTime, + sessionTimeMinutes: avgTime, + }; + }); + }, [raw]); + + const sessionData = useMemo( + () => + sessionsRaw?.map((entry, index) => ({ + ...entry, + date: entry.started_at, + sessionIndex: index + 1, + sessions_count: 1, + total_study_time_seconds: entry.study_time_seconds, + avgTime: null, + sessionTimeMinutes: Number((entry.study_time_seconds / 60).toFixed(2)), + })) ?? [], + [sessionsRaw], + ); + + const data = effectiveMode === "sessions" ? sessionData : timelineData; + const hasData = + effectiveMode === "sessions" + ? sessionData.some((d) => d.study_time_seconds > 0) + : timelineData.some((d) => d.avgTime !== null); + const handleScopeChange = (nextScope: StatsScope) => { + setScope(nextScope); + + if (nextScope === "all") { + setMode("timeline"); + } + }; + + return ( + + } + > + + + + {effectiveMode === "sessions" ? ( + `#${value.toString()}`} + interval="preserveStartEnd" + minTickGap={24} + /> + ) : ( + { + const date = new Date(value); + return `${date.getDate().toString().padStart(2, "0")}.${(date.getMonth() + 1).toString().padStart(2, "0")}`; + }} + interval="preserveStartEnd" + minTickGap={40} + /> + )} + formatMinutes(v)} + width={45} + /> + formatMinutes(Number(value))} + /> + ) : ( + formatMinutes(Number(value))} + /> + ) + } + /> + + effectiveMode === "sessions" + ? "sessionTimeMinutes" in d + : d.avgTime !== null, + ).length <= 15 + } + /> + + + + ); +} diff --git a/src/components/quiz/stats/timeline-tooltip.tsx b/src/components/quiz/stats/timeline-tooltip.tsx new file mode 100644 index 00000000..6d4f7316 --- /dev/null +++ b/src/components/quiz/stats/timeline-tooltip.tsx @@ -0,0 +1,141 @@ +"use client"; + +import type { TooltipContentProps, TooltipValueType } from "recharts"; + +import { cn } from "@/lib/utils"; +import type { TimelineEntry } from "@/types/quiz-stats"; + +type TimelineTooltipData = Partial & Record; + +interface TimelineTooltipProps extends Partial< + TooltipContentProps +> { + primaryDataKey?: string; + primaryLabel?: string; + primaryFormatter?: (value: TooltipValueType) => string; +} + +function formatDate(dateKey: string): string { + const [year, month, day] = dateKey.split("-").map(Number); + const date = new Date(year, month - 1, day); + + return date.toLocaleDateString("pl-PL", { + day: "numeric", + month: "long", + }); +} + +function formatNumber(value = 0): string { + return value.toLocaleString("pl-PL"); +} + +function formatTime(seconds = 0): string { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + + if (h > 0) { + return `${h.toString()}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; + } + + return `${m.toString()}:${s.toString().padStart(2, "0")}`; +} + +function formatAccuracy( + correct: number | undefined, + total: number | undefined, +) { + if (total === undefined || total === 0 || correct === undefined) { + return "0,0%"; + } + + return `${((correct / total) * 100).toLocaleString("pl-PL", { + maximumFractionDigits: 1, + minimumFractionDigits: 1, + })}%`; +} + +function TooltipRow({ + label, + value, +}: { + label: string; + value: string; +}): React.JSX.Element { + return ( +
+ {label} + + {value} + +
+ ); +} + +export function TimelineTooltip({ + active, + payload = [], + label, + primaryDataKey, + primaryLabel, + primaryFormatter, +}: TimelineTooltipProps) { + if (active !== true || payload.length === 0) { + return null; + } + + const primaryItem = + primaryDataKey === undefined + ? payload[0] + : (payload.find((item) => item.dataKey === primaryDataKey) ?? payload[0]); + const entry = primaryItem.payload as TimelineTooltipData | undefined; + + if (entry === undefined) { + return null; + } + + const date = + typeof entry.date === "string" + ? entry.date + : typeof label === "string" + ? label + : ""; + const totalAnswers = entry.total_answers; + const correctAnswers = entry.correct_answers; + + return ( +
+
+ {date === "" ? label : formatDate(date)} +
+ {primaryLabel !== undefined && + primaryItem.value !== undefined && + primaryFormatter !== undefined ? ( +
+ +
+ ) : null} +
+ + + + + +
+
+ ); +} diff --git a/src/components/report-bug-dialog.tsx b/src/components/report-bug-dialog.tsx index dc0fe09c..a9b3e2cd 100644 --- a/src/components/report-bug-dialog.tsx +++ b/src/components/report-bug-dialog.tsx @@ -1,9 +1,11 @@ +import { useQuery } from "@tanstack/react-query"; import { MessageSquareWarningIcon } from "lucide-react"; import { usePathname } from "next/navigation"; import { useContext, useState } from "react"; import { toast } from "sonner"; import { AppContext } from "@/app-context"; +import { quizDetailQueryKey } from "@/components/quiz/helpers/utils"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -27,7 +29,8 @@ import { SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; -import { getUserService } from "@/services"; +import { getQuizService, getUserService } from "@/services"; +import type { QuizWithUserProgress } from "@/types/quiz"; interface ReportBugDialogProps { open: boolean; @@ -51,7 +54,21 @@ export function ReportBugDialog({ open, onOpenChange }: ReportBugDialogProps) { const [formErrors, setFormErrors] = useState>({}); const [isSending, setIsSending] = useState(false); - const quizId = pathname.includes("quiz/") ? pathname.split("/").pop() : null; + const quizId = /^\/quiz\/([^/]+)/.exec(pathname)?.[1] ?? null; + const { data: currentQuiz } = useQuery({ + queryKey: quizDetailQueryKey(quizId ?? ""), + queryFn: async () => { + if (quizId == null) { + throw new Error("Cannot load quiz without quiz ID."); + } + return getQuizService().getQuizWithProgress(quizId); + }, + enabled: quizId != null, + staleTime: Infinity, + refetchOnWindowFocus: false, + }); + const canEditCurrentQuiz = + currentQuiz != null && (currentQuiz.can_edit ?? false); const validateForm = () => { const errors: Record = {}; @@ -148,11 +165,11 @@ export function ReportBugDialog({ open, onOpenChange }: ReportBugDialogProps) { Zgłoszenie błędu lub sugestia - Opisz problem lub propozycję ulepszenia + Opisz problem lub propozycję ulepszenia Testownika
- {quizId != null && ( + {quizId != null && !canEditCurrentQuiz && ( & VariantProps) { + return ( + + {children} + + ); +} + // eslint-disable-next-line react-refresh/only-export-components -export { Button, buttonVariants }; +export { Button, ButtonLink, buttonVariants }; diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx new file mode 100644 index 00000000..ade64623 --- /dev/null +++ b/src/components/ui/chart.tsx @@ -0,0 +1,402 @@ +"use client"; + +import * as React from "react"; +import type { + LegendPayload, + TooltipPayloadEntry, + TooltipValueType, +} from "recharts"; +import * as RechartsPrimitive from "recharts"; + +import { cn } from "@/lib/utils"; + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const; + +const INITIAL_DIMENSION = { width: 320, height: 200 } as const; +type TooltipNameType = number | string; + +export type ChartConfig = Record< + string, + { + label?: React.ReactNode; + icon?: React.ComponentType; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +>; + +interface ChartContextProps { + config: ChartConfig; +} + +const ChartContext = React.createContext(null); + +function useChart() { + const context = React.useContext(ChartContext); + + if (context === null) { + throw new Error("useChart must be used within a "); + } + + return context; +} + +function ChartContainer({ + id, + className, + children, + config, + initialDimension = INITIAL_DIMENSION, + ...props +}: React.ComponentProps<"div"> & { + config: ChartConfig; + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"]; + initialDimension?: { + width: number; + height: number; + }; +}) { + const uniqueId = React.useId(); + const chartId = `chart-${id ?? uniqueId.replaceAll(":", "")}`; + + return ( + +
+ + + {children} + +
+
+ ); +} + +function ChartStyle({ id, config }: { id: string; config: ChartConfig }) { + const colorConfig = Object.entries(config).filter( + ([, itemConfig]) => + itemConfig.theme !== undefined || + (itemConfig.color !== undefined && itemConfig.color !== ""), + ); + + if (colorConfig.length === 0) { + return null; + } + + return ( +