diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b78848946..39066306b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -187,7 +187,7 @@ importers: version: 13.0.1 stripe: specifier: ^20.4.1 - version: 20.4.1(@types/node@24.10.13) + version: 20.4.1(@types/node@25.6.0) thenby: specifier: ^1.4.0 version: 1.4.0 @@ -316,6 +316,42 @@ importers: specifier: ^6.0.3 version: 6.0.3 + dist: + dependencies: + '@tanstack/react-query': + specifier: ^4.33.0 + version: 4.44.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: + specifier: ^2.3.1 + version: 2.5.1 + colord: + specifier: ^2.9.2 + version: 2.9.3 + immer: + specifier: ^9.0.12 + version: 9.0.21 + moment-timezone: + specifier: ^0.5.35 + version: 0.5.48 + next: + specifier: ^13.4.0 + version: 13.5.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-basics: + specifier: ^0.36.0 + version: 0.36.0(next@13.5.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + react-intl: + specifier: ^5.24.7 + version: 5.25.1(react@18.3.1)(typescript@6.0.3) + zustand: + specifier: ^4.3.8 + version: 4.5.7(@types/react@18.3.28)(immer@9.0.21)(react@18.3.1) + packages: '@ampproject/remapping@2.3.0': @@ -568,24 +604,28 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@2.4.12': resolution: {integrity: sha512-tOwuCuZZtKi1jVzbk/5nXmIsziOB6yqN8c9r9QM0EJYPU6DpQWf11uBOSCfFKKM4H3d9ZoarvlgMfbcuD051Pw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@2.4.12': resolution: {integrity: sha512-dwTIgZrGutzhkQCuvHynCkyW6hJxUuyZqKKO0YNfaS2GUoRO+tOvxXZqZB6SkWAOdfZTzwaw8IEdUnIkHKHoew==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@2.4.12': resolution: {integrity: sha512-8pFeAnLU9QdW9jCIslB/v82bI0lhBmz2ZAKc8pVMFPO0t0wAHsoEkrUQUbMkIorTRIjbqyNZHA3lEXavsPWYSw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@2.4.12': resolution: {integrity: sha512-B0DLnx0vA9ya/3v7XyCaP+/lCpnbWbMOfUFFve+xb5OxyYvdHaS55YsSddr228Y+JAFk58agCuZTsqNiw2a6ig==} @@ -1475,18 +1515,47 @@ packages: cpu: [x64] os: [win32] + '@formatjs/ecma402-abstract@1.11.4': + resolution: {integrity: sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==} + + '@formatjs/fast-memoize@1.2.1': + resolution: {integrity: sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==} + '@formatjs/fast-memoize@3.1.2': resolution: {integrity: sha512-vPnriihkfK0lzoQGaXq+qXH23VsYyansRTkTgo2aTG0k1NjLFyZimFVdfj4C9JkSE5dm7CEngcQ5TTc1yAyBfQ==} + '@formatjs/icu-messageformat-parser@2.1.0': + resolution: {integrity: sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==} + '@formatjs/icu-messageformat-parser@3.5.4': resolution: {integrity: sha512-JVY39ROgLt+pIYngo6piyj4OVfZmXs/2FkC4wLS+ql1Eig/sGJKB7YwDO/5bkJFkfwaFAeIpgEiJc8hiYxNalw==} + '@formatjs/icu-skeleton-parser@1.3.6': + resolution: {integrity: sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==} + '@formatjs/icu-skeleton-parser@2.1.4': resolution: {integrity: sha512-8bSFZbrlvGX11ywMZxtgkPBt5Q8/etyts7j7j+GWpOVK1g43zwMIH3LZxk43HAtEP7L/jtZ+OZaMiFTOiBj9CA==} + '@formatjs/intl-displaynames@5.4.3': + resolution: {integrity: sha512-4r12A3mS5dp5hnSaQCWBuBNfi9Amgx2dzhU4lTFfhSxgb5DOAiAbMpg6+7gpWZgl4ahsj3l2r/iHIjdmdXOE2Q==} + + '@formatjs/intl-listformat@6.5.3': + resolution: {integrity: sha512-ozpz515F/+3CU+HnLi5DYPsLa6JoCfBggBSSg/8nOB5LYSFW9+ZgNQJxJ8tdhKYeODT+4qVHX27EeJLoxLGLNg==} + + '@formatjs/intl-localematcher@0.2.25': + resolution: {integrity: sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==} + '@formatjs/intl-localematcher@0.8.3': resolution: {integrity: sha512-pHUjWb9NuhnMs8+PxQdzBtZRFJHlGhrURGAbm6Ltwl82BFajeuiIR3jblSa7ia3r62rXe/0YtVpUG3xWr5bFCA==} + '@formatjs/intl@2.2.1': + resolution: {integrity: sha512-vgvyUOOrzqVaOFYzTf2d3+ToSkH2JpR7x/4U1RyoHQLmvEaTQvXJ7A2qm1Iy3brGNXC/+/7bUlc3lpH+h/LOJA==} + peerDependencies: + typescript: ^4.5 + peerDependenciesMeta: + typescript: + optional: true + '@hello-pangea/dnd@18.0.1': resolution: {integrity: sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==} peerDependencies: @@ -1529,89 +1598,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -1793,44 +1878,97 @@ packages: resolution: {integrity: sha512-4kJvV+wrt4/ncehfaOXfhv1FYKCv9EFjq6/tgXXYSlqPDcbsuTXR0DTz9IvyZ/CpPRadRD/zzIqD9wXgCmA2lg==} engines: {node: '>=18.0.0'} + '@next/env@13.5.11': + resolution: {integrity: sha512-fbb2C7HChgM7CemdCY+y3N1n8pcTKdqtQLbC7/EQtPdLvlMUT9JX/dBYl8MMZAtYG4uVMyPFHXckb68q/NRwqg==} + '@next/env@16.2.4': resolution: {integrity: sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==} + '@next/swc-darwin-arm64@13.5.9': + resolution: {integrity: sha512-pVyd8/1y1l5atQRvOaLOvfbmRwefxLhqQOzYo/M7FQ5eaRwA1+wuCn7t39VwEgDd7Aw1+AIWwd+MURXUeXhwDw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + '@next/swc-darwin-arm64@16.2.4': resolution: {integrity: sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] + '@next/swc-darwin-x64@13.5.9': + resolution: {integrity: sha512-DwdeJqP7v8wmoyTWPbPVodTwCybBZa02xjSJ6YQFIFZFZ7dFgrieKW4Eo0GoIcOJq5+JxkQyejmI+8zwDp3pwA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + '@next/swc-darwin-x64@16.2.4': resolution: {integrity: sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] + '@next/swc-linux-arm64-gnu@13.5.9': + resolution: {integrity: sha512-wdQsKsIsGSNdFojvjW3Ozrh8Q00+GqL3wTaMjDkQxVtRbAqfFBtrLPO0IuWChVUP2UeuQcHpVeUvu0YgOP00+g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@next/swc-linux-arm64-gnu@16.2.4': resolution: {integrity: sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@13.5.9': + resolution: {integrity: sha512-6VpS+bodQqzOeCwGxoimlRoosiWlSc0C224I7SQWJZoyJuT1ChNCo+45QQH+/GtbR/s7nhaUqmiHdzZC9TXnXA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] '@next/swc-linux-arm64-musl@16.2.4': resolution: {integrity: sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@13.5.9': + resolution: {integrity: sha512-XxG3yj61WDd28NA8gFASIR+2viQaYZEFQagEodhI/R49gXWnYhiflTeeEmCn7Vgnxa/OfK81h1gvhUZ66lozpw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] '@next/swc-linux-x64-gnu@16.2.4': resolution: {integrity: sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@13.5.9': + resolution: {integrity: sha512-/dnscWqfO3+U8asd+Fc6dwL2l9AZDl7eKtPNKW8mKLh4Y4wOpjJiamhe8Dx+D+Oq0GYVjuW0WwjIxYWVozt2bA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] '@next/swc-linux-x64-musl@16.2.4': resolution: {integrity: sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@13.5.9': + resolution: {integrity: sha512-T/iPnyurOK5a4HRUcxAlss8uzoEf5h9tkd+W2dSWAfzxv8WLKlUgbfk+DH43JY3Gc2xK5URLuXrxDZ2mGfk/jw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] '@next/swc-win32-arm64-msvc@16.2.4': resolution: {integrity: sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==} @@ -1838,6 +1976,18 @@ packages: cpu: [arm64] os: [win32] + '@next/swc-win32-ia32-msvc@13.5.9': + resolution: {integrity: sha512-BLiPKJomaPrTAb7ykjA0LPcuuNMLDVK177Z1xe0nAem33+9FIayU4k/OWrtSn9SAJW/U60+1hoey5z+KCHdRLQ==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@next/swc-win32-x64-msvc@13.5.9': + resolution: {integrity: sha512-/72/dZfjXXNY/u+n8gqZDjI6rxKMpYsgBBYNZKWOQw0BpBF7WCnPflRy3ZtvQ2+IYI3ZH2bPyj7K+6a6wNk90Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@next/swc-win32-x64-msvc@16.2.4': resolution: {integrity: sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==} engines: {node: '>= 10'} @@ -1885,36 +2035,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} @@ -2312,66 +2468,79 @@ packages: resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.1': resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.1': resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.1': resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.1': resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.1': resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.1': resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.1': resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.1': resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.1': resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.1': resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.1': resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.1': resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.1': resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} @@ -2533,36 +2702,42 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] '@swc/core-linux-arm64-musl@1.15.26': resolution: {integrity: sha512-iNlbvTIo425rkKzDLLWFJGnFXr3myETUdIDHcjuiPNZE8b0ogmcAuilC4yEJX7FSHGbnlsoJcCT2xf4b3VJmmQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] '@swc/core-linux-ppc64-gnu@1.15.26': resolution: {integrity: sha512-AuuEOtG+YXKIjIUup4RsxYNklx6XVB3WKWfhxG6hnfPrn7vp89RNOLbbyyprgj6Sk7k9ulwGVTJElEvmBNPSCA==} engines: {node: '>=10'} cpu: [ppc64] os: [linux] + libc: [glibc] '@swc/core-linux-s390x-gnu@1.15.26': resolution: {integrity: sha512-JcMDWQvW1BchUyRg8E0jHiTx7CQYpUr5uDEL1dnPDECrEjBEGG2ynmJ3XX70sWXql0JagqR1t3VpANYFWdUnqA==} engines: {node: '>=10'} cpu: [s390x] os: [linux] + libc: [glibc] '@swc/core-linux-x64-gnu@1.15.26': resolution: {integrity: sha512-FW7V7Mbpq4+PA7BiAq76LJs8MdNuUSylyuRVfQRkhIyeWadFroZ+KOPgjku8Z/fXzngxBRvsk+PGGB0t8mGcjA==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] '@swc/core-linux-x64-musl@1.15.26': resolution: {integrity: sha512-w8erqMHsVcdGwUfJxF6LaiTuPoKnyLOcUbhLcxiXrlLt5MLjtlgcIeUY/NWK/oPoyqkgH+/i8pOJnMTxvl83ZQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] '@swc/core-win32-arm64-msvc@1.15.26': resolution: {integrity: sha512-uDCWCNpUiqkbvPmsuPUTn/P7ag9SqNXD2JT/W3dUu7yZ2krzN+nmmoQ2xRX63/J6RYiHI7aT4jo7Z++lsljlPA==} @@ -2600,15 +2775,33 @@ packages: '@swc/helpers@0.5.18': resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} + '@swc/helpers@0.5.2': + resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} + '@swc/helpers@0.5.21': resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} '@swc/types@0.1.26': resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} + '@tanstack/query-core@4.44.0': + resolution: {integrity: sha512-swSgb7OiPRR3UuIL7NuDrZNSMGmQD+wdtHxPD7j60SvBEnxbXurl5XOirtGEX2gm2hbK6mC8kMV1I+uO3l0UOw==} + '@tanstack/query-core@5.99.0': resolution: {integrity: sha512-3Jv3WQG0BCcH7G+7lf/bP8QyBfJOXeY+T08Rin3GZ1bshvwlbPt7NrDHMEzGdKIOmOzvIQmxjk28YEQX60k7pQ==} + '@tanstack/react-query@4.44.0': + resolution: {integrity: sha512-RuIqHYrS98LrK/8kJJOJMMSQ/BCpojwsXDh7p0fBmp38ZOz6dlk+uyFRRusH+V+t3POoCsDOQ2zhomEYOeReXw==} + 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-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + '@tanstack/react-query@5.99.0': resolution: {integrity: sha512-OY2bCqPemT1LlqJ8Y2CUau4KELnIhhG9Ol3ZndPbdnB095pRbPo1cHuXTndg8iIwtoHTgwZjyaDnQ0xD0mYwAw==} peerDependencies: @@ -2663,6 +2856,11 @@ packages: '@types/glob@7.2.0': resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + '@types/hoist-non-react-statics@3.3.7': + resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} + peerDependencies: + '@types/react': '*' + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -2688,6 +2886,9 @@ packages: '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -2778,41 +2979,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -4058,6 +4267,9 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hono@4.12.9: resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==} engines: {node: '>=16.9.0'} @@ -4165,6 +4377,9 @@ packages: intl-messageformat@11.2.1: resolution: {integrity: sha512-1gAVEUt3wEPvTqML4Fsw9klZV5j0vszQxayP/fi6gUroAc8AUHiNaisBKLWxybL1AdWq1mP07YV1q8v4N92ilQ==} + intl-messageformat@9.13.0: + resolution: {integrity: sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==} + ipaddr.js@2.3.0: resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} engines: {node: '>= 10'} @@ -4811,6 +5026,12 @@ packages: resolution: {integrity: sha512-7e87vk0DdWT647wjcfEtWeMtjm+zVGqNohN/aeIymbUfjHQ2T4Sx5kM+1irVDBSloNC3CkGKxswdMoo8yhqTDg==} engines: {node: '>=10', npm: '>=6'} + moment-timezone@0.5.48: + resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + motion-dom@12.38.0: resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==} @@ -4865,6 +5086,13 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + next-basics@0.36.0: + resolution: {integrity: sha512-Nwou8pCjFuoD/ZxUw9iKC7hhZeWbo/ng0ze74yck3W89MNc/CepwCDziflAHY5XcmIVNmpXOCu9OfmzTdVRPWQ==} + peerDependencies: + next: ^13.4.0 + react: ^18.2.0 + react-dom: ^18.2.0 + next-intl-swc-plugin-extractor@4.9.1: resolution: {integrity: sha512-8whJJ6oxJz8JqkHarggmmuEDyXgC7nEnaPhZD91CJwEWW4xp0AST3Mw17YxvHyP2vAF3taWfFbs1maD+WWtz3w==} @@ -4878,6 +5106,21 @@ packages: typescript: optional: true + next@13.5.11: + resolution: {integrity: sha512-WUPJ6WbAX9tdC86kGTu92qkrRdgRqVrY++nwM+shmWQwmyxt4zhZfR59moXSI4N8GDYCBY3lIAqhzjDd4rTC8Q==} + engines: {node: '>=16.14.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + sass: + optional: true + next@16.2.4: resolution: {integrity: sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==} engines: {node: '>=20.9.0'} @@ -5714,6 +5957,11 @@ packages: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + react-dom@19.2.5: resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} peerDependencies: @@ -5790,6 +6038,10 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + react@19.2.5: resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} engines: {node: '>=0.10.0'} @@ -6149,6 +6401,10 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + string-hash@1.1.3: resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} @@ -6219,6 +6475,19 @@ packages: style-inject@0.3.0: resolution: {integrity: sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==} + styled-jsx@5.1.1: + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -6579,6 +6848,7 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -6714,6 +6984,21 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + zustand@5.0.12: resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==} engines: {node: '>=12.20.0'} @@ -7782,18 +8067,66 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true + '@formatjs/ecma402-abstract@1.11.4': + dependencies: + '@formatjs/intl-localematcher': 0.2.25 + tslib: 2.8.1 + + '@formatjs/fast-memoize@1.2.1': + dependencies: + tslib: 2.8.1 + '@formatjs/fast-memoize@3.1.2': {} + '@formatjs/icu-messageformat-parser@2.1.0': + dependencies: + '@formatjs/ecma402-abstract': 1.11.4 + '@formatjs/icu-skeleton-parser': 1.3.6 + tslib: 2.8.1 + '@formatjs/icu-messageformat-parser@3.5.4': dependencies: '@formatjs/icu-skeleton-parser': 2.1.4 + '@formatjs/icu-skeleton-parser@1.3.6': + dependencies: + '@formatjs/ecma402-abstract': 1.11.4 + tslib: 2.8.1 + '@formatjs/icu-skeleton-parser@2.1.4': {} + '@formatjs/intl-displaynames@5.4.3': + dependencies: + '@formatjs/ecma402-abstract': 1.11.4 + '@formatjs/intl-localematcher': 0.2.25 + tslib: 2.8.1 + + '@formatjs/intl-listformat@6.5.3': + dependencies: + '@formatjs/ecma402-abstract': 1.11.4 + '@formatjs/intl-localematcher': 0.2.25 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.2.25': + dependencies: + tslib: 2.8.1 + '@formatjs/intl-localematcher@0.8.3': dependencies: '@formatjs/fast-memoize': 3.1.2 + '@formatjs/intl@2.2.1(typescript@6.0.3)': + dependencies: + '@formatjs/ecma402-abstract': 1.11.4 + '@formatjs/fast-memoize': 1.2.1 + '@formatjs/icu-messageformat-parser': 2.1.0 + '@formatjs/intl-displaynames': 5.4.3 + '@formatjs/intl-listformat': 6.5.3 + intl-messageformat: 9.13.0 + tslib: 2.8.1 + optionalDependencies: + typescript: 6.0.3 + '@hello-pangea/dnd@18.0.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@babel/runtime': 7.29.2 @@ -8184,29 +8517,58 @@ snapshots: '@netlify/plugin-nextjs@5.15.9': {} + '@next/env@13.5.11': {} + '@next/env@16.2.4': {} + '@next/swc-darwin-arm64@13.5.9': + optional: true + '@next/swc-darwin-arm64@16.2.4': optional: true + '@next/swc-darwin-x64@13.5.9': + optional: true + '@next/swc-darwin-x64@16.2.4': optional: true + '@next/swc-linux-arm64-gnu@13.5.9': + optional: true + '@next/swc-linux-arm64-gnu@16.2.4': optional: true + '@next/swc-linux-arm64-musl@13.5.9': + optional: true + '@next/swc-linux-arm64-musl@16.2.4': optional: true + '@next/swc-linux-x64-gnu@13.5.9': + optional: true + '@next/swc-linux-x64-gnu@16.2.4': optional: true + '@next/swc-linux-x64-musl@13.5.9': + optional: true + '@next/swc-linux-x64-musl@16.2.4': optional: true + '@next/swc-win32-arm64-msvc@13.5.9': + optional: true + '@next/swc-win32-arm64-msvc@16.2.4': optional: true + '@next/swc-win32-ia32-msvc@13.5.9': + optional: true + + '@next/swc-win32-x64-msvc@13.5.9': + optional: true + '@next/swc-win32-x64-msvc@16.2.4': optional: true @@ -8882,6 +9244,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@swc/helpers@0.5.2': + dependencies: + tslib: 2.8.1 + '@swc/helpers@0.5.21': dependencies: tslib: 2.8.1 @@ -8890,8 +9256,18 @@ snapshots: dependencies: '@swc/counter': 0.1.3 + '@tanstack/query-core@4.44.0': {} + '@tanstack/query-core@5.99.0': {} + '@tanstack/react-query@4.44.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/query-core': 4.44.0 + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + '@tanstack/react-query@5.99.0(react@19.2.5)': dependencies: '@tanstack/query-core': 5.99.0 @@ -8954,6 +9330,11 @@ snapshots: '@types/minimatch': 6.0.0 '@types/node': 25.6.0 + '@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + hoist-non-react-statics: 3.3.2 + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -10521,6 +10902,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + hono@4.12.9: {} hosted-git-info@2.8.9: {} @@ -10609,6 +10994,13 @@ snapshots: '@formatjs/fast-memoize': 3.1.2 '@formatjs/icu-messageformat-parser': 3.5.4 + intl-messageformat@9.13.0: + dependencies: + '@formatjs/ecma402-abstract': 1.11.4 + '@formatjs/fast-memoize': 1.2.1 + '@formatjs/icu-messageformat-parser': 2.1.0 + tslib: 2.8.1 + ipaddr.js@2.3.0: {} is-array-buffer@3.0.5: @@ -11428,6 +11820,12 @@ snapshots: mmdb-lib@3.0.2: {} + moment-timezone@0.5.48: + dependencies: + moment: 2.30.1 + + moment@2.30.1: {} + motion-dom@12.38.0: dependencies: motion-utils: 12.36.0 @@ -11476,6 +11874,15 @@ snapshots: neo-async@2.6.2: {} + next-basics@0.36.0(next@13.5.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + bcryptjs: 2.4.3 + jsonwebtoken: 9.0.3 + next: 13.5.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + pure-rand: 6.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + next-intl-swc-plugin-extractor@4.9.1: {} next-intl@4.9.1(@swc/helpers@0.5.21)(next@16.2.4(@babel/core@7.29.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.3): @@ -11495,6 +11902,31 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' + next@13.5.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 13.5.11 + '@swc/helpers': 0.5.2 + busboy: 1.6.0 + caniuse-lite: 1.0.30001788 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.1(@babel/core@7.29.0)(react@18.3.1) + watchpack: 2.4.0 + optionalDependencies: + '@next/swc-darwin-arm64': 13.5.9 + '@next/swc-darwin-x64': 13.5.9 + '@next/swc-linux-arm64-gnu': 13.5.9 + '@next/swc-linux-arm64-musl': 13.5.9 + '@next/swc-linux-x64-gnu': 13.5.9 + '@next/swc-linux-x64-musl': 13.5.9 + '@next/swc-win32-arm64-msvc': 13.5.9 + '@next/swc-win32-ia32-msvc': 13.5.9 + '@next/swc-win32-x64-msvc': 13.5.9 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + next@16.2.4(@babel/core@7.29.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@next/env': 16.2.4 @@ -12359,6 +12791,12 @@ snapshots: react-stately: 3.46.0(react@19.2.5) use-sync-external-store: 1.6.0(react@19.2.5) + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + react-dom@19.2.5(react@19.2.5): dependencies: react: 19.2.5 @@ -12372,11 +12810,11 @@ snapshots: dependencies: react: 19.2.5 - react-intl@5.25.1(react@18.3.1)(typescript@5.9.3): + react-intl@5.25.1(react@18.3.1)(typescript@6.0.3): dependencies: '@formatjs/ecma402-abstract': 1.11.4 '@formatjs/icu-messageformat-parser': 2.1.0 - '@formatjs/intl': 2.2.1(typescript@5.9.3) + '@formatjs/intl': 2.2.1(typescript@6.0.3) '@formatjs/intl-displaynames': 5.4.3 '@formatjs/intl-listformat': 6.5.3 '@types/hoist-non-react-statics': 3.3.7(@types/react@18.3.28) @@ -12386,7 +12824,7 @@ snapshots: react: 18.3.1 tslib: 2.8.1 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 react-is@16.13.1: {} @@ -12437,6 +12875,10 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + react@19.2.5: {} read-cache@1.0.0: @@ -12892,6 +13334,8 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + streamsearch@1.1.0: {} + string-hash@1.1.3: {} string-length@4.0.2: @@ -12961,12 +13405,19 @@ snapshots: strip-json-comments@3.1.1: {} - stripe@20.4.1(@types/node@24.10.13): + stripe@20.4.1(@types/node@25.6.0): optionalDependencies: - '@types/node': 24.10.13 + '@types/node': 25.6.0 style-inject@0.3.0: {} + styled-jsx@5.1.1(@babel/core@7.29.0)(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + optionalDependencies: + '@babel/core': 7.29.0 + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.5): dependencies: client-only: 0.0.1 @@ -13336,6 +13787,10 @@ snapshots: intl-messageformat: 11.2.1 react: 19.2.5 + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + use-sync-external-store@1.6.0(react@19.2.5): dependencies: react: 19.2.5 @@ -13499,6 +13954,14 @@ snapshots: zod@4.3.6: {} + zustand@4.5.7(@types/react@18.3.28)(immer@9.0.21)(react@18.3.1): + dependencies: + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + immer: 9.0.21 + react: 18.3.1 + zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)): optionalDependencies: '@types/react': 19.2.14 diff --git a/prisma/migrations/20_add_billing/migration.sql b/prisma/migrations/20_add_billing/migration.sql new file mode 100644 index 000000000..4fd6d7de0 --- /dev/null +++ b/prisma/migrations/20_add_billing/migration.sql @@ -0,0 +1,64 @@ +-- CreateTable +CREATE TABLE "billing_invoice" ( + "line_id" VARCHAR(255) NOT NULL, + "provider_id" UUID NOT NULL, + "invoice_id" VARCHAR(255) NOT NULL, + "customer_id" VARCHAR(255) NOT NULL, + "invoice_status" VARCHAR(50) NOT NULL, + "invoice_period_end" TIMESTAMPTZ(6) NOT NULL, + "usage_type" VARCHAR(20) NOT NULL, + "amount_cents" INTEGER NOT NULL, + "period_start" TIMESTAMPTZ(6) NOT NULL, + "period_end" TIMESTAMPTZ(6) NOT NULL, + "period_months" INTEGER NOT NULL, + "mrr_cents" INTEGER NOT NULL, + + CONSTRAINT "billing_invoice_pkey" PRIMARY KEY ("line_id") +); + +-- CreateTable +CREATE TABLE "billing_provider" ( + "billing_id" UUID NOT NULL, + "name" VARCHAR(100) NOT NULL, + "provider" VARCHAR(50) NOT NULL, + "user_id" UUID, + "team_id" UUID, + "api_key" TEXT NOT NULL, + "sync_cursor" VARCHAR(255), + "sync_status" VARCHAR(20) NOT NULL DEFAULT 'idle', + "last_run_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "billing_provider_pkey" PRIMARY KEY ("billing_id") +); + +-- CreateIndex +CREATE INDEX "billing_invoice_provider_id_idx" ON "billing_invoice"("provider_id"); + +-- CreateIndex +CREATE INDEX "billing_invoice_invoice_id_idx" ON "billing_invoice"("invoice_id"); + +-- CreateIndex +CREATE INDEX "billing_invoice_customer_id_idx" ON "billing_invoice"("customer_id"); + +-- CreateIndex +CREATE INDEX "billing_invoice_invoice_status_idx" ON "billing_invoice"("invoice_status"); + +-- CreateIndex +CREATE INDEX "billing_invoice_usage_type_idx" ON "billing_invoice"("usage_type"); + +-- CreateIndex +CREATE INDEX "billing_provider_user_id_idx" ON "billing_provider"("user_id"); + +-- CreateIndex +CREATE INDEX "billing_provider_team_id_idx" ON "billing_provider"("team_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "billing_provider_name_user_id_key" ON "billing_provider"("name", "user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "billing_provider_name_team_id_key" ON "billing_provider"("name", "team_id"); + +-- CreateIndex +CREATE INDEX "session_replay_visit_id_idx" ON "session_replay"("visit_id"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 127ff2b43..2e4f1a2ab 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -415,7 +415,7 @@ model BillingInvoice { periodMonths Int @map("period_months") @db.Integer mrrCents Int @map("mrr_cents") @db.Integer - billingProvider BillingProvider @relation(fields: [providerId], references: [id]) + billingProvider Billing @relation(fields: [providerId], references: [id]) @@index([providerId]) @@index([invoiceId]) @@ -425,21 +425,23 @@ model BillingInvoice { @@map("billing_invoice") } -model BillingProvider { - id String @id @default(uuid()) @map("billing_provider_id") @db.Uuid - provider String @db.VarChar(50) - userId String? @map("user_id") @db.Uuid - teamId String? @map("team_id") @db.Uuid - apiKey String @map("api_key") @db.Text - syncCursor String? @map("sync_cursor") @db.VarChar(255) - syncStatus String @default("idle") @map("sync_status") @db.VarChar(20) - createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) - updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6) +model Billing { + id String @id @default(uuid()) @map("billing_id") @db.Uuid + name String @db.VarChar(100) + provider String @db.VarChar(50) + userId String? @map("user_id") @db.Uuid + teamId String? @map("team_id") @db.Uuid + apiKey String @map("api_key") @db.Text + syncCursor String? @map("sync_cursor") @db.VarChar(255) + syncStatus String @default("idle") @map("sync_status") @db.VarChar(20) + lastRunAt DateTime? @map("last_run_at") @db.Timestamptz(6) + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) invoices BillingInvoice[] - @@unique([provider, userId]) - @@unique([provider, teamId]) + @@unique([name, userId]) + @@unique([name, teamId]) @@index([userId]) @@index([teamId]) @@map("billing_provider") diff --git a/src/app/(main)/SideNav.tsx b/src/app/(main)/SideNav.tsx index 2f2c27ec4..1261f28c1 100644 --- a/src/app/(main)/SideNav.tsx +++ b/src/app/(main)/SideNav.tsx @@ -10,6 +10,7 @@ import { LinkIcon, PanelLeft, PanelsLeftBottom, + BadgeDollarSign } from '@/components/icons'; import { UserButton } from '@/components/input/UserButton'; import { Logo } from '@/components/svg'; @@ -34,13 +35,13 @@ export function SideNav(props: any) { const links = [ ...(!teamId ? [ - { - id: 'dashboard', - label: t(labels.dashboard), - path: '/dashboard', - icon: , - }, - ] + { + id: 'dashboard', + label: t(labels.dashboard), + path: '/dashboard', + icon: , + }, + ] : []), { id: 'boards', @@ -66,6 +67,12 @@ export function SideNav(props: any) { path: '/pixels', icon: , }, + { + id: 'billing', + label: t(labels.billing), + path: '/billing', + icon: , + }, ]; return ( diff --git a/src/app/(main)/billings/BillingsAddButton.tsx b/src/app/(main)/billings/BillingsAddButton.tsx new file mode 100644 index 000000000..ebff50244 --- /dev/null +++ b/src/app/(main)/billings/BillingsAddButton.tsx @@ -0,0 +1,14 @@ +import { useMessages } from '@/components/hooks'; +import { Plus } from '@/components/icons'; +import { DialogButton } from '@/components/input/DialogButton'; +import { BillingsEditForm } from './BillingsEditForm'; + +export function BillingsAddButton() { + const { t, labels } = useMessages(); + + return ( + } label={t(labels.addBillingProvider)} variant="primary" width="500px"> + {({ close }) => } + + ); +} diff --git a/src/app/(main)/billings/BillingsDataTable.tsx b/src/app/(main)/billings/BillingsDataTable.tsx new file mode 100644 index 000000000..47fe9c0da --- /dev/null +++ b/src/app/(main)/billings/BillingsDataTable.tsx @@ -0,0 +1,13 @@ +import { DataGrid } from '@/components/common/DataGrid'; +import { useBillingProvidersQuery } from '@/components/hooks'; +import { BillingsTable } from './BillingsTable'; + +export function BillingsDataTable() { + const query = useBillingProvidersQuery(); + + return ( + + {({ data }) => } + + ); +} diff --git a/src/app/(main)/billings/BillingsDeleteButton.tsx b/src/app/(main)/billings/BillingsDeleteButton.tsx new file mode 100644 index 000000000..82362f071 --- /dev/null +++ b/src/app/(main)/billings/BillingsDeleteButton.tsx @@ -0,0 +1,48 @@ +import { ConfirmationForm } from '@/components/common/ConfirmationForm'; +import { useDeleteQuery, useMessages } from '@/components/hooks'; +import { Trash } from '@/components/icons'; +import { DialogButton } from '@/components/input/DialogButton'; + +export function BillingsDeleteButton({ + providerId, + providerName, + onSave, +}: { + providerId: string; + providerName: string; + onSave?: () => void; +}) { + const { t, labels, messages, getErrorMessage } = useMessages(); + const { mutateAsync, isPending, error, touch } = useDeleteQuery( + `/billing/providers/${providerId}`, + ); + + const handleConfirm = async (close: () => void) => { + await mutateAsync(null, { + onSuccess: () => { + touch('billingProviders'); + onSave?.(); + close(); + }, + }); + }; + + return ( + } title={t(labels.confirm)} variant="quiet" width="400px"> + {({ close }) => ( + {chunks}, + })} + isLoading={isPending} + error={getErrorMessage(error)} + onConfirm={handleConfirm.bind(null, close)} + onClose={close} + buttonLabel={t(labels.delete)} + buttonVariant="danger" + /> + )} + + ); +} diff --git a/src/app/(main)/billings/BillingsEditForm.tsx b/src/app/(main)/billings/BillingsEditForm.tsx new file mode 100644 index 000000000..8e448beab --- /dev/null +++ b/src/app/(main)/billings/BillingsEditForm.tsx @@ -0,0 +1,87 @@ +import { Box, Button, Form, FormField, FormSubmitButton, ListItem, Row, Select, TextField } from '@umami/react-zen'; +import { useLoginQuery, useMessages, useUpdateQuery } from '@/components/hooks'; +import { BILLING_PROVIDER_TYPES } from '@/lib/constants'; + +interface BillingsFormValues { + name: string; + provider: string; + apiKey: string; +} + +export function BillingsEditForm({ + providerId, + providerName, + displayName, + onSave, + onClose, +}: { + providerId?: string; + providerName?: string; + displayName?: string; + onSave?: () => void; + onClose?: () => void; +}) { + const { t, labels, messages, getErrorMessage } = useMessages(); + const { user } = useLoginQuery(); + const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery( + providerId ? `/billing/providers/${providerId}` : '/billing/providers', + providerId ? undefined : { userId: user?.id }, + ); + + const handleSubmit = async (data: BillingsFormValues) => { + await mutateAsync({ name: data.name, provider: data.provider, apiKey: data.apiKey || undefined }); + toast(t(messages.saved)); + touch('billingProviders'); + onSave?.(); + onClose?.(); + }; + + return ( +
+ {({ watch, setValue }) => { + const provider = watch('provider') as string; + + return ( + <> + + + + + + + + + + + + + {onClose && ( + + )} + {t(labels.save)} + + + ); + }} +
+ ); +} diff --git a/src/app/(main)/billings/BillingsPage.tsx b/src/app/(main)/billings/BillingsPage.tsx new file mode 100644 index 000000000..7963045fe --- /dev/null +++ b/src/app/(main)/billings/BillingsPage.tsx @@ -0,0 +1,25 @@ +'use client'; +import { Column } from '@umami/react-zen'; +import { PageBody } from '@/components/common/PageBody'; +import { PageHeader } from '@/components/common/PageHeader'; +import { Panel } from '@/components/common/Panel'; +import { useMessages } from '@/components/hooks'; +import { BillingsAddButton } from './BillingsAddButton'; +import { BillingsDataTable } from './BillingsDataTable'; + +export function BillingsPage() { + const { t, labels } = useMessages(); + + return ( + + + + + + + + + + + ); +} diff --git a/src/app/(main)/billings/BillingsTable.tsx b/src/app/(main)/billings/BillingsTable.tsx new file mode 100644 index 000000000..160666bc2 --- /dev/null +++ b/src/app/(main)/billings/BillingsTable.tsx @@ -0,0 +1,34 @@ +import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen'; +import { DateDistance } from '@/components/common/DateDistance'; +import { useMessages } from '@/components/hooks'; +import { Pencil } from '@/components/icons'; +import { DialogButton } from '@/components/input/DialogButton'; +import { BillingsDeleteButton } from './BillingsDeleteButton'; +import { BillingsEditForm } from './BillingsEditForm'; + +export function BillingsTable(props: DataTableProps) { + const { t, labels } = useMessages(); + + return ( + + + + + + {(row: any) => (row.lastRunAt ? : '—')} + + + {({ id, name, provider }: any) => ( + + } title={t(labels.edit)} variant="quiet" width="500px"> + {({ close }) => ( + + )} + + + + )} + + + ); +} diff --git a/src/app/(main)/billings/[billingId]/BillingsPage.tsx b/src/app/(main)/billings/[billingId]/BillingsPage.tsx new file mode 100644 index 000000000..9ff3f0b37 --- /dev/null +++ b/src/app/(main)/billings/[billingId]/BillingsPage.tsx @@ -0,0 +1,5 @@ +'use client'; + +export function BillingsPage({ billingId }: { billingId: string }) { + return
{billingId}
; +} diff --git a/src/app/(main)/billings/[billingId]/page.tsx b/src/app/(main)/billings/[billingId]/page.tsx new file mode 100644 index 000000000..fb1518200 --- /dev/null +++ b/src/app/(main)/billings/[billingId]/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from 'next'; +import { BillingsPage } from './BillingsPage'; + +export default async function ({ params }: { params: Promise<{ billingId: string }> }) { + const { billingId } = await params; + + return ; +} + +export const metadata: Metadata = { + title: 'Billing', +}; diff --git a/src/app/(main)/billings/page.tsx b/src/app/(main)/billings/page.tsx new file mode 100644 index 000000000..2a46c83a9 --- /dev/null +++ b/src/app/(main)/billings/page.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from 'next'; +import { BillingsPage } from './BillingsPage'; + +export default function () { + return ; +} + +export const metadata: Metadata = { + title: 'Billing', +}; diff --git a/src/app/api/billing/sync/status/route.ts b/src/app/api/billing/sync/status/route.ts deleted file mode 100644 index 17591682d..000000000 --- a/src/app/api/billing/sync/status/route.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getBillingProviderSyncStatuses } from '@/queries/prisma'; - -export async function GET() { - const keys = await getBillingProviderSyncStatuses(); - return NextResponse.json(keys); -} diff --git a/src/app/api/billings/[billingId]/route.ts b/src/app/api/billings/[billingId]/route.ts new file mode 100644 index 000000000..13c4877f5 --- /dev/null +++ b/src/app/api/billings/[billingId]/route.ts @@ -0,0 +1,131 @@ +import { z } from 'zod'; +import { decrypt, encrypt, secret } from '@/lib/crypto'; +import { parseRequest } from '@/lib/request'; +import { json, notFound, ok, unauthorized } from '@/lib/response'; +import { deleteBillingById, getBillingById, maskKey, updateBilling } from '@/queries/prisma'; + +function canAccess( + user: { id: string; isAdmin: boolean }, + row: { userId: string | null; teamId: string | null }, +) { + return user.isAdmin || row.userId === user.id; +} + +export async function GET( + request: Request, + { params }: { params: Promise<{ providerId: string }> }, +) { + const { auth, error } = await parseRequest(request, z.object({})); + + if (error) { + return error(); + } + + if (!auth.user) { + return unauthorized(); + } + + const { providerId } = await params; + const row = await getBillingById(providerId); + + if (!row) { + return notFound(); + } + + if (!canAccess(auth.user, row)) { + return unauthorized(); + } + + const rawKey = decrypt(row.apiKey, secret()); + + return json({ + id: row.id, + provider: row.provider, + userId: row.userId, + teamId: row.teamId, + keyPreview: maskKey(rawKey), + syncStatus: row.syncStatus, + syncCursor: row.syncCursor, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ providerId: string }> }, +) { + const schema = z.object({ + name: z.string().max(100).optional(), + provider: z.string().max(50).optional(), + apiKey: z.string().min(1).optional(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!auth.user) { + return unauthorized(); + } + + const { providerId } = await params; + const existing = await getBillingById(providerId); + + if (!existing) { + return notFound(); + } + + if (!canAccess(auth.user, existing)) { + return unauthorized(); + } + + const update: Record = {}; + if (body.name) update.name = body.name; + if (body.provider) update.provider = body.provider; + if (body.apiKey) update.apiKey = encrypt(body.apiKey, secret()); + const row = await updateBilling(providerId, update); + const rawKey = decrypt(row.apiKey, secret()); + + return json({ + id: row.id, + provider: row.provider, + userId: row.userId, + teamId: row.teamId, + keyPreview: maskKey(rawKey), + syncStatus: row.syncStatus, + updatedAt: row.updatedAt, + }); +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ providerId: string }> }, +) { + const { auth, error } = await parseRequest(request, z.object({})); + + if (error) { + return error(); + } + + if (!auth.user) { + return unauthorized(); + } + + const { providerId } = await params; + const row = await getBillingById(providerId); + + if (!row) { + return notFound(); + } + + if (!canAccess(auth.user, row)) { + return unauthorized(); + } + + await deleteBillingById(providerId); + + return ok(); +} diff --git a/src/app/api/billing/sync/invoices/route.ts b/src/app/api/billings/[billingId]/sync/route.ts similarity index 62% rename from src/app/api/billing/sync/invoices/route.ts rename to src/app/api/billings/[billingId]/sync/route.ts index 1827f480a..c80d24186 100644 --- a/src/app/api/billing/sync/invoices/route.ts +++ b/src/app/api/billings/[billingId]/sync/route.ts @@ -1,24 +1,20 @@ -import { NextResponse } from 'next/server'; -import Stripe from 'stripe'; import { decrypt, secret } from '@/lib/crypto'; import { fetchInvoicePageBackfill, fetchInvoicePageIncremental } from '@/lib/stripe'; -import { getBillingProviderById, updateBillingProviderSync } from '@/queries/prisma'; -import { STALE_RUNNING_MS, upsertInvoiceBatch } from '@/queries/prisma/billing'; +import { getBillingById, updateBillingSync } from '@/queries/prisma'; +import { STALE_RUNNING_MS, upsertInvoiceBatch } from '@/queries/prisma/billingInvoice'; +import { NextResponse } from 'next/server'; +import Stripe from 'stripe'; -export async function GET(request: Request) { - const { searchParams } = new URL(request.url); +export async function POST( + request: Request, + { params }: { params: Promise<{ billingId: string }> }, +) { + const { billingId } = await params; + const { mode: rawMode } = await request.json(); + const mode = rawMode === 'full' ? 'full' : 'batch'; - // keyId identifies which BillingProvider (i.e. which owner) to sync. - // mode=full — fetch all pages in one request (self-hosted, no timeout concern) - // mode=batch — fetch one page and return; caller re-invokes (Vercel / default) - const keyId = searchParams.get('keyId'); - const mode = searchParams.get('mode') === 'full' ? 'full' : 'batch'; + const keyRow = await getBillingById(billingId); - if (!keyId) { - return NextResponse.json({ error: 'keyId is required' }, { status: 400 }); - } - - const keyRow = await getBillingProviderById(keyId); if (!keyRow) { return NextResponse.json({ error: 'API key not found' }, { status: 404 }); } @@ -31,7 +27,7 @@ export async function GET(request: Request) { return NextResponse.json({ skipped: true, reason: 'already running' }, { status: 409 }); } - await updateBillingProviderSync(keyId, { syncStatus: 'running' }); + await updateBillingSync(billingId, { syncStatus: 'running' }); // Decrypt the API key and create a scoped Stripe client const rawApiKey = decrypt(keyRow.apiKey, secret()); @@ -42,7 +38,6 @@ export async function GET(request: Request) { let processed = 0; let lastCursor: string | null = keyRow.syncCursor ?? null; - // If backfilling, always fetch in batch mode to avoid partial syncs. Otherwise, follow the requested mode. if (mode === 'full') { let hasMore = true; while (hasMore) { @@ -50,7 +45,7 @@ export async function GET(request: Request) { ? await fetchInvoicePageBackfill(stripe, lastCursor) : await fetchInvoicePageIncremental(stripe); - await upsertInvoiceBatch(page.data, keyId); + await upsertInvoiceBatch(page.data, billingId); processed += page.data.length; hasMore = page.has_more; @@ -59,7 +54,11 @@ export async function GET(request: Request) { if (!isBackfilling) break; } - await updateBillingProviderSync(keyId, { syncStatus: 'idle', syncCursor: null }); + await updateBillingSync(billingId, { + syncStatus: 'idle', + syncCursor: null, + lastRunAt: new Date(), + }); return NextResponse.json({ processed, hasMore: false, cursor: null, status: 'idle' }); } else { @@ -67,13 +66,17 @@ export async function GET(request: Request) { ? await fetchInvoicePageBackfill(stripe, lastCursor) : await fetchInvoicePageIncremental(stripe); - await upsertInvoiceBatch(page.data, keyId); + await upsertInvoiceBatch(page.data, billingId); const nextCursor = page.has_more && page.data.length > 0 ? page.data[page.data.length - 1].id : null; const nextStatus = page.has_more ? 'backfilling' : 'idle'; - await updateBillingProviderSync(keyId, { syncStatus: nextStatus, syncCursor: nextCursor }); + await updateBillingSync(billingId, { + syncStatus: nextStatus, + syncCursor: nextCursor, + ...(nextStatus === 'idle' && { lastRunAt: new Date() }), + }); return NextResponse.json({ processed: page.data.length, @@ -83,7 +86,7 @@ export async function GET(request: Request) { }); } } catch (err) { - await updateBillingProviderSync(keyId, { syncStatus: 'idle' }); + await updateBillingSync(billingId, { syncStatus: 'idle' }); throw err; } } diff --git a/src/app/api/billings/route.ts b/src/app/api/billings/route.ts new file mode 100644 index 000000000..c6ee9d28b --- /dev/null +++ b/src/app/api/billings/route.ts @@ -0,0 +1,108 @@ +import { z } from 'zod'; +import { decrypt, encrypt, secret } from '@/lib/crypto'; +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { pagingParams, searchParams } from '@/lib/schema'; +import { + getBillingsPage, + maskKey, + upsertBillingForTeam, + upsertBillingForUser, +} from '@/queries/prisma'; + +export async function GET(request: Request) { + const schema = z.object({ + userId: z.string().uuid().optional(), + teamId: z.string().uuid().optional(), + ...pagingParams, + ...searchParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!auth.user) { + return unauthorized(); + } + + const { userId, teamId, ...filters } = query; + + let where: Record = {}; + + if (teamId) { + if (!auth.user.isAdmin) { + return unauthorized(); + } + where = { teamId }; + } else { + const ownerId = userId ?? auth.user.id; + if (!auth.user.isAdmin && ownerId !== auth.user.id) { + return unauthorized(); + } + where = auth.user.isAdmin && !userId ? {} : { userId: ownerId }; + } + + const result = await getBillingsPage(where, filters); + + return json(result); +} + +export async function POST(request: Request) { + const schema = z.object({ + name: z.string().max(100), + provider: z.string().max(50), + apiKey: z.string().min(1), + userId: z.string().uuid().optional(), + teamId: z.string().uuid().optional(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!auth.user) { + return unauthorized(); + } + + const { name, provider, apiKey, userId, teamId } = body; + + if (!userId && !teamId) { + return new Response(JSON.stringify({ error: 'userId or teamId is required' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + if (!auth.user.isAdmin) { + if (userId && userId !== auth.user.id) { + return unauthorized(); + } + if (teamId) { + return unauthorized(); + } + } + + const encryptedKey = encrypt(apiKey, secret()); + + const row = userId + ? await upsertBillingForUser(userId, provider, name, encryptedKey) + : await upsertBillingForTeam(teamId, provider, name, encryptedKey); + + const rawKey = decrypt(row.apiKey, secret()); + + return json({ + id: row.id, + provider: row.provider, + userId: row.userId, + teamId: row.teamId, + keyPreview: maskKey(rawKey), + syncStatus: row.syncStatus, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }); +} diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index a5dfd589f..1cffec5af 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -11,6 +11,7 @@ export * from './context/useWebsite'; // Query hooks export * from './queries/useActiveUsersQuery'; +export * from './queries/useBillingProvidersQuery'; export * from './queries/useBoardQuery'; export * from './queries/useBoardSharesQuery'; export * from './queries/useBoardsQuery'; diff --git a/src/components/hooks/queries/useBillingProvidersQuery.ts b/src/components/hooks/queries/useBillingProvidersQuery.ts new file mode 100644 index 000000000..dbee7ec51 --- /dev/null +++ b/src/components/hooks/queries/useBillingProvidersQuery.ts @@ -0,0 +1,18 @@ +import type { ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useModified } from '../useModified'; +import { usePagedQuery } from '../usePagedQuery'; + +export function useBillingProvidersQuery( + params?: Record, + options?: ReactQueryOptions, +) { + const { get } = useApi(); + const { modified } = useModified('billingProviders'); + + return usePagedQuery({ + queryKey: ['billingProviders', { modified, ...params }], + queryFn: pageParams => get('/billing/providers', { ...pageParams, ...params }), + ...options, + }); +} diff --git a/src/components/messages.ts b/src/components/messages.ts index 72ce903cf..416f55671 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -314,6 +314,14 @@ export const labels: Record = { paidVideo: 'label.paid-video', grouped: 'label.grouped', other: 'label.other', + billings: 'label.billings', + billing: 'label.billing', + addBillingProvider: 'label.add-billing-provider', + provider: 'label.provider', + apiKey: 'label.api-key', + syncStatus: 'label.sync-status', + syncCursor: 'label.sync-cursor', + lastRun: 'label.last-run', boards: 'label.boards', apply: 'label.apply', match: 'label.match', diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f7c182b15..23d47def8 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -216,6 +216,10 @@ export const ROLE_PERMISSIONS = { [ROLES.teamViewOnly]: [], } as const; +export const BILLING_PROVIDER_TYPES = { + stripe: 'stripe', +}; + export const THEME_COLORS = { light: { primary: '#2680eb', diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 1da0f81b7..f3e3f0758 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -475,9 +475,9 @@ function getSearchParameters(query: string, filters: Record[]) { [key]: typeof value === 'string' ? { - [value]: query, - mode: 'insensitive', - } + [value]: query, + mode: 'insensitive', + } : parseFilter(value), }; }; @@ -518,7 +518,10 @@ function getClient() { const schema = getSchema(); - const baseAdapter = new PrismaPg({ connectionString: url }, { schema }); + console.log(schema, replicaUrl, url, 'replica adapter'); + + + const baseAdapter = new PrismaPg({ connectionString: url }, schema ? { schema } : {}); const baseClient = new PrismaClient({ adapter: baseAdapter, @@ -536,7 +539,7 @@ function getClient() { return baseClient; } - const replicaAdapter = new PrismaPg({ connectionString: replicaUrl }, { schema }); + const replicaAdapter = new PrismaPg({ connectionString: replicaUrl }, schema ? { schema } : {}); const replicaClient = new PrismaClient({ adapter: replicaAdapter, diff --git a/src/queries/prisma/billing.ts b/src/queries/prisma/billing.ts index a6b5b0cea..f69779b01 100644 --- a/src/queries/prisma/billing.ts +++ b/src/queries/prisma/billing.ts @@ -1,62 +1,121 @@ -import type Stripe from 'stripe'; +import { uuid } from '@/lib/crypto'; import prisma from '@/lib/prisma'; -export const STALE_RUNNING_MS = 2 * 60 * 1000; +const db = () => (prisma.client as any).billing; -// Upsert a batch of invoice line items into billing_invoice. -export async function upsertInvoiceBatch( - invoices: Stripe.Invoice[], - providerId: string, -): Promise { - const db = prisma.client as any; - - for (const invoice of invoices) { - const customerId = invoice.customer as string; - const invoiceStatus = invoice.status ?? 'unknown'; - const invoicePeriodEnd = new Date(invoice.period_end * 1000); - - for (const line of (invoice.lines as any).data) { - const usageType = line.pricing?.price_details?.price?.recurring?.usage_type; - const lineType: string | null = - usageType === 'licensed' - ? 'licensed' - : usageType === 'metered' - ? 'metered' - : usageType == null && line.amount != null - ? 'one_time' - : null; - - if (!lineType) continue; - - const periodMonths = Math.max( - 1, - Math.round((line.period.end - line.period.start) / (86400 * 30.44)), - ); - - await db.billingInvoice.upsert({ - where: { id: line.id }, - create: { - id: line.id, - providerId, - invoiceId: invoice.id, - customerId, - invoiceStatus, - invoicePeriodEnd, - usageType: lineType, - amountCents: line.amount, - periodStart: new Date(line.period.start * 1000), - periodEnd: new Date(line.period.end * 1000), - periodMonths, - mrrCents: Math.round(line.amount / periodMonths), - }, - update: { - invoiceStatus, - invoicePeriodEnd, - amountCents: line.amount, - usageType: lineType, - mrrCents: Math.round(line.amount / periodMonths), - }, - }); - } - } +function maskKey(apiKey: string): string { + // Show last 4 chars: sk_live_****abcd + return apiKey.length > 4 ? `****${apiKey.slice(-4)}` : '****'; } + +export async function getBillingByUser(userId: string, provider: string) { + return db().findUnique({ + where: { provider_userId: { provider, userId } }, + }); +} + +export async function getBillingByTeam(teamId: string, provider: string) { + return db().findUnique({ + where: { provider_teamId: { provider, teamId } }, + }); +} + +export async function upsertBillingForUser( + userId: string, + provider: string, + name: string, + encryptedKey: string, +) { + return db().upsert({ + where: { provider_userId: { provider, userId } }, + create: { id: uuid(), name, provider, userId, apiKey: encryptedKey, updatedAt: new Date() }, + update: { name, apiKey: encryptedKey, updatedAt: new Date() }, + }); +} + +export async function upsertBillingForTeam( + teamId: string, + provider: string, + name: string, + encryptedKey: string, +) { + return db().upsert({ + where: { provider_teamId: { provider, teamId } }, + create: { id: uuid(), name, provider, teamId, apiKey: encryptedKey, updatedAt: new Date() }, + update: { name, apiKey: encryptedKey, updatedAt: new Date() }, + }); +} + +export async function deleteBillingByUser(userId: string, provider: string) { + return db().delete({ + where: { provider_userId: { provider, userId } }, + }); +} + +export async function deleteBillingByTeam(teamId: string, provider: string) { + return db().delete({ + where: { provider_teamId: { provider, teamId } }, + }); +} + +export async function getBillingById(id: string) { + return db().findUnique({ where: { id } }); +} + +export async function updateBillingSync( + id: string, + data: { syncStatus: string; syncCursor?: string | null; lastRunAt?: Date | null }, +) { + return db().update({ where: { id }, data }); +} + +export async function getBillingSyncStatuses() { + return db().findMany({ + select: { + id: true, + provider: true, + userId: true, + teamId: true, + syncStatus: true, + syncCursor: true, + updatedAt: true, + }, + }); +} + +const providerSelect = { + id: true, + name: true, + provider: true, + userId: true, + teamId: true, + syncStatus: true, + syncCursor: true, + lastRunAt: true, + createdAt: true, + updatedAt: true, +}; + +export async function getBillingsPage( + where: Record = {}, + filters?: Record, +) { + return prisma.pagedQuery( + 'billing', + { where, select: providerSelect }, + { orderBy: 'createdAt', sortDescending: true, ...filters }, + ); +} + +export async function updateBilling( + id: string, + data: { name?: string; apiKey?: string; provider?: string }, +) { + return db().update({ where: { id }, data: { ...data, updatedAt: new Date() } }); +} + +export async function deleteBillingById(id: string) { + return db().delete({ where: { id } }); +} + +export { maskKey }; diff --git a/src/queries/prisma/billingInvoice.ts b/src/queries/prisma/billingInvoice.ts new file mode 100644 index 000000000..a6b5b0cea --- /dev/null +++ b/src/queries/prisma/billingInvoice.ts @@ -0,0 +1,62 @@ +import type Stripe from 'stripe'; +import prisma from '@/lib/prisma'; + +export const STALE_RUNNING_MS = 2 * 60 * 1000; + +// Upsert a batch of invoice line items into billing_invoice. +export async function upsertInvoiceBatch( + invoices: Stripe.Invoice[], + providerId: string, +): Promise { + const db = prisma.client as any; + + for (const invoice of invoices) { + const customerId = invoice.customer as string; + const invoiceStatus = invoice.status ?? 'unknown'; + const invoicePeriodEnd = new Date(invoice.period_end * 1000); + + for (const line of (invoice.lines as any).data) { + const usageType = line.pricing?.price_details?.price?.recurring?.usage_type; + const lineType: string | null = + usageType === 'licensed' + ? 'licensed' + : usageType === 'metered' + ? 'metered' + : usageType == null && line.amount != null + ? 'one_time' + : null; + + if (!lineType) continue; + + const periodMonths = Math.max( + 1, + Math.round((line.period.end - line.period.start) / (86400 * 30.44)), + ); + + await db.billingInvoice.upsert({ + where: { id: line.id }, + create: { + id: line.id, + providerId, + invoiceId: invoice.id, + customerId, + invoiceStatus, + invoicePeriodEnd, + usageType: lineType, + amountCents: line.amount, + periodStart: new Date(line.period.start * 1000), + periodEnd: new Date(line.period.end * 1000), + periodMonths, + mrrCents: Math.round(line.amount / periodMonths), + }, + update: { + invoiceStatus, + invoicePeriodEnd, + amountCents: line.amount, + usageType: lineType, + mrrCents: Math.round(line.amount / periodMonths), + }, + }); + } + } +} diff --git a/src/queries/prisma/billingProvider.ts b/src/queries/prisma/billingProvider.ts deleted file mode 100644 index 7d9ad9ec6..000000000 --- a/src/queries/prisma/billingProvider.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { uuid } from '@/lib/crypto'; -import prisma from '@/lib/prisma'; - -const db = () => (prisma.client as any).billingProvider; - -function maskKey(apiKey: string): string { - // Show last 4 chars: sk_live_****abcd - return apiKey.length > 4 ? `****${apiKey.slice(-4)}` : '****'; -} - -export async function getBillingProviderByUser(userId: string, provider: string) { - return db().findUnique({ - where: { provider_userId: { provider, userId } }, - }); -} - -export async function getBillingProviderByTeam(teamId: string, provider: string) { - return db().findUnique({ - where: { provider_teamId: { provider, teamId } }, - }); -} - -export async function upsertBillingProviderForUser( - userId: string, - provider: string, - encryptedKey: string, -) { - return db().upsert({ - where: { provider_userId: { provider, userId } }, - create: { id: uuid(), provider, userId, apiKey: encryptedKey }, - update: { apiKey: encryptedKey }, - }); -} - -export async function upsertBillingProviderForTeam( - teamId: string, - provider: string, - encryptedKey: string, -) { - return db().upsert({ - where: { provider_teamId: { provider, teamId } }, - create: { id: uuid(), provider, teamId, apiKey: encryptedKey }, - update: { apiKey: encryptedKey }, - }); -} - -export async function deleteBillingProviderByUser(userId: string, provider: string) { - return db().delete({ - where: { provider_userId: { provider, userId } }, - }); -} - -export async function deleteBillingProviderByTeam(teamId: string, provider: string) { - return db().delete({ - where: { provider_teamId: { provider, teamId } }, - }); -} - -export async function getBillingProviderById(id: string) { - return db().findUnique({ where: { id } }); -} - -export async function updateBillingProviderSync( - id: string, - data: { syncStatus: string; syncCursor?: string | null }, -) { - return db().update({ where: { id }, data }); -} - -export async function getBillingProviderSyncStatuses() { - return db().findMany({ - select: { - id: true, - provider: true, - userId: true, - teamId: true, - syncStatus: true, - syncCursor: true, - updatedAt: true, - }, - }); -} - -export { maskKey }; diff --git a/src/queries/prisma/index.ts b/src/queries/prisma/index.ts index ecaffdd5a..f8916fa84 100644 --- a/src/queries/prisma/index.ts +++ b/src/queries/prisma/index.ts @@ -1,4 +1,4 @@ -export * from './billingProvider'; +export * from './billing'; export * from './board'; export * from './link'; export * from './pixel';