diff --git a/README.md b/README.md index 4571b34..84c9b43 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Why this Project? https://github.com/user-attachments/assets/78b027fa-9883-4813-8086-8b6aa19767de + ## Features - ๐Ÿ“ˆ Real-time stock data from Yahoo Finance @@ -21,18 +22,32 @@ https://github.com/user-attachments/assets/78b027fa-9883-4813-8086-8b6aa19767de - ๐Ÿ“Š Interactive charts with performance visualization - ๐ŸŒ“ Dark/Light mode support - ๐Ÿ“ฑ Responsive design + - *Mobile friendly* - ๐Ÿ“… Historical data analysis + - *The portfolio is fully based on real-historical data, with configurable timeranges* - ๐Ÿ’น TTWOR (Time Travel Without Risk) calculations + - *Including metrics for TTWOR* - ๐Ÿ”„ Support for one-time and periodic investments + - *You can config your dream-portfolio by one time and periodic investments easily* - ๐Ÿ“Š Detailed performance metrics + - *See all needed performance metrics in one place* - ๐Ÿ“… Future Projection with Withdrawal Analysis and Sustainability Analysis + - *Generate a future projection based on the current portfolio performance, with a withdrawal analysis and sustainability analysis + calculator* + - *Including with best, worst and average case scenarios* +- ๐Ÿ“Š Savings Plan Performance Overview Tables + - *See the performance of your savings plans if you have multiple assets to compare them* +- ๐Ÿ“„ Export to PDF + - *Export the entire portfolio Overview to a PDF, including Future Projections of 10, 15, 20, 30 and 40 years* +- ๐Ÿ“„ Export to CSV Tables + - *Export all available tables to CSV* + ## Tech Stack -- React 18 +- React 19 - TypeScript - Tailwind CSS -- Vite +- Vite@6 - Recharts - date-fns - Lucide Icons @@ -41,8 +56,7 @@ https://github.com/user-attachments/assets/78b027fa-9883-4813-8086-8b6aa19767de ### Prerequisites -- Node.js 20 or higher -- npm or yarn +- Node.js & npm 20 or higher ### Local Development @@ -55,6 +69,10 @@ https://github.com/user-attachments/assets/78b027fa-9883-4813-8086-8b6aa19767de ![Dark Mode Preview](./docs/dark-mode.png) ![Light Mode Preview](./docs/light-mode.png) ![Future Projection Modal](./docs/future-projection.png) +![PDF Export - Page-1](./docs/analysis-page-1.png) +![PDF Export - Page-2](./docs/analysis-page-2.png) +![Scenario Projection](./docs/scenario-projection.png) + ### Credits: diff --git a/docs/analysis-page-1.png b/docs/analysis-page-1.png new file mode 100644 index 0000000..b9cb29d Binary files /dev/null and b/docs/analysis-page-1.png differ diff --git a/docs/analysis-page-2.png b/docs/analysis-page-2.png new file mode 100644 index 0000000..b12aec4 Binary files /dev/null and b/docs/analysis-page-2.png differ diff --git a/docs/dark-mode.png b/docs/dark-mode.png index 4775e39..c309342 100644 Binary files a/docs/dark-mode.png and b/docs/dark-mode.png differ diff --git a/docs/future-projection.png b/docs/future-projection.png index 5ad0484..ba6af0d 100644 Binary files a/docs/future-projection.png and b/docs/future-projection.png differ diff --git a/docs/light-mode.png b/docs/light-mode.png deleted file mode 100644 index 1c39d33..0000000 Binary files a/docs/light-mode.png and /dev/null differ diff --git a/docs/scenario-projection.png b/docs/scenario-projection.png new file mode 100644 index 0000000..f1dd7e7 Binary files /dev/null and b/docs/scenario-projection.png differ diff --git a/docs/white-mode.png b/docs/white-mode.png new file mode 100644 index 0000000..b1e5861 Binary files /dev/null and b/docs/white-mode.png differ diff --git a/eslint.config.js b/eslint.config.js index 82c2e20..25300b0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -19,6 +19,7 @@ export default tseslint.config( }, rules: { ...reactHooks.configs.recommended.rules, + '@typescript-eslint/no-explicit-any': 'off', 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, diff --git a/package-lock.json b/package-lock.json index b4ea7c0..8928217 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,39 +1,39 @@ { "name": "investment-portfolio-tracker", - "version": "0.0.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "investment-portfolio-tracker", - "version": "0.0.0", + "version": "1.0.0", "dependencies": { - "@tanstack/react-query": "^5.24.1", - "date-fns": "^3.3.1", - "lucide-react": "^0.344.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "date-fns": "^4.1.0", + "jspdf": "^2.5.2", + "jspdf-autotable": "^3.8.4", + "lucide-react": "^0.469.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", "react-router-dom": "^7.1.0", - "recharts": "^2.12.1", - "use-debounce": "^10.0.4", - "zustand": "^4.5.1" + "recharts": "^2.15.0", + "use-debounce": "^10.0.4" }, "devDependencies": { - "@eslint/js": "^9.9.1", + "@eslint/js": "^9.17.0", "@types/node": "^22.10.2", - "@types/react": "^18.3.5", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "autoprefixer": "^10.4.18", - "eslint": "^9.9.1", - "eslint-plugin-react-hooks": "^5.1.0-rc.0", - "eslint-plugin-react-refresh": "^0.4.11", - "globals": "^15.9.0", - "postcss": "^8.4.35", - "tailwindcss": "^3.4.1", - "typescript": "^5.5.3", - "typescript-eslint": "^8.3.0", - "vite": "^5.4.2" + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "eslint": "^9.17.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.16", + "globals": "^15.14.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.1", + "vite": "^6.0.5" } }, "node_modules/@alloc/quick-lru": { @@ -53,6 +53,7 @@ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -62,12 +63,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", - "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", "picocolors": "^1.0.0" }, "engines": { @@ -75,30 +78,32 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.7.tgz", - "integrity": "sha512-9ickoLz+hcXCeh7jrcin+/SLWm+GkxE2kTvoYyp38p4WkdFXfQJxDFGWp/YHjiKLPx06z2A7W8XKuqbReXDzsw==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", + "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.7.tgz", - "integrity": "sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.25.7", - "@babel/generator": "^7.25.7", - "@babel/helper-compilation-targets": "^7.25.7", - "@babel/helper-module-transforms": "^7.25.7", - "@babel/helpers": "^7.25.7", - "@babel/parser": "^7.25.7", - "@babel/template": "^7.25.7", - "@babel/traverse": "^7.25.7", - "@babel/types": "^7.25.7", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -114,12 +119,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", - "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", + "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.7", + "@babel/parser": "^7.26.3", + "@babel/types": "^7.26.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -129,13 +136,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", - "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.7", - "@babel/helper-validator-option": "^7.25.7", + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -145,28 +153,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", - "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.7", - "@babel/types": "^7.25.7" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", - "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.7", - "@babel/helper-simple-access": "^7.25.7", - "@babel/helper-validator-identifier": "^7.25.7", - "@babel/traverse": "^7.25.7" + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -176,89 +185,67 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", - "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", - "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.25.7", - "@babel/types": "^7.25.7" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", - "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", - "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", - "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", - "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.25.7", - "@babel/types": "^7.25.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", - "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz", - "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", + "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.7" + "@babel/types": "^7.26.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -268,12 +255,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.7.tgz", - "integrity": "sha512-JD9MUnLbPL0WdVK8AWC7F7tTG2OS6u/AKKnsK+NdRhUiVdnzyR1S3kKQCaRLOiaULvUiqK6Z4JQE635VgtCFeg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", + "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -283,12 +271,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.7.tgz", - "integrity": "sha512-S/JXG/KrbIY06iyJPKfxr0qRxnhNOdkNXYBl/rmwgDd72cQLH9tEGkDm/yJPGvcSIUoikzfjMios9i+xT/uv9w==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", + "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -309,30 +298,32 @@ } }, "node_modules/@babel/template": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", - "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.7", - "@babel/parser": "^7.25.7", - "@babel/types": "^7.25.7" + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", - "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "version": "7.26.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", + "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.7", - "@babel/generator": "^7.25.7", - "@babel/parser": "^7.25.7", - "@babel/template": "^7.25.7", - "@babel/types": "^7.25.7", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.3", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -345,390 +336,431 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/@babel/types": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz", - "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", + "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.7", - "@babel/helper-validator-identifier": "^7.25.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", "cpu": [ - "x64" + "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -759,21 +791,23 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", + "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.5", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -782,19 +816,24 @@ } }, "node_modules/@eslint/core": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", - "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", + "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -818,6 +857,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -826,28 +866,31 @@ } }, "node_modules/@eslint/js": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.12.0.tgz", - "integrity": "sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", + "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", + "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", - "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", + "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "levn": "^0.4.1" }, @@ -856,27 +899,43 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", - "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.5", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", - "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.0", + "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -891,10 +950,11 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -1221,30 +1281,6 @@ "win32" ] }, - "node_modules/@tanstack/query-core": { - "version": "5.62.8", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.8.tgz", - "integrity": "sha512-4fV31vDsUyvNGrKIOUNPrZztoyL187bThnoQOvAXEVlZbSiuPONpfx53634MKKdvsDir5NyOGm80ShFaoHS/mw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.62.8", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.8.tgz", - "integrity": "sha512-8TUstKxF/fysHonZsWg/hnlDVgasTdHx6Q+f1/s/oPKJBJbKUWPZEHwLTMOZgrZuroLMiqYKJ9w69Abm8mWP0Q==", - "dependencies": { - "@tanstack/query-core": "5.62.8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1368,42 +1404,45 @@ "undici-types": "~6.20.0" } }, - "node_modules/@types/prop-types": { - "version": "15.7.13", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", - "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "devOptional": true + "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": "18.3.11", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", - "integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==", - "devOptional": true, + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.2.tgz", + "integrity": "sha512-USU8ZI/xyKJwFTpjSVIrSeHBVAGagkHQKPNbxeWwql/vDmnTIBgx+TJnhFnj1NXgz8XfprU0egV2dROLGpsBEg==", + "dev": true, + "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.2.tgz", + "integrity": "sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==", "dev": true, - "dependencies": { - "@types/react": "*" + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", - "integrity": "sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz", + "integrity": "sha512-Ncvsq5CT3Gvh+uJG0Lwlho6suwDfUXH0HztslDf5I+F2wAFAZMRwYLEorumpKLzmO2suAXZ/td1tBg4NZIi9CQ==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.8.1", - "@typescript-eslint/type-utils": "8.8.1", - "@typescript-eslint/utils": "8.8.1", - "@typescript-eslint/visitor-keys": "8.8.1", + "@typescript-eslint/scope-manager": "8.18.1", + "@typescript-eslint/type-utils": "8.18.1", + "@typescript-eslint/utils": "8.18.1", + "@typescript-eslint/visitor-keys": "8.18.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1418,24 +1457,21 @@ }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz", - "integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.1.tgz", + "integrity": "sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.8.1", - "@typescript-eslint/types": "8.8.1", - "@typescript-eslint/typescript-estree": "8.8.1", - "@typescript-eslint/visitor-keys": "8.8.1", + "@typescript-eslint/scope-manager": "8.18.1", + "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/typescript-estree": "8.18.1", + "@typescript-eslint/visitor-keys": "8.18.1", "debug": "^4.3.4" }, "engines": { @@ -1446,22 +1482,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz", - "integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz", + "integrity": "sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.8.1", - "@typescript-eslint/visitor-keys": "8.8.1" + "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/visitor-keys": "8.18.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1472,13 +1505,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz", - "integrity": "sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.1.tgz", + "integrity": "sha512-jAhTdK/Qx2NJPNOTxXpMwlOiSymtR2j283TtPqXkKBdH8OAMmhiUfP0kJjc/qSE51Xrq02Gj9NY7MwK+UxVwHQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.8.1", - "@typescript-eslint/utils": "8.8.1", + "@typescript-eslint/typescript-estree": "8.18.1", + "@typescript-eslint/utils": "8.18.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1489,17 +1523,17 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz", - "integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.1.tgz", + "integrity": "sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1509,13 +1543,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz", - "integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz", + "integrity": "sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.8.1", - "@typescript-eslint/visitor-keys": "8.8.1", + "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/visitor-keys": "8.18.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1530,10 +1565,8 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -1541,6 +1574,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -1550,6 +1584,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1565,6 +1600,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1573,15 +1609,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz", - "integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.1.tgz", + "integrity": "sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.8.1", - "@typescript-eslint/types": "8.8.1", - "@typescript-eslint/typescript-estree": "8.8.1" + "@typescript-eslint/scope-manager": "8.18.1", + "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/typescript-estree": "8.18.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1591,17 +1628,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz", - "integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz", + "integrity": "sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.8.1", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.18.1", + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1611,27 +1650,16 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.2.tgz", - "integrity": "sha512-hieu+o05v4glEBucTcKMK3dlES0OeJlD9YVOAPraVMOInBCwzumaIFiUjr4bHK7NPgnAHgiskUoceKercrN8vg==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", + "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/core": "^7.25.2", - "@babel/plugin-transform-react-jsx-self": "^7.24.7", - "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, @@ -1639,14 +1667,15 @@ "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1659,6 +1688,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -1668,6 +1698,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1691,18 +1722,6 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1732,7 +1751,20 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } }, "node_modules/autoprefixer": { "version": "10.4.20", @@ -1777,6 +1809,16 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "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", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1794,6 +1836,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1843,11 +1886,24 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1881,20 +1937,33 @@ } ] }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, + "node_modules/canvg": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", + "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==", + "license": "MIT", + "optional": true, "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "@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": ">=4" + "node": ">=10.0.0" } }, + "node_modules/canvg/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/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1939,21 +2008,6 @@ "node": ">=6" } }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1967,13 +2021,15 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cookie": { "version": "1.0.2", @@ -1984,11 +2040,24 @@ "node": ">=18" } }, + "node_modules/core-js": { + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", + "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1998,6 +2067,16 @@ "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", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2126,9 +2205,10 @@ } }, "node_modules/date-fns": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", - "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -2178,11 +2258,19 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, + "node_modules/dompurify": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", + "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2202,41 +2290,43 @@ "dev": true }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" } }, "node_modules/escalade": { @@ -2248,41 +2338,33 @@ "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/eslint": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz", - "integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", + "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.11.0", - "@eslint/config-array": "^0.18.0", - "@eslint/core": "^0.6.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.12.0", - "@eslint/plugin-kit": "^0.2.0", - "@humanfs/node": "^0.16.5", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.17.0", + "@eslint/plugin-kit": "^0.2.3", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.1", + "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.1.0", - "eslint-visitor-keys": "^4.1.0", - "espree": "^10.2.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2296,8 +2378,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -2318,10 +2399,11 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.1.0-rc-fb9a90fa48-20240614", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0-rc-fb9a90fa48-20240614.tgz", - "integrity": "sha512-xsiRwaDNF5wWNC4ZHLut+x/YcAxksUd9Rizt7LaEn3bV8VyYRpXnRJQlLOfYaVy9esk4DFP4zPPnoNVjq5Gc0w==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz", + "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -2330,19 +2412,21 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.12.tgz", - "integrity": "sha512-9neVjoGv20FwYtCP6CB1dzR1vr57ZDNOXst21wd2xJ/cTlM2xLq0GWVlSNTdMn/4BtP6cHYBMCSp1wFBJ9jBsg==", + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.16.tgz", + "integrity": "sha512-slterMlxAhov/DZO8NScf6mEeMBBXodFUolijDvrtTxyezyLoTQaa73FyYus/VbTdftd8wBgBxPMRk3poleXNQ==", "dev": true, + "license": "MIT", "peerDependencies": { - "eslint": ">=7" + "eslint": ">=8.40" } }, "node_modules/eslint-scope": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", - "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -2355,10 +2439,11 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", - "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2449,14 +2534,15 @@ } }, "node_modules/espree": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", - "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.12.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.1.0" + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2482,6 +2568,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -2516,12 +2603,14 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-equals": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -2558,7 +2647,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -2575,6 +2665,12 @@ "reusify": "^1.0.4" } }, + "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/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2691,6 +2787,7 @@ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -2752,10 +2849,11 @@ } }, "node_modules/globals": { - "version": "15.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", - "integrity": "sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==", + "version": "15.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", + "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -2767,16 +2865,8 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, - "engines": { - "node": ">=4" - } + "license": "MIT" }, "node_modules/hasown": { "version": "2.0.2", @@ -2790,6 +2880,20 @@ "node": ">= 0.4" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2804,6 +2908,7 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2931,13 +3036,15 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -2946,10 +3053,11 @@ } }, "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, @@ -2967,7 +3075,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -2980,6 +3089,7 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -2987,6 +3097,33 @@ "node": ">=6" } }, + "node_modules/jspdf": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", + "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.5.4", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jspdf-autotable": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.8.4.tgz", + "integrity": "sha512-rSffGoBsJYX83iTRv8Ft7FhqfgEL2nLpGAIiqruEQQ3e4r0qdLFbPUB7N9HAle0I3XgpisvyW751VHCqKUVOgQ==", + "license": "MIT", + "peerDependencies": { + "jspdf": "^2.5.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3010,12 +3147,16 @@ } }, "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "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": ">=10" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/lines-and-columns": { @@ -3054,6 +3195,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -3066,16 +3208,18 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^3.0.2" } }, "node_modules/lucide-react": { - "version": "0.344.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.344.0.tgz", - "integrity": "sha512-6YyBnn91GB45VuVT96bYCOKElbJzUHqp65vX8cDcu55MQL9T969v4dhGClpljamuI/+KMO9P6w9Acq1CVQGvIQ==", + "version": "0.469.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.469.0.tgz", + "integrity": "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==", + "license": "ISC", "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/merge2": { @@ -3105,6 +3249,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3261,6 +3406,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -3314,11 +3460,19 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, + "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/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -3351,9 +3505,9 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "dev": true, "funding": [ { @@ -3369,9 +3523,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -3449,18 +3604,6 @@ } } }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/postcss-nested": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", @@ -3518,6 +3661,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -3527,13 +3671,15 @@ "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -3558,27 +3704,35 @@ } ] }, - "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==", + "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": { - "loose-envify": "^1.1.0" - }, + "performance-now": "^2.1.0" + } + }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "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==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.25.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.0.0" } }, "node_modules/react-is": { @@ -3639,6 +3793,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", @@ -3653,6 +3808,7 @@ "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", @@ -3742,6 +3898,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -3756,6 +3913,16 @@ "node": ">=0.10.0" } }, + "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/rollup": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", @@ -3815,18 +3982,17 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -3879,6 +4045,16 @@ "node": ">=0.10.0" } }, + "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/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -3980,6 +4156,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -4009,18 +4186,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -4033,34 +4198,45 @@ "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/tailwindcss": { - "version": "3.4.13", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz", - "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "dev": true, + "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.3.0", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.21.0", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", @@ -4070,11 +4246,15 @@ "node": ">=14.0.0" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "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", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } }, "node_modules/thenify": { "version": "3.3.1", @@ -4102,15 +4282,6 @@ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4124,10 +4295,11 @@ } }, "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", "dev": true, + "license": "MIT", "engines": { "node": ">=16" }, @@ -4160,10 +4332,11 @@ } }, "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4173,14 +4346,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.8.1.tgz", - "integrity": "sha512-R0dsXFt6t4SAFjUSKFjMh4pXDtq04SsFKCVGDP3ZOzNP7itF0jBcZYU4fMsZr4y7O7V7Nc751dDeESbe4PbQMQ==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.1.tgz", + "integrity": "sha512-Mlaw6yxuaDEPQvb/2Qwu3/TfgeBHy9iTJ3mTwe7OvpPmF6KPQjVOfGyEJpPv6Ez2C34OODChhXrzYw/9phI0MQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.8.1", - "@typescript-eslint/parser": "8.8.1", - "@typescript-eslint/utils": "8.8.1" + "@typescript-eslint/eslint-plugin": "8.18.1", + "@typescript-eslint/parser": "8.18.1", + "@typescript-eslint/utils": "8.18.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4189,10 +4363,9 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/undici-types": { @@ -4237,6 +4410,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -4253,20 +4427,22 @@ "react": "*" } }, - "node_modules/use-sync-external-store": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", - "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "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", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", @@ -4289,20 +4465,21 @@ } }, "node_modules/vite": { - "version": "5.4.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", - "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.5.tgz", + "integrity": "sha512-akD5IAH/ID5imgue2DYhzsEwCi0/4VKY31uhMLEYJwPP4TiUp8pL5PIK+Wo7H8qT8JY9i+pVfPydcFPYD1EL7g==", "dev": true, + "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "0.24.0", + "postcss": "^8.4.49", + "rollup": "^4.23.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -4311,19 +4488,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -4344,6 +4527,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, @@ -4496,7 +4685,8 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/yaml": { "version": "2.5.1", @@ -4521,33 +4711,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zustand": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.5.tgz", - "integrity": "sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q==", - "dependencies": { - "use-sync-external-store": "1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } } } } diff --git a/package.json b/package.json index 7114d0e..e1905e7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "investment-portfolio-tracker", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", @@ -10,31 +10,31 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/react-query": "^5.24.1", - "date-fns": "^3.3.1", - "lucide-react": "^0.344.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "date-fns": "^4.1.0", + "jspdf": "^2.5.2", + "jspdf-autotable": "^3.8.4", + "lucide-react": "^0.469.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", "react-router-dom": "^7.1.0", - "recharts": "^2.12.1", - "use-debounce": "^10.0.4", - "zustand": "^4.5.1" + "recharts": "^2.15.0", + "use-debounce": "^10.0.4" }, "devDependencies": { - "@eslint/js": "^9.9.1", + "@eslint/js": "^9.17.0", "@types/node": "^22.10.2", - "@types/react": "^18.3.5", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "autoprefixer": "^10.4.18", - "eslint": "^9.9.1", - "eslint-plugin-react-hooks": "^5.1.0-rc.0", - "eslint-plugin-react-refresh": "^0.4.11", - "globals": "^15.9.0", - "postcss": "^8.4.35", - "tailwindcss": "^3.4.1", - "typescript": "^5.5.3", - "typescript-eslint": "^8.3.0", - "vite": "^5.4.2" + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "eslint": "^9.17.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.16", + "globals": "^15.14.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.1", + "vite": "^6.0.5" } } diff --git a/src/App.tsx b/src/App.tsx index c5389cb..1886c6f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,66 +1,24 @@ -import { Heart, Moon, Plus, Sun } from "lucide-react"; -import React, { useState } from "react"; +import { lazy, Suspense, useState } from "react"; -import { AddAssetModal } from "./components/AddAssetModal"; -import { InvestmentFormWrapper } from "./components/InvestmentForm"; -import { PortfolioChart } from "./components/PortfolioChart"; -import { PortfolioTable } from "./components/PortfolioTable"; -import { useDarkMode } from "./providers/DarkModeProvider"; +import { AppShell } from "./components/Landing/AppShell"; +import { LoadingPlaceholder } from "./components/utils/LoadingPlaceholder"; +import { PortfolioProvider } from "./providers/PortfolioProvider"; + +const MainContent = lazy(() => import("./components/Landing/MainContent")); export default function App() { const [isAddingAsset, setIsAddingAsset] = useState(false); - const { isDarkMode, toggleDarkMode } = useDarkMode(); return ( -
-
-
-
-

