Replace custom pie chart with Recharts for better label collision handling
Some checks failed
Build and Push Docker Images / docker (push) Failing after 1m10s

- Add Recharts dependency for professional chart rendering
- Create RechartsPortfolioPieChart component with built-in label collision detection
- Replace StaticPortfolioPieChart usage in CheckoutSuccess page
- Remove old StaticPortfolioPieChart component with problematic label overlap
- Fixes jumbled/overlapping labels on portfolio distribution chart

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Matt 2025-10-31 17:48:20 +01:00
parent 6787ccd2d8
commit f21048b0d6
5 changed files with 572 additions and 203 deletions

398
package-lock.json generated
View File

@ -13,7 +13,8 @@
"framer-motion": "^12.15.0", "framer-motion": "^12.15.0",
"lucide-react": "^0.344.0", "lucide-react": "^0.344.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1",
"recharts": "^3.3.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.9.1", "@eslint/js": "^9.9.1",
@ -1161,6 +1162,32 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@reduxjs/toolkit": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.2.tgz",
"integrity": "sha512-ZAYu/NXkl/OhqTz7rfPaAhY0+e8Fr15jqNxte/2exKUxvHyQ/hcqmdekiN1f+Lcw3pE+34FCgX+26zcUE3duCg==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
"immer": "^10.0.3",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.24.0", "version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz",
@ -1375,6 +1402,18 @@
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"dev": true "dev": true
}, },
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@testing-library/dom": { "node_modules/@testing-library/dom": {
"version": "9.3.4", "version": "9.3.4",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",
@ -1631,6 +1670,69 @@
"@babel/types": "^7.20.7" "@babel/types": "^7.20.7"
} }
}, },
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@ -1647,13 +1749,13 @@
"version": "15.7.13", "version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
"dev": true "devOptional": true
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.11", "version": "18.3.11",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz",
"integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==", "integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==",
"dev": true, "devOptional": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.0.2" "csstype": "^3.0.2"
@ -1668,6 +1770,12 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.8.1", "version": "8.8.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz",
@ -2537,6 +2645,15 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -2645,7 +2762,128 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true "devOptional": true
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
}, },
"node_modules/data-urls": { "node_modules/data-urls": {
"version": "5.0.0", "version": "5.0.0",
@ -2683,6 +2921,12 @@
"integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
"dev": true "dev": true
}, },
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/deep-eql": { "node_modules/deep-eql": {
"version": "4.1.4", "version": "4.1.4",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
@ -2904,6 +3148,16 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es-toolkit": {
"version": "1.41.0",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.41.0.tgz",
"integrity": "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==",
"license": "MIT",
"workspaces": [
"docs",
"benchmarks"
]
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.21.5", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@ -3219,6 +3473,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/execa": { "node_modules/execa": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
@ -3784,6 +4044,16 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/immer": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -3832,6 +4102,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/is-arguments": { "node_modules/is-arguments": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
@ -5208,8 +5487,30 @@
"node_modules/react-is": { "node_modules/react-is": {
"version": "17.0.2", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
"dev": true },
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
}, },
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.14.2", "version": "0.14.2",
@ -5241,6 +5542,33 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/recharts": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.3.0.tgz",
"integrity": "sha512-Vi0qmTB0iz1+/Cz9o5B7irVyUjX2ynvEgImbgMt/3sKRREcUM07QiYjS1QpAVrkmVlXqy5gykq4nGWMz9AS4Rg==",
"license": "MIT",
"dependencies": {
"@reduxjs/toolkit": "1.x.x || 2.x.x",
"clsx": "^2.1.1",
"decimal.js-light": "^2.5.1",
"es-toolkit": "^1.39.3",
"eventemitter3": "^5.0.1",
"immer": "^10.1.1",
"react-redux": "8.x.x || 9.x.x",
"reselect": "5.1.1",
"tiny-invariant": "^1.3.3",
"use-sync-external-store": "^1.2.2",
"victory-vendor": "^37.0.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/redent": { "node_modules/redent": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
@ -5254,6 +5582,21 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"license": "MIT",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/regenerator-runtime": { "node_modules/regenerator-runtime": {
"version": "0.14.1", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
@ -5286,6 +5629,12 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true "dev": true
}, },
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@ -5881,6 +6230,12 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tinybench": { "node_modules/tinybench": {
"version": "2.9.0", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@ -6098,12 +6453,43 @@
"requires-port": "^1.0.0" "requires-port": "^1.0.0"
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true "dev": true
}, },
"node_modules/victory-vendor": {
"version": "37.3.6",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
"license": "MIT AND ISC",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.8", "version": "5.4.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",

View File

@ -16,7 +16,8 @@
"framer-motion": "^12.15.0", "framer-motion": "^12.15.0",
"lucide-react": "^0.344.0", "lucide-react": "^0.344.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1",
"recharts": "^3.3.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.9.1", "@eslint/js": "^9.9.1",

View File

@ -0,0 +1,176 @@
import React from 'react';
import { PieChart, Pie, Cell, ResponsiveContainer, Label } from 'recharts';
import type { OffsetProject } from '../types';
import { getProjectColor } from '../utils/portfolioColors';
interface RechartsPortfolioPieChartProps {
projects: OffsetProject[];
totalTons: number;
size?: number;
}
interface ChartDataPoint {
name: string;
value: number;
tons: number;
percentage: number;
fill: string;
}
export function RechartsPortfolioPieChart({
projects,
totalTons,
size = 280,
}: RechartsPortfolioPieChartProps) {
// Transform data to Recharts format
const chartData: ChartDataPoint[] = projects
.filter(project => project.percentage && project.percentage > 0)
.map((project, index) => {
const tons = totalTons * (project.percentage || 0);
return {
name: project.name,
value: tons,
tons: parseFloat(tons.toFixed(2)),
percentage: (project.percentage || 0) * 100,
fill: getProjectColor(index, projects.length),
};
});
// Custom label component for multi-line labels
const renderCustomLabel = (props: any) => {
const {
cx,
cy,
midAngle,
outerRadius,
name,
tons,
percentage,
fill,
} = props;
const RADIAN = Math.PI / 180;
const radius = outerRadius + 60;
const x = cx + radius * Math.cos(-midAngle * RADIAN);
const y = cy + radius * Math.sin(-midAngle * RADIAN);
const textAnchor = x > cx ? 'start' : 'end';
// Calculate line endpoint for label connector
const lineEndX = x > cx ? x + 50 : x - 50;
const labelStartX = cx + (outerRadius + 10) * Math.cos(-midAngle * RADIAN);
const labelStartY = cy + (outerRadius + 10) * Math.sin(-midAngle * RADIAN);
return (
<g>
{/* Connector line from pie edge to label */}
<line
x1={labelStartX}
y1={labelStartY}
x2={x}
y2={y}
stroke={fill}
strokeWidth="2"
className="print:stroke-gray-400"
/>
<line
x1={x}
y1={y}
x2={lineEndX}
y2={y}
stroke={fill}
strokeWidth="2"
className="print:stroke-gray-400"
/>
{/* Three-line label text */}
<text
x={lineEndX}
y={y - 20}
textAnchor={textAnchor}
className="text-sm font-semibold fill-slate-700 print:text-xs"
style={{ fontSize: '14px', fontWeight: 600 }}
>
{name}
</text>
<text
x={lineEndX}
y={y - 4}
textAnchor={textAnchor}
className="text-xs fill-slate-600 print:text-[10px]"
style={{ fontSize: '12px' }}
>
{tons} tons
</text>
<text
x={lineEndX}
y={y + 12}
textAnchor={textAnchor}
className="text-xs font-bold fill-slate-800 print:text-[10px]"
style={{ fontSize: '12px', fontWeight: 700 }}
>
{percentage.toFixed(1)}%
</text>
</g>
);
};
return (
<div className="flex flex-col items-center w-full">
{/* SVG Chart */}
<ResponsiveContainer width={size * 2.2} height={size * 1.4}>
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
labelLine={false}
label={renderCustomLabel}
outerRadius={size * 0.35}
innerRadius={size * 0.20}
paddingAngle={2}
dataKey="value"
isAnimationActive={false}
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} stroke="white" strokeWidth={2} />
))}
{/* Center label showing total */}
<Label
value={totalTons.toString()}
position="center"
className="text-2xl font-bold fill-emerald-600 print:text-xl"
style={{ fontSize: '24px', fontWeight: 700, fill: '#059669' }}
/>
<Label
value="tons CO₂"
position="center"
dy={20}
className="text-sm fill-slate-600 print:text-xs"
style={{ fontSize: '14px', fill: '#475569' }}
/>
</Pie>
</PieChart>
</ResponsiveContainer>
{/* Legend below chart (for print clarity) */}
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-3 w-full max-w-2xl print:mt-4 print:gap-2">
{chartData.map((item, index) => (
<div key={index} className="flex items-center gap-3 print:gap-2">
<div
className="w-4 h-4 rounded-sm flex-shrink-0 print:w-3 print:h-3"
style={{ backgroundColor: item.fill }}
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-800 truncate print:text-xs">
{item.name}
</p>
<p className="text-xs text-slate-600 print:text-[10px]">
{item.tons} tons ({item.percentage.toFixed(1)}%)
</p>
</div>
</div>
))}
</div>
</div>
);
}

