diff --git a/package.json b/package.json index ffe33c3..22069a8 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@types/node": "^24.6.1", "@ungap/with-resolvers": "^0.1.0", "@upstash/ratelimit": "^2.0.5", + "@upstash/redis": "^1.33.0", "@vercel/kv": "^3.0.0", "axios": "^1.8.4", "canvas": "^3.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 338579c..240913b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,7 +67,10 @@ importers: version: 0.1.0 '@upstash/ratelimit': specifier: ^2.0.5 - version: 2.0.6(@upstash/redis@1.35.4) + version: 2.0.7(@upstash/redis@1.33.0) + '@upstash/redis': + specifier: ^1.33.0 + version: 1.33.0 '@vercel/kv': specifier: ^3.0.0 version: 3.0.0 @@ -1045,6 +1048,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + '@types/node@24.6.1': resolution: {integrity: sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw==} @@ -1241,13 +1247,16 @@ packages: resolution: {integrity: sha512-7qJHGxpQgQr9/vmeS1PktEwvNAF7TI4iJDi8Pu2CFZ9YUGHZH4fOP5TfYlZ4aVxfopnELiE4BS4FBjyK7V1/xQ==} engines: {node: '>=16.0.0'} - '@upstash/ratelimit@2.0.6': - resolution: {integrity: sha512-Uak5qklMfzFN5RXltxY6IXRENu+Hgmo9iEgMPOlUs2etSQas2N+hJfbHw37OUy4vldLRXeD0OzL+YRvO2l5acg==} + '@upstash/ratelimit@2.0.7': + resolution: {integrity: sha512-qNQW4uBPKVk8c4wFGj2S/vfKKQxXx1taSJoSGBN36FeiVBBKHQgsjPbKUijZ9Xu5FyVK+pfiXWKIsQGyoje8Fw==} peerDependencies: '@upstash/redis': ^1.34.3 - '@upstash/redis@1.35.4': - resolution: {integrity: sha512-WE1ZnhFyBiIjTDW13GbO6JjkiMVVjw5VsvS8ENmvvJsze/caMQ5paxVD44+U68IUVmkXcbsLSoE+VIYsHtbQEw==} + '@upstash/redis@1.33.0': + resolution: {integrity: sha512-5WOilc7AE0ITAdE3NCyMwgOq1n3RHcqW0OfmbotiAyfA+QAEe1R7kXin8L/Yladgdc5lkA0GcYyewqKfAw53jQ==} + + '@upstash/redis@1.35.7': + resolution: {integrity: sha512-bdCdKhke+kYUjcLLuGWSeQw7OLuWIx3eyKksyToLBAlGIMX9qiII0ptp8E0y7VFE1yuBxBd/3kSzJ8774Q4g+A==} '@vercel/kv@3.0.0': resolution: {integrity: sha512-pKT8fRnfyYk2MgvyB6fn6ipJPCdfZwiKDdw7vB+HL50rjboEBHDVBEcnwfkEpVSp2AjNtoaOUH7zG+bVC/rvSg==} @@ -1467,8 +1476,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.8.10: - resolution: {integrity: sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==} + baseline-browser-mapping@2.8.31: + resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==} hasBin: true big.js@5.2.2: @@ -1494,8 +1503,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.26.3: - resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} + browserslist@4.28.0: + resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1543,6 +1552,9 @@ packages: caniuse-lite@1.0.30001746: resolution: {integrity: sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==} + caniuse-lite@1.0.30001757: + resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==} + canvas@3.2.0: resolution: {integrity: sha512-jk0GxrLtUEmW/TmFsk2WghvgHe8B0pxGilqCL21y8lHkPUGa6FTsnCNtHPOzT8O3y+N+m3espawV80bbBlgfTA==} engines: {node: ^18.12.0 || >= 20.9.0} @@ -1609,6 +1621,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + css-box-model@1.2.1: resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} @@ -1728,8 +1743,8 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - electron-to-chromium@1.5.228: - resolution: {integrity: sha512-nxkiyuqAn4MJ1QbobwqJILiDtu/jk14hEAWaMiJmNPh1Z+jqoFlBFZjdXwLWGeVSeu9hGLg6+2G9yJaW8rBIFA==} + electron-to-chromium@1.5.262: + resolution: {integrity: sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==} embla-carousel-autoplay@8.6.0: resolution: {integrity: sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==} @@ -2497,8 +2512,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - loader-runner@4.3.0: - resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + loader-runner@4.3.1: + resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} loader-utils@2.0.4: @@ -2722,8 +2737,8 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-releases@2.0.21: - resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -3235,8 +3250,8 @@ packages: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} - schema-utils@4.3.2: - resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==} + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} semver@6.3.1: @@ -3446,8 +3461,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tapable@2.2.3: - resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} tar-fs@2.1.4: @@ -3477,8 +3492,8 @@ packages: uglify-js: optional: true - terser@5.44.0: - resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} + terser@5.44.1: + resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} engines: {node: '>=10'} hasBin: true @@ -3574,11 +3589,14 @@ packages: undici-types@7.13.0: resolution: {integrity: sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + update-browserslist-db@1.1.4: + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -4580,6 +4598,10 @@ snapshots: '@types/json5@0.0.29': {} + '@types/node@24.10.1': + dependencies: + undici-types: 7.16.0 + '@types/node@24.6.1': dependencies: undici-types: 7.13.0 @@ -4779,20 +4801,24 @@ snapshots: '@upstash/core-analytics@0.0.10': dependencies: - '@upstash/redis': 1.35.4 + '@upstash/redis': 1.35.7 - '@upstash/ratelimit@2.0.6(@upstash/redis@1.35.4)': + '@upstash/ratelimit@2.0.7(@upstash/redis@1.33.0)': dependencies: '@upstash/core-analytics': 0.0.10 - '@upstash/redis': 1.35.4 + '@upstash/redis': 1.33.0 - '@upstash/redis@1.35.4': + '@upstash/redis@1.33.0': + dependencies: + crypto-js: 4.2.0 + + '@upstash/redis@1.35.7': dependencies: uncrypto: 0.1.3 '@vercel/kv@3.0.0': dependencies: - '@upstash/redis': 1.35.4 + '@upstash/redis': 1.35.7 '@webassemblyjs/ast@1.14.1': dependencies: @@ -5051,7 +5077,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.8.10: {} + baseline-browser-mapping@2.8.31: {} big.js@5.2.2: {} @@ -5078,13 +5104,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.26.3: + browserslist@4.28.0: dependencies: - baseline-browser-mapping: 2.8.10 - caniuse-lite: 1.0.30001746 - electron-to-chromium: 1.5.228 - node-releases: 2.0.21 - update-browserslist-db: 1.1.3(browserslist@4.26.3) + baseline-browser-mapping: 2.8.31 + caniuse-lite: 1.0.30001757 + electron-to-chromium: 1.5.262 + node-releases: 2.0.27 + update-browserslist-db: 1.1.4(browserslist@4.28.0) bson@6.10.4: {} @@ -5126,6 +5152,8 @@ snapshots: caniuse-lite@1.0.30001746: {} + caniuse-lite@1.0.30001757: {} + canvas@3.2.0: dependencies: node-addon-api: 7.1.1 @@ -5200,6 +5228,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-js@4.2.0: {} + css-box-model@1.2.1: dependencies: tiny-invariant: 1.3.3 @@ -5303,7 +5333,7 @@ snapshots: dependencies: safe-buffer: 5.2.1 - electron-to-chromium@1.5.228: {} + electron-to-chromium@1.5.262: {} embla-carousel-autoplay@8.6.0(embla-carousel@8.6.0): dependencies: @@ -5334,7 +5364,7 @@ snapshots: enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 - tapable: 2.2.3 + tapable: 2.3.0 es-abstract@1.24.0: dependencies: @@ -5451,8 +5481,8 @@ snapshots: '@typescript-eslint/parser': 8.45.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) @@ -5471,7 +5501,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -5482,22 +5512,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.45.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -5508,7 +5538,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.45.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -6195,7 +6225,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 24.6.1 + '@types/node': 24.10.1 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -6279,7 +6309,7 @@ snapshots: lines-and-columns@1.2.4: {} - loader-runner@4.3.0: {} + loader-runner@4.3.1: {} loader-utils@2.0.4: dependencies: @@ -6465,7 +6495,7 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - node-releases@2.0.21: {} + node-releases@2.0.27: {} normalize-path@3.0.0: {} @@ -6953,7 +6983,7 @@ snapshots: ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) - schema-utils@4.3.2: + schema-utils@4.3.3: dependencies: '@types/json-schema': 7.0.15 ajv: 8.17.1 @@ -7217,7 +7247,7 @@ snapshots: - tsx - yaml - tapable@2.2.3: {} + tapable@2.3.0: {} tar-fs@2.1.4: dependencies: @@ -7249,12 +7279,12 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 - terser: 5.44.0 + terser: 5.44.1 webpack: 5.102.0 - terser@5.44.0: + terser@5.44.1: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.15.0 @@ -7363,6 +7393,8 @@ snapshots: undici-types@7.13.0: {} + undici-types@7.16.0: {} + unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.3 @@ -7387,9 +7419,9 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - update-browserslist-db@1.1.3(browserslist@4.26.3): + update-browserslist-db@1.1.4(browserslist@4.28.0): dependencies: - browserslist: 4.26.3 + browserslist: 4.28.0 escalade: 3.2.0 picocolors: 1.1.1 @@ -7451,7 +7483,7 @@ snapshots: '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.26.3 + browserslist: 4.28.0 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.3 es-module-lexer: 1.7.0 @@ -7460,11 +7492,11 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 + loader-runner: 4.3.1 mime-types: 2.1.35 neo-async: 2.6.2 - schema-utils: 4.3.2 - tapable: 2.2.3 + schema-utils: 4.3.3 + tapable: 2.3.0 terser-webpack-plugin: 5.3.14(webpack@5.102.0) watchpack: 2.4.4 webpack-sources: 3.3.3 diff --git a/src/app/api/report-tag/route.ts b/src/app/api/report-tag/route.ts new file mode 100644 index 0000000..41b9929 --- /dev/null +++ b/src/app/api/report-tag/route.ts @@ -0,0 +1,95 @@ +import { NextResponse } from "next/server"; +import { connectToDatabase } from "@/lib/database/mongoose"; +import TagReport from "@/db/tagReport"; +import { Ratelimit } from "@upstash/ratelimit"; +import { redis } from "@/lib/utils/redis"; + +const ALLOWED_EXAMS = ["CAT-1", "CAT-2", "FAT"]; +const ALLOWED_FIELDS = ["subject", "courseCode", "exam", "slot", "year"]; + +const ratelimit = new Ratelimit({ + redis, + limiter: Ratelimit.slidingWindow(3, "1 h"),//per id - 3 request - per hour + analytics: true, +}); + +function getClientIp(req: any): string { + return req.ip || "127.0.0.1"; +} + +export async function POST(req: Request & { ip?: string }) { + try { + await connectToDatabase(); + + const body = await req.json(); + const { paperId } = body; + + if (!paperId) { + return NextResponse.json( + { error: "paperId is required" }, + { status: 400 } + ); + } + const ip = getClientIp(req); + const key = `${ip}::${paperId}`; + const { success } = await ratelimit.limit(key); + + if (!success) { + return NextResponse.json( + { error: "Rate limit exceeded for reporting." }, + { status: 429 } + ); + } + const MAX_REPORTS_PER_PAPER = 5; + const count = await TagReport.countDocuments({ paperId }); + + if (count >= MAX_REPORTS_PER_PAPER) { + return NextResponse.json( + { error: "Received many reports; we are currently working on it." }, + { status: 429 } + ); + } + const reportedFields = (body.reportedFields ?? []) + .map((r: any) => ({ + field: String(r.field).trim(), + value: r.value?.trim(), + })) + .filter((r: any) => r.field); + + for (const rf of reportedFields) { + if (!ALLOWED_FIELDS.includes(rf.field)) { + return NextResponse.json( + { error: `Invalid field: ${rf.field}` }, + { status: 400 } + ); + } + if (rf.field === "exam" && rf.value) { + if (!ALLOWED_EXAMS.includes(rf.value)) { + return NextResponse.json( + { error: `Invalid exam value: ${rf.value}` }, + { status: 400 } + ); + } + } + } + + const newReport = await TagReport.create({ + paperId, + reportedFields, + comment: body.comment, + reporterEmail: body.reporterEmail, + reporterId: body.reporterId, + }); + + return NextResponse.json( + { message: "Report submitted.", report: newReport }, + { status: 201 } + ); + } catch (err) { + console.error(err); + return NextResponse.json( + { error: "Failed to submit tag report." }, + { status: 500 } + ); + } +} diff --git a/src/app/paper/[id]/page.tsx b/src/app/paper/[id]/page.tsx index c94af26..e353df0 100644 --- a/src/app/paper/[id]/page.tsx +++ b/src/app/paper/[id]/page.tsx @@ -167,6 +167,11 @@ const PaperPage = async ({ params }: { params: { id: string } }) => { diff --git a/src/components/ReportButton.tsx b/src/components/ReportButton.tsx new file mode 100644 index 0000000..df3bd5b --- /dev/null +++ b/src/components/ReportButton.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { useState } from "react"; +import { FaFlag } from "react-icons/fa6"; +import { Button } from "./ui/button"; +import ReportTagModal from "./ReportTagModal"; + +export default function ReportButton({ + paperId, subject, exam, slot, year +}: { + paperId: string; + subject?: string; + exam?: string; + slot?: string; + year?: string; +}) { + const [open, setOpen] = useState(false); + + return ( + <> + + + + + ); +} diff --git a/src/components/ReportTagModal.tsx b/src/components/ReportTagModal.tsx new file mode 100644 index 0000000..d90e241 --- /dev/null +++ b/src/components/ReportTagModal.tsx @@ -0,0 +1,432 @@ +"use client"; + +import React, { useState, useEffect, useMemo } from "react"; +import { FaFlag } from "react-icons/fa"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { MultiSelect } from "@/components/multi-select"; +import { + Select, + SelectTrigger, + SelectContent, + SelectValue, + SelectItem, +} from "@/components/ui/select"; +import axios from "axios"; +import toast from "react-hot-toast"; + +interface ReportTagModalProps { + paperId: string; + subject?: string; + exam?: string; + slot?: string; + year?: string; + toolbarStyle?: boolean; + open?: boolean; + setOpen?: (v: boolean) => void; +} + +const ReportTagModal = ({ + paperId, + subject, + exam, + slot, + year, + toolbarStyle = false, + open, + setOpen, +}: ReportTagModalProps) => { + const [internalOpen, setInternalOpen] = useState(false); + const isControlled = open !== undefined && setOpen !== undefined; + + const modalOpen = isControlled ? open! : internalOpen; + const modalSetOpen = isControlled ? setOpen! : setInternalOpen; + const [comment, setComment] = useState(""); + const [email, setEmail] = useState(""); + const [selectedCategories, setSelectedCategories] = useState([]); + const [categoryValues, setCategoryValues] = useState>( + {}, + ); + const [originalCategoryValues, setOriginalCategoryValues] = useState< + Record + >({}); + const [originalEmail, setOriginalEmail] = useState(""); + const [loading, setLoading] = useState(false); + + const SCROLL_THRESHOLD = 4; + const contentClass = `bg-[#F3F5FF] dark:bg-[#070114] border-[#3A3745] items-start sm:max-w-3xl w-full ${ + selectedCategories.length > SCROLL_THRESHOLD + ? "max-h-[70vh] overflow-y-auto" + : "" + }`; + + const isDirty = useMemo(() => { + if (selectedCategories.length === 0) return false; + for (const c of selectedCategories) { + const curr = (categoryValues[c] || "").trim(); + const orig = (originalCategoryValues[c] || "").trim(); + if (curr !== orig) return true; + } + + if (selectedCategories.includes("subject")) { + const currCode = (categoryValues["courseCode"] || "").trim(); + const origCode = (originalCategoryValues["courseCode"] || "").trim(); + if (currCode !== origCode) return true; + } + + return false; + }, [selectedCategories, categoryValues, originalCategoryValues]); + + useEffect(() => { + for (const c of selectedCategories) { + if (categoryValues[c]) continue; + if (c === "subject" && subject) { + const m = subject.match(/^(.*)\s*\[([^\]]+)\]\s*$/); + if (m?.[1] && m?.[2]) { + const name = m[1].trim(); + const code = m[2].trim(); + setCategoryValues((s) => ({ ...s, subject: name, courseCode: code })); + } else { + setCategoryValues((s) => ({ ...s, subject })); + } + } else if (c === "exam" && exam) + setCategoryValues((s) => ({ ...s, [c]: exam })); + else if (c === "slot" && slot) + setCategoryValues((s) => ({ ...s, [c]: slot })); + else if (c === "year" && year) + setCategoryValues((s) => ({ ...s, [c]: year })); + } + }, [selectedCategories, subject, exam, slot, year]); + + useEffect(() => { + if (open) { + const base: Record = {}; + if (subject) { + const m = subject.match(/^(.*)\s*\[([^\]]+)\]\s*$/); + if (m?.[1] && m?.[2]) { + base["subject"] = m[1].trim(); + base["courseCode"] = m[2].trim(); + } else { + base["subject"] = subject; + } + } + if (exam) base["exam"] = exam; + if (slot) base["slot"] = slot; + if (year) base["year"] = year; + setOriginalCategoryValues(base); + setOriginalEmail(""); + } else { + setSelectedCategories([]); + setCategoryValues({}); + setComment(""); + setEmail(""); + setOriginalCategoryValues({}); + setOriginalEmail(""); + } + }, [open, subject, exam, slot, year]); + + const handleSubmit = async () => { + if (!paperId) { + toast.error("Missing paper id."); + return; + } + + if (selectedCategories.includes("subject")) { + const sub = (categoryValues.subject || "").trim(); + if (!sub) { + toast.error("Subject name cannot be empty."); + return; + } +} + + if (selectedCategories.includes("slot")) { + const v = (categoryValues.slot || "").trim(); + const slotRegex = /^[A-G][1-2]$/; + if (!slotRegex.test(v)) { + toast.error("Slot must be from A1 to G2 (e.g., D1, B2)."); + return; + } + } + + if (selectedCategories.includes("year")) { + const y = (categoryValues.year || "").trim(); + const yearRegex = /^\d{4}(-\d{4})?$/; + if (!yearRegex.test(y)) { + toast.error("Year must be a valid format (e.g., 2024 or 2024-2025)."); + return; + } + } + + const reportedFields: { field: string; value?: string }[] = []; + + for (const c of selectedCategories) { + if (c === "subject") { + const newSub = (categoryValues.subject || "").trim(); + const oldSub = (originalCategoryValues.subject || "").trim(); + + if (newSub !== oldSub) { + reportedFields.push({ field: "subject", value: newSub }); + } + + const newCode = (categoryValues.courseCode || "").trim(); + const oldCode = (originalCategoryValues.courseCode || "").trim(); + + if (newCode !== oldCode) { + reportedFields.push({ field: "courseCode", value: newCode }); + } + + continue; + } + + const newVal = (categoryValues[c] || "").trim(); + const oldVal = (originalCategoryValues[c] || "").trim(); + + if (newVal !== oldVal) { + reportedFields.push({ field: c, value: newVal }); + } + } + +if (reportedFields.length === 0 && comment.trim().length === 0) { + toast.error("Please change a tag or write a comment."); + return; +} + + setLoading(true); + + try { + const res = await axios.post("/api/report-tag", { + paperId, + reportedFields, + comment, + reporterEmail: email || undefined, + }); + + toast.success("Reported successfully. Thank you, We will work on that."); + + modalSetOpen(false); + setComment(""); + setEmail(""); + setSelectedCategories([]); + setCategoryValues({}); + } catch (err: any) { + console.error(err); + + const msg = + err?.response?.data?.error || + err?.message || + "Failed to submit report."; + + toast.error(msg); + } finally { + setLoading(false); + } + +}; + + return ( + + {!isControlled && ( + + + + )} + + + +
+ Report Wrong Tags +
+ + Help us improve tagging — suggest correct tags and add an optional + comment. + +
+ +
+
+ + setSelectedCategories(vals)} + defaultValue={[]} + placeholder="Select fields" + /> +
+ + {selectedCategories.length > 0 && ( +
+ +
+ {selectedCategories.map((c) => { + if (c === "subject") { + return ( +
+ + + setCategoryValues((s) => ({ + ...s, + subject: e.target.value, + })) + } + placeholder="Subject name" + className="mb-2 w-full" + /> + + + setCategoryValues((s) => ({ + ...s, + courseCode: e.target.value, + })) + } + placeholder="e.g. BMAT205L" + className="w-full" + /> +
+ ); + } else if (c === "exam") { + return ( +
+ + + + + +
+ ); + } else if (c === "slot") { + return ( +
+ + { + let v = e.target.value.toUpperCase(); + v = v.replace(/[^A-Z0-9]/g, ""); + + setCategoryValues((s) => ({ ...s, slot: v })); + }} + placeholder="e.g. D1" + className="w-full" + /> +
+ ); + } + else if(c==="year"){ + return ( +
+ + + setCategoryValues((s) => ({ ...s, year: e.target.value })) + } + placeholder="e.g. 2024-2025" + className="w-full" + /> +
+ ); + } + return ( +
+ + + setCategoryValues((s) => ({ + ...s, + [c]: e.target.value, + })) + } + className="w-full" + /> +
+ ); + })} +
+
+ )} + +
+ + setComment(e.target.value)} + placeholder="eg: Paper quality is not good" + /> +
+ +
+ + setEmail(e.target.value)} + placeholder="you@example.com" + /> +
+ +
+ +
+
+
+
+ ); +}; + +export default ReportTagModal; diff --git a/src/components/pdfViewer.tsx b/src/components/pdfViewer.tsx index c7a72d9..852ec91 100644 --- a/src/components/pdfViewer.tsx +++ b/src/components/pdfViewer.tsx @@ -1,7 +1,6 @@ "use client"; -import "react-pdf/dist/Page/AnnotationLayer.css"; -import "react-pdf/dist/Page/TextLayer.css"; +import ReportButton from "./ReportButton"; import { useState, useRef, useCallback, useEffect } from "react"; import { Document, Page, pdfjs } from "react-pdf"; import { Download, ZoomIn, ZoomOut, Maximize2, Minimize2 } from "lucide-react"; @@ -9,12 +8,6 @@ import { Button } from "./ui/button"; import { downloadFile } from "../lib/utils/download"; import ShareButton from "./ShareButton"; import Loader from "./ui/loader"; -import { - FaGreaterThan, - FaLessThan, - FaAngleUp, - FaAngleDown, -} from "react-icons/fa6"; pdfjs.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.8.69/pdf.worker.min.mjs"; @@ -22,9 +15,14 @@ pdfjs.GlobalWorkerOptions.workerSrc = interface PdfViewerProps { url: string; name: string; + paperId: string; + subject?: string; + exam?: string; + slot?: string; + year?: string; } -export default function PdfViewer({ url, name }: PdfViewerProps) { +export default function PdfViewer({ url, name, paperId, subject, exam, slot, year }: PdfViewerProps) { const [numPages, setNumPages] = useState(); const [pageNumber, setPageNumber] = useState(1); const [scale, setScale] = useState(1); @@ -204,14 +202,6 @@ export default function PdfViewer({ url, name }: PdfViewerProps) { {!isFullscreen && (
- - of {numPages ?? 1} -
@@ -265,6 +248,13 @@ export default function PdfViewer({ url, name }: PdfViewerProps) { > {isFullscreen ? : } +
)} @@ -302,8 +292,8 @@ export default function PdfViewer({ url, name }: PdfViewerProps) { @@ -314,13 +304,6 @@ export default function PdfViewer({ url, name }: PdfViewerProps) { {isFullscreen && (
- of {numPages ?? 1} -
@@ -425,24 +401,15 @@ export default function PdfViewer({ url, name }: PdfViewerProps) { /> of {numPages ?? 1}
- - - -
- + + )} ); diff --git a/src/db/tagReport.ts b/src/db/tagReport.ts new file mode 100644 index 0000000..efe5692 --- /dev/null +++ b/src/db/tagReport.ts @@ -0,0 +1,40 @@ +import mongoose, { Schema, type Document, type Model } from "mongoose"; + +export interface ITagReport extends Document { + paperId: string; + comment?: string; + reporterEmail?: string; + reporterId?: string; + reportedFields?: { field: string; value?: string }[]; + resolved: boolean; + createdAt: Date; +} + +const tagReportSchema = new Schema({ + paperId: { type: String, required: true }, + comment: { type: String }, + reporterEmail: { type: String }, + reporterId: { type: String }, + reportedFields: { + type: [ + new Schema( + { + field: { type: String, required: true }, + value: { type: String }, + }, + { _id: false }, + ), + ], + default: [], + }, + resolved: { type: Boolean, default: false }, + createdAt: { type: Date, default: Date.now }, +}); + +tagReportSchema.index({ paperId: 1, resolved: 1, createdAt: -1 }); + +const TagReport: Model = + mongoose.models.TagReport ?? + mongoose.model("TagReport", tagReportSchema); + +export default TagReport; diff --git a/src/lib/utils/redis.ts b/src/lib/utils/redis.ts new file mode 100644 index 0000000..284c9d0 --- /dev/null +++ b/src/lib/utils/redis.ts @@ -0,0 +1,6 @@ +import { Redis } from "@upstash/redis"; + +export const redis = new Redis({ + url: process.env.UPSTASH_REDIS_REST_URL!, + token: process.env.UPSTASH_REDIS_REST_TOKEN!, +});