Portfolio Simulator

-
- - -
-
- -
-
- -
-
- -
-
- - - {isAddingAsset && setIsAddingAsset(false)} />} -
- - - Built with by Tomato6966 - -
-
+ + setIsAddingAsset(true)}> + }> + + + + ); } diff --git a/src/components/AddAssetModal.tsx b/src/components/AddAssetModal.tsx deleted file mode 100644 index 5850386..0000000 --- a/src/components/AddAssetModal.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { Search, X } from "lucide-react"; -import React, { useState } from "react"; -import { useDebouncedCallback } from "use-debounce"; - -import { getHistoricalData, searchAssets } from "../services/yahooFinanceService"; -import { usePortfolioStore } from "../store/portfolioStore"; -import { Asset } from "../types"; - -export const AddAssetModal = ({ onClose }: { onClose: () => void }) => { - const [search, setSearch] = useState(''); - const [searchResults, setSearchResults] = useState([]); - const [loading, setLoading] = useState(false); - const { addAsset, dateRange } = usePortfolioStore((state) => ({ - addAsset: state.addAsset, - dateRange: state.dateRange, - })); - - const handleSearch = async (query: string) => { - if (query.length < 2) return; - setLoading(true); - try { - const results = await searchAssets(query); - setSearchResults(results); - } catch (error) { - console.error('Error searching assets:', error); - } finally { - setLoading(false); - } - }; - - const debouncedSearch = useDebouncedCallback(handleSearch, 750); - - const handleAssetSelect = async (asset: Asset) => { - setLoading(true); - try { - const historicalData = await getHistoricalData( - asset.symbol, - dateRange.startDate, - dateRange.endDate - ); - - const assetWithHistory = { - ...asset, - historicalData, - }; - - addAsset(assetWithHistory); - onClose(); - } catch (error) { - console.error('Error fetching historical data:', error); - } finally { - setLoading(false); - } - }; - - return ( -
-
-
-

Add Asset

- -
- -
- { - setSearch(e.target.value); - debouncedSearch(e.target.value); - }} - /> - -
- -
- {loading ? ( -
Loading...
- ) : ( - searchResults.map((result) => ( - - )) - )} -
-
-
- ); -}; diff --git a/src/components/DateRangePicker.tsx b/src/components/DateRangePicker.tsx deleted file mode 100644 index d2acddc..0000000 --- a/src/components/DateRangePicker.tsx +++ /dev/null @@ -1,41 +0,0 @@ - - -interface DateRangePickerProps { - startDate: string; - endDate: string; - onStartDateChange: (date: string) => void; - onEndDateChange: (date: string) => void; -} - -export const DateRangePicker = ({ - startDate, - endDate, - onStartDateChange, - onEndDateChange, -}: DateRangePickerProps) => { - return ( -
-
- - onStartDateChange(e.target.value)} - max={endDate} - className="w-full p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300 [&::-webkit-calendar-picker-indicator]:dark:invert" - /> -
-
- - onEndDateChange(e.target.value)} - min={startDate} - max={new Date().toISOString().split('T')[0]} - className="w-full p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300 [&::-webkit-calendar-picker-indicator]:dark:invert" - /> -
-
- ); -}; diff --git a/src/components/EditInvestmentModal.tsx b/src/components/EditInvestmentModal.tsx deleted file mode 100644 index c70a5ea..0000000 --- a/src/components/EditInvestmentModal.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { X } from "lucide-react"; -import { useState } from "react"; - -import { usePortfolioStore } from "../store/portfolioStore"; -import { Investment } from "../types"; - -interface EditInvestmentModalProps { - investment: Investment; - assetId: string; - onClose: () => void; -} - -export const EditInvestmentModal = ({ investment, assetId, onClose }: EditInvestmentModalProps) => { - const updateInvestment = usePortfolioStore((state) => state.updateInvestment); - const [amount, setAmount] = useState(investment.amount.toString()); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - updateInvestment(assetId, investment.id, { - ...investment, - amount: parseFloat(amount), - }); - onClose(); - }; - - return ( -
-
-
-

Edit Investment

- -
- -
-
- - setAmount(e.target.value)} - className="w-full p-2 border rounded" - step="0.01" - min="0" - required - /> -
- - {investment.type === 'periodic' && ( -
-

- Note: Editing a periodic investment will affect all future investments. -

-
- )} - -
- - -
-
-
-
- ); -}; diff --git a/src/components/InvestmentForm.tsx b/src/components/InvestmentForm.tsx index 2ed0aad..ebcfb84 100644 --- a/src/components/InvestmentForm.tsx +++ b/src/components/InvestmentForm.tsx @@ -1,15 +1,15 @@ import { Loader2 } from "lucide-react"; import React, { useState } from "react"; -import { usePortfolioStore } from "../store/portfolioStore"; +import { usePortfolioSelector } from "../hooks/usePortfolio"; import { generatePeriodicInvestments } from "../utils/calculations/assetValue"; -export const InvestmentFormWrapper = () => { - const [selectedAsset, setSelectedAsset] = useState(null); - const { assets, clearAssets } = usePortfolioStore((state) => ({ +export default function InvestmentFormWrapper() { + const { assets, clearAssets } = usePortfolioSelector((state) => ({ assets: state.assets, clearAssets: state.clearAssets, })); + const [selectedAsset, setSelectedAsset] = useState(null); const handleClearAssets = () => { if (window.confirm('Are you sure you want to delete all assets? This action cannot be undone.')) { @@ -74,7 +74,7 @@ const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clea const [yearInterval, setYearInterval] = useState('1'); const [isSubmitting, setIsSubmitting] = useState(false); - const { dateRange, addInvestment } = usePortfolioStore((state) => ({ + const { dateRange, addInvestment } = usePortfolioSelector((state) => ({ dateRange: state.dateRange, addInvestment: state.addInvestment, })); @@ -83,46 +83,48 @@ const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clea e.preventDefault(); setIsSubmitting(true); - try { - if (type === "single") { - const investment = { - id: crypto.randomUUID(), - assetId, - type, - amount: parseFloat(amount), - date - }; - addInvestment(assetId, investment); - } else { - const periodicSettings = { - startDate: date, - dayOfMonth: parseInt(dayOfMonth), - interval: parseInt(interval), - amount: parseFloat(amount), - ...(isDynamic ? { - dynamic: { - type: dynamicType, - value: parseFloat(dynamicValue), - yearInterval: parseInt(yearInterval), - }, - } : undefined), - }; - - const investments = generatePeriodicInvestments( - periodicSettings, - dateRange.endDate, - assetId - ); - - for (const investment of investments) { + setTimeout(async () => { + try { + if (type === "single") { + const investment = { + id: crypto.randomUUID(), + assetId, + type, + amount: parseFloat(amount), + date + }; addInvestment(assetId, investment); + } else { + const periodicSettings = { + startDate: date, + dayOfMonth: parseInt(dayOfMonth), + interval: parseInt(interval), + amount: parseFloat(amount), + ...(isDynamic ? { + dynamic: { + type: dynamicType, + value: parseFloat(dynamicValue), + yearInterval: parseInt(yearInterval), + }, + } : undefined), + }; + + const investments = generatePeriodicInvestments( + periodicSettings, + dateRange.endDate, + assetId + ); + + for (const investment of investments) { + addInvestment(assetId, investment); + } } + } finally { + setIsSubmitting(false); + setAmount(''); + clearSelectedAsset(); } - } finally { - setIsSubmitting(false); - setAmount(''); - clearSelectedAsset(); - } + }, 10); }; return ( @@ -264,7 +266,7 @@ const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clea + + + + {children} + + + + Built with by Tomato6966 + + + + ); +}; diff --git a/src/components/Landing/MainContent.tsx b/src/components/Landing/MainContent.tsx new file mode 100644 index 0000000..e426a72 --- /dev/null +++ b/src/components/Landing/MainContent.tsx @@ -0,0 +1,38 @@ +import { lazy, Suspense } from "react"; + +import { LoadingPlaceholder } from "../utils/LoadingPlaceholder"; + +const AddAssetModal = lazy(() => import("../Modals/AddAssetModal")); +const InvestmentFormWrapper = lazy(() => import("../InvestmentForm")); +const PortfolioChart = lazy(() => import("../PortfolioChart")); +const PortfolioTable = lazy(() => import("../PortfolioTable")); + + +export default function MainContent({ isAddingAsset, setIsAddingAsset }: { isAddingAsset: boolean, setIsAddingAsset: (value: boolean) => void }) { + return ( + <> +
+
+ }> + + +
+
+ }> + + +
+
+ + }> + + + + {isAddingAsset && ( + + setIsAddingAsset(false)} /> + + )} + + ); +}; diff --git a/src/components/Modals/AddAssetModal.tsx b/src/components/Modals/AddAssetModal.tsx new file mode 100644 index 0000000..6b2eaa3 --- /dev/null +++ b/src/components/Modals/AddAssetModal.tsx @@ -0,0 +1,112 @@ +import { Loader2, Search, X } from "lucide-react"; +import { useState } from "react"; +import { useDebouncedCallback } from "use-debounce"; + +import { usePortfolioSelector } from "../../hooks/usePortfolio"; +import { getHistoricalData, searchAssets } from "../../services/yahooFinanceService"; +import { Asset } from "../../types"; + +export default function AddAssetModal({ onClose }: { onClose: () => void }) { + const [ search, setSearch ] = useState(''); + const [ searchResults, setSearchResults ] = useState([]); + const [ loading, setLoading ] = useState(null); + const { addAsset, dateRange, assets } = usePortfolioSelector((state) => ({ + addAsset: state.addAsset, + dateRange: state.dateRange, + assets: state.assets, + })); + + const handleSearch = (query: string) => { + if (query.length < 2) return; + setLoading("searching"); + setTimeout(async () => { + try { + const results = await searchAssets(query); + setSearchResults(results.filter((result) => !assets.some((asset) => asset.symbol === result.symbol))); + } catch (error) { + console.error('Error searching assets:', error); + } finally { + setLoading(null); + } + }, 10); + }; + + const debouncedSearch = useDebouncedCallback(handleSearch, 750); + + const handleAssetSelect = (asset: Asset) => { + setLoading("adding"); + setTimeout(async () => { + try { + const { historicalData, longName } = await getHistoricalData( + asset.symbol, + dateRange.startDate, + dateRange.endDate + ); + + const assetWithHistory = { + ...asset, + // override name with the fetched long Name if available + name: longName || asset.name, + historicalData, + }; + + addAsset(assetWithHistory); + onClose(); + } catch (error) { + console.error('Error fetching historical data:', error); + } finally { + setLoading(null); + } + }, 10); + }; + + return ( +
+
+
+

Add Asset

+ +
+ +
+ { + setSearch(e.target.value); + debouncedSearch(e.target.value); + }} + /> + +
+ +
+ {loading ? ( +
+ + {loading === "searching" ? "Searching Assets..." : "Fetching Details & Adding..."} +
+ ) : ( + searchResults.map((result) => ( + + )) + )} +
+
+
+ ); +}; diff --git a/src/components/Modals/EditInvestmentModal.tsx b/src/components/Modals/EditInvestmentModal.tsx new file mode 100644 index 0000000..1727a6b --- /dev/null +++ b/src/components/Modals/EditInvestmentModal.tsx @@ -0,0 +1,81 @@ +import { X } from "lucide-react"; +import { useState } from "react"; + +import { usePortfolioSelector } from "../../hooks/usePortfolio"; +import { Investment } from "../../types"; + +interface EditInvestmentModalProps { + investment: Investment; + assetId: string; + onClose: () => void; +} + +export const EditInvestmentModal = ({ investment, assetId, onClose }: EditInvestmentModalProps) => { + const { updateInvestment } = usePortfolioSelector((state) => ({ + updateInvestment: state.updateInvestment, + })); + const [amount, setAmount] = useState(investment.amount.toString()); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + updateInvestment(assetId, investment.id, { + ...investment, + amount: parseFloat(amount), + }); + onClose(); + }; + + return ( +
+
+
+

Edit Investment

+ +
+ +
+
+ + setAmount(e.target.value)} + className="w-full p-2 border rounded" + step="0.01" + min="0" + required + /> +
+ + {investment.type === 'periodic' && ( +
+

+ Note: Editing a periodic investment will affect all future investments. +

+
+ )} + +
+ + +
+
+
+
+ ); +}; diff --git a/src/components/FutureProjectionModal.tsx b/src/components/Modals/FutureProjectionModal.tsx similarity index 75% rename from src/components/FutureProjectionModal.tsx rename to src/components/Modals/FutureProjectionModal.tsx index a682973..caca238 100644 --- a/src/components/FutureProjectionModal.tsx +++ b/src/components/Modals/FutureProjectionModal.tsx @@ -4,54 +4,41 @@ import { Bar, BarChart, CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; -import { usePortfolioStore } from "../store/portfolioStore"; -import { calculateFutureProjection } from "../utils/calculations/futureProjection"; -import { formatCurrency } from "../utils/formatters"; +import { usePortfolioSelector } from "../../hooks/usePortfolio"; +import { calculateFutureProjection } from "../../utils/calculations/futureProjection"; +import { formatCurrency } from "../../utils/formatters"; + +import type { ProjectionData, SustainabilityAnalysis, WithdrawalPlan } from "../../types"; interface FutureProjectionModalProps { - onClose: () => void; performancePerAnno: number; + bestPerformancePerAnno: { percentage: number, year: number }[]; + worstPerformancePerAnno: { percentage: number, year: number }[]; + onClose: () => void; } -type ChartType = 'line' | 'bar'; +export type ChartType = 'line' | 'bar'; -export interface WithdrawalPlan { - amount: number; - interval: 'monthly' | 'yearly'; - startTrigger: 'date' | 'portfolioValue' | 'auto'; - startDate?: string; - startPortfolioValue?: number; - enabled: boolean; - autoStrategy?: { - type: 'maintain' | 'deplete' | 'grow'; - targetYears?: number; - targetGrowth?: number; - }; -} +type ScenarioCalc = { projection: ProjectionData[], sustainability: SustainabilityAnalysis | null, avaragedAmount: number, percentage: number, percentageAveraged: number }; -export interface ProjectionData { - date: string; - value: number; - invested: number; - withdrawals: number; - totalWithdrawn: number; -} - -export interface SustainabilityAnalysis { - yearsToReachTarget: number; - targetValue: number; - sustainableYears: number | 'infinite'; -} - -export const FutureProjectionModal = ({ onClose, performancePerAnno }: FutureProjectionModalProps) => { +export const FutureProjectionModal = ({ + performancePerAnno, + bestPerformancePerAnno, + worstPerformancePerAnno, + onClose +}: FutureProjectionModalProps) => { const [years, setYears] = useState('10'); const [isCalculating, setIsCalculating] = useState(false); const [chartType, setChartType] = useState('line'); const [projectionData, setProjectionData] = useState([]); + const [scenarios, setScenarios] = useState<{ best: ScenarioCalc, worst: ScenarioCalc }>({ + best: { projection: [], sustainability: null, avaragedAmount: 0, percentage: 0, percentageAveraged: 0 }, + worst: { projection: [], sustainability: null, avaragedAmount: 0, percentage: 0, percentageAveraged: 0 }, + }); const [withdrawalPlan, setWithdrawalPlan] = useState({ amount: 0, interval: 'monthly', - startTrigger: 'date', + startTrigger: 'auto', startDate: new Date().toISOString().split('T')[0], startPortfolioValue: 0, enabled: false, @@ -63,7 +50,9 @@ export const FutureProjectionModal = ({ onClose, performancePerAnno }: FuturePro }); const [sustainabilityAnalysis, setSustainabilityAnalysis] = useState(null); - const { assets } = usePortfolioStore(); + const { assets } = usePortfolioSelector((state) => ({ + assets: state.assets, + })); const calculateProjection = useCallback(async () => { setIsCalculating(true); @@ -76,14 +65,44 @@ export const FutureProjectionModal = ({ onClose, performancePerAnno }: FuturePro ); setProjectionData(projection); setSustainabilityAnalysis(sustainability); + const slicedBestCase = bestPerformancePerAnno.slice(0, Math.floor(bestPerformancePerAnno.length / 2)); + const slicedWorstCase = worstPerformancePerAnno.slice(0, Math.floor(worstPerformancePerAnno.length / 2)); + const bestCase = slicedBestCase.reduce((acc, curr) => acc + curr.percentage, 0) / slicedBestCase.length || 0; + const worstCase = slicedWorstCase.reduce((acc, curr) => acc + curr.percentage, 0) / slicedWorstCase.length || 0; + + const bestCaseAvaraged = (bestCase + performancePerAnno) / 2; + const worstCaseAvaraged = (worstCase + performancePerAnno) / 2; + setScenarios({ + best: { + ...await calculateFutureProjection( + assets, + parseInt(years), + bestCaseAvaraged, + withdrawalPlan.enabled ? withdrawalPlan : undefined + ), + avaragedAmount: slicedBestCase.length, + percentageAveraged: bestCaseAvaraged, + percentage: bestCase + }, + worst: { + ...await calculateFutureProjection( + assets, + parseInt(years), + worstCaseAvaraged, + withdrawalPlan.enabled ? withdrawalPlan : undefined + ), + avaragedAmount: slicedWorstCase.length, + percentage: worstCase, + percentageAveraged: worstCaseAvaraged + } + }); } catch (error) { console.error('Error calculating projection:', error); } finally { setIsCalculating(false); } - }, [assets, years, withdrawalPlan, performancePerAnno]); + }, [assets, years, withdrawalPlan, performancePerAnno, bestPerformancePerAnno, worstPerformancePerAnno]); - // eslint-disable-next-line @typescript-eslint/no-explicit-any const CustomTooltip = ({ active, payload, label }: any) => { if (active && payload && payload.length) { const value = payload[0].value; @@ -122,6 +141,36 @@ export const FutureProjectionModal = ({ onClose, performancePerAnno }: FuturePro return null; }; + + const CustomScenarioTooltip = ({ active, payload, label }: any) => { + if (active && payload && payload.length) { + const bestCase = payload.find((p: any) => p.dataKey === 'bestCase')?.value || 0; + const baseCase = payload.find((p: any) => p.dataKey === 'baseCase')?.value || 0; + const worstCase = payload.find((p: any) => p.dataKey === 'worstCase')?.value || 0; + const invested = payload.find((p: any) => p.dataKey === 'invested')?.value || 0; + + return ( +
+

+ {new Date(label).toLocaleDateString('de-DE')} +

+

+ Best-Case: {formatCurrency(bestCase)} {((bestCase - invested) / invested * 100).toFixed(2)}% +

+

+ Avg. Base-Case: {formatCurrency(baseCase)} {((baseCase - invested) / invested * 100).toFixed(2)}% +

+

+ Worst-Case: {formatCurrency(worstCase)} {((worstCase - invested) / invested * 100).toFixed(2)}% +

+
+ ); + } + return null; + }; + + + const renderChart = () => { if (isCalculating) { return ( @@ -230,6 +279,97 @@ export const FutureProjectionModal = ({ onClose, performancePerAnno }: FuturePro ); }; + const renderScenarioDescription = () => { + if (!scenarios.best.projection.length) return null; + + return ( +
+

Scenario Calculations

+
    +
  • + Avg. Base Case: Using historical average return of {performancePerAnno.toFixed(2)}% +
  • +
  • + Best Case: Average of top 50% performing years ({scenarios.best.avaragedAmount} years) at {scenarios.best.percentage.toFixed(2)}%, + averaged with base case to {scenarios.best.percentageAveraged.toFixed(2)}% +
  • +
  • + Worst Case: Average of bottom 50% performing years ({scenarios.worst.avaragedAmount} years) at {scenarios.worst.percentage.toFixed(2)}%, + averaged with base case to {scenarios.worst.percentageAveraged.toFixed(2)}% +
  • +
+
+ ); + }; + + const renderScenarioChart = () => { + if (!scenarios.best.projection.length) return null; + + // Create a merged and sorted dataset for consistent x-axis + const mergedData = projectionData.map(basePoint => { + const date = basePoint.date; + const bestPoint = scenarios.best.projection.find(p => p.date === date); + const worstPoint = scenarios.worst.projection.find(p => p.date === date); + + return { + date, + bestCase: bestPoint?.value || 0, + baseCase: basePoint.value, + worstCase: worstPoint?.value || 0, + invested: basePoint.invested + }; + }).sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); + + return ( +
+

Scenario Comparison

+
+ + + + new Date(date).toLocaleDateString('de-DE', { + year: 'numeric', + month: 'numeric' + })} + /> + + }/> + + + + + + +
+
+ ); + }; + return (
@@ -263,7 +403,7 @@ export const FutureProjectionModal = ({ onClose, performancePerAnno }: FuturePro
+ {renderScenarioDescription()}
@@ -370,7 +511,7 @@ export const FutureProjectionModal = ({ onClose, performancePerAnno }: FuturePro > - +
@@ -547,6 +688,7 @@ export const FutureProjectionModal = ({ onClose, performancePerAnno }: FuturePro
{renderChart()}
+ {renderScenarioChart()} diff --git a/src/components/PortfolioChart.tsx b/src/components/PortfolioChart.tsx index 5d8eb2a..9693a25 100644 --- a/src/components/PortfolioChart.tsx +++ b/src/components/PortfolioChart.tsx @@ -6,49 +6,20 @@ import { } from "recharts"; import { useDebouncedCallback } from "use-debounce"; -import { useDarkMode } from "../providers/DarkModeProvider"; +import { useDarkMode } from "../hooks/useDarkMode"; +import { usePortfolioSelector } from "../hooks/usePortfolio"; import { getHistoricalData } from "../services/yahooFinanceService"; -import { usePortfolioStore } from "../store/portfolioStore"; import { DateRange } from "../types"; import { calculatePortfolioValue } from "../utils/calculations/portfolioValue"; -import { DateRangePicker } from "./DateRangePicker"; +import { getHexColor } from "../utils/formatters"; +import { DateRangePicker } from "./utils/DateRangePicker"; -const LIGHT_MODE_COLORS = [ - '#2563eb', '#dc2626', '#059669', '#7c3aed', '#ea580c', - '#0891b2', '#be123c', '#1d4ed8', '#b91c1c', '#047857', - '#6d28d9', '#c2410c', '#0e7490', '#9f1239', '#1e40af', - '#991b1b', '#065f46', '#5b21b6', '#9a3412', '#155e75', - '#881337', '#1e3a8a', '#7f1d1d', '#064e3b', '#4c1d95' -]; - -const DARK_MODE_COLORS = [ - '#60a5fa', '#f87171', '#34d399', '#a78bfa', '#fb923c', - '#22d3ee', '#fb7185', '#3b82f6', '#ef4444', '#10b981', - '#8b5cf6', '#f97316', '#06b6d4', '#f43f5e', '#2563eb', - '#dc2626', '#059669', '#7c3aed', '#ea580c', '#0891b2', - '#be123c', '#1d4ed8', '#b91c1c', '#047857', '#6d28d9' -]; - -const getHexColor = (usedColors: Set, isDarkMode: boolean): string => { - const colorPool = isDarkMode ? DARK_MODE_COLORS : LIGHT_MODE_COLORS; - - // Find first unused color - const availableColor = colorPool.find(color => !usedColors.has(color)); - - if (availableColor) { - return availableColor; - } - - // Fallback to random color if all predefined colors are used - return `#${Math.floor(Math.random()*16777215).toString(16)}`; -}; - -export const PortfolioChart = () => { +export default function PortfolioChart() { const [isFullscreen, setIsFullscreen] = useState(false); const [hideAssets, setHideAssets] = useState(false); const [hiddenAssets, setHiddenAssets] = useState>(new Set()); const { isDarkMode } = useDarkMode(); - const { assets, dateRange, updateDateRange, updateAssetHistoricalData } = usePortfolioStore((state) => ({ + const { assets, dateRange, updateDateRange, updateAssetHistoricalData } = usePortfolioSelector((state) => ({ assets: state.assets, dateRange: state.dateRange, updateDateRange: state.updateDateRange, @@ -57,11 +28,13 @@ export const PortfolioChart = () => { const fetchHistoricalData = useCallback( async (startDate: string, endDate: string) => { - assets.forEach(async (asset) => { - const historicalData = await getHistoricalData(asset.symbol, startDate, endDate); - updateAssetHistoricalData(asset.id, historicalData); - }); - }, [assets, updateAssetHistoricalData]); + for (const asset of assets) { + const { historicalData, longName } = await getHistoricalData(asset.symbol, startDate, endDate); + updateAssetHistoricalData(asset.id, historicalData, longName); + } + }, + [assets, updateAssetHistoricalData] + ); const debouncedFetchHistoricalData = useDebouncedCallback(fetchHistoricalData, 1500, { maxWait: 5000, @@ -69,14 +42,14 @@ export const PortfolioChart = () => { const assetColors: Record = useMemo(() => { const usedColors = new Set(); - return assets.reduce((colors, asset) => { - const color = getHexColor(usedColors, isDarkMode); - usedColors.add(color); - return { - ...colors, - [asset.id]: color, - }; - }, {}); + return assets.reduce((colors, asset) => { + const color = getHexColor(usedColors, isDarkMode); + usedColors.add(color); + return { + ...colors, + [asset.id]: color, + }; + }, {}); // eslint-disable-next-line react-hooks/exhaustive-deps }, [assets.map(a => a.id).join(','), isDarkMode]); @@ -84,7 +57,7 @@ export const PortfolioChart = () => { const allAssetsInvestedKapitals = useMemo>(() => { const investedKapitals: Record = {}; - for(const asset of assets) { + for (const asset of assets) { investedKapitals[asset.id] = asset.investments.reduce((acc, curr) => acc + curr.amount, 0); } @@ -101,7 +74,7 @@ export const PortfolioChart = () => { }; processed["ttwor"] = 0; - for(const asset of assets) { + for (const asset of assets) { const initialPrice = data[0].assets[asset.id]; const currentPrice = point.assets[asset.id]; if (initialPrice && currentPrice) { @@ -133,7 +106,7 @@ export const PortfolioChart = () => { const toggleAllAssets = useCallback(() => { setHideAssets(!hideAssets); setHiddenAssets(new Set()); - }, [hideAssets] ); + }, [hideAssets]); const CustomLegend = useCallback(({ payload }: any) => { return ( @@ -168,9 +141,8 @@ export const PortfolioChart = () => { + + + + + + + + {!isSavingsPlanOverviewDisabled && showSavingsPlans && savingsPlansPerformance.length > 0 && ( +
+
+

Savings Plans Performance

+ +
+ + + + + + + + + + + + + {savingsPlansPerformance.map((plan) => ( + + + + + + + + + ))} + +
AssetInterval AmountTotal InvestedCurrent ValuePerformance (%)Performance (p.a.)
{plan.assetName}{plan.amount}โ‚ฌ{plan.totalInvested.toFixed(2)}โ‚ฌ{plan.currentValue.toFixed(2)}{plan.performancePercentage.toFixed(2)}%{plan.performancePerAnnoPerformance.toFixed(2)}%
+
+ )} + +
+
+

Positions Overview

+ +
+
+ + + + + + + + + + + + + + + {performance.summary && ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + )} + {performance.investments.sort((a, b) => a.date.localeCompare(b.date)).map((inv, index) => { + const asset = assets.find(a => a.name === inv.assetName)!; + const investment = asset.investments.find(i => i.id === inv.id)! || inv; + const filtered = performance.investments.filter(v => v.assetName === inv.assetName); + const avgBuyIn = filtered.reduce((acc, curr) => acc + curr.investedAtPrice, 0) / filtered.length; + const isLast = index === performance.investments.length - 1; + + return ( + + + + + + + + + + + ); + })} + +
AssetTypeDateInvested Amount + + Current Amount + + + + Buy-In (avg) + + + + Performance (%) + + Actions
Total Portfolioโ‚ฌ{performance.summary.totalInvested.toFixed(2)}โ‚ฌ{performance.summary.currentValue.toFixed(2)} + {performance.summary.performancePercentage.toFixed(2)}% +
    +
  • (avg. acc. {averagePerformance}%)
  • +
  • (avg. p.a. {performance.summary.performancePerAnnoPerformance.toFixed(2)}%)
  • +
  • (best p.a. {performance.summary.bestPerformancePerAnno?.[0]?.percentage?.toFixed(2) || "0.00"}% {performance.summary.bestPerformancePerAnno?.[0]?.year || "N/A"})
  • +
  • (worst p.a. {performance.summary.worstPerformancePerAnno?.[0]?.percentage?.toFixed(2) || "0.00"}% {performance.summary.worstPerformancePerAnno?.[0]?.year || "N/A"})
  • +
+
TTWOR{new Date(performance.investments[0]?.date).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}โ‚ฌ{performance.summary.totalInvested.toFixed(2)}โ‚ฌ{performance.summary.ttworValue.toFixed(2)}{performance.summary.ttworPercentage.toFixed(2)}%
{inv.assetName} + {investment?.type === 'periodic' ? ( + + + SavingsPlan + + ) : ( + + + OneTime + + )} + {format(new Date(inv.date), 'dd.MM.yyyy')}โ‚ฌ{inv.investedAmount.toFixed(2)}โ‚ฌ{inv.currentValue.toFixed(2)}โ‚ฌ{inv.investedAtPrice.toFixed(2)} (โ‚ฌ{avgBuyIn.toFixed(2)}){inv.performancePercentage.toFixed(2)}% +
+ + +
+
+
+
+ + + {editingInvestment && ( + setEditingInvestment(null)} + /> + )} + {showProjection && ( + setShowProjection(false)} + /> )} ); }; - -export const PortfolioTable = () => { - const { assets, removeInvestment, clearInvestments } = usePortfolioStore((state) => ({ - assets: state.assets, - removeInvestment: state.removeInvestment, - clearInvestments: state.clearInvestments, - })); - - const [editingInvestment, setEditingInvestment] = useState<{ - investment: Investment; - assetId: string; - } | null>(null); - - const performance = useMemo(() => calculateInvestmentPerformance(assets), [assets]); - - const averagePerformance = useMemo(() => { - return ((performance.investments.reduce((sum, inv) => sum + inv.performancePercentage, 0) / performance.investments.length) || 0).toFixed(2); - }, [performance.investments]); - - const handleDelete = useCallback((investmentId: string, assetId: string) => { - if (window.confirm("Are you sure you want to delete this investment?")) { - removeInvestment(assetId, investmentId); - } - }, [removeInvestment]); - - const handleClearAll = useCallback(() => { - if (window.confirm("Are you sure you want to clear all investments?")) { - clearInvestments(); - } - }, [clearInvestments]); - - const performanceTooltip = useMemo(() => ( -
-

The performance of your portfolio is {performance.summary.performancePercentage.toFixed(2)}%

-

The average (acc.) performance of all positions is {averagePerformance}%

-

The average (p.a.) performance of every year is {performance.summary.performancePerAnnoPerformance.toFixed(2)}%

-

- Note: An average performance of positions doesn't always match your entire portfolio's average, - especially with single investments or investments on different time ranges. -

-
- ), [performance.summary.performancePercentage, averagePerformance, performance.summary.performancePerAnnoPerformance]); - - const buyInTooltip = useMemo(() => ( -
-

"Buy-in" shows the asset's price when that position was bought.

-

"Avg" shows the average buy-in price across all positions for that asset.

-
- ), []); - - const currentAmountTooltip = useMemo(() => ( - "The current value of your investment based on the latest market price." - ), []); - - const ttworTooltip = useMemo(() => ( -
-

Time Travel Without Risk (TTWOR) shows how your portfolio would have performed if all investments had been made at the beginning of the period.

-

- It helps to evaluate the impact of your investment timing strategy compared to a single early investment. -

-
- ), []); - - const [showProjection, setShowProjection] = useState(false); - - return ( -
-
-

Portfolio's Positions Overview

-
- - -
-
-
- - - - - - - - - - - - - - - {performance.summary && ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - )} - {performance.investments.map((inv, index) => { - const asset = assets.find(a => a.name === inv.assetName)!; - const investment = asset.investments.find(i => i.id === inv.id)! || inv; - const filtered = performance.investments.filter(v => v.assetName === inv.assetName); - const avgBuyIn = filtered.reduce((acc, curr) => acc + curr.investedAtPrice, 0) / filtered.length; - const isLast = index === performance.investments.length - 1; - - return ( - - - - - - - - - - - ); - })} - -
AssetTypeDateInvested Amount - - Current Amount - - - - Buy-In (avg) - - - - Performance (%) - - Actions
Total Portfolioโ‚ฌ{performance.summary.totalInvested.toFixed(2)}โ‚ฌ{performance.summary.currentValue.toFixed(2)} - {performance.summary.performancePercentage.toFixed(2)}% -
    -
  • (avg. acc. {averagePerformance}%)
  • -
  • (avg. p.a. {performance.summary.performancePerAnnoPerformance.toFixed(2)}%)
  • -
-
TTWOR{new Date(performance.investments[0]?.date).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}โ‚ฌ{performance.summary.totalInvested.toFixed(2)}โ‚ฌ{performance.summary.ttworValue.toFixed(2)}{performance.summary.ttworPercentage.toFixed(2)}%
{inv.assetName} - {investment?.type === 'periodic' ? ( - - - SavingsPlan - - ) : ( - - - OneTime - - )} - {format(new Date(inv.date), 'dd.MM.yyyy')}โ‚ฌ{inv.investedAmount.toFixed(2)}โ‚ฌ{inv.currentValue.toFixed(2)}โ‚ฌ{inv.investedAtPrice.toFixed(2)} (โ‚ฌ{avgBuyIn.toFixed(2)}){inv.performancePercentage.toFixed(2)}% -
- - -
-
-
- {editingInvestment && ( - setEditingInvestment(null)} - /> - )} - {showProjection && ( - setShowProjection(false)} /> - )} -
- ); -}; diff --git a/src/components/utils/DateRangePicker.tsx b/src/components/utils/DateRangePicker.tsx new file mode 100644 index 0000000..0bdb75e --- /dev/null +++ b/src/components/utils/DateRangePicker.tsx @@ -0,0 +1,82 @@ +import { useRef } from "react"; +import { useDebouncedCallback } from "use-debounce"; + +interface DateRangePickerProps { + startDate: string; + endDate: string; + onStartDateChange: (date: string) => void; + onEndDateChange: (date: string) => void; +} + +export const DateRangePicker = ({ + startDate, + endDate, + onStartDateChange, + onEndDateChange, +}: DateRangePickerProps) => { + const startDateRef = useRef(null); + const endDateRef = useRef(null); + + const isValidDate = (dateString: string) => { + const date = new Date(dateString); + return date instanceof Date && !isNaN(date.getTime()) && dateString.length === 10; + }; + + const debouncedStartDateChange = useDebouncedCallback( + (newDate: string) => { + if (newDate !== startDate && isValidDate(newDate)) { + onStartDateChange(newDate); + } + }, + 750 + ); + + const debouncedEndDateChange = useDebouncedCallback( + (newDate: string) => { + if (newDate !== endDate && isValidDate(newDate)) { + onEndDateChange(newDate); + } + }, + 750 + ); + + const handleStartDateChange = () => { + if (startDateRef.current) { + debouncedStartDateChange(startDateRef.current.value); + } + }; + + const handleEndDateChange = () => { + if (endDateRef.current) { + debouncedEndDateChange(endDateRef.current.value); + } + }; + + return ( +
+
+ + +
+
+ + +
+
+ ); +}; diff --git a/src/components/utils/LoadingPlaceholder.tsx b/src/components/utils/LoadingPlaceholder.tsx new file mode 100644 index 0000000..7d89551 --- /dev/null +++ b/src/components/utils/LoadingPlaceholder.tsx @@ -0,0 +1,11 @@ +import { Loader2 } from "lucide-react"; + +interface LoadingPlaceholderProps { + className?: string; +} + +export const LoadingPlaceholder = ({ className = "" }: LoadingPlaceholderProps) => ( +
+ +
+); diff --git a/src/components/utils/ToolTip.tsx b/src/components/utils/ToolTip.tsx new file mode 100644 index 0000000..0153b13 --- /dev/null +++ b/src/components/utils/ToolTip.tsx @@ -0,0 +1,29 @@ +import { HelpCircle } from "lucide-react"; +import { ReactNode, useState } from "react"; + +interface TooltipProps { + content: string | ReactNode; + children: ReactNode; +} + +export const Tooltip = ({ content, children }: TooltipProps) => { + const [show, setShow] = useState(false); + + return ( +
+
setShow(true)} + onMouseLeave={() => setShow(false)} + > + {children} + +
+ {show && ( +
+ {content} +
+ )} +
+ ); +}; diff --git a/src/hooks/useDarkMode.tsx b/src/hooks/useDarkMode.tsx new file mode 100644 index 0000000..98fed17 --- /dev/null +++ b/src/hooks/useDarkMode.tsx @@ -0,0 +1,11 @@ +import { useContext } from "react"; + +import { DarkModeContext } from "../providers/DarkModeProvider"; + +export const useDarkMode = () => { + const context = useContext(DarkModeContext); + if (!context) { + throw new Error('useDarkMode must be used within a DarkModeProvider'); + } + return context; +}; diff --git a/src/hooks/usePortfolio.tsx b/src/hooks/usePortfolio.tsx new file mode 100644 index 0000000..21e1456 --- /dev/null +++ b/src/hooks/usePortfolio.tsx @@ -0,0 +1,18 @@ +import { useContext, useMemo } from "react"; + +import { PortfolioContext, PortfolioContextType } from "../providers/PortfolioProvider"; + +// main way of how to access the context +const usePortfolio = () => { + const context = useContext(PortfolioContext); + if (!context) { + throw new Error('usePortfolio must be used within a PortfolioProvider'); + } + return context; +}; + +// performance optimized way of accessing the context +export const usePortfolioSelector = (selector: (state: PortfolioContextType) => T): T => { + const context = usePortfolio(); + return useMemo(() => selector(context), [selector, context]); +}; diff --git a/src/index.css b/src/index.css index 3525aeb..bce3f0b 100644 --- a/src/index.css +++ b/src/index.css @@ -5,61 +5,61 @@ /* Modern Scrollbar Styling */ /* Webkit (Chrome, Safari, Edge) */ ::-webkit-scrollbar { - width: 8px; - height: 8px; + width: 8px; + height: 8px; } ::-webkit-scrollbar-track { - background: transparent; + background: transparent; } ::-webkit-scrollbar-thumb { - background-color: #94a3b8; - border-radius: 9999px; - border: 2px solid transparent; - background-clip: content-box; + background-color: #94a3b8; + border-radius: 9999px; + border: 2px solid transparent; + background-clip: content-box; } ::-webkit-scrollbar-thumb:hover { - background-color: #64748b; + background-color: #64748b; } /* Ensure transparent background for the scrollbar area */ ::-webkit-scrollbar-corner, ::-webkit-scrollbar-track-piece { - background: transparent !important; + background: transparent !important; } /* Dark mode */ .dark ::-webkit-scrollbar { - background: black !important; + background: black !important; } .dark ::-webkit-scrollbar-track { - background: black !important; + background: black !important; } .dark ::-webkit-scrollbar-thumb { - background-color: #475569 !important; + background-color: #475569 !important; } .dark ::-webkit-scrollbar-thumb:hover { - background-color: #64748b !important; + background-color: #64748b !important; } /* Firefox */ * { - scrollbar-width: thin; - scrollbar-color: #94a3b8 #1d2127; + scrollbar-width: thin; + scrollbar-color: #94a3b8 #1d2127; } .dark * { - scrollbar-color: #475569 #1d212799 !important; + scrollbar-color: #475569 #1d212799 !important; } /* For Internet Explorer */ body { - -ms-overflow-style: auto; + -ms-overflow-style: auto; } /* Remove default white background in dark mode */ @@ -67,58 +67,59 @@ body { .dark ::-webkit-scrollbar-track, .dark ::-webkit-scrollbar-corner, .dark ::-webkit-scrollbar-track-piece { - background-color: transparent !important; + background-color: transparent !important; } /* Ensure the app background extends properly */ -html, body { - background: inherit; +html, +body { + background: inherit; } /* Scrollbar Styling fรผr Investment Form */ .scrollbar-styled { - scrollbar-gutter: stable both-edges; - overflow-y: scroll !important; + scrollbar-gutter: stable both-edges; + overflow-y: scroll !important; } .scrollbar-styled::-webkit-scrollbar { - width: 8px; - height: 8px; - display: block; + width: 8px; + height: 8px; + display: block; } .scrollbar-styled::-webkit-scrollbar-track { - background: transparent; - margin: 4px; + background: transparent; + margin: 4px; } .scrollbar-styled::-webkit-scrollbar-thumb { - background-color: #94a3b8; - border-radius: 9999px; - border: 2px solid transparent; - background-clip: content-box; - min-height: 40px; + background-color: #94a3b8; + border-radius: 9999px; + border: 2px solid transparent; + background-clip: content-box; + min-height: 40px; } .scrollbar-styled::-webkit-scrollbar-thumb:hover { - background-color: #64748b; + background-color: #64748b; } /* Dark mode */ .dark .scrollbar-styled::-webkit-scrollbar-thumb { - background-color: #475569; + background-color: #475569; } .dark .scrollbar-styled::-webkit-scrollbar-thumb:hover { - background-color: #64748b; + background-color: #64748b; } /* Firefox */ .scrollbar-styled { - scrollbar-width: thin; - scrollbar-color: #94a3b8 transparent; + scrollbar-width: thin; + scrollbar-color: #94a3b8 transparent; } .dark .scrollbar-styled { - scrollbar-color: #475569 transparent; + scrollbar-color: #475569 transparent; } diff --git a/src/main.tsx b/src/main.tsx index c77618e..01ce20f 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,9 +7,9 @@ import App from "./App.tsx"; import { DarkModeProvider } from "./providers/DarkModeProvider.tsx"; createRoot(document.getElementById('root')!).render( - - - - - + + + + + ); diff --git a/src/providers/DarkModeProvider.tsx b/src/providers/DarkModeProvider.tsx index 7a80aa8..b1ee206 100644 --- a/src/providers/DarkModeProvider.tsx +++ b/src/providers/DarkModeProvider.tsx @@ -1,19 +1,11 @@ -import { createContext, useContext, useEffect, useState } from "react"; +import { createContext, useEffect, useState } from "react"; interface DarkModeContextType { isDarkMode: boolean; toggleDarkMode: () => void; } -const DarkModeContext = createContext(undefined); - -export const useDarkMode = () => { - const context = useContext(DarkModeContext); - if (!context) { - throw new Error('useDarkMode must be used within a DarkModeProvider'); - } - return context; -}; +export const DarkModeContext = createContext(undefined); export const DarkModeProvider = ({ children }: { children: React.ReactNode }) => { const [isDarkMode, setIsDarkMode] = useState(() => { diff --git a/src/providers/PortfolioProvider.tsx b/src/providers/PortfolioProvider.tsx new file mode 100644 index 0000000..db88119 --- /dev/null +++ b/src/providers/PortfolioProvider.tsx @@ -0,0 +1,172 @@ +import { format, startOfYear } from "date-fns"; +import { createContext, useMemo, useReducer } from "react"; + +import { Asset, DateRange, HistoricalData, Investment } from "../types"; + +// State Types +interface PortfolioState { + assets: Asset[]; + isLoading: boolean; + dateRange: DateRange; +} + +// Action Types +type PortfolioAction = + | { type: 'SET_LOADING'; payload: boolean } + | { type: 'ADD_ASSET'; payload: Asset } + | { type: 'REMOVE_ASSET'; payload: string } + | { type: 'CLEAR_ASSETS' } + | { type: 'ADD_INVESTMENT'; payload: { assetId: string; investment: Investment } } + | { type: 'REMOVE_INVESTMENT'; payload: { assetId: string; investmentId: string } } + | { type: 'UPDATE_DATE_RANGE'; payload: DateRange } + | { type: 'UPDATE_ASSET_HISTORICAL_DATA'; payload: { assetId: string; historicalData: HistoricalData[]; longName?: string } } + | { type: 'UPDATE_INVESTMENT'; payload: { assetId: string; investmentId: string; investment: Investment } } + | { type: 'CLEAR_INVESTMENTS' } + | { type: 'SET_ASSETS'; payload: Asset[] }; + +// Initial State +const initialState: PortfolioState = { + assets: [], + isLoading: false, + dateRange: { + startDate: format(startOfYear(new Date()), 'yyyy-MM-dd'), + endDate: format(new Date(), 'yyyy-MM-dd'), + }, +}; + +// Reducer +const portfolioReducer = (state: PortfolioState, action: PortfolioAction): PortfolioState => { + switch (action.type) { + case 'SET_LOADING': + return { ...state, isLoading: action.payload }; + + case 'ADD_ASSET': + return { ...state, assets: [...state.assets, action.payload] }; + + case 'REMOVE_ASSET': + return { + ...state, + assets: state.assets.filter(asset => asset.id !== action.payload) + }; + + case 'CLEAR_ASSETS': + return { ...state, assets: [] }; + + case 'ADD_INVESTMENT': + return { + ...state, + assets: state.assets.map(asset => + asset.id === action.payload.assetId + ? { ...asset, investments: [...asset.investments, action.payload.investment] } + : asset + ) + }; + + case 'REMOVE_INVESTMENT': + return { + ...state, + assets: state.assets.map(asset => + asset.id === action.payload.assetId + ? { + ...asset, + investments: asset.investments.filter(inv => inv.id !== action.payload.investmentId) + } + : asset + ) + }; + + case 'UPDATE_DATE_RANGE': + return { ...state, dateRange: action.payload }; + + case 'UPDATE_ASSET_HISTORICAL_DATA': + return { + ...state, + assets: state.assets.map(asset => + asset.id === action.payload.assetId + ? { + ...asset, + historicalData: action.payload.historicalData, + name: action.payload.longName || asset.name + } + : asset + ) + }; + + case 'UPDATE_INVESTMENT': + return { + ...state, + assets: state.assets.map(asset => + asset.id === action.payload.assetId + ? { + ...asset, + investments: asset.investments.map(inv => + inv.id === action.payload.investmentId ? action.payload.investment : inv + ) + } + : asset + ) + }; + + case 'CLEAR_INVESTMENTS': + return { + ...state, + assets: state.assets.map(asset => ({ ...asset, investments: [] })) + }; + + case 'SET_ASSETS': + return { ...state, assets: action.payload }; + + default: + return state; + } +}; + +// Context +export interface PortfolioContextType extends PortfolioState { + setLoading: (loading: boolean) => void; + addAsset: (asset: Asset) => void; + removeAsset: (assetId: string) => void; + clearAssets: () => void; + addInvestment: (assetId: string, investment: Investment) => void; + removeInvestment: (assetId: string, investmentId: string) => void; + updateDateRange: (dateRange: DateRange) => void; + updateAssetHistoricalData: (assetId: string, historicalData: HistoricalData[], longName?: string) => void; + updateInvestment: (assetId: string, investmentId: string, investment: Investment) => void; + clearInvestments: () => void; + setAssets: (assets: Asset[]) => void; +} + +export const PortfolioContext = createContext(null); + +// Provider Component +export const PortfolioProvider = ({ children }: { children: React.ReactNode }) => { + const [state, dispatch] = useReducer(portfolioReducer, initialState); + + // Memoized actions + const actions = useMemo(() => ({ + setLoading: (loading: boolean) => dispatch({ type: 'SET_LOADING', payload: loading }), + addAsset: (asset: Asset) => dispatch({ type: 'ADD_ASSET', payload: asset }), + removeAsset: (assetId: string) => dispatch({ type: 'REMOVE_ASSET', payload: assetId }), + clearAssets: () => dispatch({ type: 'CLEAR_ASSETS' }), + addInvestment: (assetId: string, investment: Investment) => + dispatch({ type: 'ADD_INVESTMENT', payload: { assetId, investment } }), + removeInvestment: (assetId: string, investmentId: string) => + dispatch({ type: 'REMOVE_INVESTMENT', payload: { assetId, investmentId } }), + updateDateRange: (dateRange: DateRange) => + dispatch({ type: 'UPDATE_DATE_RANGE', payload: dateRange }), + updateAssetHistoricalData: (assetId: string, historicalData: HistoricalData[], longName?: string) => + dispatch({ type: 'UPDATE_ASSET_HISTORICAL_DATA', payload: { assetId, historicalData, longName } }), + updateInvestment: (assetId: string, investmentId: string, investment: Investment) => + dispatch({ type: 'UPDATE_INVESTMENT', payload: { assetId, investmentId, investment } }), + clearInvestments: () => dispatch({ type: 'CLEAR_INVESTMENTS' }), + setAssets: (assets: Asset[]) => dispatch({ type: 'SET_ASSETS', payload: assets }), + }), []); + + const value = useMemo(() => ({ ...state, ...actions }), [state, actions]); + + return ( + + {children} + + ); +}; diff --git a/src/services/yahooFinanceService.ts b/src/services/yahooFinanceService.ts index a52289e..515e3f7 100644 --- a/src/services/yahooFinanceService.ts +++ b/src/services/yahooFinanceService.ts @@ -1,42 +1,4 @@ -import { Asset } from "../types"; - -interface YahooQuoteDocument { - symbol: string; - shortName: string; - regularMarketPrice: { - raw: number; - fmt: string; - }; - regularMarketChange: { - raw: number; - fmt: string; - }; - regularMarketPercentChange: { - raw: number; - fmt: string; - }; - exchange: string; - quoteType: string; -} - -interface YahooSearchResponse { - finance: { - result: [{ - documents: YahooQuoteDocument[]; - }]; - error: null | string; - }; -} - -interface YahooChartResult { - timestamp: number[]; - indicators: { - quote: [{ - close: number[]; - }]; - }; -} - +import type { Asset, YahooSearchResponse, YahooChartResult } from "../types"; // this is only needed when hosted staticly without a proxy server or smt // TODO change it to use the proxy server @@ -46,73 +8,79 @@ const YAHOO_API = 'https://query1.finance.yahoo.com'; const API_BASE = isDev ? '/yahoo' : `${CORS_PROXY}${encodeURIComponent(YAHOO_API)}`; export const searchAssets = async (query: string): Promise => { - try { - const params = new URLSearchParams({ - query, - lang: 'en-US', - type: 'equity,etf', - }); + try { + const params = new URLSearchParams({ + query, + lang: 'en-US', + type: 'equity,etf', + longName: 'true', + }); - const url = `${API_BASE}/v1/finance/lookup${!isDev ? encodeURIComponent(`?${params}`) : `?${params}`}`; - const response = await fetch(url); - if (!response.ok) throw new Error('Network response was not ok'); + const url = `${API_BASE}/v1/finance/lookup${!isDev ? encodeURIComponent(`?${params}`) : `?${params}`}`; + const response = await fetch(url); + if (!response.ok) throw new Error('Network response was not ok'); - const data = await response.json() as YahooSearchResponse; + const data = await response.json() as YahooSearchResponse; - if (data.finance.error) { - throw new Error(data.finance.error); + if (data.finance.error) { + throw new Error(data.finance.error); + } + + if (!data.finance.result?.[0]?.documents) { + return []; + } + + return data.finance.result[0].documents + .filter(quote => quote.quoteType === 'equity' || quote.quoteType === 'etf') + .map((quote) => ({ + id: quote.symbol, + isin: '', // not provided by Yahoo Finance API + wkn: '', // not provided by Yahoo Finance API + name: quote.shortName, + rank: quote.rank, + symbol: quote.symbol, + quoteType: quote.quoteType, + price: quote.regularMarketPrice.raw, + priceChange: quote.regularMarketChange.raw, + priceChangePercent: quote.regularMarketPercentChange.raw, + exchange: quote.exchange, + historicalData: [], + investments: [], + })); + } catch (error) { + console.error('Error searching assets:', error); + return []; } - - if (!data.finance.result?.[0]?.documents) { - return []; - } - - return data.finance.result[0].documents - .filter(quote => quote.quoteType === 'equity' || quote.quoteType === 'etf') - .map((quote) => ({ - id: quote.symbol, - isin: '', // not provided by Yahoo Finance API - wkn: '', // not provided by Yahoo Finance API - name: quote.shortName, - symbol: quote.symbol, - price: quote.regularMarketPrice.raw, - priceChange: quote.regularMarketChange.raw, - priceChangePercent: quote.regularMarketPercentChange.raw, - exchange: quote.exchange, - historicalData: [], - investments: [], - })); - } catch (error) { - console.error('Error searching assets:', error); - return []; - } }; export const getHistoricalData = async (symbol: string, startDate: string, endDate: string) => { - try { - const start = Math.floor(new Date(startDate).getTime() / 1000); - const end = Math.floor(new Date(endDate).getTime() / 1000); + try { + const start = Math.floor(new Date(startDate).getTime() / 1000); + const end = Math.floor(new Date(endDate).getTime() / 1000); - const params = new URLSearchParams({ - period1: start.toString(), - period2: end.toString(), - interval: '1d', - }); + const params = new URLSearchParams({ + period1: start.toString(), + period2: end.toString(), + interval: '1d', + }); - const url = `${API_BASE}/v8/finance/chart/${symbol}${!isDev ? encodeURIComponent(`?${params}`) : `?${params}`}`; - const response = await fetch(url); - if (!response.ok) throw new Error('Network response was not ok'); + const url = `${API_BASE}/v8/finance/chart/${symbol}${!isDev ? encodeURIComponent(`?${params}`) : `?${params}`}`; + const response = await fetch(url); + if (!response.ok) throw new Error('Network response was not ok'); - const data = await response.json(); - const { timestamp, indicators } = data.chart.result[0] as YahooChartResult; - const quotes = indicators.quote[0]; + const data = await response.json(); + const { timestamp, indicators, meta } = data.chart.result[0] as YahooChartResult; + const quotes = indicators.quote[0]; - return timestamp.map((time: number, index: number) => ({ - date: new Date(time * 1000).toISOString().split('T')[0], - price: quotes.close[index], - })); - } catch (error) { - console.error('Error fetching historical data:', error); - return []; - } + return { + historicalData: timestamp.map((time: number, index: number) => ({ + date: new Date(time * 1000).toISOString().split('T')[0], + price: quotes.close[index], + })), + longName: meta.longName + } + } catch (error) { + console.error('Error fetching historical data:', error); + return { historicalData: [], longName: '' }; + } }; diff --git a/src/store/portfolioStore.ts b/src/store/portfolioStore.ts deleted file mode 100644 index ba08554..0000000 --- a/src/store/portfolioStore.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { format, startOfYear } from "date-fns"; -import { create } from "zustand"; - -import { Asset, DateRange, HistoricalData, Investment } from "../types"; - -interface PortfolioState { - assets: Asset[]; - dateRange: DateRange; - addAsset: (asset: Asset) => void; - removeAsset: (assetId: string) => void; - clearAssets: () => void; - addInvestment: (assetId: string, investment: Investment) => void; - removeInvestment: (assetId: string, investmentId: string) => void; - updateDateRange: (dateRange: DateRange) => void; - updateAssetHistoricalData: (assetId: string, historicalData: HistoricalData[]) => void; - updateInvestment: (assetId: string, investmentId: string, updatedInvestment: Investment) => void; - clearInvestments: () => void; - setAssets: (assets: Asset[]) => void; -} - -export const usePortfolioStore = create((set) => ({ - assets: [], - dateRange: { - startDate: format(startOfYear(new Date()), 'yyyy-MM-dd'), - endDate: format(new Date(), 'yyyy-MM-dd'), - }, - addAsset: (asset) => - set((state) => ({ assets: [...state.assets, asset] })), - removeAsset: (assetId) => - set((state) => ({ - assets: state.assets.filter((asset) => asset.id !== assetId), - })), - clearAssets: () => - set(() => ({ assets: [] })), - addInvestment: (assetId, investment) => - set((state) => ({ - assets: state.assets.map((asset) => - asset.id === assetId - ? { ...asset, investments: [...asset.investments, investment] } - : asset - ), - })), - removeInvestment: (assetId, investmentId) => - set((state) => ({ - assets: state.assets.map((asset) => - asset.id === assetId - ? { - ...asset, - investments: asset.investments.filter((inv) => inv.id !== investmentId), - } - : asset - ), - })), - updateDateRange: (dateRange) => - set(() => ({ dateRange })), - updateAssetHistoricalData: (assetId, historicalData) => - set((state) => ({ - assets: state.assets.map((asset) => - asset.id === assetId - ? { ...asset, historicalData } - : asset - ), - })), - updateInvestment: (assetId, investmentId, updatedInvestment) => - set((state) => ({ - assets: state.assets.map((asset) => - asset.id === assetId - ? { - ...asset, - investments: asset.investments.map((inv) => - inv.id === investmentId ? updatedInvestment : inv - ), - } - : asset - ), - })), - clearInvestments: () => - set((state) => ({ - assets: state.assets.map((asset) => ({ ...asset, investments: [] })), - })), - setAssets: (assets) => set({ assets }), -})); diff --git a/src/types/index.ts b/src/types/index.ts index 940a11e..7e981eb 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,35 +1,37 @@ export interface Asset { - id: string; - isin: string; - name: string; - wkn: string; - symbol: string; - historicalData: HistoricalData[]; - investments: Investment[]; + id: string; + isin: string; + name: string; + quoteType: string; + rank: string; + wkn: string; + symbol: string; + historicalData: HistoricalData[]; + investments: Investment[]; } export interface HistoricalData { - date: string; - price: number; + date: string; + price: number; } export interface Investment { - id: string; - assetId: string; - type: 'single' | 'periodic'; - amount: number; - date?: string; - periodicGroupId?: string; + id: string; + assetId: string; + type: 'single' | 'periodic'; + amount: number; + date?: string; + periodicGroupId?: string; } export interface PeriodicSettings { - dayOfMonth: number; - interval: number; - dynamic?: { - type: 'percentage' | 'fixed'; - value: number; - yearInterval: number; - }; + dayOfMonth: number; + interval: number; + dynamic?: { + type: 'percentage' | 'fixed'; + value: number; + yearInterval: number; + }; } export interface InvestmentPerformance { @@ -41,10 +43,166 @@ export interface InvestmentPerformance { currentValue: number; performancePercentage: number; periodicGroupId?: string; - avgBuyIn: number; } export interface DateRange { - startDate: string; - endDate: string; + startDate: string; + endDate: string; +} + +export interface InvestmentPerformance { + id: string; + assetName: string; + date: string; + investedAmount: number; + investedAtPrice: number; + currentValue: number; + performancePercentage: number; +} + +export interface PortfolioPerformance { + investments: InvestmentPerformance[]; + summary: { + totalInvested: number; + currentValue: number; + performancePercentage: number; + performancePerAnnoPerformance: number; + ttworValue: number; + ttworPercentage: number; + bestPerformancePerAnno: { percentage: number, year: number }[]; + worstPerformancePerAnno: { percentage: number, year: number }[]; + }; +} + +export type DayData = { + date: string; + total: number; + invested: number; + percentageChange: number; + /* Current price of asset */ + assets: { [key: string]: number }; +}; + +export interface WithdrawalPlan { + amount: number; + interval: 'monthly' | 'yearly'; + startTrigger: 'date' | 'portfolioValue' | 'auto'; + startDate?: string; + startPortfolioValue?: number; + enabled: boolean; + autoStrategy?: { + type: 'maintain' | 'deplete' | 'grow'; + targetYears?: number; + targetGrowth?: number; + }; +} + +export interface ProjectionData { + date: string; + value: number; + invested: number; + withdrawals: number; + totalWithdrawn: number; +} + +export interface SustainabilityAnalysis { + yearsToReachTarget: number; + targetValue: number; + sustainableYears: number | 'infinite'; +} + +export interface PeriodicSettings { + startDate: string; + dayOfMonth: number; + interval: number; + amount: number; + dynamic?: { + type: 'percentage' | 'fixed'; + value: number; + yearInterval: number; + }; +} + +interface YahooQuoteDocument { + symbol: string; + shortName: string; + rank: string; + regularMarketPrice: { + raw: number; + fmt: string; + }; + regularMarketChange: { + raw: number; + fmt: string; + }; + regularMarketPercentChange: { + raw: number; + fmt: string; + }; + exchange: string; + quoteType: string; +} + +export interface YahooSearchResponse { + finance: { + result: [{ + documents: YahooQuoteDocument[]; + }]; + error: null | string; + }; +} + +export interface YahooChartResult { + timestamp: number[]; + meta: { + currency: string; + symbol: string; + exchangeName: string; + fullExchangeName: string; + instrumentType: string; + firstTradeDate: number; + regularMarketTime: number; + hasPrePostMarketData: boolean; + gmtoffset: number; + timezone: string; + exchangeTimezoneName: string; + regularMarketPrice: number; + fiftyTwoWeekHigh: number; + fiftyTwoWeekLow: number; + regularMarketDayHigh: number; + regularMarketDayLow: number; + regularMarketVolume: number; + longName: string; + shortName: string; + chartPreviousClose: number; + priceHint: number; + currentTradingPeriod: { + pre: { + timezone: string; + start: number; + end: number; + gmtoffset: number; + }; + regular: { + timezone: string; + start: number; + end: number; + gmtoffset: number; + }; + post: { + timezone: string; + start: number; + end: number; + gmtoffset: number; + }; + }; + dataGranularity: string; + range: string; + validRanges: string[]; + } + indicators: { + quote: [{ + close: number[]; + }]; + }; } diff --git a/src/utils/calculations/assetValue.ts b/src/utils/calculations/assetValue.ts index 463080f..d049235 100644 --- a/src/utils/calculations/assetValue.ts +++ b/src/utils/calculations/assetValue.ts @@ -1,25 +1,13 @@ import { isAfter, isBefore, isSameDay } from "date-fns"; -import { Asset, Investment } from "../../types"; - -export interface PeriodicSettings { - startDate: string; - dayOfMonth: number; - interval: number; - amount: number; - dynamic?: { - type: 'percentage' | 'fixed'; - value: number; - yearInterval: number; - }; -} +import type { Asset, Investment, PeriodicSettings } from "../../types"; export const calculateAssetValueAtDate = (asset: Asset, date: Date, currentPrice: number) => { let totalShares = 0; - const buyIns:number[] = []; + const buyIns: number[] = []; // Calculate shares for each investment up to the given date - for(const investment of asset.investments) { + for (const investment of asset.investments) { const invDate = new Date(investment.date!); if (isAfter(invDate, date) || isSameDay(invDate, date)) continue; @@ -70,7 +58,8 @@ export const generatePeriodicInvestments = (settings: PeriodicSettings, endDate: // Check if we've reached a year interval for increase if (yearsSinceStart > 0 && yearsSinceStart % settings.dynamic.yearInterval === 0) { if (settings.dynamic.type === 'percentage') { - currentAmount *= (1 + settings.dynamic.value / 100); + console.log('percentage', settings.dynamic.value, (1 + (settings.dynamic.value / 100))); + currentAmount *= (1 + (settings.dynamic.value / 100)); } else { currentAmount += settings.dynamic.value; } diff --git a/src/utils/calculations/futureProjection.ts b/src/utils/calculations/futureProjection.ts index d872f6e..c40206e 100644 --- a/src/utils/calculations/futureProjection.ts +++ b/src/utils/calculations/futureProjection.ts @@ -1,244 +1,242 @@ import { addMonths, differenceInYears, format } from "date-fns"; -import { Asset, Investment } from "../../types"; - import type { - ProjectionData, SustainabilityAnalysis, WithdrawalPlan -} from "../../components/FutureProjectionModal"; + ProjectionData, SustainabilityAnalysis, WithdrawalPlan, Asset, Investment +} from "../../types"; const findOptimalStartingPoint = ( - currentPortfolioValue: number, - monthlyGrowth: number, - desiredWithdrawal: number, - strategy: WithdrawalPlan['autoStrategy'], - interval: 'monthly' | 'yearly' + currentPortfolioValue: number, + monthlyGrowth: number, + desiredWithdrawal: number, + strategy: WithdrawalPlan['autoStrategy'], + interval: 'monthly' | 'yearly' ): { startDate: string; requiredPortfolioValue: number } => { - const monthlyWithdrawal = interval === 'yearly' ? desiredWithdrawal / 12 : desiredWithdrawal; - let requiredPortfolioValue = 0; + const monthlyWithdrawal = interval === 'yearly' ? desiredWithdrawal / 12 : desiredWithdrawal; + let requiredPortfolioValue = 0; - // Declare variables outside switch - const months = (strategy?.targetYears || 30) * 12; - const r = monthlyGrowth; - const targetGrowth = (strategy?.targetGrowth || 2) / 100; - const targetMonthlyGrowth = Math.pow(1 + targetGrowth, 1/12) - 1; + // Declare variables outside switch + const months = (strategy?.targetYears || 30) * 12; + const r = monthlyGrowth; + const targetGrowth = (strategy?.targetGrowth || 2) / 100; + const targetMonthlyGrowth = Math.pow(1 + targetGrowth, 1 / 12) - 1; - switch (strategy?.type) { - case 'maintain': - requiredPortfolioValue = monthlyWithdrawal / monthlyGrowth; - break; - case 'deplete': - requiredPortfolioValue = (monthlyWithdrawal * (Math.pow(1 + r, months) - 1)) / (r * Math.pow(1 + r, months)); - break; - case 'grow': - requiredPortfolioValue = monthlyWithdrawal / (monthlyGrowth - targetMonthlyGrowth); - break; - } + switch (strategy?.type) { + case 'maintain': + requiredPortfolioValue = monthlyWithdrawal / monthlyGrowth; + break; + case 'deplete': + requiredPortfolioValue = (monthlyWithdrawal * (Math.pow(1 + r, months) - 1)) / (r * Math.pow(1 + r, months)); + break; + case 'grow': + requiredPortfolioValue = monthlyWithdrawal / (monthlyGrowth - targetMonthlyGrowth); + break; + } - // Calculate when we'll reach the required value - const monthsToReach = Math.ceil( - Math.log(requiredPortfolioValue / currentPortfolioValue) / - Math.log(1 + monthlyGrowth) - ); + // Calculate when we'll reach the required value + const monthsToReach = Math.ceil( + Math.log(requiredPortfolioValue / currentPortfolioValue) / + Math.log(1 + monthlyGrowth) + ); - const startDate = new Date(); - startDate.setMonth(startDate.getMonth() + Math.max(0, monthsToReach)); + const startDate = new Date(); + startDate.setMonth(startDate.getMonth() + Math.max(0, monthsToReach)); - return { - startDate: startDate.toISOString().split('T')[0], - requiredPortfolioValue, - }; + return { + startDate: startDate.toISOString().split('T')[0], + requiredPortfolioValue, + }; }; export const calculateFutureProjection = async ( - currentAssets: Asset[], - yearsToProject: number, - annualReturnRate: number, - withdrawalPlan?: WithdrawalPlan, + currentAssets: Asset[], + yearsToProject: number, + annualReturnRate: number, + withdrawalPlan?: WithdrawalPlan, ): Promise<{ - projection: ProjectionData[]; - sustainability: SustainabilityAnalysis; + projection: ProjectionData[]; + sustainability: SustainabilityAnalysis; }> => { - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise(resolve => setTimeout(resolve, 1000)); - const projectionData: ProjectionData[] = []; - const maxProjectionYears = 100; // Project up to 100 years to find true sustainability - const endDateForDisplay = addMonths(new Date(), yearsToProject * 12); - const endDateForCalculation = addMonths(new Date(), maxProjectionYears * 12); + const projectionData: ProjectionData[] = []; + const maxProjectionYears = 100; // Project up to 100 years to find true sustainability + const endDateForDisplay = addMonths(new Date(), yearsToProject * 12); + const endDateForCalculation = addMonths(new Date(), maxProjectionYears * 12); - // Get all periodic investment patterns - const periodicInvestments = currentAssets.flatMap(asset => { - const patterns = new Map(); + // Get all periodic investment patterns + const periodicInvestments = currentAssets.flatMap(asset => { + const patterns = new Map(); - asset.investments.forEach(inv => { - if (inv.type === 'periodic' && inv.periodicGroupId) { - if (!patterns.has(inv.periodicGroupId)) { - patterns.set(inv.periodicGroupId, []); - } - patterns.get(inv.periodicGroupId)!.push(inv); - } + asset.investments.forEach(inv => { + if (inv.type === 'periodic' && inv.periodicGroupId) { + if (!patterns.has(inv.periodicGroupId)) { + patterns.set(inv.periodicGroupId, []); + } + patterns.get(inv.periodicGroupId)!.push(inv); + } + }); + + return Array.from(patterns.values()) + .map(group => ({ + pattern: group.sort((a, b) => + new Date(a.date!).getTime() - new Date(b.date!).getTime() + ) + })); }); - return Array.from(patterns.values()) - .map(group => ({ - pattern: group.sort((a, b) => - new Date(a.date!).getTime() - new Date(b.date!).getTime() - ) - })); - }); + // Project future investments + const futureInvestments = periodicInvestments.flatMap(({ pattern }) => { + if (pattern.length < 2) return []; - // Project future investments - const futureInvestments = periodicInvestments.flatMap(({ pattern }) => { - if (pattern.length < 2) return []; + const lastInvestment = pattern[pattern.length - 1]; + const secondLastInvestment = pattern[pattern.length - 2]; - const lastInvestment = pattern[pattern.length - 1]; - const secondLastInvestment = pattern[pattern.length - 2]; + const interval = new Date(lastInvestment.date!).getTime() - + new Date(secondLastInvestment.date!).getTime(); + const amountDiff = lastInvestment.amount - secondLastInvestment.amount; - const interval = new Date(lastInvestment.date!).getTime() - - new Date(secondLastInvestment.date!).getTime(); - const amountDiff = lastInvestment.amount - secondLastInvestment.amount; + const future: Investment[] = []; + let currentDate = new Date(lastInvestment.date!); + let currentAmount = lastInvestment.amount; - const future: Investment[] = []; - let currentDate = new Date(lastInvestment.date!); - let currentAmount = lastInvestment.amount; + while (currentDate <= endDateForCalculation) { + currentDate = new Date(currentDate.getTime() + interval); + currentAmount += amountDiff; + + future.push({ + ...lastInvestment, + date: format(currentDate, 'yyyy-MM-dd'), + amount: currentAmount, + }); + } + + return future; + }); + + // Calculate monthly values + let currentDate = new Date(); + let totalInvested = currentAssets.reduce( + (sum, asset) => sum + asset.investments.reduce( + (assetSum, inv) => assetSum + inv.amount, 0 + ), 0 + ); + + let totalWithdrawn = 0; + let yearsToReachTarget = 0; + let targetValue = 0; + let sustainableYears: number | 'infinite' = 'infinite'; + let portfolioValue = totalInvested; // Initialize portfolio value with current investments + let withdrawalsStarted = false; + let withdrawalStartDate: Date | null = null; + let portfolioDepletionDate: Date | null = null; + + // Calculate optimal withdrawal plan if auto strategy is selected + if (withdrawalPlan?.enabled && withdrawalPlan.startTrigger === 'auto') { + const { startDate, requiredPortfolioValue } = findOptimalStartingPoint( + portfolioValue, + Math.pow(1 + annualReturnRate / 100, 1 / 12) - 1, + withdrawalPlan.amount, + withdrawalPlan.autoStrategy, + withdrawalPlan.interval + ); + + withdrawalPlan.startDate = startDate; + withdrawalPlan.startPortfolioValue = requiredPortfolioValue; + } while (currentDate <= endDateForCalculation) { - currentDate = new Date(currentDate.getTime() + interval); - currentAmount += amountDiff; + // Check if withdrawals should start + if (!withdrawalsStarted && withdrawalPlan?.enabled) { + withdrawalsStarted = withdrawalPlan.startTrigger === 'date' + ? new Date(currentDate) >= new Date(withdrawalPlan.startDate!) + : portfolioValue >= (withdrawalPlan.startPortfolioValue || 0); - future.push({ - ...lastInvestment, - date: format(currentDate, 'yyyy-MM-dd'), - amount: currentAmount, - }); - } - - return future; - }); - - // Calculate monthly values - let currentDate = new Date(); - let totalInvested = currentAssets.reduce( - (sum, asset) => sum + asset.investments.reduce( - (assetSum, inv) => assetSum + inv.amount, 0 - ), 0 - ); - - let totalWithdrawn = 0; - let yearsToReachTarget = 0; - let targetValue = 0; - let sustainableYears: number | 'infinite' = 'infinite'; - let portfolioValue = totalInvested; // Initialize portfolio value with current investments - let withdrawalsStarted = false; - let withdrawalStartDate: Date | null = null; - let portfolioDepletionDate: Date | null = null; - - // Calculate optimal withdrawal plan if auto strategy is selected - if (withdrawalPlan?.enabled && withdrawalPlan.startTrigger === 'auto') { - const { startDate, requiredPortfolioValue } = findOptimalStartingPoint( - portfolioValue, - Math.pow(1 + annualReturnRate/100, 1/12) - 1, - withdrawalPlan.amount, - withdrawalPlan.autoStrategy, - withdrawalPlan.interval - ); - - withdrawalPlan.startDate = startDate; - withdrawalPlan.startPortfolioValue = requiredPortfolioValue; - } - - while (currentDate <= endDateForCalculation) { - // Check if withdrawals should start - if (!withdrawalsStarted && withdrawalPlan?.enabled) { - withdrawalsStarted = withdrawalPlan.startTrigger === 'date' - ? new Date(currentDate) >= new Date(withdrawalPlan.startDate!) - : portfolioValue >= (withdrawalPlan.startPortfolioValue || 0); - - if (withdrawalsStarted) { - withdrawalStartDate = new Date(currentDate); - } - } - - // Handle monthly growth if portfolio isn't depleted - if (portfolioValue > 0) { - const monthlyReturn = Math.pow(1 + annualReturnRate/100, 1/12) - 1; - portfolioValue *= (1 + monthlyReturn); - } - - // Add new investments only if withdrawals haven't started - if (!withdrawalsStarted) { - const monthInvestments = futureInvestments.filter( - inv => new Date(inv.date!).getMonth() === currentDate.getMonth() && - new Date(inv.date!).getFullYear() === currentDate.getFullYear() - ); - - const monthlyInvestment = monthInvestments.reduce( - (sum, inv) => sum + inv.amount, 0 - ); - totalInvested += monthlyInvestment; - portfolioValue += monthlyInvestment; - } - - - // Handle withdrawals - let monthlyWithdrawal = 0; - if (withdrawalsStarted && portfolioValue > 0) { - monthlyWithdrawal = withdrawalPlan!.interval === 'monthly' - ? withdrawalPlan!.amount - : (currentDate.getMonth() === 0 ? withdrawalPlan!.amount : 0); - - portfolioValue -= monthlyWithdrawal; - if (portfolioValue < 0) { - monthlyWithdrawal += portfolioValue; // Adjust final withdrawal - portfolioValue = 0; - if (sustainableYears === 'infinite') { - sustainableYears = differenceInYears(currentDate, withdrawalStartDate!); + if (withdrawalsStarted) { + withdrawalStartDate = new Date(currentDate); + } } - } - totalWithdrawn += monthlyWithdrawal; + + // Handle monthly growth if portfolio isn't depleted + if (portfolioValue > 0) { + const monthlyReturn = Math.pow(1 + annualReturnRate / 100, 1 / 12) - 1; + portfolioValue *= (1 + monthlyReturn); + } + + // Add new investments only if withdrawals haven't started + if (!withdrawalsStarted) { + const monthInvestments = futureInvestments.filter( + inv => new Date(inv.date!).getMonth() === currentDate.getMonth() && + new Date(inv.date!).getFullYear() === currentDate.getFullYear() + ); + + const monthlyInvestment = monthInvestments.reduce( + (sum, inv) => sum + inv.amount, 0 + ); + totalInvested += monthlyInvestment; + portfolioValue += monthlyInvestment; + } + + + // Handle withdrawals + let monthlyWithdrawal = 0; + if (withdrawalsStarted && portfolioValue > 0) { + monthlyWithdrawal = withdrawalPlan!.interval === 'monthly' + ? withdrawalPlan!.amount + : (currentDate.getMonth() === 0 ? withdrawalPlan!.amount : 0); + + portfolioValue -= monthlyWithdrawal; + if (portfolioValue < 0) { + monthlyWithdrawal += portfolioValue; // Adjust final withdrawal + portfolioValue = 0; + if (sustainableYears === 'infinite') { + sustainableYears = differenceInYears(currentDate, withdrawalStartDate!); + } + } + totalWithdrawn += monthlyWithdrawal; + } + + // Update target metrics + if (withdrawalsStarted && !targetValue) { + targetValue = portfolioValue; + yearsToReachTarget = differenceInYears(currentDate, new Date()); + } + + if (portfolioValue <= 0 && !portfolioDepletionDate) { + portfolioDepletionDate = new Date(currentDate); + } + + // Only add to projection data if within display timeframe + if (currentDate <= endDateForDisplay) { + projectionData.push({ + date: format(currentDate, 'yyyy-MM-dd'), + value: Math.max(0, portfolioValue), + invested: totalInvested, + withdrawals: monthlyWithdrawal, + totalWithdrawn, + }); + } + + currentDate = addMonths(currentDate, 1); } - // Update target metrics - if (withdrawalsStarted && !targetValue) { - targetValue = portfolioValue; - yearsToReachTarget = differenceInYears(currentDate, new Date()); + // Calculate actual sustainability duration + let actualSustainableYears: number | 'infinite' = 'infinite'; + if (portfolioDepletionDate) { + actualSustainableYears = differenceInYears( + portfolioDepletionDate, + withdrawalStartDate || new Date() + ); + } else if (portfolioValue > 0) { + // If portfolio is still growing after maxProjectionYears, it's truly sustainable + actualSustainableYears = 'infinite'; } - if (portfolioValue <= 0 && !portfolioDepletionDate) { - portfolioDepletionDate = new Date(currentDate); - } - - // Only add to projection data if within display timeframe - if (currentDate <= endDateForDisplay) { - projectionData.push({ - date: format(currentDate, 'yyyy-MM-dd'), - value: Math.max(0, portfolioValue), - invested: totalInvested, - withdrawals: monthlyWithdrawal, - totalWithdrawn, - }); - } - - currentDate = addMonths(currentDate, 1); - } - - // Calculate actual sustainability duration - let actualSustainableYears: number | 'infinite' = 'infinite'; - if (portfolioDepletionDate) { - actualSustainableYears = differenceInYears( - portfolioDepletionDate, - withdrawalStartDate || new Date() - ); - } else if (portfolioValue > 0) { - // If portfolio is still growing after maxProjectionYears, it's truly sustainable - actualSustainableYears = 'infinite'; - } - - return { - projection: projectionData, - sustainability: { - yearsToReachTarget, - targetValue, - sustainableYears: actualSustainableYears, - }, - }; + return { + projection: projectionData, + sustainability: { + yearsToReachTarget, + targetValue, + sustainableYears: actualSustainableYears, + }, + }; }; diff --git a/src/utils/calculations/performance.ts b/src/utils/calculations/performance.ts index ccf3d56..f436c90 100644 --- a/src/utils/calculations/performance.ts +++ b/src/utils/calculations/performance.ts @@ -1,28 +1,7 @@ import { differenceInDays, isAfter, isBefore } from "date-fns"; -import { Asset } from "../../types"; +import type { Asset, InvestmentPerformance, PortfolioPerformance } from "../../types"; -export interface InvestmentPerformance { - id: string; - assetName: string; - date: string; - investedAmount: number; - investedAtPrice: number; - currentValue: number; - performancePercentage: number; -} - -export interface PortfolioPerformance { - investments: InvestmentPerformance[]; - summary: { - totalInvested: number; - currentValue: number; - performancePercentage: number; - performancePerAnnoPerformance: number; - ttworValue: number; - ttworPercentage: number; - }; -} export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerformance => { const investments: InvestmentPerformance[] = []; @@ -36,7 +15,7 @@ export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerfor const investedPerAsset: Record = {}; // Sammle erste und letzte Preise fรผr jedes Asset - for(const asset of assets) { + for (const asset of assets) { firstDayPrices[asset.id] = asset.historicalData[0]?.price || 0; currentPrices[asset.id] = asset.historicalData[asset.historicalData.length - 1]?.price || 0; investedPerAsset[asset.id] = asset.investments.reduce((sum, inv) => sum + inv.amount, 0); @@ -52,8 +31,8 @@ export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerfor }, 0); // Finde das frรผheste Investmentdatum - for(const asset of assets) { - for(const investment of asset.investments) { + for (const asset of assets) { + for (const investment of asset.investments) { const investmentDate = new Date(investment.date!); if (!earliestDate || isBefore(investmentDate, earliestDate)) { earliestDate = investmentDate; @@ -61,11 +40,90 @@ export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerfor } } + // Calculate portfolio-level annual performances + const annualPerformances: { year: number; percentage: number }[] = []; + + // Calculate portfolio performance for each year + const now = new Date(); + const startYear = earliestDate ? earliestDate.getFullYear() : now.getFullYear(); + const endYear = now.getFullYear(); + + for (let year = startYear; year <= endYear; year++) { + const yearStart = new Date(year, 0, 1); // 1. Januar + const yearEnd = year === endYear ? new Date(year, now.getMonth(), now.getDate()) : new Date(year, 11, 31); // Aktuelles Datum oder 31. Dez. + + const investmentsPerformances:number[] = []; + + for (const asset of assets) { + // Get prices for the start and end of the year + const startPrice = asset.historicalData.filter(d => + new Date(d.date).getFullYear() === year && + new Date(d.date).getMonth() === 0 + ).sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()).find(d => d.price !== 0)?.price || 0; + + const endPrice = asset.historicalData.filter(d => + new Date(d.date).getFullYear() === year + ).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()).find(d => d.price !== 0)?.price || 0; + + if (startPrice === 0 || endPrice === 0) { + console.warn(`Skipping asset for year ${year} due to missing start or end price`); + continue; // รœberspringe, wenn keine Daten vorhanden + } + + // Get all investments made before or during this year + const relevantInvestments = asset.investments.filter(inv => + new Date(inv.date!) <= yearEnd && new Date(inv.date!) >= yearStart + ); + + for (const investment of relevantInvestments) { + const investmentPrice = asset.historicalData.find( + (data) => data.date === investment.date + )?.price || 0; + + const previousPrice = investmentPrice || asset.historicalData.filter( + (data) => isBefore(new Date(data.date), new Date(investment.date!)) + ).reverse().find((v) => v.price !== 0)?.price || 0; + + const buyInPrice = investmentPrice || previousPrice || asset.historicalData.filter( + (data) => isAfter(new Date(data.date), new Date(investment.date!)) + ).find((v) => v.price !== 0)?.price || 0; + + + if (buyInPrice > 0) { + const shares = investment.amount / buyInPrice; // Berechne Anzahl der Shares + const endValue = shares * endPrice; + const startValue = shares * startPrice; + investmentsPerformances.push((endValue - startValue) / startValue * 100); + } + } + } + + // Calculate performance for the year + if (investmentsPerformances.length > 0) { + const percentage = investmentsPerformances.reduce((acc, curr) => acc + curr, 0) / investmentsPerformances.length; + + if (!isNaN(percentage)) { + annualPerformances.push({ year, percentage }); + } else { + console.warn(`Invalid percentage calculated for year ${year}`); + } + } else { + console.warn(`Skipping year ${year} due to zero portfolio values`); + } + } + + // Get best and worst years for the entire portfolio + const bestPerformancePerAnno = annualPerformances.length > 0 + ? Array.from(annualPerformances).sort((a, b) => b.percentage - a.percentage) + : []; + + const worstPerformancePerAnno = Array.from(bestPerformancePerAnno).reverse() + // Normale Performance-Berechnungen... - for(const asset of assets) { + for (const asset of assets) { const currentPrice = asset.historicalData[asset.historicalData.length - 1]?.price || 0; - for(const investment of asset.investments) { + for (const investment of asset.investments) { const investmentPrice = asset.historicalData.find( (data) => data.date === investment.date )?.price || 0; @@ -87,6 +145,7 @@ export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerfor date: investment.date!, investedAmount: investment.amount, investedAtPrice: buyInPrice, + periodicGroupId: investment.periodicGroupId, currentValue, performancePercentage: investment.amount > 0 ? (((currentValue - investment.amount) / investment.amount)) * 100 @@ -128,6 +187,8 @@ export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerfor performancePerAnnoPerformance, ttworValue, ttworPercentage, + worstPerformancePerAnno: worstPerformancePerAnno, + bestPerformancePerAnno: bestPerformancePerAnno }, }; }; diff --git a/src/utils/calculations/portfolioValue.ts b/src/utils/calculations/portfolioValue.ts index 61956e4..29fbf08 100644 --- a/src/utils/calculations/portfolioValue.ts +++ b/src/utils/calculations/portfolioValue.ts @@ -1,16 +1,9 @@ import { addDays, isAfter, isBefore } from "date-fns"; -import { Asset, DateRange } from "../../types"; import { calculateAssetValueAtDate } from "./assetValue"; -type DayData = { - date: string; - total: number; - invested: number; - percentageChange: number; - /* Current price of asset */ - assets: { [key: string]: number }; -}; +import type { Asset, DateRange, DayData } from "../../types"; + export const calculatePortfolioValue = (assets: Asset[], dateRange: DateRange) => { const { startDate, endDate } = dateRange; const data: DayData[] = []; @@ -31,9 +24,9 @@ export const calculatePortfolioValue = (assets: Asset[], dateRange: DateRange) = // this should contain the percentage gain of all investments till now const pPercents: number[] = []; - for(const asset of assets) { + for (const asset of assets) { // calculate the invested kapital - for(const investment of asset.investments) { + for (const investment of asset.investments) { if (!isAfter(new Date(investment.date!), currentDate)) { dayData.invested += investment.amount; } @@ -56,7 +49,7 @@ export const calculatePortfolioValue = (assets: Asset[], dateRange: DateRange) = dayData.assets[asset.id] = currentValueOfAsset; const percent = ((currentValueOfAsset - avgBuyIn) / avgBuyIn) * 100; - if(!Number.isNaN(percent)) pPercents.push(percent); + if (!Number.isNaN(percent)) pPercents.push(percent); } } diff --git a/src/utils/export.ts b/src/utils/export.ts new file mode 100644 index 0000000..f48ce24 --- /dev/null +++ b/src/utils/export.ts @@ -0,0 +1,338 @@ +import "jspdf-autotable"; + +import { jsPDF } from "jspdf"; + +import { Asset } from "../types"; +import { calculateFutureProjection } from "./calculations/futureProjection"; + +// Add type augmentation for the autotable plugin +interface jsPDFWithPlugin extends jsPDF { + autoTable: (options: any) => void; +} + +const formatEuro = (value: number) => { + return `โ‚ฌ${value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ".").replace(".", ",").replace(/,(\d{3})/g, ".$1")}`; +}; + +export const downloadTableAsCSV = (tableData: any[], filename: string) => { + const headers = Object.keys(tableData[0]) + .filter(header => !header.toLowerCase().includes('id')); + + const csvContent = [ + headers.map(title => title.charAt(0).toUpperCase() + title.slice(1)).join(','), + ...tableData.map(row => + headers.map(header => { + const value = row[header]?.toString().replace(/,/g, ''); + return isNaN(Number(value)) + ? `"${value}"` + : formatEuro(Number(value)).replace('โ‚ฌ', ''); + }).join(',') + ) + ].join('\n'); + + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = `${filename}.csv`; + link.click(); +}; + +export const generatePortfolioPDF = async ( + assets: Asset[], + performance: any, + savingsPlansPerformance: any[], + performancePerAnno: number +) => { + const doc = new jsPDF() as jsPDFWithPlugin; + doc.setFont('Arial', 'normal'); + let yPos = 20; + + // Title + doc.setFontSize(20); + doc.text('Portfolio Analysis Report', 15, yPos); + yPos += 15; + + // Explanations + doc.setFontSize(12); + doc.setTextColor(100); + + // TTWOR Explanation + doc.text('Understanding TTWOR (Time Travel Without Risk):', 15, yPos); + yPos += 7; + const ttworText = + 'TTWOR shows how your portfolio would have performed if all investments had been made at ' + + 'the beginning of the period. This metric helps evaluate the impact of your investment ' + + 'timing strategy compared to a single early investment. A higher portfolio performance ' + + 'than TTWOR indicates successful timing of investments.'; + + const ttworLines = doc.splitTextToSize(ttworText, 180); + doc.text(ttworLines, 20, yPos); + yPos += ttworLines.length * 7; + + doc.setTextColor(0); + + // Portfolio Summary + doc.setFontSize(16); + doc.text('Portfolio Summary', 15, yPos); + yPos += 10; + + doc.setFontSize(12); + doc.text(`Total Invested: ${formatEuro(performance.summary.totalInvested)}`, 20, yPos); + yPos += 7; + doc.text(`Current Value: ${formatEuro(performance.summary.currentValue)}`, 20, yPos); + yPos += 7; + doc.text(`Performance: ${performance.summary.performancePercentage.toFixed(2)}% (p.a. ${performance.summary.performancePerAnnoPerformance.toFixed(2)}%)`, 20, yPos); + yPos += 7; + + // TTWOR values in italic + doc.setFont('Arial', 'italic'); + doc.text(`TTWOR* Value: ${formatEuro(performance.summary.ttworValue)} (would perform: ${performance.summary.ttworPercentage.toFixed(2)}%)`, 20, yPos); + doc.setFont('Arial', 'normal'); + yPos += 15; + + // Add Positions Overview table + doc.setFontSize(16); + doc.text('Positions Overview', 15, yPos); + yPos += 10; + + // Prepare positions data + const positionsTableData = [ + // Summary row + [ + 'Total Portfolio', + '', + '', + formatEuro(performance.summary.totalInvested), + formatEuro(performance.summary.currentValue), + '', + `${performance.summary.performancePercentage.toFixed(2)}% (p.a. ${performance.summary.performancePerAnnoPerformance.toFixed(2)}%)`, + ], + // TTWOR row + [ + 'TTWOR*', + '', + performance.investments[0]?.date + ? new Date(performance.investments[0].date).toLocaleDateString('de-DE') + : '', + formatEuro(performance.summary.totalInvested), + formatEuro(performance.summary.ttworValue), + '', + `${performance.summary.ttworPercentage.toFixed(2)}%`, + ], + // Individual positions + ...performance.investments.sort((a: any, b: any) => a.date.localeCompare(b.date)).map((inv: any) => { + const asset = assets.find(a => a.name === inv.assetName)!; + const investment = asset.investments.find(i => i.id === inv.id)! || inv; + const filtered = performance.investments.filter((v: any) => v.assetName === inv.assetName); + const avgBuyIn = filtered.reduce((acc: any, curr: any) => acc + curr.investedAtPrice, 0) / filtered.length; + + return [ + inv.assetName, + investment.type === 'periodic' ? 'SavingsPlan' : 'OneTime', + new Date(inv.date).toLocaleDateString('de-DE'), + formatEuro(inv.investedAmount), + formatEuro(inv.currentValue), + `${formatEuro(inv.investedAtPrice)} (${formatEuro(avgBuyIn)})`, + `${inv.performancePercentage.toFixed(2)}%`, + ]; + }), + ]; + + doc.autoTable({ + startY: yPos, + head: [['Asset', 'Type', 'Date', 'Invested Amount', 'Current Value', 'Buy-In (avg)', 'Performance']], + body: positionsTableData, + styles: { + cellPadding: 2, + fontSize: 8, + }, + headStyles: { + fillColor: [240, 240, 240], + textColor: [0, 0, 0], + fontStyle: 'bold', + }, + // Style for summary and TTWOR rows + rowStyles: (row:number) => { + if (row === 0) return { fontStyle: 'bold', fillColor: [245, 245, 245] }; + if (row === 1) return { fontStyle: 'italic', textColor: [100, 100, 100] }; + return {}; + }, + }); + yPos = (doc as any).lastAutoTable.finalY + 15; + + // Savings Plans Table if exists + if (savingsPlansPerformance.length > 0) { + doc.setFontSize(16); + doc.text('Savings Plans Performance', 15, yPos); + yPos += 10; + + const savingsPlansTableData = savingsPlansPerformance.map(plan => [ + plan.assetName, + formatEuro(plan.amount), + formatEuro(plan.totalInvested), + formatEuro(plan.currentValue), + `${plan.performancePercentage.toFixed(2)}%`, + `${plan.performancePerAnnoPerformance.toFixed(2)}%` + ]); + + doc.autoTable({ + startY: yPos, + head: [['Asset', 'Interval Amount', 'Total Invested', 'Current Value', 'Performance', 'Performance (p.a.)']], + body: savingsPlansTableData, + }); + yPos = (doc as any).lastAutoTable.finalY + 15; + } + + // Add page break before future projections + doc.addPage(); + yPos = 20; + + // Future Projections + doc.setFontSize(16); + doc.text('Future Projections', 15, yPos); + yPos += 15; + doc.setFontSize(12); + doc.setTextColor(100); + // Future Projections Explanation + doc.text('About Future Projections:', 15, yPos); + yPos += 7; + const projectionText = + 'The future projections are calculated using your portfolio\'s historical performance ' + + `(${performancePerAnno.toFixed(2)}% p.a.) as a baseline. The chart shows different time horizons ` + + 'to help visualize potential growth scenarios. These projections are estimates based on ' + + 'historical data and should not be considered guaranteed returns.'; + + doc.setTextColor(0); + const projectionLines = doc.splitTextToSize(projectionText, 180); + doc.text(projectionLines, 20, yPos); + yPos += projectionLines.length * 7 - 7; + + + const years = [10, 15, 20, 30, 40]; + const chartWidth = 180; + const chartHeight = 100; + + // Calculate all projections first + const allProjections = await Promise.all(years.map(async year => { + const { projection } = await calculateFutureProjection(assets, year, performancePerAnno, { + enabled: false, + amount: 0, + interval: 'monthly', + startTrigger: 'date' + }); + return { year, projection }; + })); + + // Show summary table + const projectionSummary = allProjections.map(({ year, projection }) => { + const projected = projection[projection.length - 1]; + return [ + `${year} Years`, + formatEuro(projected.invested), + formatEuro(projected.value), + `${((projected.value - projected.invested) / projected.invested * 100).toFixed(2)}%` + ]; + }); + + doc.autoTable({ + startY: yPos, + head: [['Timeframe', 'Invested Amount', 'Expected Value', '% Gain']], + body: projectionSummary, + }); + yPos = (doc as any).lastAutoTable.finalY + 15; + + // Draw combined chart + const maxValue = Math.max(...allProjections.flatMap(p => p.projection.map(d => d.value))); + const yAxisSteps = 5; + const stepSize = maxValue / yAxisSteps; + const legendHeight = 40; // Height for legend section + + // Draw axes + doc.setDrawColor(200); + doc.line(15, yPos, 15, yPos + chartHeight); // Y axis + doc.line(15, yPos + chartHeight, 15 + chartWidth, yPos + chartHeight); // X axis + + // Draw Y-axis labels and grid lines + doc.setFontSize(8); + doc.setDrawColor(230); + for (let i = 0; i <= yAxisSteps; i++) { + const value = maxValue - (i * stepSize); + const y = yPos + (i * (chartHeight / yAxisSteps)); + doc.text(formatEuro(value), 5, y + 3); + doc.line(15, y, 15 + chartWidth, y); // Grid line + } + + const colors: [number, number, number][] = [ + [0, 100, 255], // Blue + [255, 100, 0], // Orange + [0, 200, 100], // Green + [200, 0, 200], // Purple + [255, 0, 0], // Red + ]; + + // Draw lines for each projection + allProjections.forEach(({ projection }, index) => { + const points = projection.map((p, i) => [ + 15 + (i * (chartWidth / projection.length)), + yPos + chartHeight - (p.value / maxValue * chartHeight) + ]); + + doc.setDrawColor(...(colors[index])); + doc.setLineWidth(0.5); + points.forEach((point, i) => { + if (i > 0) { + doc.line(points[i - 1][0], points[i - 1][1], point[0], point[1]); + } + }); + }); + + // Add date labels + doc.setFontSize(8); + doc.setDrawColor(0); + + // Draw legend at bottom + const legendY = yPos + chartHeight + 20; + const legendItemWidth = chartWidth / years.length; + + doc.setFontSize(8); + allProjections.forEach(({ year }, index) => { + const x = 15 + (index * legendItemWidth); + + // Draw color line + doc.setDrawColor(...colors[index]); + doc.setLineWidth(1); + doc.line(x, legendY + 4, x + 15, legendY + 4); + + // Draw text + doc.setTextColor(0); + doc.text(`${year} Years`, x + 20, legendY + 6); + }); + + yPos += chartHeight + legendHeight; // Update yPos to include legend space + + // Add footer with link + const footerText = 'Built by Tomato6966 - SourceCode'; + const link = 'https://github.com/Tomato6966/investment-portfolio-simulator'; + + doc.setFontSize(10); + doc.setTextColor(100); + const pageHeight = doc.internal.pageSize.height; + + // Add to all pages + // @ts-expect-error - doc.internal.getNumberOfPages() is not typed + const totalPages = doc.internal.getNumberOfPages(); + for (let i = 1; i <= totalPages; i++) { + doc.setPage(i); + + // Footer text with link + doc.text(footerText, 15, pageHeight - 10); + + // Add link annotation + doc.link(15, pageHeight - 15, doc.getTextWidth(footerText), 10, { url: link }); + + // Page numbers + doc.text(`Page ${i} of ${totalPages}`, doc.internal.pageSize.width - 30, pageHeight - 10); + } + + doc.save('portfolio-analysis.pdf'); +}; diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts index 002ce46..3b0188a 100644 --- a/src/utils/formatters.ts +++ b/src/utils/formatters.ts @@ -4,3 +4,33 @@ export const formatCurrency = (value: number): string => { maximumFractionDigits: 2 })}`; }; + +const LIGHT_MODE_COLORS = [ + '#2563eb', '#dc2626', '#059669', '#7c3aed', '#ea580c', + '#0891b2', '#be123c', '#1d4ed8', '#b91c1c', '#047857', + '#6d28d9', '#c2410c', '#0e7490', '#9f1239', '#1e40af', + '#991b1b', '#065f46', '#5b21b6', '#9a3412', '#155e75', + '#881337', '#1e3a8a', '#7f1d1d', '#064e3b', '#4c1d95' +]; + +const DARK_MODE_COLORS = [ + '#60a5fa', '#f87171', '#34d399', '#a78bfa', '#fb923c', + '#22d3ee', '#fb7185', '#3b82f6', '#ef4444', '#10b981', + '#8b5cf6', '#f97316', '#06b6d4', '#f43f5e', '#2563eb', + '#dc2626', '#059669', '#7c3aed', '#ea580c', '#0891b2', + '#be123c', '#1d4ed8', '#b91c1c', '#047857', '#6d28d9' +]; + +export const getHexColor = (usedColors: Set, isDarkMode: boolean): string => { + const colorPool = isDarkMode ? DARK_MODE_COLORS : LIGHT_MODE_COLORS; + + // Find first unused color + const availableColor = colorPool.find(color => !usedColors.has(color)); + + if (availableColor) { + return availableColor; + } + + // Fallback to random color if all predefined colors are used + return `#${Math.floor(Math.random() * 16777215).toString(16)}`; +};