diff --git a/bun.lock b/bun.lock index 41f8fc1..9fa629e 100644 --- a/bun.lock +++ b/bun.lock @@ -14,11 +14,9 @@ "elysia": "^1.4.22", "elysia-rate-limit": "^4.5.0", "jsonwebtoken": "^9.0.3", - "pdfmake": "^0.3.3", "pg": "^8.18.0", "pino": "^10.3.0", "pino-pretty": "^13.1.3", - "playwright": "^1.58.2", "resend": "6.4.2", "typeorm": "^0.3.28", }, @@ -28,8 +26,9 @@ "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.2", "@faker-js/faker": "^10.2.0", + "@types/autocannon": "^7.12.7", "@types/jsonwebtoken": "^9.0.10", - "@types/pdfmake": "^0.3.0", + "autocannon": "^8.0.0", "bun-types": "^1.3.8", "eslint": "^9.39.2", "globals": "^16.5.0", @@ -43,12 +42,16 @@ "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@assemblyscript/loader": ["@assemblyscript/loader@0.19.23", "", {}, "sha512-ulkCYfFbYj01ie1MDOyxv2F6SpRN1TOj7fQxbP07D6HmeR+gr2JLSmINKjga2emB+b1L2KGrFKBTc+e00p54nw=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], + "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], + "@commitlint/cli": ["@commitlint/cli@20.4.1", "", { "dependencies": { "@commitlint/format": "^20.4.0", "@commitlint/lint": "^20.4.1", "@commitlint/load": "^20.4.0", "@commitlint/read": "^20.4.0", "@commitlint/types": "^20.4.0", "tinyexec": "^1.0.0", "yargs": "^17.0.0" }, "bin": { "commitlint": "./cli.js" } }, "sha512-uuFKKpc7OtQM+6SRqT+a4kV818o1pS+uvv/gsRhyX7g4x495jg+Q7P0+O9VNGyLXBYP0syksS7gMRDJKcekr6A=="], "@commitlint/config-conventional": ["@commitlint/config-conventional@20.4.1", "", { "dependencies": { "@commitlint/types": "^20.4.0", "conventional-changelog-conventionalcommits": "^9.1.0" } }, "sha512-0YUvIeBtpi86XriqrR+TCULVFiyYTIOEPjK7tTRMxjcBm1qlzb+kz7IF2WxL6Fq5DaundG8VO37BNgMkMTBwqA=="], @@ -107,7 +110,7 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - "@faker-js/faker": ["@faker-js/faker@10.2.0", "", {}, "sha512-rTXwAsIxpCqzUnZvrxVh3L0QA0NzToqWBLAhV+zDV3MIIwiQhAZHMdPCIaj5n/yADu/tyk12wIPgL6YHGXJP+g=="], + "@faker-js/faker": ["@faker-js/faker@10.3.0", "", {}, "sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -119,6 +122,8 @@ "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@minimistjs/subarg": ["@minimistjs/subarg@1.0.0", "", { "dependencies": { "minimist": "^1.1.0" } }, "sha512-Q/ONBiM2zNeYUy0mVSO44mWWKYM3UHuEK43PKIOzJCbvUnPoMH1K+gk3cf1kgnCVJFlWmddahQQCmrmBGlk9jQ=="], + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], @@ -141,12 +146,12 @@ "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], - "@swc/helpers": ["@swc/helpers@0.5.18", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ=="], - "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@types/autocannon": ["@types/autocannon@7.12.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-Pd4nPf7wRpacULa6D/EC9x3CwzFQXwA0z5WFuik/fvJjW44V3WzBTM3jtt8nSBoflUNgswPiMCtgrr1bwnAcMg=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], @@ -155,11 +160,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], - - "@types/pdfkit": ["@types/pdfkit@0.17.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-odAmVuuguRxKh1X4pbMrJMp8ecwNqHRw6lweupvzK+wuyNmi6wzlUlGVZ9EqMvp3Bs2+L9Ty0sRlrvKL+gsQZg=="], - - "@types/pdfmake": ["@types/pdfmake@0.3.0", "", { "dependencies": { "@types/node": "*", "@types/pdfkit": "*" } }, "sha512-WjkNTseNkoT7Rpg3bfjV1tM5k4BzgmNX7WJwodw1T02KyKSyf4/vCy/2nThnUcsKglYu8blFXmVTXtht39E5YA=="], + "@types/node": ["@types/node@25.2.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.54.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/type-utils": "8.54.0", "@typescript-eslint/utils": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ=="], @@ -189,7 +190,7 @@ "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - "ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="], + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -207,13 +208,15 @@ "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + "autocannon": ["autocannon@8.0.0", "", { "dependencies": { "@minimistjs/subarg": "^1.0.0", "chalk": "^4.1.0", "char-spinner": "^1.0.1", "cli-table3": "^0.6.0", "color-support": "^1.1.1", "cross-argv": "^2.0.0", "form-data": "^4.0.0", "has-async-hooks": "^1.0.0", "hdr-histogram-js": "^3.0.0", "hdr-histogram-percentiles-obj": "^3.0.0", "http-parser-js": "^0.5.2", "hyperid": "^3.0.0", "lodash.chunk": "^4.2.0", "lodash.clonedeep": "^4.5.0", "lodash.flatten": "^4.4.0", "manage-path": "^2.0.0", "on-net-listen": "^1.1.1", "pretty-bytes": "^5.4.1", "progress": "^2.0.3", "reinterval": "^1.1.0", "retimer": "^3.0.0", "semver": "^7.3.2", "timestring": "^6.0.0" }, "bin": { "autocannon": "autocannon.js" } }, "sha512-fMMcWc2JPFcUaqHeR6+PbmEpTxCrPZyBUM95oG4w3ngJ8NfBNas/ZXA+pTHXLqJ0UlFVTcy05GC25WxKx/M20A=="], + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "axios": ["axios@1.13.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg=="], + "axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "base64-js": ["base64-js@0.0.8", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="], @@ -221,13 +224,11 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="], - "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], @@ -239,18 +240,22 @@ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "char-spinner": ["char-spinner@1.0.1", "", {}, "sha512-acv43vqJ0+N0rD+Uw3pDHSxP30FHrywu2NO6/wBaHChJIizpDeBUd6NjqhNhy9LGaEAhZAXn46QzmlAvIWd16g=="], + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + "cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="], + "cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="], "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], @@ -273,9 +278,9 @@ "cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.2.0", "", { "dependencies": { "jiti": "^2.6.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "cross-argv": ["cross-argv@2.0.0", "", {}, "sha512-YIaY9TR5Nxeb8SMdtrU8asWVM4jqJDNDYlKV21LxtYcfNJhp1kEsgSa6qXwXgzN0WQWGODps0+TlGp2xQSHwOg=="], - "crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "dargs": ["dargs@8.1.0", "", {}, "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw=="], @@ -295,11 +300,9 @@ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - "dfa": ["dfa@1.2.0", "", {}, "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="], - "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], - "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], + "dotenv": ["dotenv@17.2.4", "", {}, "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -307,7 +310,7 @@ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], - "elysia": ["elysia@1.4.22", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.6", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Q90VCb1RVFxnFaRV0FDoSylESQQLWgLHFmWciQJdX9h3b2cSasji9KWEUvaJuy/L9ciAGg4RAhUVfsXHg5K2RQ=="], + "elysia": ["elysia@1.4.23", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-mFIT/hEnNfrfbjGRUqunLNcURJfSXpKY4j+EWr4vP6Eoulf7feqs0WQLZwlgFZCxhdyfu0mrypIZ4mNJcEVVlQ=="], "elysia-rate-limit": ["elysia-rate-limit@4.5.0", "", { "dependencies": { "@alloc/quick-lru": "5.2.0", "debug": "4.3.4" }, "peerDependencies": { "elysia": ">= 1.0.0" } }, "sha512-nsbl3WLvrGiG/SdTgevAsjCUJhY34Bgf+7bDOYrjTPZyS7Hd4MLuLc4MUr9TFsSJWPvKpzyrX2HW8IWjRbex1Q=="], @@ -353,7 +356,7 @@ "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], - "exact-mirror": ["exact-mirror@0.2.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-7s059UIx9/tnOKSySzUk5cPGkoILhTE4p6ncf6uIPaQ+9aRBQzQjc9+q85l51+oZ+P6aBxh084pD0CzBQPcFUA=="], + "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], "fast-copy": ["fast-copy@4.0.2", "", {}, "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw=="], @@ -387,16 +390,12 @@ "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], - "fontkit": ["fontkit@2.0.4", "", { "dependencies": { "@swc/helpers": "^0.5.12", "brotli": "^1.3.2", "clone": "^2.1.2", "dfa": "^1.2.0", "fast-deep-equal": "^3.1.3", "restructure": "^3.0.0", "tiny-inflate": "^1.0.3", "unicode-properties": "^1.4.0", "unicode-trie": "^2.0.0" } }, "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g=="], - "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], - "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], @@ -419,6 +418,8 @@ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "has-async-hooks": ["has-async-hooks@1.0.0", "", {}, "sha512-YF0VPGjkxr7AyyQQNykX8zK4PvtEDsUJAPqwu06UFz1lb6EvI53sPh5H1kWxg8NXI5LsfRCZ8uX9NkYDZBb/mw=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], @@ -429,12 +430,20 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hdr-histogram-js": ["hdr-histogram-js@3.0.1", "", { "dependencies": { "@assemblyscript/loader": "^0.19.21", "base64-js": "^1.2.0", "pako": "^1.0.3" } }, "sha512-l3GSdZL1Jr1C0kyb461tUjEdrRPZr8Qry7jByltf5JGrA0xvqOSrxRBfcrJqqV/AMEtqqhHhC6w8HW0gn76tRQ=="], + + "hdr-histogram-percentiles-obj": ["hdr-histogram-percentiles-obj@3.0.0", "", {}, "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw=="], + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + "http-parser-js": ["http-parser-js@0.5.10", "", {}, "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + "hyperid": ["hyperid@3.3.0", "", { "dependencies": { "buffer": "^5.2.1", "uuid": "^8.3.2", "uuid-parse": "^1.1.0" } }, "sha512-7qhCVT4MJIoEsNcbhglhdmBKb09QtcmJNiIQGq7js/Khf5FtQQ9bzcAuloeqBeee7XD7JqDeve9KNlQya5tSGQ=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -479,8 +488,6 @@ "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], - "jpeg-exif": ["jpeg-exif@1.1.4", "", {}, "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ=="], - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -503,8 +510,6 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - "linebreak": ["linebreak@1.1.0", "", { "dependencies": { "base64-js": "0.0.8", "unicode-trie": "^2.0.0" } }, "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ=="], - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "lint-staged": ["lint-staged@16.2.7", "", { "dependencies": { "commander": "^14.0.2", "listr2": "^9.0.5", "micromatch": "^4.0.8", "nano-spawn": "^2.0.0", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow=="], @@ -515,6 +520,12 @@ "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + "lodash.chunk": ["lodash.chunk@4.2.0", "", {}, "sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w=="], + + "lodash.clonedeep": ["lodash.clonedeep@4.5.0", "", {}, "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="], + + "lodash.flatten": ["lodash.flatten@4.4.0", "", {}, "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="], + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], @@ -545,6 +556,8 @@ "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "manage-path": ["manage-path@2.0.0", "", {}, "sha512-NJhyB+PJYTpxhxZJ3lecIGgh4kwIY2RAh44XvAz9UlqthlQwtPBf62uBVR8XaD8CRuSjQ6TnZH2lNJkbLPZM2A=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], @@ -579,6 +592,8 @@ "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], + "on-net-listen": ["on-net-listen@1.1.2", "", {}, "sha512-y1HRYy8s/RlcBvDUwKXSmkODMdx4KSuIvloCnQYJ2LdBBC1asY4HtfhXwe3UWknLakATZDnbzht2Ijw3M1EqFg=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], @@ -593,7 +608,7 @@ "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], - "pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], @@ -607,10 +622,6 @@ "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], - "pdfkit": ["pdfkit@0.17.2", "", { "dependencies": { "crypto-js": "^4.2.0", "fontkit": "^2.0.4", "jpeg-exif": "^1.1.4", "linebreak": "^1.1.0", "png-js": "^1.0.0" } }, "sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw=="], - - "pdfmake": ["pdfmake@0.3.3", "", { "dependencies": { "linebreak": "^1.1.0", "pdfkit": "^0.17.2", "xmldoc": "^2.0.3" } }, "sha512-jSnF8rVLkbLLX37bnXWRFhEDO48quE7OIg7lgWBa6ihAbpCxASaBLWFOXNxSDeLBNt92304SBwpYcPkJnIArlA=="], - "pg": ["pg@8.18.0", "", { "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ=="], "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], @@ -641,14 +652,6 @@ "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], -<<<<<<< Updated upstream - "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], - - "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], -======= - "png-js": ["png-js@1.0.0", "", {}, "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="], ->>>>>>> Stashed changes - "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], @@ -661,8 +664,12 @@ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], + "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], @@ -677,6 +684,8 @@ "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], + "reinterval": ["reinterval@1.1.0", "", {}, "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -689,7 +698,7 @@ "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - "restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="], + "retimer": ["retimer@3.0.0", "", {}, "sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA=="], "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], @@ -697,11 +706,9 @@ "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], - "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], - "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -741,7 +748,7 @@ "thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="], - "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], + "timestring": ["timestring@6.0.0", "", {}, "sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA=="], "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], @@ -773,16 +780,14 @@ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "unicode-properties": ["unicode-properties@1.4.1", "", { "dependencies": { "base64-js": "^1.3.0", "unicode-trie": "^2.0.0" } }, "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg=="], - - "unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="], - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "uuid-parse": ["uuid-parse@1.1.0", "", {}, "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], @@ -795,8 +800,6 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "xmldoc": ["xmldoc@2.0.3", "", { "dependencies": { "sax": "^1.4.3" } }, "sha512-6gRk4NY/Jvg67xn7OzJuxLRsGgiXBaPUQplVJ/9l99uIugxh4FTOewYz5ic8WScj7Xx/2WvhENiQKwkK9RpE4w=="], - "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -833,10 +836,6 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "brotli/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "buffer/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "cli-truncate/string-width": ["string-width@8.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw=="], "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -847,6 +846,10 @@ "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "hyperid/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "hyperid/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "log-update/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "pino-pretty/strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], @@ -855,7 +858,7 @@ "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], - "svix/@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], + "svix/@types/node": ["@types/node@22.19.10", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw=="], "svix/uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], @@ -863,8 +866,6 @@ "typeorm/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "unicode-properties/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], diff --git a/package.json b/package.json index d29a410..e1f44f0 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,9 @@ "elysia": "^1.4.22", "elysia-rate-limit": "^4.5.0", "jsonwebtoken": "^9.0.3", - "pdfmake": "^0.3.3", "pg": "^8.18.0", "pino": "^10.3.0", "pino-pretty": "^13.1.3", - "playwright": "^1.58.2", "resend": "6.4.2", "typeorm": "^0.3.28" }, @@ -35,8 +33,9 @@ "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.2", "@faker-js/faker": "^10.2.0", + "@types/autocannon": "^7.12.7", "@types/jsonwebtoken": "^9.0.10", - "@types/pdfmake": "^0.3.0", + "autocannon": "^8.0.0", "bun-types": "^1.3.8", "eslint": "^9.39.2", "globals": "^16.5.0", diff --git a/src/modules/reports/application/GenerateWrappedReport.ts b/src/modules/reports/application/GenerateWrappedReport.ts deleted file mode 100644 index 125fc51..0000000 --- a/src/modules/reports/application/GenerateWrappedReport.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { IReportsRepository } from "../domain/IReportsRepository"; -import { IPdfGenerator, WrappedReportData } from "../domain/IPdfGenerator"; - -export class GenerateWrappedReport { - constructor( - private readonly repository: IReportsRepository, - private readonly pdfGenerator: IPdfGenerator - ) { } - - async execute(userId: string): Promise { - const seasons = [3, 4, 5]; - const playerStats = await this.repository.getPlayerStats(userId, seasons); - const rivals = await this.repository.getTopRivals(userId, seasons); - - // Aggregate stats - const totalWins = playerStats.reduce((sum, s) => sum + s.wins, 0); - const totalLosses = playerStats.reduce((sum, s) => sum + s.losses, 0); - const totalMatches = totalWins + totalLosses; - const totalWinRate = totalMatches > 0 ? ((totalWins / totalMatches) * 100).toFixed(1) + "%" : "0%"; - - const reportData: WrappedReportData = { - totalMatches, - totalWins, - totalLosses, - totalWinRate, - seasons: seasons.map(season => { - const stats = playerStats.find(s => s.season === season); - const rival = rivals.find(r => r.season === season); - - if (!stats) return { - season, - stats: null, - rival: null - }; - - const winRate = (stats.wins + stats.losses) > 0 - ? ((stats.wins / (stats.wins + stats.losses)) * 100).toFixed(1) + "%" - : "0%"; - - return { - season, - stats: { - season: stats.season, - points: stats.points, - wins: stats.wins, - losses: stats.losses, - winRate - }, - rival: rival ? { - name: rival.rivalName, - wins: rival.wins, - matches: rival.matches - } : null - }; - }) - }; - - return this.pdfGenerator.generate(reportData); - } -} - diff --git a/src/modules/reports/domain/IPdfGenerator.ts b/src/modules/reports/domain/IPdfGenerator.ts deleted file mode 100644 index 1790f52..0000000 --- a/src/modules/reports/domain/IPdfGenerator.ts +++ /dev/null @@ -1,31 +0,0 @@ -export interface SeasonStats { - season: number; - points: number; - wins: number; - losses: number; - winRate: string; -} - -export interface SeasonRival { - name: string; - wins: number; - matches: number; -} - -export interface SeasonReportData { - season: number; - stats: SeasonStats | null; - rival: SeasonRival | null; -} - -export interface WrappedReportData { - totalMatches: number; - totalWins: number; - totalLosses: number; - totalWinRate: string; - seasons: SeasonReportData[]; -} - -export interface IPdfGenerator { - generate(data: WrappedReportData): Promise; -} diff --git a/src/modules/reports/domain/IReportsRepository.ts b/src/modules/reports/domain/IReportsRepository.ts deleted file mode 100644 index afbaa66..0000000 --- a/src/modules/reports/domain/IReportsRepository.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface PlayerSeasonStats { - season: number; - wins: number; - losses: number; - points: number; -} - -export interface PlayerRival { - season: number; - rivalId: string; - rivalName: string; - matches: number; - wins: number; -} - -export interface IReportsRepository { - getPlayerStats(userId: string, seasons: number[]): Promise; - getTopRivals(userId: string, seasons: number[]): Promise; -} diff --git a/src/modules/reports/infrastructure/PdfMakeGenerator.ts b/src/modules/reports/infrastructure/PdfMakeGenerator.ts deleted file mode 100644 index 4283aab..0000000 --- a/src/modules/reports/infrastructure/PdfMakeGenerator.ts +++ /dev/null @@ -1,175 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-require-imports -const PdfPrinter = require('pdfmake/js/Printer').default; -import { TDocumentDefinitions } from "pdfmake/interfaces"; -import { IPdfGenerator, WrappedReportData } from "../domain/IPdfGenerator"; - -export class PdfMakeGenerator implements IPdfGenerator { - async generate(data: WrappedReportData): Promise { - const fonts = { - Roboto: { - normal: "node_modules/pdfmake/fonts/Roboto/Roboto-Regular.ttf", - bold: "node_modules/pdfmake/fonts/Roboto/Roboto-Medium.ttf", - italics: "node_modules/pdfmake/fonts/Roboto/Roboto-Italic.ttf", - bolditalics: "node_modules/pdfmake/fonts/Roboto/Roboto-MediumItalic.ttf" - } - }; - - const printer = new PdfPrinter(fonts); - - const docDefinition: TDocumentDefinitions = { - pageMargins: [40, 60, 40, 60], - defaultStyle: { - font: 'Roboto', - fontSize: 12, - color: '#333333' - }, - background: [ - { - canvas: [ - { type: 'rect', x: 0, y: 0, w: 595.28, h: 841.89, color: '#f8f9fa' } // Light gray background - ] - } - ], - content: [ - { - text: "✨ YOUR EVOLUTION WRAPPED ✨", - style: "header", - alignment: "center", - margin: [0, 20, 0, 40] - }, - { - columns: [ - { width: '*', text: '' }, - { - width: 'auto', - stack: [ - { text: "🏆 ALL TIME STATS (S3-S5)", style: "subheader", alignment: "center" }, - { - table: { - body: [ - [ - { text: "🔥 Matches", style: "statLabel" }, - { text: data.totalMatches.toString(), style: "statVal" }, - { text: "✅ Wins", style: "statLabel" }, - { text: data.totalWins.toString(), style: "statVal" } - ], - [ - { text: "❌ Losses", style: "statLabel" }, - { text: data.totalLosses.toString(), style: "statVal" }, - { text: "📈 Win Rate", style: "statLabel" }, - { text: data.totalWinRate, style: "statVal" } - ] - ] - }, - layout: 'noBorders', - style: "statTable" - } - ] - }, - { width: '*', text: '' } - ] - }, - { text: "", margin: [0, 20] }, - // Season Breakdown - ...data.seasons.map(seasonData => { - const stats = seasonData.stats; - const rival = seasonData.rival; - - if (!stats) return null; - - return [ - { - canvas: [ - { type: 'line', x1: 0, y1: 0, x2: 515, y2: 0, lineWidth: 1, lineColor: '#e0e0e0' } - ], - margin: [0, 20, 0, 20] - }, - { - text: `📅 SEASON ${seasonData.season}`, - style: "seasonHeader", - margin: [0, 0, 0, 10] - }, - { - columns: [ - { - width: '50%', - stack: [ - { text: `Points: ${stats.points} 💎`, margin: [0, 2, 0, 2] }, - { text: `Record: ${stats.wins}W - ${stats.losses}L`, margin: [0, 2, 0, 2] }, - { text: `Win Rate: ${stats.winRate}`, margin: [0, 2, 0, 2] } - ], - style: "seasonStats" - }, - { - width: '50%', - stack: [ - { text: "💀 Top Rival:", bold: true, color: '#555' }, - rival ? { text: `${rival.name.toUpperCase()}`, fontSize: 14, bold: true, margin: [0, 2, 0, 0] } : { text: "No matches", italics: true, color: '#999' }, - rival ? { text: `${rival.wins} wins in ${rival.matches} games`, fontSize: 10, color: '#777' } : {} - ], - alignment: 'right' - } - ] - } - ]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }).filter(Boolean) as any[] - ], - styles: { - header: { - fontSize: 26, - bold: true, - color: '#1a1a1a', - characterSpacing: 2 - }, - subheader: { - fontSize: 16, - bold: true, - color: '#444444', - margin: [0, 0, 0, 10] - }, - seasonHeader: { - fontSize: 18, - bold: true, - color: '#2c3e50' - }, - statLabel: { - fontSize: 12, - color: '#666666', - margin: [0, 5, 10, 5] - }, - statVal: { - fontSize: 14, - bold: true, - color: '#000000', - margin: [0, 5, 20, 5] - }, - statTable: { - margin: [0, 10, 0, 10] - }, - seasonStats: { - fontSize: 12, - color: '#444' - } - } - }; - - return new Promise((resolve, reject) => { - try { - const pdfDoc = printer.createPdfKitDocument(docDefinition); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Promise.resolve(pdfDoc).then((doc: any) => { - const chunks: Uint8Array[] = []; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - doc.on('data', (chunk: any) => chunks.push(chunk)); - doc.on('end', () => resolve(Buffer.concat(chunks))); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - doc.on('error', (err: any) => reject(err)); - doc.end(); - }).catch(reject); - } catch (err) { - reject(err); - } - }); - } -} diff --git a/src/modules/reports/infrastructure/ReportsController.ts b/src/modules/reports/infrastructure/ReportsController.ts deleted file mode 100644 index a90857b..0000000 --- a/src/modules/reports/infrastructure/ReportsController.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { GenerateWrappedReport } from "../application/GenerateWrappedReport"; -import { ReportsPostgresRepository } from "../infrastructure/ReportsPostgresRepository"; -import { PdfMakeGenerator } from "../infrastructure/PdfMakeGenerator"; - -export class ReportsController { - async getWrapped(context: { user: { profile: { id: string } } }) { - const userId = context.user.profile.id; - const generateReport = new GenerateWrappedReport( - new ReportsPostgresRepository(), - new PdfMakeGenerator() - ); - - const pdfBuffer = await generateReport.execute(userId); - - return new Response(pdfBuffer as unknown as BodyInit, { - headers: { - "Content-Type": "application/pdf", - "Content-Disposition": `attachment; filename="evolution-wrapped-${userId}.pdf"` - } - }); - } -} diff --git a/src/modules/reports/infrastructure/ReportsPostgresRepository.ts b/src/modules/reports/infrastructure/ReportsPostgresRepository.ts deleted file mode 100644 index e553135..0000000 --- a/src/modules/reports/infrastructure/ReportsPostgresRepository.ts +++ /dev/null @@ -1,101 +0,0 @@ - - -import { dataSource } from "../../../evolution-types/src/data-source"; -import { PlayerStatsEntity } from "../../../evolution-types/src/entities/PlayerStatsEntity"; -import { MatchResumeEntity } from "../../../evolution-types/src/entities/MatchResumeEntity"; -import { IReportsRepository, PlayerRival, PlayerSeasonStats } from "../domain/IReportsRepository"; - -export class ReportsPostgresRepository implements IReportsRepository { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private playerStatsRepository: any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private matchRepository: any; - - constructor() { - this.playerStatsRepository = dataSource.getRepository(PlayerStatsEntity); - this.matchRepository = dataSource.getRepository(MatchResumeEntity); - } - - async getPlayerStats(userId: string, seasons: number[]): Promise { - const stats = await this.playerStatsRepository - .createQueryBuilder("stats") - .where("stats.user_id = :userId", { userId }) - .andWhere("stats.season IN (:...seasons)", { seasons }) - .getMany(); - - return stats.map(s => ({ - season: s.season, - wins: s.wins, - losses: s.losses, - points: s.points - })); - } - - async getTopRivals(userId: string, seasons: number[]): Promise { - const rivals: PlayerRival[] = []; - - for (const season of seasons) { - const matches = await this.matchRepository - .createQueryBuilder("match") - .where("match.userId = :userId", { userId }) - .andWhere("match.season = :season", { season }) - .getMany(); - - const rivalStats = new Map(); - - for (const match of matches) { - if (!match.opponentIds || match.opponentIds.length === 0) continue; - const rivalId = match.opponentIds[0]; // Assuming 1v1 mostly, taking first opponent - const rivalName = match.opponentNames && match.opponentNames.length > 0 ? match.opponentNames[0] : "Unknown"; - - if (!rivalStats.has(rivalId)) { - rivalStats.set(rivalId, { wins: 0, matches: 0, name: rivalName }); - } - - const stats = rivalStats.get(rivalId)!; - stats.matches++; - if (match.winner) { // If user is the winner? - // match.winner is a boolean. match.userId is the user. - // References say 'winner' column. - // Usually means "Did this player win?". - // Let's verify. Match.ts says "winner: boolean". - // MatchResumeEntity says "winner: boolean". - // In Evolution API, usually match is stored from perspective of userId. - // If winner is true, userId won. - // Wait, I need to know if the user won against the rival. - // If match.winner is true, user won. - // So rival lost. - // But "wins" in PlayerRival context usually means "User's wins against Rival". - if (match.winner) { - stats.wins++; - } - } - } - - // Find top rival - let topRivalId = ""; - let maxMatches = -1; - - for (const [id, stats] of rivalStats.entries()) { - if (stats.matches > maxMatches) { - maxMatches = stats.matches; - topRivalId = id; - } - } - - if (topRivalId) { - const stats = rivalStats.get(topRivalId)!; - rivals.push({ - season, - rivalId: topRivalId, - rivalName: stats.name, - matches: stats.matches, - wins: stats.wins - }); - } - } - - return rivals; - } -} - diff --git a/src/modules/wrapped/application/GenerateSeasonWrapped.ts b/src/modules/wrapped/application/GenerateSeasonWrapped.ts deleted file mode 100644 index f264ae4..0000000 --- a/src/modules/wrapped/application/GenerateSeasonWrapped.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { WrappedRepository } from "../domain/WrappedRepository"; -import type { PdfGenerator } from "../infrastructure/PdfGenerator"; -import { config } from "../../../config"; - -export interface GenerateOptions { - locale?: string; - theme?: "dark" | "light"; - includeMatchList?: boolean; - singlePage?: boolean; -} - -export class GenerateSeasonWrapped { - constructor( - private readonly repository: WrappedRepository, - private readonly pdfGenerator: PdfGenerator, - ) { } - - async execute( - seasonId: number, - playerId: string, - options: GenerateOptions = {}, - ): Promise<{ pdf: Buffer; playerName: string }> { - // Prevent generating wrapped for the current/active season - if (seasonId >= config.season) { - throw new Error(`Season ${seasonId} Wrapped is not available yet.`); - } - const data = await this.repository.getSeasonWrappedData(seasonId, playerId); - - if (!data) { - throw new Error(`No data found for player ${playerId} in season ${seasonId}`); - } - - const pdf = await this.pdfGenerator.generate(data, { - locale: options.locale ?? "es", - theme: options.theme ?? "dark", - includeMatchList: options.includeMatchList ?? false, - singlePage: options.singlePage ?? false, - }); - - return { pdf, playerName: data.playerName }; - } -} diff --git a/src/modules/wrapped/application/ThemeStrategyFactory.ts b/src/modules/wrapped/application/ThemeStrategyFactory.ts new file mode 100644 index 0000000..d7647e0 --- /dev/null +++ b/src/modules/wrapped/application/ThemeStrategyFactory.ts @@ -0,0 +1,20 @@ +import type { IThemeStrategy } from "../domain/IThemeStrategy"; + +export class ThemeStrategyFactory { + private strategies: Map = new Map(); + + register(name: string, strategy: IThemeStrategy): void { + this.strategies.set(name, strategy); + } + + get(name: string): IThemeStrategy { + const strategy = this.strategies.get(name); + if (!strategy) { + // Fallback to dark theme if requested theme doesn't exist + const dark = this.strategies.get("dark"); + if (!dark) throw new Error("Default 'dark' theme not registered"); + return dark; + } + return strategy; + } +} diff --git a/src/modules/wrapped/domain/IThemeStrategy.ts b/src/modules/wrapped/domain/IThemeStrategy.ts new file mode 100644 index 0000000..cf04691 --- /dev/null +++ b/src/modules/wrapped/domain/IThemeStrategy.ts @@ -0,0 +1,30 @@ +import type { SeasonWrapped } from "./SeasonWrapped"; + +export interface ThemePhrases { + coverTitle: string; + coverSubtitle: string; + statsTitle: string; + statsSubtitle: string; + rivalsTitle: string; + rivalsSubtitle: string; + achievementsTitle: string; + achievementsSubtitle: string; + summaryTitle: string; + summarySubtitle: string; + [key: string]: string; +} + +export interface GenerateOptions { + locale: string; + theme: string; + includeMatchList: boolean; + singlePage?: boolean; +} + +export interface IThemeStrategy { + getName(): string; + getStylesheet(): string; + getBackground(): string; + getPhrases(data: SeasonWrapped): ThemePhrases; + renderSpecialSections(data: SeasonWrapped, options: GenerateOptions, background: string): string; +} diff --git a/src/modules/wrapped/domain/SeasonWrapped.ts b/src/modules/wrapped/domain/SeasonWrapped.ts index 220c29b..837915b 100644 --- a/src/modules/wrapped/domain/SeasonWrapped.ts +++ b/src/modules/wrapped/domain/SeasonWrapped.ts @@ -17,6 +17,7 @@ export class SeasonWrapped { public readonly banListStats: BanListStats[], public readonly nemesis: Nemesis | null, public readonly victim: Victim | null, + public readonly mostPlayedOpponent: Nemesis | null, public readonly achievements: Achievement[], public readonly ranking: PlayerRanking, public readonly extraStats: ExtraStats, diff --git a/src/modules/wrapped/infrastructure/PdfGenerator.ts b/src/modules/wrapped/infrastructure/PdfGenerator.ts deleted file mode 100644 index 6132432..0000000 --- a/src/modules/wrapped/infrastructure/PdfGenerator.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { chromium } from "playwright"; -import type { SeasonWrapped } from "../domain/SeasonWrapped"; -import { renderTemplate, renderSinglePageTemplate } from "./templates/templateRenderer"; - -export interface GenerateOptions { - locale: string; - theme: "dark" | "light"; - includeMatchList: boolean; - singlePage?: boolean; -} - -export class PdfGenerator { - async generate(data: SeasonWrapped, options: GenerateOptions): Promise { - const html = options.singlePage - ? renderSinglePageTemplate(data, options) - : renderTemplate(data, options); - - const browser = await chromium.launch({ - headless: true, - }); - - const page = await browser.newPage(); - - await page.setContent(html, { - waitUntil: "networkidle", - }); - - const pdf = await page.pdf({ - format: "A4", - printBackground: true, - margin: { - top: 0, - right: 0, - bottom: 0, - left: 0, - }, - preferCSSPageSize: true, - }); - - await browser.close(); - - return Buffer.from(pdf); - } -} diff --git a/src/modules/wrapped/infrastructure/WrappedController.ts b/src/modules/wrapped/infrastructure/WrappedController.ts index a8ea3ff..53e5b9a 100644 --- a/src/modules/wrapped/infrastructure/WrappedController.ts +++ b/src/modules/wrapped/infrastructure/WrappedController.ts @@ -1,6 +1,4 @@ -import { GenerateSeasonWrapped } from "../application/GenerateSeasonWrapped"; import { GetSeasonWrappedData } from "../application/GetSeasonWrappedData"; -import { PdfGenerator } from "./PdfGenerator"; import { WrappedPostgresRepository } from "./WrappedPostgresRepository"; // Domain errors @@ -19,36 +17,6 @@ export class ValidationError extends Error { } export class WrappedController { - async generatePdf(context: { - params: { seasonId: string; playerId: string }; - query: { locale?: string; theme?: "dark" | "light"; includeMatchList?: string; singlePage?: string }; - }): Promise<{ pdf: Buffer; seasonId: number; playerId: string; playerName: string }> { - const seasonId = parseInt(context.params.seasonId, 10); - const { playerId } = context.params; - - // Validation - if (isNaN(seasonId) || seasonId < 1) { - throw new ValidationError("Season ID must be a valid positive integer"); - } - - if (!playerId || !/^[a-f0-9-]{36}$/i.test(playerId)) { - throw new ValidationError("Player ID must be a valid UUID"); - } - - const repository = new WrappedPostgresRepository(); - const pdfGenerator = new PdfGenerator(); - const useCase = new GenerateSeasonWrapped(repository, pdfGenerator); - - const { pdf, playerName } = await useCase.execute(seasonId, playerId, { - locale: context.query.locale || "es", - theme: context.query.theme || "dark", - includeMatchList: context.query.includeMatchList === "true", - singlePage: context.query.singlePage === "true", - }); - - return { pdf, seasonId, playerId, playerName }; - } - async getData(context: { params: { seasonId: string; playerId: string } }) { const seasonId = parseInt(context.params.seasonId, 10); const { playerId } = context.params; diff --git a/src/modules/wrapped/infrastructure/WrappedPostgresRepository.ts b/src/modules/wrapped/infrastructure/WrappedPostgresRepository.ts index 8ff7079..83ee493 100644 --- a/src/modules/wrapped/infrastructure/WrappedPostgresRepository.ts +++ b/src/modules/wrapped/infrastructure/WrappedPostgresRepository.ts @@ -38,7 +38,8 @@ export class WrappedPostgresRepository implements WrappedRepository { [], null, null, - [], + null, // mostPlayedOpponent + [], // achievements { position: 0, totalPlayers: 0, points: 0, rankBadge: "Challenger" }, { mostPlayedBanList: null, uniqueOpponents: 0, bestDay: null }, ); @@ -74,6 +75,7 @@ export class WrappedPostgresRepository implements WrappedRepository { banListStats, nemesis, victim, + null, // mostPlayedOpponent achievements, ranking, extraStats, @@ -81,7 +83,7 @@ export class WrappedPostgresRepository implements WrappedRepository { } private async getGlobalStats(seasonId: number, playerId: string): Promise { - + const result = await dataSource.query( ` SELECT diff --git a/src/modules/wrapped/infrastructure/WrappedStorageService.ts b/src/modules/wrapped/infrastructure/WrappedStorageService.ts deleted file mode 100644 index f03b90f..0000000 --- a/src/modules/wrapped/infrastructure/WrappedStorageService.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { existsSync, mkdirSync, writeFileSync } from "fs"; -import { join } from "path"; - -export class WrappedStorageService { - private readonly storagePath: string; - - constructor() { - this.storagePath = join(process.cwd(), "storage", "wrapped"); - this.ensureDirectoryExists(); - } - - private ensureDirectoryExists(): void { - if (!existsSync(this.storagePath)) { - mkdirSync(this.storagePath, { recursive: true }); - } - } - - getFilePath(seasonId: number, playerId: string, locale: string, theme: string): string { - return join(this.storagePath, `season_${seasonId}_${playerId}_${locale}_${theme}.pdf`); - } - - exists(seasonId: number, playerId: string, locale: string, theme: string): boolean { - const filePath = this.getFilePath(seasonId, playerId, locale, theme); - return existsSync(filePath); - } - - save(seasonId: number, playerId: string, locale: string, theme: string, buffer: Buffer): void { - const filePath = this.getFilePath(seasonId, playerId, locale, theme); - writeFileSync(filePath, buffer); - } -} diff --git a/src/modules/wrapped/infrastructure/templates/optimized/black_rose_dragon.png b/src/modules/wrapped/infrastructure/templates/optimized/black_rose_dragon.png new file mode 100644 index 0000000..1a0409b Binary files /dev/null and b/src/modules/wrapped/infrastructure/templates/optimized/black_rose_dragon.png differ diff --git a/src/modules/wrapped/infrastructure/templates/styles.css b/src/modules/wrapped/infrastructure/templates/styles.css index ac8e248..a785ad6 100644 --- a/src/modules/wrapped/infrastructure/templates/styles.css +++ b/src/modules/wrapped/infrastructure/templates/styles.css @@ -31,32 +31,31 @@ --radius-xl: 32px; --radius-full: 9999px; - /* Modern Dark Theme */ - --bg-base: #0B1120; - /* Very dark blue/slate */ - --bg-card: #151e32; - /* Rich dark blue */ - --bg-card-hover: #1e2942; - --bg-highlight: #1E293B; + /* Premium Dark Theme */ + --bg-base: #010409; + /* Deep Black */ + --bg-card: #0d1117; + /* GitHub Dark Card Style */ + --bg-card-hover: #161b22; + --bg-highlight: #161b22; - --border-subtle: rgba(255, 255, 255, 0.05); - --border-medium: rgba(59, 130, 246, 0.2); + --border-subtle: rgba(255, 255, 255, 0.08); + --border-medium: rgba(255, 255, 255, 0.12); - --text-primary: #FFFFFF; - --text-secondary: #94A3B8; - --text-muted: #64748B; + --text-primary: #f0f6fc; + --text-secondary: #8b949e; + --text-muted: #484f58; /* Semantic Colors */ - --color-win: #3B82F6; - /* Blue for wins as per image */ - --color-loss: #64748B; - /* Muted slate for losses */ - --color-accent: #3B82F6; - /* Primary Blue */ - --color-accent-glow: rgba(59, 130, 246, 0.5); + --color-win: var(--color-accent); + --color-loss: var(--text-muted); + --color-accent: #58a6ff; + --color-accent-glow: rgba(88, 166, 255, 0.4); - --gradient-primary: linear-gradient(135deg, #3B82F6 0%, #2563EB 100%); - --gradient-dark: linear-gradient(180deg, rgba(15, 23, 42, 0) 0%, rgba(15, 23, 42, 0.8) 100%); + --gradient-primary: linear-gradient(135deg, var(--color-accent) 0%, var(--bg-card) 100%); + --gradient-dark: linear-gradient(180deg, rgba(1, 4, 9, 0) 0%, rgba(1, 4, 9, 0.8) 100%); + + transition: all 0.3s ease; } * { @@ -837,38 +836,39 @@ body { grid-template-columns: 1fr !important; } } + /* Download Floating Action Button */ .download-fab { - position: fixed; - bottom: 32px; - right: 32px; - width: 64px; - height: 64px; - background: var(--color-accent); - color: white; - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - box-shadow: 0 4px 12px rgba(0,0,0,0.3); - cursor: pointer; - z-index: 1000; - transition: transform 0.2s, background 0.2s; - text-decoration: none; + position: fixed; + bottom: 32px; + right: 32px; + width: 64px; + height: 64px; + background: var(--color-accent); + color: white; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + cursor: pointer; + z-index: 1000; + transition: transform 0.2s, background 0.2s; + text-decoration: none; } .download-fab:hover { - transform: scale(1.1); - background: #2563EB; + transform: scale(1.1); + background: #2563EB; } .download-fab svg { - width: 32px; - height: 32px; + width: 32px; + height: 32px; } @media print { - .download-fab { - display: none !important; - } -} + .download-fab { + display: none !important; + } +} \ No newline at end of file diff --git a/src/modules/wrapped/infrastructure/templates/templateRenderer.ts b/src/modules/wrapped/infrastructure/templates/templateRenderer.ts index 16343a2..2271086 100644 --- a/src/modules/wrapped/infrastructure/templates/templateRenderer.ts +++ b/src/modules/wrapped/infrastructure/templates/templateRenderer.ts @@ -1,8 +1,8 @@ -import type { GenerateOptions } from "../PdfGenerator"; import type { SeasonWrapped } from "../../domain/SeasonWrapped"; import { readFileSync, existsSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; +import type { IThemeStrategy, ThemePhrases, GenerateOptions } from "../../domain/IThemeStrategy"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -30,13 +30,20 @@ const images = { icon: getImageAsBase64('yugioh_chapter_icon.png'), // Small chapter icon }; -export function renderTemplate(data: SeasonWrapped, options: GenerateOptions): string { +export function renderTemplate(data: SeasonWrapped, options: GenerateOptions, themeStrategy: IThemeStrategy): string { const styles = readFileSync(join(__dirname, "styles.css"), "utf-8"); const themeCss = getSeasonTheme(data.seasonId); + const themeStylesheet = themeStrategy.getStylesheet(); + const phrases = themeStrategy.getPhrases(data); + + // Select theme background or random monster + let randomMonster = themeStrategy.getBackground(); + if (!randomMonster) { + const monsterImages = [images.decorative1, images.decorative2].filter(Boolean); + randomMonster = monsterImages[Math.floor(Math.random() * monsterImages.length)] || images.decorative1; + } - // Select one random monster image to use as background for ALL pages - const monsterImages = [images.decorative1, images.decorative2].filter(Boolean); - const randomMonster = monsterImages[Math.floor(Math.random() * monsterImages.length)] || images.decorative1; + const specialSections = themeStrategy.renderSpecialSections(data, options, randomMonster); return ` @@ -48,39 +55,66 @@ export function renderTemplate(data: SeasonWrapped, options: GenerateOptions): s - ${renderCoverPage(data, options, randomMonster)} - ${renderGlobalStatsPage(data, options, randomMonster)} - ${renderBanListPages(data, options, randomMonster)} - ${renderChartsPage(data, options, randomMonster)} - ${(data.nemesis || data.victim) ? renderRivalsPage(data, options, randomMonster) : ""} - ${data.achievements.length > 0 ? renderAchievementsPage(data, options, randomMonster) : ""} - ${renderRankingPage(data, options, randomMonster)} - ${renderSummaryPage(data, options, randomMonster)} - - - - - - - - + ${renderCoverPage(data, options, randomMonster, phrases)} + ${renderGlobalStatsPage(data, options, randomMonster, phrases)} + ${renderBanListPages(data, options, randomMonster, phrases)} + ${renderChartsPage(data, options, randomMonster, phrases)} + ${(data.nemesis || data.victim) ? renderRivalsPage(data, options, randomMonster, phrases) : ""} + ${specialSections} + ${renderRankingPage(data, options, randomMonster, phrases)} + ${renderSummaryPage(data, options, randomMonster, phrases)} `.trim(); } // Single-page compact version for evaluation -export function renderSinglePageTemplate(data: SeasonWrapped, options: GenerateOptions): string { +export function renderSinglePageTemplate(data: SeasonWrapped, options: GenerateOptions, themeStrategy: IThemeStrategy): string { const styles = readFileSync(join(__dirname, "styles.css"), "utf-8"); const singlePageStyles = readFileSync(join(__dirname, "styles_single_page.css"), "utf-8"); const themeCss = getSeasonTheme(data.seasonId); - - // Select one random monster image to use as background for ALL pages - const monsterImages = [images.decorative1, images.decorative2].filter(Boolean); - const randomMonster = monsterImages[Math.floor(Math.random() * monsterImages.length)] || images.decorative1; + const themeStylesheet = themeStrategy.getStylesheet(); + const phrases = themeStrategy.getPhrases(data); + + // Select theme background or random monster + let randomMonster = themeStrategy.getBackground(); + if (!randomMonster) { + const monsterImages = [images.decorative1, images.decorative2].filter(Boolean); + randomMonster = monsterImages[Math.floor(Math.random() * monsterImages.length)] || images.decorative1; + } return ` @@ -93,34 +127,35 @@ export function renderSinglePageTemplate(data: SeasonWrapped, options: GenerateO ${styles} ${singlePageStyles} ${themeCss} + ${themeStylesheet} - ${renderCoverPage(data, options, randomMonster)} - ${renderGlobalStatsPage(data, options, randomMonster)} - ${renderBanListPages(data, options, randomMonster)} - ${renderChartsPage(data, options, randomMonster)} - ${(data.nemesis || data.victim) ? renderRivalsPage(data, options, randomMonster) : ""} - ${data.achievements.length > 0 ? renderAchievementsPage(data, options, randomMonster) : ""} - ${renderRankingPage(data, options, randomMonster)} + ${renderCoverPage(data, options, randomMonster, phrases)} + ${renderGlobalStatsPage(data, options, randomMonster, phrases)} + ${renderBanListPages(data, options, randomMonster, phrases)} + ${renderChartsPage(data, options, randomMonster, phrases)} + ${(data.nemesis || data.victim) ? renderRivalsPage(data, options, randomMonster, phrases) : ""} + ${data.achievements.length > 0 ? renderAchievementsPage(data, options, randomMonster, phrases) : ""} + ${renderRankingPage(data, options, randomMonster, phrases)} `.trim(); } -function renderHeader(title: string, seasonName: string): string { +function renderHeader(title: string, seasonName: string, phrases: ThemePhrases): string { return `
- DUELIST WRAPPED + ${phrases.brandName || "DUELIST WRAPPED"}
${seasonName}
`; } -function renderCoverPage(data: SeasonWrapped, options: GenerateOptions, randomMonster: string): string { +function renderCoverPage(data: SeasonWrapped, options: GenerateOptions, randomMonster: string, phrases: ThemePhrases): string { return ` @@ -133,7 +168,7 @@ function renderCoverPage(data: SeasonWrapped, options: GenerateOptions, randomMo -

${options.locale === "es" ? "TU TEMPORADA EN DUELOS" : "YOUR DUELING SEASON"}

+

${phrases.coverTitle}

${escapeHtml(data.playerName)}

@@ -149,19 +184,22 @@ function renderCoverPage(data: SeasonWrapped, options: GenerateOptions, randomMo `; } -function renderGlobalStatsPage(data: SeasonWrapped, options: GenerateOptions, randomMonster: string): string { +function renderGlobalStatsPage(data: SeasonWrapped, options: GenerateOptions, randomMonster: string, phrases: ThemePhrases): string { const stats = data.globalStats; return `
${randomMonster ? `
` : ''} - ${renderHeader("Season Overview", data.seasonName)} + ${renderHeader("Season Overview", data.seasonName, phrases)}
${images.icon ? `` : ''} - ${options.locale === "es" ? "CAPÍTULO 1" : "CHAPTER 1"} + ${phrases.chapter1 || (options.locale === "es" ? "CAPÍTULO 1" : "CHAPTER 1")}
-

${options.locale === "es" ? "Resumen de Temporada" : "Season Overview"}

+

${phrases.statsTitle}

+

+ ${phrases.statsSubtitle} +

@@ -208,7 +246,7 @@ function renderGlobalStatsPage(data: SeasonWrapped, options: GenerateOptions, ra `; } -function renderBanListPages(data: SeasonWrapped, options: GenerateOptions, randomMonster: string): string { +function renderBanListPages(data: SeasonWrapped, options: GenerateOptions, randomMonster: string, phrases: ThemePhrases): string { if (data.banListStats.length === 0) return ""; // Take top 3 banlists to fit on one page if possible, or paginate @@ -217,13 +255,13 @@ function renderBanListPages(data: SeasonWrapped, options: GenerateOptions, rando return `
${randomMonster ? `
` : ''} - ${renderHeader("Formats", data.seasonName)} + ${renderHeader("Formats", data.seasonName, phrases)}
${images.icon ? `` : ''} - ${options.locale === "es" ? "CAPÍTULO 2" : "CHAPTER 2"} + ${phrases.chapter2 || (options.locale === "es" ? "CAPÍTULO 2" : "CHAPTER 2")}
-

${options.locale === "es" ? "Formatos Dominados" : "Mastered Formats"}

+

${phrases.statsTitle}

MAIN FORMAT
@@ -255,56 +293,65 @@ function renderBanListPages(data: SeasonWrapped, options: GenerateOptions, rando `; } -function renderRivalsPage(data: SeasonWrapped, options: GenerateOptions, randomMonster: string): string { +function renderRivalsPage(data: SeasonWrapped, options: GenerateOptions, randomMonster: string, phrases: ThemePhrases): string { return `
${randomMonster ? `
` : ''} - ${renderHeader("Rivals", data.seasonName)} + ${renderHeader("Rivals", data.seasonName, phrases)}
${images.icon ? `` : ''} ${options.locale === "es" ? "CAPÍTULO 3" : "CHAPTER 3"}
-

${options.locale === "es" ? "Tus Rivales" : "Your Rivals"}

+

${phrases.rivalsTitle}

+

+ ${phrases.rivalsSubtitle} +

${data.nemesis ? ` -
+
- 👻 SEASON NEMESIS + 👻 ${options.locale === "es" ? "EL ROMPECORAZONES (NÉMESIS)" : "THE HEARTBREAKER (NEMESIS)"}

${escapeHtml(data.nemesis.playerName)}

+

+ ${options.locale === "es" ? "Te robó los Life Points... y la dignidad." : "Stole your Life Points... and your pride."} +

${data.nemesis.totalMatches} matches - ${data.nemesis.losses} losses + ${data.nemesis.losses} ${options.locale === "es" ? "derrotas" : "losses"}
` : ""} ${data.victim ? ` -
+
- 🎯 SEASON VICTIM + 🎯 ${options.locale === "es" ? "TU ADMIRADOR SECRETO (VÍCTIMA)" : "YOUR SECRET ADMIRER (VICTIM)"}

${escapeHtml(data.victim.playerName)}

+

+ ${options.locale === "es" ? "Te quiere tanto que te deja ganar (o eso dices)." : "They love you so much they let you win (or so you say)."} +

${data.victim.totalMatches} matches - ${data.victim.wins} wins + ${data.victim.wins} ${options.locale === "es" ? "victorias" : "wins"}
` : ""} @@ -318,18 +365,18 @@ function renderRivalsPage(data: SeasonWrapped, options: GenerateOptions, randomM if (!archRival) return ""; return ` -
+
- ⚔️ ${options.locale === "es" ? "ARCHIENEMIGO" : "ARCH-RIVAL"} + ⚔️ ${options.locale === "es" ? "MEDIA NARANJA (ARCHIENEMIGO)" : "BETTER HALF (ARCH-RIVAL)"}

${escapeHtml(archRival.playerName)}

-

- ${options.locale === "es" ? "Máximo rival de la temporada" : "Top rival of the season"} +

+ ${options.locale === "es" ? "No pueden vivir el uno sin el otro... en el campo." : "Can't live without each other... on the field."}

@@ -344,7 +391,7 @@ function renderRivalsPage(data: SeasonWrapped, options: GenerateOptions, randomM `; } -function renderChartsPage(data: SeasonWrapped, options: GenerateOptions, randomMonster: string): string { +function renderChartsPage(data: SeasonWrapped, options: GenerateOptions, randomMonster: string, phrases: ThemePhrases): string { if (data.banListStats.length === 0) return ""; // Calculate max matches for scaling @@ -353,7 +400,7 @@ function renderChartsPage(data: SeasonWrapped, options: GenerateOptions, randomM return `
${randomMonster ? `
` : ''} - ${renderHeader("Evolution", data.seasonName)} + ${renderHeader("Evolution", data.seasonName, phrases)}
${options.locale === "es" ? "CAPÍTULO 4" : "CHAPTER 4"}

${options.locale === "es" ? "Evolución" : "Evolution"}

@@ -377,16 +424,16 @@ function renderChartsPage(data: SeasonWrapped, options: GenerateOptions, randomM `; } -function renderAchievementsPage(data: SeasonWrapped, options: GenerateOptions, randomMonster: string): string { +function renderAchievementsPage(data: SeasonWrapped, options: GenerateOptions, randomMonster: string, phrases: ThemePhrases): string { if (data.achievements.length === 0) return ""; return `
${randomMonster ? `
` : ''} - ${renderHeader("Logros", data.seasonName)} + ${renderHeader("Logros", data.seasonName, phrases)}
${options.locale === "es" ? "CAPÍTULO 5" : "CHAPTER 5"}
-

${options.locale === "es" ? "Logros" : "Achievements"}

+

${phrases.achievementsTitle}

${data.achievements.map(ach => ` @@ -417,11 +464,11 @@ function renderAchievementsPage(data: SeasonWrapped, options: GenerateOptions, r `; } -function renderRankingPage(data: SeasonWrapped, options: GenerateOptions, randomMonster: string): string { +function renderRankingPage(data: SeasonWrapped, options: GenerateOptions, randomMonster: string, phrases: ThemePhrases): string { return `
${randomMonster ? `
` : ''} - ${renderHeader("Ranking", data.seasonName)} + ${renderHeader("Ranking", data.seasonName, phrases)}
${options.locale === "es" ? "FINAL" : "FINALE"}

${options.locale === "es" ? "Posición" : "Ranking"}

@@ -463,15 +510,15 @@ function renderRankingPage(data: SeasonWrapped, options: GenerateOptions, random `; } -function renderSummaryPage(data: SeasonWrapped, options: GenerateOptions, randomMonster: string): string { +function renderSummaryPage(data: SeasonWrapped, options: GenerateOptions, randomMonster: string, phrases: ThemePhrases): string { return `
${randomMonster ? `
` : ''}
-

${escapeHtml(data.playerName)}

-
${data.seasonName}
+

${phrases.summaryTitle || escapeHtml(data.playerName)}

+
${phrases.summarySubtitle || data.seasonName}
@@ -571,29 +618,29 @@ function getBanListFlavor(winrate: number): string { function getSeasonTheme(seasonId: number): string { const themes: Record = { - // Season 3: Nature/Wind - Emerald/Green (Old S1 Theme) + // Season 3: Wind/Nature - Refined Teal/Forest 3: { - accent: '#10B981', - bgBase: '#064E3B', - bgCard: '#065F46' + accent: '#2DD4BF', + bgBase: '#041010', + bgCard: '#0A1F1F' }, - // Season 4: Fire/Invasion - Red/Orange (Old S2 Theme) + // Season 4: Fire/Invasion - Refined Muted Coral/Maroon 4: { - accent: '#EF4444', - bgBase: '#450A0A', - bgCard: '#7F1D1D' + accent: '#F87171', + bgBase: '#110707', + bgCard: '#1F0D0D' }, - // Season 5: Water/Abyss - Cyan/Blue + // Season 5: Water/Abyss - Refined Midnight/Cyan 5: { - accent: '#06B6D4', - bgBase: '#083344', - bgCard: '#164E63' + accent: '#38BDF8', + bgBase: '#050C14', + bgCard: '#0D1B2A' }, - // Season 6: Current/Tech - Blue (Default) + // Season 6: Current/Tech - Refined Indigo/Slate 6: { - accent: '#3B82F6', - bgBase: '#0B1120', - bgCard: '#151e32' + accent: '#818CF8', + bgBase: '#0A0F1E', + bgCard: '#161B33' } }; diff --git a/src/modules/wrapped/infrastructure/themes/AbstractThemeStrategy.ts b/src/modules/wrapped/infrastructure/themes/AbstractThemeStrategy.ts new file mode 100644 index 0000000..2c9d3c0 --- /dev/null +++ b/src/modules/wrapped/infrastructure/themes/AbstractThemeStrategy.ts @@ -0,0 +1,51 @@ +import type { IThemeStrategy, ThemePhrases, GenerateOptions } from "../../domain/IThemeStrategy"; +import type { SeasonWrapped } from "../../domain/SeasonWrapped"; +import { readFileSync, existsSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export abstract class AbstractThemeStrategy implements IThemeStrategy { + abstract getName(): string; + + getStylesheet(): string { + return ""; // Override in concrete classes if needed + } + + getBackground(): string { + return ""; + } + + getPhrases(data: SeasonWrapped): ThemePhrases { + return { + coverTitle: "EVOLUTION WRAPPED", + coverSubtitle: `Temporada ${data.seasonId}`, + statsTitle: "RESUMEN DE TEMPORADA", + statsSubtitle: "Tus números en el campo de batalla", + rivalsTitle: "ARCHI-RIVAL", + rivalsSubtitle: "El duelo nunca termina", + achievementsTitle: "LOGROS OBTENIDOS", + achievementsSubtitle: "Tu legado en Evolution", + summaryTitle: "RESUMEN FINAL", + summarySubtitle: "¡Nos vemos en el próximo duelo!" + }; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + renderSpecialSections(_data: SeasonWrapped, _options: GenerateOptions, _background: string): string { + return ""; + } + + protected getImageAsBase64(filename: string): string { + const imagesPath = join(__dirname, "..", "templates", "optimized"); + const filePath = join(imagesPath, filename); + if (existsSync(filePath)) { + const buffer = readFileSync(filePath); + const extension = filename.split('.').pop(); + return `data:image/${extension};base64,${buffer.toString('base64')}`; + } + return ""; + } +} diff --git a/src/modules/wrapped/infrastructure/themes/DarkThemeStrategy.ts b/src/modules/wrapped/infrastructure/themes/DarkThemeStrategy.ts new file mode 100644 index 0000000..de811a0 --- /dev/null +++ b/src/modules/wrapped/infrastructure/themes/DarkThemeStrategy.ts @@ -0,0 +1,20 @@ +import { AbstractThemeStrategy } from "./AbstractThemeStrategy"; + +export class DarkThemeStrategy extends AbstractThemeStrategy { + getName(): string { + return "dark"; + } + + getStylesheet(): string { + return ` + :root { + --color-bg: #0f172a; + --color-surface: #1e293b; + --color-text: #f1f5f9; + --color-text-muted: #94a3b8; + --color-accent: #38bdf8; + --color-border: #334155; + } + `; + } +} diff --git a/src/modules/wrapped/infrastructure/themes/LightThemeStrategy.ts b/src/modules/wrapped/infrastructure/themes/LightThemeStrategy.ts new file mode 100644 index 0000000..7c99c8c --- /dev/null +++ b/src/modules/wrapped/infrastructure/themes/LightThemeStrategy.ts @@ -0,0 +1,20 @@ +import { AbstractThemeStrategy } from "./AbstractThemeStrategy"; + +export class LightThemeStrategy extends AbstractThemeStrategy { + getName(): string { + return "light"; + } + + getStylesheet(): string { + return ` + :root { + --color-bg: #f8fafc; + --color-surface: #ffffff; + --color-text: #0f172a; + --color-text-muted: #64748b; + --color-accent: #0ea5e9; + --color-border: #e2e8f0; + } + `; + } +} diff --git a/src/modules/wrapped/infrastructure/themes/ValentineThemeStrategy.ts b/src/modules/wrapped/infrastructure/themes/ValentineThemeStrategy.ts new file mode 100644 index 0000000..9070cba --- /dev/null +++ b/src/modules/wrapped/infrastructure/themes/ValentineThemeStrategy.ts @@ -0,0 +1,182 @@ +import { AbstractThemeStrategy } from "./AbstractThemeStrategy"; +import type { SeasonWrapped } from "../../domain/SeasonWrapped"; +import type { ThemePhrases } from "../../domain/IThemeStrategy"; + +export class ValentineThemeStrategy extends AbstractThemeStrategy { + getName(): string { + return "valentines"; + } + + getStylesheet(): string { + return ` + :root { + --bg-base: #0f0506; /* Very dark deep red */ + --bg-card: #1a0a0b; /* Slightly lighter dark red surface */ + --text-primary: #fee2e2; /* Very light pink/near white */ + --text-secondary: #fda4af; /* Soft pink */ + --text-muted: #9f1239; /* Deep rose */ + --color-accent: #e11d48; /* Vibrant red */ + --border-subtle: rgba(225, 29, 72, 0.1); + --border-medium: rgba(225, 29, 72, 0.3); + --color-accent-rgb: 225, 29, 72; + } + + .page { + background: var(--bg-base) !important; + color: var(--text-primary); + page-break-after: always !important; + } + + .page-bg-decoration { + opacity: 0.15 !important; + filter: hue-rotate(340deg) brightness(0.8) saturate(1.5) !important; + mix-blend-mode: screen !important; + } + + .card-container { + background: var(--bg-card) !important; + border: 1px solid var(--border-subtle) !important; + box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5); + } + + .rival-card { + background: linear-gradient(135deg, rgba(26, 10, 11, 0.9) 0%, rgba(31, 12, 13, 0.9) 100%) !important; + border: 2px solid rgba(225, 29, 72, 0.4) !important; + /* Removed backdrop-filter due to PDF generation issues */ + } + + .page-title::after { + background: var(--color-accent) !important; + box-shadow: 0 0 15px var(--color-accent); + } + + .header-bar { + border-bottom-color: var(--border-subtle) !important; + } + + .chapter-super { + color: var(--color-accent) !important; + text-shadow: 0 0 10px rgba(225, 29, 72, 0.5); + } + + h1, h2, h3, .page-title { + color: #fda4af !important; /* Soft light pink for titles on dark bg */ + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); + } + + .pill { + background: rgba(225, 29, 72, 0.1) !important; + color: #fda4af !important; + border: 1px solid rgba(225, 29, 72, 0.2); + } + + .progress-track { + background: rgba(0, 0, 0, 0.3) !important; + } + `; + } + + getBackground(): string { + return this.getImageAsBase64("black_rose_dragon.png"); + } + + getPhrases(data: SeasonWrapped): ThemePhrases { + const base = super.getPhrases(data); + + const lovePhrases = [ + "¡Si aún nadie te lo ha dicho, feliz día del amor y la amistad!", + "Más que un duelista, eres un rompecorazones.", + "Tu Deck y tú: Una historia de amor mejor que Crepúsculo.", + "Activaste mi carta trampa: ¡Amor Incondicional!", + "¿Tu corazón tiene 8000 LP? Porque el mío bajó a 0 al verte jugar.", + "Ni el Dragón Blanco de Ojos Azules brilla tanto como tu sonrisa (o tu winrate).", + "Eres el 'Polimerización' de mi vida: nos haces uno solo.", + "Si fueras una carta, serías Prohibida... por exceso de facha.", + "Mi Deck late por ti más fuerte que un combo de 20 minutos.", + "No necesito el Corazón de las Cartas si tengo el tuyo." + ]; + + const statsPhrases = [ + "Tus números enamoran (aunque a los Ban Lists no tanto).", + "Duelista por fuera, poeta por dentro.", + "Repartiendo amor y combos por igual.", + "Tus Life Points bajan, pero mi cariño por tus jugadas sube.", + "¿Quién necesita Tinder si tienes este Win Rate?", + "Tus victorias son la flecha de Cupido en mi ranking.", + "Analizando tu pasión: 50% Skill, 50% Suerte, 100% Amor.", + "Incluso Exodia envidia lo completo que eres.", + "Trazando el camino del amor... un duelo a la vez.", + "Tus estadísticas dicen: ¡CÁSATE CONMIGO! (o al menos jueguen otra)." + ]; + + const rivalPhrases = [ + "Love is a Battlefield... y aquí perdiste contra este.", + "Tu Archi-Rival o tu 'Enemies to Lovers' arc.", + "Relación complicada: Se dan con todo en el campo.", + "Tu media naranja... de destrucción masiva.", + "Tóxicos, pero apasionados. El duelo nunca termina.", + "¿Rivalidad o tensión sexual? El log no miente.", + "Ni el odio ni el amor son tan fuertes como este 2-0.", + "Tu destino está ligado a este duelista... por los siglos de los siglos.", + "El roce hace el cariño... y las negaciones hacen el drama.", + "Tu amor platónico (porque platónicamente lo quieres ver fuera del torneo)." + ]; + + const achievementPhrases = [ + "Coleccionando triunfos y suspiros.", + "Logros que llegan directo al corazón.", + "Tu legado es puro amor al arte del duelo.", + "Brillando más que una carta holográfica en San Valentín.", + "Desbloqueaste el logro más difícil: ¡Caerle bien a todos!", + "Tus trofeos son los pétalos de una rosa de victoria.", + "Logros obtenidos con sudor, lágrimas y mucho cariño.", + "Cada medalla es un 'te quiero' de la comunidad.", + "Tu vitrina está llena, pero siempre hay espacio para más amor.", + "Nivel de Duelista: Enamorado de la victoria." + ]; + + const summaryPhrases = [ + "¡Nos vemos en el próximo duelo, Cupido de las cartas!", + "Que tus robos sean siempre de corazón.", + "Duelo terminado, pero el amor por el juego sigue.", + "Sigue robando corazones (y victorias).", + "Game Over? No, ¡Love Start!", + "Gracias por compartir tu pasión con nosotros.", + "Tu viaje continúa, ¡llénalo de duelos y abrazos!", + "Recuerda: la mejor jugada es la que se hace con amigos.", + "Nos vemos en el próximo turno de la vida.", + "¡Hasta la próxima, leyenda del romance!" + ]; + + const coverTitles = [ + "EVOLUTION VALENTINE", + "CORAZÓN DE LAS CARTAS (Y DEL MÍO)", + "8000 LP DE PURA PASIÓN", + "LOVE IS A BATTLEFIELD (CON TRAPAS)", + "TÚ, YO Y UN DUELO NOCHE", + "¡TE ELIJO A TI! (ESPERA, JUEGO EQUIVOCADO)", + "ROBANDO EL CORAZÓN DE LAS CARTAS", + "CUPIDO DUELISTA: EDICIÓN LIMITADA", + "MI DECK DE AMOR: TOP TIER", + "ROMPECORAZONES EN TURNO 1", + "AMOR A PRIMERA JUGADA", + "DIME QUE ME AMAS (O QUE NO TIENES ASH)" + ]; + + const random = (arr: string[]) => arr[Math.floor(Math.random() * arr.length)]; + + return { + ...base, + coverTitle: random(coverTitles), + coverSubtitle: random(lovePhrases), + statsTitle: "PASIÓN POR EL DUELO", + statsSubtitle: random(statsPhrases), + rivalsTitle: "LOVE & WAR", + rivalsSubtitle: random(rivalPhrases), + achievementsTitle: "TU LEGADO DE AMOR", + achievementsSubtitle: random(achievementPhrases), + summaryTitle: "RESUMEN CON AMOR", + summarySubtitle: random(summaryPhrases) + }; + } +} diff --git a/src/server/routes/reports-router.ts b/src/server/routes/reports-router.ts deleted file mode 100644 index 2480695..0000000 --- a/src/server/routes/reports-router.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { bearer } from "@elysiajs/bearer"; -import { Elysia } from "elysia"; -import { ReportsController } from "../../modules/reports/infrastructure/ReportsController"; -import { JWT } from "../../shared/JWT"; -import { config } from "../../config"; -import { AuthenticationError } from "../../shared/errors/AuthenticationError"; - -const jwt = new JWT(config.jwt); - -export const reportsRouter = new Elysia() - .group("/reports", (app) => - app - .use(bearer()) - .get("/wrapped", async (context) => { - const token = context.bearer; - if (!token) { - throw new AuthenticationError("No token provided"); - } - const decoded = jwt.decode(token) as { id: string } | null; - if (!decoded || !decoded.id) { - throw new AuthenticationError("Invalid token"); - } - - return new ReportsController().getWrapped({ user: { profile: { id: decoded.id } } }); - }, { - detail: { - tags: ['Reports'], - summary: 'Get Player Wrapped Report', - description: 'Generates a PDF report of player statistics for seasons 3, 4, and 5.', - security: [{ bearerAuth: [] }], - responses: { - 200: { - description: 'PDF Report retrieved successfully', - content: { - 'application/pdf': {} - } - }, - 401: { description: 'Unauthorized' } - } - } - }) - ); diff --git a/src/server/routes/wrapped-router.ts b/src/server/routes/wrapped-router.ts index 388e450..9bbb9c4 100644 --- a/src/server/routes/wrapped-router.ts +++ b/src/server/routes/wrapped-router.ts @@ -40,7 +40,7 @@ export const wrappedRouter = new Elysia() .use( rateLimit({ duration: 60000, - max: 10, + max: 100, }) ) // HTML endpoint @@ -59,13 +59,24 @@ export const wrappedRouter = new Elysia() }); const { renderTemplate } = await import("../../modules/wrapped/infrastructure/templates/templateRenderer"); + const { ThemeStrategyFactory } = await import("../../modules/wrapped/application/ThemeStrategyFactory"); + const { DarkThemeStrategy } = await import("../../modules/wrapped/infrastructure/themes/DarkThemeStrategy"); + const { LightThemeStrategy } = await import("../../modules/wrapped/infrastructure/themes/LightThemeStrategy"); + const { ValentineThemeStrategy } = await import("../../modules/wrapped/infrastructure/themes/ValentineThemeStrategy"); + + const themeFactory = new ThemeStrategyFactory(); + themeFactory.register("dark", new DarkThemeStrategy()); + themeFactory.register("light", new LightThemeStrategy()); + themeFactory.register("valentines", new ValentineThemeStrategy()); + + const strategy = themeFactory.get(query.theme || "dark"); + const locale = (query.locale || "es") as string; - const theme = (query.theme === "light" ? "light" : "dark") as "dark" | "light"; const html = renderTemplate(result, { locale, - theme, + theme: query.theme || "dark", includeMatchList: false, - }); + }, strategy); set.headers["Content-Type"] = "text/html"; return html; @@ -117,102 +128,6 @@ export const wrappedRouter = new Elysia() } } ) - // PDF endpoint - .get( - "/:seasonId/wrapped/:playerId/pdf", - async ({ params, query, bearer, set }) => { - try { - // Authorization: Only owner or admin can access - authorizeWrappedAccess(bearer, params.playerId); - - const result = await controller.generatePdf({ - params: { - seasonId: params.seasonId, - playerId: params.playerId, - }, - query: { - locale: query.locale, - theme: query.theme as "dark" | "light" | undefined, - includeMatchList: query.includeMatchList, - singlePage: query.singlePage, - }, - }); - - // Set proper headers for PDF response - set.status = 200; - set.headers["Content-Type"] = "application/pdf"; - - // Sanitize filename to prevent header injection or filesystem issues - const safePlayerName = result.playerName.replace(/[^a-z0-9]/gi, '_').toLowerCase(); - const filename = `${safePlayerName}-season-${result.seasonId}-wrapped.pdf`; - - set.headers["Content-Disposition"] = `inline; filename="${filename}"`; - - // Caching headers (1 hour cache) - set.headers["Cache-Control"] = "public, max-age=3600"; - set.headers["Last-Modified"] = new Date().toUTCString(); - - // ETag for conditional requests - const etag = `"${result.seasonId}-${result.playerId}-${query.locale || 'es'}-${query.theme || 'dark'}"`; - set.headers["ETag"] = etag; - - return result.pdf; - } catch (error) { - if (error instanceof UnauthorizedError) { - set.status = 401; - return { - error: "UnauthorizedError", - message: error.message - }; - } - - if (error instanceof ValidationError) { - set.status = 422; - return { - error: "ValidationError", - message: error.message, - details: { - seasonId: params.seasonId, - playerId: params.playerId - } - }; - } - - if (error instanceof NotFoundError) { - set.status = 404; - return { - error: "NotFoundError", - message: error.message - }; - } - - // Internal server error - console.error("PDF generation error:", error); - set.status = 500; - return { - error: "InternalServerError", - message: "Failed to generate PDF" - }; - } - }, - { - params: t.Object({ - seasonId: t.String({ description: "ID of the season" }), - playerId: t.String({ description: "UUID of the player" }), - }), - query: t.Object({ - locale: t.Optional(t.String({ description: "Language code (es, en)", default: "es" })), - theme: t.Optional(t.String({ description: "Color theme (dark, light)", default: "dark" })), - includeMatchList: t.Optional(t.String({ description: "Include match history? (true/false)", default: "false" })), - singlePage: t.Optional(t.String({ description: "Render as single page for debugging?", default: "false" })), - }), - detail: { - tags: ["Wrapped"], - summary: "Download Season Wrapped PDF", - description: "Generates and returns a PDF report of the player's season stats. Protected: Owner or Admin only.", - } - } - ) // JSON endpoint (for debugging) .get( diff --git a/src/server/server.ts b/src/server/server.ts index ec5f42e..dde6722 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -14,7 +14,7 @@ import { statsRouter } from "./routes/stats-router"; import { tournamentRouter } from "./routes/tournament-router"; import { userRouter } from "./routes/user-router"; import { wrappedRouter } from "./routes/wrapped-router"; -import { reportsRouter } from "./routes/reports-router"; + export class Server { private readonly app: Elysia; @@ -115,14 +115,14 @@ export class Server { .use(tournamentRouter) .use(statsRouter) .use(wrappedRouter) - .use(reportsRouter); + }); this.logger = logger; } start(): void { - this.app.listen(process.env.PORT ?? 3000, () => - this.logger.info(`Server started on port ${process.env.PORT ?? 3000}`), - ); + this.app.listen(process.env.PORT ?? 3000, () => { + this.logger.info(`Server started on port ${process.env.PORT ?? 3000}!`); + }); } }