View File

@ -1,194 +0,0 @@
import React from 'react';
import type { OffsetProject } from '../types';
import { getProjectColor } from '../utils/portfolioColors';
interface StaticPortfolioPieChartProps {
projects: OffsetProject[];
totalTons: number;
size?: number;
}
export function StaticPortfolioPieChart({
projects,
totalTons,
size = 280,
}: StaticPortfolioPieChartProps) {
const centerX = size / 2;
const centerY = size / 2;
const radius = size * 0.35;
const innerRadius = size * 0.20; // Slightly smaller for better labeling
const labelRadius = size * 0.50; // Distance for label lines
// Helper function to convert polar coordinates to cartesian
const polarToCartesian = (centerX: number, centerY: number, radius: number, angleInDegrees: number) => {
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
return {
x: centerX + radius * Math.cos(angleInRadians),
y: centerY + radius * Math.sin(angleInRadians),
};
};
// Generate SVG path for a donut segment
const createSegmentPath = (
startAngle: number,
endAngle: number,
outerR: number,
innerR: number
): string => {
const start = polarToCartesian(centerX, centerY, outerR, endAngle);
const end = polarToCartesian(centerX, centerY, outerR, startAngle);
const innerStart = polarToCartesian(centerX, centerY, innerR, endAngle);
const innerEnd = polarToCartesian(centerX, centerY, innerR, startAngle);
const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1';
return [
'M', start.x, start.y,
'A', outerR, outerR, 0, largeArcFlag, 0, end.x, end.y,
'L', innerEnd.x, innerEnd.y,
'A', innerR, innerR, 0, largeArcFlag, 1, innerStart.x, innerStart.y,
'Z',
].join(' ');
};
// Calculate segments with label positions
const segments: Array<{
path: string;
percentage: number;
project: OffsetProject;
color: string;
midAngle: number;
tons: number;
}> = [];
let currentAngle = 0;
projects.forEach((project, index) => {
if (!project.percentage) return;
const percentage = project.percentage * 100;
const segmentAngle = (percentage / 100) * 360;
const midAngle = currentAngle + segmentAngle / 2;
const tons = parseFloat((totalTons * project.percentage).toFixed(2));
const color = getProjectColor(index, projects.length);
segments.push({
path: createSegmentPath(currentAngle, currentAngle + segmentAngle, radius, innerRadius),
percentage,
project,
color,
midAngle,
tons,
});
currentAngle += segmentAngle;
});
return (
<div className="flex flex-col items-center w-full">
{/* SVG Chart */}
<svg
width={size * 2.2}
height={size * 1.4}
viewBox={`0 0 ${size * 2.2} ${size * 1.4}`}
className="overflow-visible print:overflow-visible"
>
{/* Donut segments */}
<g transform={`translate(${size * 1.1 - centerX}, ${size * 0.7 - centerY})`}>
{segments.map((segment, index) => {
const labelPoint = polarToCartesian(centerX, centerY, labelRadius, segment.midAngle);
const textAnchor = labelPoint.x > centerX ? 'start' : 'end';
const lineEndX = labelPoint.x > centerX ? labelPoint.x + 40 : labelPoint.x - 40;
return (
<g key={index}>
{/* Segment path */}
<path
d={segment.path}
fill={segment.color}
stroke="white"
strokeWidth="2"
className="print:stroke-gray-300"
/>
{/* Label line */}
<line
x1={labelPoint.x}
y1={labelPoint.y}
x2={lineEndX}
y2={labelPoint.y}
stroke={segment.color}
strokeWidth="2"
className="print:stroke-gray-400"
/>
{/* Label text */}
<text
x={lineEndX}
y={labelPoint.y - 20}
textAnchor={textAnchor}
className="text-sm font-semibold fill-slate-700 print:text-xs"
>
{segment.project.name}
</text>
<text
x={lineEndX}
y={labelPoint.y - 4}
textAnchor={textAnchor}
className="text-xs fill-slate-600 print:text-[10px]"
>
{segment.tons} tons
</text>
<text
x={lineEndX}
y={labelPoint.y + 12}
textAnchor={textAnchor}
className="text-xs font-bold fill-slate-800 print:text-[10px]"
>
{segment.percentage.toFixed(1)}%
</text>
</g>
);
})}
{/* Center text showing total */}
<text
x={centerX}
y={centerY - 10}
textAnchor="middle"
className="text-2xl font-bold fill-emerald-600 print:text-xl"
>
{totalTons}
</text>
<text
x={centerX}
y={centerY + 8}
textAnchor="middle"
className="text-sm fill-slate-600 print:text-xs"
>
tons CO
</text>
</g>
</svg>
{/* Legend below chart (for print clarity) */}
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-3 w-full max-w-2xl print:mt-4 print:gap-2">
{segments.map((segment, index) => (
<div key={index} className="flex items-center gap-3 print:gap-2">
<div
className="w-4 h-4 rounded-sm flex-shrink-0 print:w-3 print:h-3"
style={{ backgroundColor: segment.color }}
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-800 truncate print:text-xs">
{segment.project.name}
</p>
<p className="text-xs text-slate-600 print:text-[10px]">
{segment.tons} tons ({segment.percentage.toFixed(1)}%)
</p>
</div>
</div>
))}
</div>
</div>
);
}

