diff --git a/package.json b/package.json index dfa18af427..53841b8869 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@aws-sdk/client-dynamodb": "3.738.0", "@aws-sdk/lib-dynamodb": "3.738.0", "@better-giving/assets": "1.0.18", + "@better-giving/balance-tx": "1.0.2", "@better-giving/donation": "1.0.12", "@better-giving/endowment": "1.0.36", "@better-giving/fundraiser": "1.0.11", @@ -86,6 +87,7 @@ "react-dropzone-esm": "15.2.0", "react-hook-form": "7.53.0", "react-player": "2.16.0", + "recharts": "2.15.1", "remix-client-cache": "1.1.0", "remix-utils": "8.1.0", "sonner": "1.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1de9aecc7..25a14070a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@better-giving/assets': specifier: 1.0.18 version: 1.0.18 + '@better-giving/balance-tx': + specifier: 1.0.2 + version: 1.0.2(@better-giving/types@1.0.5) '@better-giving/donation': specifier: 1.0.12 version: 1.0.12(@better-giving/types@1.0.5)(valibot@0.42.0(typescript@5.5.4)) @@ -179,6 +182,9 @@ importers: react-player: specifier: 2.16.0 version: 2.16.0(react@18.2.0) + recharts: + specifier: 2.15.1 + version: 2.15.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) remix-client-cache: specifier: 1.1.0 version: 1.1.0(@remix-run/react@2.15.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -546,6 +552,11 @@ packages: '@better-giving/assets@1.0.18': resolution: {integrity: sha512-FGM1fyGpuPVZroTJOXBHpQ2bEf1BXgsmH4XS9CEm6WWD+MCQcPSA+9Ydig4YMl543oITXm8JqLiVAzFJ2/Qj9A==} + '@better-giving/balance-tx@1.0.2': + resolution: {integrity: sha512-mSEFxXa9YSTzmbZBg3PAMq4vSJUl9VdyuhvH8uKOBPZwcqNlHs0L0Ciqpz0RwbNG7wIonL5Y8mhiCPlm+yASLw==} + peerDependencies: + '@better-giving/types': workspace:* + '@better-giving/donation@1.0.12': resolution: {integrity: sha512-+1udROUWBT2ZYHw4UGsmESui3OcJvshSeaak2DsJGE9VjMQnSEUnuVewanh1M2utQYG2gvgNzjtUy28MK7aTLQ==} peerDependencies: @@ -2018,6 +2029,33 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/d3-array@3.2.1': + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.0': + resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} + + '@types/d3-scale@4.0.8': + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -2487,6 +2525,50 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + data-uri-to-buffer@3.0.1: resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} engines: {node: '>= 6'} @@ -2515,6 +2597,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} @@ -2578,6 +2663,9 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dotenv@16.4.7: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} @@ -2714,6 +2802,9 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -2742,6 +2833,10 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-equals@5.2.2: + resolution: {integrity: sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==} + engines: {node: '>=6.0.0'} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -2982,6 +3077,10 @@ packages: inline-style-parser@0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -3972,6 +4071,9 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-player@2.16.0: resolution: {integrity: sha512-mAIPHfioD7yxO0GNYVFD1303QFtI3lyyQZLY229UEAp/a10cSW+hPcakg0Keq8uWJxT2OiT/4Gt+Lc9bD6bJmQ==} peerDependencies: @@ -4000,6 +4102,18 @@ packages: react: ^16.8.6 || 17 - 18 react-dom: ^16.8.6 || 17 - 18 + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -4015,6 +4129,16 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.1: + resolution: {integrity: sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -4353,6 +4477,9 @@ packages: tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -4586,6 +4713,9 @@ packages: vfile@5.3.7: resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + vite-node@1.6.0: resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} engines: {node: ^18.0.0 || >=20.0.0} @@ -5378,6 +5508,10 @@ snapshots: '@better-giving/assets@1.0.18': {} + '@better-giving/balance-tx@1.0.2(@better-giving/types@1.0.5)': + dependencies: + '@better-giving/types': 1.0.5 + '@better-giving/donation@1.0.12(@better-giving/types@1.0.5)(valibot@0.42.0(typescript@5.5.4))': dependencies: '@better-giving/types': 1.0.5 @@ -6831,6 +6965,30 @@ snapshots: '@types/cookie@0.6.0': {} + '@types/d3-array@3.2.1': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.0': {} + + '@types/d3-scale@4.0.8': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.0 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -7341,6 +7499,44 @@ snapshots: csstype@3.1.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + data-uri-to-buffer@3.0.1: {} data-urls@5.0.0: @@ -7358,6 +7554,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + decimal.js@10.4.3: {} decode-named-character-reference@1.0.2: @@ -7398,6 +7596,11 @@ snapshots: dom-accessibility-api@0.6.3: {} + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.26.7 + csstype: 3.1.3 + dotenv@16.4.7: {} dunder-proto@1.0.1: @@ -7566,6 +7769,8 @@ snapshots: event-target-shim@5.0.1: {} + eventemitter3@4.0.7: {} + eventemitter3@5.0.1: {} execa@5.1.1: @@ -7626,6 +7831,8 @@ snapshots: fast-diff@1.3.0: {} + fast-equals@5.2.2: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7882,6 +8089,8 @@ snapshots: inline-style-parser@0.1.1: {} + internmap@2.0.3: {} + ipaddr.js@1.9.1: {} is-alphabetical@2.0.1: {} @@ -8977,6 +9186,8 @@ snapshots: react-is@17.0.2: {} + react-is@18.3.1: {} + react-player@2.16.0(react@18.2.0): dependencies: deepmerge: 4.3.1 @@ -9005,6 +9216,23 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + react-smooth@4.0.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + fast-equals: 5.2.2 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + + react-transition-group@4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.26.7 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react@18.2.0: dependencies: loose-envify: 1.4.0 @@ -9029,6 +9257,23 @@ snapshots: dependencies: picomatch: 2.3.1 + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + redent@3.0.0: dependencies: indent-string: 4.0.0 @@ -9394,6 +9639,8 @@ snapshots: tiny-case@1.0.3: {} + tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -9606,6 +9853,23 @@ snapshots: unist-util-stringify-position: 3.0.3 vfile-message: 3.1.4 + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.8 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + vite-node@1.6.0(@types/node@18.18.13)(lightningcss@1.29.1): dependencies: cac: 6.7.14 diff --git a/src/.server/npo-sfws.ts b/src/.server/npo-sfws.ts new file mode 100644 index 0000000000..6c9692d66a --- /dev/null +++ b/src/.server/npo-sfws.ts @@ -0,0 +1,94 @@ +import type { BalanceTx } from "@better-giving/balance-tx"; +import { tables } from "@better-giving/types/list"; +import { addWeeks, format, subWeeks } from "date-fns"; +import type { Item, SfwPage } from "types/npo-sfws"; +import { QueryCommand, apes } from "./aws/db"; +import { env } from "./env"; + +const key = (date: Date) => `${format(date, "yy")}${format(date, "II")}`; + +export const npoSfws = async (id: number, limit = 52 / 4) => { + const cmd = new QueryCommand({ + TableName: tables["balance-txs"], + KeyConditionExpression: "PK = :pk AND begins_with(SK, :sk)", + ExpressionAttributeValues: { + ":pk": `SFW#${id}` satisfies BalanceTx.SFW.Key["PK"], + ":sk": env, + }, + ScanIndexForward: false, + Limit: limit, + }); + const items = await apes + .send(cmd) + .then((res) => (res.Items || []) as any); + + if (items.length === 0) return []; + + const map = items.reduce( + (acc, item) => { + acc[item.week] = item; + return acc; + }, + {} as Record + ); + const latest = new Date(items[0].date); + const oldest = subWeeks(latest, limit); + + const filled: Item[] = []; + let currDate = oldest; + while (currDate <= latest) { + const currKey = key(currDate); + const prev = map[key(subWeeks(currDate, 1))]; + const curr = map[currKey]; + + if (!curr) { + const item: Item = { + date: currDate.toISOString(), + week: currKey, + endow_id: id, + environment: env, + // if this is not defined, a whole withdraw must have been performed in previous SFW + flow: prev ? -prev.end : 0, + start: prev ? prev.end : 0, + end: 0, + filler: true, + }; + filled.push(item); + currDate = addWeeks(currDate, 1); + continue; + } + filled.push((({ PK, SK, ...x }) => x)(curr)); + currDate = addWeeks(currDate, 1); + } + + // graph boundary + let min = filled[0].end || 500e12; // 500T global wealth + let max = filled[0].end; + + const firstNotFilled = filled.findIndex((x) => !x.filler); + const metered: (Item & { twr: number })[] = []; + + //disregard all filler items in front + for (let i = Math.max(0, firstNotFilled); i < filled.length; i++) { + const prev = metered.at(-1); //last item pushed + const curr = filled[i]; + + // this week returnÆ’ + const sub = + curr.start && !curr.filler ? (curr.end - curr.start) / curr.start : 0; + // running time weighted return + const twr = ((prev?.twr ?? 0) + 1) * (1 + sub) - 1; + + metered.push({ ...curr, twr }); + + min = Math.min(min, curr.end); + max = Math.max(max, curr.end); + } + + return { + all: metered, + min, + max, + twr: metered.at(-1)?.twr ?? 0, + } satisfies SfwPage; +}; diff --git a/src/.server/npo-txs.ts b/src/.server/npo-txs.ts index 854b4baf63..387401c733 100644 --- a/src/.server/npo-txs.ts +++ b/src/.server/npo-txs.ts @@ -1,32 +1,14 @@ +import type { BalanceTx } from "@better-giving/balance-tx"; import { tables } from "@better-giving/types/list"; import { QueryCommand, apes } from "./aws/db"; import { env } from "./env"; -export const getNpoSf = async (id: number, numWeeks: number) => { - const cmd = new QueryCommand({ - TableName: tables["balance-txs"], - KeyConditionExpression: "PK = :pk AND begins_with(SK, :sk)", - ExpressionAttributeValues: { - ":pk": `SFW#${id}`, - ":sk": env, - }, - ScanIndexForward: false, - Limit: numWeeks, - }); - - return apes.send(cmd).then((res) => res.Items); -}; export const npoTxs = async (id: number, nextKey: string | null) => { - const startKey = nextKey - ? JSON.parse(Buffer.from(nextKey, "base64").toString()) - : undefined; - console.log({ id, nextKey, startKey }); - const command = new QueryCommand({ TableName: tables["balance-txs"], KeyConditionExpression: "PK = :PK AND begins_with(SK, :SK)", ExpressionAttributeValues: { - ":PK": `Tx#${+id}`, + ":PK": `Tx#${+id}` satisfies BalanceTx.DBRecord["PK"], ":SK": env, }, ScanIndexForward: false, @@ -37,7 +19,7 @@ export const npoTxs = async (id: number, nextKey: string | null) => { }); const result = await apes.send(command); - const items = result.Items || []; + const items = (result.Items || []) as BalanceTx.DBRecord[]; const toBase64 = (key: Record | undefined) => key ? Buffer.from(JSON.stringify(key)).toString("base64") : undefined; diff --git a/src/pages/admin/charity/dashboard/figure.tsx b/src/pages/admin/charity/dashboard/figure.tsx index a52e4c6cf8..ee0a6d9665 100644 --- a/src/pages/admin/charity/dashboard/figure.tsx +++ b/src/pages/admin/charity/dashboard/figure.tsx @@ -10,6 +10,7 @@ type Props = { /** must be wrapped by tooltip content */ tooltip?: ReactNode; actions?: ReactNode; + perf?: ReactNode; }; export default function Figure(props: Props) { @@ -26,7 +27,9 @@ export default function Figure(props: Props) { {props.icon} -

{props.amount}

+

+ {props.amount} {props.perf} +

{props.actions} ); diff --git a/src/pages/admin/charity/dashboard/loaded.tsx b/src/pages/admin/charity/dashboard/loaded.tsx index 3113e50851..06a264c9d4 100644 --- a/src/pages/admin/charity/dashboard/loaded.tsx +++ b/src/pages/admin/charity/dashboard/loaded.tsx @@ -12,6 +12,7 @@ import { monthPeriod } from "./month-period"; import { Movements } from "./movements"; import { PayoutHistory } from "./payout-history"; import { Schedule } from "./schedule"; +import { SfPerf } from "./sf-perf"; import { Summary } from "./summary"; interface Props { @@ -76,6 +77,7 @@ export function Loaded({ classes = "", ...props }: Props) { } icon={} amount={`$ ${humanize(props.balances.sustainabilityFundBal, 2)}`} + perf={} actions={ { + const date = new Date(dateString); + return date.toLocaleDateString("en-US", { + month: "2-digit", + day: "2-digit", + }); +}; + +export function SfPerChart( + props: SfwPage & { open: boolean; onClose: () => void } +) { + return ( + + + + + + + } /> + + + + + ); +} + +interface ICustomTooltip { + active?: boolean; + payload?: Array<{ + payload: Item; + }>; + label?: string; +} +const CustomTooltip: React.FC = ({ + active, + payload, + label, +}) => { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+

+ {formatDate(label ?? "")} + {humanize(data.end)} +

+ {data.flow ? ( + data.flow > 0 ? ( +

+ Deposit +

+ ) : ( +

+ Withdrawal +

+ ) + ) : null} + + {data.flow ?

${humanize(Math.abs(data.flow))}

: null} +
+ ); + } + return null; +}; diff --git a/src/pages/admin/charity/dashboard/sf-perf.tsx b/src/pages/admin/charity/dashboard/sf-perf.tsx new file mode 100644 index 0000000000..93a64d32f5 --- /dev/null +++ b/src/pages/admin/charity/dashboard/sf-perf.tsx @@ -0,0 +1,35 @@ +import { useState } from "react"; +import useSWR from "swr/immutable"; +import type { SfwPage } from "types/npo-sfws"; +import { SfPerChart } from "./sf-perf-chart"; + +interface Props { + id: number; + classes?: string; +} + +const sfwPage = (id: string) => + fetch(`/api/npo/${id}/sfws`).then((res) => res.json()); + +export function SfPerf({ id, classes = "" }: Props) { + const [expanded, expand] = useState(false); + const { data } = useSWR(id.toString(), sfwPage); + if (!data || data.all.length === 0) return null; + return ( +
+ + {expanded && ( + expand(false)} /> + )} +
+ ); +} diff --git a/src/routes/api.npo.$id.sfws.ts b/src/routes/api.npo.$id.sfws.ts new file mode 100644 index 0000000000..1d1c171333 --- /dev/null +++ b/src/routes/api.npo.$id.sfws.ts @@ -0,0 +1,18 @@ +import { endowIdParam } from "@better-giving/endowment/schema"; +import type { LoaderFunction } from "@vercel/remix"; +import * as v from "valibot"; +import { cognito } from ".server/auth"; +import { npoSfws } from ".server/npo-sfws"; + +export const loader: LoaderFunction = async ({ params, request }) => { + const id = v.parse(endowIdParam, params.id); + const { user } = await cognito.retrieve(request); + if (!user) return new Response(null, { status: 401 }); + if (!user.endowments.includes(id) && !user.groups.includes("ap-admin")) + return new Response(null, { status: 403 }); + + const page = await npoSfws(id); + return new Response(JSON.stringify(page), { + headers: { "content-type": "application/json" }, + }); +}; diff --git a/src/types/npo-sfws.ts b/src/types/npo-sfws.ts new file mode 100644 index 0000000000..55fad4c685 --- /dev/null +++ b/src/types/npo-sfws.ts @@ -0,0 +1,11 @@ +import type { BalanceTx } from "@better-giving/balance-tx"; +export interface Item extends Omit { + filler?: true; +} + +export interface SfwPage { + all: Item[]; + twr: number; + min: number; + max: number; +}