diff --git a/package-lock.json b/package-lock.json
index ff371ab..4413c93 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -67,6 +67,7 @@
"devDependencies": {
"@tailwindcss/vite": "4.1.12",
"@vitejs/plugin-react": "4.7.0",
+ "puppeteer": "^25.1.0",
"tailwindcss": "4.1.12",
"vite": "6.3.5"
},
@@ -113,7 +114,6 @@
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
@@ -430,7 +430,6 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -474,7 +473,6 @@
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -1100,7 +1098,6 @@
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz",
"integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/core-downloads-tracker": "^7.3.5",
@@ -1298,12 +1295,36 @@
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
- "peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
+ "node_modules/@puppeteer/browsers": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.4.tgz",
+ "integrity": "sha512-HGM8iAmGTf+Y7t0373szVbTmt3d7vPkYL/1bpOkOFO0YUYLgSeuYBCzESklogNPvOBnZ/MRD5f07OkpqH1trtA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "modern-tar": "^0.7.6",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "browsers": "lib/main-cli.js"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ },
+ "peerDependencies": {
+ "proxy-agent": ">=8.0.1"
+ },
+ "peerDependenciesMeta": {
+ "proxy-agent": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/number": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
@@ -3365,16 +3386,6 @@
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"license": "MIT"
},
- "node_modules/@types/react": {
- "version": "19.2.17",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
- "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "csstype": "^3.2.2"
- }
- },
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
@@ -3405,6 +3416,32 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
"node_modules/aria-hidden": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
@@ -3465,7 +3502,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -3530,6 +3566,23 @@
"node": ">=18"
}
},
+ "node_modules/chromium-bidi": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz",
+ "integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "mitt": "^3.0.1",
+ "zod": "^3.24.1"
+ },
+ "engines": {
+ "node": ">=20.19.0 <22.0.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "devtools-protocol": "*"
+ }
+ },
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@@ -3548,6 +3601,21 @@
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT"
},
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -3573,6 +3641,26 @@
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
@@ -3749,7 +3837,6 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"license": "MIT",
- "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
@@ -3794,6 +3881,13 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
+ "node_modules/devtools-protocol": {
+ "version": "0.0.1624250",
+ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1624250.tgz",
+ "integrity": "sha512-YFAat/lOiIk0ARmBweG+ygrEcbZrq5B9urRyUoeQKp53MlidHXE2TmTbxKcaXoQj7u/aX+jebDO4BW55rs0WwA==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/dnd-core": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
@@ -3826,8 +3920,7 @@
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/embla-carousel-react": {
"version": "8.6.0",
@@ -3851,6 +3944,13 @@
"embla-carousel": "8.6.0"
}
},
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/enhanced-resolve": {
"version": "5.24.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz",
@@ -4053,6 +4153,16 @@
"node": ">=6.9.0"
}
},
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
@@ -4152,6 +4262,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/jiti": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
@@ -4447,6 +4567,19 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -4529,6 +4662,23 @@
"node": ">= 18"
}
},
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/modern-tar": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz",
+ "integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/motion": {
"version": "12.23.24",
"resolved": "https://registry.npmjs.org/motion/-/motion-12.23.24.tgz",
@@ -4681,7 +4831,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -4735,17 +4884,44 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
- "node_modules/react": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
- "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
- "license": "MIT",
- "peer": true,
+ "node_modules/puppeteer": {
+ "version": "25.1.0",
+ "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.1.0.tgz",
+ "integrity": "sha512-7L6/0JM7XStK99lIL4xQySyNEXNfII6pk0BxkI5kKBTOhR7AsoQiv067YTsE/rIXxQiq9ajlO4WcqBjS/FWK1A==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
"dependencies": {
- "loose-envify": "^1.1.0"
+ "@puppeteer/browsers": "3.0.4",
+ "chromium-bidi": "16.0.1",
+ "devtools-protocol": "0.0.1624250",
+ "lilconfig": "^3.1.3",
+ "puppeteer-core": "25.1.0",
+ "typed-query-selector": "^2.12.2"
+ },
+ "bin": {
+ "puppeteer": "lib/puppeteer/node/cli.js"
},
"engines": {
- "node": ">=0.10.0"
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/puppeteer-core": {
+ "version": "25.1.0",
+ "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.1.0.tgz",
+ "integrity": "sha512-jKzy5y4WG6uNuFbTWgW1D7mqoT9o0nllc/6a1DGF775T1mPmgw3scdFEtEq67yVFikavQmbYq6NLfbTfxHSlqQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@puppeteer/browsers": "3.0.4",
+ "chromium-bidi": "16.0.1",
+ "devtools-protocol": "0.0.1624250",
+ "typed-query-selector": "^2.12.2",
+ "webdriver-bidi-protocol": "0.4.2",
+ "ws": "^8.21.0"
+ },
+ "engines": {
+ "node": ">=22.12.0"
}
},
"node_modules/react-day-picker": {
@@ -4801,20 +4977,6 @@
"dnd-core": "^16.0.1"
}
},
- "node_modules/react-dom": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
- "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "loose-envify": "^1.1.0",
- "scheduler": "^0.23.2"
- },
- "peerDependencies": {
- "react": "^18.3.1"
- }
- },
"node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
@@ -5070,6 +5232,16 @@
"@babel/runtime": "^7.9.2"
}
},
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
@@ -5151,15 +5323,6 @@
"fsevents": "~2.3.2"
}
},
- "node_modules/scheduler": {
- "version": "0.23.2",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
- "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.1.0"
- }
- },
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -5211,6 +5374,34 @@
"integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
"license": "MIT"
},
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
@@ -5325,6 +5516,13 @@
"url": "https://github.com/sponsors/Wombosvideo"
}
},
+ "node_modules/typed-query-selector": {
+ "version": "2.12.2",
+ "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
+ "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -5440,7 +5638,6 @@
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -5519,6 +5716,63 @@
"loose-envify": "^1.0.0"
}
},
+ "node_modules/webdriver-bidi-protocol": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz",
+ "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -5526,22 +5780,43 @@
"dev": true,
"license": "ISC"
},
- "node_modules/yaml": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
- "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
- "license": "ISC",
- "optional": true,
- "peer": true,
- "bin": {
- "yaml": "bin.mjs"
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
},
"engines": {
- "node": ">= 14.6"
- },
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "dev": true,
+ "license": "MIT",
"funding": {
- "url": "https://github.com/sponsors/eemeli"
+ "url": "https://github.com/sponsors/colinhacks"
}
}
}
diff --git a/package.json b/package.json
index fad7042..6dcb0f4 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"version": "0.0.1",
"type": "module",
"scripts": {
- "build": "vite build",
+ "build": "vite build && node scripts/prerender.js",
"dev": "vite"
},
"dependencies": {
@@ -36,8 +36,8 @@
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-switch": "1.1.3",
"@radix-ui/react-tabs": "1.1.3",
- "@radix-ui/react-toggle-group": "1.1.2",
"@radix-ui/react-toggle": "1.1.2",
+ "@radix-ui/react-toggle-group": "1.1.2",
"@radix-ui/react-tooltip": "1.1.8",
"canvas-confetti": "1.9.4",
"class-variance-authority": "0.7.1",
@@ -67,6 +67,7 @@
"devDependencies": {
"@tailwindcss/vite": "4.1.12",
"@vitejs/plugin-react": "4.7.0",
+ "puppeteer": "^25.1.0",
"tailwindcss": "4.1.12",
"vite": "6.3.5"
},
@@ -87,4 +88,4 @@
"vite": "6.3.5"
}
}
-}
\ No newline at end of file
+}
diff --git a/scripts/prerender.js b/scripts/prerender.js
new file mode 100644
index 0000000..f35bc31
--- /dev/null
+++ b/scripts/prerender.js
@@ -0,0 +1,161 @@
+#!/usr/bin/env node
+
+/**
+ * Pre-rendering script for static HTML generation
+ * Runs after `vite build` to generate static HTML files for better SEO
+ */
+
+import puppeteer from 'puppeteer';
+import fs from 'fs';
+import path from 'path';
+import http from 'http';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const DIST_DIR = path.join(__dirname, '../dist');
+const PORT = 3000;
+const ROUTES = ['/', '404.html'];
+
+// Simple HTTP server to serve dist files
+function startServer(port) {
+ return new Promise((resolve) => {
+ const server = http.createServer((req, res) => {
+ // Normalize the URL pathname to prevent directory traversal
+ let urlPath = new URL(req.url, `http://localhost:${port}`).pathname;
+ urlPath = path.normalize(urlPath);
+
+ // Default to index.html for root or 404.html
+ if (urlPath === '/' || urlPath === '/404.html') {
+ urlPath = '/index.html';
+ }
+
+ // Resolve the full file path
+ let filePath = path.resolve(DIST_DIR, '.' + urlPath);
+ const distDirResolved = path.resolve(DIST_DIR);
+
+ // Validate that the resolved path is within DIST_DIR (prevent directory traversal)
+ if (!filePath.startsWith(distDirResolved)) {
+ res.writeHead(403);
+ res.end('Forbidden');
+ return;
+ }
+
+ fs.readFile(filePath, (err, data) => {
+ if (err) {
+ res.writeHead(404);
+ res.end('Not found');
+ return;
+ }
+
+ const ext = path.extname(filePath);
+ const contentType = {
+ '.html': 'text/html',
+ '.js': 'application/javascript',
+ '.css': 'text/css',
+ '.json': 'application/json',
+ '.png': 'image/png',
+ '.jpg': 'image/jpeg',
+ '.svg': 'image/svg+xml',
+ '.woff': 'font/woff',
+ '.woff2': 'font/woff2',
+ }[ext] || 'text/plain';
+
+ res.writeHead(200, { 'Content-Type': contentType });
+ res.end(data);
+ });
+ });
+
+ server.listen(port, () => {
+ console.log(`[Prerender] Server started on http://localhost:${port}`);
+ resolve(server);
+ });
+ });
+}
+
+async function prerender() {
+ console.log('[Prerender] Starting pre-rendering process...');
+
+ if (!fs.existsSync(DIST_DIR)) {
+ console.error(`[Prerender] Error: dist directory not found at ${DIST_DIR}`);
+ process.exit(1);
+ }
+
+ let server;
+ let browser;
+
+ try {
+ // Start server
+ server = await startServer(PORT);
+
+ // Launch browser
+ browser = await puppeteer.launch({ headless: 'new' });
+ console.log('[Prerender] Browser launched');
+
+ // Track failed routes for error reporting
+ const failedRoutes = [];
+
+ // Pre-render each route
+ for (const route of ROUTES) {
+ // Skip 404.html as it's not a real route
+ if (route === '404.html') {
+ continue;
+ }
+
+ const routePath = route === '/' ? '/index.html' : `/${route}`;
+ const url = `http://localhost:${PORT}${route}`;
+
+ try {
+ const page = await browser.newPage();
+
+ // Set viewport to ensure consistent rendering
+ await page.setViewport({ width: 1280, height: 800 });
+
+ // Wait for page to fully load
+ await page.goto(url, { waitUntil: 'networkidle2' });
+ console.log(`[Prerender] Rendered: ${route}`);
+
+ // Get HTML content
+ const html = await page.content();
+
+ // Save to file
+ const outputPath = path.join(DIST_DIR, routePath);
+ const outputDir = path.dirname(outputPath);
+
+ if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ }
+
+ fs.writeFileSync(outputPath, html);
+ console.log(`[Prerender] Saved: ${outputPath}`);
+
+ await page.close();
+ } catch (error) {
+ console.error(`[Prerender] Error rendering ${route}:`, error.message);
+ failedRoutes.push(route);
+ }
+ }
+
+ // Check for failed routes and exit with error if any occurred
+ if (failedRoutes.length > 0) {
+ throw new Error(`Pre-rendering failed for routes: ${failedRoutes.join(', ')}`);
+ }
+
+ console.log('[Prerender] ✓ Pre-rendering completed successfully');
+ } catch (error) {
+ console.error('[Prerender] Fatal error:', error);
+ process.exit(1);
+ } finally {
+ // Cleanup
+ if (browser) {
+ await browser.close();
+ }
+ if (server) {
+ server.close();
+ }
+ }
+}
+
+prerender();
+
diff --git a/src/main.tsx b/src/main.tsx
index 681bd5d..6b6ffc1 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -3,8 +3,12 @@ import { BrowserRouter } from "react-router";
import App from "./app/App.tsx";
import "./styles/index.css";
-createRoot(document.getElementById("root")!).render(
-
-
-
-);
\ No newline at end of file
+const root = document.getElementById("root");
+
+if (root) {
+ createRoot(root).render(
+
+
+
+ );
+}