View File

@ -3,7 +3,7 @@ import { motion } from 'framer-motion';
import { getOrderDetails } from '../api/checkoutClient'; import { getOrderDetails } from '../api/checkoutClient';
import { OrderDetailsResponse } from '../types'; import { OrderDetailsResponse } from '../types';
import { CarbonImpactComparison } from '../components/CarbonImpactComparison'; import { CarbonImpactComparison } from '../components/CarbonImpactComparison';
import { StaticPortfolioPieChart } from '../components/StaticPortfolioPieChart'; import { RechartsPortfolioPieChart } from '../components/RechartsPortfolioPieChart';
import { useCalculatorState } from '../hooks/useCalculatorState'; import { useCalculatorState } from '../hooks/useCalculatorState';
interface CheckoutSuccessProps { interface CheckoutSuccessProps {
@ -313,7 +313,7 @@ export default function CheckoutSuccess({
<p className="text-slate-600 text-center mb-8 print:text-sm print:mb-4"> <p className="text-slate-600 text-center mb-8 print:text-sm print:mb-4">
Your {order.tons} tons of CO offsets are distributed across these verified projects: Your {order.tons} tons of CO offsets are distributed across these verified projects:
</p> </p>
<StaticPortfolioPieChart <RechartsPortfolioPieChart
projects={orderDetails.order.portfolio.projects} projects={orderDetails.order.portfolio.projects}
totalTons={order.tons} totalTons={order.tons}
size={280} size={280}