diff --git a/package-lock.json b/package-lock.json index d4695fc..255125d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,10 @@ "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", "framer-motion": "^12.23.24", + "html2canvas": "^1.4.1", + "html2canvas-pro": "^1.6.6", "input-otp": "^1.4.2", + "jspdf": "^4.0.0", "lucide": "^0.544.0", "lucide-react": "^0.545.0", "next": "^16.0.7", @@ -220,6 +223,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -236,6 +240,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -252,6 +257,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -268,6 +274,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -284,6 +291,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -300,6 +308,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -316,6 +325,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -332,6 +342,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -348,6 +359,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -364,6 +376,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -380,6 +393,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -396,6 +410,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -412,6 +427,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -428,6 +444,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -444,6 +461,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -460,6 +478,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -476,6 +495,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -492,6 +512,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -508,6 +529,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -524,6 +546,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -540,6 +563,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -556,6 +580,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -622,6 +647,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -638,6 +664,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -654,6 +681,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -670,6 +698,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -686,6 +715,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -702,6 +732,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -718,6 +749,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -734,6 +766,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -750,6 +783,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -766,6 +800,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -782,6 +817,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -798,6 +834,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -814,6 +851,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -830,6 +868,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -846,6 +885,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -862,6 +902,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -878,6 +919,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -894,6 +936,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -910,6 +953,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -926,6 +970,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -942,6 +987,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -958,6 +1004,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -974,6 +1021,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -990,6 +1038,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1006,6 +1055,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1022,6 +1072,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4185,6 +4236,12 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, "node_modules/@types/pg": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", @@ -4196,6 +4253,13 @@ "pg-types": "^2.2.0" } }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/react": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", @@ -4216,6 +4280,13 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.53.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz", @@ -5427,6 +5498,15 @@ "node": ">= 0.4" } }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -6168,6 +6248,26 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/cfb": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", @@ -6659,6 +6759,18 @@ "node": ">=0.10.0" } }, + "node_modules/core-js": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", + "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -6769,6 +6881,15 @@ "node": ">=8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -7490,6 +7611,16 @@ "npm": ">=1.2" } }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -8917,6 +9048,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, + "node_modules/fast-png/node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/fast-sha256": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", @@ -8983,6 +9131,12 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -9962,6 +10116,32 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/html2canvas-pro": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/html2canvas-pro/-/html2canvas-pro-1.6.6.tgz", + "integrity": "sha512-5mRhTXZhv4B0kIcsn3bFBjol2o8vzP35mhtxdXBGPA3V3gZd6Sa2PIIFbT//DiqAX8UuywlcJit5jRKej4nV4Q==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -10170,6 +10350,12 @@ "node": ">=12" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/is-accessor-descriptor": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", @@ -10911,6 +11097,23 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jspdf": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.0.0.tgz", + "integrity": "sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.2.4", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -12973,6 +13176,13 @@ "node": ">= 0.10" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/pg": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.1.tgz", @@ -13502,6 +13712,16 @@ ], "license": "MIT" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -13980,6 +14200,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -14243,6 +14470,16 @@ "dev": true, "license": "MIT" }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -15071,6 +15308,16 @@ "figgy-pudding": "^3.5.1" } }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/standardwebhooks": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", @@ -15456,6 +15703,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/svix": { "version": "1.84.1", "resolved": "https://registry.npmjs.org/svix/-/svix-1.84.1.tgz", @@ -15575,6 +15832,15 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -16396,6 +16662,15 @@ "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", "license": "ISC" }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", diff --git a/package.json b/package.json index 42dd7ec..fdb72f7 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,10 @@ "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", "framer-motion": "^12.23.24", + "html2canvas": "^1.4.1", + "html2canvas-pro": "^1.6.6", "input-otp": "^1.4.2", + "jspdf": "^4.0.0", "lucide": "^0.544.0", "lucide-react": "^0.545.0", "next": "^16.0.7", diff --git a/src/app/graphs/page.tsx b/src/app/graphs/page.tsx index f2d84b0..1f679d6 100644 --- a/src/app/graphs/page.tsx +++ b/src/app/graphs/page.tsx @@ -20,7 +20,7 @@ import { Link, Share, } from "lucide-react"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import BarGraph, { type BarDataset } from "@/components/BarGraph"; import { Breadcrumbs } from "@/components/Breadcrumbs"; @@ -35,6 +35,7 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { downloadGraph } from "@/lib/export-to-pdf"; // define Project type type Project = { @@ -89,6 +90,7 @@ const groupByLabels: Record = { export default function GraphsPage() { const [allProjects, setAllProjects] = useState([]); const [filters, setFilters] = useState(defaultFilters); + //const [isExporting, setIsExporting] = useState(false); potentially for loading stat const [gatewayCities, setGatewayCities] = useState([]); const [chartType, setChartType] = useState<"line" | "bar">("line"); const [timePeriod, setTimePeriod] = useState< @@ -103,6 +105,7 @@ export default function GraphsPage() { start: 2020, end: 2025, }); + const svgRef = useRef(null); // Fetch all project data useEffect(() => { @@ -398,6 +401,7 @@ export default function GraphsPage() { variant="outline" size="sm" className="flex items-center gap-2" + onClick={() => downloadGraph(svgRef)} > Export @@ -589,6 +593,7 @@ export default function GraphsPage() { measuredAsLabels[filters.measuredAs] } xAxisLabel={groupByLabels[filters.groupBy]} + svgRefCopy={svgRef} /> ) : ( )} diff --git a/src/app/signin/page.tsx b/src/app/signin/page.tsx index 6255d92..b2a3c7f 100644 --- a/src/app/signin/page.tsx +++ b/src/app/signin/page.tsx @@ -1,4 +1,5 @@ import AuthForm from "@/components/AuthForm"; +import Dashboard from "@/components/Dashboard"; import WarpShader from "@/components/WarpShader"; export default function SignInPage() { diff --git a/src/components/BarGraph.tsx b/src/components/BarGraph.tsx index 70a206d..5405b6e 100644 --- a/src/components/BarGraph.tsx +++ b/src/components/BarGraph.tsx @@ -23,17 +23,22 @@ type BarGraphProps = { dataset: BarDataset[]; yAxisLabel: string; xAxisLabel: string; + svgRefCopy: React.RefObject; }; export default function BarGraph({ dataset, yAxisLabel, xAxisLabel, + svgRefCopy, }: BarGraphProps) { const svgRef = useRef(null); // Use same color scheme as LineGraph - const colorScale = d3.scaleOrdinal(d3.schemeCategory10); + //const colorScale = d3.scaleOrdinal(d3.schemeCategory10); + const colorScale = d3.scaleOrdinal( + d3.schemeCategory10.map((c) => c.toString()), + ); useEffect(() => { if (!svgRef.current || dataset.length === 0) return; @@ -181,6 +186,8 @@ export default function BarGraph({ xOffset += itemWidth; return transform; }); + + svgRefCopy.current = svgRef.current; }, [dataset]); return ( diff --git a/src/components/ConditionalLayout.tsx b/src/components/ConditionalLayout.tsx index 509526a..b870882 100644 --- a/src/components/ConditionalLayout.tsx +++ b/src/components/ConditionalLayout.tsx @@ -9,13 +9,14 @@ export default function ConditionalLayout({ }: { children: React.ReactNode; }) { - const pathname = usePathname(); - const isAuthPage = pathname === "/signin"; + //const pathname = usePathname(); + + //const isAuthPage = pathname === "/signin"; // If on auth pages, just render children without sidebar - if (isAuthPage) { - return
{children}
; - } + // if (isAuthPage) { + // return
{children}
; + // } // Otherwise, render with sidebar and responsive layout return ( diff --git a/src/components/LineGraph.tsx b/src/components/LineGraph.tsx index a625c6d..da00b44 100644 --- a/src/components/LineGraph.tsx +++ b/src/components/LineGraph.tsx @@ -12,12 +12,14 @@ type MultiLineGraphProps = { datasets: GraphDataset[]; yAxisLabel: string; xAxisLabel: string; + svgRefCopy: React.RefObject; }; export default function MultiLineGraph({ datasets, yAxisLabel, xAxisLabel, + svgRefCopy, }: MultiLineGraphProps) { const svgRef = useRef(null); const wrapperRef = useRef(null); @@ -39,7 +41,11 @@ export default function MultiLineGraph({ }>({}); // Memoize color scale to prevent re-running useEffect unnecessarily - const colorScale = useMemo(() => d3.scaleOrdinal(d3.schemeCategory10), []); + //const colorScale = useMemo(() => d3.scaleOrdinal(d3.schemeCategory10), []); + const colorScale = useMemo( + () => d3.scaleOrdinal(d3.schemeCategory10.map((c) => c.toString())), + [], + ); useEffect(() => { const svg = d3.select(svgRef.current); @@ -390,6 +396,8 @@ export default function MultiLineGraph({ return transform; }); }); + + svgRefCopy.current = svgRef.current; }, [datasets, xAxisLabel, yAxisLabel, colorScale]); return ( diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 08e9026..c226cea 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -13,12 +13,12 @@ import { ChevronRight, User, } from "lucide-react"; -import { authClient } from "@/lib/auth-client"; +//import { authClient } from "@/lib/auth-client"; export default function Sidebar() { const pathname = usePathname(); const [isOverviewOpen, setIsOverviewOpen] = useState(false); - const { data: session } = authClient.useSession(); + //const { data: session } = authClient.useSession(); // Automatically open Overview if any subitem is active useEffect(() => { @@ -203,7 +203,7 @@ export default function Sidebar() { -
+ {/*
{session?.user?.image ? ( {session?.user?.email || "Loading..."} -
+
*/} ); } diff --git a/src/lib/export-to-pdf.ts b/src/lib/export-to-pdf.ts new file mode 100644 index 0000000..6e2b9a6 --- /dev/null +++ b/src/lib/export-to-pdf.ts @@ -0,0 +1,76 @@ +/*************************************************************** + * + * /src/lib/export-to-pdf.ts + * + * Author: Will and Justin + * Date: 2/1/2025 + * + * Summary: Export an svg graph as a pdf + **************************************************************/ + +import React from "react"; +import html2canvas from "html2canvas-pro"; +import jsPDF from "jspdf"; + +export async function downloadGraph( + svgRef: React.RefObject, +) { + const newSVG = getClonedSvg(svgRef); + if (!newSVG) return; + + // Get SVG dimensions from viewBox or attributes with fallbacks + const viewBox = newSVG.getAttribute("viewBox"); + let svgWidth = 1000; + let svgHeight = 400; + let aspectRatio = svgHeight / svgWidth; + + if (viewBox) { + const viewBoxValues = viewBox.split(" "); + svgWidth = parseFloat(viewBoxValues[2]) || 1000; + svgHeight = parseFloat(viewBoxValues[3]) || 400; + } else { + const width = newSVG.getAttribute("width"); + const height = newSVG.getAttribute("height"); + if (width) svgWidth = parseFloat(width) || 1000; + if (height) svgHeight = parseFloat(height) || 400; + } + + aspectRatio = svgHeight / svgWidth; + + // Adds the svg element to the page temporarily (offscreen) + const wrapper = document.createElement("div"); + wrapper.style.position = "fixed"; + wrapper.style.left = "-9999px"; + wrapper.style.top = "-9999px"; + wrapper.style.width = `${svgWidth}px`; + wrapper.style.height = `${svgHeight}px`; + wrapper.appendChild(newSVG); + document.body.append(wrapper); + + const canvas = await html2canvas(wrapper, { + backgroundColor: "#fff", + scale: 2, + }); + + const pdf = new jsPDF(); + const pdfWidth = pdf.internal.pageSize.getWidth(); + const pdfHeight = pdfWidth * aspectRatio; + + // Add image with proper dimensions that match PDF width while preserving aspect ratio + pdf.addImage(canvas, "JPEG", 0, 0, pdfWidth, pdfHeight); + pdf.save("graph.pdf"); + + document.body.removeChild(wrapper); +} + +export function getClonedSvg( + svgRef: React.RefObject, +): SVGSVGElement | null { + const original = svgRef.current; + if (!original) return null; + + //Creates and returns a clone of the svg element passed in + const clone = original.cloneNode(true) as SVGSVGElement; + + return clone; +}