diff --git a/next.config.js b/next.config.ts similarity index 72% rename from next.config.js rename to next.config.ts index 67cb7cf..56e558e 100644 --- a/next.config.js +++ b/next.config.ts @@ -5,6 +5,7 @@ import "./src/env.js"; import pwa from "next-pwa"; import { env } from "./src/env.js"; +import { type NextConfig } from "next"; const withPWA = pwa({ dest: "public", @@ -13,6 +14,11 @@ const withPWA = pwa({ skipWaiting: true, }); -export default withPWA({ +const nextConfig = { eslint: { ignoreDuringBuilds: true }, -}); + experimental: { + turbo: {}, + }, +} satisfies NextConfig; + +export default withPWA(nextConfig); diff --git a/package.json b/package.json index ffafa2b..f2e3fae 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,9 @@ "typecheck": "tsc --noEmit", "android": "pnpm build && pnpm export && pnpm dlx cap sync android && pnpm dlx cap open android" }, + "overrides": { + "react-is": "^19.0.0-rc-69d4b800-20241021" + }, "dependencies": { "@auth/drizzle-adapter": "^1.7.2", "@clerk/nextjs": "^6.14.3", @@ -54,6 +57,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.55.0", + "recharts": "^2.15.2", "server-only": "^0.0.1", "sonner": "^2.0.3", "superjson": "^2.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4a18bf..4811d5a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,9 @@ importers: react-hook-form: specifier: ^7.55.0 version: 7.55.0(react@19.1.0) + recharts: + specifier: ^2.15.2 + version: 2.15.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) server-only: specifier: ^0.0.1 version: 0.0.1 @@ -2049,6 +2052,33 @@ packages: peerDependencies: typescript: '>=5.7.2' + '@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.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@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/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -2363,6 +2393,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-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -2384,6 +2458,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -2415,6 +2492,9 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} @@ -2616,6 +2696,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -2623,6 +2706,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + 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'} @@ -2790,6 +2877,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -3077,6 +3168,10 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -3317,6 +3412,9 @@ packages: resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} engines: {node: '>=6'} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3341,6 +3439,12 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -3361,6 +3465,12 @@ packages: '@types/react': optional: true + 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-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -3371,10 +3481,26 @@ packages: '@types/react': optional: true + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + react@19.1.0: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.2: + resolution: {integrity: sha512-xv9lVztv3ingk7V3Jf05wfAZbM9Q2umJzu5t/cfnAK7LUslNrGT7LPBr74G+ok8kSCeFMaePmWMg0rcYOnczTw==} + 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 + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -3690,6 +3816,9 @@ packages: engines: {node: '>=10'} hasBin: true + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3827,6 +3956,9 @@ packages: react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + watchpack@2.4.0: resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} engines: {node: '>=10.13.0'} @@ -5678,6 +5810,30 @@ snapshots: dependencies: typescript: 5.8.2 + '@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.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -6048,6 +6204,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-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -6070,6 +6264,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + deepmerge@4.3.1: {} define-data-property@1.1.4: @@ -6104,6 +6300,11 @@ snapshots: dependencies: path-type: 4.0.0 + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.27.0 + csstype: 3.1.3 + dot-case@3.0.4: dependencies: no-case: 3.0.4 @@ -6331,10 +6532,14 @@ snapshots: esutils@2.0.3: {} + eventemitter3@4.0.7: {} + events@3.3.0: {} fast-deep-equal@3.1.3: {} + fast-equals@5.2.2: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6528,6 +6733,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + internmap@2.0.3: {} + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -6774,6 +6981,10 @@ snapshots: lodash@4.17.21: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lower-case@2.0.2: dependencies: tslib: 2.8.1 @@ -7002,6 +7213,12 @@ snapshots: pretty-bytes@5.6.0: {} + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + punycode@2.3.1: {} querystringify@2.2.0: {} @@ -7021,6 +7238,10 @@ snapshots: dependencies: react: 19.1.0 + react-is@16.13.1: {} + + react-is@18.3.1: {} + react-remove-scroll-bar@2.3.8(@types/react@19.1.0)(react@19.1.0): dependencies: react: 19.1.0 @@ -7040,6 +7261,14 @@ snapshots: optionalDependencies: '@types/react': 19.1.0 + react-smooth@4.0.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + fast-equals: 5.2.2 + prop-types: 15.8.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-transition-group: 4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-style-singleton@2.2.3(@types/react@19.1.0)(react@19.1.0): dependencies: get-nonce: 1.0.1 @@ -7048,8 +7277,34 @@ snapshots: optionalDependencies: '@types/react': 19.1.0 + react-transition-group@4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@babel/runtime': 7.27.0 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react@19.1.0: {} + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -7436,6 +7691,8 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + tiny-invariant@1.3.3: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -7573,6 +7830,23 @@ snapshots: - '@types/react' - '@types/react-dom' + 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.9 + '@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 + watchpack@2.4.0: dependencies: glob-to-regexp: 0.4.1 diff --git a/src/app/(router)/add/page.tsx b/src/app/(router)/add/page.tsx index 2995d59..a29ff43 100644 --- a/src/app/(router)/add/page.tsx +++ b/src/app/(router)/add/page.tsx @@ -1,23 +1,9 @@ -import ExpenseForm from "@/app/_components/expense/expense-form"; -import Header from "@/components/header"; - -import { Button } from "@/components/ui/button"; -import { api, HydrateClient } from "@/trpc/server"; -import { currentUser } from "@clerk/nextjs/server"; +import ExpensePage from "@/app/_components/expense/create-expense-page"; +import { api } from "@/trpc/server"; import React from "react"; export default async function Page() { - const user = await currentUser(); const sessionUser = await api.user.getSessionUser(); - return ( - -
- -
- -
- ); + return ; } diff --git a/src/app/(router)/analytics/page.tsx b/src/app/(router)/analytics/page.tsx new file mode 100644 index 0000000..ecf112d --- /dev/null +++ b/src/app/(router)/analytics/page.tsx @@ -0,0 +1,20 @@ +import { AnalyticsRadarChart } from "@/components/charts/radar-chart"; +import Header from "@/components/header"; +import Section from "@/components/section"; +import { api } from "@/trpc/server"; +import React from "react"; + +async function AnalyticsPage() { + const analyticsData = await api.expense.getAnalytics(); + + return ( + <> +
+
+ +
+ + ); +} + +export default AnalyticsPage; diff --git a/src/app/(router)/expense/page.tsx b/src/app/(router)/expense/page.tsx index 4dbeca6..b1c06b6 100644 --- a/src/app/(router)/expense/page.tsx +++ b/src/app/(router)/expense/page.tsx @@ -1,14 +1,17 @@ import ExpenseCard from "@/app/_components/expense/expense-card"; import Header from "@/components/header"; +import { Icons } from "@/components/icons"; import Section from "@/components/section"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { api } from "@/trpc/server"; +import { currentUser } from "@clerk/nextjs/server"; import Link from "next/link"; import React from "react"; export default async function Page() { const splits = await api.expense.getAll(); - + const sessionUser = await currentUser(); return ( <>
@@ -17,14 +20,42 @@ export default async function Page() {
-
- Expense Stats + {/* STATS */} + {/*
+
+

Expenses

+ {splits.length} +
+ +
+

Expenses

+ {splits.length} +
+ +
+

Balance

+ + {getAmount(812.47)} + +
+
*/} + + {/* FILTER BAR */} +
+ +
    {splits.map((expense) => (
  • - +
  • ))}
diff --git a/src/app/(router)/page.tsx b/src/app/(router)/page.tsx index 4d29abb..cbd7404 100644 --- a/src/app/(router)/page.tsx +++ b/src/app/(router)/page.tsx @@ -9,6 +9,7 @@ import { Card, CardHeader, CardTitle } from "@/components/ui/card"; import { getAmount } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import Link from "next/link"; +import { Badge } from "@/components/ui/badge"; export default async function Home() { return ( @@ -32,7 +33,14 @@ export default async function Home() {
- + + + Analytics +

diff --git a/src/app/_components/expense/create-expense-page.tsx b/src/app/_components/expense/create-expense-page.tsx new file mode 100644 index 0000000..b9a7ea3 --- /dev/null +++ b/src/app/_components/expense/create-expense-page.tsx @@ -0,0 +1,57 @@ +"use client"; +import React from "react"; +import ExpenseForm from "./expense-form"; +import Header from "@/components/header"; +import { Button } from "@/components/ui/button"; +import type { User } from "@/server/db/schema"; +import { useExpenseStore } from "@/lib/store/expense-store"; +import ExpenseSplit from "./expense-split"; +import ExpenseParticipants from "./expense-participants"; +import { Separator } from "@/components/ui/separator"; +import { useSearchParams } from "next/navigation"; +import { api } from "@/trpc/react"; + +function ExpensePage({ sessionUser }: { sessionUser: User }) { + const addParticipants = useExpenseStore((state) => state.addParticipants); + const setPayments = useExpenseStore((state) => state.setPayments); + React.useEffect(() => { + addParticipants([sessionUser]); + setPayments([{ amount: 0, userId: sessionUser.id }]); + }, []); + + const groupBadges = useExpenseStore((state) => state.groupBadges); + const addGroupBadges = useExpenseStore((state) => state.addGroupBadges); + + const searchParams = useSearchParams(); + const paramGroup = searchParams.get("groupId"); + const paramUser = searchParams.get("userId"); + // React.useEffect(() => { + // console.log("Params: ", paramGroup, paramUser); + // if (paramGroup?.length) { + // if (!groupBadges.find((group) => group.id === paramGroup)) { + // const { data: group } = api.group.get.useQuery({ + // id: paramGroup, + // }); + // if (group) addGroupBadges([group]); + // if (group?.members?.length) + // addParticipants(group.members.map(({ user }) => user)); + // } + // } + // }, [paramGroup, paramUser]); + return ( + <> +

+ +
+ + + + + + + ); +} + +export default ExpensePage; diff --git a/src/app/_components/expense/expense-card.tsx b/src/app/_components/expense/expense-card.tsx index e2f7f9a..dc5a280 100644 --- a/src/app/_components/expense/expense-card.tsx +++ b/src/app/_components/expense/expense-card.tsx @@ -1,4 +1,3 @@ -import Avatar from "@/components/avatar"; import { Card, CardContent, @@ -10,28 +9,81 @@ import { Drawer, DrawerClose, DrawerContent, - DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer"; - import { Separator } from "@/components/ui/separator"; -import { getAmount } from "@/lib/utils"; -import type { Expense } from "@/server/db/schema"; +import { cn, getAmount } from "@/lib/utils"; +import type { + Expense, + ExpenseGroupBadge, + ExpenseSplit, +} from "@/server/db/schema"; import React from "react"; import { UserBadge } from "./expense-participants"; import { Icons } from "@/components/icons"; - import ExpenseDetails from "./expense-details"; import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { calculateTotalValue } from "@/lib/utils/expense"; -function ExpenseCard({ expense }: { expense: Expense }) { +export const ExpenseSplitsList = ({ + splits, +}: { + splits: Array; +}) => { + return ( +
    + {splits?.map((split, idx) => ( +
  • + + owes + + + + + {getAmount(Number(split.amount))} + +
  • + ))} +
+ ); +}; + +export const ExpenseGroupBadgesList = ({ + groupBadges, +}: { + groupBadges: Array; +}) => { + return ( +
    + {groupBadges.map(({ group }) => ( +
  • + + + {group!.name} + +
  • + ))} +
+ ); +}; + +function ExpenseCard({ + expense, + sessionUserId, +}: { + expense: Expense; + sessionUserId: string; +}) { + const toatalBalance = calculateTotalValue(sessionUserId, expense.splits!); + const totalValue = toatalBalance.owed - toatalBalance.owes; return ( - +

@@ -43,36 +95,52 @@ function ExpenseCard({ expense }: { expense: Expense }) { - - {expense?.splits?.map((split, idx) => ( -

- - - - - - {getAmount(Number(split.amount))} - -
- ))} - - + {expense.groupBadges?.length ? ( +
+ Groups: + +
+ ) : null} - - You Owe/Get - - {getAmount(Number(expense.amount))} + + + + +
+ {expense?.splits!.length > 2 && ( + <> +
+

+ {expense?.splits!.length - 2} more +

+ + )} + +
+ + + You {totalValue < 0 ? "Owe" : "Get"} + + + {getAmount(Number(totalValue))} - Are you absolutely sure? - This action cannot be undone. +
+ {expense.description} + {getAmount(Number(expense.amount))} +
-
+
diff --git a/src/app/_components/expense/expense-details.tsx b/src/app/_components/expense/expense-details.tsx index 02e0a71..1ce3604 100644 --- a/src/app/_components/expense/expense-details.tsx +++ b/src/app/_components/expense/expense-details.tsx @@ -1,8 +1,14 @@ import type { Expense } from "@/server/db/schema"; import React from "react"; +import { ExpenseGroupBadgesList, ExpenseSplitsList } from "./expense-card"; function ExpenseDetails({ expense }: { expense: Expense }) { - return
ExpenseDetails for expense: {expense.id}
; + return ( + <> + + + + ); } export default ExpenseDetails; diff --git a/src/app/_components/expense/expense-form.tsx b/src/app/_components/expense/expense-form.tsx index 1cc03a1..b95bdb1 100644 --- a/src/app/_components/expense/expense-form.tsx +++ b/src/app/_components/expense/expense-form.tsx @@ -17,12 +17,9 @@ import { expenseSchema } from "@/lib/validations/expense"; import { Textarea } from "@/components/ui/textarea"; import { NumberInput } from "@/components/number-input"; import { EuroIcon } from "lucide-react"; -import ExpenseSplit from "./expense-split"; import { toast } from "sonner"; import { api } from "@/trpc/react"; import { useExpenseStore } from "@/lib/store/expense-store"; -import ExpenseParticipants from "./expense-participants"; -import { Separator } from "@/components/ui/separator"; import { calculateDepts } from "@/lib/utils/expense"; import { cn } from "@/lib/utils"; import { useRouter } from "next/navigation"; @@ -53,10 +50,11 @@ function ExpenseForm({ const recalculateSplits = useExpenseStore((state) => state.recalculateSplits); const amount = form.watch("amount"); const resetExpenseStore = useExpenseStore((state) => state.resetExpenseStore); + const groupBadges = useExpenseStore((state) => state.groupBadges); const router = useRouter(); - + const createExpense = api.expense.create.useMutation({ - onSuccess(expense) { + onSuccess() { form.reset(); resetExpenseStore(); addParticipants([sessionUser]); @@ -73,6 +71,7 @@ function ExpenseForm({ }); }, }); + function onSubmit(expense: z.infer) { if (createExpense.isPending) return; if (participants.length <= 1) @@ -89,14 +88,13 @@ function ExpenseForm({ payments ); - createExpense.mutate({ expense, debs }); + createExpense.mutate({ + expense, + debs, + groupBadges: groupBadges.map(({ id }) => id), + }); } - React.useEffect(() => { - addParticipants([sessionUser]); - setPayments([{ amount, userId: sessionUser.id }]); - }, []); - const handleAmountChange = (value: number) => { setAmount(value); const firstUserId = payments[0]?.userId ?? sessionUser.id; @@ -164,78 +162,6 @@ function ExpenseForm({ )} /> - - - - - - {/* - - Expense - Split - - - ( - - Amount - {/* -
- - -
- -
- )} - /> - ( - - Description - -