diff --git a/js/package.json b/js/package.json
index ac0d4846d..04ce7a18b 100644
--- a/js/package.json
+++ b/js/package.json
@@ -22,6 +22,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "@mantine/charts": "^8.1.2",
"@mantine/core": "^8.1.2",
"@mantine/dates": "^8.1.2",
"@mantine/form": "^8.1.2",
@@ -42,6 +43,7 @@
"react-router": "^7.18.0",
"react-router-dom": "^7.18.0",
"react-shiki": "^0.9.1",
+ "recharts": "^2.15.4",
"type-fest": "github:tahminator/type-fest#main",
"use-debounce": "^10.0.4",
"use-immer": "^0.11.0",
diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml
index 03ea35cda..0a36d8fb9 100644
--- a/js/pnpm-lock.yaml
+++ b/js/pnpm-lock.yaml
@@ -31,6 +31,9 @@ importers:
.:
dependencies:
+ '@mantine/charts':
+ specifier: ^8.1.2
+ version: 8.1.2(@mantine/core@8.1.2(@mantine/hooks@8.1.2(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@8.1.2(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(recharts@2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
'@mantine/core':
specifier: ^8.1.2
version: 8.1.2(@mantine/hooks@8.1.2(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -91,6 +94,9 @@ importers:
react-shiki:
specifier: ^0.9.1
version: 0.9.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ recharts:
+ specifier: ^2.15.4
+ version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
type-fest:
specifier: github:tahminator/type-fest#main
version: https://codeload.github.com/tahminator/type-fest/tar.gz/d0bbbbe83ef6c42d130cbf2fdbfd8a48bbc989bd
@@ -721,6 +727,15 @@ packages:
'@keyv/serialize@1.0.2':
resolution: {integrity: sha512-+E/LyaAeuABniD/RvUezWVXKpeuvwLEA9//nE9952zBaOdBd2mQ3pPoM8cUe2X6IcMByfuSLzmYqnYshG60+HQ==}
+ '@mantine/charts@8.1.2':
+ resolution: {integrity: sha512-K1AfGQa+ABFLWNXT8d1mu+Zsqv6pG1LT+1FnIorpyxe3DOsWLXP3Tr4JHW9QSS0qFyvpOiFuKeCMdDUkdzyMfQ==}
+ peerDependencies:
+ '@mantine/core': 8.1.2
+ '@mantine/hooks': 8.1.2
+ react: ^18.x || ^19.x
+ react-dom: ^18.x || ^19.x
+ recharts: ^2.13.3
+
'@mantine/core@8.1.2':
resolution: {integrity: sha512-+maX0a1+fxh6Lvnzi0qb0AZsCnnHlIiTE/hFC+dd3eRfUW2PEKJ5/wTpmrX8IGyxa+NS+fXjZD/cU4Yt9xNjdg==}
peerDependencies:
@@ -772,7 +787,6 @@ packages:
engines: {node: ^22.20 || ^24.12 || >=25}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@@ -824,42 +838,36 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
- libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.0':
resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
- libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.0':
resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.0':
resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.0':
resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.0':
resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
- libc: [musl]
'@parcel/watcher-win32-arm64@2.5.0':
resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==}
@@ -927,79 +935,66 @@ packages:
resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==}
cpu: [arm]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.62.4':
resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==}
cpu: [arm]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.62.4':
resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.62.4':
resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.62.4':
resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==}
cpu: [loong64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.62.4':
resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==}
cpu: [loong64]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.62.4':
resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==}
cpu: [ppc64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.62.4':
resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==}
cpu: [ppc64]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.62.4':
resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==}
cpu: [riscv64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.62.4':
resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==}
cpu: [riscv64]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.62.4':
resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==}
cpu: [s390x]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.62.4':
resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.62.4':
resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==}
cpu: [x64]
os: [linux]
- libc: [musl]
'@rollup/rollup-openbsd-x64@4.62.4':
resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==}
@@ -1101,28 +1096,24 @@ packages:
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.2.1':
resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.2.1':
resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.2.1':
resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
- libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.2.1':
resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==}
@@ -1219,6 +1210,33 @@ packages:
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+ '@types/d3-array@3.2.2':
+ resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
+
+ '@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.2.0':
+ resolution: {integrity: sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==}
+
+ '@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==}
@@ -1675,6 +1693,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.2:
+ resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
+ 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-urls@6.0.1:
resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==}
engines: {node: '>=20'}
@@ -1703,6 +1765,9 @@ packages:
supports-color:
optional: true
+ decimal.js-light@2.5.1:
+ resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
+
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
@@ -1904,6 +1969,9 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
+ eventemitter3@4.0.7:
+ resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+
expect-type@1.3.0:
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
@@ -1911,6 +1979,10 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+ fast-equals@5.4.5:
+ resolution: {integrity: sha512-yKEpcFuRPu/pTQL2VB8YKE2yFldrLlIsoWtJeBnAdqEhvu21OgkFT0C8njf/xYc2LSZGEr3K9JGjthHJxnVLvQ==}
+ engines: {node: '>=6.0.0'}
+
fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'}
@@ -2192,6 +2264,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-alphabetical@2.0.1:
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
@@ -2489,56 +2565,48 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
- libc: [glibc]
lightningcss-linux-arm64-gnu@1.33.0:
resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
- libc: [glibc]
lightningcss-linux-arm64-musl@1.31.1:
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
- libc: [musl]
lightningcss-linux-arm64-musl@1.33.0:
resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
- libc: [musl]
lightningcss-linux-x64-gnu@1.31.1:
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
- libc: [glibc]
lightningcss-linux-x64-gnu@1.33.0:
resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
- libc: [glibc]
lightningcss-linux-x64-musl@1.31.1:
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
- libc: [musl]
lightningcss-linux-x64-musl@1.33.0:
resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
- libc: [musl]
lightningcss-win32-arm64-msvc@1.31.1:
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}
@@ -2585,6 +2653,9 @@ packages:
lodash.truncate@4.4.2:
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
+ lodash@4.18.1:
+ resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
+
longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
@@ -3031,6 +3102,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-number-format@5.4.4:
resolution: {integrity: sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==}
peerDependencies:
@@ -3091,6 +3165,12 @@ packages:
'@types/react-dom':
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'}
@@ -3125,6 +3205,17 @@ packages:
resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==}
engines: {node: '>= 14.18.0'}
+ recharts-scale@0.4.5:
+ resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
+
+ recharts@2.15.4:
+ resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==}
+ engines: {node: '>=14'}
+ deprecated: 1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide
+ 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'}
@@ -3448,6 +3539,9 @@ packages:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
+ 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==}
@@ -3672,6 +3766,9 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+ victory-vendor@36.9.2:
+ resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
+
vite-tsconfig-paths@5.1.4:
resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==}
peerDependencies:
@@ -4300,6 +4397,14 @@ snapshots:
dependencies:
buffer: 6.0.3
+ '@mantine/charts@8.1.2(@mantine/core@8.1.2(@mantine/hooks@8.1.2(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@8.1.2(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(recharts@2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))':
+ dependencies:
+ '@mantine/core': 8.1.2(@mantine/hooks@8.1.2(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@mantine/hooks': 8.1.2(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ recharts: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+
'@mantine/core@8.1.2(@mantine/hooks@8.1.2(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -4728,6 +4833,30 @@ snapshots:
'@types/deep-eql': 4.0.2
assertion-error: 2.0.1
+ '@types/d3-array@3.2.2': {}
+
+ '@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.2.0':
+ dependencies:
+ '@types/d3-path': 3.1.1
+
+ '@types/d3-time@3.0.4': {}
+
+ '@types/d3-timer@3.0.2': {}
+
'@types/debug@4.1.12':
dependencies:
'@types/ms': 2.1.0
@@ -5267,6 +5396,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.2: {}
+
+ 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.2
+ 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-urls@6.0.1:
dependencies:
whatwg-mimetype: 5.0.0
@@ -5308,6 +5475,8 @@ snapshots:
optionalDependencies:
supports-color: 5.5.0
+ decimal.js-light@2.5.1: {}
+
decimal.js@10.6.0: {}
decode-named-character-reference@1.2.0:
@@ -5633,10 +5802,14 @@ snapshots:
esutils@2.0.3: {}
+ eventemitter3@4.0.7: {}
+
expect-type@1.3.0: {}
fast-deep-equal@3.1.3: {}
+ fast-equals@5.4.5: {}
+
fast-glob@3.3.2:
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -5938,6 +6111,8 @@ snapshots:
hasown: 2.0.2
side-channel: 1.1.0
+ internmap@2.0.3: {}
+
is-alphabetical@2.0.1: {}
is-alphanumerical@2.0.1:
@@ -6285,6 +6460,8 @@ snapshots:
lodash.truncate@4.4.2: {}
+ lodash@4.18.1: {}
+
longest-streak@3.1.0: {}
loose-envify@1.4.0:
@@ -6864,6 +7041,8 @@ snapshots:
react-is@17.0.2: {}
+ react-is@18.3.1: {}
+
react-number-format@5.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
@@ -6919,6 +7098,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ fast-equals: 5.4.5
+ prop-types: 15.8.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+
react-style-singleton@2.2.3(@types/react@18.3.18)(react@18.3.1):
dependencies:
get-nonce: 1.0.1
@@ -6955,6 +7142,23 @@ snapshots:
readdirp@4.1.1: {}
+ recharts-scale@0.4.5:
+ dependencies:
+ decimal.js-light: 2.5.1
+
+ recharts@2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ clsx: 2.1.1
+ eventemitter3: 4.0.7
+ lodash: 4.18.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-is: 18.3.1
+ react-smooth: 4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ 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
@@ -7390,6 +7594,8 @@ snapshots:
tapable@2.3.0: {}
+ tiny-invariant@1.3.3: {}
+
tinybench@2.9.0: {}
tinyexec@1.0.2: {}
@@ -7602,6 +7808,23 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
+ victory-vendor@36.9.2:
+ dependencies:
+ '@types/d3-array': 3.2.2
+ '@types/d3-ease': 3.0.2
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-scale': 4.0.9
+ '@types/d3-shape': 3.2.0
+ '@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-tsconfig-paths@5.1.4(typescript@5.6.3)(vite@7.3.6(@types/node@24.0.10)(jiti@2.6.1)(lightningcss@1.33.0)(sass@1.83.1)):
dependencies:
debug: 4.4.0
diff --git a/js/src/app/user/[userId]/UserProfile.page.tsx b/js/src/app/user/[userId]/UserProfile.page.tsx
index 9ffd5ba78..b02b4edd0 100644
--- a/js/src/app/user/[userId]/UserProfile.page.tsx
+++ b/js/src/app/user/[userId]/UserProfile.page.tsx
@@ -1,3 +1,4 @@
+import MetricsChart from "@/app/user/[userId]/_components/UserProfile/MetricsChart/MetricsChart";
import ProfilePicture from "@/app/user/[userId]/_components/UserProfile/ProfilePicture/ProfilePicture";
import UserProfileHeader from "@/app/user/[userId]/_components/UserProfile/UserProfileHeader";
import UserTags from "@/app/user/[userId]/_components/UserProfile/UserTags/UserTags";
@@ -64,6 +65,7 @@ export default function UserProfilePage() {
+
+ HttpResponse.json({
+ success: true,
+ message: "Metrics loaded!",
+ payload: {
+ hasNextPage: false,
+ pages: 1,
+ items: MOCK_METRICS,
+ success: true,
+ },
+ }),
+);
+
+const userMetricsErrorHandler = http.get(userMetricsUrl.url.toString(), () =>
+ HttpResponse.error(),
+);
+
+const mockUseUserMetricsQuery = vi.fn();
+
+vi.mock("@/lib/api/queries/user", () => ({
+ useUserMetricsQuery: (...args: unknown[]) => mockUseUserMetricsQuery(...args),
+}));
+
+const BASE_QUERY_RESULT = {
+ data: undefined,
+ status: "pending",
+ page: 1,
+ goBack: vi.fn(),
+ goForward: vi.fn(),
+ goTo: vi.fn(),
+};
+
+describe("MetricsChart succeeded", () => {
+ afterEach(() => {
+ cleanup();
+ vi.clearAllMocks();
+ });
+
+ let renderProviderFn: TestUtilTypes.RenderWithAllProvidersFn | null = null;
+ beforeEach(() => {
+ renderProviderFn = TestUtils.getRenderWithAllProvidersFn();
+ mockUseUserMetricsQuery.mockReturnValue(BASE_QUERY_RESULT);
+ });
+
+ it("should render skeleton metrics chart initially", () => {
+ const MOCK_USER_ID = uuid();
+ renderProviderFn?.();
+ const element = screen.getByTestId("user-metrics-chart-skeleton");
+ expect(element).toBeInTheDocument();
+ expect(element).toBeVisible();
+ });
+
+ it("should render AreaChart eventually", async () => {
+ mockUseUserMetricsQuery.mockReturnValue({
+ ...BASE_QUERY_RESULT,
+ status: "success",
+ data: {
+ payload: { items: [], pages: 1, hasNextPage: false },
+ success: true,
+ },
+ });
+ const MOCK_USER_ID = uuid();
+ renderProviderFn?.();
+ const element = await screen.findByTestId(/metrics-area-chart.*$/);
+ expect(element).toBeInTheDocument();
+ expect(element).toBeVisible();
+ });
+
+ it("should show data message toast and not render AreaChart when data is not successful", async () => {
+ const mockMessage = "Data not success.";
+ mockUseUserMetricsQuery.mockReturnValue({
+ ...BASE_QUERY_RESULT,
+ status: "success",
+ data: {
+ payload: { items: [], pages: 1, hasNextPage: false },
+ success: false,
+ message: mockMessage,
+ },
+ });
+ const MOCK_USER_ID = uuid();
+ renderProviderFn?.();
+
+ await waitFor(() => {
+ expect(screen.getByText(mockMessage)).toBeInTheDocument();
+ });
+
+ await waitFor(() => {
+ expect(
+ screen.queryByTestId(/metrics-area-chart.*$/),
+ ).not.toBeInTheDocument();
+ });
+ });
+});
+
+describe("UserMetricsChart with successful API", () => {
+ const server = setupServer(userMetricsSuccessHandler);
+
+ beforeAll(() => server.listen());
+ afterEach(() => {
+ server.resetHandlers();
+ cleanup();
+ vi.clearAllMocks();
+ });
+ afterAll(() => server.close());
+
+ let renderProviderFn: TestUtilTypes.RenderWithAllProvidersFn | null = null;
+ beforeEach(() => {
+ renderProviderFn = TestUtils.getRenderWithAllProvidersFn();
+
+ mockUseUserMetricsQuery.mockReturnValue({
+ ...BASE_QUERY_RESULT,
+ status: "success",
+ data: {
+ payload: { items: MOCK_METRICS, pages: 2, hasNextPage: true },
+ success: true,
+ },
+ });
+ });
+
+ it("should have formatted dates for chart after successful API call", async () => {
+ renderProviderFn?.();
+
+ const element = await screen.findByTestId(
+ /metrics-area-chart Oct 01, 2026.*$/,
+ );
+ expect(element).toBeInTheDocument();
+ expect(element).toBeVisible();
+ });
+
+ it("should have correct userMetrics pages and hasNextPage based on data payload", async () => {
+ renderProviderFn?.();
+
+ const element = await screen.findByTestId(
+ "metrics-area-chart Oct 01, 2026 pages2 hasNextPagetrue",
+ );
+ expect(element).toBeInTheDocument();
+ expect(element).toBeVisible();
+ });
+});
+
+describe("UserMetricsChart error state", () => {
+ const server = setupServer(userMetricsErrorHandler);
+
+ beforeAll(() => server.listen());
+ afterEach(() => {
+ server.resetHandlers();
+ cleanup();
+ vi.clearAllMocks();
+ });
+ afterAll(() => server.close());
+
+ let renderProviderFn: TestUtilTypes.RenderWithAllProvidersFn | null = null;
+ beforeEach(() => {
+ renderProviderFn = TestUtils.getRenderWithAllProvidersFn();
+ mockUseUserMetricsQuery.mockReturnValue({
+ ...BASE_QUERY_RESULT,
+ status: "error",
+ data: undefined,
+ });
+ });
+
+ it("should show error toast when API errors", async () => {
+ renderProviderFn?.();
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(/sorry, something went wrong/i),
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/js/src/app/user/[userId]/_components/UserProfile/MetricsChart/MetricsChart.tsx b/js/src/app/user/[userId]/_components/UserProfile/MetricsChart/MetricsChart.tsx
new file mode 100644
index 000000000..77a7eca24
--- /dev/null
+++ b/js/src/app/user/[userId]/_components/UserProfile/MetricsChart/MetricsChart.tsx
@@ -0,0 +1,68 @@
+import MetricsChartSkeleton from "@/app/user/[userId]/_components/UserProfile/MetricsChart/MetricsChartSkeleton";
+import Paginator from "@/components/ui/table/Paginator";
+import Toast from "@/components/ui/toast/Toast";
+import { useUserMetricsQuery } from "@/lib/api/queries/user";
+import { theme } from "@/lib/theme";
+import { AreaChart } from "@mantine/charts";
+import { Box, Divider } from "@mantine/core";
+import "@mantine/charts/styles.css";
+import d from "dayjs";
+import utc from "dayjs/plugin/utc";
+
+d.extend(utc);
+
+export default function MetricsChart({ userId }: { readonly userId: string }) {
+ const { data, status, page, goBack, goForward, goTo } = useUserMetricsQuery({
+ userId,
+ });
+
+ if (status === "pending") {
+ return ;
+ }
+
+ if (status === "error") {
+ return (
+
+ );
+ }
+
+ if (!data.success) {
+ return ;
+ }
+
+ const userMetrics = data.payload;
+ const pageData = userMetrics.items.map((item) => {
+ return {
+ ...item,
+ createdAt: d(item.createdAt).utc().format("MMM DD, YYYY"),
+ };
+ });
+
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+}
diff --git a/js/src/app/user/[userId]/_components/UserProfile/MetricsChart/MetricsChartSkeleton.tsx b/js/src/app/user/[userId]/_components/UserProfile/MetricsChart/MetricsChartSkeleton.tsx
new file mode 100644
index 000000000..1304d5380
--- /dev/null
+++ b/js/src/app/user/[userId]/_components/UserProfile/MetricsChart/MetricsChartSkeleton.tsx
@@ -0,0 +1,15 @@
+import { Flex, Skeleton, Divider } from "@mantine/core";
+
+export default function MetricsChartSkeleton() {
+ return (
+
+
+
+
+ );
+}
diff --git a/js/src/lib/api/queries/user/index.ts b/js/src/lib/api/queries/user/index.ts
index 9f18fc482..29023a294 100644
--- a/js/src/lib/api/queries/user/index.ts
+++ b/js/src/lib/api/queries/user/index.ts
@@ -304,6 +304,87 @@ export const useGetAllUsersInfiniteQuery = ({
};
};
+/**
+ * Fetches the user's metrics.
+ */
+export const useUserMetricsQuery = ({
+ userId,
+ initialPage = 1,
+ tieToUrl = false,
+ pageSize = 20,
+}: {
+ userId: string;
+ initialPage?: number;
+ tieToUrl?: boolean;
+ pageSize?: number;
+}) => {
+ const { page, goBack, goForward, goTo } = usePagination({
+ initialPage: initialPage,
+ tieToUrl: tieToUrl,
+ });
+
+ const {
+ startDate,
+ endDate,
+ setStartDate,
+ setEndDate,
+ debouncedStartDate,
+ debouncedEndDate,
+ } = useURLDateRange(tieToUrl);
+
+ // if seconds are not included, use start of day and end of day
+ const processedStartDate = useMemo(() => {
+ if (debouncedStartDate) {
+ if (debouncedStartDate.includes(":"))
+ return d.utc(debouncedStartDate).toISOString();
+ return d(debouncedStartDate).startOf("day").utc().toISOString();
+ }
+ return d(0).utc().toISOString();
+ }, [debouncedStartDate]);
+ const processedEndDate = useMemo(() => {
+ if (debouncedEndDate) {
+ if (debouncedEndDate.includes(":"))
+ return d.utc(debouncedEndDate).toISOString();
+ return d(debouncedEndDate).endOf("day").utc().toISOString();
+ }
+ return undefined;
+ }, [debouncedEndDate]);
+
+ const apiURL = ApiURL.create("/api/user/{userId}/metrics", {
+ method: "GET",
+ params: {
+ userId,
+ },
+ queries: {
+ page,
+ pageSize,
+ startDate: processedStartDate,
+ endDate: processedEndDate,
+ },
+ });
+
+ const { queryKey } = apiURL;
+ const query = useQuery({
+ queryKey,
+ queryFn: () => fetchUserMetrics(apiURL),
+ placeholderData: (previousData, previousQuery) =>
+ previousQuery?.queryKey[2] === queryKey[2] ? previousData : undefined,
+ });
+
+ return {
+ ...query,
+ page,
+ goBack,
+ goForward,
+ goTo,
+ pageSize,
+ startDate,
+ endDate,
+ setStartDate,
+ setEndDate,
+ };
+};
+
async function fetchUserProfile({
url,
method,
@@ -350,3 +431,19 @@ async function fetchAllUsers({
return json;
}
+
+async function fetchUserMetrics({
+ url,
+ method,
+ res,
+}: ApiURL<"/api/user/{userId}/metrics", "get">) {
+ const response = await fetch(url, {
+ method,
+ });
+
+ await sleep(600);
+
+ const json = res(await response.json());
+
+ return json;
+}